mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(anthropic): 支持 API Key Bearer 认证方式
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user