mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
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 的账号语义不变(映射优先)
This commit is contained in:
@@ -848,6 +848,10 @@ func resolveRequestedModelInMapping(mapping map[string]string, requestedModel st
|
||||
// 会把未知模型原样透传,Codex 上游对这类模型必然返回不可重试的 400,导致
|
||||
// 请求卡死在该账号上、无法 failover 到真正支持该模型的 API Key 账号(#3662)。
|
||||
// 未知/自定义别名仍保持允许(兼容渠道级映射),见 isOpenAIOAuthServableModel。
|
||||
//
|
||||
// 例外:DeepSeek 平台的空映射不再是「允许所有」,改按官方模型白名单判定
|
||||
// (isDeepseekServableModel)——未知模型名透传上游只会得到 404/400,并误触发
|
||||
// per-(账号,模型) 30 分钟冷却;带 [1m] 上下文后缀的写法先归一化再比对。
|
||||
func (a *Account) IsModelSupported(requestedModel string) bool {
|
||||
// 透传模式仅替换认证、模型语义完全交由上游决定,因此放行所有模型。
|
||||
// 该短路必须在 model_mapping 判定之前:账号从"白名单模式"切换到透传后,
|
||||
@@ -861,6 +865,9 @@ func (a *Account) IsModelSupported(requestedModel string) bool {
|
||||
if a.IsOpenAIOAuth() {
|
||||
return isOpenAIOAuthServableModel(requestedModel)
|
||||
}
|
||||
if a.Platform == PlatformDeepseek {
|
||||
return isDeepseekServableModel(requestedModel)
|
||||
}
|
||||
return true // 无映射 = 允许所有
|
||||
}
|
||||
if mappingSupportsRequestedModel(mapping, requestedModel) {
|
||||
|
||||
@@ -231,6 +231,82 @@ func TestAccountIsModelSupported(t *testing.T) {
|
||||
requestedModel: "gemini-3-flash",
|
||||
expected: false,
|
||||
},
|
||||
|
||||
// DeepSeek 空映射:官方模型白名单(不再是「允许所有」)
|
||||
{
|
||||
name: "deepseek empty mapping allows official flash",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-flash",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping normalizes claude code long context suffix",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-flash[1m]",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping matches case-insensitively",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-FLASH",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping allows versioned pro name",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-v4-pro-0813",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping rejects retired chat model",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-chat",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping rejects foreign claude model",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "claude-sonnet-4-6",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping rejects unknown gpt model",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "gpt-future-model",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deepseek empty mapping rejects typo",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "deepseek-flas",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "deepseek explicit mapping wins over whitelist",
|
||||
platform: PlatformDeepseek,
|
||||
credentials: map[string]any{
|
||||
"model_mapping": map[string]any{
|
||||
"foo-bar": "deepseek-flash",
|
||||
},
|
||||
},
|
||||
requestedModel: "foo-bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non deepseek empty mapping still allows all",
|
||||
platform: PlatformAnthropic,
|
||||
credentials: map[string]any{},
|
||||
requestedModel: "any-model",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -1228,7 +1228,15 @@ func normalizeOpenAIModelForUpstream(account *Account, model string) string {
|
||||
if account == nil || account.UsesOpenAICodexProtocol() {
|
||||
return normalizeCodexModel(model)
|
||||
}
|
||||
return strings.TrimSpace(model)
|
||||
model = strings.TrimSpace(model)
|
||||
if account.Platform == PlatformDeepseek {
|
||||
// DeepSeek 走 OpenAI 兼容入口,不经 Anthropic 入站的 [1m] 后缀归一
|
||||
// (parseGatewayRequestCurrentBody 仅处理 PlatformAnthropic 协议)。
|
||||
// 官方 Claude Code 接入要求 ANTHROPIC_MODEL=deepseek-flash[1m] 写法,
|
||||
// 出站前剥离泄漏的客户端上下文后缀,转发规范名给上游。
|
||||
return normalizeClaudeCodeLongContextModel(model)
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
func SupportsVerbosity(model string) bool {
|
||||
|
||||
@@ -82,6 +82,43 @@ func isOpenAIOAuthServableModel(requestedModel string) bool {
|
||||
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.
|
||||
|
||||
@@ -429,6 +429,30 @@ func TestNormalizeOpenAIModelForUpstream(t *testing.T) {
|
||||
model: "gpt-4.1",
|
||||
want: "gpt-4.1",
|
||||
},
|
||||
{
|
||||
name: "deepseek strips claude code long context suffix",
|
||||
account: &Account{Type: AccountTypeAPIKey, Platform: PlatformDeepseek},
|
||||
model: "deepseek-flash[1m]",
|
||||
want: "deepseek-flash",
|
||||
},
|
||||
{
|
||||
name: "deepseek strips duplicated long context suffix",
|
||||
account: &Account{Type: AccountTypeAPIKey, Platform: PlatformDeepseek},
|
||||
model: "deepseek-flash[1M][1m]",
|
||||
want: "deepseek-flash",
|
||||
},
|
||||
{
|
||||
name: "deepseek preserves plain model",
|
||||
account: &Account{Type: AccountTypeAPIKey, Platform: PlatformDeepseek},
|
||||
model: "deepseek-flash",
|
||||
want: "deepseek-flash",
|
||||
},
|
||||
{
|
||||
name: "non deepseek preserves long context suffix",
|
||||
account: &Account{Type: AccountTypeAPIKey, Platform: PlatformOpenAI},
|
||||
model: "deepseek-flash[1m]",
|
||||
want: "deepseek-flash[1m]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user