//go:build unit package service import ( "testing" "github.com/stretchr/testify/require" ) func newOpenAIOAuthAccountForModelTest() *Account { return &Account{ ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, } } func TestIsModelSupported_OpenAIOAuthEmptyMapping_ServableModels(t *testing.T) { account := newOpenAIOAuthAccountForModelTest() servable := []string{ "", // 空模型交由上层必填校验 "gpt-5.4", "gpt-5.4-high", // 推理后缀变体 "gpt-5.3-codex", "gpt-5.1-codex-mini", "gpt-5", "codex-mini-latest", "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) } } func TestIsModelSupported_OpenAIOAuthEmptyMapping_RejectsForeignModels(t *testing.T) { account := newOpenAIOAuthAccountForModelTest() // Codex 上游必然以不可重试的 400 拒绝这些厂商家族;调度阶段就应跳过 // 该账号,让显式声明支持的 API Key 账号接手(#3662)。 foreign := []string{ "deepseek-v4", "deepseek-chat", "glm-4.7", "kimi-k2", "k3", // Kimi Code bare ID(无厂商前缀,需精确拒绝) "k3-256k", // Kimi Code bare ID "provider/k3", // vendor/model 取 last segment 后仍为 k3 "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) } } func TestIsModelSupported_OpenAIOAuthExplicitMappingUnchanged(t *testing.T) { account := newOpenAIOAuthAccountForModelTest() account.Credentials = map[string]any{ "model_mapping": map[string]any{ "deepseek-v4": "gpt-5.4", "k3": "gpt-5.4", // 显式映射优先:bare k3 仍可被账号声明支持 }, } // 显式映射沿用原有语义:命中映射即支持,未命中即不支持。 require.True(t, account.IsModelSupported("deepseek-v4")) require.True(t, account.IsModelSupported("k3")) require.False(t, account.IsModelSupported("glm-4.7")) } func TestIsModelSupported_OpenAIOAuthPassthroughAllowsAll(t *testing.T) { account := newOpenAIOAuthAccountForModelTest() account.Extra = map[string]any{"openai_passthrough": true} // 透传模式仅替换认证,模型语义由上游决定,保持"允许所有"。 require.True(t, account.IsModelSupported("deepseek-v4")) } func TestIsModelSupported_OpenAIOAuthPassthroughIgnoresLeftoverMapping(t *testing.T) { account := newOpenAIOAuthAccountForModelTest() account.Extra = map[string]any{"openai_passthrough": true} // 账号从"白名单模式"切到透传后,credentials 里常残留旧的非空 model_mapping。 // 透传应无视该白名单,放行不在其中的模型(issue #4936);否则透传账号会被 // 调度期的 IsModelSupported 排除,客户端收到 404 "not supported by any account"。 account.Credentials = map[string]any{ "model_mapping": map[string]any{"gpt-5.4": "gpt-5.4"}, } require.True(t, account.IsModelSupported("gpt-5.6-sol"), "透传应放行不在残留白名单中的新模型") require.True(t, account.IsModelSupported("deepseek-v4"), "透传应放行任意模型") } func TestIsModelSupported_OpenAIAPIKeyEmptyMappingAllowsAll(t *testing.T) { account := &Account{ ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, } // API Key 账号(第三方 OpenAI 兼容上游)可服务任意别名,语义不变。 require.True(t, account.IsModelSupported("deepseek-v4")) require.True(t, account.IsModelSupported("gpt-5.4")) } func TestIsModelSupported_NonOpenAIPlatformsUnchanged(t *testing.T) { anthropic := &Account{ID: 3, Platform: PlatformAnthropic, Type: AccountTypeOAuth} require.True(t, anthropic.IsModelSupported("claude-sonnet-4-6")) require.True(t, anthropic.IsModelSupported("deepseek-v4")) } 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.True(t, isOpenAIOAuthServableModel("DeepThink-x")) // 非黑名单前缀,保持允许 require.False(t, isOpenAIOAuthServableModel("DeepSeek-V4")) // 大小写不敏感 require.False(t, isOpenAIOAuthServableModel("qwen3-235b-thinking")) require.True(t, isOpenAIOAuthServableModel("deepseekcoder")) // 无连字符 → 非黑名单前缀,保持允许 require.False(t, isOpenAIOAuthServableModel("k3")) require.False(t, isOpenAIOAuthServableModel("k3-256k")) require.False(t, isOpenAIOAuthServableModel("provider/k3")) require.True(t, isOpenAIOAuthServableModel("my-k3-alias")) // 非精确 bare ID,自定义别名 fail-open }