fix(providers): route Anthropic API key to native ClaudeProvider instead of HTTPProvider

When using `provider: "anthropic"` with a plain `api_key` (no `auth_method`),
the factory incorrectly left `providerType` as `providerTypeHTTPCompat`,
causing requests to be sent via the OpenAI-compatible HTTP provider.
This resulted in 404 errors because the Anthropic API does not have a
`/v1/chat/completions` endpoint and requires different headers/body format.

Add a new `providerTypeClaudeAPIKey` that routes to `NewClaudeProviderWithBaseURL`,
which uses the native Anthropic SDK with the correct `/v1/messages` endpoint
and `x-api-key` header. Fix both the explicit provider path and the model-name
fallback detection path.

Closes #269
This commit is contained in:
Nikolas de Hor 2026-02-18 20:07:48 -03:00
parent 3bb4f4ecc6
commit 6c065d2852
4 changed files with 61 additions and 8 deletions

View file

@ -17,6 +17,7 @@ type providerType int
const ( const (
providerTypeHTTPCompat providerType = iota providerTypeHTTPCompat providerType = iota
providerTypeClaudeAuth providerTypeClaudeAuth
providerTypeClaudeAPIKey
providerTypeCodexAuth providerTypeCodexAuth
providerTypeCodexCLIToken providerTypeCodexCLIToken
providerTypeClaudeCLI providerTypeClaudeCLI
@ -91,6 +92,8 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
if sel.apiBase == "" { if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase sel.apiBase = defaultAnthropicAPIBase
} }
sel.providerType = providerTypeClaudeAPIKey
return sel, nil
} }
case "openrouter": case "openrouter":
if cfg.Providers.OpenRouter.APIKey != "" { if cfg.Providers.OpenRouter.APIKey != "" {
@ -241,6 +244,8 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
if sel.apiBase == "" { if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase sel.apiBase = defaultAnthropicAPIBase
} }
sel.providerType = providerTypeClaudeAPIKey
return sel, nil
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) &&
(cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""): (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch

View file

@ -120,7 +120,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
} }
return provider, modelID, nil 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 apiBase := cfg.APIBase
if apiBase == "" { if apiBase == "" {
apiBase = "https://api.anthropic.com/v1" apiBase = "https://api.anthropic.com/v1"
@ -128,13 +130,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
if cfg.APIKey == "" { if cfg.APIKey == "" {
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model) return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
} }
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout( return NewClaudeProviderWithBaseURL(cfg.APIKey, apiBase), modelID, nil
cfg.APIKey,
apiBase,
cfg.Proxy,
cfg.MaxTokensField,
cfg.RequestTimeout,
), modelID, nil
case "antigravity": case "antigravity":
return NewAntigravityProvider(), modelID, nil return NewAntigravityProvider(), modelID, nil

View file

@ -178,6 +178,12 @@ func TestCreateProviderFromConfig_Anthropic(t *testing.T) {
if modelID != "claude-sonnet-4.6" { if modelID != "claude-sonnet-4.6" {
t.Errorf("modelID = %q, want %q", 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) { func TestCreateProviderFromConfig_Antigravity(t *testing.T) {

View file

@ -105,6 +105,36 @@ func TestResolveProviderSelection(t *testing.T) {
}, },
wantType: providerTypeClaudeAuth, 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", name: "openai oauth routes to codex auth provider",
setup: func(cfg *config.Config) { setup: func(cfg *config.Config) {
@ -313,6 +343,22 @@ func TestCreateProviderReturnsClaudeProviderForAnthropicOAuth(t *testing.T) {
// TODO: Test custom APIBase when createClaudeAuthProvider supports it // 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) { func TestCreateProviderReturnsCodexProviderForOpenAIOAuth(t *testing.T) {
// TODO: This test requires openai protocol to support auth_method: "oauth" // TODO: This test requires openai protocol to support auth_method: "oauth"
// which is not yet implemented in the new factory_provider.go // which is not yet implemented in the new factory_provider.go