fix(anthropic): 补齐创建账号页自动透传开关并验证后端透传参数

- 在 CreateAccountModal 为 Anthropic API Key 增加自动透传开关

- 创建请求写入 extra.anthropic_passthrough 并补充状态重置

- 新增 AccountHandler 单测,验证 extra 字段从请求到 CreateAccountInput 的透传
This commit is contained in:
yangjianbo
2026-02-21 14:40:31 +08:00
parent bde9dbc57a
commit fdfc739b72
2 changed files with 121 additions and 1 deletions

View File

@@ -0,0 +1,67 @@
package admin
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func TestAccountHandler_Create_AnthropicAPIKeyPassthroughExtraForwarded(t *testing.T) {
gin.SetMode(gin.TestMode)
adminSvc := newStubAdminService()
handler := NewAccountHandler(
adminSvc,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
router := gin.New()
router.POST("/api/v1/admin/accounts", handler.Create)
body := map[string]any{
"name": "anthropic-key-1",
"platform": "anthropic",
"type": "apikey",
"credentials": map[string]any{
"api_key": "sk-ant-xxx",
"base_url": "https://api.anthropic.com",
},
"extra": map[string]any{
"anthropic_passthrough": true,
},
"concurrency": 1,
"priority": 1,
}
raw, err := json.Marshal(body)
require.NoError(t, err)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts", bytes.NewReader(raw))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Len(t, adminSvc.createdAccounts, 1)
created := adminSvc.createdAccounts[0]
require.Equal(t, "anthropic", created.Platform)
require.Equal(t, "apikey", created.Type)
require.NotNil(t, created.Extra)
require.Equal(t, true, created.Extra["anthropic_passthrough"])
}

View File

@@ -1697,6 +1697,36 @@
</div> </div>
</div> </div>
<!-- Anthropic API Key 自动透传开关 -->
<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">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.anthropic.apiKeyPassthrough') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.anthropic.apiKeyPassthroughDesc') }}
</p>
</div>
<button
type="button"
@click="anthropicPassthroughEnabled = !anthropicPassthroughEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
anthropicPassthroughEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
anthropicPassthroughEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
</div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 --> <!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<div <div
v-if="form.platform === 'openai' && accountCategory === 'oauth-based'" v-if="form.platform === 'openai' && accountCategory === 'oauth-based'"
@@ -2290,6 +2320,7 @@ const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true) const autoPauseOnExpired = ref(true)
const openaiPassthroughEnabled = ref(false) const openaiPassthroughEnabled = ref(false)
const codexCLIOnlyEnabled = ref(false) const codexCLIOnlyEnabled = ref(false)
const anthropicPassthroughEnabled = ref(false)
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const upstreamBaseUrl = ref('') // For upstream type: base URL const upstreamBaseUrl = ref('') // For upstream type: base URL
@@ -2526,6 +2557,9 @@ watch(
openaiPassthroughEnabled.value = false openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false codexCLIOnlyEnabled.value = false
} }
if (newPlatform !== 'anthropic') {
anthropicPassthroughEnabled.value = false
}
// Reset OAuth states // Reset OAuth states
oauth.resetState() oauth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
@@ -2542,6 +2576,9 @@ watch(
if (platform === 'openai' && category !== 'oauth-based') { if (platform === 'openai' && category !== 'oauth-based') {
codexCLIOnlyEnabled.value = false codexCLIOnlyEnabled.value = false
} }
if (platform !== 'anthropic' || category !== 'apikey') {
anthropicPassthroughEnabled.value = false
}
} }
) )
@@ -2791,6 +2828,7 @@ const resetForm = () => {
autoPauseOnExpired.value = true autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false openaiPassthroughEnabled.value = false
codexCLIOnlyEnabled.value = false codexCLIOnlyEnabled.value = false
anthropicPassthroughEnabled.value = false
// Reset quota control state // Reset quota control state
windowCostEnabled.value = false windowCostEnabled.value = false
windowCostLimit.value = null windowCostLimit.value = null
@@ -2845,6 +2883,21 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
return Object.keys(extra).length > 0 ? extra : undefined return Object.keys(extra).length > 0 ? extra : undefined
} }
const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unknown> | undefined => {
if (form.platform !== 'anthropic' || accountCategory.value !== 'apikey') {
return base
}
const extra: Record<string, unknown> = { ...(base || {}) }
if (anthropicPassthroughEnabled.value) {
extra.anthropic_passthrough = true
} else {
delete extra.anthropic_passthrough
}
return Object.keys(extra).length > 0 ? extra : undefined
}
const buildSoraExtra = ( const buildSoraExtra = (
base?: Record<string, unknown>, base?: Record<string, unknown>,
linkedOpenAIAccountId?: string | number linkedOpenAIAccountId?: string | number
@@ -3015,7 +3068,7 @@ const handleSubmit = async () => {
} }
form.credentials = credentials form.credentials = credentials
const extra = buildOpenAIExtra() const extra = buildAnthropicExtra(buildOpenAIExtra())
await doCreateAccount({ await doCreateAccount({
...form, ...form,