mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3627 from alfadb/fix/ollama-anthropic-bearer-auth
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)
|
||||
|
||||
@@ -5947,7 +5947,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")
|
||||
@@ -6900,7 +6900,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
|
||||
@@ -10429,7 +10429,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")
|
||||
@@ -10521,7 +10521,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,
|
||||
|
||||
@@ -2653,6 +2653,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.platform === 'anthropic' && accountCategory === 'apikey'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.anthropic.apiKeyAuthScheme') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.anthropic.apiKeyAuthSchemeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<select v-model="anthropicAPIKeyAuthScheme" class="input w-52 text-sm">
|
||||
<option value="x_api_key">{{ t('admin.accounts.anthropic.apiKeyAuthSchemeXApiKey') }}</option>
|
||||
<option value="authorization_bearer">{{ t('admin.accounts.anthropic.apiKeyAuthSchemeBearer') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anthropic API Key: Web Search Emulation (hidden when global disabled) -->
|
||||
<div
|
||||
v-if="form.platform === 'anthropic' && accountCategory === 'apikey' && webSearchGlobalEnabled"
|
||||
@@ -3503,7 +3521,9 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
|
||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
const codexCLIOnlyAppServerEnabled = ref(false)
|
||||
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
|
||||
const anthropicPassthroughEnabled = ref(false)
|
||||
const anthropicAPIKeyAuthScheme = ref<AnthropicAPIKeyAuthScheme>('x_api_key')
|
||||
const webSearchEmulationMode = ref('default')
|
||||
const webSearchGlobalEnabled = ref(false)
|
||||
const {
|
||||
@@ -3945,6 +3965,7 @@ watch(
|
||||
}
|
||||
if (newPlatform !== 'anthropic') {
|
||||
anthropicPassthroughEnabled.value = false
|
||||
anthropicAPIKeyAuthScheme.value = 'x_api_key'
|
||||
webSearchEmulationMode.value = 'default'
|
||||
}
|
||||
// Reset OAuth states
|
||||
@@ -3967,6 +3988,7 @@ watch(
|
||||
}
|
||||
if (platform !== 'anthropic' || category !== 'apikey') {
|
||||
anthropicPassthroughEnabled.value = false
|
||||
anthropicAPIKeyAuthScheme.value = 'x_api_key'
|
||||
webSearchEmulationMode.value = 'default'
|
||||
}
|
||||
}
|
||||
@@ -4346,6 +4368,7 @@ const resetForm = () => {
|
||||
codexCLIOnlyEnabled.value = false
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
anthropicPassthroughEnabled.value = false
|
||||
anthropicAPIKeyAuthScheme.value = 'x_api_key'
|
||||
webSearchEmulationMode.value = 'default'
|
||||
// Reset quota control state
|
||||
windowCostEnabled.value = false
|
||||
@@ -4465,6 +4488,11 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk
|
||||
} else {
|
||||
delete extra.anthropic_passthrough
|
||||
}
|
||||
if (anthropicAPIKeyAuthScheme.value === 'authorization_bearer') {
|
||||
extra.anthropic_apikey_auth_scheme = 'authorization_bearer'
|
||||
} else {
|
||||
delete extra.anthropic_apikey_auth_scheme
|
||||
}
|
||||
if (webSearchEmulationMode.value === 'default') {
|
||||
delete extra.web_search_emulation
|
||||
} else {
|
||||
|
||||
@@ -1537,6 +1537,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="account?.platform === 'anthropic' && account?.type === 'apikey'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.anthropic.apiKeyAuthScheme') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.anthropic.apiKeyAuthSchemeDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<select v-model="anthropicAPIKeyAuthScheme" class="input w-52 text-sm">
|
||||
<option value="x_api_key">{{ t('admin.accounts.anthropic.apiKeyAuthSchemeXApiKey') }}</option>
|
||||
<option value="authorization_bearer">{{ t('admin.accounts.anthropic.apiKeyAuthSchemeBearer') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anthropic API Key: Web Search Emulation (hidden when global disabled) -->
|
||||
<div
|
||||
v-if="account?.platform === 'anthropic' && account?.type === 'apikey' && webSearchGlobalEnabled"
|
||||
@@ -2610,7 +2628,9 @@ const codexCLIOnlyEnabled = ref(false)
|
||||
const codexCLIOnlyAppServerEnabled = ref(false)
|
||||
type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled'
|
||||
const codexImageGenerationBridgeMode = ref<CodexImageGenerationBridgeMode>('inherit')
|
||||
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
|
||||
const anthropicPassthroughEnabled = ref(false)
|
||||
const anthropicAPIKeyAuthScheme = ref<AnthropicAPIKeyAuthScheme>('x_api_key')
|
||||
const webSearchEmulationMode = ref('default')
|
||||
const webSearchGlobalEnabled = ref(false)
|
||||
const {
|
||||
@@ -3015,6 +3035,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
codexCLIOnlyAppServerEnabled.value = false
|
||||
codexImageGenerationBridgeMode.value = 'inherit'
|
||||
anthropicPassthroughEnabled.value = false
|
||||
anthropicAPIKeyAuthScheme.value = 'x_api_key'
|
||||
webSearchEmulationMode.value = 'default'
|
||||
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
|
||||
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
|
||||
@@ -3061,6 +3082,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
}
|
||||
if (newAccount.platform === 'anthropic' && newAccount.type === 'apikey') {
|
||||
anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true
|
||||
anthropicAPIKeyAuthScheme.value = extra?.anthropic_apikey_auth_scheme === 'authorization_bearer'
|
||||
? 'authorization_bearer'
|
||||
: 'x_api_key'
|
||||
// 三态:string "default"/"enabled"/"disabled",向后兼容旧 bool
|
||||
const wsVal = extra?.web_search_emulation
|
||||
if (wsVal === 'enabled' || wsVal === 'disabled') {
|
||||
@@ -4101,6 +4125,11 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
delete newExtra.anthropic_passthrough
|
||||
}
|
||||
if (anthropicAPIKeyAuthScheme.value === 'authorization_bearer') {
|
||||
newExtra.anthropic_apikey_auth_scheme = 'authorization_bearer'
|
||||
} else {
|
||||
delete newExtra.anthropic_apikey_auth_scheme
|
||||
}
|
||||
if (webSearchEmulationMode.value === 'default') {
|
||||
delete newExtra.web_search_emulation
|
||||
} else {
|
||||
|
||||
@@ -3595,6 +3595,10 @@ export default {
|
||||
apiKeyPassthrough: 'Auto passthrough (auth only)',
|
||||
apiKeyPassthroughDesc:
|
||||
'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.',
|
||||
apiKeyAuthScheme: 'Upstream auth scheme',
|
||||
apiKeyAuthSchemeDesc: 'Choose the API key auth header used when forwarding to an Anthropic-compatible upstream. Ollama Cloud uses Authorization: Bearer.',
|
||||
apiKeyAuthSchemeXApiKey: 'x-api-key',
|
||||
apiKeyAuthSchemeBearer: 'Authorization: Bearer',
|
||||
webSearchEmulation: 'Web Search Emulation',
|
||||
webSearchEmulationDesc:
|
||||
'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally. Default follows channel config.',
|
||||
|
||||
@@ -3763,6 +3763,10 @@ export default {
|
||||
apiKeyPassthrough: '自动透传(仅替换认证)',
|
||||
apiKeyPassthroughDesc:
|
||||
'仅对 Anthropic API Key 生效。开启后,messages/count_tokens 请求将透传上游并仅替换认证,保留计费/并发/审计及必要安全过滤;关闭即可回滚到现有兼容链路。',
|
||||
apiKeyAuthScheme: '上游认证方式',
|
||||
apiKeyAuthSchemeDesc: '选择转发到 Anthropic-compatible 上游时使用的 API Key 认证头。Ollama Cloud 使用 Authorization: Bearer。',
|
||||
apiKeyAuthSchemeXApiKey: 'x-api-key',
|
||||
apiKeyAuthSchemeBearer: 'Authorization: Bearer',
|
||||
webSearchEmulation: 'Web Search 模拟',
|
||||
webSearchEmulationDesc:
|
||||
'为该 API Key 账号启用 web search 模拟。客户端发送纯 web_search 请求时,由网关调用第三方搜索 API 并构造响应返回。默认跟随渠道配置。',
|
||||
|
||||
Reference in New Issue
Block a user