mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 20:12:39 +08:00
refactor(platform): 多协议供应商的转发与探测判断改查 profile
平台清单之外,后端还有几处用"国产厂商"或"国产厂商 ∪ OpenCode"表示 多协议供应商,新登记的供应商会被漏掉(例如上游模型同步直接报不支持)。 - 适用于全部多协议供应商的判断改用 IsMultiProtocolAPIKey:上游模型同步与 模型目录基址、Anthropic count_tokens 本地估算、HTML 403 豁免与累计计数、Claude 型号 计费候选过滤; - 新增 RoutesProtocolByInbound(profile 分流方式为按入站协议),替换原来 只针对国产厂商、实际描述"按入站协议分流"的判断:Responses 直转回退、 Responses 能力标记(含创建 / 更新后的异步探测调度)、上游计费探测的 adaptive 基址、Anthropic API Key 鉴权方式、OpenAI 输入 token 本地估算。 按模型分流的供应商(OpenCode)仍走 routesByModel 分支。 依赖各家额度 / 余额接口的逻辑(Coding Plan 窗口、余额不足冷却等)仍按平台 显式判断。现有平台上两组判断与改动前逐一相同(测试覆盖全部平台与非法值); 另测试临时登记的新供应商按分流方式分别获得国产厂商与 OpenCode 的行为。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eefa7dd9dc
commit
c556aee8fd
@@ -1226,7 +1226,7 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
// 网关会按"现状即证据"默认走 Responses。
|
||||
func (h *AccountHandler) scheduleOpenAIResponsesProbe(account *service.Account) {
|
||||
if account == nil || account.Type != service.AccountTypeAPIKey ||
|
||||
(account.Platform != service.PlatformOpenAI && !service.IsCNProvider(account.Platform)) {
|
||||
(account.Platform != service.PlatformOpenAI && !account.RoutesProtocolByInbound()) {
|
||||
return
|
||||
}
|
||||
if h.accountTestService == nil {
|
||||
|
||||
@@ -21,7 +21,7 @@ func (a *Account) GetAnthropicAPIKeyAuthScheme() string {
|
||||
if a == nil || a.Type != AccountTypeAPIKey {
|
||||
return AnthropicAPIKeyAuthSchemeXAPIKey
|
||||
}
|
||||
if a.Platform != PlatformAnthropic && !a.IsCNProvider() {
|
||||
if a.Platform != PlatformAnthropic && !a.RoutesProtocolByInbound() {
|
||||
return AnthropicAPIKeyAuthSchemeXAPIKey
|
||||
}
|
||||
|
||||
|
||||
@@ -130,8 +130,8 @@ func (s *AccountTestService) ProbeOpenAIAPIKeyResponsesSupport(ctx context.Conte
|
||||
if account.Type != AccountTypeAPIKey {
|
||||
return
|
||||
}
|
||||
if account.IsCNProvider() {
|
||||
// 国产 OpenAI 兼容上游默认仅支持 /v1/chat/completions。直接落标 false
|
||||
if account.RoutesProtocolByInbound() {
|
||||
// 按入站协议分流的供应商(国产厂商等)默认仅支持 /v1/chat/completions。直接落标 false
|
||||
// 走 Chat Completions 直转,跳过网络探测。
|
||||
// 例外:deepseek / kimi 的固定 responses 和 adaptive 账号使用官方原生
|
||||
// Responses 端点,落标 force_responses;其余协议显式重置为 auto,避免
|
||||
|
||||
@@ -150,7 +150,7 @@ func prepareNativeOpenAIInputTokensCountRequest(body []byte, account *Account) (
|
||||
}
|
||||
|
||||
func shouldEstimateOpenAIInputTokensLocally(account *Account) bool {
|
||||
if account == nil || account.IsGrok() || account.IsCNProvider() || account.Type == AccountTypeUpstream {
|
||||
if account == nil || account.IsGrok() || account.RoutesProtocolByInbound() || account.Type == AccountTypeUpstream {
|
||||
return true
|
||||
}
|
||||
if account.Type != AccountTypeAPIKey {
|
||||
@@ -264,14 +264,14 @@ func (s *OpenAIGatewayService) ForwardCountTokensAsAnthropic(
|
||||
return fmt.Errorf("count_tokens: missing account")
|
||||
}
|
||||
|
||||
// 国产供应商与 OpenCode(全部协议,含 anthropic):一律本地估算,不发上游请求。
|
||||
// 多协议 API Key 供应商(国产厂商与聚合平台,全部协议,含 anthropic):一律本地估算,不发上游请求。
|
||||
// 依据(2026-08 核实):三家的 Anthropic 兼容层均未提供
|
||||
// /v1/messages/count_tokens——DeepSeek 官方 anthropic_api 文档无此端点
|
||||
// (且注明 anthropic-version 头被忽略),聚合网关 OpenModel 明确标注
|
||||
// count_tokens 为 "Anthropic only",Kimi/智谱亦无任何文档承诺。转发上游
|
||||
// 只会常态 404,且错误还会流入账号处置逻辑误伤整账号调度;Claude Code
|
||||
// 高频调用此端点,本地 tiktoken 估算是与 Grok 一致的既有方案。
|
||||
if account.IsCNProvider() || account.IsOpenCodeGo() {
|
||||
if account.IsMultiProtocolAPIKey() {
|
||||
estimated, err := estimateAnthropicCountTokensLocally(body)
|
||||
if err != nil {
|
||||
writeAnthropicCountTokensError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
||||
|
||||
@@ -1351,9 +1351,10 @@ func shouldForwardOpenAIResponsesViaRawChatCompletions(account *Account) bool {
|
||||
// Grok/GPT/Muse into Chat Completions.
|
||||
return false
|
||||
}
|
||||
if account.IsCNProvider() {
|
||||
// CN 的显式协议配置优先于异步探针 Extra;adaptive 仅 DeepSeek / Kimi
|
||||
// 有原生 Responses,GLM 回退 Chat Completions。
|
||||
if account.RoutesProtocolByInbound() {
|
||||
// 按入站协议分流的供应商(国产厂商等):显式协议配置优先于异步探针
|
||||
// Extra;adaptive 仅在供应商有原生 Responses 端点时直转,否则回退
|
||||
// Chat Completions(如 GLM)。
|
||||
switch account.GetAPIProtocol() {
|
||||
case APIProtocolChatCompletions:
|
||||
return true
|
||||
|
||||
@@ -930,7 +930,7 @@ func groupMediaPricingLooksIncomplete(group *Group) bool {
|
||||
// 运营者的修复手段是配置账号级 model_mapping(映射到已定价的 CN 模型)或
|
||||
// 分组/渠道显式定价。
|
||||
func (s *OpenAIGatewayService) filterCNProviderBillingModelCandidates(ctx context.Context, account *Account, apiKey *APIKey, candidates []string) []string {
|
||||
if account == nil || (!account.IsCNProvider() && !account.IsOpenCodeGo()) {
|
||||
if account == nil || !account.IsMultiProtocolAPIKey() {
|
||||
return candidates
|
||||
}
|
||||
out := make([]string, 0, len(candidates))
|
||||
|
||||
@@ -116,3 +116,59 @@ func TestProviderProfilesUseOpenAIGateway(t *testing.T) {
|
||||
require.Equal(t, domain.PlatformGatewayOpenAI, spec.Gateway, platform)
|
||||
}
|
||||
}
|
||||
|
||||
// 转发、探测与计费中"多协议供应商"类判断改为查 profile:现有平台上与改动前的
|
||||
// 国产厂商 / 国产厂商 ∪ OpenCode 判断逐一相同。
|
||||
func TestProviderProfileAccountPredicatesMatchLegacy(t *testing.T) {
|
||||
for _, platform := range platformProbeValues {
|
||||
account := &Account{Platform: platform, Type: AccountTypeAPIKey}
|
||||
require.Equal(t, legacyIsCNProvider(platform), account.RoutesProtocolByInbound(), platform)
|
||||
require.Equal(t, legacyIsCNProvider(platform) || platform == PlatformOpenCodeGo, account.IsMultiProtocolAPIKey(), platform)
|
||||
}
|
||||
var nilAccount *Account
|
||||
require.False(t, nilAccount.RoutesProtocolByInbound())
|
||||
}
|
||||
|
||||
// 仅在 providerProfiles 中登记的新供应商按分流方式自动获得对应行为:按入站协议
|
||||
// 分流的与国产厂商一致,按模型分流的与 OpenCode 一致。
|
||||
func TestNewlyRegisteredProviderInheritsRoutingBehaviour(t *testing.T) {
|
||||
const byInbound, byModel = "test_inbound_provider", "test_model_provider"
|
||||
providerProfiles[byInbound] = &ProviderProfile{
|
||||
Platform: byInbound, DefaultMode: AccountModePayG, Routing: ProviderRoutingByInbound,
|
||||
Modes: map[string]ProviderEndpoints{AccountModePayG: {BaseURLs: map[string]string{
|
||||
APIProtocolChatCompletions: "https://inbound.example.com/v1",
|
||||
APIProtocolAnthropic: "https://inbound.example.com/anthropic",
|
||||
}}},
|
||||
}
|
||||
providerProfiles[byModel] = &ProviderProfile{
|
||||
Platform: byModel, DefaultMode: AccountModePayG, Routing: ProviderRoutingByModel,
|
||||
Modes: map[string]ProviderEndpoints{AccountModePayG: {BaseURLs: map[string]string{
|
||||
APIProtocolChatCompletions: "https://model.example.com/v1",
|
||||
}}},
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
delete(providerProfiles, byInbound)
|
||||
delete(providerProfiles, byModel)
|
||||
})
|
||||
|
||||
inbound := &Account{Platform: byInbound, Type: AccountTypeAPIKey, Credentials: map[string]any{
|
||||
"api_key": "sk-test", "api_protocol": APIProtocolAdaptive,
|
||||
"base_url": "https://inbound.example.com/v1",
|
||||
}}
|
||||
require.True(t, inbound.IsMultiProtocolAPIKey())
|
||||
require.True(t, inbound.RoutesProtocolByInbound())
|
||||
// 无原生 Responses 端点的 adaptive 账号回退 Chat Completions(与 GLM 相同)。
|
||||
require.True(t, shouldForwardOpenAIResponsesViaRawChatCompletions(inbound))
|
||||
require.True(t, shouldEstimateOpenAIInputTokensLocally(inbound))
|
||||
require.Equal(t, "https://inbound.example.com/v1", upstreamModelRegistryBaseURL(inbound))
|
||||
|
||||
model := &Account{Platform: byModel, Type: AccountTypeAPIKey, Credentials: map[string]any{
|
||||
"api_key": "sk-test", "api_protocol": APIProtocolAdaptive,
|
||||
"base_url": "https://model.example.com/v1",
|
||||
}}
|
||||
require.True(t, model.IsMultiProtocolAPIKey())
|
||||
require.False(t, model.RoutesProtocolByInbound())
|
||||
// 按模型分流:protocol_rules 决定协议,不按 Responses 能力标记回退(与 OpenCode 相同)。
|
||||
require.False(t, shouldForwardOpenAIResponsesViaRawChatCompletions(model))
|
||||
require.Equal(t, "https://model.example.com/v1", upstreamModelRegistryBaseURL(model))
|
||||
}
|
||||
|
||||
@@ -204,3 +204,12 @@ func (a *Account) routesByModel() bool {
|
||||
profile := a.providerProfile()
|
||||
return profile != nil && profile.Routing == ProviderRoutingByModel
|
||||
}
|
||||
|
||||
// RoutesProtocolByInbound 报告账号是否为按入站协议分流的多协议 API Key 供应商
|
||||
// (单一厂商型,目前即国产厂商)。这类账号共享"显式协议优先、adaptive 选同协议
|
||||
// 原生端点"的转发、Responses 能力标记与本地 token 估算逻辑;按模型分流的聚合
|
||||
// 平台由 protocol_rules 决定协议,走 routesByModel 分支。
|
||||
func (a *Account) RoutesProtocolByInbound() bool {
|
||||
profile := a.providerProfile()
|
||||
return profile != nil && profile.Routing != ProviderRoutingByModel
|
||||
}
|
||||
|
||||
@@ -1003,7 +1003,7 @@ func (s *RateLimitService) handle403(ctx context.Context, account *Account, upst
|
||||
// 国产供应商与 openai 同口径:HTML 403(CDN/代理拦截页)不构成账号失效证据,
|
||||
// 且 403 在 failover 状态集里会被逐账号重放——直接 SetError 会让一个坏请求/
|
||||
// 一层坏代理连环永久禁用整组账号。走 HTML 豁免 + N 次累计 + 临时冷却。
|
||||
if account.Platform == PlatformOpenAI || IsCNProvider(account.Platform) || account.IsOpenCodeGo() {
|
||||
if account.Platform == PlatformOpenAI || account.IsMultiProtocolAPIKey() {
|
||||
return s.handleOpenAI403(ctx, account, upstreamMsg, responseBody)
|
||||
}
|
||||
// 非 Antigravity 平台:保持原有行为
|
||||
|
||||
@@ -627,7 +627,7 @@ func (s *UpstreamBillingProbeService) probeLoadedAccount(ctx context.Context, ac
|
||||
return s.persistProbeFailure(ctx, account, intervalMinutes, now, 0, "missing_api_key", 0)
|
||||
}
|
||||
baseURL := account.GetCredential("base_url")
|
||||
if account.IsCNProvider() && account.IsAdaptiveAPIProtocol() {
|
||||
if account.RoutesProtocolByInbound() && account.IsAdaptiveAPIProtocol() {
|
||||
baseURL = account.GetCNProtocolBaseURL(APIProtocolChatCompletions)
|
||||
}
|
||||
if account.Platform == PlatformOpenAI {
|
||||
|
||||
@@ -625,7 +625,7 @@ func upstreamModelRegistryBaseURL(account *Account) string {
|
||||
return ""
|
||||
}
|
||||
switch {
|
||||
case account.IsOpenAI() || account.IsCNProvider() || account.IsOpenCodeGo():
|
||||
case account.IsOpenAI() || account.IsMultiProtocolAPIKey():
|
||||
return account.GetOpenAIFormatBaseURL()
|
||||
case account.IsGrok():
|
||||
return account.GetGrokBaseURL()
|
||||
@@ -792,9 +792,8 @@ func (s *AccountTestService) buildUpstreamModelsRequest(ctx context.Context, acc
|
||||
return s.buildAntigravityAPIKeyModelsRequest(ctx, account)
|
||||
case account.IsGrok():
|
||||
return s.buildGrokUpstreamModelsRequest(ctx, account)
|
||||
case account.IsOpenAI() || account.IsCNProvider() || account.IsOpenCodeGo():
|
||||
// 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)与 OpenCode Go
|
||||
// 复用 OpenAI /v1/models 探测。
|
||||
case account.IsOpenAI() || account.IsMultiProtocolAPIKey():
|
||||
// 多协议 API Key 供应商(国产厂商与聚合平台)复用 OpenAI /v1/models 探测。
|
||||
return s.buildOpenAIUpstreamModelsRequest(ctx, account)
|
||||
case account.IsGemini():
|
||||
return s.buildGeminiUpstreamModelsRequest(ctx, account)
|
||||
|
||||
Reference in New Issue
Block a user