fix(anthropic): 支持 API Key Bearer 认证方式

This commit is contained in:
alfadb
2026-07-02 10:43:08 +08:00
parent 7dc7cfce1d
commit 7869b7fe38
11 changed files with 238 additions and 14 deletions
@@ -60,3 +60,59 @@ func TestAccount_IsAnthropicAPIKeyPassthroughEnabled(t *testing.T) {
require.False(t, openai.IsAnthropicAPIKeyPassthroughEnabled())
})
}
func TestAccount_GetAnthropicAPIKeyAuthScheme(t *testing.T) {
tests := []struct {
name string
account *Account
want string
}{
{
name: "missing extra defaults to x-api-key",
account: &Account{
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
},
want: AnthropicAPIKeyAuthSchemeXAPIKey,
},
{
name: "explicit bearer",
account: &Account{
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Extra: map[string]any{
"anthropic_apikey_auth_scheme": AnthropicAPIKeyAuthSchemeAuthorizationBearer,
},
},
want: AnthropicAPIKeyAuthSchemeAuthorizationBearer,
},
{
name: "invalid value defaults to x-api-key",
account: &Account{
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Extra: map[string]any{
"anthropic_apikey_auth_scheme": "bearer",
},
},
want: AnthropicAPIKeyAuthSchemeXAPIKey,
},
{
name: "non Anthropic API key defaults to x-api-key",
account: &Account{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Extra: map[string]any{
"anthropic_apikey_auth_scheme": AnthropicAPIKeyAuthSchemeAuthorizationBearer,
},
},
want: AnthropicAPIKeyAuthSchemeXAPIKey,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, tt.account.GetAnthropicAPIKeyAuthScheme())
})
}
}
@@ -228,20 +228,15 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
// Determine authentication method and API URL
var authToken string
var useBearer bool
var apiURL string
if account.IsOAuth() {
// OAuth or Setup Token - use Bearer token
useBearer = true
apiURL = testClaudeAPIURL
authToken = account.GetCredential("access_token")
if authToken == "" {
return s.sendErrorAndEnd(c, "No access token available")
}
} else if account.Type == "apikey" {
// API Key - use x-api-key header
useBearer = false
authToken = account.GetCredential("api_key")
if authToken == "" {
return s.sendErrorAndEnd(c, "No API key available")
@@ -292,12 +287,12 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
}
// Set authentication header
if useBearer {
if account.IsOAuth() {
req.Header.Set("anthropic-beta", claude.DefaultBetaHeader)
req.Header.Set("Authorization", "Bearer "+authToken)
} else {
req.Header.Set("anthropic-beta", claude.APIKeyBetaHeader)
req.Header.Set("x-api-key", authToken)
setAnthropicAPIKeyAuthHeader(req.Header, account, authToken)
}
// Get proxy URL
@@ -0,0 +1,37 @@
package service
import (
"net/http"
"strings"
)
const (
anthropicAPIKeyAuthSchemeExtraKey = "anthropic_apikey_auth_scheme"
AnthropicAPIKeyAuthSchemeXAPIKey = "x_api_key"
AnthropicAPIKeyAuthSchemeAuthorizationBearer = "authorization_bearer"
)
// GetAnthropicAPIKeyAuthScheme returns the upstream authentication scheme for
// Anthropic API-key accounts. Missing or invalid values keep the historical
// x-api-key behavior.
func (a *Account) GetAnthropicAPIKeyAuthScheme() string {
if a == nil || a.Platform != PlatformAnthropic || a.Type != AccountTypeAPIKey {
return AnthropicAPIKeyAuthSchemeXAPIKey
}
switch strings.TrimSpace(a.GetExtraString(anthropicAPIKeyAuthSchemeExtraKey)) {
case AnthropicAPIKeyAuthSchemeAuthorizationBearer:
return AnthropicAPIKeyAuthSchemeAuthorizationBearer
default:
return AnthropicAPIKeyAuthSchemeXAPIKey
}
}
func setAnthropicAPIKeyAuthHeader(header http.Header, account *Account, token string) {
if account.GetAnthropicAPIKeyAuthScheme() == AnthropicAPIKeyAuthSchemeAuthorizationBearer {
header.Set("Authorization", "Bearer "+token)
return
}
header.Set("x-api-key", token)
}
@@ -261,6 +261,56 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_ForwardCountTokensPreservesBo
require.Empty(t, rec.Header().Get("Set-Cookie"))
}
func TestGatewayService_AnthropicAPIKeyPassthrough_BearerAuthScheme(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
c.Request.Header.Set("Authorization", "Bearer inbound-token")
c.Request.Header.Set("X-Api-Key", "inbound-api-key")
c.Request.Header.Set("Cookie", "secret=1")
svc := &GatewayService{
cfg: &config.Config{
Security: config.SecurityConfig{
URLAllowlist: config.URLAllowlistConfig{Enabled: false},
},
},
}
account := &Account{
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "ollama-key",
"base_url": "https://ollama.com",
},
Extra: map[string]any{
"anthropic_passthrough": true,
"anthropic_apikey_auth_scheme": AnthropicAPIKeyAuthSchemeAuthorizationBearer,
},
}
msgReq, wireBody, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(
context.Background(), c, account, []byte(`{"model":"gpt-oss:20b","messages":[]}`), "ollama-key",
)
require.NoError(t, err)
require.Equal(t, "https://ollama.com/v1/messages?beta=true", msgReq.URL.String())
require.JSONEq(t, `{"model":"gpt-oss:20b","messages":[]}`, string(wireBody))
require.Equal(t, "Bearer ollama-key", getHeaderRaw(msgReq.Header, "authorization"))
require.Empty(t, getHeaderRaw(msgReq.Header, "x-api-key"))
require.Empty(t, getHeaderRaw(msgReq.Header, "cookie"))
countReq, err := svc.buildCountTokensRequestAnthropicAPIKeyPassthrough(
context.Background(), c, account, []byte(`{"model":"gpt-oss:20b","messages":[]}`), "ollama-key",
)
require.NoError(t, err)
require.Equal(t, "https://ollama.com/v1/messages/count_tokens?beta=true", countReq.URL.String())
require.Equal(t, "Bearer ollama-key", getHeaderRaw(countReq.Header, "authorization"))
require.Empty(t, getHeaderRaw(countReq.Header, "x-api-key"))
require.Empty(t, getHeaderRaw(countReq.Header, "cookie"))
}
// TestGatewayService_AnthropicAPIKeyPassthrough_ModelMappingEdgeCases 覆盖透传模式下模型映射的各种边界情况
func TestGatewayService_AnthropicAPIKeyPassthrough_ModelMappingEdgeCases(t *testing.T) {
gin.SetMode(gin.TestMode)
+4 -4
View File
@@ -5884,7 +5884,7 @@ func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough(
req.Header.Del("x-api-key")
req.Header.Del("x-goog-api-key")
req.Header.Del("cookie")
setHeaderRaw(req.Header, "x-api-key", token)
setAnthropicAPIKeyAuthHeader(req.Header, account, token)
if getHeaderRaw(req.Header, "content-type") == "" {
setHeaderRaw(req.Header, "content-type", "application/json")
@@ -6818,7 +6818,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
if tokenType == "oauth" {
setHeaderRaw(req.Header, "authorization", "Bearer "+token)
} else {
setHeaderRaw(req.Header, "x-api-key", token)
setAnthropicAPIKeyAuthHeader(req.Header, account, token)
}
// 白名单透传 headers
@@ -10290,7 +10290,7 @@ func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough(
req.Header.Del("x-api-key")
req.Header.Del("x-goog-api-key")
req.Header.Del("cookie")
req.Header.Set("x-api-key", token)
setAnthropicAPIKeyAuthHeader(req.Header, account, token)
if req.Header.Get("content-type") == "" {
req.Header.Set("content-type", "application/json")
@@ -10382,7 +10382,7 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
if tokenType == "oauth" {
setHeaderRaw(req.Header, "authorization", "Bearer "+token)
} else {
setHeaderRaw(req.Header, "x-api-key", token)
setAnthropicAPIKeyAuthHeader(req.Header, account, token)
}
// 白名单透传 headers(恢复真实 wire casing)
+7 -3
View File
@@ -154,6 +154,7 @@ func (s *AccountTestService) buildAnthropicUpstreamModelsRequest(ctx context.Con
baseURL := "https://api.anthropic.com"
authHeaderName := ""
authHeaderValue := ""
apiKeyAuthToken := ""
betaHeader := ""
if account.IsOAuth() {
@@ -180,8 +181,7 @@ func (s *AccountTestService) buildAnthropicUpstreamModelsRequest(ctx context.Con
if strings.TrimSpace(baseURL) == "" {
baseURL = "https://api.anthropic.com"
}
authHeaderName = "x-api-key"
authHeaderValue = apiKey
apiKeyAuthToken = apiKey
betaHeader = claude.APIKeyBetaHeader
} else {
return nil, newUpstreamModelSyncUnsupportedError(
@@ -203,7 +203,11 @@ func (s *AccountTestService) buildAnthropicUpstreamModelsRequest(ctx context.Con
req.Header.Set("Accept", "application/json")
req.Header.Set("anthropic-version", "2023-06-01")
req.Header.Set("anthropic-beta", betaHeader)
req.Header.Set(authHeaderName, authHeaderValue)
if authHeaderName != "" {
req.Header.Set(authHeaderName, authHeaderValue)
} else {
setAnthropicAPIKeyAuthHeader(req.Header, account, apiKeyAuthToken)
}
return req, nil
}
@@ -93,6 +93,23 @@ func TestBuildUpstreamModelsRequestsForAPIKeyAccounts(t *testing.T) {
require.Equal(t, "anthropic-key", anthropicReq.Header.Get("x-api-key"))
require.Equal(t, "2023-06-01", anthropicReq.Header.Get("anthropic-version"))
anthropicBearerReq, err := svc.buildAnthropicUpstreamModelsRequest(ctx, &Account{
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "ollama-key",
"base_url": "https://ollama.com",
},
Extra: map[string]any{
"anthropic_apikey_auth_scheme": AnthropicAPIKeyAuthSchemeAuthorizationBearer,
},
})
require.NoError(t, err)
require.Equal(t, "https://ollama.com/v1/models", anthropicBearerReq.URL.String())
require.Equal(t, "Bearer ollama-key", anthropicBearerReq.Header.Get("Authorization"))
require.Empty(t, anthropicBearerReq.Header.Get("x-api-key"))
require.Equal(t, "2023-06-01", anthropicBearerReq.Header.Get("anthropic-version"))
openAIReq, err := svc.buildOpenAIUpstreamModelsRequest(ctx, &Account{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,