fix(anthropic): 补齐创建账号页自动透传开关并验证后端透传参数
- 在 CreateAccountModal 为 Anthropic API Key 增加自动透传开关 - 创建请求写入 extra.anthropic_passthrough 并补充状态重置 - 新增 AccountHandler 单测,验证 extra 字段从请求到 CreateAccountInput 的透传
This commit is contained in:
@@ -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"])
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user