From 85051616f9e47e6e7668a0231ee4b3598bd23dcf Mon Sep 17 00:00:00 2001
From: SavitarC <71271200+SavitarC@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:30:08 +0800
Subject: [PATCH] feat(accounts): add adaptive API protocol routing
---
backend/internal/domain/constants.go | 1 +
backend/internal/service/account.go | 74 ++++++++++-
.../internal/service/account_test_service.go | 3 +-
.../service/adaptive_api_protocol_test.go | 124 ++++++++++++++++++
backend/internal/service/cn_providers_test.go | 78 +++++++++--
backend/internal/service/domain_constants.go | 1 +
.../service/openai_apikey_responses_probe.go | 3 +-
.../openai_apikey_responses_probe_test.go | 37 ++++++
.../openai_gateway_chat_completions.go | 5 +
.../service/openai_gateway_forward.go | 7 +
.../service/openai_gateway_messages.go | 2 +-
.../service/openai_gateway_request_body.go | 3 +-
.../components/account/CnBaseUrlPresets.vue | 2 +-
.../components/account/CreateAccountModal.vue | 80 +++++++++--
.../components/account/EditAccountModal.vue | 99 +++++++++++++-
.../__tests__/CreateAccountModal.spec.ts | 22 ++++
.../__tests__/EditAccountModal.spec.ts | 31 +++++
.../credentialsBuilder.cnAdaptive.spec.ts | 39 ++++++
.../components/account/credentialsBuilder.ts | 17 ++-
.../src/i18n/locales/en/admin/accounts.ts | 4 +
.../src/i18n/locales/zh/admin/accounts.ts | 4 +
21 files changed, 605 insertions(+), 31 deletions(-)
create mode 100644 backend/internal/service/adaptive_api_protocol_test.go
create mode 100644 frontend/src/components/account/__tests__/credentialsBuilder.cnAdaptive.spec.ts
diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go
index 3640d1b82e..cd821165a4 100644
--- a/backend/internal/domain/constants.go
+++ b/backend/internal/domain/constants.go
@@ -44,6 +44,7 @@ const (
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
APIProtocolResponses = "responses" // OpenAI Responses(仅 deepseek,适配 Codex)
+ APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
)
// Account type constants
diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go
index cd66ef89ce..20d4ec5823 100644
--- a/backend/internal/service/account.go
+++ b/backend/internal/service/account.go
@@ -1310,6 +1310,13 @@ func (a *Account) GetOpenAIBaseURL() string {
if !a.IsOpenAI() && !a.IsCNProvider() {
return ""
}
+ if a.IsCNProvider() && a.IsAdaptiveAPIProtocol() {
+ if baseURLs, ok := a.Credentials["api_base_urls"].(map[string]any); ok {
+ if baseURL, ok := baseURLs[APIProtocolChatCompletions].(string); ok && strings.TrimSpace(baseURL) != "" {
+ return strings.TrimSpace(baseURL)
+ }
+ }
+ }
if a.Type == AccountTypeAPIKey || a.Type == AccountTypeUpstream {
if baseURL := strings.TrimSpace(a.GetCredential("base_url")); baseURL != "" {
return baseURL
@@ -1361,6 +1368,8 @@ func (a *Account) GetAPIProtocol() string {
return APIProtocolChatCompletions
}
switch strings.TrimSpace(a.GetCredential("api_protocol")) {
+ case APIProtocolAdaptive:
+ return APIProtocolAdaptive
case APIProtocolAnthropic:
return APIProtocolAnthropic
case APIProtocolResponses:
@@ -1373,6 +1382,66 @@ func (a *Account) GetAPIProtocol() string {
return APIProtocolChatCompletions
}
+// IsAdaptiveAPIProtocol 报告账号是否按入站协议动态选择供应商原生端点。
+func (a *Account) IsAdaptiveAPIProtocol() bool {
+ return a.GetAPIProtocol() == APIProtocolAdaptive
+}
+
+// GetCNProtocolBaseURL 返回国产供应商指定协议的上游 base URL。
+// adaptive 账号优先使用 api_base_urls 中的分协议地址,缺失时按平台和
+// account_mode 使用官方默认端点。base_url 继续作为 Chat Completions 地址兼容旧字段。
+func (a *Account) GetCNProtocolBaseURL(protocol string) string {
+ if a == nil || !a.IsCNProvider() {
+ return ""
+ }
+ if a.IsAdaptiveAPIProtocol() {
+ if baseURLs, ok := a.Credentials["api_base_urls"].(map[string]any); ok {
+ if baseURL, ok := baseURLs[protocol].(string); ok && strings.TrimSpace(baseURL) != "" {
+ return strings.TrimSpace(baseURL)
+ }
+ }
+ if protocol == APIProtocolChatCompletions {
+ if baseURL := strings.TrimSpace(a.GetCredential("base_url")); baseURL != "" {
+ return baseURL
+ }
+ }
+ }
+ return a.defaultCNProtocolBaseURL(protocol)
+}
+
+func (a *Account) defaultCNProtocolBaseURL(protocol string) string {
+ switch protocol {
+ case APIProtocolAnthropic:
+ switch a.Platform {
+ case PlatformKimi:
+ if a.GetAccountMode() == AccountModeCoding {
+ return DefaultKimiCodingAnthropicBaseURL
+ }
+ return DefaultKimiPayGAnthropicBaseURL
+ case PlatformZhipu:
+ return DefaultZhipuAnthropicBaseURL
+ case PlatformDeepseek:
+ return DefaultDeepseekAnthropicBaseURL
+ }
+ case APIProtocolChatCompletions, APIProtocolResponses:
+ switch a.Platform {
+ case PlatformKimi:
+ if a.GetAccountMode() == AccountModeCoding {
+ return DefaultKimiCodingBaseURL
+ }
+ return DefaultKimiPayGBaseURL
+ case PlatformZhipu:
+ if a.GetAccountMode() == AccountModeCoding {
+ return DefaultZhipuCodingBaseURL
+ }
+ return DefaultZhipuPayGBaseURL
+ case PlatformDeepseek:
+ return DefaultDeepseekBaseURL
+ }
+ }
+ return ""
+}
+
// IsAnthropicProtocol 报告账号是否以原生 Anthropic 协议接入上游
// (/v1/messages 直通,适配 Claude Code 等客户端)。
func (a *Account) IsAnthropicProtocol() bool {
@@ -1383,9 +1452,12 @@ func (a *Account) IsAnthropicProtocol() bool {
// (上游路径为 {base}/v1/messages)。优先取凭证 base_url,缺失时按
// 供应商 × 接入模式返回默认端点。非 Anthropic 协议账号返回空串。
func (a *Account) GetAnthropicProtocolBaseURL() string {
- if a == nil || !a.IsAnthropicProtocol() {
+ if a == nil || (!a.IsAnthropicProtocol() && !a.IsAdaptiveAPIProtocol()) {
return ""
}
+ if a.IsAdaptiveAPIProtocol() {
+ return a.GetCNProtocolBaseURL(APIProtocolAnthropic)
+ }
if a.Type == AccountTypeAPIKey || a.Type == AccountTypeUpstream {
if baseURL := strings.TrimSpace(a.GetCredential("base_url")); baseURL != "" {
return baseURL
diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go
index a2a08f2e0a..900d3f8e15 100644
--- a/backend/internal/service/account_test_service.go
+++ b/backend/internal/service/account_test_service.go
@@ -285,7 +285,8 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
}
// Route to platform-specific test method
- if account.IsCNProvider() && account.GetAPIProtocol() == APIProtocolChatCompletions {
+ if account.IsCNProvider() &&
+ (account.GetAPIProtocol() == APIProtocolChatCompletions || account.IsAdaptiveAPIProtocol()) {
return s.testCNProviderChatCompletionsConnection(c, account, modelID, prompt)
}
diff --git a/backend/internal/service/adaptive_api_protocol_test.go b/backend/internal/service/adaptive_api_protocol_test.go
new file mode 100644
index 0000000000..1da65203c6
--- /dev/null
+++ b/backend/internal/service/adaptive_api_protocol_test.go
@@ -0,0 +1,124 @@
+//go:build unit
+
+package service
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/require"
+ "github.com/tidwall/gjson"
+)
+
+func adaptiveProtocolTestAccount(platform string, baseURLs map[string]any) *Account {
+ return &Account{
+ ID: 701,
+ Name: "adaptive-cn",
+ Platform: platform,
+ Type: AccountTypeAPIKey,
+ Concurrency: 1,
+ Credentials: map[string]any{
+ "api_key": "sk-test",
+ "api_protocol": APIProtocolAdaptive,
+ "account_mode": AccountModePayG,
+ "api_base_urls": baseURLs,
+ },
+ }
+}
+
+func adaptiveProtocolTestContext(path string, body []byte) *gin.Context {
+ recorder := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(recorder)
+ c.Request = httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
+ c.Request.Header.Set("Content-Type", "application/json")
+ return c
+}
+
+func TestAdaptiveProtocolRoutesChatCompletionsToNativeChat(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ body := []byte(`{"model":"glm-4.7","messages":[{"role":"user","content":"hello"}],"stream":false}`)
+ upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
+ svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
+ account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
+ APIProtocolChatCompletions: "http://chat.example",
+ APIProtocolAnthropic: "http://anthropic.example",
+ })
+
+ _, err := svc.ForwardAsChatCompletions(context.Background(), adaptiveProtocolTestContext("/v1/chat/completions", 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())
+}
+
+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}`)
+ upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
+ svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
+ account := adaptiveProtocolTestAccount(PlatformZhipu, map[string]any{
+ APIProtocolChatCompletions: "http://chat.example",
+ APIProtocolAnthropic: "http://anthropic.example",
+ })
+
+ _, err := svc.ForwardAsAnthropic(context.Background(), adaptiveProtocolTestContext("/v1/messages", body), account, body, "", "")
+ require.Error(t, err)
+ require.Equal(t, "http://anthropic.example/v1/messages", upstream.lastReq.URL.String())
+ require.Equal(t, "glm-4.7", gjson.GetBytes(upstream.lastBody, "model").String())
+}
+
+func TestAdaptiveProtocolConvertsKimiResponsesToChatCompletions(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ body := []byte(`{"model":"kimi-k2.5","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: "http://chat.example",
+ APIProtocolAnthropic: "http://anthropic.example",
+ })
+
+ _, 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())
+}
+
+func TestAdaptiveProtocolRoutesDeepSeekResponsesToNativeResponses(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ body := []byte(`{"model":"deepseek-v4","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(PlatformDeepseek, map[string]any{
+ APIProtocolChatCompletions: "http://chat.example",
+ APIProtocolAnthropic: "http://anthropic.example",
+ APIProtocolResponses: "http://responses.example",
+ })
+
+ _, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses", body), account, body)
+ require.Error(t, err)
+ require.Equal(t, "http://responses.example/responses", upstream.lastReq.URL.String())
+ require.False(t, gjson.GetBytes(upstream.lastBody, "store").Bool())
+ require.False(t, gjson.GetBytes(upstream.lastBody, "previous_response_id").Exists())
+}
+
+func TestAdaptiveProtocolConvertsDeepSeekResponsesCompactToChatCompletions(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ body := []byte(`{"model":"deepseek-v4","input":"hello","stream":false}`)
+ upstream := &httpUpstreamRecorder{err: errors.New("stop after capture")}
+ svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
+ account := adaptiveProtocolTestAccount(PlatformDeepseek, map[string]any{
+ APIProtocolChatCompletions: "http://chat.example",
+ APIProtocolAnthropic: "http://anthropic.example",
+ APIProtocolResponses: "http://responses.example",
+ })
+
+ _, err := svc.Forward(context.Background(), adaptiveProtocolTestContext("/v1/responses/compact", body), account, body)
+ require.Error(t, err)
+ require.Equal(t, "http://chat.example/v1/chat/completions", upstream.lastReq.URL.String())
+}
diff --git a/backend/internal/service/cn_providers_test.go b/backend/internal/service/cn_providers_test.go
index 24b8bd4405..8f46e828e1 100644
--- a/backend/internal/service/cn_providers_test.go
+++ b/backend/internal/service/cn_providers_test.go
@@ -55,9 +55,9 @@ func TestCNMillisToRFC3339(t *testing.T) {
t.Parallel()
// 1700000000 秒 = 1700000000000 毫秒
want := time.UnixMilli(1700000000000).UTC().Format(time.RFC3339)
- require.Equal(t, want, cnMillisToRFC3339(1700000000)) // 秒级
- require.Equal(t, want, cnMillisToRFC3339(1700000000000)) // 毫秒级
- require.Equal(t, "", cnMillisToRFC3339(0)) // 非正
+ require.Equal(t, want, cnMillisToRFC3339(1700000000)) // 秒级
+ require.Equal(t, want, cnMillisToRFC3339(1700000000000)) // 毫秒级
+ require.Equal(t, "", cnMillisToRFC3339(0)) // 非正
require.Equal(t, "", cnMillisToRFC3339(-1))
}
@@ -294,8 +294,8 @@ func TestEvaluateAccountSchedulingThreshold_KimiCodingPlan(t *testing.T) {
account := &Account{
Platform: PlatformKimi,
Extra: map[string]any{
- "kimi_5h_used_percent": 90.0,
- "kimi_5h_reset_at": reset.Format(time.RFC3339),
+ "kimi_5h_used_percent": 90.0,
+ "kimi_5h_reset_at": reset.Format(time.RFC3339),
"kimi_weekly_used_percent": 30.0,
"kimi_weekly_reset_at": now.Add(7 * 24 * time.Hour).Format(time.RFC3339),
},
@@ -410,9 +410,9 @@ func TestGetOpenAIProtocolAPIKey_CNProviders(t *testing.T) {
t.Parallel()
kimi := &Account{
- Platform: PlatformKimi,
- Type: AccountTypeAPIKey,
- Credentials: map[string]any{"api_key": "sk-kimi"},
+ Platform: PlatformKimi,
+ Type: AccountTypeAPIKey,
+ Credentials: map[string]any{"api_key": "sk-kimi"},
}
require.Equal(t, "sk-kimi", kimi.GetOpenAIProtocolAPIKey())
require.False(t, kimi.IsOpenAIApiKey(), "IsOpenAIApiKey stays openai-only for scheduling gates")
@@ -486,12 +486,66 @@ 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, 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 TestAdaptiveProtocolBaseURLs(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ platform string
+ mode string
+ wantChat string
+ wantAnthropic string
+ wantResponses string
+ }{
+ {"kimi payg", PlatformKimi, AccountModePayG, DefaultKimiPayGBaseURL, DefaultKimiPayGAnthropicBaseURL, DefaultKimiPayGBaseURL},
+ {"kimi coding", PlatformKimi, AccountModeCoding, DefaultKimiCodingBaseURL, DefaultKimiCodingAnthropicBaseURL, DefaultKimiCodingBaseURL},
+ {"zhipu payg", PlatformZhipu, AccountModePayG, DefaultZhipuPayGBaseURL, DefaultZhipuAnthropicBaseURL, DefaultZhipuPayGBaseURL},
+ {"zhipu coding", PlatformZhipu, AccountModeCoding, DefaultZhipuCodingBaseURL, DefaultZhipuAnthropicBaseURL, DefaultZhipuCodingBaseURL},
+ {"deepseek", PlatformDeepseek, AccountModePayG, DefaultDeepseekBaseURL, DefaultDeepseekAnthropicBaseURL, DefaultDeepseekBaseURL},
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ account := &Account{Platform: tc.platform, Type: AccountTypeAPIKey, Credentials: map[string]any{
+ "api_protocol": APIProtocolAdaptive,
+ "account_mode": tc.mode,
+ }}
+ require.Equal(t, tc.wantChat, account.GetCNProtocolBaseURL(APIProtocolChatCompletions))
+ require.Equal(t, tc.wantAnthropic, account.GetCNProtocolBaseURL(APIProtocolAnthropic))
+ require.Equal(t, tc.wantResponses, account.GetCNProtocolBaseURL(APIProtocolResponses))
+ require.Equal(t, tc.wantAnthropic, account.GetAnthropicProtocolBaseURL())
+ })
+ }
+}
+
+func TestAdaptiveProtocolBaseURLOverrides(t *testing.T) {
+ t.Parallel()
+
+ account := &Account{Platform: PlatformDeepseek, Type: AccountTypeAPIKey, Credentials: map[string]any{
+ "api_protocol": APIProtocolAdaptive,
+ "base_url": "https://legacy-chat.example.com",
+ "api_base_urls": map[string]any{
+ APIProtocolChatCompletions: "https://chat.example.com",
+ APIProtocolAnthropic: "https://anthropic.example.com",
+ APIProtocolResponses: "https://responses.example.com",
+ },
+ }}
+
+ require.Equal(t, "https://chat.example.com", account.GetOpenAIBaseURL())
+ require.Equal(t, "https://chat.example.com", account.GetCNProtocolBaseURL(APIProtocolChatCompletions))
+ require.Equal(t, "https://anthropic.example.com", account.GetAnthropicProtocolBaseURL())
+ require.Equal(t, "https://responses.example.com", account.GetCNProtocolBaseURL(APIProtocolResponses))
+}
+
// TestAnthropicProtocolBaseURL 验证 Anthropic 协议默认端点与协议感知的
// OpenAI 格式 base 回退。
func TestAnthropicProtocolBaseURL(t *testing.T) {
@@ -600,6 +654,14 @@ func TestNormalizeDeepSeekResponsesRequestBody(t *testing.T) {
require.False(t, gjson.GetBytes(normalized, "previous_response_id").Exists())
require.Equal(t, "deepseek-v4-pro", gjson.GetBytes(normalized, "model").String())
+ deepseekAdaptive := &Account{
+ Platform: PlatformDeepseek, Type: AccountTypeAPIKey,
+ Credentials: map[string]any{"api_protocol": APIProtocolAdaptive},
+ }
+ adaptiveNormalized := normalizeDeepSeekResponsesRequestBody(deepseekAdaptive, body)
+ require.False(t, gjson.GetBytes(adaptiveNormalized, "store").Bool())
+ require.False(t, gjson.GetBytes(adaptiveNormalized, "previous_response_id").Exists())
+
// 非 responses 协议(deepseek CC 账号)原样返回
deepseekCC := &Account{Platform: PlatformDeepseek, Type: AccountTypeAPIKey}
require.Equal(t, string(body), string(normalizeDeepSeekResponsesRequestBody(deepseekCC, body)))
diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go
index ba35301be0..860276b753 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -64,6 +64,7 @@ const (
APIProtocolChatCompletions = domain.APIProtocolChatCompletions
APIProtocolAnthropic = domain.APIProtocolAnthropic
APIProtocolResponses = domain.APIProtocolResponses
+ APIProtocolAdaptive = domain.APIProtocolAdaptive
)
// 国产 OpenAI 兼容供应商各模式的默认 base_url。
diff --git a/backend/internal/service/openai_apikey_responses_probe.go b/backend/internal/service/openai_apikey_responses_probe.go
index 645a581694..43ad7c82d7 100644
--- a/backend/internal/service/openai_apikey_responses_probe.go
+++ b/backend/internal/service/openai_apikey_responses_probe.go
@@ -127,7 +127,8 @@ func (s *AccountTestService) ProbeOpenAIAPIKeyResponsesSupport(ctx context.Conte
// 不存在 /v1/responses 端点。直接落标 false 走 Chat Completions 直转,跳过网络探测。
// 例外:deepseek 的 responses 协议账号(api_protocol=responses)使用官方原生
// /responses 端点,落标 force_responses 强制走 Responses 路径。
- if account.GetAPIProtocol() == APIProtocolResponses {
+ if account.GetAPIProtocol() == APIProtocolResponses ||
+ (account.Platform == PlatformDeepseek && account.IsAdaptiveAPIProtocol()) {
_ = s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{
openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceResponses),
openai_compat.ExtraKeyResponsesSupported: true,
diff --git a/backend/internal/service/openai_apikey_responses_probe_test.go b/backend/internal/service/openai_apikey_responses_probe_test.go
index fac351a0ec..1b9cdd65e6 100644
--- a/backend/internal/service/openai_apikey_responses_probe_test.go
+++ b/backend/internal/service/openai_apikey_responses_probe_test.go
@@ -49,6 +49,43 @@ func TestProbeOpenAIAPIKeyResponsesSupportUsesCodexProbeHeaders(t *testing.T) {
require.Equal(t, true, updates[openai_compat.ExtraKeyResponsesSupported])
}
+func TestProbeOpenAIAPIKeyResponsesSupportAdaptiveCNProviders(t *testing.T) {
+ tests := []struct {
+ name string
+ id int64
+ platform string
+ wantSupport bool
+ wantForceMode bool
+ }{
+ {name: "deepseek adaptive supports responses", id: 201, platform: PlatformDeepseek, wantSupport: true, wantForceMode: true},
+ {name: "kimi adaptive falls back to chat", id: 202, platform: PlatformKimi, wantSupport: false},
+ {name: "zhipu adaptive falls back to chat", id: 203, platform: PlatformZhipu, wantSupport: false},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ updateCalls := make(chan map[string]any, 1)
+ account := Account{
+ ID: tc.id, Platform: tc.platform, Type: AccountTypeAPIKey,
+ Credentials: map[string]any{"api_key": "sk-test", "api_protocol": APIProtocolAdaptive},
+ }
+ repo := &snapshotUpdateAccountRepo{
+ stubOpenAIAccountRepo: stubOpenAIAccountRepo{accounts: []Account{account}},
+ updateExtraCalls: updateCalls,
+ }
+ svc := &AccountTestService{accountRepo: repo}
+
+ svc.ProbeOpenAIAPIKeyResponsesSupport(context.Background(), account.ID)
+
+ updates := <-updateCalls
+ require.Equal(t, tc.wantSupport, updates[openai_compat.ExtraKeyResponsesSupported])
+ if tc.wantForceMode {
+ require.Equal(t, string(openai_compat.ResponsesSupportModeForceResponses), updates[openai_compat.ExtraKeyResponsesMode])
+ }
+ })
+ }
+}
+
func TestDecideResponsesProbeSupport(t *testing.T) {
fnCall := []byte(`{"output":[{"type":"reasoning"},{"type":"function_call","name":"probe_ping"}]}`)
reasoningOnly := []byte(`{"output":[{"type":"reasoning"}]}`)
diff --git a/backend/internal/service/openai_gateway_chat_completions.go b/backend/internal/service/openai_gateway_chat_completions.go
index 8308c4c0c3..5dac6e9268 100644
--- a/backend/internal/service/openai_gateway_chat_completions.go
+++ b/backend/internal/service/openai_gateway_chat_completions.go
@@ -88,6 +88,11 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
}
+ // 自适应账号的 Chat Completions 入站始终使用供应商原生 CC 端点。
+ if account.IsAdaptiveAPIProtocol() {
+ return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
+ }
+
// 入口分流(国产供应商 Anthropic 协议):上游为供应商原生 Anthropic 端点,
// CC 入站请求经 CC→Responses→Anthropic 转换链直通该端点。必须先于
// ShouldUseResponsesAPI 分流:该类账号经 probe 落标
diff --git a/backend/internal/service/openai_gateway_forward.go b/backend/internal/service/openai_gateway_forward.go
index 47c02d866e..415164cc8e 100644
--- a/backend/internal/service/openai_gateway_forward.go
+++ b/backend/internal/service/openai_gateway_forward.go
@@ -115,6 +115,10 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if account.IsAnthropicProtocol() {
return s.forwardResponsesViaNativeAnthropic(ctx, c, account, body, reqModel)
}
+ if account.IsAdaptiveAPIProtocol() &&
+ (account.Platform != PlatformDeepseek || isOpenAIResponsesCompactPath(c)) {
+ return s.forwardResponsesViaRawChatCompletions(ctx, c, account, body)
+ }
if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
return s.forwardResponsesViaRawChatCompletions(ctx, c, account, body)
@@ -1055,6 +1059,9 @@ 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() {
+ baseURL = account.GetCNProtocolBaseURL(APIProtocolResponses)
+ }
if baseURL == "" {
targetURL = openaiPlatformAPIURL
} else {
diff --git a/backend/internal/service/openai_gateway_messages.go b/backend/internal/service/openai_gateway_messages.go
index 4b0aa58edf..99c74bedd2 100644
--- a/backend/internal/service/openai_gateway_messages.go
+++ b/backend/internal/service/openai_gateway_messages.go
@@ -41,7 +41,7 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
// thinking / tool_use / cache 语义,适配 Claude Code 等原生客户端。
// 必须先于 ShouldUseResponsesAPI 分流:Anthropic 协议账号经 probe 落标
// openai_responses_supported=false,会先命中下方的 CC 直转分支。
- if account.IsAnthropicProtocol() {
+ if account.IsAnthropicProtocol() || account.IsAdaptiveAPIProtocol() {
return s.forwardAnthropicViaNativeAnthropicEndpoint(ctx, c, account, body, defaultMappedModel)
}
diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go
index f2efddcf77..59199cea05 100644
--- a/backend/internal/service/openai_gateway_request_body.go
+++ b/backend/internal/service/openai_gateway_request_body.go
@@ -59,7 +59,8 @@ func buildOpenAIResponsesURLForPlatform(platform string, base string) string {
// 强制 store=false 并清除 previous_response_id(官方 /responses 不支持服务端
// 状态存储,携带这些字段会被拒绝)。非 deepseek responses 协议账号原样返回。
func normalizeDeepSeekResponsesRequestBody(account *Account, body []byte) []byte {
- if account == nil || account.Platform != PlatformDeepseek || account.GetAPIProtocol() != APIProtocolResponses {
+ if account == nil || account.Platform != PlatformDeepseek ||
+ (account.GetAPIProtocol() != APIProtocolResponses && !account.IsAdaptiveAPIProtocol()) {
return body
}
normalized, err := sjson.SetBytes(body, "store", false)
diff --git a/frontend/src/components/account/CnBaseUrlPresets.vue b/frontend/src/components/account/CnBaseUrlPresets.vue
index 5a5172555a..45ccb32dc3 100644
--- a/frontend/src/components/account/CnBaseUrlPresets.vue
+++ b/frontend/src/components/account/CnBaseUrlPresets.vue
@@ -30,7 +30,7 @@ const props = defineProps<{
/** 当前已选账号类型,用于过滤和高亮匹配的预设 */
mode?: 'payg' | 'coding'
/** 当前已选 API 协议,用于过滤和高亮匹配的预设 */
- protocol?: 'chat_completions' | 'anthropic' | 'responses'
+ protocol?: 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
/** 当前输入框中的 base url,用于高亮完全匹配项 */
currentUrl?: string
}>()
diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue
index 47ee8c27d5..546c5f0066 100644
--- a/frontend/src/components/account/CreateAccountModal.vue
+++ b/frontend/src/components/account/CreateAccountModal.vue
@@ -537,7 +537,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
-
+ {{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }} +
++ {{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }} +
+