mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #6463 from DuskLin/feat/kimi-native-responses
feat(kimi): 原生转发 OpenAI Responses API
This commit is contained in:
@@ -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" // 按入站协议优先选择供应商原生端点
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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') : ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user