Files
sub2api/backend/internal/service/provider_profile_test.go
T
SavitarCandClaude Opus 5.5 8ac2936a3d refactor(gateway): 三个入站入口的上游协议分流收敛为 resolveUpstreamProtocol
此前 /v1/responses、/v1/chat/completions、/v1/messages 各自用一串 if 决定
上游协议:OpenCode 按模型规则、国产 adaptive 按入站、显式 api_protocol、
Responses 探针四个来源的优先级靠语句先后隐式决定,且每个入口都有
IsOpenCodeGo 特判。

- provider profile 新增 Routing(by_inbound / by_model),OpenCode 登记为
  by_model;routesByModel 取代转发路径与 GetAPIProtocol 默认值中的
  IsOpenCodeGo 判断。
- 新增 upstream_protocol_routing.go:resolveUpstreamProtocol 以单一函数给出
  上游协议(按模型分流 → adaptive 按入站 → 显式 anthropic → CC 直转条件
  → Responses),resolveModelRoutedProtocol 以 profile 规则泛化 OpenCode
  的规则解析;三个入口改为 switch 该结果选择既有转换路径,Responses 形状
  请求体的转换合并为 convertResponsesShapedChatBody。

转换函数本身不变。新增分流矩阵测试经真实入口遍历 平台 × api_protocol ×
探针 × 账号类型 × 接入模式 × protocol_rules × 入站 × 模型(2045 组),
与改动前逐条 diff 仅 6 条不同:智谱 adaptive 的非 API Key 账号(UI 无法
创建)在 /v1/responses 入站时由 Responses 改为回落 CC,与
/v1/chat/completions 入口一致;这 6 组前后都在发出上游请求前报错。

协议规则与映射模型相关的类型、函数已被所有按模型分流的供应商共用,同时去掉
OpenCode 前缀(OpenCodeGoProtocolRule → ProtocolRule、matchOpenCodeGoProtocolRules
→ matchProtocolRules、NormalizeOpenCodeGoProtocolRulesCredentials →
NormalizeProtocolRulesCredentials、resolveOpenCodeGoMappedModel →
resolveMappedUpstreamModel 等),并删除收敛后已无调用方的
defaultOpenCodeProtocolRules。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:10:32 +08:00

189 lines
6.8 KiB
Go

