feat(kimi): forward native OpenAI Responses API

Moonshot pay-as-you-go and Kimi For Coding both expose native
/v1/responses. Stop converting Kimi Responses traffic to Chat
Completions, and treat Kimi like DeepSeek for protocol selection while
keeping /v1/responses URL construction.
This commit is contained in:
刘佳林
2026-09-01 16:50:04 +08:00
parent a2fb09260a
commit e377c4358c
20 changed files with 280 additions and 89 deletions
@@ -1319,7 +1319,7 @@
/>
</div>
</div>
<p v-if="form.platform !== 'deepseek'" class="input-hint">
<p v-if="!cnSupportsNativeResponses(form.platform)" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
@@ -3814,6 +3814,7 @@ import {
applyAntigravityProjectID,
applyHeaderOverride,
applyInterceptWarmup,
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
isHeaderOverrideCapable,
@@ -4006,7 +4007,7 @@ const upstreamBillingAutoProbeEnabled = ref(true)
// ── 国产供应商(Kimi / Zhipu / DeepSeek)账号类型、API 协议与端点 ──
const accountMode = ref<CnAccountMode>('payg')
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
// responses=deepseek / kimi 原生 Responses 端点(Codex)。与账号类型正交。
const apiProtocol = ref<CnApiProtocol>('adaptive')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const zhipuOrganization = ref('')
@@ -4027,14 +4028,14 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
}
return 'kimi'
})
// 当前平台可选的协议档(responses 仅 deepseek)。
// 当前平台可选的协议档(responses 仅 deepseek / kimi)。
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' }
]
if (form.platform === 'deepseek') {
if (cnSupportsNativeResponses(form.platform)) {
opts.push({ value: 'responses', labelKey: 'responses' })
}
return opts
@@ -4044,7 +4045,7 @@ const cnAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; l
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (form.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
if (cnSupportsNativeResponses(form.platform)) opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
@@ -72,7 +72,7 @@
<input v-model="editAdaptiveBaseUrls[item.value]" type="text" class="input" />
</div>
</div>
<p v-if="account.platform !== 'deepseek'" class="input-hint">
<p v-if="!cnSupportsNativeResponses(account.platform)" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
@@ -2909,6 +2909,7 @@ import {
isHeaderOverrideCapable,
splitHeaderOverridesObject,
validateHeaderOverrideRows,
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
@@ -3057,7 +3058,7 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (props.account?.platform === 'deepseek') {
if (cnSupportsNativeResponses(props.account?.platform ?? '')) {
opts.push({ value: 'responses', labelKey: 'responses' })
}
return opts
@@ -3067,7 +3068,7 @@ const editAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol;
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (props.account?.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
if (cnSupportsNativeResponses(props.account?.platform ?? '')) opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
watch(editApiProtocol, (protocol, previousProtocol) => {
@@ -3947,7 +3948,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
storedProtocol === 'responses'
? storedProtocol
: 'chat_completions'
if (newAccount.platform !== 'deepseek' && editApiProtocol.value === 'responses') {
if (!cnSupportsNativeResponses(newAccount.platform) && editApiProtocol.value === 'responses') {
editApiProtocol.value = 'chat_completions'
}
const adaptiveDefaults = defaultCNAdaptiveBaseUrls(newAccount.platform, editAccountMode.value)
@@ -375,7 +375,31 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
base_url: 'https://api.moonshot.cn/v1',
api_base_urls: {
chat_completions: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic'
anthropic: 'https://api.moonshot.cn/anthropic',
responses: 'https://api.moonshot.cn/v1'
}
})
})
it('submits adaptive Kimi Coding Plan Responses endpoint', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Kimi')
await selectButtonByText(wrapper, 'admin.accounts.cnProviders.accountMode.coding')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Kimi coding')
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-kimi-coding')
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: 'coding',
api_protocol: 'adaptive',
base_url: 'https://api.kimi.com/coding/v1',
api_base_urls: {
chat_completions: 'https://api.kimi.com/coding/v1',
anthropic: 'https://api.kimi.com/coding',
responses: 'https://api.kimi.com/coding/v1'
}
})
})
@@ -355,6 +355,39 @@ describe('EditAccountModal', () => {
})
})
it('preserves adaptive Kimi Responses endpoint on submit', async () => {
const account = buildAccount()
account.platform = 'kimi'
account.credentials = {
api_key: 'sk-kimi',
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',
responses: 'https://api.moonshot.cn/v1'
}
}
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: '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',
responses: 'https://api.moonshot.cn/v1'
}
})
})
it('preserves adaptive GLM endpoints on submit', async () => {
const account = buildAccount()
account.platform = 'zhipu'
@@ -1,18 +1,27 @@
import { describe, expect, it } from 'vitest'
import { defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
import { cnSupportsNativeResponses, defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
describe('cnSupportsNativeResponses', () => {
it('is true for DeepSeek and Kimi only', () => {
expect(cnSupportsNativeResponses('deepseek')).toBe(true)
expect(cnSupportsNativeResponses('kimi')).toBe(true)
expect(cnSupportsNativeResponses('zhipu')).toBe(false)
expect(cnSupportsNativeResponses('openai')).toBe(false)
})
})
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: ''
responses: 'https://api.moonshot.cn/v1'
})
expect(defaultCNAdaptiveBaseUrls('kimi', 'coding')).toEqual({
chat_completions: 'https://api.kimi.com/coding/v1',
anthropic: 'https://api.kimi.com/coding',
responses: ''
responses: 'https://api.kimi.com/coding/v1'
})
})
@@ -256,10 +256,15 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
export type CnAccountMode = 'payg' | 'coding'
/** 仅 deepseek 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
/** deepseek / kimi 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
export type CnApiProtocol = 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
export type CnNativeApiProtocol = Exclude<CnApiProtocol, 'adaptive'>
/** DeepSeek 与 Kimi(按量付费 / Coding Plan)提供原生 Responses 端点。 */
export function cnSupportsNativeResponses(platform: string): boolean {
return platform === 'deepseek' || platform === 'kimi'
}
export interface CnBaseUrlPreset {
mode: CnAccountMode
protocol: CnApiProtocol
@@ -273,8 +278,10 @@ export const CN_BASE_URL_PRESETS: Record<'kimi' | 'zhipu' | 'deepseek', CnBaseUr
kimi: [
{ mode: 'payg', protocol: 'chat_completions', label: 'Moonshot', url: 'https://api.moonshot.cn/v1' },
{ mode: 'payg', protocol: 'anthropic', label: 'Moonshot Anthropic', url: 'https://api.moonshot.cn/anthropic' },
{ mode: 'payg', protocol: 'responses', label: 'Moonshot Responses', url: 'https://api.moonshot.cn/v1' },
{ mode: 'coding', protocol: 'chat_completions', label: 'Kimi For Coding', url: 'https://api.kimi.com/coding/v1' },
{ mode: 'coding', protocol: 'anthropic', label: 'Kimi Coding Anthropic', url: 'https://api.kimi.com/coding' }
{ mode: 'coding', protocol: 'anthropic', label: 'Kimi Coding Anthropic', url: 'https://api.kimi.com/coding' },
{ mode: 'coding', protocol: 'responses', label: 'Kimi Coding Responses', url: 'https://api.kimi.com/coding/v1' }
],
zhipu: [
{ mode: 'payg', protocol: 'chat_completions', label: 'GLM PaaS', url: 'https://open.bigmodel.cn/api/paas/v4' },
@@ -307,7 +314,7 @@ export function defaultCNBaseUrl(
return ''
}
}
// responses 仅 deepseek:base 与 chat_completions 相同(端点路径差异由后端处理)。
// responses:Kimi / DeepSeek 的 base 与 chat_completions 相同(端点路径差异由后端处理)。
switch (platform) {
case 'kimi':
return mode === 'coding' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1'
@@ -330,7 +337,7 @@ export function defaultCNAdaptiveBaseUrls(
return {
chat_completions: defaultCNBaseUrl(platform, mode, 'chat_completions'),
anthropic: defaultCNBaseUrl(platform, mode, 'anthropic'),
responses: platform === 'deepseek' ? defaultCNBaseUrl(platform, mode, 'responses') : ''
responses: cnSupportsNativeResponses(platform) ? defaultCNBaseUrl(platform, mode, 'responses') : ''
}
}