fix(auth): strip model suffix in GitHub Copilot executor before upstream call

GitHub Copilot API rejects model names with suffixes (e.g. claude-opus-4.6(medium)).
The OAuthModelAlias resolution correctly maps aliases like 'opus(medium)' to
'claude-opus-4.6(medium)' preserving the suffix, but the executor must strip the
suffix before sending to the upstream API since Copilot only accepts bare model names.

Update normalizeModel in github_copilot_executor to strip suffixes using
thinking.ParseSuffix, matching the pattern used by other executors.

Also add test coverage for:
- OAuthModelAliasChannel github-copilot and kiro channel resolution
- Suffix preservation in alias resolution for github-copilot
- normalizeModel suffix stripping in github_copilot_executor
This commit is contained in:
Anilcan Cakir
2026-02-10 23:34:19 +03:00
parent 91841a5519
commit bcd2208b51
3 changed files with 99 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/google/uuid"
copilotauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
@@ -471,9 +472,14 @@ func detectVisionContent(body []byte) bool {
return false
}
// normalizeModel is a no-op as GitHub Copilot accepts model names directly.
// Model mapping should be done at the registry level if needed.
func (e *GitHubCopilotExecutor) normalizeModel(_ string, body []byte) []byte {
// normalizeModel strips the suffix (e.g. "(medium)") from the model name
// before sending to GitHub Copilot, as the upstream API does not accept
// suffixed model identifiers.
func (e *GitHubCopilotExecutor) normalizeModel(model string, body []byte) []byte {
baseModel := thinking.ParseSuffix(model).ModelName
if baseModel != model {
body, _ = sjson.SetBytes(body, "model", baseModel)
}
return body
}

View File

@@ -0,0 +1,54 @@
package executor
import (
"testing"
"github.com/tidwall/gjson"
)
func TestGitHubCopilotNormalizeModel_StripsSuffix(t *testing.T) {
t.Parallel()
tests := []struct {
name string
model string
wantModel string
}{
{
name: "suffix stripped",
model: "claude-opus-4.6(medium)",
wantModel: "claude-opus-4.6",
},
{
name: "no suffix unchanged",
model: "claude-opus-4.6",
wantModel: "claude-opus-4.6",
},
{
name: "different suffix stripped",
model: "gpt-4o(high)",
wantModel: "gpt-4o",
},
{
name: "numeric suffix stripped",
model: "gemini-2.5-pro(8192)",
wantModel: "gemini-2.5-pro",
},
}
e := &GitHubCopilotExecutor{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
body := []byte(`{"model":"` + tt.model + `","messages":[]}`)
got := e.normalizeModel(tt.model, body)
gotModel := gjson.GetBytes(got, "model").String()
if gotModel != tt.wantModel {
t.Fatalf("normalizeModel() model = %q, want %q", gotModel, tt.wantModel)
}
})
}
}