package synthesizer import ( "strconv" "strings" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestNewConfigSynthesizer(t *testing.T) { synth := NewConfigSynthesizer() if synth == nil { t.Fatal("expected non-nil synthesizer") } } func TestConfigSynthesizer_Synthesize_NilContext(t *testing.T) { synth := NewConfigSynthesizer() auths, err := synth.Synthesize(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected empty auths, got %d", len(auths)) } } func TestConfigSynthesizer_Synthesize_NilConfig(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: nil, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected empty auths, got %d", len(auths)) } } func TestConfigSynthesizer_GeminiKeys(t *testing.T) { tests := []struct { name string geminiKeys []config.GeminiKey wantLen int validate func(*testing.T, []*coreauth.Auth) }{ { name: "single gemini key", geminiKeys: []config.GeminiKey{ {APIKey: "test-key-123", Prefix: "team-a"}, }, wantLen: 1, validate: func(t *testing.T, auths []*coreauth.Auth) { if auths[0].Provider != "gemini" { t.Errorf("expected provider gemini, got %s", auths[0].Provider) } if auths[0].Prefix != "team-a" { t.Errorf("expected prefix team-a, got %s", auths[0].Prefix) } if auths[0].Label != "gemini-apikey" { t.Errorf("expected label gemini-apikey, got %s", auths[0].Label) } if auths[0].Attributes["api_key"] != "test-key-123" { t.Errorf("expected api_key test-key-123, got %s", auths[0].Attributes["api_key"]) } if auths[0].Metadata != nil { t.Errorf("expected metadata to be nil when disable_cooling not set, got %v", auths[0].Metadata) } if auths[0].Status != coreauth.StatusActive { t.Errorf("expected status active, got %s", auths[0].Status) } }, }, { name: "gemini key disable cooling", geminiKeys: []config.GeminiKey{ {APIKey: "test-key-123", Prefix: "team-a", DisableCooling: boolPointer(true)}, }, wantLen: 1, validate: func(t *testing.T, auths []*coreauth.Auth) { if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) } }, }, { name: "gemini key with base url and proxy", geminiKeys: []config.GeminiKey{ { APIKey: "api-key", BaseURL: "https://custom.api.com", ProxyURL: "http://proxy.local:8080", Prefix: "custom", }, }, wantLen: 1, validate: func(t *testing.T, auths []*coreauth.Auth) { if auths[0].Attributes["base_url"] != "https://custom.api.com" { t.Errorf("expected base_url https://custom.api.com, got %s", auths[0].Attributes["base_url"]) } if auths[0].ProxyURL != "http://proxy.local:8080" { t.Errorf("expected proxy_url http://proxy.local:8080, got %s", auths[0].ProxyURL) } }, }, { name: "gemini key with headers", geminiKeys: []config.GeminiKey{ { APIKey: "api-key", Headers: map[string]string{"X-Custom": "value"}, }, }, wantLen: 1, validate: func(t *testing.T, auths []*coreauth.Auth) { if auths[0].Attributes["header:X-Custom"] != "value" { t.Errorf("expected header:X-Custom=value, got %s", auths[0].Attributes["header:X-Custom"]) } }, }, { name: "empty api key skipped", geminiKeys: []config.GeminiKey{ {APIKey: ""}, {APIKey: " "}, {APIKey: "valid-key"}, }, wantLen: 1, }, { name: "multiple gemini keys", geminiKeys: []config.GeminiKey{ {APIKey: "key-1", Prefix: "a"}, {APIKey: "key-2", Prefix: "b"}, {APIKey: "key-3", Prefix: "c"}, }, wantLen: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: tt.geminiKeys, }, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != tt.wantLen { t.Fatalf("expected %d auths, got %d", tt.wantLen, len(auths)) } if tt.validate != nil && len(auths) > 0 { tt.validate(t, auths) } }) } } func TestConfigSynthesizer_InteractionsKeys(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ InteractionsKey: []config.GeminiKey{{ APIKey: "interactions-key", BaseURL: "https://interactions.example.com", ProxyURL: "http://proxy.local:8080", Prefix: "native", Headers: map[string]string{"X-Custom": "value"}, }}, }, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := synth.Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if len(auths) != 1 { t.Fatalf("auth count = %d, want 1", len(auths)) } auth := auths[0] if auth.Provider != "gemini-interactions" { t.Fatalf("provider = %q, want gemini-interactions", auth.Provider) } if auth.Label != "interactions-apikey" { t.Fatalf("label = %q, want interactions-apikey", auth.Label) } if auth.Prefix != "native" { t.Fatalf("prefix = %q, want native", auth.Prefix) } if auth.ProxyURL != "http://proxy.local:8080" { t.Fatalf("proxy URL = %q, want http://proxy.local:8080", auth.ProxyURL) } if got := auth.Attributes["api_key"]; got != "interactions-key" { t.Fatalf("api_key = %q, want interactions-key", got) } if got := auth.Attributes["base_url"]; got != "https://interactions.example.com" { t.Fatalf("base_url = %q, want https://interactions.example.com", got) } if got := auth.Attributes["header:X-Custom"]; got != "value" { t.Fatalf("header:X-Custom = %q, want value", got) } } func TestConfigSynthesizer_ClaudeKeys(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ ClaudeKey: []config.ClaudeKey{ { APIKey: "sk-ant-api-xxx", Prefix: "main", BaseURL: "https://api.anthropic.com", DisableCooling: boolPointer(true), RebuildMidSystemMessage: true, FingerprintProfile: "claude-code-cli", Models: []config.ClaudeModel{ {Name: "claude-3-opus"}, {Name: "claude-3-sonnet"}, }, }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Provider != "claude" { t.Errorf("expected provider claude, got %s", auths[0].Provider) } if auths[0].Label != "claude-apikey" { t.Errorf("expected label claude-apikey, got %s", auths[0].Label) } if auths[0].Prefix != "main" { t.Errorf("expected prefix main, got %s", auths[0].Prefix) } if auths[0].Attributes["api_key"] != "sk-ant-api-xxx" { t.Errorf("expected api_key sk-ant-api-xxx, got %s", auths[0].Attributes["api_key"]) } if auths[0].Attributes["config_index"] != "0" { t.Errorf("expected config_index 0, got %s", auths[0].Attributes["config_index"]) } if _, ok := auths[0].Attributes["models_hash"]; !ok { t.Error("expected models_hash in attributes") } if got := auths[0].Attributes["rebuild_mid_system_message"]; got != "true" { t.Errorf("expected rebuild_mid_system_message=true, got %s", got) } if got := auths[0].Attributes["fingerprint_profile"]; got != "claude-code-cli" { t.Errorf("expected fingerprint_profile=claude-code-cli, got %s", got) } if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) } } func TestConfigSynthesizer_ClaudeKeys_SkipsEmptyAndHeaders(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ ClaudeKey: []config.ClaudeKey{ {APIKey: ""}, // empty, should be skipped {APIKey: " "}, // whitespace, should be skipped {APIKey: "valid-key", Headers: map[string]string{"X-Custom": "value"}}, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth (empty keys skipped), got %d", len(auths)) } if auths[0].Attributes["header:X-Custom"] != "value" { t.Errorf("expected header:X-Custom=value, got %s", auths[0].Attributes["header:X-Custom"]) } } func TestConfigSynthesizer_CodexKeys(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ CodexKey: []config.CodexKey{ { APIKey: "codex-key-123", Prefix: "dev", BaseURL: "https://api.openai.com", ProxyURL: "http://proxy.local", Websockets: true, AlphaSearch: true, DisableCooling: boolPointer(true), }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Provider != "codex" { t.Errorf("expected provider codex, got %s", auths[0].Provider) } if auths[0].Label != "codex-apikey" { t.Errorf("expected label codex-apikey, got %s", auths[0].Label) } if auths[0].ProxyURL != "http://proxy.local" { t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL) } if auths[0].Attributes["websockets"] != "true" { t.Errorf("expected websockets=true, got %s", auths[0].Attributes["websockets"]) } if auths[0].Attributes[coreauth.AttributeCodexAlphaSearch] != "true" { t.Errorf("expected codex_alpha_search=true, got %s", auths[0].Attributes[coreauth.AttributeCodexAlphaSearch]) } if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { t.Errorf("expected disable_cooling=true, got %v", auths[0].Metadata["disable_cooling"]) } } func TestConfigSynthesizer_XAIKeys(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ XAIKey: []config.XAIKey{{ APIKey: "xai-key-123", Prefix: "grok", BaseURL: "https://api.x.ai/v1", ProxyURL: "http://proxy.local", Websockets: true, AlphaSearch: true, DisableCooling: boolPointer(true), Headers: map[string]string{"X-Custom": "value"}, Models: []config.XAIModel{{Name: "grok-4.5", Alias: "grok-latest"}}, }}, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := synth.Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if len(auths) != 1 { t.Fatalf("auth count = %d, want 1", len(auths)) } auth := auths[0] if auth.Provider != "xai" { t.Fatalf("provider = %q, want xai", auth.Provider) } if auth.Label != "xai-apikey" { t.Fatalf("label = %q, want xai-apikey", auth.Label) } if auth.Attributes["websockets"] != "true" { t.Fatalf("websockets = %q, want true", auth.Attributes["websockets"]) } if _, exists := auth.Attributes[coreauth.AttributeCodexAlphaSearch]; exists { t.Fatal("xAI auth unexpectedly contains codex_alpha_search") } if auth.Attributes["base_url"] != "https://api.x.ai/v1" { t.Fatalf("base_url = %q, want https://api.x.ai/v1", auth.Attributes["base_url"]) } if auth.Attributes["header:X-Custom"] != "value" { t.Fatalf("custom header = %q, want value", auth.Attributes["header:X-Custom"]) } if auth.Attributes["models_hash"] == "" { t.Fatal("models_hash is empty") } if auth.ProxyURL != "http://proxy.local" { t.Fatalf("proxy URL = %q, want http://proxy.local", auth.ProxyURL) } if disabled, ok := auth.Metadata["disable_cooling"].(bool); !ok || !disabled { t.Fatalf("disable_cooling = %#v, want true", auth.Metadata["disable_cooling"]) } } func TestConfigSynthesizer_XAIKeys_AllowsEmptyAPIKeyWithBaseURL(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ XAIKey: []config.CodexKey{ { APIKey: "", BaseURL: "https://custom-xai.example.com", Headers: map[string]string{"Custom-Auth": "secret"}, }, { APIKey: " ", BaseURL: "https://custom-xai-2.example.com", }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 2 { t.Fatalf("expected 2 auths for empty API keys with base URL, got %d", len(auths)) } if auths[0].Attributes["base_url"] != "https://custom-xai.example.com" { t.Fatalf("expected base_url=https://custom-xai.example.com, got %s", auths[0].Attributes["base_url"]) } if auths[0].Attributes["header:Custom-Auth"] != "secret" { t.Fatalf("expected header:Custom-Auth=secret, got %s", auths[0].Attributes["header:Custom-Auth"]) } if auths[0].Attributes["auth_kind"] != "apikey" { t.Fatalf("expected auth_kind=apikey, got %s", auths[0].Attributes["auth_kind"]) } if _, exists := auths[0].Attributes["api_key"]; exists { t.Fatalf("expected no api_key attribute for empty key, got %s", auths[0].Attributes["api_key"]) } } func TestConfigSynthesizer_ClaudeKeys_AllowsEmptyAPIKeyWithBaseURL(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ ClaudeKey: []config.ClaudeKey{ { APIKey: "", BaseURL: "https://custom-claude.example.com", Headers: map[string]string{"Custom-Auth": "secret"}, }, { APIKey: " ", BaseURL: "https://custom-claude-2.example.com", }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 2 { t.Fatalf("expected 2 auths for empty API keys with base URL, got %d", len(auths)) } if auths[0].Attributes["base_url"] != "https://custom-claude.example.com" { t.Fatalf("expected base_url=https://custom-claude.example.com, got %s", auths[0].Attributes["base_url"]) } if auths[0].Attributes["header:Custom-Auth"] != "secret" { t.Fatalf("expected header:Custom-Auth=secret, got %s", auths[0].Attributes["header:Custom-Auth"]) } if auths[0].Attributes["auth_kind"] != "apikey" { t.Fatalf("expected auth_kind=apikey, got %s", auths[0].Attributes["auth_kind"]) } if _, exists := auths[0].Attributes["api_key"]; exists { t.Fatalf("expected no api_key attribute for empty key, got %s", auths[0].Attributes["api_key"]) } } func TestConfigSynthesizer_GeminiKeys_AllowsEmptyAPIKeyWithBaseURL(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: []config.GeminiKey{ { APIKey: "", BaseURL: "https://custom-gemini.example.com", Headers: map[string]string{"Custom-Auth": "secret"}, }, }, InteractionsKey: []config.GeminiKey{ { APIKey: "", BaseURL: "https://custom-interactions.example.com", }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 2 { t.Fatalf("expected 2 auths for empty API keys with base URL, got %d", len(auths)) } if auths[0].Attributes["base_url"] != "https://custom-gemini.example.com" { t.Fatalf("expected base_url=https://custom-gemini.example.com, got %s", auths[0].Attributes["base_url"]) } if auths[0].Attributes["header:Custom-Auth"] != "secret" { t.Fatalf("expected header:Custom-Auth=secret, got %s", auths[0].Attributes["header:Custom-Auth"]) } if auths[0].Attributes["auth_kind"] != "apikey" { t.Fatalf("expected auth_kind=apikey, got %s", auths[0].Attributes["auth_kind"]) } if _, exists := auths[0].Attributes["api_key"]; exists { t.Fatalf("expected no api_key attribute for empty key, got %s", auths[0].Attributes["api_key"]) } if auths[1].Attributes["base_url"] != "https://custom-interactions.example.com" { t.Fatalf("expected base_url=https://custom-interactions.example.com, got %s", auths[1].Attributes["base_url"]) } } func TestConfigSynthesizer_CodexKeys_SkipsEmptyAndHeaders(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ CodexKey: []config.CodexKey{ {APIKey: ""}, // empty key without base URL, should be skipped {APIKey: " "}, // whitespace key without base URL, should be skipped {APIKey: "valid-key", Headers: map[string]string{"Authorization": "Bearer xyz"}}, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth (empty keys skipped), got %d", len(auths)) } if auths[0].Attributes["header:Authorization"] != "Bearer xyz" { t.Errorf("expected header:Authorization=Bearer xyz, got %s", auths[0].Attributes["header:Authorization"]) } if _, exists := auths[0].Attributes[coreauth.AttributeCodexAlphaSearch]; exists { t.Fatal("default alpha-search=false unexpectedly generated codex_alpha_search") } } func TestConfigSynthesizer_CodexKeys_AllowsEmptyAPIKeyWithBaseURL(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ CodexKey: []config.CodexKey{ { APIKey: "", BaseURL: "https://custom-codex.example.com", Headers: map[string]string{"Custom-Auth": "secret"}, }, { APIKey: " ", BaseURL: "https://custom-codex-2.example.com", }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 2 { t.Fatalf("expected 2 auths for empty API keys with base URL, got %d", len(auths)) } if auths[0].Attributes["base_url"] != "https://custom-codex.example.com" { t.Fatalf("expected base_url=https://custom-codex.example.com, got %s", auths[0].Attributes["base_url"]) } if auths[0].Attributes["header:Custom-Auth"] != "secret" { t.Fatalf("expected header:Custom-Auth=secret, got %s", auths[0].Attributes["header:Custom-Auth"]) } if auths[0].Attributes["auth_kind"] != "apikey" { t.Fatalf("expected auth_kind=apikey, got %s", auths[0].Attributes["auth_kind"]) } if _, exists := auths[0].Attributes["api_key"]; exists { t.Fatalf("expected no api_key attribute for empty key, got %s", auths[0].Attributes["api_key"]) } } func TestConfigSynthesizer_OpenAICompat(t *testing.T) { tests := []struct { name string compat []config.OpenAICompatibility wantLen int }{ { name: "with APIKeyEntries", compat: []config.OpenAICompatibility{ { Name: "CustomProvider", BaseURL: "https://custom.api.com", DisableCooling: boolPointer(true), APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "key-1"}, {APIKey: "key-2"}, }, }, }, wantLen: 2, }, { name: "empty APIKeyEntries included (legacy)", compat: []config.OpenAICompatibility{ { Name: "EmptyKeys", BaseURL: "https://empty.api.com", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: ""}, {APIKey: " "}, }, }, }, wantLen: 2, }, { name: "without APIKeyEntries (fallback)", compat: []config.OpenAICompatibility{ { Name: "NoKeyProvider", BaseURL: "https://no-key.api.com", }, }, wantLen: 1, }, { name: "empty name defaults", compat: []config.OpenAICompatibility{ { Name: "", BaseURL: "https://default.api.com", }, }, wantLen: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ OpenAICompatibility: tt.compat, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != tt.wantLen { t.Fatalf("expected %d auths, got %d", tt.wantLen, len(auths)) } if tt.name == "with APIKeyEntries" { for i := range auths { if v, ok := auths[i].Metadata["disable_cooling"].(bool); !ok || !v { t.Fatalf("expected auth[%d].disable_cooling=true, got %v", i, auths[i].Metadata["disable_cooling"]) } } } }) } } func TestConfigSynthesizer_OpenAICompat_UsesNamespacedProviderKey(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{ { Name: "kimi", BaseURL: "https://kimi-compatible.example.com/v1", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "test-key"}, }, }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } auth := auths[0] if auth.Provider != "openai-compatible-kimi" { t.Fatalf("provider = %q, want openai-compatible-kimi", auth.Provider) } if auth.Attributes["provider_key"] != "openai-compatible-kimi" { t.Fatalf("provider_key = %q, want openai-compatible-kimi", auth.Attributes["provider_key"]) } if auth.Attributes["compat_name"] != "kimi" { t.Fatalf("compat_name = %q, want kimi", auth.Attributes["compat_name"]) } if auth.Attributes["config_index"] != "0" { t.Fatalf("config_index = %q, want 0", auth.Attributes["config_index"]) } } func TestConfigSynthesizer_VertexCompat(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ VertexCompatAPIKey: []config.VertexCompatKey{ { APIKey: "vertex-key-123", BaseURL: "https://vertex.googleapis.com", Prefix: "vertex-prod", }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Provider != "vertex" { t.Errorf("expected provider vertex, got %s", auths[0].Provider) } if auths[0].Label != "vertex-apikey" { t.Errorf("expected label vertex-apikey, got %s", auths[0].Label) } if auths[0].Prefix != "vertex-prod" { t.Errorf("expected prefix vertex-prod, got %s", auths[0].Prefix) } } func TestConfigSynthesizer_VertexCompat_SkipsEmptyAndHeaders(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "", BaseURL: "https://vertex.api"}, // empty key creates auth without api_key attr {APIKey: " ", BaseURL: "https://vertex.api"}, // whitespace key creates auth without api_key attr {APIKey: "valid-key", BaseURL: "https://vertex.api", Headers: map[string]string{"X-Vertex": "test"}}, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } // Vertex compat doesn't skip empty keys - it creates auths without api_key attribute if len(auths) != 3 { t.Fatalf("expected 3 auths, got %d", len(auths)) } // First two should not have api_key attribute if _, ok := auths[0].Attributes["api_key"]; ok { t.Error("expected first auth to not have api_key attribute") } if _, ok := auths[1].Attributes["api_key"]; ok { t.Error("expected second auth to not have api_key attribute") } // Third should have headers if auths[2].Attributes["header:X-Vertex"] != "test" { t.Errorf("expected header:X-Vertex=test, got %s", auths[2].Attributes["header:X-Vertex"]) } } func TestConfigSynthesizer_OpenAICompat_WithModelsHash(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{ { Name: "TestProvider", BaseURL: "https://test.api.com", Models: []config.OpenAICompatibilityModel{ {Name: "model-a"}, {Name: "model-b"}, }, APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "key-with-models"}, }, }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if _, ok := auths[0].Attributes["models_hash"]; !ok { t.Error("expected models_hash in attributes") } if auths[0].Attributes["api_key"] != "key-with-models" { t.Errorf("expected api_key key-with-models, got %s", auths[0].Attributes["api_key"]) } } func TestConfigSynthesizer_OpenAICompat_FallbackWithModels(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ OpenAICompatibility: []config.OpenAICompatibility{ { Name: "NoKeyWithModels", BaseURL: "https://nokey.api.com", Models: []config.OpenAICompatibilityModel{ {Name: "model-x"}, }, Headers: map[string]string{"X-API": "header-value"}, // No APIKeyEntries - should use fallback path }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if _, ok := auths[0].Attributes["models_hash"]; !ok { t.Error("expected models_hash in fallback path") } if auths[0].Attributes["header:X-API"] != "header-value" { t.Errorf("expected header:X-API=header-value, got %s", auths[0].Attributes["header:X-API"]) } } func TestConfigSynthesizer_VertexCompat_WithModels(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ VertexCompatAPIKey: []config.VertexCompatKey{ { APIKey: "vertex-key", BaseURL: "https://vertex.api", Models: []config.VertexCompatModel{ {Name: "gemini-pro", Alias: "pro"}, {Name: "gemini-ultra", Alias: "ultra"}, }, }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if _, ok := auths[0].Attributes["models_hash"]; !ok { t.Error("expected models_hash in vertex auth with models") } } func TestConfigSynthesizer_IDStability(t *testing.T) { cfg := &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "stable-key", Prefix: "test"}, }, } // Generate IDs twice with fresh generators synth1 := NewConfigSynthesizer() ctx1 := &SynthesisContext{ Config: cfg, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), } auths1, _ := synth1.Synthesize(ctx1) synth2 := NewConfigSynthesizer() ctx2 := &SynthesisContext{ Config: cfg, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), } auths2, _ := synth2.Synthesize(ctx2) if auths1[0].ID != auths2[0].ID { t.Errorf("same config should produce same ID: got %q and %q", auths1[0].ID, auths2[0].ID) } } func TestConfigSynthesizer_RejectsInvalidWeightsForAllAPIKeyTypes(t *testing.T) { invalidWeight := config.MaxCredentialWeight + 1 tests := []struct { name string cfg *config.Config wantPath string }{ { name: "gemini", cfg: &config.Config{GeminiKey: []config.GeminiKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "gemini-api-key[0].weight", }, { name: "interactions", cfg: &config.Config{InteractionsKey: []config.GeminiKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "interactions-api-key[0].weight", }, { name: "claude", cfg: &config.Config{ClaudeKey: []config.ClaudeKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "claude-api-key[0].weight", }, { name: "codex", cfg: &config.Config{CodexKey: []config.CodexKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "codex-api-key[0].weight", }, { name: "xai", cfg: &config.Config{XAIKey: []config.XAIKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "xai-api-key[0].weight", }, { name: "openai compatibility", cfg: &config.Config{OpenAICompatibility: []config.OpenAICompatibility{{ APIKeyEntries: []config.OpenAICompatibilityAPIKey{{APIKey: "key", Weight: &invalidWeight}}, }}}, wantPath: "openai-compatibility[0].api-key-entries[0].weight", }, { name: "vertex", cfg: &config.Config{VertexCompatAPIKey: []config.VertexCompatKey{{APIKey: "key", Weight: &invalidWeight}}}, wantPath: "vertex-api-key[0].weight", }, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { auths, errSynthesize := NewConfigSynthesizer().Synthesize(&SynthesisContext{ Config: testCase.cfg, Now: time.Now(), IDGenerator: NewStableIDGenerator(), }) if errSynthesize == nil { t.Fatal("Synthesize() accepted an invalid credential weight") } if auths != nil { t.Fatalf("Synthesize() auths = %#v, want nil", auths) } if !strings.Contains(errSynthesize.Error(), "synthesize config API key auths: "+testCase.wantPath) { t.Fatalf("Synthesize() error = %q, want contextual path %q", errSynthesize, testCase.wantPath) } }) } } func TestConfigSynthesizer_OmittedWeightRemainsUnset(t *testing.T) { auths, errSynthesize := NewConfigSynthesizer().Synthesize(&SynthesisContext{ Config: &config.Config{GeminiKey: []config.GeminiKey{{APIKey: "key"}}}, Now: time.Now(), IDGenerator: NewStableIDGenerator(), }) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if len(auths) != 1 { t.Fatalf("auth count = %d, want 1", len(auths)) } if _, exists := auths[0].Attributes[coreauth.AttributeWeight]; exists { t.Fatal("omitted weight was added to synthesized attributes") } } func TestConfigSynthesizer_NormalizesNonPositiveWeightToZero(t *testing.T) { weight := -5 auths, errSynthesize := NewConfigSynthesizer().Synthesize(&SynthesisContext{ Config: &config.Config{GeminiKey: []config.GeminiKey{{APIKey: "key", Weight: &weight}}}, Now: time.Now(), IDGenerator: NewStableIDGenerator(), }) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if len(auths) != 1 { t.Fatalf("auth count = %d, want 1", len(auths)) } if gotWeight := auths[0].Attributes[coreauth.AttributeWeight]; gotWeight != "0" { t.Fatalf("weight = %q, want 0", gotWeight) } } func TestConfigSynthesizer_PropagatesWeightsForAllAPIKeyTypes(t *testing.T) { weight := func(value int) *int { return &value } synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: []config.GeminiKey{{APIKey: "gemini", Weight: weight(1)}}, InteractionsKey: []config.GeminiKey{{APIKey: "interactions", Weight: weight(2)}}, ClaudeKey: []config.ClaudeKey{{APIKey: "claude", Weight: weight(3)}}, CodexKey: []config.CodexKey{{APIKey: "codex", Weight: weight(4)}}, XAIKey: []config.XAIKey{{APIKey: "xai", Weight: weight(5)}}, OpenAICompatibility: []config.OpenAICompatibility{{ Name: "compat", BaseURL: "https://compat.example.com", APIKeyEntries: []config.OpenAICompatibilityAPIKey{{ APIKey: "compat", Weight: weight(6), }}, }}, VertexCompatAPIKey: []config.VertexCompatKey{{APIKey: "vertex", Weight: weight(7)}}, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := synth.Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if len(auths) != 7 { t.Fatalf("auth count = %d, want 7", len(auths)) } for index, auth := range auths { wantWeight := strconv.Itoa(index + 1) if gotWeight := auth.Attributes[coreauth.AttributeWeight]; gotWeight != wantWeight { t.Fatalf("auth[%d] weight = %q, want %q", index, gotWeight, wantWeight) } } } func TestConfigSynthesizer_AllProviders(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "gemini-key"}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "claude-key"}, }, CodexKey: []config.CodexKey{ {APIKey: "codex-key"}, }, XAIKey: []config.XAIKey{ {APIKey: "xai-key"}, }, OpenAICompatibility: []config.OpenAICompatibility{ {Name: "compat", BaseURL: "https://compat.api"}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "vertex-key", BaseURL: "https://vertex.api"}, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 6 { t.Fatalf("expected 6 auths, got %d", len(auths)) } providers := make(map[string]bool) for _, a := range auths { providers[a.Provider] = true } expected := []string{"gemini", "claude", "codex", "xai", "openai-compatible-compat", "vertex"} for _, p := range expected { if !providers[p] { t.Errorf("expected provider %s not found", p) } } } func TestConfigSynthesizer_RequestRetry(t *testing.T) { zero := 0 positive := 2 negative := -1 synth := NewConfigSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "gemini-zero", RequestRetry: &zero}, {APIKey: "gemini-positive", RequestRetry: &positive}, {APIKey: "gemini-negative", RequestRetry: &negative}, {APIKey: "gemini-unset"}, }, InteractionsKey: []config.GeminiKey{ {APIKey: "interactions-zero", RequestRetry: &zero}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "claude-positive", RequestRetry: &positive}, }, CodexKey: []config.CodexKey{ {APIKey: "codex-zero", RequestRetry: &zero}, }, XAIKey: []config.XAIKey{ {APIKey: "xai-positive", RequestRetry: &positive}, }, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "compat", BaseURL: "https://compat.api", RequestRetry: &zero, APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "compat-key"}, }, }, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "vertex-positive", BaseURL: "https://vertex.api", RequestRetry: &positive}, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := synth.Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } want := map[string]any{ "gemini-zero": 0, "gemini-positive": 2, "gemini-negative": nil, "gemini-unset": nil, "interactions-zero": 0, "claude-positive": 2, "codex-zero": 0, "xai-positive": 2, "compat-key": 0, "vertex-positive": 2, } got := make(map[string]any, len(auths)) for _, auth := range auths { key := auth.Attributes["api_key"] if auth.Metadata == nil { got[key] = nil continue } if value, exists := auth.Metadata["request_retry"]; exists { got[key] = value continue } got[key] = nil } for key, expected := range want { actual, exists := got[key] if !exists { t.Fatalf("missing synthesized auth for %s", key) } if actual != expected { t.Fatalf("%s request_retry = %v, want %v", key, actual, expected) } } } func TestConfigSynthesizer_RequestScopedErrors(t *testing.T) { synth := NewConfigSynthesizer() rules := []config.RequestScopedErrorRule{ { Status: 400, Match: []string{"maximum_context_length"}, Action: "stop", }, } ctx := &SynthesisContext{ Config: &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "gemini-key", RequestScopedErrors: rules}, }, InteractionsKey: []config.GeminiKey{ {APIKey: "interactions-key", RequestScopedErrors: rules}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "claude-key", RequestScopedErrors: rules}, }, CodexKey: []config.CodexKey{ {APIKey: "codex-key", BaseURL: "https://codex.api", RequestScopedErrors: rules}, }, XAIKey: []config.CodexKey{ {APIKey: "xai-key", BaseURL: "https://xai.api", RequestScopedErrors: rules}, }, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "compat", BaseURL: "https://compat.api", RequestScopedErrors: rules, APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "compat-key"}, }, }, }, }, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := synth.Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } for _, auth := range auths { if auth.Metadata == nil { t.Fatalf("auth %s has nil metadata", auth.ID) } val, exists := auth.Metadata["request_scoped_errors"] if !exists { t.Fatalf("auth %s missing request_scoped_errors in metadata", auth.ID) } extracted, ok := val.([]config.RequestScopedErrorRule) if !ok || len(extracted) != 1 || extracted[0].Action != "stop" { t.Fatalf("auth %s unexpected request_scoped_errors: %#v", auth.ID, val) } } }