account_wildcard_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "github.com/Wei-Shaw/sub2api/internal/domain"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  7. )
  8. func TestGrokAccountModelMappingCacheInvalidatesWithRuntimeSettings(t *testing.T) {
  9. original := xai.RuntimeModelMappingOptions()
  10. t.Cleanup(func() { xai.SetRuntimeModelMappingOptions(original) })
  11. account := &Account{Platform: PlatformGrok, Credentials: map[string]any{}}
  12. xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{})
  13. requireMappedModel(t, account, "claude-sonnet-4-5", "claude-sonnet-4-5")
  14. xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{
  15. DefaultText: "grok-build-0.1",
  16. EnableCrossClientMap: true,
  17. })
  18. requireMappedModel(t, account, "claude-sonnet-4-5", "grok-build-0.1")
  19. }
  20. func requireMappedModel(t *testing.T, account *Account, requested, expected string) {
  21. t.Helper()
  22. if actual := account.GetMappedModel(requested); actual != expected {
  23. t.Fatalf("GetMappedModel(%q) = %q, want %q", requested, actual, expected)
  24. }
  25. }
  26. func TestMatchWildcard(t *testing.T) {
  27. tests := []struct {
  28. name string
  29. pattern string
  30. str string
  31. expected bool
  32. }{
  33. // 精确匹配
  34. {"exact match", "claude-sonnet-4-5", "claude-sonnet-4-5", true},
  35. {"exact mismatch", "claude-sonnet-4-5", "claude-opus-4-5", false},
  36. // 通配符匹配
  37. {"wildcard prefix match", "claude-*", "claude-sonnet-4-5", true},
  38. {"wildcard prefix match 2", "claude-*", "claude-opus-4-5-thinking", true},
  39. {"wildcard prefix mismatch", "claude-*", "gemini-3-flash", false},
  40. {"wildcard partial match", "gemini-3*", "gemini-3-flash", true},
  41. {"wildcard partial match 2", "gemini-3*", "gemini-3-pro-image", true},
  42. {"wildcard partial mismatch", "gemini-3*", "gemini-2.5-flash", false},
  43. // 边界情况
  44. {"empty pattern exact", "", "", true},
  45. {"empty pattern mismatch", "", "claude", false},
  46. {"single star", "*", "anything", true},
  47. {"star at end only", "abc*", "abcdef", true},
  48. {"star at end empty suffix", "abc*", "abc", true},
  49. }
  50. for _, tt := range tests {
  51. t.Run(tt.name, func(t *testing.T) {
  52. result := matchWildcard(tt.pattern, tt.str)
  53. if result != tt.expected {
  54. t.Errorf("matchWildcard(%q, %q) = %v, want %v", tt.pattern, tt.str, result, tt.expected)
  55. }
  56. })
  57. }
  58. }
  59. func TestMatchWildcardMappingResult(t *testing.T) {
  60. tests := []struct {
  61. name string
  62. mapping map[string]string
  63. requestedModel string
  64. expected string
  65. matched bool
  66. }{
  67. // 精确匹配优先于通配符
  68. {
  69. name: "exact match takes precedence",
  70. mapping: map[string]string{
  71. "claude-sonnet-4-5": "claude-sonnet-4-5-exact",
  72. "claude-*": "claude-default",
  73. },
  74. requestedModel: "claude-sonnet-4-5",
  75. expected: "claude-sonnet-4-5-exact",
  76. matched: true,
  77. },
  78. // 最长通配符优先
  79. {
  80. name: "longer wildcard takes precedence",
  81. mapping: map[string]string{
  82. "claude-*": "claude-default",
  83. "claude-sonnet-*": "claude-sonnet-default",
  84. "claude-sonnet-4*": "claude-sonnet-4-series",
  85. },
  86. requestedModel: "claude-sonnet-4-5",
  87. expected: "claude-sonnet-4-series",
  88. matched: true,
  89. },
  90. // 单个通配符
  91. {
  92. name: "single wildcard",
  93. mapping: map[string]string{
  94. "claude-*": "claude-mapped",
  95. },
  96. requestedModel: "claude-opus-4-5",
  97. expected: "claude-mapped",
  98. matched: true,
  99. },
  100. // 无匹配返回原始模型
  101. {
  102. name: "no match returns original",
  103. mapping: map[string]string{
  104. "claude-*": "claude-mapped",
  105. },
  106. requestedModel: "gemini-3-flash",
  107. expected: "gemini-3-flash",
  108. matched: false,
  109. },
  110. // 空映射返回原始模型
  111. {
  112. name: "empty mapping returns original",
  113. mapping: map[string]string{},
  114. requestedModel: "claude-sonnet-4-5",
  115. expected: "claude-sonnet-4-5",
  116. matched: false,
  117. },
  118. // Gemini 模型映射
  119. {
  120. name: "gemini wildcard mapping",
  121. mapping: map[string]string{
  122. "gemini-3*": "gemini-3-pro-high",
  123. "gemini-2.5*": "gemini-2.5-flash",
  124. },
  125. requestedModel: "gemini-3-flash-preview",
  126. expected: "gemini-3-pro-high",
  127. matched: true,
  128. },
  129. }
  130. for _, tt := range tests {
  131. t.Run(tt.name, func(t *testing.T) {
  132. result, matched := matchWildcardMappingResult(tt.mapping, tt.requestedModel)
  133. if result != tt.expected || matched != tt.matched {
  134. t.Errorf("matchWildcardMappingResult(%v, %q) = (%q, %v), want (%q, %v)", tt.mapping, tt.requestedModel, result, matched, tt.expected, tt.matched)
  135. }
  136. })
  137. }
  138. }
  139. func TestAccountIsModelSupported(t *testing.T) {
  140. tests := []struct {
  141. name string
  142. platform string
  143. credentials map[string]any
  144. requestedModel string
  145. expected bool
  146. }{
  147. // 无映射 = 允许所有
  148. {
  149. name: "no mapping allows all",
  150. credentials: nil,
  151. requestedModel: "any-model",
  152. expected: true,
  153. },
  154. {
  155. name: "empty mapping allows all",
  156. credentials: map[string]any{},
  157. requestedModel: "any-model",
  158. expected: true,
  159. },
  160. // 精确匹配
  161. {
  162. name: "exact match supported",
  163. credentials: map[string]any{
  164. "model_mapping": map[string]any{
  165. "claude-sonnet-4-5": "target-model",
  166. },
  167. },
  168. requestedModel: "claude-sonnet-4-5",
  169. expected: true,
  170. },
  171. {
  172. name: "exact match not supported",
  173. credentials: map[string]any{
  174. "model_mapping": map[string]any{
  175. "claude-sonnet-4-5": "target-model",
  176. },
  177. },
  178. requestedModel: "claude-opus-4-5",
  179. expected: false,
  180. },
  181. // 通配符匹配
  182. {
  183. name: "wildcard match supported",
  184. credentials: map[string]any{
  185. "model_mapping": map[string]any{
  186. "claude-*": "claude-sonnet-4-5",
  187. },
  188. },
  189. requestedModel: "claude-opus-4-5-thinking",
  190. expected: true,
  191. },
  192. {
  193. name: "gemini customtools alias matches normalized mapping",
  194. platform: PlatformGemini,
  195. credentials: map[string]any{
  196. "model_mapping": map[string]any{
  197. "gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
  198. },
  199. },
  200. requestedModel: "gemini-3.1-pro-preview-customtools",
  201. expected: true,
  202. },
  203. {
  204. name: "wildcard match not supported",
  205. credentials: map[string]any{
  206. "model_mapping": map[string]any{
  207. "claude-*": "claude-sonnet-4-5",
  208. },
  209. },
  210. requestedModel: "gemini-3-flash",
  211. expected: false,
  212. },
  213. }
  214. for _, tt := range tests {
  215. t.Run(tt.name, func(t *testing.T) {
  216. account := &Account{
  217. Platform: tt.platform,
  218. Credentials: tt.credentials,
  219. }
  220. result := account.IsModelSupported(tt.requestedModel)
  221. if result != tt.expected {
  222. t.Errorf("IsModelSupported(%q) = %v, want %v", tt.requestedModel, result, tt.expected)
  223. }
  224. })
  225. }
  226. }
  227. func TestAccountGetMappedModel(t *testing.T) {
  228. tests := []struct {
  229. name string
  230. platform string
  231. credentials map[string]any
  232. requestedModel string
  233. expected string
  234. }{
  235. // 无映射 = 返回原始模型
  236. {
  237. name: "no mapping returns original",
  238. credentials: nil,
  239. requestedModel: "claude-sonnet-4-5",
  240. expected: "claude-sonnet-4-5",
  241. },
  242. {
  243. name: "no mapping preserves gemini customtools model",
  244. platform: PlatformGemini,
  245. credentials: nil,
  246. requestedModel: "gemini-3.1-pro-preview-customtools",
  247. expected: "gemini-3.1-pro-preview-customtools",
  248. },
  249. // 精确匹配
  250. {
  251. name: "exact match",
  252. credentials: map[string]any{
  253. "model_mapping": map[string]any{
  254. "claude-sonnet-4-5": "target-model",
  255. },
  256. },
  257. requestedModel: "claude-sonnet-4-5",
  258. expected: "target-model",
  259. },
  260. // 通配符匹配(最长优先)
  261. {
  262. name: "wildcard longest match",
  263. credentials: map[string]any{
  264. "model_mapping": map[string]any{
  265. "claude-*": "claude-default",
  266. "claude-sonnet-*": "claude-sonnet-mapped",
  267. },
  268. },
  269. requestedModel: "claude-sonnet-4-5",
  270. expected: "claude-sonnet-mapped",
  271. },
  272. // 无匹配返回原始模型
  273. {
  274. name: "gemini customtools alias resolves through normalized mapping",
  275. platform: PlatformGemini,
  276. credentials: map[string]any{
  277. "model_mapping": map[string]any{
  278. "gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
  279. },
  280. },
  281. requestedModel: "gemini-3.1-pro-preview-customtools",
  282. expected: "gemini-3.1-pro-preview",
  283. },
  284. {
  285. name: "gemini customtools exact mapping wins over normalized fallback",
  286. platform: PlatformGemini,
  287. credentials: map[string]any{
  288. "model_mapping": map[string]any{
  289. "gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
  290. "gemini-3.1-pro-preview-customtools": "gemini-3.1-pro-preview-customtools",
  291. },
  292. },
  293. requestedModel: "gemini-3.1-pro-preview-customtools",
  294. expected: "gemini-3.1-pro-preview-customtools",
  295. },
  296. {
  297. name: "no match returns original",
  298. credentials: map[string]any{
  299. "model_mapping": map[string]any{
  300. "gemini-*": "gemini-mapped",
  301. },
  302. },
  303. requestedModel: "claude-sonnet-4-5",
  304. expected: "claude-sonnet-4-5",
  305. },
  306. }
  307. for _, tt := range tests {
  308. t.Run(tt.name, func(t *testing.T) {
  309. account := &Account{
  310. Platform: tt.platform,
  311. Credentials: tt.credentials,
  312. }
  313. result := account.GetMappedModel(tt.requestedModel)
  314. if result != tt.expected {
  315. t.Errorf("GetMappedModel(%q) = %q, want %q", tt.requestedModel, result, tt.expected)
  316. }
  317. })
  318. }
  319. }
  320. func TestAccountGetModelMapping_AntigravityNormalizesGemini31ProAliases(t *testing.T) {
  321. t.Parallel()
  322. account := &Account{
  323. Platform: PlatformAntigravity,
  324. Credentials: map[string]any{
  325. "model_mapping": map[string]any{
  326. domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel,
  327. "gemini-3.1-pro-high": "gemini-3.1-pro-high",
  328. "gemini-3.1-pro-preview": "gemini-3.1-pro-high",
  329. },
  330. },
  331. }
  332. mapping := account.GetModelMapping()
  333. if got := mapping["gemini-3.1-pro"]; got != domain.AntigravityGemini31ProAgentModel {
  334. t.Fatalf("expected gemini-3.1-pro to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got)
  335. }
  336. if got := mapping["gemini-3.1-pro-high"]; got != domain.AntigravityGemini31ProAgentModel {
  337. t.Fatalf("expected gemini-3.1-pro-high to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got)
  338. }
  339. if got := mapping["gemini-3.1-pro-preview"]; got != domain.AntigravityGemini31ProAgentModel {
  340. t.Fatalf("expected gemini-3.1-pro-preview to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got)
  341. }
  342. }
  343. func TestAccountGetModelMapping_AntigravityPreservesGemini31ProOverrides(t *testing.T) {
  344. t.Parallel()
  345. account := &Account{
  346. Platform: PlatformAntigravity,
  347. Credentials: map[string]any{
  348. "model_mapping": map[string]any{
  349. domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel,
  350. "gemini-3.1-pro-high": "custom-high",
  351. "gemini-3.1-pro-preview": "custom-preview",
  352. },
  353. },
  354. }
  355. mapping := account.GetModelMapping()
  356. if got := mapping["gemini-3.1-pro-high"]; got != "custom-high" {
  357. t.Fatalf("expected gemini-3.1-pro-high override to be preserved, got %q", got)
  358. }
  359. if got := mapping["gemini-3.1-pro-preview"]; got != "custom-preview" {
  360. t.Fatalf("expected gemini-3.1-pro-preview override to be preserved, got %q", got)
  361. }
  362. if got := mapping["gemini-3.1-pro"]; got != domain.AntigravityGemini31ProAgentModel {
  363. t.Fatalf("expected gemini-3.1-pro alias to default to %q, got %q", domain.AntigravityGemini31ProAgentModel, got)
  364. }
  365. }
  366. func TestAccountGetModelMapping_AntigravityGemini31ProAliasesRespectWildcard(t *testing.T) {
  367. t.Parallel()
  368. account := &Account{
  369. Platform: PlatformAntigravity,
  370. Credentials: map[string]any{
  371. "model_mapping": map[string]any{
  372. domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel,
  373. "gemini-3.1-*": "custom-wildcard",
  374. },
  375. },
  376. }
  377. mapping := account.GetModelMapping()
  378. if got := mapping["gemini-3.1-pro"]; got != "" {
  379. t.Fatalf("expected gemini-3.1-pro exact alias to stay unset when wildcard exists, got %q", got)
  380. }
  381. if got := mapping["gemini-3.1-pro-high"]; got != "" {
  382. t.Fatalf("expected gemini-3.1-pro-high exact alias to stay unset when wildcard exists, got %q", got)
  383. }
  384. if got := mapping["gemini-3.1-pro-preview"]; got != "" {
  385. t.Fatalf("expected gemini-3.1-pro-preview exact alias to stay unset when wildcard exists, got %q", got)
  386. }
  387. }
  388. func TestAccountResolveMappedModel(t *testing.T) {
  389. tests := []struct {
  390. name string
  391. platform string
  392. credentials map[string]any
  393. requestedModel string
  394. expectedModel string
  395. expectedMatch bool
  396. }{
  397. {
  398. name: "no mapping reports unmatched",
  399. credentials: nil,
  400. requestedModel: "gpt-5.4",
  401. expectedModel: "gpt-5.4",
  402. expectedMatch: false,
  403. },
  404. {
  405. name: "exact passthrough mapping still counts as matched",
  406. credentials: map[string]any{
  407. "model_mapping": map[string]any{
  408. "gpt-5.4": "gpt-5.4",
  409. },
  410. },
  411. requestedModel: "gpt-5.4",
  412. expectedModel: "gpt-5.4",
  413. expectedMatch: true,
  414. },
  415. {
  416. name: "wildcard passthrough mapping still counts as matched",
  417. credentials: map[string]any{
  418. "model_mapping": map[string]any{
  419. "gpt-*": "gpt-5.4",
  420. },
  421. },
  422. requestedModel: "gpt-5.4",
  423. expectedModel: "gpt-5.4",
  424. expectedMatch: true,
  425. },
  426. {
  427. name: "gemini customtools alias reports normalized match",
  428. platform: PlatformGemini,
  429. credentials: map[string]any{
  430. "model_mapping": map[string]any{
  431. "gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
  432. },
  433. },
  434. requestedModel: "gemini-3.1-pro-preview-customtools",
  435. expectedModel: "gemini-3.1-pro-preview",
  436. expectedMatch: true,
  437. },
  438. {
  439. name: "gemini customtools exact mapping reports exact match",
  440. platform: PlatformGemini,
  441. credentials: map[string]any{
  442. "model_mapping": map[string]any{
  443. "gemini-3.1-pro-preview": "gemini-3.1-pro-preview",
  444. "gemini-3.1-pro-preview-customtools": "gemini-3.1-pro-preview-customtools",
  445. },
  446. },
  447. requestedModel: "gemini-3.1-pro-preview-customtools",
  448. expectedModel: "gemini-3.1-pro-preview-customtools",
  449. expectedMatch: true,
  450. },
  451. {
  452. name: "missing mapping reports unmatched",
  453. credentials: map[string]any{
  454. "model_mapping": map[string]any{
  455. "gpt-5.2": "gpt-5.2",
  456. },
  457. },
  458. requestedModel: "gpt-5.4",
  459. expectedModel: "gpt-5.4",
  460. expectedMatch: false,
  461. },
  462. }
  463. for _, tt := range tests {
  464. t.Run(tt.name, func(t *testing.T) {
  465. account := &Account{
  466. Platform: tt.platform,
  467. Credentials: tt.credentials,
  468. }
  469. mappedModel, matched := account.ResolveMappedModel(tt.requestedModel)
  470. if mappedModel != tt.expectedModel || matched != tt.expectedMatch {
  471. t.Fatalf("ResolveMappedModel(%q) = (%q, %v), want (%q, %v)", tt.requestedModel, mappedModel, matched, tt.expectedModel, tt.expectedMatch)
  472. }
  473. })
  474. }
  475. }
  476. func TestAccountGetModelMapping_AntigravityEnsuresGeminiDefaultPassthroughs(t *testing.T) {
  477. account := &Account{
  478. Platform: PlatformAntigravity,
  479. Credentials: map[string]any{
  480. "model_mapping": map[string]any{
  481. "gemini-3-pro-high": "gemini-3.1-pro-high",
  482. },
  483. },
  484. }
  485. mapping := account.GetModelMapping()
  486. if mapping["gemini-3-flash"] != "gemini-3-flash" {
  487. t.Fatalf("expected gemini-3-flash passthrough to be auto-filled, got: %q", mapping["gemini-3-flash"])
  488. }
  489. if mapping["gemini-3.1-pro-high"] != "gemini-3.1-pro-high" {
  490. t.Fatalf("expected gemini-3.1-pro-high passthrough to be auto-filled, got: %q", mapping["gemini-3.1-pro-high"])
  491. }
  492. if mapping["gemini-3.1-pro-low"] != "gemini-3.1-pro-low" {
  493. t.Fatalf("expected gemini-3.1-pro-low passthrough to be auto-filled, got: %q", mapping["gemini-3.1-pro-low"])
  494. }
  495. }
  496. func TestAccountGetModelMapping_GoogleOneUsesConservativeDefaults(t *testing.T) {
  497. account := &Account{
  498. Platform: PlatformGemini,
  499. Type: AccountTypeOAuth,
  500. Credentials: map[string]any{
  501. "oauth_type": "google_one",
  502. },
  503. }
  504. mapping := account.GetModelMapping()
  505. for _, model := range []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"} {
  506. if mapping[model] != model {
  507. t.Fatalf("expected Google One model %q to map to itself, got %q", model, mapping[model])
  508. }
  509. }
  510. for _, model := range []string{"gemini-2.5-flash-image", "gemini-3.1-flash-image", "gemini-3.5-flash"} {
  511. if _, ok := mapping[model]; ok {
  512. t.Fatalf("did not expect unsupported Google One model %q", model)
  513. }
  514. }
  515. if account.IsModelSupported("gemini-3.5-flash") {
  516. t.Fatal("Google One defaults must not treat unsupported models as eligible")
  517. }
  518. }
  519. func TestAccountGetModelMapping_GoogleOnePreservesExplicitMapping(t *testing.T) {
  520. account := &Account{
  521. Platform: PlatformGemini,
  522. Type: AccountTypeOAuth,
  523. Credentials: map[string]any{
  524. "oauth_type": "google_one",
  525. "model_mapping": map[string]any{
  526. "custom-model": "gemini-2.5-flash",
  527. },
  528. },
  529. }
  530. mapping := account.GetModelMapping()
  531. if mapping["custom-model"] != "gemini-2.5-flash" {
  532. t.Fatalf("expected explicit Google One mapping to be preserved, got %v", mapping)
  533. }
  534. if _, ok := mapping["gemini-2.5-flash"]; ok {
  535. t.Fatalf("did not expect defaults to overwrite an explicit mapping: %v", mapping)
  536. }
  537. }
  538. func TestAccountGetModelMapping_AntigravityRespectsWildcardOverride(t *testing.T) {
  539. account := &Account{
  540. Platform: PlatformAntigravity,
  541. Credentials: map[string]any{
  542. "model_mapping": map[string]any{
  543. "gemini-3*": "gemini-3.1-pro-high",
  544. },
  545. },
  546. }
  547. mapping := account.GetModelMapping()
  548. if _, exists := mapping["gemini-3-flash"]; exists {
  549. t.Fatalf("did not expect explicit gemini-3-flash passthrough when wildcard already exists")
  550. }
  551. if _, exists := mapping["gemini-3.1-pro-high"]; exists {
  552. t.Fatalf("did not expect explicit gemini-3.1-pro-high passthrough when wildcard already exists")
  553. }
  554. if _, exists := mapping["gemini-3.1-pro-low"]; exists {
  555. t.Fatalf("did not expect explicit gemini-3.1-pro-low passthrough when wildcard already exists")
  556. }
  557. if mapped := account.GetMappedModel("gemini-3-flash"); mapped != "gemini-3.1-pro-high" {
  558. t.Fatalf("expected wildcard mapping to stay effective, got: %q", mapped)
  559. }
  560. }
  561. func TestAccountGetModelMapping_CacheInvalidatesOnCredentialsReplace(t *testing.T) {
  562. account := &Account{
  563. Credentials: map[string]any{
  564. "model_mapping": map[string]any{
  565. "claude-3-5-sonnet": "upstream-a",
  566. },
  567. },
  568. }
  569. first := account.GetModelMapping()
  570. if first["claude-3-5-sonnet"] != "upstream-a" {
  571. t.Fatalf("unexpected first mapping: %v", first)
  572. }
  573. account.Credentials = map[string]any{
  574. "model_mapping": map[string]any{
  575. "claude-3-5-sonnet": "upstream-b",
  576. },
  577. }
  578. second := account.GetModelMapping()
  579. if second["claude-3-5-sonnet"] != "upstream-b" {
  580. t.Fatalf("expected cache invalidated after credentials replace, got: %v", second)
  581. }
  582. }
  583. func TestAccountGetModelMapping_CacheInvalidatesOnMappingLenChange(t *testing.T) {
  584. rawMapping := map[string]any{
  585. "claude-sonnet": "sonnet-a",
  586. }
  587. account := &Account{
  588. Credentials: map[string]any{
  589. "model_mapping": rawMapping,
  590. },
  591. }
  592. first := account.GetModelMapping()
  593. if len(first) != 1 {
  594. t.Fatalf("unexpected first mapping length: %d", len(first))
  595. }
  596. rawMapping["claude-opus"] = "opus-b"
  597. second := account.GetModelMapping()
  598. if second["claude-opus"] != "opus-b" {
  599. t.Fatalf("expected cache invalidated after mapping len change, got: %v", second)
  600. }
  601. }
  602. func TestAccountGetModelMapping_CacheInvalidatesOnInPlaceValueChange(t *testing.T) {
  603. rawMapping := map[string]any{
  604. "claude-sonnet": "sonnet-a",
  605. }
  606. account := &Account{
  607. Credentials: map[string]any{
  608. "model_mapping": rawMapping,
  609. },
  610. }
  611. first := account.GetModelMapping()
  612. if first["claude-sonnet"] != "sonnet-a" {
  613. t.Fatalf("unexpected first mapping: %v", first)
  614. }
  615. rawMapping["claude-sonnet"] = "sonnet-b"
  616. second := account.GetModelMapping()
  617. if second["claude-sonnet"] != "sonnet-b" {
  618. t.Fatalf("expected cache invalidated after in-place value change, got: %v", second)
  619. }
  620. }