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:
parent
3bb4f4ecc6
commit
6c065d2852
4 changed files with 61 additions and 8 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue