fix(accounts): harden adaptive protocol compatibility

This commit is contained in:
SavitarC
2026-08-20 13:57:25 +08:00
parent 85051616f9
commit b3092145db
15 changed files with 862 additions and 79 deletions
@@ -1281,7 +1281,12 @@
<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" />
<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">
@@ -4064,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
}
})
@@ -3831,27 +3831,48 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editAccountMode.value = credentials.account_mode === 'coding' ? 'coding' : 'payg'
const storedProtocol = credentials.api_protocol
editApiProtocol.value =
storedProtocol === 'adaptive' || storedProtocol === 'anthropic' || storedProtocol === 'responses'
storedProtocol === 'adaptive' ||
storedProtocol === 'chat_completions' ||
storedProtocol === 'anthropic' ||
storedProtocol === 'responses'
? storedProtocol
: 'adaptive'
: '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) || {}
editAdaptiveBaseUrls.value = {
chat_completions: typeof storedBaseUrls.chat_completions === 'string'
? storedBaseUrls.chat_completions
: adaptiveDefaults.chat_completions,
anthropic: typeof storedBaseUrls.anthropic === 'string'
? storedBaseUrls.anthropic
: adaptiveDefaults.anthropic,
responses: typeof storedBaseUrls.responses === 'string'
? storedBaseUrls.responses
: adaptiveDefaults.responses
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 =
const platformDefaultUrl =
newAccount.platform === 'openai'
? 'https://api.openai.com'
: newAccount.platform === 'gemini'
@@ -3863,9 +3884,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
newAccount.platform === 'deepseek'
? defaultCNBaseUrl(newAccount.platform, editAccountMode.value, editApiProtocol.value)
: 'https://api.anthropic.com'
editBaseUrl.value = isCNApiKeyAccount.value && editApiProtocol.value === 'adaptive'
? editAdaptiveBaseUrls.value.chat_completions
: (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)
@@ -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,
},
},
@@ -272,7 +286,6 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
it('submits adaptive Kimi protocol endpoints', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Kimi')
await selectButtonByText(wrapper, 'admin.accounts.cnProviders.apiProtocol.adaptive')
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')
@@ -291,6 +304,22 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
})
})
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')
@@ -374,6 +374,147 @@ describe('EditAccountModal', () => {
})
})
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 = {