openai_oauth_model_support_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func newOpenAIOAuthAccountForModelTest() *Account {
  8. return &Account{
  9. ID: 1,
  10. Platform: PlatformOpenAI,
  11. Type: AccountTypeOAuth,
  12. }
  13. }
  14. func TestIsModelSupported_OpenAIOAuthEmptyMapping_ServableModels(t *testing.T) {
  15. account := newOpenAIOAuthAccountForModelTest()
  16. servable := []string{
  17. "", // 空模型交由上层必填校验
  18. "gpt-5.4",
  19. "gpt-5.4-high", // 推理后缀变体
  20. "gpt-5.3-codex",
  21. "gpt-5.1-codex-mini",
  22. "gpt-5",
  23. "codex-mini-latest",
  24. "gpt5.3codexspark", // 别名拼写
  25. "gpt-image-1", // 图像生成模型
  26. "claude-sonnet-4-6", // /v1/messages 调度默认映射兜底
  27. "claude-3-opus-20240229",
  28. "gpt-4o", // 保守 fail-open:非黑名单模型保持允许
  29. "my-custom-alias", // 自定义别名可能由渠道级映射在转发前改写,保持允许
  30. }
  31. for _, model := range servable {
  32. require.True(t, account.IsModelSupported(model), "expected %q to be servable by empty-mapping OpenAI OAuth account", model)
  33. }
  34. }
  35. func TestIsModelSupported_OpenAIOAuthEmptyMapping_RejectsForeignModels(t *testing.T) {
  36. account := newOpenAIOAuthAccountForModelTest()
  37. // Codex 上游必然以不可重试的 400 拒绝这些厂商家族;调度阶段就应跳过
  38. // 该账号,让显式声明支持的 API Key 账号接手(#3662)。
  39. foreign := []string{
  40. "deepseek-v4",
  41. "deepseek-chat",
  42. "glm-4.7",
  43. "kimi-k2",
  44. "k3", // Kimi Code bare ID(无厂商前缀,需精确拒绝)
  45. "k3-256k", // Kimi Code bare ID
  46. "provider/k3", // vendor/model 取 last segment 后仍为 k3
  47. "moonshot-v1-128k",
  48. "gemini-3.0-pro",
  49. "grok-4",
  50. "qwen3-max",
  51. "minimax-m2.5",
  52. "llama-3.3-70b",
  53. "provider/deepseek-v4", // vendor/model 形式取最后一段判定
  54. }
  55. for _, model := range foreign {
  56. require.False(t, account.IsModelSupported(model), "expected %q to be rejected by empty-mapping OpenAI OAuth account", model)
  57. }
  58. }
  59. func TestIsModelSupported_OpenAIOAuthExplicitMappingUnchanged(t *testing.T) {
  60. account := newOpenAIOAuthAccountForModelTest()
  61. account.Credentials = map[string]any{
  62. "model_mapping": map[string]any{
  63. "deepseek-v4": "gpt-5.4",
  64. "k3": "gpt-5.4", // 显式映射优先:bare k3 仍可被账号声明支持
  65. },
  66. }
  67. // 显式映射沿用原有语义:命中映射即支持,未命中即不支持。
  68. require.True(t, account.IsModelSupported("deepseek-v4"))
  69. require.True(t, account.IsModelSupported("k3"))
  70. require.False(t, account.IsModelSupported("glm-4.7"))
  71. }
  72. func TestIsModelSupported_OpenAIOAuthPassthroughAllowsAll(t *testing.T) {
  73. account := newOpenAIOAuthAccountForModelTest()
  74. account.Extra = map[string]any{"openai_passthrough": true}
  75. // 透传模式仅替换认证,模型语义由上游决定,保持"允许所有"。
  76. require.True(t, account.IsModelSupported("deepseek-v4"))
  77. }
  78. func TestIsModelSupported_OpenAIOAuthPassthroughIgnoresLeftoverMapping(t *testing.T) {
  79. account := newOpenAIOAuthAccountForModelTest()
  80. account.Extra = map[string]any{"openai_passthrough": true}
  81. // 账号从"白名单模式"切到透传后,credentials 里常残留旧的非空 model_mapping。
  82. // 透传应无视该白名单,放行不在其中的模型(issue #4936);否则透传账号会被
  83. // 调度期的 IsModelSupported 排除,客户端收到 404 "not supported by any account"。
  84. account.Credentials = map[string]any{
  85. "model_mapping": map[string]any{"gpt-5.4": "gpt-5.4"},
  86. }
  87. require.True(t, account.IsModelSupported("gpt-5.6-sol"), "透传应放行不在残留白名单中的新模型")
  88. require.True(t, account.IsModelSupported("deepseek-v4"), "透传应放行任意模型")
  89. }
  90. func TestIsModelSupported_OpenAIAPIKeyEmptyMappingAllowsAll(t *testing.T) {
  91. account := &Account{
  92. ID: 2,
  93. Platform: PlatformOpenAI,
  94. Type: AccountTypeAPIKey,
  95. }
  96. // API Key 账号(第三方 OpenAI 兼容上游)可服务任意别名,语义不变。
  97. require.True(t, account.IsModelSupported("deepseek-v4"))
  98. require.True(t, account.IsModelSupported("gpt-5.4"))
  99. }
  100. func TestIsModelSupported_NonOpenAIPlatformsUnchanged(t *testing.T) {
  101. anthropic := &Account{ID: 3, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
  102. require.True(t, anthropic.IsModelSupported("claude-sonnet-4-6"))
  103. require.True(t, anthropic.IsModelSupported("deepseek-v4"))
  104. }
  105. func TestIsOpenAIOAuthServableModel(t *testing.T) {
  106. require.True(t, isOpenAIOAuthServableModel("gpt-5.4-high"))
  107. require.True(t, isOpenAIOAuthServableModel(" gpt-5.3-codex "))
  108. require.True(t, isOpenAIOAuthServableModel("claude-3-5-haiku-20241022"))
  109. require.True(t, isOpenAIOAuthServableModel("DeepThink-x")) // 非黑名单前缀,保持允许
  110. require.False(t, isOpenAIOAuthServableModel("DeepSeek-V4")) // 大小写不敏感
  111. require.False(t, isOpenAIOAuthServableModel("qwen3-235b-thinking"))
  112. require.True(t, isOpenAIOAuthServableModel("deepseekcoder")) // 无连字符 → 非黑名单前缀,保持允许
  113. require.False(t, isOpenAIOAuthServableModel("k3"))
  114. require.False(t, isOpenAIOAuthServableModel("k3-256k"))
  115. require.False(t, isOpenAIOAuthServableModel("provider/k3"))
  116. require.True(t, isOpenAIOAuthServableModel("my-k3-alias")) // 非精确 bare ID,自定义别名 fail-open
  117. }