diff --git a/pkg/providers/factory.go b/pkg/providers/factory.go index 5b3e42b9e..2f36a97af 100644 --- a/pkg/providers/factory.go +++ b/pkg/providers/factory.go @@ -17,6 +17,7 @@ type providerType int const ( providerTypeHTTPCompat providerType = iota providerTypeClaudeAuth + providerTypeClaudeAPIKey providerTypeCodexAuth providerTypeCodexCLIToken providerTypeClaudeCLI @@ -91,6 +92,8 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { if sel.apiBase == "" { sel.apiBase = defaultAnthropicAPIBase } + sel.providerType = providerTypeClaudeAPIKey + return sel, nil } case "openrouter": if cfg.Providers.OpenRouter.APIKey != "" { @@ -241,6 +244,8 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { if sel.apiBase == "" { sel.apiBase = defaultAnthropicAPIBase } + sel.providerType = providerTypeClaudeAPIKey + return sel, nil case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""): sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index 155317a3b..35fe04ff6 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -120,7 +120,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err } return provider, modelID, nil } - // Use API key with HTTP API + // Use API key with native Claude provider (not OpenAI-compatible HTTP). + // The Anthropic API uses /v1/messages (not /v1/chat/completions) and + // requires x-api-key header, so HTTPProvider would produce 404 errors. apiBase := cfg.APIBase if apiBase == "" { apiBase = "https://api.anthropic.com/v1" @@ -128,13 +130,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err if cfg.APIKey == "" { return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model) } - return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( - cfg.APIKey, - apiBase, - cfg.Proxy, - cfg.MaxTokensField, - cfg.RequestTimeout, - ), modelID, nil + return NewClaudeProviderWithBaseURL(cfg.APIKey, apiBase), modelID, nil case "antigravity": return NewAntigravityProvider(), modelID, nil diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index 78389f331..ac62ad118 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -178,6 +178,12 @@ func TestCreateProviderFromConfig_Anthropic(t *testing.T) { if modelID != "claude-sonnet-4.6" { t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4.6") } + // Anthropic API key must route to native ClaudeProvider (not HTTPProvider). + // The Anthropic API uses /v1/messages and x-api-key header, which are + // incompatible with the OpenAI-compatible HTTPProvider. + if _, ok := provider.(*ClaudeProvider); !ok { + t.Errorf("expected *ClaudeProvider for anthropic API key, got %T", provider) + } } func TestCreateProviderFromConfig_Antigravity(t *testing.T) { diff --git a/pkg/providers/factory_test.go b/pkg/providers/factory_test.go index f7a916d9e..9116dd57d 100644 --- a/pkg/providers/factory_test.go +++ b/pkg/providers/factory_test.go @@ -105,6 +105,36 @@ func TestResolveProviderSelection(t *testing.T) { }, wantType: providerTypeClaudeAuth, }, + { + name: "explicit anthropic provider with api key routes to claude api key provider", + setup: func(cfg *config.Config) { + cfg.Agents.Defaults.Provider = "anthropic" + cfg.Agents.Defaults.Model = "claude-opus-4-6" + cfg.Providers.Anthropic.APIKey = "sk-ant-api03-test" + }, + wantType: providerTypeClaudeAPIKey, + wantAPIBase: "https://api.anthropic.com/v1", + }, + { + name: "explicit claude provider with api key and custom base routes to claude api key provider", + setup: func(cfg *config.Config) { + cfg.Agents.Defaults.Provider = "claude" + cfg.Agents.Defaults.Model = "claude-opus-4-6" + cfg.Providers.Anthropic.APIKey = "sk-ant-api03-test" + cfg.Providers.Anthropic.APIBase = "https://proxy.example.com/v1" + }, + wantType: providerTypeClaudeAPIKey, + wantAPIBase: "https://proxy.example.com/v1", + }, + { + name: "claude model name with api key infers claude api key provider", + setup: func(cfg *config.Config) { + cfg.Agents.Defaults.Model = "claude-opus-4-6" + cfg.Providers.Anthropic.APIKey = "sk-ant-api03-test" + }, + wantType: providerTypeClaudeAPIKey, + wantAPIBase: "https://api.anthropic.com/v1", + }, { name: "openai oauth routes to codex auth provider", setup: func(cfg *config.Config) { @@ -313,6 +343,22 @@ func TestCreateProviderReturnsClaudeProviderForAnthropicOAuth(t *testing.T) { // TODO: Test custom APIBase when createClaudeAuthProvider supports it } +func TestCreateProviderReturnsClaudeProviderForAnthropicAPIKey(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Agents.Defaults.Provider = "anthropic" + cfg.Agents.Defaults.Model = "claude-opus-4-6" + cfg.Providers.Anthropic.APIKey = "sk-ant-api03-test" + + provider, err := CreateProvider(cfg) + if err != nil { + t.Fatalf("CreateProvider() error = %v", err) + } + + if _, ok := provider.(*ClaudeProvider); !ok { + t.Fatalf("provider type = %T, want *ClaudeProvider", provider) + } +} + func TestCreateProviderReturnsCodexProviderForOpenAIOAuth(t *testing.T) { // TODO: This test requires openai protocol to support auth_method: "oauth" // which is not yet implemented in the new factory_provider.go