gateway_service_antigravity_whitelist_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestGatewayService_isModelSupportedByAccount_AntigravityModelMapping(t *testing.T) {
  10. svc := &GatewayService{}
  11. // 使用 model_mapping 作为白名单(通配符匹配)
  12. account := &Account{
  13. Platform: PlatformAntigravity,
  14. Credentials: map[string]any{
  15. "model_mapping": map[string]any{
  16. "claude-*": "claude-sonnet-4-5",
  17. "gemini-3-*": "gemini-3-flash",
  18. },
  19. },
  20. }
  21. // claude-* 通配符匹配
  22. require.True(t, svc.isModelSupportedByAccount(account, "claude-sonnet-4-5"))
  23. require.True(t, svc.isModelSupportedByAccount(account, "claude-haiku-4-5"))
  24. require.True(t, svc.isModelSupportedByAccount(account, "claude-opus-4-6"))
  25. // gemini-3-* 通配符匹配
  26. require.True(t, svc.isModelSupportedByAccount(account, "gemini-3-flash"))
  27. require.True(t, svc.isModelSupportedByAccount(account, "gemini-3-pro-high"))
  28. // gemini-2.5-* 不匹配(不在 model_mapping 中)
  29. require.False(t, svc.isModelSupportedByAccount(account, "gemini-2.5-flash"))
  30. require.False(t, svc.isModelSupportedByAccount(account, "gemini-2.5-pro"))
  31. // 其他平台模型不支持
  32. require.False(t, svc.isModelSupportedByAccount(account, "gpt-4"))
  33. // 空模型允许
  34. require.True(t, svc.isModelSupportedByAccount(account, ""))
  35. }
  36. func TestGatewayService_isModelSupportedByAccount_AntigravityNoMapping(t *testing.T) {
  37. svc := &GatewayService{}
  38. // 未配置 model_mapping 时,使用默认映射(domain.DefaultAntigravityModelMapping)
  39. // 只有默认映射中的模型才被支持
  40. account := &Account{
  41. Platform: PlatformAntigravity,
  42. Credentials: map[string]any{},
  43. }
  44. // 默认映射中的模型应该被支持
  45. require.True(t, svc.isModelSupportedByAccount(account, "claude-sonnet-4-5"))
  46. require.True(t, svc.isModelSupportedByAccount(account, "gemini-3-flash"))
  47. require.True(t, svc.isModelSupportedByAccount(account, "gemini-2.5-pro"))
  48. require.True(t, svc.isModelSupportedByAccount(account, "claude-haiku-4-5"))
  49. // 不在默认映射中的模型不被支持
  50. require.False(t, svc.isModelSupportedByAccount(account, "claude-3-5-sonnet-20241022"))
  51. require.False(t, svc.isModelSupportedByAccount(account, "claude-unknown-model"))
  52. // 非 claude-/gemini- 前缀仍然不支持
  53. require.False(t, svc.isModelSupportedByAccount(account, "gpt-4"))
  54. }
  55. // TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode 测试 thinking 模式下的模型支持检查
  56. // 验证调度时使用映射后的最终模型名(包括 thinking 后缀)来检查 model_mapping 支持
  57. func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *testing.T) {
  58. svc := &GatewayService{}
  59. tests := []struct {
  60. name string
  61. modelMapping map[string]any
  62. requestedModel string
  63. thinkingEnabled bool
  64. expected bool
  65. }{
  66. // 场景 1: 只配置 claude-sonnet-4-5-thinking,请求 claude-sonnet-4-5 + thinking=true
  67. // mapAntigravityModel 找不到 claude-sonnet-4-5 的映射 → 返回 false
  68. {
  69. name: "thinking_enabled_no_base_mapping_returns_false",
  70. modelMapping: map[string]any{
  71. "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
  72. },
  73. requestedModel: "claude-sonnet-4-5",
  74. thinkingEnabled: true,
  75. expected: false,
  76. },
  77. // 场景 2: 只配置 claude-sonnet-4-5-thinking,请求 claude-sonnet-4-5 + thinking=false
  78. // mapAntigravityModel 找不到 claude-sonnet-4-5 的映射 → 返回 false
  79. {
  80. name: "thinking_disabled_no_base_mapping_returns_false",
  81. modelMapping: map[string]any{
  82. "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
  83. },
  84. requestedModel: "claude-sonnet-4-5",
  85. thinkingEnabled: false,
  86. expected: false,
  87. },
  88. // 场景 3: 配置 claude-sonnet-4-5(非 thinking),请求 claude-sonnet-4-5 + thinking=true
  89. // 最终模型名 = claude-sonnet-4-5-thinking,不在 mapping 中,应该不匹配
  90. {
  91. name: "thinking_enabled_no_match_non_thinking_mapping",
  92. modelMapping: map[string]any{
  93. "claude-sonnet-4-5": "claude-sonnet-4-5",
  94. },
  95. requestedModel: "claude-sonnet-4-5",
  96. thinkingEnabled: true,
  97. expected: false,
  98. },
  99. // 场景 4: 配置两种模型,请求 claude-sonnet-4-5 + thinking=true,应该匹配 thinking 版本
  100. {
  101. name: "both_models_thinking_enabled_matches_thinking",
  102. modelMapping: map[string]any{
  103. "claude-sonnet-4-5": "claude-sonnet-4-5",
  104. "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
  105. },
  106. requestedModel: "claude-sonnet-4-5",
  107. thinkingEnabled: true,
  108. expected: true,
  109. },
  110. // 场景 5: 配置两种模型,请求 claude-sonnet-4-5 + thinking=false,应该匹配非 thinking 版本
  111. {
  112. name: "both_models_thinking_disabled_matches_non_thinking",
  113. modelMapping: map[string]any{
  114. "claude-sonnet-4-5": "claude-sonnet-4-5",
  115. "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
  116. },
  117. requestedModel: "claude-sonnet-4-5",
  118. thinkingEnabled: false,
  119. expected: true,
  120. },
  121. // 场景 6: 通配符 claude-* 应该同时匹配 thinking 和非 thinking
  122. {
  123. name: "wildcard_matches_thinking",
  124. modelMapping: map[string]any{
  125. "claude-*": "claude-sonnet-4-5",
  126. },
  127. requestedModel: "claude-sonnet-4-5",
  128. thinkingEnabled: true,
  129. expected: true, // claude-sonnet-4-5-thinking 匹配 claude-*
  130. },
  131. // 场景 7: 只配置 thinking 变体但没有基础模型映射 → 返回 false
  132. // mapAntigravityModel 找不到 claude-opus-4-6 的映射
  133. {
  134. name: "opus_thinking_no_base_mapping_returns_false",
  135. modelMapping: map[string]any{
  136. "claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
  137. },
  138. requestedModel: "claude-opus-4-6",
  139. thinkingEnabled: true,
  140. expected: false,
  141. },
  142. }
  143. for _, tt := range tests {
  144. t.Run(tt.name, func(t *testing.T) {
  145. account := &Account{
  146. Platform: PlatformAntigravity,
  147. Credentials: map[string]any{
  148. "model_mapping": tt.modelMapping,
  149. },
  150. }
  151. ctx := context.WithValue(context.Background(), ctxkey.ThinkingEnabled, tt.thinkingEnabled)
  152. result := svc.isModelSupportedByAccountWithContext(ctx, account, tt.requestedModel)
  153. require.Equal(t, tt.expected, result,
  154. "isModelSupportedByAccountWithContext(ctx[thinking=%v], account, %q) = %v, want %v",
  155. tt.thinkingEnabled, tt.requestedModel, result, tt.expected)
  156. })
  157. }
  158. }
  159. // TestGatewayService_isModelSupportedByAccount_CustomMappingNotInDefault 测试自定义模型映射中
  160. // 不在 DefaultAntigravityModelMapping 中的模型能通过调度
  161. func TestGatewayService_isModelSupportedByAccount_CustomMappingNotInDefault(t *testing.T) {
  162. svc := &GatewayService{}
  163. // 自定义映射中包含不在默认映射中的模型
  164. account := &Account{
  165. Platform: PlatformAntigravity,
  166. Credentials: map[string]any{
  167. "model_mapping": map[string]any{
  168. "my-custom-model": "actual-upstream-model",
  169. "gpt-4o": "some-upstream-model",
  170. "llama-3-70b": "llama-3-70b-upstream",
  171. "claude-sonnet-4-5": "claude-sonnet-4-5",
  172. },
  173. },
  174. }
  175. // 自定义模型应该通过(不在 DefaultAntigravityModelMapping 中也可以)
  176. require.True(t, svc.isModelSupportedByAccount(account, "my-custom-model"))
  177. require.True(t, svc.isModelSupportedByAccount(account, "gpt-4o"))
  178. require.True(t, svc.isModelSupportedByAccount(account, "llama-3-70b"))
  179. require.True(t, svc.isModelSupportedByAccount(account, "claude-sonnet-4-5"))
  180. // 不在自定义映射中的模型不通过
  181. require.False(t, svc.isModelSupportedByAccount(account, "gpt-3.5-turbo"))
  182. require.False(t, svc.isModelSupportedByAccount(account, "unknown-model"))
  183. // 空模型允许
  184. require.True(t, svc.isModelSupportedByAccount(account, ""))
  185. }
  186. // TestGatewayService_isModelSupportedByAccountWithContext_CustomMappingThinking
  187. // 测试自定义映射 + thinking 模式的交互
  188. func TestGatewayService_isModelSupportedByAccountWithContext_CustomMappingThinking(t *testing.T) {
  189. svc := &GatewayService{}
  190. // 自定义映射同时配置基础模型和 thinking 变体
  191. account := &Account{
  192. Platform: PlatformAntigravity,
  193. Credentials: map[string]any{
  194. "model_mapping": map[string]any{
  195. "claude-sonnet-4-5": "claude-sonnet-4-5",
  196. "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
  197. "my-custom-model": "upstream-model",
  198. },
  199. },
  200. }
  201. // thinking=true: claude-sonnet-4-5 → mapped=claude-sonnet-4-5 → +thinking → check IsModelSupported(claude-sonnet-4-5-thinking)=true
  202. ctx := context.WithValue(context.Background(), ctxkey.ThinkingEnabled, true)
  203. require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "claude-sonnet-4-5"))
  204. // thinking=false: claude-sonnet-4-5 → mapped=claude-sonnet-4-5 → check IsModelSupported(claude-sonnet-4-5)=true
  205. ctx = context.WithValue(context.Background(), ctxkey.ThinkingEnabled, false)
  206. require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "claude-sonnet-4-5"))
  207. // 自定义模型(非 claude)不受 thinking 后缀影响,mapped 成功即通过
  208. ctx = context.WithValue(context.Background(), ctxkey.ThinkingEnabled, true)
  209. require.True(t, svc.isModelSupportedByAccountWithContext(ctx, account, "my-custom-model"))
  210. }