From b3da00d2edd7a42547f59be46dda672f69349720 Mon Sep 17 00:00:00 2001 From: kavore <161734431+kavore@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:59:21 +0300 Subject: [PATCH] fix: add default copilot claude model aliases for oauth routing --- internal/config/config.go | 22 +++--- .../config/oauth_model_alias_migration.go | 15 ++++ internal/config/oauth_model_alias_test.go | 76 +++++++++++++++++++ .../service_oauth_model_alias_test.go | 23 ++++++ 4 files changed, 126 insertions(+), 10 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e28483d5..c4a2384e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -759,22 +759,24 @@ func (cfg *Config) SanitizeOAuthModelAlias() { return } - // Inject default Kiro aliases if no user-configured kiro aliases exist + // Inject channel defaults when the channel is absent in user config. + // Presence is checked case-insensitively and includes explicit nil/empty markers. if cfg.OAuthModelAlias == nil { cfg.OAuthModelAlias = make(map[string][]OAuthModelAlias) } - if _, hasKiro := cfg.OAuthModelAlias["kiro"]; !hasKiro { - // Check case-insensitive too - found := false + hasChannel := func(channel string) bool { for k := range cfg.OAuthModelAlias { - if strings.EqualFold(strings.TrimSpace(k), "kiro") { - found = true - break + if strings.EqualFold(strings.TrimSpace(k), channel) { + return true } } - if !found { - cfg.OAuthModelAlias["kiro"] = defaultKiroAliases() - } + return false + } + if !hasChannel("kiro") { + cfg.OAuthModelAlias["kiro"] = defaultKiroAliases() + } + if !hasChannel("github-copilot") { + cfg.OAuthModelAlias["github-copilot"] = defaultGitHubCopilotAliases() } if len(cfg.OAuthModelAlias) == 0 { diff --git a/internal/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go index 639cbccd..b5bf2fb3 100644 --- a/internal/config/oauth_model_alias_migration.go +++ b/internal/config/oauth_model_alias_migration.go @@ -42,6 +42,21 @@ func defaultKiroAliases() []OAuthModelAlias { } } +// defaultGitHubCopilotAliases returns default oauth-model-alias entries that +// expose Claude hyphen-style IDs for GitHub Copilot Claude models. +// This keeps compatibility with clients (e.g. Claude Code) that use +// Anthropic-style model IDs like "claude-opus-4-6". +func defaultGitHubCopilotAliases() []OAuthModelAlias { + return []OAuthModelAlias{ + {Name: "claude-haiku-4.5", Alias: "claude-haiku-4-5", Fork: true}, + {Name: "claude-opus-4.1", Alias: "claude-opus-4-1", Fork: true}, + {Name: "claude-opus-4.5", Alias: "claude-opus-4-5", Fork: true}, + {Name: "claude-opus-4.6", Alias: "claude-opus-4-6", Fork: true}, + {Name: "claude-sonnet-4.5", Alias: "claude-sonnet-4-5", Fork: true}, + {Name: "claude-sonnet-4.6", Alias: "claude-sonnet-4-6", Fork: true}, + } +} + // defaultAntigravityAliases returns the default oauth-model-alias configuration // for the antigravity channel when neither field exists. func defaultAntigravityAliases() []OAuthModelAlias { diff --git a/internal/config/oauth_model_alias_test.go b/internal/config/oauth_model_alias_test.go index 5cf05502..6d914b59 100644 --- a/internal/config/oauth_model_alias_test.go +++ b/internal/config/oauth_model_alias_test.go @@ -107,6 +107,44 @@ func TestSanitizeOAuthModelAlias_InjectsDefaultKiroAliases(t *testing.T) { } } +func TestSanitizeOAuthModelAlias_InjectsDefaultGitHubCopilotAliases(t *testing.T) { + cfg := &Config{ + OAuthModelAlias: map[string][]OAuthModelAlias{ + "codex": { + {Name: "gpt-5", Alias: "g5"}, + }, + }, + } + + cfg.SanitizeOAuthModelAlias() + + copilotAliases := cfg.OAuthModelAlias["github-copilot"] + if len(copilotAliases) == 0 { + t.Fatal("expected default github-copilot aliases to be injected") + } + + aliasSet := make(map[string]bool, len(copilotAliases)) + for _, a := range copilotAliases { + aliasSet[a.Alias] = true + if !a.Fork { + t.Fatalf("expected all default github-copilot aliases to have fork=true, got fork=false for %q", a.Alias) + } + } + expectedAliases := []string{ + "claude-haiku-4-5", + "claude-opus-4-1", + "claude-opus-4-5", + "claude-opus-4-6", + "claude-sonnet-4-5", + "claude-sonnet-4-6", + } + for _, expected := range expectedAliases { + if !aliasSet[expected] { + t.Fatalf("expected default github-copilot alias %q to be present", expected) + } + } +} + func TestSanitizeOAuthModelAlias_DoesNotOverrideUserKiroAliases(t *testing.T) { // When user has configured kiro aliases, defaults should NOT be injected cfg := &Config{ @@ -128,6 +166,26 @@ func TestSanitizeOAuthModelAlias_DoesNotOverrideUserKiroAliases(t *testing.T) { } } +func TestSanitizeOAuthModelAlias_DoesNotOverrideUserGitHubCopilotAliases(t *testing.T) { + cfg := &Config{ + OAuthModelAlias: map[string][]OAuthModelAlias{ + "github-copilot": { + {Name: "claude-opus-4.6", Alias: "my-opus", Fork: true}, + }, + }, + } + + cfg.SanitizeOAuthModelAlias() + + copilotAliases := cfg.OAuthModelAlias["github-copilot"] + if len(copilotAliases) != 1 { + t.Fatalf("expected 1 user-configured github-copilot alias, got %d", len(copilotAliases)) + } + if copilotAliases[0].Alias != "my-opus" { + t.Fatalf("expected user alias to be preserved, got %q", copilotAliases[0].Alias) + } +} + func TestSanitizeOAuthModelAlias_DoesNotReinjectAfterExplicitDeletion(t *testing.T) { // When user explicitly deletes kiro aliases (key exists with nil value), // defaults should NOT be re-injected on subsequent sanitize calls (#222). @@ -154,6 +212,24 @@ func TestSanitizeOAuthModelAlias_DoesNotReinjectAfterExplicitDeletion(t *testing } } +func TestSanitizeOAuthModelAlias_GitHubCopilotDoesNotReinjectAfterExplicitDeletion(t *testing.T) { + cfg := &Config{ + OAuthModelAlias: map[string][]OAuthModelAlias{ + "github-copilot": nil, // explicitly deleted + }, + } + + cfg.SanitizeOAuthModelAlias() + + copilotAliases := cfg.OAuthModelAlias["github-copilot"] + if len(copilotAliases) != 0 { + t.Fatalf("expected github-copilot aliases to remain empty after explicit deletion, got %d aliases", len(copilotAliases)) + } + if _, exists := cfg.OAuthModelAlias["github-copilot"]; !exists { + t.Fatal("expected github-copilot key to be preserved as nil marker after sanitization") + } +} + func TestSanitizeOAuthModelAlias_DoesNotReinjectAfterExplicitDeletionEmpty(t *testing.T) { // Same as above but with empty slice instead of nil (PUT with empty body). cfg := &Config{ diff --git a/sdk/cliproxy/service_oauth_model_alias_test.go b/sdk/cliproxy/service_oauth_model_alias_test.go index 2caf7a17..2f90d1df 100644 --- a/sdk/cliproxy/service_oauth_model_alias_test.go +++ b/sdk/cliproxy/service_oauth_model_alias_test.go @@ -90,3 +90,26 @@ func TestApplyOAuthModelAlias_ForkAddsMultipleAliases(t *testing.T) { t.Fatalf("expected forked model name %q, got %q", "models/g5-2", out[2].Name) } } + +func TestApplyOAuthModelAlias_DefaultGitHubCopilotAliasViaSanitize(t *testing.T) { + cfg := &config.Config{} + cfg.SanitizeOAuthModelAlias() + + models := []*ModelInfo{ + {ID: "claude-opus-4.6", Name: "models/claude-opus-4.6"}, + } + + out := applyOAuthModelAlias(cfg, "github-copilot", "oauth", models) + if len(out) != 2 { + t.Fatalf("expected 2 models (original + default alias), got %d", len(out)) + } + if out[0].ID != "claude-opus-4.6" { + t.Fatalf("expected first model id %q, got %q", "claude-opus-4.6", out[0].ID) + } + if out[1].ID != "claude-opus-4-6" { + t.Fatalf("expected second model id %q, got %q", "claude-opus-4-6", out[1].ID) + } + if out[1].Name != "models/claude-opus-4-6" { + t.Fatalf("expected aliased model name %q, got %q", "models/claude-opus-4-6", out[1].Name) + } +}