//go:build unit
package service
import (
"testing"
"github.com/stretchr/testify/require"
)
// legacyProviderDefaultBaseURL 复述 provider profile 引入前 account.go 中逐平台
// switch 的默认端点逻辑,作为等价性基准。
func legacyProviderDefaultBaseURL(platform, mode, protocol string) string {
coding := mode == AccountModeCoding
zen := mode == AccountModeZen
switch protocol {
case APIProtocolAnthropic:
switch platform {
case PlatformKimi:
if coding {
return DefaultKimiCodingAnthropicBaseURL
}
return DefaultKimiPayGAnthropicBaseURL
case PlatformZhipu:
return DefaultZhipuAnthropicBaseURL
case PlatformDeepseek:
return DefaultDeepseekAnthropicBaseURL
case PlatformMiniMax:
return DefaultMiniMaxAnthropicBaseURL
case PlatformOpenCodeGo:
if zen {
return DefaultOpenCodeZenAnthropicBaseURL
}
return DefaultOpenCodeGoAnthropicBaseURL
}
case APIProtocolChatCompletions, APIProtocolResponses:
if platform == PlatformZhipu && protocol == APIProtocolResponses {
// 唯一的函数级差异:旧 switch 对智谱 Responses 也返回 CC 基址,profile
// 以"缺条目即不支持"建模后返回空串。所有运行时调用方都先经
// UsesNativeCNResponses / SupportsNativeCNResponses 守卫,智谱不会走到这里。
return ""
}
switch platform {
case PlatformKimi:
if coding {
return DefaultKimiCodingBaseURL
}
return DefaultKimiPayGBaseURL
case PlatformZhipu:
if coding {
return DefaultZhipuCodingBaseURL
}
return DefaultZhipuPayGBaseURL
case PlatformDeepseek:
return DefaultDeepseekBaseURL
case PlatformMiniMax:
return DefaultMiniMaxBaseURL
case PlatformOpenCodeGo:
if zen {
return DefaultOpenCodeZenBaseURL
}
return DefaultOpenCodeGoBaseURL
}
}
return ""
}
func TestProviderProfile_DefaultBaseURLsMatchLegacySwitches(t *testing.T) {
t.Parallel()
platforms := []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo}
modes := []string{"", AccountModePayG, AccountModeCoding, AccountModeZen, AccountModeGo, " coding ", "unknown"}
for _, platform := range platforms {
for _, mode := range modes {
legacyMode := mode
if legacyMode == " coding " {
legacyMode = AccountModeCoding
}
newAccount := func(protocol string) *Account {
return &Account{
Platform: platform,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "sk-test",
"account_mode": mode,
"api_protocol": protocol,
},
}
}
name := platform + "/" + mode
chat := newAccount(APIProtocolChatCompletions)
require.Equal(t, legacyProviderDefaultBaseURL(platform, legacyMode, APIProtocolChatCompletions), chat.GetOpenAIBaseURL(), name)
anthropic := newAccount(APIProtocolAnthropic)
require.Equal(t, legacyProviderDefaultBaseURL(platform, legacyMode, APIProtocolAnthropic), anthropic.GetAnthropicProtocolBaseURL(), name)
require.Equal(t, legacyProviderDefaultBaseURL(platform, legacyMode, APIProtocolChatCompletions), anthropic.GetOpenAIFormatBaseURL(), name)
adaptive := newAccount(APIProtocolAdaptive)
for _, protocol := range []string{APIProtocolChatCompletions, APIProtocolResponses, APIProtocolAnthropic, "unknown"} {
require.Equal(t, legacyProviderDefaultBaseURL(platform, legacyMode, protocol), adaptive.GetCNProtocolBaseURL(protocol), name+"/"+protocol)
}
}
}
}
func TestProviderProfile_ProfilesMatchMultiProtocolPlatforms(t *testing.T) {
t.Parallel()
for _, platform := range []string{
PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo, PlatformComposite, "",
} {
want := IsCNProvider(platform) || platform == PlatformOpenCodeGo
require.Equal(t, want, IsMultiProtocolAPIKeyProvider(platform), platform)
require.Equal(t, want, LookupProviderProfile(platform) != nil, platform)
}
for platform, profile := range providerProfiles {
require.Equal(t, platform, profile.Platform)
_, ok := profile.Modes[profile.DefaultMode]
require.True(t, ok, "%s default mode %q must be registered", platform, profile.DefaultMode)
}
}
func TestProviderProfile_NativeResponsesMatchesLegacy(t *testing.T) {
t.Parallel()
legacy := map[string]bool{
PlatformDeepseek: true,
PlatformKimi: true,
PlatformMiniMax: true,
PlatformOpenCodeGo: true,
}
modes := []string{"", AccountModePayG, AccountModeCoding, AccountModeZen, AccountModeGo, "unknown"}
for _, platform := range []string{PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo} {
for _, mode := range modes {
account := &Account{Platform: platform, Type: AccountTypeAPIKey, Credentials: map[string]any{"account_mode": mode}}
require.Equal(t, legacy[platform], account.SupportsNativeCNResponses(), platform+"/"+mode)
}
}
require.False(t, (*Account)(nil).SupportsNativeCNResponses())
}
func TestProviderProfile_BaseURLKeysAreNativeProtocols(t *testing.T) {
t.Parallel()
for platform, profile := range providerProfiles {
for mode, endpoints := range profile.Modes {
require.NotEmpty(t, endpoints.BaseURLs[APIProtocolChatCompletions], "%s/%s must have a chat_completions base", platform, mode)
for protocol, baseURL := range endpoints.BaseURLs {
require.True(t, isNativeUpstreamProtocol(protocol), "%s/%s has unknown protocol key %q", platform, mode, protocol)
require.NotEmpty(t, baseURL, "%s/%s/%s", platform, mode, protocol)
}
}
}
}
func TestProviderProfile_ResponsesPathMatchesLegacy(t *testing.T) {
t.Parallel()
legacy := func(platform, base string) string {
if platform == PlatformDeepseek {
return buildOpenAIEndpointURL(base, "/responses")
}
return buildOpenAIResponsesURL(base)
}
bases := []string{
"https://api.deepseek.com", "https://relay.example.com", "https://relay.example.com/v1",
"https://opencode.ai/zen/go/v1", "https://open.bigmodel.cn/api/paas/v4", "https://api.moonshot.cn/v1",
}
for _, platform := range []string{PlatformOpenAI, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax, PlatformOpenCodeGo} {
for _, base := range bases {
require.Equal(t, legacy(platform, base), buildOpenAIResponsesURLForPlatform(platform, base), platform+" "+base)
}
}
}
func TestProviderProfile_OpenCodeProtocolRulesByMode(t *testing.T) {
t.Parallel()
openCode := LookupProviderProfile(PlatformOpenCodeGo)
require.Equal(t, DefaultOpenCodeGoProtocolRules(), openCode.Endpoints(AccountModeGo).ProtocolRules)
require.Equal(t, DefaultOpenCodeGoProtocolRules(), openCode.Endpoints("").ProtocolRules)
require.Equal(t, DefaultOpenCodeZenProtocolRules(), openCode.Endpoints(AccountModeZen).ProtocolRules)
require.Empty(t, LookupProviderProfile(PlatformKimi).Endpoints(AccountModeCoding).ProtocolRules)
}