mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
Merge pull request #5842 from SavitarC/feat/adaptive-api-protocol
feat(accounts): 添加账号支持自适应API 协议
This commit is contained in:
@@ -30,7 +30,7 @@ const props = defineProps<{
|
||||
/** 当前已选账号类型,用于过滤和高亮匹配的预设 */
|
||||
mode?: 'payg' | 'coding'
|
||||
/** 当前已选 API 协议,用于过滤和高亮匹配的预设 */
|
||||
protocol?: 'chat_completions' | 'anthropic' | 'responses'
|
||||
protocol?: 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
|
||||
/** 当前输入框中的 base url,用于高亮完全匹配项 */
|
||||
currentUrl?: string
|
||||
}>()
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon :name="opt.value === 'anthropic' ? 'sparkles' : opt.value === 'responses' ? 'terminal' : 'chat'" size="sm" />
|
||||
<Icon :name="opt.value === 'adaptive' ? 'swap' : opt.value === 'anthropic' ? 'sparkles' : opt.value === 'responses' ? 'terminal' : 'chat'" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t(`admin.accounts.cnProviders.apiProtocol.${opt.labelKey}`) }}</span>
|
||||
@@ -1250,7 +1250,7 @@
|
||||
|
||||
<!-- API Key input (only for apikey type, excluding Antigravity which has its own fields) -->
|
||||
<div v-if="form.type === 'apikey' && form.platform !== 'antigravity'" class="space-y-4">
|
||||
<div>
|
||||
<div v-if="!isCNPlatform || apiProtocol !== 'adaptive'">
|
||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
v-model="apiKeyBaseUrl"
|
||||
@@ -1274,6 +1274,25 @@
|
||||
@select="onCnPresetSelect"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.endpoints') }}</label>
|
||||
<div class="mt-2 space-y-3">
|
||||
<div v-for="item in cnAdaptiveProtocolOptions" :key="item.value">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t(`admin.accounts.cnProviders.apiProtocol.${item.labelKey}`) }}
|
||||
</label>
|
||||
<input
|
||||
v-model="adaptiveBaseUrls[item.value]"
|
||||
type="text"
|
||||
class="input"
|
||||
:data-testid="`cn-adaptive-base-url-${item.value}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="form.platform !== 'deepseek'" class="input-hint">
|
||||
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
|
||||
<input
|
||||
@@ -3746,11 +3765,13 @@ import {
|
||||
applyAntigravityProjectID,
|
||||
applyHeaderOverride,
|
||||
applyInterceptWarmup,
|
||||
defaultCNAdaptiveBaseUrls,
|
||||
defaultCNBaseUrl,
|
||||
isHeaderOverrideCapable,
|
||||
validateHeaderOverrideRows,
|
||||
type CnAccountMode,
|
||||
type CnApiProtocol,
|
||||
type CnNativeApiProtocol,
|
||||
type HeaderOverrideRow
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
@@ -3932,7 +3953,12 @@ const upstreamBillingAutoProbeEnabled = ref(true)
|
||||
const accountMode = ref<CnAccountMode>('payg')
|
||||
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
|
||||
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
|
||||
const apiProtocol = ref<CnApiProtocol>('chat_completions')
|
||||
const apiProtocol = ref<CnApiProtocol>('adaptive')
|
||||
const adaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
|
||||
chat_completions: '',
|
||||
anthropic: '',
|
||||
responses: ''
|
||||
})
|
||||
const isCNPlatform = computed(
|
||||
() => form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek'
|
||||
)
|
||||
@@ -3947,6 +3973,7 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
|
||||
// 当前平台可选的协议档(responses 仅 deepseek)。
|
||||
const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnApiProtocol; labelKey: string }> = [
|
||||
{ value: 'adaptive', labelKey: 'adaptive' },
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
@@ -3955,6 +3982,18 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
|
||||
}
|
||||
return opts
|
||||
})
|
||||
const cnAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnNativeApiProtocol; labelKey: string }> = [
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
if (form.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
|
||||
return opts
|
||||
})
|
||||
|
||||
function resetAdaptiveBaseUrls(platform: 'kimi' | 'zhipu' | 'deepseek', mode: CnAccountMode) {
|
||||
adaptiveBaseUrls.value = defaultCNAdaptiveBaseUrls(platform, mode)
|
||||
}
|
||||
// 当前选中平台的品牌色(选中卡片描边 / 图标底色),与 platformColors 取色一致。
|
||||
const cnAccentActiveClass = computed(() => {
|
||||
switch (form.platform) {
|
||||
@@ -3981,24 +4020,44 @@ const cnAccentIconClass = computed(() => {
|
||||
}
|
||||
})
|
||||
// 切换国产供应商平台:强制 apikey 类型,deepseek 无 coding 套餐故锁定 payg,
|
||||
// 协议回落 chat_completions,并把 base url 重置为该平台默认端点。
|
||||
// 协议回落 adaptive,并把 base url 重置为该平台默认端点。
|
||||
function selectCNPlatform(platform: 'kimi' | 'zhipu' | 'deepseek') {
|
||||
form.platform = platform
|
||||
form.type = 'apikey'
|
||||
accountCategory.value = 'apikey'
|
||||
apiProtocol.value = 'chat_completions'
|
||||
apiProtocol.value = 'adaptive'
|
||||
if (platform === 'deepseek') {
|
||||
accountMode.value = 'payg'
|
||||
}
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(platform, accountMode.value, apiProtocol.value)
|
||||
resetAdaptiveBaseUrls(platform, accountMode.value)
|
||||
}
|
||||
// 账号类型 / 协议变更时同步默认 base url。
|
||||
watch(accountMode, (mode) => {
|
||||
watch(accountMode, (mode, previousMode) => {
|
||||
if (!isCNPlatform.value) return
|
||||
if (apiProtocol.value === 'adaptive') {
|
||||
const previousDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, previousMode)
|
||||
const nextDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, mode)
|
||||
for (const item of cnAdaptiveProtocolOptions.value) {
|
||||
if (!adaptiveBaseUrls.value[item.value] || adaptiveBaseUrls.value[item.value] === previousDefaults[item.value]) {
|
||||
adaptiveBaseUrls.value[item.value] = nextDefaults[item.value]
|
||||
}
|
||||
}
|
||||
apiKeyBaseUrl.value = adaptiveBaseUrls.value.chat_completions
|
||||
return
|
||||
}
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, mode, apiProtocol.value)
|
||||
})
|
||||
watch(apiProtocol, (protocol) => {
|
||||
if (!isCNPlatform.value) return
|
||||
if (protocol === 'adaptive') {
|
||||
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, accountMode.value)
|
||||
for (const item of cnAdaptiveProtocolOptions.value) {
|
||||
if (!adaptiveBaseUrls.value[item.value]) adaptiveBaseUrls.value[item.value] = defaults[item.value]
|
||||
}
|
||||
apiKeyBaseUrl.value = adaptiveBaseUrls.value.chat_completions
|
||||
return
|
||||
}
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, accountMode.value, protocol)
|
||||
})
|
||||
// 点击预设端点:同时回填 base url、账号类型与协议。
|
||||
@@ -4010,10 +4069,13 @@ function onCnPresetSelect(preset: { mode: CnAccountMode; protocol: CnApiProtocol
|
||||
|
||||
const syncPreviewCredentials = computed(() => {
|
||||
if (!apiKeyValue.value) return undefined
|
||||
const baseUrl = isCNPlatform.value && apiProtocol.value === 'adaptive'
|
||||
? adaptiveBaseUrls.value.chat_completions.trim() || apiKeyBaseUrl.value.trim()
|
||||
: apiKeyBaseUrl.value.trim()
|
||||
return {
|
||||
platform: form.platform,
|
||||
type: form.type,
|
||||
base_url: apiKeyBaseUrl.value || undefined,
|
||||
base_url: baseUrl || undefined,
|
||||
api_key: apiKeyValue.value
|
||||
}
|
||||
})
|
||||
@@ -4956,7 +5018,8 @@ const resetForm = () => {
|
||||
accountCategory.value = 'oauth-based'
|
||||
addMethod.value = 'oauth'
|
||||
accountMode.value = 'payg'
|
||||
apiProtocol.value = 'chat_completions'
|
||||
apiProtocol.value = 'adaptive'
|
||||
adaptiveBaseUrls.value = { chat_completions: '', anthropic: '', responses: '' }
|
||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||
apiKeyValue.value = ''
|
||||
upstreamBillingAutoProbeEnabled.value = true
|
||||
@@ -5425,10 +5488,19 @@ const handleSubmit = async () => {
|
||||
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
|
||||
credentials.account_mode = accountMode.value
|
||||
credentials.api_protocol = apiProtocol.value
|
||||
if (apiProtocol.value === 'adaptive') {
|
||||
const defaults = defaultCNAdaptiveBaseUrls(form.platform, accountMode.value)
|
||||
const protocolBaseUrls: Record<string, string> = {}
|
||||
for (const item of cnAdaptiveProtocolOptions.value) {
|
||||
protocolBaseUrls[item.value] = (adaptiveBaseUrls.value[item.value] || defaults[item.value]).trim()
|
||||
}
|
||||
credentials.api_base_urls = protocolBaseUrls
|
||||
credentials.base_url = protocolBaseUrls.chat_completions
|
||||
}
|
||||
const resolvedCNBase = (
|
||||
apiKeyBaseUrl.value.trim() || defaultCNBaseUrl(form.platform, accountMode.value, apiProtocol.value)
|
||||
).trim()
|
||||
if (resolvedCNBase) {
|
||||
if (apiProtocol.value !== 'adaptive' && resolvedCNBase) {
|
||||
credentials.base_url = resolvedCNBase
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<!-- API Key fields (only for apikey type) -->
|
||||
<div v-if="account.type === 'apikey'" class="space-y-4">
|
||||
<div>
|
||||
<div v-if="!isCNApiKeyAccount || editApiProtocol !== 'adaptive'">
|
||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
v-model="editBaseUrl"
|
||||
@@ -62,6 +62,20 @@
|
||||
@select="onCnPresetSelect"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.endpoints') }}</label>
|
||||
<div class="mt-2 space-y-3">
|
||||
<div v-for="item in editAdaptiveProtocolOptions" :key="item.value">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t(`admin.accounts.cnProviders.apiProtocol.${item.labelKey}`) }}
|
||||
</label>
|
||||
<input v-model="editAdaptiveBaseUrls[item.value]" type="text" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="account.platform !== 'deepseek'" class="input-hint">
|
||||
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Account Mode Selection (CN providers) -->
|
||||
<div v-if="isCNApiKeyAccount">
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.accountMode.title') }}</label>
|
||||
@@ -2802,11 +2816,13 @@ import {
|
||||
isHeaderOverrideCapable,
|
||||
splitHeaderOverridesObject,
|
||||
validateHeaderOverrideRows,
|
||||
defaultCNAdaptiveBaseUrls,
|
||||
defaultCNBaseUrl,
|
||||
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
|
||||
HEADER_OVERRIDES_CREDENTIAL_KEY,
|
||||
type CnAccountMode,
|
||||
type CnApiProtocol,
|
||||
type CnNativeApiProtocol,
|
||||
type HeaderOverrideRow
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
@@ -2909,8 +2925,13 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
|
||||
}
|
||||
return 'kimi'
|
||||
})
|
||||
const editApiProtocol = ref<CnApiProtocol>('chat_completions')
|
||||
const editApiProtocol = ref<CnApiProtocol>('adaptive')
|
||||
const editAccountMode = ref<CnAccountMode>('payg')
|
||||
const editAdaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
|
||||
chat_completions: '',
|
||||
anthropic: '',
|
||||
responses: ''
|
||||
})
|
||||
// 回填窗口标志:syncFormFromAccount 会同步改写 editAccountMode / editApiProtocol,
|
||||
// 而 watcher(pre-flush)在同步代码执行完之后才触发——若不抑制,会把刚恢复的
|
||||
// 存储版 base_url(可能是用户自定义/中转地址)覆盖为官方预设并在下次保存时持久化。
|
||||
@@ -2930,6 +2951,7 @@ const cnAccountModeOptions = computed<Array<{ value: CnAccountMode; labelKey: 'p
|
||||
)
|
||||
const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnApiProtocol; labelKey: string }> = [
|
||||
{ value: 'adaptive', labelKey: 'adaptive' },
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
@@ -2938,11 +2960,35 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
|
||||
}
|
||||
return opts
|
||||
})
|
||||
watch(editApiProtocol, (protocol) => {
|
||||
const editAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnNativeApiProtocol; labelKey: string }> = [
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
if (props.account?.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
|
||||
return opts
|
||||
})
|
||||
watch(editApiProtocol, (protocol, previousProtocol) => {
|
||||
if (!isCNApiKeyAccount.value || syncingForm.value) return
|
||||
if (protocol === 'adaptive') {
|
||||
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, editAccountMode.value)
|
||||
for (const item of editAdaptiveProtocolOptions.value) {
|
||||
if (!editAdaptiveBaseUrls.value[item.value]) editAdaptiveBaseUrls.value[item.value] = defaults[item.value]
|
||||
}
|
||||
if (previousProtocol !== 'adaptive' && editBaseUrl.value.trim()) {
|
||||
editAdaptiveBaseUrls.value[previousProtocol] = editBaseUrl.value.trim()
|
||||
}
|
||||
editBaseUrl.value = editAdaptiveBaseUrls.value.chat_completions
|
||||
return
|
||||
}
|
||||
if (previousProtocol === 'adaptive') {
|
||||
editBaseUrl.value = editAdaptiveBaseUrls.value[protocol] ||
|
||||
defaultCNBaseUrl(props.account!.platform, editAccountMode.value, protocol)
|
||||
return
|
||||
}
|
||||
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, editAccountMode.value, protocol)
|
||||
})
|
||||
watch(editAccountMode, (mode) => {
|
||||
watch(editAccountMode, (mode, previousMode) => {
|
||||
if (!isCNApiKeyAccount.value || syncingForm.value) return
|
||||
// deepseek 无 coding 套餐:防御性回退(UI 已隐藏该选项)。
|
||||
const effectiveMode = props.account!.platform === 'deepseek' && mode === 'coding' ? 'payg' : mode
|
||||
@@ -2950,6 +2996,17 @@ watch(editAccountMode, (mode) => {
|
||||
editAccountMode.value = effectiveMode
|
||||
return
|
||||
}
|
||||
if (editApiProtocol.value === 'adaptive') {
|
||||
const previousDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, previousMode)
|
||||
const nextDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, mode)
|
||||
for (const item of editAdaptiveProtocolOptions.value) {
|
||||
if (!editAdaptiveBaseUrls.value[item.value] || editAdaptiveBaseUrls.value[item.value] === previousDefaults[item.value]) {
|
||||
editAdaptiveBaseUrls.value[item.value] = nextDefaults[item.value]
|
||||
}
|
||||
}
|
||||
editBaseUrl.value = editAdaptiveBaseUrls.value.chat_completions
|
||||
return
|
||||
}
|
||||
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, mode, editApiProtocol.value)
|
||||
})
|
||||
const cnProtocolDescKey = computed(
|
||||
@@ -3774,10 +3831,46 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
editAccountMode.value = credentials.account_mode === 'coding' ? 'coding' : 'payg'
|
||||
const storedProtocol = credentials.api_protocol
|
||||
editApiProtocol.value =
|
||||
storedProtocol === 'anthropic' || storedProtocol === 'responses' ? storedProtocol : 'chat_completions'
|
||||
storedProtocol === 'adaptive' ||
|
||||
storedProtocol === 'chat_completions' ||
|
||||
storedProtocol === 'anthropic' ||
|
||||
storedProtocol === 'responses'
|
||||
? storedProtocol
|
||||
: 'chat_completions'
|
||||
if (newAccount.platform !== 'deepseek' && editApiProtocol.value === 'responses') {
|
||||
editApiProtocol.value = 'chat_completions'
|
||||
}
|
||||
const adaptiveDefaults = defaultCNAdaptiveBaseUrls(newAccount.platform, editAccountMode.value)
|
||||
const storedBaseUrls = (credentials.api_base_urls as Record<string, unknown> | undefined) || {}
|
||||
const legacyBaseUrl = typeof credentials.base_url === 'string' ? credentials.base_url.trim() : ''
|
||||
const storedChatBaseUrl = typeof storedBaseUrls.chat_completions === 'string'
|
||||
? storedBaseUrls.chat_completions.trim()
|
||||
: ''
|
||||
const storedAnthropicBaseUrl = typeof storedBaseUrls.anthropic === 'string'
|
||||
? storedBaseUrls.anthropic.trim()
|
||||
: ''
|
||||
const storedResponsesBaseUrl = typeof storedBaseUrls.responses === 'string'
|
||||
? storedBaseUrls.responses.trim()
|
||||
: ''
|
||||
const nextAdaptiveBaseUrls: Record<CnNativeApiProtocol, string> = {
|
||||
chat_completions: storedChatBaseUrl || adaptiveDefaults.chat_completions,
|
||||
anthropic: storedAnthropicBaseUrl || adaptiveDefaults.anthropic,
|
||||
responses: storedResponsesBaseUrl || adaptiveDefaults.responses
|
||||
}
|
||||
const legacyProtocol: CnNativeApiProtocol = editApiProtocol.value === 'anthropic'
|
||||
? 'anthropic'
|
||||
: editApiProtocol.value === 'responses'
|
||||
? 'responses'
|
||||
: 'chat_completions'
|
||||
const storedLegacyBaseUrl = legacyProtocol === 'anthropic'
|
||||
? storedAnthropicBaseUrl
|
||||
: legacyProtocol === 'responses'
|
||||
? storedResponsesBaseUrl
|
||||
: storedChatBaseUrl
|
||||
if (legacyBaseUrl && !storedLegacyBaseUrl) {
|
||||
nextAdaptiveBaseUrls[legacyProtocol] = legacyBaseUrl
|
||||
}
|
||||
editAdaptiveBaseUrls.value = nextAdaptiveBaseUrls
|
||||
}
|
||||
const platformDefaultUrl =
|
||||
newAccount.platform === 'openai'
|
||||
@@ -3791,7 +3884,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
newAccount.platform === 'deepseek'
|
||||
? defaultCNBaseUrl(newAccount.platform, editAccountMode.value, editApiProtocol.value)
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
||||
editBaseUrl.value = isCNApiKeyAccount.value && editApiProtocol.value === 'adaptive'
|
||||
? editAdaptiveBaseUrls.value.chat_completions
|
||||
: (credentials.base_url as string) || platformDefaultUrl
|
||||
|
||||
// Load model mappings and detect mode
|
||||
loadModelRestrictionFromMapping(credentials.model_mapping as Record<string, unknown> | undefined)
|
||||
@@ -4463,6 +4558,17 @@ const handleSubmit = async () => {
|
||||
if (isCNApiKeyAccount.value) {
|
||||
newCredentials.account_mode = editAccountMode.value
|
||||
newCredentials.api_protocol = editApiProtocol.value
|
||||
if (editApiProtocol.value === 'adaptive') {
|
||||
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, editAccountMode.value)
|
||||
const protocolBaseUrls: Record<string, string> = {}
|
||||
for (const item of editAdaptiveProtocolOptions.value) {
|
||||
protocolBaseUrls[item.value] = (editAdaptiveBaseUrls.value[item.value] || defaults[item.value]).trim()
|
||||
}
|
||||
newCredentials.api_base_urls = protocolBaseUrls
|
||||
newCredentials.base_url = protocolBaseUrls.chat_completions
|
||||
} else {
|
||||
delete newCredentials.api_base_urls
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API key
|
||||
|
||||
@@ -110,6 +110,20 @@ const GroupSelectorStub = defineComponent({
|
||||
`,
|
||||
})
|
||||
|
||||
const ModelWhitelistSelectorStub = defineComponent({
|
||||
name: 'ModelWhitelistSelector',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
platform: String,
|
||||
syncCredentials: Object,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: '<div data-testid="model-whitelist-selector" />',
|
||||
})
|
||||
|
||||
function mountModal(groups: any[] = []) {
|
||||
return mount(CreateAccountModal, {
|
||||
props: { show: true, proxies: [], groups },
|
||||
@@ -124,7 +138,7 @@ function mountModal(groups: any[] = []) {
|
||||
ProxySelector: true,
|
||||
ProxyAdBanner: true,
|
||||
GroupSelector: GroupSelectorStub,
|
||||
ModelWhitelistSelector: true,
|
||||
ModelWhitelistSelector: ModelWhitelistSelectorStub,
|
||||
QuotaLimitCard: true,
|
||||
},
|
||||
},
|
||||
@@ -269,6 +283,43 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
|
||||
expect(probeUpstreamBillingMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('submits adaptive Kimi protocol endpoints', async () => {
|
||||
const wrapper = mountModal()
|
||||
await selectButtonByText(wrapper, 'Kimi')
|
||||
await wrapper.get('form#create-account-form input[type="text"]').setValue('Kimi adaptive')
|
||||
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-kimi')
|
||||
|
||||
await wrapper.get('form#create-account-form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(createAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(createAccountMock.mock.calls[0]?.[0]?.credentials).toMatchObject({
|
||||
account_mode: 'payg',
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://api.moonshot.cn/v1',
|
||||
api_base_urls: {
|
||||
chat_completions: 'https://api.moonshot.cn/v1',
|
||||
anthropic: 'https://api.moonshot.cn/anthropic'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the edited adaptive Chat endpoint when previewing upstream models', async () => {
|
||||
const wrapper = mountModal()
|
||||
await selectButtonByText(wrapper, 'Kimi')
|
||||
await wrapper
|
||||
.get('[data-testid="cn-adaptive-base-url-chat_completions"]')
|
||||
.setValue('https://relay.example.com/v1')
|
||||
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-relay')
|
||||
|
||||
expect(wrapper.getComponent(ModelWhitelistSelectorStub).props('syncCredentials')).toMatchObject({
|
||||
platform: 'kimi',
|
||||
type: 'apikey',
|
||||
base_url: 'https://relay.example.com/v1',
|
||||
api_key: 'sk-relay'
|
||||
})
|
||||
})
|
||||
|
||||
it('exposes Agent Identity in the OpenAI authorization methods', async () => {
|
||||
const wrapper = mountModal()
|
||||
await selectButtonByText(wrapper, 'OpenAI')
|
||||
|
||||
@@ -343,6 +343,178 @@ describe('EditAccountModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves adaptive GLM endpoints on submit', async () => {
|
||||
const account = buildAccount()
|
||||
account.platform = 'zhipu'
|
||||
account.credentials = {
|
||||
api_key: 'sk-glm',
|
||||
account_mode: 'coding',
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://open.bigmodel.cn/api/coding/paas/v4',
|
||||
api_base_urls: {
|
||||
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
|
||||
anthropic: 'https://open.bigmodel.cn/api/anthropic'
|
||||
}
|
||||
}
|
||||
updateAccountMock.mockReset().mockResolvedValue(account)
|
||||
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).toMatchObject({
|
||||
account_mode: 'coding',
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://open.bigmodel.cn/api/coding/paas/v4',
|
||||
api_base_urls: {
|
||||
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
|
||||
anthropic: 'https://open.bigmodel.cn/api/anthropic'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['explicit Chat Completions', 'chat_completions'],
|
||||
['legacy missing protocol', undefined]
|
||||
])('preserves a custom CN relay for %s accounts', async (_name, storedProtocol) => {
|
||||
const account = buildAccount()
|
||||
account.platform = 'zhipu'
|
||||
account.credentials = {
|
||||
api_key: 'sk-glm',
|
||||
account_mode: 'payg',
|
||||
base_url: 'https://relay.example.com/v1'
|
||||
}
|
||||
if (storedProtocol) {
|
||||
account.credentials.api_protocol = storedProtocol
|
||||
}
|
||||
updateAccountMock.mockReset().mockResolvedValue(account)
|
||||
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
const submittedCredentials = updateAccountMock.mock.calls[0]?.[1]?.credentials
|
||||
expect(submittedCredentials).toMatchObject({
|
||||
account_mode: 'payg',
|
||||
api_protocol: 'chat_completions',
|
||||
base_url: 'https://relay.example.com/v1'
|
||||
})
|
||||
expect(submittedCredentials).not.toHaveProperty('api_base_urls')
|
||||
})
|
||||
|
||||
it('uses the legacy base_url when adaptive endpoints are missing', async () => {
|
||||
const account = buildAccount()
|
||||
account.platform = 'zhipu'
|
||||
account.credentials = {
|
||||
api_key: 'sk-glm',
|
||||
account_mode: 'payg',
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://relay.example.com/v1',
|
||||
api_base_urls: {
|
||||
chat_completions: ' '
|
||||
}
|
||||
}
|
||||
updateAccountMock.mockReset().mockResolvedValue(account)
|
||||
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).toMatchObject({
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://relay.example.com/v1',
|
||||
api_base_urls: {
|
||||
chat_completions: 'https://relay.example.com/v1',
|
||||
anthropic: 'https://open.bigmodel.cn/api/anthropic'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('carries a fixed Chat relay into Adaptive when the user switches protocols', async () => {
|
||||
const account = buildAccount()
|
||||
account.platform = 'zhipu'
|
||||
account.credentials = {
|
||||
api_key: 'sk-glm',
|
||||
account_mode: 'payg',
|
||||
api_protocol: 'chat_completions',
|
||||
base_url: 'https://relay.example.com/v1'
|
||||
}
|
||||
updateAccountMock.mockReset().mockResolvedValue(account)
|
||||
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
const adaptiveButton = wrapper
|
||||
.findAll('button')
|
||||
.find(button => button.text().includes('admin.accounts.cnProviders.apiProtocol.adaptive'))
|
||||
expect(adaptiveButton).toBeDefined()
|
||||
await adaptiveButton!.trigger('click')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).toMatchObject({
|
||||
api_protocol: 'adaptive',
|
||||
base_url: 'https://relay.example.com/v1',
|
||||
api_base_urls: {
|
||||
chat_completions: 'https://relay.example.com/v1'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'Anthropic',
|
||||
platform: 'zhipu',
|
||||
protocol: 'anthropic',
|
||||
baseUrl: 'https://relay.example.com/anthropic',
|
||||
expectedBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
expectedProtocolUrls: {
|
||||
chat_completions: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
anthropic: 'https://relay.example.com/anthropic'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Responses',
|
||||
platform: 'deepseek',
|
||||
protocol: 'responses',
|
||||
baseUrl: 'https://relay.example.com/responses',
|
||||
expectedBaseUrl: 'https://api.deepseek.com',
|
||||
expectedProtocolUrls: {
|
||||
chat_completions: 'https://api.deepseek.com',
|
||||
anthropic: 'https://api.deepseek.com/anthropic',
|
||||
responses: 'https://relay.example.com/responses'
|
||||
}
|
||||
}
|
||||
])('keeps a fixed $name relay in its protocol slot when switching to Adaptive', async (testCase) => {
|
||||
const account = buildAccount()
|
||||
account.platform = testCase.platform
|
||||
account.credentials = {
|
||||
api_key: 'sk-cn',
|
||||
account_mode: 'payg',
|
||||
api_protocol: testCase.protocol,
|
||||
base_url: testCase.baseUrl
|
||||
}
|
||||
updateAccountMock.mockReset().mockResolvedValue(account)
|
||||
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
const adaptiveButton = wrapper
|
||||
.findAll('button')
|
||||
.find(button => button.text().includes('admin.accounts.cnProviders.apiProtocol.adaptive'))
|
||||
expect(adaptiveButton).toBeDefined()
|
||||
await adaptiveButton!.trigger('click')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).toMatchObject({
|
||||
api_protocol: 'adaptive',
|
||||
base_url: testCase.expectedBaseUrl,
|
||||
api_base_urls: testCase.expectedProtocolUrls
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves model mappings when editing the whitelist', async () => {
|
||||
const account = buildAccount()
|
||||
account.credentials.model_mapping = {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
|
||||
|
||||
describe('defaultCNAdaptiveBaseUrls', () => {
|
||||
it('resolves Kimi endpoints by account mode', () => {
|
||||
expect(defaultCNAdaptiveBaseUrls('kimi', 'payg')).toEqual({
|
||||
chat_completions: 'https://api.moonshot.cn/v1',
|
||||
anthropic: 'https://api.moonshot.cn/anthropic',
|
||||
responses: ''
|
||||
})
|
||||
expect(defaultCNAdaptiveBaseUrls('kimi', 'coding')).toEqual({
|
||||
chat_completions: 'https://api.kimi.com/coding/v1',
|
||||
anthropic: 'https://api.kimi.com/coding',
|
||||
responses: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves GLM endpoints by account mode', () => {
|
||||
expect(defaultCNAdaptiveBaseUrls('zhipu', 'payg')).toEqual({
|
||||
chat_completions: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
anthropic: 'https://open.bigmodel.cn/api/anthropic',
|
||||
responses: ''
|
||||
})
|
||||
expect(defaultCNAdaptiveBaseUrls('zhipu', 'coding')).toEqual({
|
||||
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
|
||||
anthropic: 'https://open.bigmodel.cn/api/anthropic',
|
||||
responses: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('includes all three native DeepSeek endpoints', () => {
|
||||
expect(defaultCNAdaptiveBaseUrls('deepseek', 'payg')).toEqual({
|
||||
chat_completions: 'https://api.deepseek.com',
|
||||
anthropic: 'https://api.deepseek.com/anthropic',
|
||||
responses: 'https://api.deepseek.com'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -256,8 +256,9 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
|
||||
|
||||
export type CnAccountMode = 'payg' | 'coding'
|
||||
|
||||
/** 仅 deepseek 支持 responses 协议(官方原生 /responses 端点,适配 Codex)。 */
|
||||
export type CnApiProtocol = 'chat_completions' | 'anthropic' | 'responses'
|
||||
/** 仅 deepseek 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
|
||||
export type CnApiProtocol = 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
|
||||
export type CnNativeApiProtocol = Exclude<CnApiProtocol, 'adaptive'>
|
||||
|
||||
export interface CnBaseUrlPreset {
|
||||
mode: CnAccountMode
|
||||
@@ -321,6 +322,18 @@ export function defaultCNBaseUrl(
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回自适应模式下需要配置的原生协议及其默认端点。 */
|
||||
export function defaultCNAdaptiveBaseUrls(
|
||||
platform: 'kimi' | 'zhipu' | 'deepseek',
|
||||
mode: CnAccountMode
|
||||
): Record<CnNativeApiProtocol, string> {
|
||||
return {
|
||||
chat_completions: defaultCNBaseUrl(platform, mode, 'chat_completions'),
|
||||
anthropic: defaultCNBaseUrl(platform, mode, 'anthropic'),
|
||||
responses: platform === 'deepseek' ? defaultCNBaseUrl(platform, mode, 'responses') : ''
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 国产供应商用量单元格可见性(单一事实源) =====
|
||||
// CNProviderQuotaCell / CNProviderBalanceCell 与 AccountUsageCell 的占位符判定
|
||||
// 共用,避免多处复制条件后一处改另一处漏改。
|
||||
|
||||
Reference in New Issue
Block a user