package auth import ( "testing" internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) func TestResolveOAuthUpstreamModel_SuffixPreservation(t *testing.T) { t.Parallel() tests := []struct { name string aliases map[string][]internalconfig.OAuthModelAlias channel string input string want string }{ { name: "numeric suffix preserved", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro(8192)", want: "gemini-2.5-pro-exp-03-25(8192)", }, { name: "level suffix preserved", aliases: map[string][]internalconfig.OAuthModelAlias{ "claude": {{Name: "claude-sonnet-4-5-20250514", Alias: "claude-sonnet-4-5"}}, }, channel: "claude", input: "claude-sonnet-4-5(high)", want: "claude-sonnet-4-5-20250514(high)", }, { name: "no suffix unchanged", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro", want: "gemini-2.5-pro-exp-03-25", }, { name: "config suffix takes priority", aliases: map[string][]internalconfig.OAuthModelAlias{ "claude": {{Name: "claude-sonnet-4-5-20250514(low)", Alias: "claude-sonnet-4-5"}}, }, channel: "claude", input: "claude-sonnet-4-5(high)", want: "claude-sonnet-4-5-20250514(low)", }, { name: "auto suffix preserved", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro(auto)", want: "gemini-2.5-pro-exp-03-25(auto)", }, { name: "none suffix preserved", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro(none)", want: "gemini-2.5-pro-exp-03-25(none)", }, { name: "kimi suffix preserved", aliases: map[string][]internalconfig.OAuthModelAlias{ "kimi": {{Name: "kimi-k2.5", Alias: "k2.5"}}, }, channel: "kimi", input: "k2.5(high)", want: "kimi-k2.5(high)", }, { name: "case insensitive alias lookup with suffix", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "Gemini-2.5-Pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro(high)", want: "gemini-2.5-pro-exp-03-25(high)", }, { name: "no alias returns empty", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "unknown-model(high)", want: "", }, { name: "wrong channel returns empty", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "claude", input: "gemini-2.5-pro(high)", want: "", }, { name: "empty suffix filtered out", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, }, channel: "antigravity", input: "gemini-2.5-pro()", want: "gemini-2.5-pro-exp-03-25", }, { name: "incomplete suffix treated as no suffix", aliases: map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro(high"}}, }, channel: "antigravity", input: "gemini-2.5-pro(high", want: "gemini-2.5-pro-exp-03-25", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(tt.aliases) auth := createAuthForChannel(tt.channel) got := mgr.resolveOAuthUpstreamModel(auth, tt.input) if got != tt.want { t.Errorf("resolveOAuthUpstreamModel(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func createAuthForChannel(channel string) *Auth { switch channel { case "antigravity": return &Auth{Provider: "antigravity", Attributes: map[string]string{"auth_kind": "oauth"}} case "claude": return &Auth{Provider: "claude", Attributes: map[string]string{"auth_kind": "oauth"}} case "vertex": return &Auth{Provider: "vertex", Attributes: map[string]string{"auth_kind": "oauth"}} case "codex": return &Auth{Provider: "codex", Attributes: map[string]string{"auth_kind": "oauth"}} case "aistudio": return &Auth{Provider: "aistudio"} case "kimi": return &Auth{Provider: "kimi"} default: return &Auth{Provider: channel} } } func TestOAuthModelAliasChannel_APIKeyOnlyProviderUnsupported(t *testing.T) { t.Parallel() if got := OAuthModelAliasChannel("gemini", "oauth"); got != "" { t.Fatalf("OAuthModelAliasChannel() = %q, want empty channel for API-key-only provider", got) } } func TestOAuthModelAliasChannel_Kimi(t *testing.T) { t.Parallel() if got := OAuthModelAliasChannel("kimi", "oauth"); got != "kimi" { t.Fatalf("OAuthModelAliasChannel() = %q, want %q", got, "kimi") } } func TestOAuthModelAliasChannel_PluginProvider(t *testing.T) { t.Parallel() if got := OAuthModelAliasChannel(" Sample-Provider ", "oauth"); got != "sample-provider" { t.Fatalf("OAuthModelAliasChannel() = %q, want %q", got, "sample-provider") } if got := OAuthModelAliasChannel("sample-provider", "api_key"); got != "" { t.Fatalf("OAuthModelAliasChannel() = %q, want empty channel for API key", got) } } func TestApplyOAuthModelAlias_SuffixPreservation(t *testing.T) { t.Parallel() aliases := map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{Name: "gemini-2.5-pro-exp-03-25", Alias: "gemini-2.5-pro"}}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(aliases) auth := &Auth{ID: "test-auth-id", Provider: "antigravity"} resolvedModel := mgr.applyOAuthModelAlias(auth, "gemini-2.5-pro(8192)") if resolvedModel != "gemini-2.5-pro-exp-03-25(8192)" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gemini-2.5-pro-exp-03-25(8192)") } } func TestApplyOAuthModelAlias_ForceMappingSameBasePreservesSuffix(t *testing.T) { t.Parallel() aliases := map[string][]internalconfig.OAuthModelAlias{ "antigravity": {{ Name: "gemini-2.5-pro", Alias: "gemini-2.5-pro(8192)", ForceMapping: true, }}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(aliases) auth := &Auth{ID: "test-auth-id", Provider: "antigravity"} resolvedModel := mgr.applyOAuthModelAlias(auth, "gemini-2.5-pro(8192)") if resolvedModel != "gemini-2.5-pro(8192)" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gemini-2.5-pro(8192)") } } func TestApplyOAuthModelAlias_PerAuthForceMappingSameBasePreservesSuffix(t *testing.T) { t.Parallel() mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) auth := &Auth{ ID: "test-auth-id", Provider: "antigravity", Attributes: map[string]string{ "model_aliases": `[{"name":"gemini-2.5-pro","alias":"gemini-2.5-pro(8192)","force-mapping":true}]`, }, } resolvedModel := mgr.applyOAuthModelAlias(auth, "gemini-2.5-pro(8192)") if resolvedModel != "gemini-2.5-pro(8192)" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gemini-2.5-pro(8192)") } } func TestApplyOAuthModelAlias_PerAuthOverridesGlobalAlias(t *testing.T) { t.Parallel() globalAliases := map[string][]internalconfig.OAuthModelAlias{ "codex": {{Name: "gpt-5-global", Alias: "gpt-5.5"}}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(globalAliases) auth := &Auth{ ID: "codex-auth-id", Provider: "codex", Attributes: map[string]string{ "auth_kind": "oauth", "model_aliases": `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"}]`, }, } resolvedModel := mgr.applyOAuthModelAlias(auth, "gpt-5.5(high)") if resolvedModel != "gpt-5.3-codex-spark(high)" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gpt-5.3-codex-spark(high)") } } func TestApplyOAuthModelAlias_PerAuthAliasSkipsAPIKey(t *testing.T) { t.Parallel() mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) auth := &Auth{ ID: "codex-api-key-auth", Provider: "codex", Attributes: map[string]string{ "auth_kind": "api_key", "model_aliases": `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"}]`, }, } resolvedModel := mgr.applyOAuthModelAlias(auth, "gpt-5.5") if resolvedModel != "gpt-5.5" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "gpt-5.5") } } func TestApplyOAuthModelAlias_PluginProvider(t *testing.T) { t.Parallel() aliases := map[string][]internalconfig.OAuthModelAlias{ "sample-provider": {{Name: "sample-model-latest", Alias: "sample-latest"}}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(aliases) auth := &Auth{ID: "sample-provider-auth", Provider: "sample-provider", Attributes: map[string]string{"auth_kind": "oauth"}} resolvedModel := mgr.applyOAuthModelAlias(auth, "sample-latest") if resolvedModel != "sample-model-latest" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "sample-model-latest") } } func TestApplyOAuthModelAlias_PluginProviderSkipsAPIKey(t *testing.T) { t.Parallel() aliases := map[string][]internalconfig.OAuthModelAlias{ "sample-provider": {{Name: "sample-model-latest", Alias: "sample-latest"}}, } mgr := NewManager(nil, nil, nil) mgr.SetConfig(&internalconfig.Config{}) mgr.SetOAuthModelAlias(aliases) auth := &Auth{ID: "sample-provider-auth", Provider: "sample-provider", Attributes: map[string]string{"auth_kind": "api_key"}} resolvedModel := mgr.applyOAuthModelAlias(auth, "sample-latest") if resolvedModel != "sample-latest" { t.Errorf("applyOAuthModelAlias() model = %q, want %q", resolvedModel, "sample-latest") } } func TestApplyOAuthModelAliasWithResult_ForceMappingUsesConfigAliasNotRequestSuffix(t *testing.T) { t.Parallel() mgr := NewManager(nil, nil, nil) mgr.SetOAuthModelAlias(map[string][]internalconfig.OAuthModelAlias{ "codex": {{ Name: "gpt-5.4", Alias: "gpt-5.4-fast", Fork: true, ForceMapping: true, }}, }) auth := &Auth{ID: "t", Provider: "codex"} res := mgr.applyOAuthModelAliasWithResult(auth, "gpt-5.4-fast(high)") if res.UpstreamModel != "gpt-5.4(high)" { t.Fatalf("upstream = %q want gpt-5.4(high)", res.UpstreamModel) } if res.OriginalAlias != "gpt-5.4-fast" { t.Fatalf("OriginalAlias = %q want gpt-5.4-fast", res.OriginalAlias) } } func TestApplyOAuthModelAliasWithResultPrefersExactSuffixedAlias(t *testing.T) { t.Parallel() manager := NewManager(nil, nil, nil) manager.SetOAuthModelAlias(map[string][]internalconfig.OAuthModelAlias{ "codex": { {Name: "base-upstream", Alias: "public", Fork: true}, {Name: "low-upstream", Alias: "public(low)", Fork: true, ForceMapping: true}, }, }) auth := &Auth{ID: "exact-suffix", Provider: "codex"} result := manager.applyOAuthModelAliasWithResult(auth, "public(low)") if result.UpstreamModel != "low-upstream(low)" || !result.ForceMapping { t.Fatalf("exact suffixed alias result = %+v, want low-upstream(low) with force mapping", result) } } func TestApplyOAuthModelAliasWithResult_NoForceMappingPreservesRequestedModelInOriginalAlias(t *testing.T) { t.Parallel() mgr := NewManager(nil, nil, nil) mgr.SetOAuthModelAlias(map[string][]internalconfig.OAuthModelAlias{ "codex": {{ Name: "gpt-5.4", Alias: "gpt-5.4-fast", Fork: true, ForceMapping: false, }}, }) auth := &Auth{ID: "t", Provider: "codex"} res := mgr.applyOAuthModelAliasWithResult(auth, "gpt-5.4-fast(high)") if res.ForceMapping { t.Fatal("expected ForceMapping false") } if res.OriginalAlias != "gpt-5.4-fast(high)" { t.Fatalf("OriginalAlias = %q want requested model when force-mapping off", res.OriginalAlias) } }