mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(scheduler): 改用异族厂商前缀黑名单,兼容渠道级模型映射
自审发现白名单守卫有误伤面:渠道级模型映射在账号选定之后才改写 请求体模型名,调度过滤看到的是改写前的原始别名。若按 Codex 白名单 判定,「渠道映射自定义别名 → gpt-5.x + 空映射 OAuth 账号」的既有 部署会被误判为 model_not_found。 改为保守 fail-open 黑名单:空映射默认仍允许,仅排除 deepseek/glm/ kimi/qwen/gemini/grok 等明确异族厂商前缀(Codex 上游绝无可能服务, 透传必然换来不可重试 400)。前缀分类先例见 ResolveThinkingProtocol。
This commit is contained in:
@@ -752,11 +752,11 @@ func resolveRequestedModelInMapping(mapping map[string]string, requestedModel st
|
||||
// IsModelSupported 检查模型是否在 model_mapping 中(支持通配符)
|
||||
// 如果未配置 mapping,返回 true(允许所有模型)。
|
||||
//
|
||||
// 例外:OpenAI OAuth 账号(Codex 上游)的空映射不再视为「允许所有」——
|
||||
// 转发阶段 normalizeOpenAIModelForUpstream 会把未知模型原样透传,Codex 上游
|
||||
// 对 deepseek-*/glm-* 等第三方模型别名必然返回不可重试的 400,导致请求
|
||||
// 卡死在该账号上、无法 failover 到真正支持该模型的 API Key 账号(#3662)。
|
||||
// 此处的可服务判定与转发阶段的归一化行为对齐,见 isOpenAIOAuthServableModel。
|
||||
// 例外:OpenAI OAuth 账号(Codex 上游)的空映射会排除明确属于其他厂商
|
||||
// 家族的模型(deepseek-*/glm-* 等)——转发阶段 normalizeOpenAIModelForUpstream
|
||||
// 会把未知模型原样透传,Codex 上游对这类模型必然返回不可重试的 400,导致
|
||||
// 请求卡死在该账号上、无法 failover 到真正支持该模型的 API Key 账号(#3662)。
|
||||
// 未知/自定义别名仍保持允许(兼容渠道级映射),见 isOpenAIOAuthServableModel。
|
||||
func (a *Account) IsModelSupported(requestedModel string) bool {
|
||||
mapping := a.GetModelMapping()
|
||||
if len(mapping) == 0 {
|
||||
|
||||
@@ -20,32 +20,54 @@ func resolveOpenAIForwardModel(account *Account, requestedModel, defaultMappedMo
|
||||
return mappedModel
|
||||
}
|
||||
|
||||
// openAIOAuthForeignModelPrefixes 列出明确属于其他厂商家族的模型名前缀。
|
||||
// Codex 上游不可能服务这些模型:转发阶段 normalizeOpenAIModelForUpstream
|
||||
// 对未知模型原样透传,上游必然返回不可重试的 400。
|
||||
//
|
||||
// 采用保守黑名单而非 Codex 模型白名单:未知/自定义别名保持「允许」,
|
||||
// 以兼容渠道级模型映射等「账号选定之后才改写模型名」的部署方式
|
||||
// (调度过滤看到的是改写前的原始模型名)。前缀分类的先例见
|
||||
// ResolveThinkingProtocol(thinking_protocol.go)。
|
||||
var openAIOAuthForeignModelPrefixes = []string{
|
||||
"deepseek",
|
||||
"glm-",
|
||||
"kimi-",
|
||||
"moonshot",
|
||||
"qwen",
|
||||
"qwq-",
|
||||
"minimax",
|
||||
"gemini-",
|
||||
"gemma-",
|
||||
"grok-",
|
||||
"doubao-",
|
||||
"hunyuan-",
|
||||
"llama",
|
||||
"meta-llama",
|
||||
"mistral",
|
||||
"mixtral",
|
||||
"baichuan",
|
||||
"ernie-",
|
||||
"step-",
|
||||
"seed-",
|
||||
"yi-",
|
||||
}
|
||||
|
||||
// isOpenAIOAuthServableModel 判断「空 model_mapping 的 OpenAI OAuth 账号」能否
|
||||
// 服务请求模型。与转发阶段 normalizeOpenAIModelForUpstream → normalizeCodexModel
|
||||
// 的行为对齐:只有会被归一到已知 Codex 模型集合的请求(含 gpt-image-* 与
|
||||
// 推理后缀变体),或 /v1/messages 调度下有默认映射兜底的 claude-* 系列,
|
||||
// 才视为可服务。其余模型(deepseek-*/glm-* 等第三方别名)原样透传必然被
|
||||
// Codex 上游以不可重试的 400 拒绝,应在调度阶段就跳过该账号(#3662)。
|
||||
// 服务请求模型。空映射默认仍是「允许」,仅排除明确属于其他厂商家族的模型
|
||||
// (deepseek-*/glm-* 等)——这类请求原样透传必然被 Codex 上游以不可重试的
|
||||
// 400 拒绝,且不触发 failover,应在调度阶段就跳过该账号,把请求让给
|
||||
// 显式声明支持该模型的账号(#3662)。
|
||||
func isOpenAIOAuthServableModel(requestedModel string) bool {
|
||||
model := strings.TrimSpace(requestedModel)
|
||||
model := strings.ToLower(lastOpenAIModelSegment(requestedModel))
|
||||
if model == "" {
|
||||
return true // 空模型交由上层必填校验处理
|
||||
}
|
||||
// /v1/messages 调度:claude-* 系列由分组/全局默认映射兜底,见 resolveOpenAIForwardModel。
|
||||
if claudeMessagesDispatchFamily(model) != "" {
|
||||
return true
|
||||
}
|
||||
if _, ok := normalizeKnownCodexModel(model); ok {
|
||||
return true
|
||||
}
|
||||
// 兜底剥离 -low/-high 等推理后缀(gpt-5.4-high → gpt-5.4)后再试一次,
|
||||
// 与 /v1/messages 入站的 routingModel 归一化保持一致。
|
||||
if normalized := NormalizeOpenAICompatRequestedModel(model); normalized != model {
|
||||
if _, ok := normalizeKnownCodexModel(normalized); ok {
|
||||
return true
|
||||
for _, prefix := range openAIOAuthForeignModelPrefixes {
|
||||
if strings.HasPrefix(model, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// resolveOpenAICompactForwardModel determines the compact-only upstream model
|
||||
|
||||
@@ -24,14 +24,15 @@ func TestIsModelSupported_OpenAIOAuthEmptyMapping_ServableModels(t *testing.T) {
|
||||
"gpt-5.4",
|
||||
"gpt-5.4-high", // 推理后缀变体
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3-codex-xhigh",
|
||||
"gpt-5.1-codex-mini",
|
||||
"gpt-5",
|
||||
"codex-mini-latest",
|
||||
"gpt5.3codexspark", // 别名拼写归一化
|
||||
"gpt5.3codexspark", // 别名拼写
|
||||
"gpt-image-1", // 图像生成模型
|
||||
"claude-sonnet-4-6", // /v1/messages 调度默认映射兜底
|
||||
"claude-3-opus-20240229",
|
||||
"gpt-4o", // 保守 fail-open:非黑名单模型保持允许
|
||||
"my-custom-alias", // 自定义别名可能由渠道级映射在转发前改写,保持允许
|
||||
}
|
||||
for _, model := range servable {
|
||||
require.True(t, account.IsModelSupported(model), "expected %q to be servable by empty-mapping OpenAI OAuth account", model)
|
||||
@@ -41,16 +42,20 @@ func TestIsModelSupported_OpenAIOAuthEmptyMapping_ServableModels(t *testing.T) {
|
||||
func TestIsModelSupported_OpenAIOAuthEmptyMapping_RejectsForeignModels(t *testing.T) {
|
||||
account := newOpenAIOAuthAccountForModelTest()
|
||||
|
||||
// Codex 上游必然以不可重试的 400 拒绝这些模型;调度阶段就应跳过该账号,
|
||||
// 让显式声明支持的 API Key 账号接手(#3662)。
|
||||
// Codex 上游必然以不可重试的 400 拒绝这些厂商家族;调度阶段就应跳过
|
||||
// 该账号,让显式声明支持的 API Key 账号接手(#3662)。
|
||||
foreign := []string{
|
||||
"deepseek-v4",
|
||||
"deepseek-chat",
|
||||
"glm-4.7",
|
||||
"kimi-k2",
|
||||
"moonshot-v1-128k",
|
||||
"gemini-3.0-pro",
|
||||
"grok-4",
|
||||
"qwen3-max",
|
||||
"minimax-m2.5",
|
||||
"llama-3.3-70b",
|
||||
"provider/deepseek-v4", // vendor/model 形式取最后一段判定
|
||||
}
|
||||
for _, model := range foreign {
|
||||
require.False(t, account.IsModelSupported(model), "expected %q to be rejected by empty-mapping OpenAI OAuth account", model)
|
||||
@@ -98,6 +103,7 @@ func TestIsOpenAIOAuthServableModel(t *testing.T) {
|
||||
require.True(t, isOpenAIOAuthServableModel("gpt-5.4-high"))
|
||||
require.True(t, isOpenAIOAuthServableModel(" gpt-5.3-codex "))
|
||||
require.True(t, isOpenAIOAuthServableModel("claude-3-5-haiku-20241022"))
|
||||
require.False(t, isOpenAIOAuthServableModel("claude-unknown-family")) // 无 opus/sonnet/haiku 关键字,无默认映射兜底
|
||||
require.False(t, isOpenAIOAuthServableModel("deepseek-v4"))
|
||||
require.True(t, isOpenAIOAuthServableModel("DeepThink-x")) // 非黑名单前缀,保持允许
|
||||
require.False(t, isOpenAIOAuthServableModel("DeepSeek-V4")) // 大小写不敏感
|
||||
require.False(t, isOpenAIOAuthServableModel("qwen3-235b-thinking"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user