mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:06:19 +08:00
对已合并 PR #5666 + 分组入口放行后的全量功能审计发现的 P0/P1 修复, 全部先经代码与厂商文档实证再实施: 1. /v1/messages 调度闸门(P0):sanitizeGroupMessagesDispatchFields 对非 openai 平台恒置 AllowMessagesDispatch=false,而闸门豁免名单只有 grok, CN 分组经正常途径创建后恒 403——原生 Anthropic 直通(Claude Code 主用例) 完全不可达。修复:闸门对 CN 与 grok 同语义豁免;count_tokens 处的内联 裸检查统一走同一 helper;ResolveMessagesDispatchModel 对 CN 早退,避免 openai 专属的 gpt-5.x 默认映射发给 CN 上游。 2. 计费候选链(P0):候选链兜底含客户端原始模型名,配合 getFallbackPricing 的 claude→Sonnet 统一兜底,映射的 CN 模型无价时 CN 流量会按 Claude 原价 (数倍~数十倍)静默误计,且 usage 日志显示 claude-* 名无从察觉。修复: CN 账号的 claude-* 候选仅在显式分组/渠道定价时放行;候选全滤空时按 ErrModelPricingUnavailable 走零成本+告警落账(顺带修复原空候选错误会 丢弃整条 usage 记录的次生问题)。 3. 断开/中断漏记(P0,#5148 对齐,惠及 openai 平台):messages/responses/ chat_completions 三个 handler 的错误路径此前在 err!=nil 时丢弃携带的 部分 result——客户端断开排水后的完整 usage 被丢,payg 上游照常计费而 平台漏记(anthropic 网关早有同修复,openai 网关缺失)。修复:错误路径 result 非空时照常提交 usage;failover 错误恒 result=nil 无重复计费。 Responses×anthropic 流式转换器同时改为断开后继续排水至流自然结束 (末尾 message_delta 的 output_tokens 不再丢),finalize 帧补工具名反转 与客户端工具还原、仅在客户端仍连接时写出。 4. count_tokens(P1,证据修正):经实证三家 Anthropic 兼容层均无 /v1/messages/count_tokens(DeepSeek 官方文档无此端点且注明 anthropic-version 被忽略;OpenModel 标注该端点 Anthropic only), anthropic 协议转发上游=常态 404,且错误处置缺模型上下文会把不计费的 探测放大成整账号停调。修复:CN 全协议一律本地 tiktoken 估算(与 Grok 同方案),删除上游转发死代码。 5. 403 处置(P1):CN 此前落入通用 handleAuthError,单次 HTML 403(CDN/ 代理拦截页)即永久禁用,且 403 在 failover 集里会逐账号重放连环禁用 整组。修复:CN 与 openai 同口径——HTML 豁免 + 3 次累计 + 临时冷却。 新增回归测试 8 项:闸门豁免(含 openai 仍受控断言)、CN 调度映射空返回、 候选过滤三态、空候选零成本落账、断开排水 usage 完整性、HTML-403 零处罚、 结构化 403 首次临时停调。handler/service 全包测试通过。
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
|
)
|
|
|
|
const (
|
|
defaultOpenAIMessagesDispatchOpusMappedModel = "gpt-5.4"
|
|
defaultOpenAIMessagesDispatchSonnetMappedModel = "gpt-5.3-codex"
|
|
defaultOpenAIMessagesDispatchHaikuMappedModel = "gpt-5.4-mini"
|
|
)
|
|
|
|
func normalizeOpenAIMessagesDispatchMappedModel(model string) string {
|
|
model = NormalizeOpenAICompatRequestedModel(strings.TrimSpace(model))
|
|
return strings.TrimSpace(model)
|
|
}
|
|
|
|
func normalizeOpenAIMessagesDispatchModelConfig(cfg OpenAIMessagesDispatchModelConfig) OpenAIMessagesDispatchModelConfig {
|
|
out := OpenAIMessagesDispatchModelConfig{
|
|
OpusMappedModel: normalizeOpenAIMessagesDispatchMappedModel(cfg.OpusMappedModel),
|
|
SonnetMappedModel: normalizeOpenAIMessagesDispatchMappedModel(cfg.SonnetMappedModel),
|
|
HaikuMappedModel: normalizeOpenAIMessagesDispatchMappedModel(cfg.HaikuMappedModel),
|
|
}
|
|
|
|
if len(cfg.ExactModelMappings) > 0 {
|
|
out.ExactModelMappings = make(map[string]string, len(cfg.ExactModelMappings))
|
|
for requestedModel, mappedModel := range cfg.ExactModelMappings {
|
|
requestedModel = strings.TrimSpace(requestedModel)
|
|
mappedModel = normalizeOpenAIMessagesDispatchMappedModel(mappedModel)
|
|
if requestedModel == "" || mappedModel == "" {
|
|
continue
|
|
}
|
|
out.ExactModelMappings[requestedModel] = mappedModel
|
|
}
|
|
if len(out.ExactModelMappings) == 0 {
|
|
out.ExactModelMappings = nil
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func claudeMessagesDispatchFamily(model string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(model))
|
|
if !strings.HasPrefix(normalized, "claude") {
|
|
return ""
|
|
}
|
|
switch {
|
|
case strings.Contains(normalized, "opus"):
|
|
return "opus"
|
|
case strings.Contains(normalized, "sonnet"):
|
|
return "sonnet"
|
|
case strings.Contains(normalized, "haiku"):
|
|
return "haiku"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (g *Group) ResolveMessagesDispatchModel(requestedModel string) string {
|
|
if g == nil {
|
|
return ""
|
|
}
|
|
requestedModel = strings.TrimSpace(requestedModel)
|
|
if requestedModel == "" {
|
|
return ""
|
|
}
|
|
|
|
if g.Platform == PlatformGrok {
|
|
if claudeMessagesDispatchFamily(requestedModel) == "" {
|
|
return ""
|
|
}
|
|
opts := xai.RuntimeModelMappingOptions()
|
|
if !opts.EnableCrossClientMap {
|
|
return ""
|
|
}
|
|
return xai.ModelMappingWithOptions(opts)["claude-*"]
|
|
}
|
|
|
|
// 国产供应商分组:调度级模型映射不适用(其配置被 sanitize 置空,且下方的
|
|
// gpt-5.x 默认值是 openai 专属,发给 CN 上游必错)。模型改写完全交给账号级
|
|
// model_mapping;anthropic 协议上游本身接受 claude-* 模型名。
|
|
if IsCNProvider(g.Platform) {
|
|
return ""
|
|
}
|
|
|
|
cfg := normalizeOpenAIMessagesDispatchModelConfig(g.MessagesDispatchModelConfig)
|
|
if mappedModel := strings.TrimSpace(cfg.ExactModelMappings[requestedModel]); mappedModel != "" {
|
|
return mappedModel
|
|
}
|
|
|
|
switch claudeMessagesDispatchFamily(requestedModel) {
|
|
case "opus":
|
|
if mappedModel := strings.TrimSpace(cfg.OpusMappedModel); mappedModel != "" {
|
|
return mappedModel
|
|
}
|
|
return defaultOpenAIMessagesDispatchOpusMappedModel
|
|
case "sonnet":
|
|
if mappedModel := strings.TrimSpace(cfg.SonnetMappedModel); mappedModel != "" {
|
|
return mappedModel
|
|
}
|
|
return defaultOpenAIMessagesDispatchSonnetMappedModel
|
|
case "haiku":
|
|
if mappedModel := strings.TrimSpace(cfg.HaikuMappedModel); mappedModel != "" {
|
|
return mappedModel
|
|
}
|
|
return defaultOpenAIMessagesDispatchHaikuMappedModel
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func sanitizeGroupMessagesDispatchFields(g *Group) {
|
|
if g == nil || g.Platform == PlatformOpenAI {
|
|
return
|
|
}
|
|
g.AllowMessagesDispatch = false
|
|
g.DefaultMappedModel = ""
|
|
g.MessagesDispatchModelConfig = OpenAIMessagesDispatchModelConfig{}
|
|
}
|