Merge pull request #6463 from DuskLin/feat/kimi-native-responses

feat(kimi): 原生转发 OpenAI Responses API
This commit is contained in:
Wesley Liddick
2026-09-01 17:46:36 +08:00
committed by GitHub
20 changed files with 280 additions and 89 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ const (
const (
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
APIProtocolResponses = "responses" // OpenAI Responses(仅 deepseek,适配 Codex)
APIProtocolResponses = "responses" // OpenAI Responses(deepseek / kimi 原生端点,适配 Codex)
APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
)
+32 -3
View File
@@ -1387,8 +1387,8 @@ func (a *Account) IsCodingPlan() bool {
// GetAPIProtocol 返回国产供应商账号的上游 API 协议。存储于
// credentials["api_protocol"];缺失或与平台不匹配时回退 chat_completions
// (与既有行为完全一致)。responses 协议仅 deepseek 支持(官方原生 /responses
// 端点,适配 Codex);kimi/zhipu 无此端点。
// (与既有行为完全一致)。responses 协议仅 deepseek / kimi 支持(官方原生
// Responses 端点,适配 Codex);zhipu 无此端点。
func (a *Account) GetAPIProtocol() string {
if a == nil || !a.IsCNProvider() {
return APIProtocolChatCompletions
@@ -1399,7 +1399,7 @@ func (a *Account) GetAPIProtocol() string {
case APIProtocolAnthropic:
return APIProtocolAnthropic
case APIProtocolResponses:
if a.Platform == PlatformDeepseek {
if a.SupportsNativeCNResponses() {
return APIProtocolResponses
}
case APIProtocolChatCompletions:
@@ -1408,6 +1408,35 @@ func (a *Account) GetAPIProtocol() string {
return APIProtocolChatCompletions
}
// SupportsNativeCNResponses 报告该国产供应商是否提供原生 Responses 端点。
// DeepSeek 官方为 /responses(无 /v1);Kimi 按量付费与 Coding Plan 均为
// /v1/responses(moonshot.cn / kimi.com/coding)。
func (a *Account) SupportsNativeCNResponses() bool {
if a == nil {
return false
}
switch a.Platform {
case PlatformDeepseek, PlatformKimi:
return true
default:
return false
}
}
// UsesNativeCNResponses 报告当前账号是否应按原生 Responses 协议转发
// (显式 responses,或 adaptive 且平台具备原生端点)。
func (a *Account) UsesNativeCNResponses() bool {
if a == nil || !a.SupportsNativeCNResponses() {
return false
}
switch a.GetAPIProtocol() {
case APIProtocolResponses, APIProtocolAdaptive:
return true
default:
return false
}
}
// IsAdaptiveAPIProtocol 报告账号是否按入站协议动态选择供应商原生端点。
func (a *Account) IsAdaptiveAPIProtocol() bool {
return a.GetAPIProtocol() == APIProtocolAdaptive
@@ -18,8 +18,8 @@ import (
const accountTestSuppressCompletionContextKey = "account_test_suppress_completion"
// testCNProviderAdaptiveConnection verifies every native endpoint used by an
// adaptive CN-provider account. Kimi and Zhipu use Chat Completions plus
// Anthropic; DeepSeek additionally uses its native Responses endpoint.
// adaptive CN-provider account. Zhipu uses Chat Completions plus Anthropic;
// DeepSeek and Kimi additionally use their native Responses endpoints.
func (s *AccountTestService) testCNProviderAdaptiveConnection(c *gin.Context, account *Account, modelID string, prompt string) error {
testModelID := strings.TrimSpace(modelID)
if testModelID == "" {
@@ -44,7 +44,7 @@ func (s *AccountTestService) testCNProviderAdaptiveConnection(c *gin.Context, ac
return err
}
if account.Platform == PlatformDeepseek {
if account.SupportsNativeCNResponses() {
if err := s.testCNProviderAdaptiveResponsesConnection(c, account, testModelID, authToken); err != nil {
return err
}
@@ -159,8 +159,8 @@ func (s *AccountTestService) testCNProviderAdaptiveResponsesConnection(c *gin.Co
apiURL := buildOpenAIResponsesURLForPlatform(account.Platform, baseURL)
payload := createOpenAITestPayload(testModelID, false)
// DeepSeek's native Responses endpoint is stateless and does not need the
// OpenAI probe's synthetic instructions.
// DeepSeek / Kimi native Responses endpoints are stateless and do not need
// the OpenAI probe's synthetic instructions.
delete(payload, "instructions")
payloadBytes, _ := json.Marshal(payload)
payloadBytes = normalizeDeepSeekResponsesRequestBody(account, payloadBytes)
@@ -83,36 +83,25 @@ data: {"type":"response.completed"}
}
func TestAccountTestService_AdaptiveChatOnlyProvidersTestChatAndAnthropicEndpoints(t *testing.T) {
for index, testCase := range []struct {
name string
platform string
model string
}{
{name: "Kimi", platform: PlatformKimi, model: "kimi-k2.5"},
{name: "Zhipu", platform: PlatformZhipu, model: "glm-4.7"},
} {
t.Run(testCase.name, func(t *testing.T) {
account := adaptiveCNAccountTestAccount(int64(301+index), testCase.platform)
svc, upstream := adaptiveCNAccountTestService(
account,
adaptiveCNChatTestResponse(),
adaptiveCNAnthropicTestResponse(),
)
c, recorder := newTestContext()
account := adaptiveCNAccountTestAccount(301, PlatformZhipu)
svc, upstream := adaptiveCNAccountTestService(
account,
adaptiveCNChatTestResponse(),
adaptiveCNAnthropicTestResponse(),
)
c, recorder := newTestContext()
err := svc.TestAccountConnection(c, account.ID, testCase.model, "hello", AccountTestModeDefault)
err := svc.TestAccountConnection(c, account.ID, "glm-4.7", "hello", AccountTestModeDefault)
require.NoError(t, err)
require.Len(t, upstream.requests, 2)
require.Equal(t, "http://chat.example/v1/chat/completions", upstream.requests[0].URL.String())
require.Equal(t, "http://anthropic.example/v1/messages", upstream.requests[1].URL.String())
require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[0].Header.Get("Authorization"))
require.Equal(t, "sk-adaptive-test", upstream.requests[1].Header.Get("x-api-key"))
require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_start"`))
require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
require.Contains(t, recorder.Body.String(), "已通过原生 /v1/messages 验证")
})
}
require.NoError(t, err)
require.Len(t, upstream.requests, 2)
require.Equal(t, "http://chat.example/v1/chat/completions", upstream.requests[0].URL.String())
require.Equal(t, "http://anthropic.example/v1/messages", upstream.requests[1].URL.String())
require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[0].Header.Get("Authorization"))
require.Equal(t, "sk-adaptive-test", upstream.requests[1].Header.Get("x-api-key"))
require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_start"`))
require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
require.Contains(t, recorder.Body.String(), "已通过原生 /v1/messages 验证")
}
func TestAccountTestService_AdaptiveDeepSeekAlsoTestsResponsesEndpoint(t *testing.T) {
@@ -139,6 +128,30 @@ func TestAccountTestService_AdaptiveDeepSeekAlsoTestsResponsesEndpoint(t *testin
require.Contains(t, recorder.Body.String(), "已通过原生 /responses 验证")
}
func TestAccountTestService_AdaptiveKimiAlsoTestsResponsesEndpoint(t *testing.T) {
account := adaptiveCNAccountTestAccount(306, PlatformKimi)
svc, upstream := adaptiveCNAccountTestService(
account,
adaptiveCNChatTestResponse(),
adaptiveCNAnthropicTestResponse(),
adaptiveCNResponsesTestResponse(),
)
c, recorder := newTestContext()
err := svc.TestAccountConnection(c, account.ID, "k3-256k", "", AccountTestModeDefault)
require.NoError(t, err)
require.Len(t, upstream.requests, 3)
require.Equal(t, "http://responses.example/v1/responses", upstream.requests[2].URL.String())
require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.requests[2].Context()))
require.Equal(t, "Bearer sk-adaptive-test", upstream.requests[2].Header.Get("Authorization"))
require.True(t, gjson.GetBytes(upstream.bodies[2], "stream").Bool())
require.False(t, gjson.GetBytes(upstream.bodies[2], "store").Bool())
require.False(t, gjson.GetBytes(upstream.bodies[2], "instructions").Exists())
require.Equal(t, 1, strings.Count(recorder.Body.String(), `"type":"test_complete"`))
require.Contains(t, recorder.Body.String(), "已通过原生 /responses 验证")
}
func TestAccountTestService_AdaptiveStopsAndNamesFailingEndpoint(t *testing.T) {
account := adaptiveCNAccountTestAccount(303, PlatformDeepseek)
svc, upstream := adaptiveCNAccountTestService(
@@ -116,10 +116,10 @@ func TestAdaptiveProtocolRoutesResponsesShapedChatToNativeResponses(t *testing.T
func TestAdaptiveProtocolConvertsResponsesShapedChatForChatOnlyProvider(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"kimi-k2.5","input":"hello","max_output_tokens":32,"stream":false}`)
body := []byte(`{"model":"glm-4.7","input":"hello","max_output_tokens":32,"stream":false}`)
upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
APIProtocolChatCompletions: "http://chat.example",
APIProtocolAnthropic: "http://anthropic.example",
})
@@ -131,6 +131,24 @@ func TestAdaptiveProtocolConvertsResponsesShapedChatForChatOnlyProvider(t *testi
require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
}
func TestAdaptiveProtocolRoutesKimiResponsesShapedChatToNativeResponses(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"k3-256k","input":"hello","max_output_tokens":32,"stream":false}`)
upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
APIProtocolChatCompletions: "http://chat.example",
APIProtocolAnthropic: "http://anthropic.example",
APIProtocolResponses: "http://responses.example/v1",
})
_, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", body), account, body, "", "")
require.Error(t, err)
require.Equal(t, "http://responses.example/v1/responses", upstream.lastReq.URL.String())
require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
}
func TestAdaptiveProtocolRoutesMessagesToNativeAnthropic(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"glm-4.7","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
@@ -147,21 +165,42 @@ func TestAdaptiveProtocolRoutesMessagesToNativeAnthropic(t *testing.T) {
require.Equal(t, "glm-4.7", gjson.GetBytes(upstream.lastBody, "model").String())
}
func TestAdaptiveProtocolConvertsKimiResponsesToChatCompletions(t *testing.T) {
func TestAdaptiveProtocolRoutesKimiResponsesToNativeResponses(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"kimi-k2.5","input":"hello","stream":false}`)
body := []byte(`{"model":"k3-256k","input":"hello","store":true,"previous_response_id":"resp_old","stream":false}`)
upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
APIProtocolChatCompletions: "http://chat.example",
APIProtocolAnthropic: "http://anthropic.example",
APIProtocolResponses: "http://responses.example/v1",
})
_, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
require.Error(t, err)
require.Equal(t, "http://chat.example/v1/chat/completions", upstream.lastReq.URL.String())
require.True(t, gjson.GetBytes(upstream.lastBody, "messages").IsArray())
require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
require.Equal(t, "http://responses.example/v1/responses", upstream.lastReq.URL.String())
require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
require.False(t, gjson.GetBytes(upstream.lastBody, "store").Bool())
require.False(t, gjson.GetBytes(upstream.lastBody, "previous_response_id").Exists())
}
func TestAdaptiveProtocolRoutesKimiCodingResponsesToNativeResponses(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"k3-256k","input":"hello","stream":false}`)
upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
account := adaptiveProtocolTestAccount(PlatformKimi, map[string]any{
APIProtocolChatCompletions: "https://api.kimi.com/coding/v1",
APIProtocolAnthropic: "https://api.kimi.com/coding",
APIProtocolResponses: "https://api.kimi.com/coding/v1",
})
account.Credentials["account_mode"] = AccountModeCoding
_, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
require.Error(t, err)
require.Equal(t, "https://api.kimi.com/coding/v1/responses", upstream.lastReq.URL.String())
require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
}
func TestAdaptiveProtocolRoutesDeepSeekResponsesToNativeResponses(t *testing.T) {
+30 -3
View File
@@ -469,7 +469,7 @@ func TestBuildUpstreamModelsRequest_CNProviders(t *testing.T) {
}
// TestGetAPIProtocol 验证协议凭证维度的平台校验矩阵:
// responses 仅 deepseek;缺失/非法值回退 chat_completions(与旧行为一致)。
// responses 仅 deepseek / kimi;缺失/非法值回退 chat_completions(与旧行为一致)。
func TestGetAPIProtocol(t *testing.T) {
t.Parallel()
@@ -486,15 +486,24 @@ func TestGetAPIProtocol(t *testing.T) {
require.Equal(t, APIProtocolAnthropic, mk(PlatformKimi, APIProtocolAnthropic).GetAPIProtocol())
require.Equal(t, APIProtocolAnthropic, mk(PlatformDeepseek, APIProtocolAnthropic).GetAPIProtocol())
require.Equal(t, APIProtocolResponses, mk(PlatformDeepseek, APIProtocolResponses).GetAPIProtocol())
require.Equal(t, APIProtocolResponses, mk(PlatformKimi, APIProtocolResponses).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformKimi, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformZhipu, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolAdaptive, mk(PlatformDeepseek, APIProtocolAdaptive).GetAPIProtocol())
require.Equal(t, APIProtocolChatCompletions, mk(PlatformKimi, APIProtocolResponses).GetAPIProtocol(), "kimi 无 responses 端点")
require.Equal(t, APIProtocolChatCompletions, mk(PlatformZhipu, APIProtocolResponses).GetAPIProtocol(), "zhipu 无 responses 端点")
require.Equal(t, APIProtocolChatCompletions, mk(PlatformKimi, "bogus").GetAPIProtocol(), "非法值回退默认")
require.Equal(t, APIProtocolChatCompletions, (&Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}).GetAPIProtocol(), "非 CN 供应商恒为默认")
}
func TestSupportsNativeCNResponses(t *testing.T) {
t.Parallel()
require.True(t, (&Account{Platform: PlatformDeepseek}).SupportsNativeCNResponses())
require.True(t, (&Account{Platform: PlatformKimi}).SupportsNativeCNResponses())
require.True(t, (&Account{Platform: PlatformKimi, Credentials: map[string]any{"account_mode": AccountModeCoding}}).SupportsNativeCNResponses())
require.False(t, (&Account{Platform: PlatformZhipu}).SupportsNativeCNResponses())
require.False(t, (&Account{Platform: PlatformOpenAI}).SupportsNativeCNResponses())
}
func TestAdaptiveProtocolBaseURLs(t *testing.T) {
t.Parallel()
@@ -639,10 +648,12 @@ func TestBuildOpenAIResponsesURLForPlatform(t *testing.T) {
require.Equal(t, "https://relay.example.com/v1/responses", buildOpenAIResponsesURLForPlatform(PlatformDeepseek, "https://relay.example.com/v1"))
require.Equal(t, "https://api.openai.com/v1/responses", buildOpenAIResponsesURLForPlatform(PlatformOpenAI, "https://api.openai.com"))
require.Equal(t, "https://open.bigmodel.cn/api/paas/v4/responses", buildOpenAIResponsesURLForPlatform(PlatformZhipu, "https://open.bigmodel.cn/api/paas/v4"))
require.Equal(t, "https://api.moonshot.cn/v1/responses", buildOpenAIResponsesURLForPlatform(PlatformKimi, "https://api.moonshot.cn/v1"))
require.Equal(t, "https://api.kimi.com/coding/v1/responses", buildOpenAIResponsesURLForPlatform(PlatformKimi, "https://api.kimi.com/coding/v1"))
}
// TestNormalizeDeepSeekResponsesRequestBody 无状态适配:强制 store=false、
// 清除 previous_response_id;非 deepseek responses 协议原样返回。
// 清除 previous_response_id;非原生 CN Responses 协议原样返回。
func TestNormalizeDeepSeekResponsesRequestBody(t *testing.T) {
t.Parallel()
@@ -668,6 +679,22 @@ func TestNormalizeDeepSeekResponsesRequestBody(t *testing.T) {
deepseekCC := &Account{Platform: PlatformDeepseek, Type: AccountTypeAPIKey}
require.Equal(t, string(body), string(normalizeDeepSeekResponsesRequestBody(deepseekCC, body)))
kimiResponses := &Account{
Platform: PlatformKimi, Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_protocol": APIProtocolResponses},
}
kimiNormalized := normalizeDeepSeekResponsesRequestBody(kimiResponses, body)
require.False(t, gjson.GetBytes(kimiNormalized, "store").Bool())
require.False(t, gjson.GetBytes(kimiNormalized, "previous_response_id").Exists())
kimiCodingAdaptive := &Account{
Platform: PlatformKimi, Type: AccountTypeAPIKey,
Credentials: map[string]any{"api_protocol": APIProtocolAdaptive, "account_mode": AccountModeCoding},
}
kimiCodingNormalized := normalizeDeepSeekResponsesRequestBody(kimiCodingAdaptive, body)
require.False(t, gjson.GetBytes(kimiCodingNormalized, "store").Bool())
require.False(t, gjson.GetBytes(kimiCodingNormalized, "previous_response_id").Exists())
// openai 账号原样返回
openai := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
require.Equal(t, string(body), string(normalizeDeepSeekResponsesRequestBody(openai, body)))
@@ -123,12 +123,12 @@ func (s *AccountTestService) ProbeOpenAIAPIKeyResponsesSupport(ctx context.Conte
return
}
if account.IsCNProvider() {
// 国产 OpenAI 兼容上游(kimi/zhipu/deepseek)普遍仅支持 /v1/chat/completions,
// 不存在 /v1/responses 端点。直接落标 false 走 Chat Completions 直转,跳过网络探测。
// 例外:deepseek 的固定 responses 和 adaptive 账号使用官方原生 /responses
// 端点,落标 force_responses;其余协议显式重置为 auto,避免切换后残留强制模式。
if account.GetAPIProtocol() == APIProtocolResponses ||
(account.Platform == PlatformDeepseek && account.IsAdaptiveAPIProtocol()) {
// 国产 OpenAI 兼容上游默认仅支持 /v1/chat/completions。直接落标 false
// 走 Chat Completions 直转,跳过网络探测。
// 例外:deepseek / kimi 的固定 responses 和 adaptive 账号使用官方原生
// Responses 端点,落标 force_responses;其余协议显式重置为 auto,避免
// 切换后残留强制模式。
if account.UsesNativeCNResponses() {
_ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{
openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceResponses),
openai_compat.ExtraKeyResponsesSupported: true,
@@ -60,7 +60,8 @@ func TestProbeOpenAIAPIKeyResponsesSupportCNProviders(t *testing.T) {
}{
{name: "deepseek adaptive supports responses", id: 201, platform: PlatformDeepseek, protocol: APIProtocolAdaptive, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
{name: "deepseek chat clears forced responses", id: 202, platform: PlatformDeepseek, protocol: APIProtocolChatCompletions, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
{name: "kimi adaptive falls back to chat", id: 203, platform: PlatformKimi, protocol: APIProtocolAdaptive, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
{name: "kimi adaptive supports responses", id: 203, platform: PlatformKimi, protocol: APIProtocolAdaptive, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
{name: "kimi responses protocol supports responses", id: 205, platform: PlatformKimi, protocol: APIProtocolResponses, wantSupport: true, wantMode: string(openai_compat.ResponsesSupportModeForceResponses)},
{name: "zhipu adaptive falls back to chat", id: 204, platform: PlatformZhipu, protocol: APIProtocolAdaptive, wantSupport: false, wantMode: string(openai_compat.ResponsesSupportModeAuto)},
}
@@ -102,13 +102,13 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
isResponsesShape := !gjson.GetBytes(body, "messages").Exists() && gjson.GetBytes(body, "input").Exists()
// 自适应账号的标准 Chat Completions 入站使用供应商原生 CC 端点。
// Responses 形状下,DeepSeek 继续走下方原生 Responses 链;Kimi/GLM
// Responses 形状下,DeepSeek / Kimi 继续走下方原生 Responses 链;GLM
// 没有 Responses 端点,先转换成 Chat Completions 再直转。
if account.IsAdaptiveAPIProtocol() {
if !isResponsesShape {
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
}
if account.Platform != PlatformDeepseek {
if !account.SupportsNativeCNResponses() {
var responsesReq apicompat.ResponsesRequest
if err := json.Unmarshal(body, &responsesReq); err != nil {
return nil, fmt.Errorf("parse responses-shaped chat completions request: %w", err)
@@ -126,7 +126,7 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
}
return s.forwardAsRawChatCompletions(ctx, c, account, chatBody, defaultMappedModel)
}
// DeepSeek 原生 Responses 请求继续走下方 Responses→Chat 回程转换。
// DeepSeek / Kimi 原生 Responses 请求继续走下方 Responses→Chat 回程转换。
}
// 入口分流(国产供应商 Anthropic 协议):上游为供应商原生 Anthropic 端点,
@@ -39,7 +39,8 @@ var openaiCCRawAllowedHeaders = map[string]bool{
// `{base_url}/v1/chat/completions`,**不**做 CC↔Responses 协议转换。
//
// 适用场景:account.platform=openai && account.type=apikey && 上游已被探测确认
// 不支持 /v1/responses 端点(如 DeepSeek/Kimi/GLM/Qwen 等第三方 OpenAI 兼容上游)。
// 不支持 /v1/responses 端点(如 GLM/Qwen 等第三方 OpenAI 兼容上游);CN 供应商
// 固定 chat_completions 协议也走此路径。
//
// 与 ForwardAsChatCompletions 的关键差异:
//
@@ -133,8 +133,8 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
}
}
nativeDeepSeekResponses := account.Platform == PlatformDeepseek &&
(account.GetAPIProtocol() == APIProtocolResponses || account.IsAdaptiveAPIProtocol())
nativeCNResponses := account.UsesNativeCNResponses()
nativeDeepSeekResponses := account.Platform == PlatformDeepseek && nativeCNResponses
if nativeDeepSeekResponses && account.Type == AccountTypeAPIKey && !compactPath &&
needsOpenAIResponsesClientToolAdaptation(body) {
adaptedBody, mapping, adaptErr := adaptOpenAIResponsesClientTools(body)
@@ -360,7 +360,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
instructions := gjson.GetBytes(body, "instructions")
instructionsEmpty := !instructions.Exists() || instructions.Type != gjson.String || strings.TrimSpace(instructions.String()) == ""
if instructionsEmpty && account.UsesOpenAICodexProtocol() && !compatMessagesBridge && !nativeDeepSeekResponses {
if instructionsEmpty && account.UsesOpenAICodexProtocol() && !compatMessagesBridge && !nativeCNResponses {
markPatchSet("instructions", defaultCodexSynthInstructions(reqModel))
}
@@ -1268,13 +1268,13 @@ func shouldForwardOpenAIResponsesViaRawChatCompletions(account *Account) bool {
return false
}
if account.IsCNProvider() {
// CN 的显式协议配置优先于异步探针 Extra;adaptive 仅 DeepSeek 有原生
// Responses,Kimi/GLM 回退 Chat Completions。
// CN 的显式协议配置优先于异步探针 Extra;adaptive 仅 DeepSeek / Kimi
// 有原生 Responses,GLM 回退 Chat Completions。
switch account.GetAPIProtocol() {
case APIProtocolChatCompletions:
return true
case APIProtocolAdaptive:
return account.Platform != PlatformDeepseek
return !account.SupportsNativeCNResponses()
default:
return false
}
@@ -1298,7 +1298,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
case AccountTypeAPIKey:
// API Key accounts use Platform API or custom base URL
baseURL := account.GetOpenAIBaseURL()
if account.Platform == PlatformDeepseek && account.IsAdaptiveAPIProtocol() {
if account.UsesNativeCNResponses() && account.IsAdaptiveAPIProtocol() {
baseURL = account.GetCNProtocolBaseURL(APIProtocolResponses)
}
if baseURL == "" {
@@ -1315,7 +1315,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
}
targetURL = appendOpenAIResponsesRequestPathSuffix(targetURL, openAIResponsesRequestPathSuffix(c))
// DeepSeek 原生 Responses 端点为无状态实现:强制 store=false、清除
// DeepSeek / Kimi 原生 Responses 端点为无状态实现:强制 store=false、清除
// previous_response_id,避免携带状态字段被上游拒绝。
body = normalizeDeepSeekResponsesRequestBody(account, body)
@@ -591,6 +591,9 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
}
case AccountTypeAPIKey:
baseURL := account.GetOpenAIBaseURL()
if account.UsesNativeCNResponses() && account.IsAdaptiveAPIProtocol() {
baseURL = account.GetCNProtocolBaseURL(APIProtocolResponses)
}
if baseURL != "" {
validatedURL, err := s.validateUpstreamBaseURL(baseURL)
if err != nil {
@@ -601,7 +604,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
}
targetURL = appendOpenAIResponsesRequestPathSuffix(targetURL, openAIResponsesRequestPathSuffix(c))
// DeepSeek 原生 Responses 端点为无状态实现(见 normalizeDeepSeekResponsesRequestBody)。
// DeepSeek / Kimi 原生 Responses 端点为无状态实现(见 normalizeDeepSeekResponsesRequestBody)。
body = normalizeDeepSeekResponsesRequestBody(account, body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
@@ -118,12 +118,12 @@ func deleteOpenAIResponsesNoneReasoningEffortFromObject(account *Account, body m
}
}
// normalizeDeepSeekResponsesRequestBody 适配 DeepSeek 无状态 Responses 端点:
// 强制 store=false 并清除 previous_response_id(官方 /responses 不支持服务端
// 状态存储,携带这些字段会被拒绝)。非 deepseek responses 协议账号原样返回。
// normalizeDeepSeekResponsesRequestBody 适配无状态 CN Responses 端点:
// 强制 store=false 并清除 previous_response_id(DeepSeek / Kimi 官方
// Responses 均不支持服务端状态存储,携带这些字段会被拒绝)。
// 非原生 Responses 协议账号原样返回。
func normalizeDeepSeekResponsesRequestBody(account *Account, body []byte) []byte {
if account == nil || account.Platform != PlatformDeepseek ||
(account.GetAPIProtocol() != APIProtocolResponses && !account.IsAdaptiveAPIProtocol()) {
if account == nil || !account.UsesNativeCNResponses() {
return body
}
normalized, err := sjson.SetBytes(body, "store", false)
@@ -41,6 +41,9 @@ func (s *OpenAIGatewayService) buildOpenAIResponsesWSURL(account *Account) (stri
}
case AccountTypeAPIKey:
baseURL := account.GetOpenAIBaseURL()
if account.UsesNativeCNResponses() && account.IsAdaptiveAPIProtocol() {
baseURL = account.GetCNProtocolBaseURL(APIProtocolResponses)
}
if baseURL == "" {
targetURL = openaiPlatformAPIURL
} else {
@@ -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') : ''
}
}