mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
fix(accounts): harden adaptive protocol compatibility
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user