Files
sub2api/backend/internal/service/openai_model_mapping.go
T
alfadb a939553c87 fix(deepseek): 模型名校验不再默认兜底,未配置映射时按官方名单校验
- 未配置 model_mapping 的 DeepSeek 账号不再放行任意模型名,改为官方名单精确匹配
  (deepseek-flash / deepseek-v4-pro / deepseek-v4-flash /
   deepseek-v4-flash-vision-exp / deepseek-v4-pro-0813)
- 校验前先归一化:剥离 Claude Code 客户端上下文后缀 [1m](含重复形式)并统一小写,
  使官方 Claude Code 接入写法 deepseek-flash[1m] 继续可用
- 未知模型名在调度阶段即拒绝,返回 404 model_not_found,避免透传上游后触发
  per-(账号,模型) 冷却、并避免上游把不认识的模型名静默兜底成 deepseek-flash
- 出站前剥离泄漏的 [1m] 后缀,仅作用于 DeepSeek 平台
- 配置了 model_mapping 的账号语义不变(映射优先)
2026-09-15 10:42:29 +08:00

140 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import "strings"
// resolveOpenAIForwardModel 解析 OpenAI 兼容转发使用的模型。
// messagesDispatchMappedModel 是调用方已为 /v1/messages 解析的显式调度结果;
// 普通 OpenAI 请求必须传空,避免将分组配置作为通用模型兜底。
func resolveOpenAIForwardModel(account *Account, requestedModel, messagesDispatchMappedModel string) string {
messagesDispatchMappedModel = strings.TrimSpace(messagesDispatchMappedModel)
if account == nil {
if messagesDispatchMappedModel != "" {
return messagesDispatchMappedModel
}
return requestedModel
}
mappedModel, matched := account.ResolveMappedModel(requestedModel)
if !matched && messagesDispatchMappedModel != "" {
return messagesDispatchMappedModel
}
return mappedModel
}
// openAIOAuthForeignModelPrefixes 列出明确属于其他厂商家族的模型名前缀。
// Codex 上游不可能服务这些模型:转发阶段 normalizeOpenAIModelForUpstream
// 对未知模型原样透传,上游必然返回不可重试的 400。
//
// 采用保守黑名单而非 Codex 模型白名单:未知/自定义别名保持「允许」,
// 以兼容渠道级模型映射等「账号选定之后才改写模型名」的部署方式
// (调度过滤看到的是改写前的原始模型名)。前缀分类的先例见
// ResolveThinkingProtocol(thinking_protocol.go)。
var openAIOAuthForeignModelPrefixes = []string{
"deepseek-",
"glm-",
"kimi-",
"moonshot-",
"qwen-",
"qwen2-",
"qwen3-",
"qwen4-",
"qwq-",
"minimax-",
"gemini-",
"gemma-",
"grok-",
"doubao-",
"hunyuan-",
"llama-",
"llama2-",
"llama3-",
"meta-llama",
"mistral-",
"mixtral-",
"baichuan-",
"ernie-",
"step-",
"seed-",
"yi-",
}
// isOpenAIOAuthServableModel 判断「空 model_mapping 的 OpenAI OAuth 账号」能否
// 服务请求模型。空映射默认仍是「允许」,仅排除明确属于其他厂商家族的模型
// (deepseek-*/glm-*、以及 Kimi Code 官方 bare ID k3 / k3-256k 等)——这类
// 请求原样透传必然被 Codex 上游以不可重试的 400 拒绝,且不触发 failover,
// 应在调度阶段就跳过该账号,把请求让给显式声明支持该模型的账号(#3662)。
// bare k3 仅精确匹配(取 last segment 后),不使用宽泛 k3- 前缀,以免误伤
// 自定义别名;显式 model_mapping 命中路径不经过本函数,语义不变。
func isOpenAIOAuthServableModel(requestedModel string) bool {
model := strings.ToLower(lastOpenAIModelSegment(requestedModel))
if model == "" {
return true // 空模型交由上层必填校验处理
}
// Kimi Code 官方 bare model ID:无厂商前缀,prefix 黑名单挡不住。
if model == "k3" || model == "k3-256k" {
return false
}
for _, prefix := range openAIOAuthForeignModelPrefixes {
if strings.HasPrefix(model, prefix) {
return false
}
}
return true
}
// deepseekServableModels 列出 DeepSeek 平台账号在「未配置 model_mapping」时
// 可服务的官方模型名(精确匹配,小写比较)。deepseek-flash 与 deepseek-v4-pro
// 为官方现行名;deepseek-v4-flash / deepseek-v4-flash-vision-exp 为处于兼容
// 路由期的旧名(上游仍接受并按 flash 价计费);deepseek-v4-pro-0813 为与
// 计费 pro 档口径一致的版本化名。
var deepseekServableModels = []string{
"deepseek-flash",
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-v4-flash-vision-exp",
"deepseek-v4-pro-0813",
}
// isDeepseekServableModel 报告 DeepSeek 平台账号在「未配置 model_mapping」时
// 可服务的模型名。官方现行模型为 deepseek-flash 与 deepseek-v4-pro;旧名
// deepseek-v4-flash / deepseek-v4-flash-vision-exp 处于兼容路由期(上游仍接受并
// 按 flash 价计费),deepseek-v4-pro-0813 为版本化名。其余一律拒绝:既避免把
// 未知模型透传给上游(上游 404/400 会触发 per-model 冷却并掩盖真实配置错误),
// 也避免上游把不认识的模型名静默兜底成 deepseek-flash。
// Source: https://api-docs.deepseek.com/quick_start/pricing
//
// https://api-docs.deepseek.com/guides/anthropic_api
func isDeepseekServableModel(requestedModel string) bool {
// 官方 Claude Code 接入文档要求 ANTHROPIC_MODEL=deepseek-flash[1m]:
// [1m] 是客户端上下文选择器,先按同一规则归一化再比对白名单。
model := strings.ToLower(normalizeClaudeCodeLongContextModel(strings.TrimSpace(requestedModel)))
if model == "" {
return true // 空模型交由上层必填校验处理
}
for _, servable := range deepseekServableModels {
if model == servable {
return true
}
}
return false
}
// resolveOpenAICompactForwardModel determines the compact-only upstream model
// for /responses/compact requests. It never affects normal /responses traffic.
// When no compact-specific mapping matches, the input model is returned as-is.
func resolveOpenAICompactForwardModel(account *Account, model string) string {
trimmedModel := strings.TrimSpace(model)
if trimmedModel == "" || account == nil {
return trimmedModel
}
mappedModel, matched := account.ResolveCompactMappedModel(trimmedModel)
if !matched {
return trimmedModel
}
if trimmedMapped := strings.TrimSpace(mappedModel); trimmedMapped != "" {
return trimmedMapped
}
return trimmedModel
}