feat(accounts): add adaptive API protocol routing

This commit is contained in:
SavitarC
2026-08-20 11:52:05 +08:00
parent 1b5dc676a9
commit 85051616f9
21 changed files with 605 additions and 31 deletions
+1
View File
@@ -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
+73 -1
View File
@@ -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
@@ -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)
}
@@ -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())
}
+70 -8
View File
@@ -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)))
@@ -64,6 +64,7 @@ const (
APIProtocolChatCompletions = domain.APIProtocolChatCompletions
APIProtocolAnthropic = domain.APIProtocolAnthropic
APIProtocolResponses = domain.APIProtocolResponses
APIProtocolAdaptive = domain.APIProtocolAdaptive
)
// 国产 OpenAI 兼容供应商各模式的默认 base_url。
@@ -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,
@@ -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"}]}`)
@@ -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 落标
@@ -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 {
@@ -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)
}
@@ -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)
@@ -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
}>()
@@ -537,7 +537,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon :name="opt.value === 'anthropic' ? 'sparkles' : opt.value === 'responses' ? 'terminal' : 'chat'" size="sm" />
<Icon :name="opt.value === 'adaptive' ? 'swap' : opt.value === 'anthropic' ? 'sparkles' : opt.value === 'responses' ? 'terminal' : 'chat'" size="sm" />
</div>
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t(`admin.accounts.cnProviders.apiProtocol.${opt.labelKey}`) }}</span>
@@ -1250,7 +1250,7 @@
<!-- API Key input (only for apikey type, excluding Antigravity which has its own fields) -->
<div v-if="form.type === 'apikey' && form.platform !== 'antigravity'" class="space-y-4">
<div>
<div v-if="!isCNPlatform || apiProtocol !== 'adaptive'">
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
<input
v-model="apiKeyBaseUrl"
@@ -1274,6 +1274,20 @@
@select="onCnPresetSelect"
/>
</div>
<div v-else>
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.endpoints') }}</label>
<div class="mt-2 space-y-3">
<div v-for="item in cnAdaptiveProtocolOptions" :key="item.value">
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
{{ t(`admin.accounts.cnProviders.apiProtocol.${item.labelKey}`) }}
</label>
<input v-model="adaptiveBaseUrls[item.value]" type="text" class="input" />
</div>
</div>
<p v-if="form.platform !== 'deepseek'" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
<input
@@ -3746,11 +3760,13 @@ import {
applyAntigravityProjectID,
applyHeaderOverride,
applyInterceptWarmup,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
isHeaderOverrideCapable,
validateHeaderOverrideRows,
type CnAccountMode,
type CnApiProtocol,
type CnNativeApiProtocol,
type HeaderOverrideRow
} from '@/components/account/credentialsBuilder'
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
@@ -3932,7 +3948,12 @@ const upstreamBillingAutoProbeEnabled = ref(true)
const accountMode = ref<CnAccountMode>('payg')
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
const apiProtocol = ref<CnApiProtocol>('chat_completions')
const apiProtocol = ref<CnApiProtocol>('adaptive')
const adaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
responses: ''
})
const isCNPlatform = computed(
() => form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek'
)
@@ -3947,6 +3968,7 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
// 当前平台可选的协议档(responses 仅 deepseek)。
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' }
]
@@ -3955,6 +3977,18 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
}
return opts
})
const cnAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; labelKey: string }>>(() => {
const opts: Array<{ value: CnNativeApiProtocol; labelKey: string }> = [
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (form.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
function resetAdaptiveBaseUrls(platform: 'kimi' | 'zhipu' | 'deepseek', mode: CnAccountMode) {
adaptiveBaseUrls.value = defaultCNAdaptiveBaseUrls(platform, mode)
}
// 当前选中平台的品牌色(选中卡片描边 / 图标底色),与 platformColors 取色一致。
const cnAccentActiveClass = computed(() => {
switch (form.platform) {
@@ -3981,24 +4015,44 @@ const cnAccentIconClass = computed(() => {
}
})
// 切换国产供应商平台:强制 apikey 类型,deepseek 无 coding 套餐故锁定 payg,
// 协议回落 chat_completions,并把 base url 重置为该平台默认端点。
// 协议回落 adaptive,并把 base url 重置为该平台默认端点。
function selectCNPlatform(platform: 'kimi' | 'zhipu' | 'deepseek') {
form.platform = platform
form.type = 'apikey'
accountCategory.value = 'apikey'
apiProtocol.value = 'chat_completions'
apiProtocol.value = 'adaptive'
if (platform === 'deepseek') {
accountMode.value = 'payg'
}
apiKeyBaseUrl.value = defaultCNBaseUrl(platform, accountMode.value, apiProtocol.value)
resetAdaptiveBaseUrls(platform, accountMode.value)
}
// 账号类型 / 协议变更时同步默认 base url。
watch(accountMode, (mode) => {
watch(accountMode, (mode, previousMode) => {
if (!isCNPlatform.value) return
if (apiProtocol.value === 'adaptive') {
const previousDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, previousMode)
const nextDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, mode)
for (const item of cnAdaptiveProtocolOptions.value) {
if (!adaptiveBaseUrls.value[item.value] || adaptiveBaseUrls.value[item.value] === previousDefaults[item.value]) {
adaptiveBaseUrls.value[item.value] = nextDefaults[item.value]
}
}
apiKeyBaseUrl.value = adaptiveBaseUrls.value.chat_completions
return
}
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, mode, apiProtocol.value)
})
watch(apiProtocol, (protocol) => {
if (!isCNPlatform.value) return
if (protocol === 'adaptive') {
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, accountMode.value)
for (const item of cnAdaptiveProtocolOptions.value) {
if (!adaptiveBaseUrls.value[item.value]) adaptiveBaseUrls.value[item.value] = defaults[item.value]
}
apiKeyBaseUrl.value = adaptiveBaseUrls.value.chat_completions
return
}
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, accountMode.value, protocol)
})
// 点击预设端点:同时回填 base url、账号类型与协议。
@@ -4956,7 +5010,8 @@ const resetForm = () => {
accountCategory.value = 'oauth-based'
addMethod.value = 'oauth'
accountMode.value = 'payg'
apiProtocol.value = 'chat_completions'
apiProtocol.value = 'adaptive'
adaptiveBaseUrls.value = { chat_completions: '', anthropic: '', responses: '' }
apiKeyBaseUrl.value = 'https://api.anthropic.com'
apiKeyValue.value = ''
upstreamBillingAutoProbeEnabled.value = true
@@ -5425,10 +5480,19 @@ const handleSubmit = async () => {
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
credentials.account_mode = accountMode.value
credentials.api_protocol = apiProtocol.value
if (apiProtocol.value === 'adaptive') {
const defaults = defaultCNAdaptiveBaseUrls(form.platform, accountMode.value)
const protocolBaseUrls: Record<string, string> = {}
for (const item of cnAdaptiveProtocolOptions.value) {
protocolBaseUrls[item.value] = (adaptiveBaseUrls.value[item.value] || defaults[item.value]).trim()
}
credentials.api_base_urls = protocolBaseUrls
credentials.base_url = protocolBaseUrls.chat_completions
}
const resolvedCNBase = (
apiKeyBaseUrl.value.trim() || defaultCNBaseUrl(form.platform, accountMode.value, apiProtocol.value)
).trim()
if (resolvedCNBase) {
if (apiProtocol.value !== 'adaptive' && resolvedCNBase) {
credentials.base_url = resolvedCNBase
}
}
@@ -28,7 +28,7 @@
<!-- API Key fields (only for apikey type) -->
<div v-if="account.type === 'apikey'" class="space-y-4">
<div>
<div v-if="!isCNApiKeyAccount || editApiProtocol !== 'adaptive'">
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
<input
v-model="editBaseUrl"
@@ -62,6 +62,20 @@
@select="onCnPresetSelect"
/>
</div>
<div v-else>
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.endpoints') }}</label>
<div class="mt-2 space-y-3">
<div v-for="item in editAdaptiveProtocolOptions" :key="item.value">
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
{{ t(`admin.accounts.cnProviders.apiProtocol.${item.labelKey}`) }}
</label>
<input v-model="editAdaptiveBaseUrls[item.value]" type="text" class="input" />
</div>
</div>
<p v-if="account.platform !== 'deepseek'" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
<!-- Account Mode Selection (CN providers) -->
<div v-if="isCNApiKeyAccount">
<label class="input-label">{{ t('admin.accounts.cnProviders.accountMode.title') }}</label>
@@ -2802,11 +2816,13 @@ import {
isHeaderOverrideCapable,
splitHeaderOverridesObject,
validateHeaderOverrideRows,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
HEADER_OVERRIDES_CREDENTIAL_KEY,
type CnAccountMode,
type CnApiProtocol,
type CnNativeApiProtocol,
type HeaderOverrideRow
} from '@/components/account/credentialsBuilder'
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
@@ -2909,8 +2925,13 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
}
return 'kimi'
})
const editApiProtocol = ref<CnApiProtocol>('chat_completions')
const editApiProtocol = ref<CnApiProtocol>('adaptive')
const editAccountMode = ref<CnAccountMode>('payg')
const editAdaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
responses: ''
})
// 回填窗口标志:syncFormFromAccount 会同步改写 editAccountMode / editApiProtocol,
// 而 watcher(pre-flush)在同步代码执行完之后才触发——若不抑制,会把刚恢复的
// 存储版 base_url(可能是用户自定义/中转地址)覆盖为官方预设并在下次保存时持久化。
@@ -2930,6 +2951,7 @@ const cnAccountModeOptions = computed<Array<{ value: CnAccountMode; labelKey: 'p
)
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' }
]
@@ -2938,11 +2960,35 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
}
return opts
})
watch(editApiProtocol, (protocol) => {
const editAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; labelKey: string }>>(() => {
const opts: Array<{ value: CnNativeApiProtocol; labelKey: string }> = [
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (props.account?.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
watch(editApiProtocol, (protocol, previousProtocol) => {
if (!isCNApiKeyAccount.value || syncingForm.value) return
if (protocol === 'adaptive') {
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, editAccountMode.value)
for (const item of editAdaptiveProtocolOptions.value) {
if (!editAdaptiveBaseUrls.value[item.value]) editAdaptiveBaseUrls.value[item.value] = defaults[item.value]
}
if (previousProtocol !== 'adaptive' && editBaseUrl.value.trim()) {
editAdaptiveBaseUrls.value[previousProtocol] = editBaseUrl.value.trim()
}
editBaseUrl.value = editAdaptiveBaseUrls.value.chat_completions
return
}
if (previousProtocol === 'adaptive') {
editBaseUrl.value = editAdaptiveBaseUrls.value[protocol] ||
defaultCNBaseUrl(props.account!.platform, editAccountMode.value, protocol)
return
}
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, editAccountMode.value, protocol)
})
watch(editAccountMode, (mode) => {
watch(editAccountMode, (mode, previousMode) => {
if (!isCNApiKeyAccount.value || syncingForm.value) return
// deepseek 无 coding 套餐:防御性回退(UI 已隐藏该选项)。
const effectiveMode = props.account!.platform === 'deepseek' && mode === 'coding' ? 'payg' : mode
@@ -2950,6 +2996,17 @@ watch(editAccountMode, (mode) => {
editAccountMode.value = effectiveMode
return
}
if (editApiProtocol.value === 'adaptive') {
const previousDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, previousMode)
const nextDefaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, mode)
for (const item of editAdaptiveProtocolOptions.value) {
if (!editAdaptiveBaseUrls.value[item.value] || editAdaptiveBaseUrls.value[item.value] === previousDefaults[item.value]) {
editAdaptiveBaseUrls.value[item.value] = nextDefaults[item.value]
}
}
editBaseUrl.value = editAdaptiveBaseUrls.value.chat_completions
return
}
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, mode, editApiProtocol.value)
})
const cnProtocolDescKey = computed(
@@ -3774,12 +3831,27 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editAccountMode.value = credentials.account_mode === 'coding' ? 'coding' : 'payg'
const storedProtocol = credentials.api_protocol
editApiProtocol.value =
storedProtocol === 'anthropic' || storedProtocol === 'responses' ? storedProtocol : 'chat_completions'
storedProtocol === 'adaptive' || storedProtocol === 'anthropic' || storedProtocol === 'responses'
? storedProtocol
: 'adaptive'
if (newAccount.platform !== 'deepseek' && editApiProtocol.value === 'responses') {
editApiProtocol.value = 'chat_completions'
}
const adaptiveDefaults = defaultCNAdaptiveBaseUrls(newAccount.platform, editAccountMode.value)
const storedBaseUrls = (credentials.api_base_urls as Record<string, unknown> | undefined) || {}
editAdaptiveBaseUrls.value = {
chat_completions: typeof storedBaseUrls.chat_completions === 'string'
? storedBaseUrls.chat_completions
: adaptiveDefaults.chat_completions,
anthropic: typeof storedBaseUrls.anthropic === 'string'
? storedBaseUrls.anthropic
: adaptiveDefaults.anthropic,
responses: typeof storedBaseUrls.responses === 'string'
? storedBaseUrls.responses
: adaptiveDefaults.responses
}
}
const platformDefaultUrl =
const platformDefaultUrl =
newAccount.platform === 'openai'
? 'https://api.openai.com'
: newAccount.platform === 'gemini'
@@ -3791,7 +3863,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
newAccount.platform === 'deepseek'
? defaultCNBaseUrl(newAccount.platform, editAccountMode.value, editApiProtocol.value)
: 'https://api.anthropic.com'
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
editBaseUrl.value = isCNApiKeyAccount.value && editApiProtocol.value === 'adaptive'
? editAdaptiveBaseUrls.value.chat_completions
: (credentials.base_url as string) || platformDefaultUrl
// Load model mappings and detect mode
loadModelRestrictionFromMapping(credentials.model_mapping as Record<string, unknown> | undefined)
@@ -4463,6 +4537,17 @@ const handleSubmit = async () => {
if (isCNApiKeyAccount.value) {
newCredentials.account_mode = editAccountMode.value
newCredentials.api_protocol = editApiProtocol.value
if (editApiProtocol.value === 'adaptive') {
const defaults = defaultCNAdaptiveBaseUrls(cnPresetPlatform.value, editAccountMode.value)
const protocolBaseUrls: Record<string, string> = {}
for (const item of editAdaptiveProtocolOptions.value) {
protocolBaseUrls[item.value] = (editAdaptiveBaseUrls.value[item.value] || defaults[item.value]).trim()
}
newCredentials.api_base_urls = protocolBaseUrls
newCredentials.base_url = protocolBaseUrls.chat_completions
} else {
delete newCredentials.api_base_urls
}
}
// Handle API key
@@ -269,6 +269,28 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(probeUpstreamBillingMock).not.toHaveBeenCalled()
})
it('submits adaptive Kimi protocol endpoints', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Kimi')
await selectButtonByText(wrapper, 'admin.accounts.cnProviders.apiProtocol.adaptive')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Kimi adaptive')
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-kimi')
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: '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'
}
})
})
it('exposes Agent Identity in the OpenAI authorization methods', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
@@ -343,6 +343,37 @@ describe('EditAccountModal', () => {
})
})
it('preserves adaptive GLM endpoints on submit', async () => {
const account = buildAccount()
account.platform = 'zhipu'
account.credentials = {
api_key: 'sk-glm',
account_mode: 'coding',
api_protocol: 'adaptive',
base_url: 'https://open.bigmodel.cn/api/coding/paas/v4',
api_base_urls: {
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
anthropic: 'https://open.bigmodel.cn/api/anthropic'
}
}
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: 'coding',
api_protocol: 'adaptive',
base_url: 'https://open.bigmodel.cn/api/coding/paas/v4',
api_base_urls: {
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
anthropic: 'https://open.bigmodel.cn/api/anthropic'
}
})
})
it('preserves model mappings when editing the whitelist', async () => {
const account = buildAccount()
account.credentials.model_mapping = {
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
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: ''
})
expect(defaultCNAdaptiveBaseUrls('kimi', 'coding')).toEqual({
chat_completions: 'https://api.kimi.com/coding/v1',
anthropic: 'https://api.kimi.com/coding',
responses: ''
})
})
it('resolves GLM endpoints by account mode', () => {
expect(defaultCNAdaptiveBaseUrls('zhipu', 'payg')).toEqual({
chat_completions: 'https://open.bigmodel.cn/api/paas/v4',
anthropic: 'https://open.bigmodel.cn/api/anthropic',
responses: ''
})
expect(defaultCNAdaptiveBaseUrls('zhipu', 'coding')).toEqual({
chat_completions: 'https://open.bigmodel.cn/api/coding/paas/v4',
anthropic: 'https://open.bigmodel.cn/api/anthropic',
responses: ''
})
})
it('includes all three native DeepSeek endpoints', () => {
expect(defaultCNAdaptiveBaseUrls('deepseek', 'payg')).toEqual({
chat_completions: 'https://api.deepseek.com',
anthropic: 'https://api.deepseek.com/anthropic',
responses: 'https://api.deepseek.com'
})
})
})
@@ -256,8 +256,9 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
export type CnAccountMode = 'payg' | 'coding'
/** 仅 deepseek 支持 responses 协议(官方原生 /responses 端点,适配 Codex)。 */
export type CnApiProtocol = 'chat_completions' | 'anthropic' | 'responses'
/** 仅 deepseek 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
export type CnApiProtocol = 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
export type CnNativeApiProtocol = Exclude<CnApiProtocol, 'adaptive'>
export interface CnBaseUrlPreset {
mode: CnAccountMode
@@ -321,6 +322,18 @@ export function defaultCNBaseUrl(
}
}
/** 返回自适应模式下需要配置的原生协议及其默认端点。 */
export function defaultCNAdaptiveBaseUrls(
platform: 'kimi' | 'zhipu' | 'deepseek',
mode: CnAccountMode
): Record<CnNativeApiProtocol, string> {
return {
chat_completions: defaultCNBaseUrl(platform, mode, 'chat_completions'),
anthropic: defaultCNBaseUrl(platform, mode, 'anthropic'),
responses: platform === 'deepseek' ? defaultCNBaseUrl(platform, mode, 'responses') : ''
}
}
// ===== 国产供应商用量单元格可见性(单一事实源) =====
// CNProviderQuotaCell / CNProviderBalanceCell 与 AccountUsageCell 的占位符判定
// 共用,避免多处复制条件后一处改另一处漏改。
@@ -118,6 +118,10 @@ export default {
},
apiProtocol: {
title: 'API Protocol',
adaptive: 'Adaptive',
adaptiveDesc: 'Uses the matching native provider endpoint for each inbound protocol, converting only when unavailable.',
endpoints: 'Protocol endpoints',
responsesFallbackDesc: 'Responses requests are converted to Chat Completions because this provider has no native Responses endpoint.',
chatCompletions: 'Chat Completions',
chatCompletionsDesc: 'Standard OpenAI-compatible endpoint; requests in other formats are converted.',
anthropic: 'Anthropic',
@@ -321,6 +321,10 @@ export default {
},
apiProtocol: {
title: 'API 协议',
adaptive: '自适应',
adaptiveDesc: '按入站协议优先使用供应商原生端点,仅在没有对应端点时转换。',
endpoints: '协议端点',
responsesFallbackDesc: '该供应商没有原生 Responses 端点,Responses 请求将转换为 Chat Completions。',
chatCompletions: 'Chat Completions',
chatCompletionsDesc: '标准 OpenAI 兼容端点,其他格式请求将被转换。',
anthropic: 'Anthropic',