From e1e9fc43c17f886ab6b06f95d50ec554e33c824b Mon Sep 17 00:00:00 2001 From: Longwu Ou Date: Wed, 18 Mar 2026 12:30:22 -0400 Subject: [PATCH 1/2] fix: normalize model name in TranslateRequest fallback to prevent prefix leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no request translator is registered for a format pair (e.g. openai-response → openai-response), TranslateRequest returned the raw payload unchanged. This caused client-side model prefixes (e.g. "copilot/gpt-5-mini") to leak into upstream requests, resulting in "The requested model is not supported" errors from providers. The fallback path now updates the "model" field in the payload to match the resolved model name before returning. --- sdk/translator/registry.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/translator/registry.go b/sdk/translator/registry.go index ace97137..98909c10 100644 --- a/sdk/translator/registry.go +++ b/sdk/translator/registry.go @@ -3,6 +3,9 @@ package translator import ( "context" "sync" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" ) // Registry manages translation functions across schemas. @@ -39,7 +42,9 @@ func (r *Registry) Register(from, to Format, request RequestTransform, response } // TranslateRequest converts a payload between schemas, returning the original payload -// if no translator is registered. +// if no translator is registered. When falling back to the original payload, the +// "model" field is still updated to match the resolved model name so that +// client-side prefixes (e.g. "copilot/gpt-5-mini") are not leaked upstream. func (r *Registry) TranslateRequest(from, to Format, model string, rawJSON []byte, stream bool) []byte { r.mu.RLock() defer r.mu.RUnlock() @@ -49,6 +54,11 @@ func (r *Registry) TranslateRequest(from, to Format, model string, rawJSON []byt return fn(model, rawJSON, stream) } } + if model != "" && gjson.GetBytes(rawJSON, "model").String() != model { + if updated, err := sjson.SetBytes(rawJSON, "model", model); err == nil { + return updated + } + } return rawJSON } From 1e27990561d4aca4165e9e3d7f03eff59812e0d9 Mon Sep 17 00:00:00 2001 From: Longwu Ou Date: Wed, 18 Mar 2026 12:43:40 -0400 Subject: [PATCH 2/2] address PR review: log sjson error and add unit tests - Log a warning instead of silently ignoring sjson.SetBytes errors in the TranslateRequest fallback path - Add registry_test.go with tests covering the fallback model normalization and verifying registered transforms take precedence --- sdk/translator/registry.go | 5 +- sdk/translator/registry_test.go | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 sdk/translator/registry_test.go diff --git a/sdk/translator/registry.go b/sdk/translator/registry.go index 98909c10..e3d5182d 100644 --- a/sdk/translator/registry.go +++ b/sdk/translator/registry.go @@ -4,6 +4,7 @@ import ( "context" "sync" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -55,7 +56,9 @@ func (r *Registry) TranslateRequest(from, to Format, model string, rawJSON []byt } } if model != "" && gjson.GetBytes(rawJSON, "model").String() != model { - if updated, err := sjson.SetBytes(rawJSON, "model", model); err == nil { + if updated, err := sjson.SetBytes(rawJSON, "model", model); err != nil { + log.Warnf("translator: failed to normalize model in request fallback: %v", err) + } else { return updated } } diff --git a/sdk/translator/registry_test.go b/sdk/translator/registry_test.go new file mode 100644 index 00000000..1cd4fb12 --- /dev/null +++ b/sdk/translator/registry_test.go @@ -0,0 +1,92 @@ +package translator + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestTranslateRequest_FallbackNormalizesModel(t *testing.T) { + r := NewRegistry() + + tests := []struct { + name string + model string + payload string + wantModel string + wantUnchanged bool + }{ + { + name: "prefixed model is rewritten", + model: "gpt-5-mini", + payload: `{"model":"copilot/gpt-5-mini","input":"ping"}`, + wantModel: "gpt-5-mini", + }, + { + name: "matching model is left unchanged", + model: "gpt-5-mini", + payload: `{"model":"gpt-5-mini","input":"ping"}`, + wantModel: "gpt-5-mini", + wantUnchanged: true, + }, + { + name: "empty model leaves payload unchanged", + model: "", + payload: `{"model":"copilot/gpt-5-mini","input":"ping"}`, + wantModel: "copilot/gpt-5-mini", + wantUnchanged: true, + }, + { + name: "deeply prefixed model is rewritten", + model: "gpt-5.3-codex", + payload: `{"model":"team/gpt-5.3-codex","stream":true}`, + wantModel: "gpt-5.3-codex", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := []byte(tt.payload) + got := r.TranslateRequest(Format("a"), Format("b"), tt.model, input, false) + + gotModel := gjson.GetBytes(got, "model").String() + if gotModel != tt.wantModel { + t.Errorf("model = %q, want %q", gotModel, tt.wantModel) + } + + if tt.wantUnchanged && string(got) != tt.payload { + t.Errorf("payload was modified when it should not have been:\ngot: %s\nwant: %s", got, tt.payload) + } + + // Verify other fields are preserved. + for _, key := range []string{"input", "stream"} { + orig := gjson.Get(tt.payload, key) + if !orig.Exists() { + continue + } + after := gjson.GetBytes(got, key) + if orig.Raw != after.Raw { + t.Errorf("field %q changed: got %s, want %s", key, after.Raw, orig.Raw) + } + } + }) + } +} + +func TestTranslateRequest_RegisteredTransformTakesPrecedence(t *testing.T) { + r := NewRegistry() + from := Format("openai-response") + to := Format("openai-response") + + r.Register(from, to, func(model string, rawJSON []byte, stream bool) []byte { + return []byte(`{"model":"from-transform"}`) + }, ResponseTransform{}) + + input := []byte(`{"model":"copilot/gpt-5-mini","input":"ping"}`) + got := r.TranslateRequest(from, to, "gpt-5-mini", input, false) + + gotModel := gjson.GetBytes(got, "model").String() + if gotModel != "from-transform" { + t.Errorf("expected registered transform to take precedence, got model = %q", gotModel) + } +}