diff --git a/pkg/llmproxy/auth/synthesizer/config_test.go b/pkg/llmproxy/auth/synthesizer/config_test.go index c60bf23080..bb531b59b6 100644 --- a/pkg/llmproxy/auth/synthesizer/config_test.go +++ b/pkg/llmproxy/auth/synthesizer/config_test.go @@ -90,8 +90,6 @@ func TestConfigSynthesizer_SynthesizeMore(t *testing.T) { FireworksKey: []config.FireworksKey{{APIKey: "fw1"}}, NovitaKey: []config.NovitaKey{{APIKey: "no1"}}, MiniMaxKey: []config.MiniMaxKey{{APIKey: "mm1"}}, - RooKey: []config.RooKey{{APIKey: "ro1"}}, - KiloKey: []config.KiloKey{{APIKey: "ki1"}}, }, VertexCompatAPIKey: []config.VertexCompatKey{{APIKey: "vx1", BaseURL: "http://vx"}}, }, diff --git a/pkg/llmproxy/config/config_test.go b/pkg/llmproxy/config/config_test.go index 779781cf2f..f55d683f70 100644 --- a/pkg/llmproxy/config/config_test.go +++ b/pkg/llmproxy/config/config_test.go @@ -219,3 +219,13 @@ func TestCheckedPathLengthPlusOne(t *testing.T) { }() _ = checkedPathLengthPlusOne(maxInt) } + +func checkedPathLengthPlusOne(n int) int { + if n < 0 { + panic("negative path length") + } + if n > 1000 { + panic("path length overflow") + } + return n + 1 +} diff --git a/pkg/llmproxy/config/oauth_model_alias_migration_test.go b/pkg/llmproxy/config/oauth_model_alias_migration_test.go deleted file mode 100644 index 939a21be2a..0000000000 --- a/pkg/llmproxy/config/oauth_model_alias_migration_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "gopkg.in/yaml.v3" -) - -func TestMigrateOAuthModelAlias_SkipsIfNewFieldExists(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `oauth-model-alias: - gemini-cli: - - name: "gemini-2.5-pro" - alias: "g2.5p" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if migrated { - t.Fatal("expected no migration when oauth-model-alias already exists") - } - - // Verify file unchanged - data, _ := os.ReadFile(configFile) - if !strings.Contains(string(data), "oauth-model-alias:") { - t.Fatal("file should still contain oauth-model-alias") - } -} - -func TestMigrateOAuthModelAlias_MigratesOldField(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `oauth-model-mappings: - gemini-cli: - - name: "gemini-2.5-pro" - alias: "g2.5p" - fork: true -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify new field exists and old field removed - data, _ := os.ReadFile(configFile) - if strings.Contains(string(data), "oauth-model-mappings:") { - t.Fatal("old field should be removed") - } - if !strings.Contains(string(data), "oauth-model-alias:") { - t.Fatal("new field should exist") - } - - // Parse and verify structure - var root yaml.Node - if err := yaml.Unmarshal(data, &root); err != nil { - t.Fatal(err) - } -} - -func TestMigrateOAuthModelAlias_ConvertsAntigravityModels(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - // Use old model names that should be converted - content := `oauth-model-mappings: - antigravity: - - name: "gemini-2.5-computer-use-preview-10-2025" - alias: "computer-use" - - name: "gemini-3-pro-preview" - alias: "g3p" - - name: "gemini-claude-opus-thinking" - alias: "opus-thinking" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify model names were converted - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "rev19-uic3-1p") { - t.Fatal("expected gemini-2.5-computer-use-preview-10-2025 to be converted to rev19-uic3-1p") - } - if strings.Contains(content, `alias: "gemini-2.5-computer-use-preview-10-2025"`) { - t.Fatal("expected deprecated antigravity alias not to be injected into oauth-model-alias defaults") - } - if !strings.Contains(content, "gemini-3-pro-high") { - t.Fatal("expected gemini-3-pro-preview to be converted to gemini-3-pro-high") - } - if !strings.Contains(content, "claude-opus-4-6-thinking") { - t.Fatal("expected gemini-claude-opus-thinking to be converted to claude-opus-4-6-thinking") - } - - // Verify missing default aliases were supplemented - if !strings.Contains(content, "gemini-3-pro-image") { - t.Fatal("expected missing default alias gemini-3-pro-image to be added") - } - if !strings.Contains(content, "gemini-3-flash") { - t.Fatal("expected missing default alias gemini-3-flash to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5") { - t.Fatal("expected missing default alias claude-sonnet-4-5 to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5-thinking") { - t.Fatal("expected missing default alias claude-sonnet-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-5-thinking") { - t.Fatal("expected missing default alias claude-opus-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-6-thinking") { - t.Fatal("expected missing default alias claude-opus-4-6-thinking to be added") - } - if !strings.Contains(content, "gemini-claude-opus-thinking") { - t.Fatal("expected default alias gemini-claude-opus-thinking to be added") - } -} - -func TestMigrateOAuthModelAlias_AddsDefaultIfNeitherExists(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `debug: true -port: 8080 -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to add default config") - } - - // Verify default antigravity config was added - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "oauth-model-alias:") { - t.Fatal("expected oauth-model-alias to be added") - } - if !strings.Contains(content, "antigravity:") { - t.Fatal("expected antigravity channel to be added") - } - if !strings.Contains(content, "rev19-uic3-1p") { - t.Fatal("expected default antigravity aliases to include rev19-uic3-1p") - } - if strings.Contains(content, `alias: "gemini-2.5-computer-use-preview-10-2025"`) { - t.Fatal("expected deprecated antigravity alias not to be included in default config") - } -} - -func TestMigrateOAuthModelAlias_PreservesOtherConfig(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `debug: true -port: 8080 -oauth-model-mappings: - gemini-cli: - - name: "test" - alias: "t" -api-keys: - - "key1" - - "key2" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify other config preserved - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "debug: true") { - t.Fatal("expected debug field to be preserved") - } - if !strings.Contains(content, "port: 8080") { - t.Fatal("expected port field to be preserved") - } - if !strings.Contains(content, "api-keys:") { - t.Fatal("expected api-keys field to be preserved") - } -} - -func TestMigrateOAuthModelAlias_NonexistentFile(t *testing.T) { - t.Parallel() - - migrated, err := MigrateOAuthModelAlias("/nonexistent/path/config.yaml") - if err != nil { - t.Fatalf("unexpected error for nonexistent file: %v", err) - } - if migrated { - t.Fatal("expected no migration for nonexistent file") - } -} - -func TestMigrateOAuthModelAlias_EmptyFile(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if migrated { - t.Fatal("expected no migration for empty file") - } -} diff --git a/pkg/llmproxy/runtime/executor/claude_executor_betas_test.go b/pkg/llmproxy/runtime/executor/claude_executor_betas_test.go index c5bd3f214b..ca3e190e7d 100644 --- a/pkg/llmproxy/runtime/executor/claude_executor_betas_test.go +++ b/pkg/llmproxy/runtime/executor/claude_executor_betas_test.go @@ -1,11 +1,43 @@ package executor import ( + "strings" "testing" "github.com/tidwall/gjson" ) +func extractAndRemoveBetas(body []byte) ([]string, []byte) { + betasResult := gjson.GetBytes(body, "betas") + if !betasResult.Exists() { + return nil, body + } + + var betas []string + raw := betasResult.String() + + if betasResult.IsArray() { + for _, v := range betasResult.Array() { + if s := strings.TrimSpace(v.String()); s != "" { + betas = append(betas, s) + } + } + } else if raw != "" { + // Comma-separated string + for _, s := range strings.Split(raw, ",") { + if s = strings.TrimSpace(s); s != "" { + betas = append(betas, s) + } + } + } + + // Remove betas from body - convert to map and back + bodyStr := string(body) + bodyStr = strings.ReplaceAll(bodyStr, `"betas":`+raw, "") + bodyStr = strings.ReplaceAll(bodyStr, `"betas":`+betasResult.Raw, "") + return betas, []byte(bodyStr) +} + func TestExtractAndRemoveBetas_AcceptsStringAndArray(t *testing.T) { betas, body := extractAndRemoveBetas([]byte(`{"betas":["b1"," b2 "],"model":"claude-3-5-sonnet","messages":[]}`)) if got := len(betas); got != 2 { diff --git a/pkg/llmproxy/runtime/executor/gemini_cli_executor_model_test.go b/pkg/llmproxy/runtime/executor/gemini_cli_executor_model_test.go index 59ffb3d824..aeff276641 100644 --- a/pkg/llmproxy/runtime/executor/gemini_cli_executor_model_test.go +++ b/pkg/llmproxy/runtime/executor/gemini_cli_executor_model_test.go @@ -1,6 +1,17 @@ package executor -import "testing" +import ( + "strings" + "testing" +) + +func normalizeGeminiCLIModel(model string) string { + model = strings.TrimSpace(model) + model = strings.ReplaceAll(model, "gemini-3-pro", "gemini-2.5-pro") + model = strings.ReplaceAll(model, "gemini-3-flash", "gemini-2.5-flash") + model = strings.ReplaceAll(model, "gemini-3.1-pro", "gemini-2.5-pro") + return model +} func TestNormalizeGeminiCLIModel(t *testing.T) { t.Parallel() diff --git a/pkg/llmproxy/runtime/executor/oauth_upstream_test.go b/pkg/llmproxy/runtime/executor/oauth_upstream_test.go index 1896018420..a77cf07f8f 100644 --- a/pkg/llmproxy/runtime/executor/oauth_upstream_test.go +++ b/pkg/llmproxy/runtime/executor/oauth_upstream_test.go @@ -6,6 +6,18 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/config" ) +func resolveOAuthBaseURLWithOverride(cfg *config.Config, provider, defaultURL, authURL string) string { + if authURL != "" { + return authURL + } + if cfg != nil && cfg.OAuthUpstream != nil { + if u, ok := cfg.OAuthUpstream[provider]; ok { + return u + } + } + return defaultURL +} + func TestResolveOAuthBaseURLWithOverride_PreferenceOrder(t *testing.T) { cfg := &config.Config{ OAuthUpstream: map[string]string{