@alumbwe/anvil 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/package.json +6 -2
  2. package/src/agents/bundled-agents.generated.ts +4 -4
  3. package/src/chat.tsx +1703 -1698
  4. package/src/cli-args.ts +134 -134
  5. package/src/components/status-bar.tsx +331 -313
  6. package/src/hooks/helpers/send-message.ts +663 -654
  7. package/src/hooks/use-send-message.ts +806 -773
  8. package/src/index.tsx +473 -473
  9. package/src/state/chat-store.ts +553 -544
  10. package/src/utils/create-run-config.ts +121 -121
  11. package/src/utils/format-compact-number.ts +37 -0
  12. package/vendor/@anvil/common/package.json +27 -0
  13. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
  14. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
  15. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
  16. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
  17. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
  18. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
  19. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
  20. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
  21. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
  22. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
  23. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
  24. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
  25. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
  26. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
  27. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
  28. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
  29. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
  30. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
  31. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
  32. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
  33. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
  34. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
  35. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
  36. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
  37. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
  38. package/vendor/@anvil/common/src/actions.ts +215 -0
  39. package/vendor/@anvil/common/src/analytics-core.ts +69 -0
  40. package/vendor/@anvil/common/src/analytics.ts +93 -0
  41. package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
  42. package/vendor/@anvil/common/src/browser-actions.ts +413 -0
  43. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
  44. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
  45. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
  46. package/vendor/@anvil/common/src/constants/agents.ts +99 -0
  47. package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
  48. package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
  49. package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
  50. package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
  51. package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
  52. package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
  53. package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
  54. package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
  55. package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
  56. package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
  57. package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
  58. package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
  59. package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
  60. package/vendor/@anvil/common/src/constants/auth.ts +18 -0
  61. package/vendor/@anvil/common/src/constants/byok.ts +2 -0
  62. package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
  63. package/vendor/@anvil/common/src/constants/composio.ts +34 -0
  64. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
  65. package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
  66. package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
  67. package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
  68. package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
  69. package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
  70. package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
  71. package/vendor/@anvil/common/src/constants/images.ts +51 -0
  72. package/vendor/@anvil/common/src/constants/index.ts +7 -0
  73. package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
  74. package/vendor/@anvil/common/src/constants/limits.ts +23 -0
  75. package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
  76. package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
  77. package/vendor/@anvil/common/src/constants/paths.ts +69 -0
  78. package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
  79. package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
  80. package/vendor/@anvil/common/src/constants/skills.ts +60 -0
  81. package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
  82. package/vendor/@anvil/common/src/constants/ui.ts +25 -0
  83. package/vendor/@anvil/common/src/env-ci.ts +36 -0
  84. package/vendor/@anvil/common/src/env-process.ts +95 -0
  85. package/vendor/@anvil/common/src/env-schema.ts +95 -0
  86. package/vendor/@anvil/common/src/env.ts +24 -0
  87. package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
  88. package/vendor/@anvil/common/src/mcp/client.ts +233 -0
  89. package/vendor/@anvil/common/src/old-constants.ts +10 -0
  90. package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
  91. package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
  92. package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
  93. package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
  94. package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
  95. package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
  96. package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
  97. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
  98. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
  99. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
  100. package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
  101. package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
  102. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
  103. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
  104. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
  105. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
  106. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
  107. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
  108. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
  109. package/vendor/@anvil/common/src/testing/errors.ts +33 -0
  110. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
  111. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
  112. package/vendor/@anvil/common/src/testing/index.ts +84 -0
  113. package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
  114. package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
  115. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
  116. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
  117. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
  118. package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
  119. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
  120. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
  121. package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
  122. package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
  123. package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
  124. package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
  125. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
  126. package/vendor/@anvil/common/src/testing/setup.ts +282 -0
  127. package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
  128. package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
  129. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
  130. package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
  131. package/vendor/@anvil/common/src/tools/constants.ts +117 -0
  132. package/vendor/@anvil/common/src/tools/list.ts +190 -0
  133. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
  134. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
  135. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
  136. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
  137. package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
  138. package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
  139. package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
  140. package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
  141. package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
  142. package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
  143. package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
  144. package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
  145. package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
  146. package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
  147. package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
  148. package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
  149. package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
  150. package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
  151. package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
  152. package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
  153. package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
  154. package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
  155. package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
  156. package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
  157. package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
  158. package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
  159. package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
  160. package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
  161. package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
  162. package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
  163. package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
  164. package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
  165. package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
  166. package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
  167. package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
  168. package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
  169. package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
  170. package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
  171. package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
  172. package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
  173. package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
  174. package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
  175. package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
  176. package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
  177. package/vendor/@anvil/common/src/tools/utils.ts +24 -0
  178. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
  179. package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
  180. package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
  181. package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
  182. package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
  183. package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
  184. package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
  185. package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
  186. package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
  187. package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
  188. package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
  189. package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
  190. package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
  191. package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
  192. package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
  193. package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
  194. package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
  195. package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
  196. package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
  197. package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
  198. package/vendor/@anvil/common/src/types/function-params.ts +33 -0
  199. package/vendor/@anvil/common/src/types/grant.ts +20 -0
  200. package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
  201. package/vendor/@anvil/common/src/types/json.ts +29 -0
  202. package/vendor/@anvil/common/src/types/mcp.ts +24 -0
  203. package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
  204. package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
  205. package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
  206. package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
  207. package/vendor/@anvil/common/src/types/organization.ts +118 -0
  208. package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
  209. package/vendor/@anvil/common/src/types/publisher.ts +67 -0
  210. package/vendor/@anvil/common/src/types/session-state.ts +149 -0
  211. package/vendor/@anvil/common/src/types/skill.ts +86 -0
  212. package/vendor/@anvil/common/src/types/source.ts +11 -0
  213. package/vendor/@anvil/common/src/types/spawn.ts +13 -0
  214. package/vendor/@anvil/common/src/types/subscription.ts +67 -0
  215. package/vendor/@anvil/common/src/types/usage.ts +16 -0
  216. package/vendor/@anvil/common/src/types/util.ts +3 -0
  217. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
  218. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
  219. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
  220. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
  221. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
  222. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
  223. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
  224. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
  225. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
  226. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
  227. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
  228. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
  229. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
  230. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
  231. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
  232. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
  233. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
  234. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
  235. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
  236. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
  237. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
  238. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
  239. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
  240. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
  241. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
  242. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
  243. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
  244. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
  245. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
  246. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
  247. package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
  248. package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
  249. package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
  250. package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
  251. package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
  252. package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
  253. package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
  254. package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
  255. package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
  256. package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
  257. package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
  258. package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
  259. package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
  260. package/vendor/@anvil/common/src/util/array.ts +31 -0
  261. package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
  262. package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
  263. package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
  264. package/vendor/@anvil/common/src/util/credentials.ts +26 -0
  265. package/vendor/@anvil/common/src/util/currency.ts +25 -0
  266. package/vendor/@anvil/common/src/util/dates.ts +81 -0
  267. package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
  268. package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
  269. package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
  270. package/vendor/@anvil/common/src/util/error.ts +562 -0
  271. package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
  272. package/vendor/@anvil/common/src/util/file.ts +339 -0
  273. package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
  274. package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
  275. package/vendor/@anvil/common/src/util/log-data.ts +57 -0
  276. package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
  277. package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
  278. package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
  279. package/vendor/@anvil/common/src/util/messages.ts +685 -0
  280. package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
  281. package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
  282. package/vendor/@anvil/common/src/util/object.ts +128 -0
  283. package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
  284. package/vendor/@anvil/common/src/util/path.ts +15 -0
  285. package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
  286. package/vendor/@anvil/common/src/util/promise.ts +73 -0
  287. package/vendor/@anvil/common/src/util/random.ts +15 -0
  288. package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
  289. package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
  290. package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
  291. package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
  292. package/vendor/@anvil/common/src/util/saxy.ts +741 -0
  293. package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
  294. package/vendor/@anvil/common/src/util/skills.ts +36 -0
  295. package/vendor/@anvil/common/src/util/split-data.ts +259 -0
  296. package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
  297. package/vendor/@anvil/common/src/util/string.ts +426 -0
  298. package/vendor/@anvil/common/src/util/system-info.ts +53 -0
  299. package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
  300. package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
  301. package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
  302. package/vendor/@anvil/common/src/util/xml.ts +17 -0
  303. package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
  304. package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
  305. package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
  306. package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
  307. package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
  308. package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
  309. package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
  310. package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
  311. package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
  312. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  313. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  314. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  315. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
  316. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
  317. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
  318. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
  319. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
  320. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
  321. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
  322. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
  323. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
  324. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
  325. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
  326. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
  327. package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
  328. package/vendor/@anvil/sdk/package.json +21 -0
@@ -0,0 +1,1408 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import {
4
+ canAnvilModelSpawnGeminiThinker,
5
+ DEFAULT_ANVIL_MODEL_ID,
6
+ DEFAULT_ANVIL_WEB_MODEL_ID,
7
+ FALLBACK_ANVIL_MODEL_ID,
8
+ ANVIL_WEB_DEEMPHASIZED_MODEL_IDS,
9
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
10
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
11
+ ANVIL_DESKTOP_SESSION_LIMITS,
12
+ ANVIL_FABLE_5_MODEL_ID,
13
+ ANVIL_ENABLE_MIMO_MODELS_IN_UI,
14
+ ANVIL_GLM_V52_MODEL_ID,
15
+ ANVIL_GPT_5_6_LUNA_MAX_PRICE,
16
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
17
+ ANVIL_GPT_5_6_LUNA_PROVIDER_ROUTE,
18
+ ANVIL_GPT_5_6_LUNA_REASONING_EFFORT,
19
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
20
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
21
+ ANVIL_MUSE_SPARK_REASONING_EFFORT,
22
+ getAnvilModelReasoningEffort,
23
+ MUSE_SPARK_12_CONTRIBUTOR_UPSTREAM_MODEL_ID,
24
+ MUSE_SPARK_FALLBACK_AFTER_MS,
25
+ MUSE_SPARK_FALLBACK_MODEL_ID,
26
+ MUSE_SPARK_FALLBACK_NOTICE,
27
+ isMuseSparkModelId,
28
+ LIMITED_ANVIL_MODEL_ID,
29
+ LIMITED_ANVIL_MODEL_IDS,
30
+ ANVIL_MIMO_V25_MODEL_ID,
31
+ ANVIL_MODELS,
32
+ ANVIL_WEB_GOD_ONLY_MODELS,
33
+ ANVIL_WEB_ALL_MODELS,
34
+ ANVIL_WEB_MODELS,
35
+ ANVIL_WEB_RETIRED_PICKER_MODEL_IDS,
36
+ ANVIL_WEB_STANDARD_MODEL_IDS,
37
+ SUPPORTED_ANVIL_MODELS,
38
+ getAnvilDeploymentAvailabilityLabel,
39
+ getAnvilDesktopSessionBucket,
40
+ getAnvilModel,
41
+ getAnvilModelImageSupport,
42
+ getAnvilWebModel,
43
+ getAnvilModelsForAccessTier,
44
+ getRecommendedAnvilModelId,
45
+ getRecommendedAnvilWebModelId,
46
+ isAnvilWebDeemphasizedModelId,
47
+ isAnvilDeploymentHours,
48
+ isAnvilGlmV52ModelId,
49
+ isAnvilGpt56LunaModelId,
50
+ isAnvilLimitedOfferModelId,
51
+ isAnvilSessionModelAllowedForAccessTier,
52
+ isAnvilSessionModelAvailable,
53
+ isAnvilTracedModelId,
54
+ isAnvilWebGeoExemptModelId,
55
+ isAnvilWebSelectableModelId,
56
+ isAnvilModelId,
57
+ isAnvilMultimodalModelId,
58
+ isAnvilModelAllowedForAccessTier,
59
+ isAnvilPremiumModelId,
60
+ isAnvilWebGodOnlyModelId,
61
+ isAnvilWebRememberableModelId,
62
+ isAnvilWebModelAllowedForLimitedTier,
63
+ isAnvilWebModelId,
64
+ isAnvilWebMultimodalModelId,
65
+ isAnvilWebPremiumModelId,
66
+ resolveRememberedAnvilWebModel,
67
+ isSupportedAnvilModelId,
68
+ isAnvilSessionModelId,
69
+ resolveAnvilWebModel,
70
+ resolveAnvilWebModelForLimitedTier,
71
+ resolveAnvilModelForAccessTier,
72
+ resolveAnvilSessionModelForAccessTier,
73
+ getAnvilModelSupersededBy,
74
+ migrateSupersededAnvilModelPreference,
75
+ } from '../constants/anvil-models'
76
+ import type { AnvilModelOption } from '../constants/anvil-models'
77
+ import { minimaxModels } from '../constants/model-config'
78
+
79
+ const ANVIL_KIMI_MODEL_ID = 'moonshotai/kimi-k2.7-code'
80
+ // Both removed 2026-08-04. Held as literals, not imported constants, so these
81
+ // guards keep asserting on the WIRE ids even if a constant of the same name is
82
+ // ever reintroduced.
83
+ const ANVIL_MIMO_V25_PRO_MODEL_ID = 'mimo/mimo-v2.5-pro'
84
+ const ANVIL_CROF_GLM_V52_MODEL_ID = 'crof/glm-5.2'
85
+
86
+ const MINIMAX_M3_MODEL_ID = minimaxModels.minimaxM3
87
+
88
+ describe('anvil model availability', () => {
89
+ test('defaults to V4 Pro and falls back to V4 Flash for new clients', () => {
90
+ // The two constants answer different questions, and since the CLI/Desktop
91
+ // default moved to the premium V4 Pro 08/13 GA build (2026-08-12) they name
92
+ // different models again: the default is what we RECOMMEND, the fallback is
93
+ // what is always joinable when the premium pool is spent.
94
+ expect(DEFAULT_ANVIL_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
95
+ expect(FALLBACK_ANVIL_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
96
+ // The default is premium, so every surface that offers it has to know the
97
+ // live quota — that is the whole reason the fallback exists separately.
98
+ expect(isAnvilPremiumModelId(DEFAULT_ANVIL_MODEL_ID)).toBe(true)
99
+ expect(isAnvilPremiumModelId(FALLBACK_ANVIL_MODEL_ID)).toBe(false)
100
+ })
101
+
102
+ test('desktop concurrency splits full access into 1 premium and 3 unlimited sessions', () => {
103
+ expect(
104
+ getAnvilDesktopSessionBucket(
105
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
106
+ 'full',
107
+ ),
108
+ ).toBe('premium')
109
+ expect(
110
+ getAnvilDesktopSessionBucket(
111
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
112
+ 'full',
113
+ ),
114
+ ).toBe('unlimited')
115
+ expect(ANVIL_DESKTOP_SESSION_LIMITS).toEqual({
116
+ premium: 1,
117
+ unlimited: 3,
118
+ })
119
+ expect(
120
+ getAnvilDesktopSessionBucket(
121
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
122
+ 'limited',
123
+ ),
124
+ ).toBe('premium')
125
+ })
126
+
127
+ test('DeepSeek Pro carries the AI-training warning before selection', () => {
128
+ const deepseek = ANVIL_MODELS.find(
129
+ (m) => m.id === ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
130
+ )
131
+ expect((deepseek as { warning?: string } | undefined)?.warning).toBe(
132
+ 'May use data for AI training',
133
+ )
134
+ })
135
+
136
+ test('DeepSeek Flash carries the AI-training warning before selection', () => {
137
+ const deepseek = ANVIL_MODELS.find(
138
+ (m) => m.id === ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
139
+ )
140
+ expect((deepseek as { warning?: string } | undefined)?.warning).toBe(
141
+ 'May use data for AI training',
142
+ )
143
+ })
144
+
145
+ test('only the DeepSeek family is trace-stored in free mode; M3 has no warning', () => {
146
+ const m3 = ANVIL_MODELS.find((m) => m.id === MINIMAX_M3_MODEL_ID)
147
+ expect((m3 as { warning?: string } | undefined)?.warning).toBeUndefined()
148
+ // The DeepSeek family discloses AI training and IS stored.
149
+ expect(isAnvilTracedModelId(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)).toBe(
150
+ true,
151
+ )
152
+ expect(isAnvilTracedModelId(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
153
+ true,
154
+ )
155
+ // Everything else (incl. M3 on Fireworks) is NOT stored.
156
+ expect(isAnvilTracedModelId(MINIMAX_M3_MODEL_ID)).toBe(false)
157
+ expect(isAnvilTracedModelId(ANVIL_KIMI_MODEL_ID)).toBe(false)
158
+ expect(isAnvilTracedModelId(ANVIL_MIMO_V25_MODEL_ID)).toBe(false)
159
+ expect(isAnvilTracedModelId(null)).toBe(false)
160
+ })
161
+
162
+ test('trace storage follows machine-readable data-use metadata', () => {
163
+ const models: readonly AnvilModelOption[] = SUPPORTED_ANVIL_MODELS
164
+ for (const model of models) {
165
+ expect(isAnvilTracedModelId(model.id)).toBe(
166
+ model.dataUse === 'training',
167
+ )
168
+ expect(model.warning !== undefined).toBe(model.dataUse === 'training')
169
+ }
170
+ })
171
+
172
+ test('DeepSeek V4 Flash is selectable and non-premium', () => {
173
+ expect(ANVIL_MODELS.map((model) => model.id)).toContain(
174
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
175
+ )
176
+ expect(isAnvilModelId(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(true)
177
+ expect(isAnvilPremiumModelId(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)).toBe(
178
+ false,
179
+ )
180
+ })
181
+
182
+ test('MiMo 2.5 remains supported and follows the UI rollout flag', () => {
183
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).toContain(
184
+ ANVIL_MIMO_V25_MODEL_ID,
185
+ )
186
+
187
+ if (ANVIL_ENABLE_MIMO_MODELS_IN_UI) {
188
+ expect(ANVIL_MODELS.map((model) => model.id)).toContain(
189
+ ANVIL_MIMO_V25_MODEL_ID,
190
+ )
191
+ } else {
192
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(
193
+ ANVIL_MIMO_V25_MODEL_ID,
194
+ )
195
+ }
196
+
197
+ expect(isAnvilPremiumModelId(ANVIL_MIMO_V25_MODEL_ID)).toBe(false)
198
+ expect(getAnvilModelImageSupport(ANVIL_MIMO_V25_MODEL_ID)).toBe(true)
199
+ })
200
+
201
+ test('MiMo 2.5 Pro is fully removed from Anvil', () => {
202
+ // Retired from the client pickers 2026-07-31, server half removed
203
+ // 2026-08-04 once the tail had decayed from ~170 to ~33 daily users. Same
204
+ // two-stage shape Kimi K2.7 Code went through. Paid/BYOK MiMo Pro is
205
+ // unaffected; it never resolves through these helpers.
206
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
207
+ ANVIL_MIMO_V25_PRO_MODEL_ID,
208
+ )
209
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(
210
+ ANVIL_MIMO_V25_PRO_MODEL_ID,
211
+ )
212
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).not.toContain(
213
+ ANVIL_MIMO_V25_PRO_MODEL_ID,
214
+ )
215
+ expect(isAnvilModelId(ANVIL_MIMO_V25_PRO_MODEL_ID)).toBe(false)
216
+ expect(isSupportedAnvilModelId(ANVIL_MIMO_V25_PRO_MODEL_ID)).toBe(
217
+ false,
218
+ )
219
+ expect(isAnvilSessionModelId(ANVIL_MIMO_V25_PRO_MODEL_ID)).toBe(false)
220
+ expect(isAnvilPremiumModelId(ANVIL_MIMO_V25_PRO_MODEL_ID)).toBe(false)
221
+ // The non-Pro model must not be caught by the removal: the ids share a
222
+ // prefix, and anvilModelIdMatches only tolerates dated suffixes.
223
+ expect(isAnvilSessionModelId(ANVIL_MIMO_V25_MODEL_ID)).toBe(true)
224
+ })
225
+
226
+ test('reports image support only for known Anvil models', () => {
227
+ expect(
228
+ getAnvilModelImageSupport(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID),
229
+ ).toBe(false)
230
+ expect(getAnvilModelImageSupport(MINIMAX_M3_MODEL_ID)).toBe(true)
231
+ expect(
232
+ getAnvilModelImageSupport('vendor/new-vision-model'),
233
+ ).toBeUndefined()
234
+
235
+ for (const model of SUPPORTED_ANVIL_MODELS) {
236
+ expect(isAnvilMultimodalModelId(model.id)).toBe(model.multimodal)
237
+ }
238
+ for (const model of ANVIL_WEB_ALL_MODELS) {
239
+ expect(isAnvilWebMultimodalModelId(model.id)).toBe(model.multimodal)
240
+ }
241
+ })
242
+
243
+ test('Kimi K2.7 Code is fully removed from Anvil', () => {
244
+ // Removed 2026-07-31 (client pickers went first, on 2026-07-30). The server
245
+ // half is gone too, so a stale client selection is no longer admitted —
246
+ // that tail was still spending ~$2.3k/day. Paid/BYOK Kimi is unaffected;
247
+ // it never resolves through these helpers.
248
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
249
+ ANVIL_KIMI_MODEL_ID,
250
+ )
251
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(
252
+ ANVIL_KIMI_MODEL_ID,
253
+ )
254
+ expect(
255
+ getAnvilModelsForAccessTier('full').map((m) => m.id),
256
+ ).not.toContain(ANVIL_KIMI_MODEL_ID)
257
+ expect(isAnvilModelId(ANVIL_KIMI_MODEL_ID)).toBe(false)
258
+ expect(isSupportedAnvilModelId(ANVIL_KIMI_MODEL_ID)).toBe(false)
259
+ expect(getAnvilWebModel(ANVIL_KIMI_MODEL_ID).id).toBe(
260
+ FALLBACK_ANVIL_MODEL_ID,
261
+ )
262
+ expect(isAnvilPremiumModelId(ANVIL_KIMI_MODEL_ID)).toBe(false)
263
+ expect(
264
+ isAnvilModelAllowedForAccessTier(ANVIL_KIMI_MODEL_ID, 'full'),
265
+ ).toBe(false)
266
+ expect(
267
+ resolveAnvilModelForAccessTier(ANVIL_KIMI_MODEL_ID, 'full'),
268
+ ).toBe(FALLBACK_ANVIL_MODEL_ID)
269
+ // Session admission no longer accepts it either, so live stale sessions
270
+ // resolve to the fallback instead of continuing on Kimi.
271
+ expect(
272
+ isAnvilSessionModelAllowedForAccessTier(
273
+ ANVIL_KIMI_MODEL_ID,
274
+ 'full',
275
+ ),
276
+ ).toBe(false)
277
+ expect(
278
+ resolveAnvilSessionModelForAccessTier(ANVIL_KIMI_MODEL_ID, 'full', {
279
+ includeGodOnly: false,
280
+ }),
281
+ ).toBe(FALLBACK_ANVIL_MODEL_ID)
282
+ // Retired K2.6 is no longer a anvil model; stale saved selections must
283
+ // fall back rather than be admitted.
284
+ expect(isSupportedAnvilModelId('moonshotai/kimi-k2.6')).toBe(false)
285
+ expect(
286
+ isAnvilModelAllowedForAccessTier('moonshotai/kimi-k2.6', 'full'),
287
+ ).toBe(false)
288
+ expect(
289
+ resolveAnvilModelForAccessTier('moonshotai/kimi-k2.6', 'full'),
290
+ ).not.toBe('moonshotai/kimi-k2.6')
291
+ })
292
+
293
+ test('both HY3 routes are fully removed from Anvil', () => {
294
+ // HY3 was withdrawn from the Web picker during the initial rollout and left
295
+ // in ANVIL_WEB_RETIRED_PICKER_MODEL_IDS, which is a client-side filter
296
+ // and therefore not a gate at all — the same mistake that let the CrofAI
297
+ // GLM route be farmed. Removed outright 2026-08-04, along with the
298
+ // god-only paid OpenRouter route.
299
+ //
300
+ // As of 2026-08-07 the wire-id CONSTANTS are gone too: hy3-fallback.ts and
301
+ // the Atlas Cloud adapter that was its paid lane have been deleted, so
302
+ // nothing routes `tencent/hy3` on any path, paid or free. The slugs are
303
+ // spelled out literally here precisely because no constant remains to
304
+ // import — that is the point of the test.
305
+ for (const hy3Id of ['tencent/hy3:free', 'tencent/hy3']) {
306
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(hy3Id)
307
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
308
+ hy3Id,
309
+ )
310
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).not.toContain(hy3Id)
311
+ expect(
312
+ ANVIL_WEB_GOD_ONLY_MODELS.map((model) => model.id),
313
+ ).not.toContain(hy3Id)
314
+ expect(ANVIL_WEB_ALL_MODELS.map((model) => model.id)).not.toContain(
315
+ hy3Id,
316
+ )
317
+
318
+ expect(isAnvilModelId(hy3Id)).toBe(false)
319
+ expect(isSupportedAnvilModelId(hy3Id)).toBe(false)
320
+ expect(isAnvilWebModelId(hy3Id, { includeGodOnly: true })).toBe(false)
321
+ expect(isAnvilWebGodOnlyModelId(hy3Id)).toBe(false)
322
+ expect(isAnvilSessionModelId(hy3Id)).toBe(false)
323
+ // No pool may meter it, in either direction: premium would hand it out
324
+ // free, and standard would leave it unlimited.
325
+ expect(isAnvilWebPremiumModelId(hy3Id)).toBe(false)
326
+ expect(isAnvilPremiumModelId(hy3Id)).toBe(false)
327
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(hy3Id)
328
+ // A stale saved selection downgrades rather than resolving to itself.
329
+ expect(resolveAnvilWebModel(hy3Id, { includeGodOnly: true })).toBe(
330
+ FALLBACK_ANVIL_MODEL_ID,
331
+ )
332
+ expect(getAnvilWebModel(hy3Id).id).toBe(FALLBACK_ANVIL_MODEL_ID)
333
+ }
334
+ })
335
+
336
+ test('the picker-retirement list is empty, and that is deliberate', () => {
337
+ // Both former occupants (HY3, CrofAI GLM 5.2) were farmed or left publicly
338
+ // advertised precisely because a picker-only retirement is a UI change, not
339
+ // a gate. If this fails, something was parked here instead of removed —
340
+ // check that the id being reachable by a direct API caller is actually
341
+ // harmless before accepting it.
342
+ expect(ANVIL_WEB_RETIRED_PICKER_MODEL_IDS).toEqual([])
343
+ for (const model of ANVIL_WEB_ALL_MODELS) {
344
+ expect(isAnvilWebSelectableModelId(model.id)).toBe(true)
345
+ }
346
+ })
347
+
348
+ test('GLM 5.2 is referral-only and reachable by exactly one model id', () => {
349
+ // The earned route stays selectable — removing the other GLM route must
350
+ // never take this one down with it.
351
+ expect(isAnvilWebSelectableModelId(ANVIL_GLM_V52_MODEL_ID)).toBe(true)
352
+ // Every other web model is unaffected.
353
+ expect(
354
+ isAnvilWebSelectableModelId(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID),
355
+ ).toBe(true)
356
+ })
357
+
358
+ test('CLI access-tier resolver preserves GLM at every tier', () => {
359
+ expect(
360
+ resolveAnvilModelForAccessTier(ANVIL_GLM_V52_MODEL_ID, 'full'),
361
+ ).toBe(ANVIL_GLM_V52_MODEL_ID)
362
+ // Since bounties (2026-08-03), GLM survives the limited-tier coercion: a
363
+ // bounty-earned session is redeemable in every region. The entitlement
364
+ // gate moved DOWN into the GLM quota pool, which at limited tier counts
365
+ // ONLY grants minted redeemable_at_limited_tier — referral GLM still buys
366
+ // a limited-tier user nothing. Coercing here instead would rewrite a
367
+ // deliberate pick to DeepSeek and strand the session they earned.
368
+ expect(
369
+ resolveAnvilModelForAccessTier(ANVIL_GLM_V52_MODEL_ID, 'limited'),
370
+ ).toBe(ANVIL_GLM_V52_MODEL_ID)
371
+ // Everything else still collapses to the limited model.
372
+ expect(
373
+ resolveAnvilModelForAccessTier(ANVIL_KIMI_MODEL_ID, 'limited'),
374
+ ).toBe(LIMITED_ANVIL_MODEL_ID)
375
+ })
376
+
377
+ test('the CrofAI GLM 5.2 wire id is fully removed', () => {
378
+ // Retired from the pickers 2026-07-30 and deleted 2026-08-04. The picker
379
+ // retirement was client-side only, so hand-written API callers kept
380
+ // admitting sessions on this id and drawing GLM 5.2 from the free daily
381
+ // PREMIUM pool instead of the earned GLM pool — 12-49 distinct accounts a
382
+ // day, five days after it was supposedly unreachable. No shipped client
383
+ // ever bundled it, so deleting it breaks nothing.
384
+ //
385
+ // The invariant this guards: GLM 5.2 must have exactly ONE wire id. The
386
+ // quota pool is chosen by model id, so a second id is a second entitlement.
387
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).not.toContain(
388
+ ANVIL_CROF_GLM_V52_MODEL_ID,
389
+ )
390
+ expect(ANVIL_WEB_ALL_MODELS.map((model) => model.id)).not.toContain(
391
+ ANVIL_CROF_GLM_V52_MODEL_ID,
392
+ )
393
+ expect(isAnvilWebModelId(ANVIL_CROF_GLM_V52_MODEL_ID)).toBe(false)
394
+ expect(isAnvilSessionModelId(ANVIL_CROF_GLM_V52_MODEL_ID)).toBe(false)
395
+ // Critically: it must not be metered by the free daily premium pool, which
396
+ // is the door this whole removal closes.
397
+ expect(isAnvilWebPremiumModelId(ANVIL_CROF_GLM_V52_MODEL_ID)).toBe(
398
+ false,
399
+ )
400
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(
401
+ ANVIL_CROF_GLM_V52_MODEL_ID,
402
+ )
403
+ // A stale saved selection downgrades to the always-available fallback.
404
+ expect(resolveAnvilWebModel(ANVIL_CROF_GLM_V52_MODEL_ID)).toBe(
405
+ FALLBACK_ANVIL_MODEL_ID,
406
+ )
407
+ // The earned route is untouched.
408
+ expect(isAnvilGlmV52ModelId(ANVIL_GLM_V52_MODEL_ID)).toBe(true)
409
+ expect(isAnvilSessionModelId(ANVIL_GLM_V52_MODEL_ID)).toBe(true)
410
+ })
411
+
412
+ test('GLM 5.2 is never remembered as the default model', () => {
413
+ // GLM runs out long before the rest of the picker, so remembering it would
414
+ // strand a new thread / app / page load on a model that fails admission.
415
+ expect(isAnvilWebRememberableModelId(ANVIL_GLM_V52_MODEL_ID)).toBe(
416
+ false,
417
+ )
418
+ expect(resolveRememberedAnvilWebModel(ANVIL_GLM_V52_MODEL_ID)).toBe(
419
+ DEFAULT_ANVIL_WEB_MODEL_ID,
420
+ )
421
+ // Retired picker models self-heal to the always-available fallback, while
422
+ // god-only models remain rememberable when the caller opts in.
423
+ expect(
424
+ resolveRememberedAnvilWebModel(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID),
425
+ ).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
426
+ expect(resolveRememberedAnvilWebModel(ANVIL_KIMI_MODEL_ID)).toBe(
427
+ FALLBACK_ANVIL_MODEL_ID,
428
+ )
429
+ expect(
430
+ resolveRememberedAnvilWebModel(ANVIL_KIMI_K3_ECO_MODEL_ID, {
431
+ includeGodOnly: true,
432
+ }),
433
+ ).toBe(ANVIL_KIMI_K3_ECO_MODEL_ID)
434
+ // A retired/unknown saved id keeps the pre-existing resolution: the
435
+ // always-available fallback, not the premium default.
436
+ expect(resolveRememberedAnvilWebModel('some/retired-model')).toBe(
437
+ FALLBACK_ANVIL_MODEL_ID,
438
+ )
439
+ })
440
+
441
+ test('every Web picker model falls into exactly one quota group', () => {
442
+ // The Web/Cloud picker groups rows by these two predicates (referral GLM,
443
+ // premium) and treats the remainder as Standard. Each group is metered by a
444
+ // different pool, so a model matching both — or a premium model matching
445
+ // neither and silently landing in the free Standard group — is a quota bug,
446
+ // not a cosmetic one.
447
+ for (const model of ANVIL_WEB_MODELS) {
448
+ const groups = [
449
+ isAnvilGlmV52ModelId(model.id),
450
+ isAnvilWebPremiumModelId(model.id),
451
+ ].filter(Boolean)
452
+ expect({ id: model.id, groups: groups.length }).toEqual({
453
+ id: model.id,
454
+ // Zero groups means the Standard pool, which is only correct for a
455
+ // model that is not marked premium.
456
+ groups: model.premium ? 1 : 0,
457
+ })
458
+ }
459
+ })
460
+
461
+ test('the removed CrofAI GLM 5.2 id is admitted at no access tier', () => {
462
+ for (const tier of ['limited', 'full'] as const) {
463
+ expect(
464
+ isAnvilSessionModelAllowedForAccessTier(
465
+ ANVIL_CROF_GLM_V52_MODEL_ID,
466
+ tier,
467
+ ),
468
+ ).toBe(false)
469
+ }
470
+ expect(
471
+ isAnvilWebModelAllowedForLimitedTier(ANVIL_CROF_GLM_V52_MODEL_ID),
472
+ ).toBe(false)
473
+ expect(isAnvilWebGeoExemptModelId(ANVIL_CROF_GLM_V52_MODEL_ID)).toBe(
474
+ false,
475
+ )
476
+ expect(
477
+ resolveAnvilWebModelForLimitedTier(ANVIL_CROF_GLM_V52_MODEL_ID),
478
+ ).toBe(LIMITED_ANVIL_MODEL_ID)
479
+ })
480
+
481
+ test('bounty GLM 5.2 survives the Web limited-tier coercion', () => {
482
+ // Regression: this coercion ran BEFORE the quota pool got a say, so a
483
+ // limited-region user who had earned a bounty session had their pick
484
+ // rewritten to the flash model and could never spend the reward. The
485
+ // entitlement gate is the GLM pool (bounty grants only) — not this
486
+ // allowlist, which is purely about what the picker may display.
487
+ expect(
488
+ isAnvilWebModelAllowedForLimitedTier(ANVIL_GLM_V52_MODEL_ID),
489
+ ).toBe(true)
490
+ expect(
491
+ resolveAnvilWebModelForLimitedTier(ANVIL_GLM_V52_MODEL_ID),
492
+ ).toBe(ANVIL_GLM_V52_MODEL_ID)
493
+
494
+ // The CrofAI GLM route is a paid premium model, NOT the earned one, and
495
+ // must stay coerced away — the two ids are easy to confuse.
496
+ expect(
497
+ isAnvilWebModelAllowedForLimitedTier(ANVIL_CROF_GLM_V52_MODEL_ID),
498
+ ).toBe(false)
499
+ })
500
+
501
+ test('Kimi K3 is a god-only Anvil Web/Cloud test model', () => {
502
+ // The wire id must keep the `crof/` prefix and the `-eco` build suffix:
503
+ // isCrofModel keys off the exact id, and CrofAI also serves a full
504
+ // `kimi-k3` at twice the price. See kimi-k3-god-only.test.ts.
505
+ expect(ANVIL_KIMI_K3_ECO_MODEL_ID).toBe('crof/kimi-k3-eco')
506
+
507
+ expect(ANVIL_WEB_GOD_ONLY_MODELS.map((model) => model.id)).toContain(
508
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
509
+ )
510
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).not.toContain(
511
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
512
+ )
513
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
514
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
515
+ )
516
+
517
+ expect(isAnvilWebModelId(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(false)
518
+ expect(
519
+ isAnvilWebModelId(ANVIL_KIMI_K3_ECO_MODEL_ID, {
520
+ includeGodOnly: true,
521
+ }),
522
+ ).toBe(true)
523
+ expect(isAnvilWebGodOnlyModelId(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(
524
+ true,
525
+ )
526
+ expect(isAnvilWebPremiumModelId(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(
527
+ true,
528
+ )
529
+ // Never reachable from the CLI/Desktop picker or a limited-tier browser.
530
+ expect(isAnvilPremiumModelId(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(false)
531
+ expect(isAnvilModelId(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(false)
532
+ expect(
533
+ isAnvilWebModelAllowedForLimitedTier(ANVIL_KIMI_K3_ECO_MODEL_ID),
534
+ ).toBe(false)
535
+
536
+ expect(resolveAnvilWebModel(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(
537
+ FALLBACK_ANVIL_MODEL_ID,
538
+ )
539
+ expect(
540
+ resolveAnvilWebModel(ANVIL_KIMI_K3_ECO_MODEL_ID, {
541
+ includeGodOnly: true,
542
+ }),
543
+ ).toBe(ANVIL_KIMI_K3_ECO_MODEL_ID)
544
+
545
+ const model = getAnvilWebModel(ANVIL_KIMI_K3_ECO_MODEL_ID)
546
+ // 'Kimi K3', not 'Kimi K3 Eco' — deliberate, see kimi-k3-god-only.test.ts.
547
+ expect(model.displayName).toBe('Kimi K3')
548
+ expect(model.tagline).toBe('Via CrofAI')
549
+ expect(model.experimental).toBe(true)
550
+ expect(model.multimodal).toBe(false)
551
+ expect(getAnvilModelImageSupport(ANVIL_KIMI_K3_ECO_MODEL_ID)).toBe(
552
+ false,
553
+ )
554
+ })
555
+
556
+ test('Ling 3.0 Flash and Greg 2 are fully removed from Anvil', () => {
557
+ // All three were god-only test rows, removed 2026-08-07. Spelled literally
558
+ // because no constant remains to import.
559
+ for (const removedId of [
560
+ 'inclusionai/ling-3.0-flash:free',
561
+ 'crof/greg-2-ultra',
562
+ 'crof/greg-2-super',
563
+ ]) {
564
+ expect(ANVIL_WEB_ALL_MODELS.map((model) => model.id)).not.toContain(
565
+ removedId,
566
+ )
567
+ expect(
568
+ ANVIL_WEB_GOD_ONLY_MODELS.map((model) => model.id),
569
+ ).not.toContain(removedId)
570
+ expect(isAnvilWebModelId(removedId, { includeGodOnly: true })).toBe(
571
+ false,
572
+ )
573
+ expect(isAnvilWebGodOnlyModelId(removedId)).toBe(false)
574
+ expect(isAnvilSessionModelId(removedId)).toBe(false)
575
+ // No pool may still meter them, in either direction.
576
+ expect(isAnvilWebPremiumModelId(removedId)).toBe(false)
577
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(removedId)
578
+ expect(resolveAnvilWebModel(removedId, { includeGodOnly: true })).toBe(
579
+ FALLBACK_ANVIL_MODEL_ID,
580
+ )
581
+ }
582
+ })
583
+
584
+ test('KAT Coder Pro V2 is fully retired from Anvil Web and Cloud', () => {
585
+ const retiredKatModelId = 'kwaipilot/kat-coder-pro-v2'
586
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).not.toContain(
587
+ retiredKatModelId,
588
+ )
589
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
590
+ retiredKatModelId,
591
+ )
592
+ expect(isAnvilWebModelId(retiredKatModelId)).toBe(false)
593
+ expect(isAnvilWebPremiumModelId(retiredKatModelId)).toBe(false)
594
+ expect(resolveAnvilWebModel(retiredKatModelId)).toBe(
595
+ FALLBACK_ANVIL_MODEL_ID,
596
+ )
597
+ })
598
+
599
+ test('MiniMax M2.7 support is fully removed', () => {
600
+ const legacyMinimaxM27 = 'minimax/minimax-m2.7'
601
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
602
+ legacyMinimaxM27,
603
+ )
604
+ expect(isAnvilModelId(legacyMinimaxM27)).toBe(false)
605
+ expect(isSupportedAnvilModelId(legacyMinimaxM27)).toBe(false)
606
+ expect(isAnvilModelAllowedForAccessTier(legacyMinimaxM27, 'full')).toBe(
607
+ false,
608
+ )
609
+ // Old clients with a saved M2.7 selection resolve to the fallback model.
610
+ expect(resolveAnvilModelForAccessTier(legacyMinimaxM27, 'full')).toBe(
611
+ FALLBACK_ANVIL_MODEL_ID,
612
+ )
613
+ })
614
+
615
+ test('MiniMax M3 is a selectable premium model on the standard daily pool', () => {
616
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).toContain(
617
+ MINIMAX_M3_MODEL_ID,
618
+ )
619
+ expect(ANVIL_MODELS.map((model) => model.id)).toContain(
620
+ MINIMAX_M3_MODEL_ID,
621
+ )
622
+ expect(getAnvilModelsForAccessTier('full').map((m) => m.id)).toContain(
623
+ MINIMAX_M3_MODEL_ID,
624
+ )
625
+ expect(isAnvilModelId(MINIMAX_M3_MODEL_ID)).toBe(true)
626
+ expect(isSupportedAnvilModelId(MINIMAX_M3_MODEL_ID)).toBe(true)
627
+ expect(isAnvilPremiumModelId(MINIMAX_M3_MODEL_ID)).toBe(true)
628
+ expect(isAnvilWebPremiumModelId(MINIMAX_M3_MODEL_ID)).toBe(true)
629
+ expect(
630
+ isAnvilModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'full'),
631
+ ).toBe(true)
632
+ // DeepSeek V4 Pro is the recommended default (2026-08-12), so it leads the
633
+ // picker list, with the unlimited Flash fallback right behind it and Luna
634
+ // ahead of M3.
635
+ expect(ANVIL_MODELS[0]!.id).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
636
+ expect(ANVIL_MODELS[1]!.id).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
637
+ expect(ANVIL_MODELS[2]!.id).toBe(ANVIL_GPT_5_6_LUNA_MODEL_ID)
638
+ expect(ANVIL_MODELS[3]!.id).toBe(MINIMAX_M3_MODEL_ID)
639
+ })
640
+
641
+ test('the recommended default leads ANVIL_MODELS, with Flash second', () => {
642
+ // ANVIL_MODELS order IS the picker row order, and it went stale once
643
+ // when the default flipped without reordering — the rows led with Flash
644
+ // while the recommendation already named Pro. Pin the two lead positions
645
+ // to the constants that drive the recommendation, so a future default
646
+ // change can't silently leave this list behind.
647
+ expect(ANVIL_MODELS[0]!.id).toBe(DEFAULT_ANVIL_MODEL_ID)
648
+ expect(ANVIL_MODELS[1]!.id).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
649
+ })
650
+
651
+ test('GPT-5.6 Luna is a premium model on every full-access surface', () => {
652
+ // The wire id must stay OpenRouter's own slug: getChatCompletionsProvider
653
+ // has no Luna branch, so it only reaches OpenRouter by falling through to
654
+ // the default route with the slug intact.
655
+ expect(ANVIL_GPT_5_6_LUNA_MODEL_ID).toBe('openai/gpt-5.6-luna')
656
+
657
+ // CLI/Desktop picker, Web/Cloud picker, and the session/chat layers.
658
+ expect(ANVIL_MODELS.map((model) => model.id)).toContain(
659
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
660
+ )
661
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).toContain(
662
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
663
+ )
664
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).toContain(
665
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
666
+ )
667
+ expect(getAnvilModelsForAccessTier('full').map((m) => m.id)).toContain(
668
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
669
+ )
670
+ // Everyone on the tier can pick it — it is not god-only and not retired.
671
+ expect(isAnvilWebGodOnlyModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(
672
+ false,
673
+ )
674
+ expect(isAnvilWebSelectableModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(
675
+ true,
676
+ )
677
+
678
+ // Metered by the SHARED daily premium pool on every surface, not a pool of
679
+ // its own and never the free standard browser pool.
680
+ expect(isAnvilPremiumModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(true)
681
+ expect(isAnvilWebPremiumModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(
682
+ true,
683
+ )
684
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(
685
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
686
+ )
687
+ expect(isAnvilGlmV52ModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(false)
688
+ // Dated snapshots can't dodge the premium quota or the pinned routing.
689
+ expect(
690
+ isAnvilPremiumModelId(`${ANVIL_GPT_5_6_LUNA_MODEL_ID}-20260709`),
691
+ ).toBe(true)
692
+ expect(
693
+ isAnvilGpt56LunaModelId(`${ANVIL_GPT_5_6_LUNA_MODEL_ID}-20260709`),
694
+ ).toBe(true)
695
+ expect(isAnvilGpt56LunaModelId(ANVIL_MIMO_V25_MODEL_ID)).toBe(false)
696
+
697
+ const model = getAnvilWebModel(ANVIL_GPT_5_6_LUNA_MODEL_ID)
698
+ expect(model.displayName).toBe('GPT-5.6 Luna')
699
+ // OpenAI's API does not train on request data, so no warning and no
700
+ // trace storage — and it accepts images.
701
+ expect(model.dataUse).toBe('service')
702
+ expect(model.warning).toBeUndefined()
703
+ expect(isAnvilTracedModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(false)
704
+ expect(getAnvilModelImageSupport(ANVIL_GPT_5_6_LUNA_MODEL_ID)).toBe(
705
+ true,
706
+ )
707
+ // Cheap per token, so it is not one of the muted "costly premium" rows.
708
+ expect(
709
+ isAnvilWebDeemphasizedModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID),
710
+ ).toBe(false)
711
+
712
+ // Limited regions stay geo-gated to the two limited-tier models.
713
+ expect(
714
+ isAnvilWebModelAllowedForLimitedTier(ANVIL_GPT_5_6_LUNA_MODEL_ID),
715
+ ).toBe(false)
716
+ expect(
717
+ isAnvilModelAllowedForAccessTier(
718
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
719
+ 'limited',
720
+ ),
721
+ ).toBe(false)
722
+ })
723
+
724
+ test('GPT-5.6 Luna carries its pinned OpenAI route, price ceiling, and effort', () => {
725
+ // These three constants are the contract web/src/llm-api/openrouter.ts
726
+ // enforces on every Luna request.
727
+ expect(ANVIL_GPT_5_6_LUNA_PROVIDER_ROUTE).toBe('openai')
728
+ expect(ANVIL_GPT_5_6_LUNA_REASONING_EFFORT).toBe('high')
729
+
730
+ // The ceiling is a cost fence, and both bounds are load-bearing. OpenRouter
731
+ // compares strictly, so a ceiling AT OpenAI's $0.10/$0.60 list price 404s
732
+ // every request ("No endpoints found that satisfy the max price") — that
733
+ // shipped on 2026-07-30 and took Luna down until it was raised. It must
734
+ // also stay well under the $1.00/$6.00 Azure/Bedrock charge, which is the
735
+ // 10x route the fence exists to block.
736
+ const { prompt, completion } = ANVIL_GPT_5_6_LUNA_MAX_PRICE
737
+ expect(prompt).toBeGreaterThan(0.1)
738
+ expect(completion).toBeGreaterThan(0.6)
739
+ expect(prompt).toBeLessThan(1.0)
740
+ expect(completion).toBeLessThan(6.0)
741
+ })
742
+
743
+ test('limited access exposes DeepSeek V4 Flash and non-Pro MiMo 2.5', () => {
744
+ expect(LIMITED_ANVIL_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
745
+ expect(LIMITED_ANVIL_MODEL_IDS).toEqual([
746
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
747
+ ANVIL_MIMO_V25_MODEL_ID,
748
+ ])
749
+ expect(getAnvilModelsForAccessTier('limited').map((m) => m.id)).toEqual([
750
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
751
+ ANVIL_MIMO_V25_MODEL_ID,
752
+ ])
753
+ expect(
754
+ isAnvilModelAllowedForAccessTier(
755
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
756
+ 'limited',
757
+ ),
758
+ ).toBe(true)
759
+ expect(
760
+ isAnvilModelAllowedForAccessTier(MINIMAX_M3_MODEL_ID, 'limited'),
761
+ ).toBe(false)
762
+ expect(
763
+ isAnvilModelAllowedForAccessTier(
764
+ ANVIL_MIMO_V25_MODEL_ID,
765
+ 'limited',
766
+ ),
767
+ ).toBe(true)
768
+ expect(
769
+ isAnvilModelAllowedForAccessTier(
770
+ ANVIL_MIMO_V25_PRO_MODEL_ID,
771
+ 'limited',
772
+ ),
773
+ ).toBe(false)
774
+ expect(
775
+ resolveAnvilModelForAccessTier(ANVIL_MIMO_V25_MODEL_ID, 'limited'),
776
+ ).toBe(ANVIL_MIMO_V25_MODEL_ID)
777
+ expect(
778
+ resolveAnvilModelForAccessTier(MINIMAX_M3_MODEL_ID, 'limited'),
779
+ ).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
780
+ })
781
+
782
+ test('recommends a joinable, in-tier model for the picker hero', () => {
783
+ // Full access → DeepSeek V4 Pro 08/13 (the recommended default since
784
+ // 2026-08-12). It is premium, so the hero HAS to flip once the daily pool
785
+ // runs dry — the assertions below are what keep the CLI/Desktop hero
786
+ // joinable at every point in a user's day.
787
+ expect(getRecommendedAnvilModelId('full')).toBe(
788
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
789
+ )
790
+ expect(getRecommendedAnvilModelId(undefined)).toBe(
791
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
792
+ )
793
+ expect(
794
+ getRecommendedAnvilModelId('full', { premiumExhausted: true }),
795
+ ).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
796
+ expect(
797
+ isAnvilPremiumModelId(
798
+ getRecommendedAnvilModelId('full', { premiumExhausted: true }),
799
+ ),
800
+ ).toBe(false)
801
+ // Limited access → DeepSeek V4 Flash, which is in the limited model set.
802
+ expect(getRecommendedAnvilModelId('limited')).toBe(
803
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
804
+ )
805
+ expect(
806
+ getAnvilModelsForAccessTier('limited').some(
807
+ (m) => m.id === getRecommendedAnvilModelId('limited'),
808
+ ),
809
+ ).toBe(true)
810
+ })
811
+
812
+ test('every surface recommends DeepSeek V4 Pro, on two separate constants', () => {
813
+ // The browser surfaces took Pro on 2026-08-12 (Luna held it from
814
+ // 2026-08-04) and the CLI/Desktop pickers followed the same day: the 08/13
815
+ // GA build wins the agentic benchmarks these surfaces run, so there is no
816
+ // longer a surface where a different model is the better first pick. They
817
+ // stay TWO constants because they have diverged before and may again.
818
+ expect(DEFAULT_ANVIL_WEB_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
819
+ expect(DEFAULT_ANVIL_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
820
+ expect(getRecommendedAnvilWebModelId('full')).toBe(
821
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
822
+ )
823
+ expect(getRecommendedAnvilWebModelId(undefined)).toBe(
824
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
825
+ )
826
+ // The recommendation must never be a model the picker also argues against.
827
+ expect(
828
+ getAnvilModelSupersededBy(
829
+ DEFAULT_ANVIL_WEB_MODEL_ID,
830
+ ANVIL_WEB_MODELS.map((model) => model.id),
831
+ ),
832
+ ).toBeUndefined()
833
+ // Pro is premium, so the pool CAN run dry — the recommended pick has to
834
+ // stay joinable, and limited tier can't name it at all.
835
+ expect(getRecommendedAnvilWebModelId('limited')).toBe(
836
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
837
+ )
838
+ expect(
839
+ getRecommendedAnvilWebModelId('full', { premiumExhausted: true }),
840
+ ).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
841
+ expect(
842
+ isAnvilPremiumModelId(
843
+ getRecommendedAnvilWebModelId('full', { premiumExhausted: true }),
844
+ ),
845
+ ).toBe(false)
846
+ // The web default must be a real, selectable web model.
847
+ expect(isAnvilWebModelId(DEFAULT_ANVIL_WEB_MODEL_ID)).toBe(true)
848
+ // …and one the limited tier is coerced OFF of, since it is premium.
849
+ expect(
850
+ isAnvilWebModelAllowedForLimitedTier(DEFAULT_ANVIL_WEB_MODEL_ID),
851
+ ).toBe(false)
852
+ })
853
+
854
+ test('de-emphasizes nothing, and never the default', () => {
855
+ // The list is empty as of 2026-08-12. MiniMax M3 was the last entry and
856
+ // left when it became the ONLY muted row: the compact treatment folds the
857
+ // tagline onto the name line, which among full-size rows reads as a broken
858
+ // row rather than a quiet one. M3 keeps its supersededBy notice, so the
859
+ // steering survives — see the test below.
860
+ expect(ANVIL_WEB_DEEMPHASIZED_MODEL_IDS).toEqual([])
861
+ expect(isAnvilWebDeemphasizedModelId(MINIMAX_M3_MODEL_ID)).toBe(false)
862
+ expect(
863
+ isAnvilWebDeemphasizedModelId(`${ANVIL_KIMI_MODEL_ID}-20260301`),
864
+ ).toBe(false)
865
+ expect(
866
+ isAnvilWebDeemphasizedModelId(DEFAULT_ANVIL_WEB_MODEL_ID),
867
+ ).toBe(false)
868
+ expect(
869
+ isAnvilWebDeemphasizedModelId(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID),
870
+ ).toBe(false)
871
+ expect(isAnvilWebDeemphasizedModelId(null)).toBe(false)
872
+ // V4 Pro left the list on 2026-08-12 too: its 08/13 GA build wins the
873
+ // quality half of the de-emphasis test again, and price alone is not
874
+ // grounds.
875
+ expect(
876
+ isAnvilWebDeemphasizedModelId(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID),
877
+ ).toBe(false)
878
+ // De-emphasis is presentation only: anything added back stays selectable.
879
+ for (const id of ANVIL_WEB_DEEMPHASIZED_MODEL_IDS) {
880
+ expect(isAnvilWebModelId(id)).toBe(true)
881
+ expect(isAnvilModelAllowedForAccessTier(id, 'full')).toBe(true)
882
+ }
883
+ })
884
+
885
+ test('points users off MiniMax M3 to V4 Flash', () => {
886
+ // Flash-0731 overtook M3 on 2026-07-31, so M3 carries a notice and a switch
887
+ // target rather than being removed — it is still selectable.
888
+ const all = ANVIL_MODELS.map((model) => model.id)
889
+ const superseded = getAnvilModelSupersededBy(MINIMAX_M3_MODEL_ID, all)
890
+ expect(superseded?.modelId).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
891
+ expect(superseded!.notice.length).toBeGreaterThan(0)
892
+ expect(superseded!.actionLabel.length).toBeGreaterThan(0)
893
+ // M3 remains a real, selectable model — this is a nudge, not a retirement.
894
+ expect(all).toContain(MINIMAX_M3_MODEL_ID)
895
+ // The recommended default is never itself marked superseded.
896
+ expect(
897
+ getAnvilModelSupersededBy(DEFAULT_ANVIL_MODEL_ID, all),
898
+ ).toBeUndefined()
899
+ })
900
+
901
+ test('points users off GPT-5.6 Luna to V4 Pro, without muting Luna', () => {
902
+ const all = ANVIL_MODELS.map((model) => model.id)
903
+ const superseded = getAnvilModelSupersededBy(
904
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
905
+ all,
906
+ )
907
+ expect(superseded?.modelId).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
908
+ expect(superseded!.notice.length).toBeGreaterThan(0)
909
+ expect(superseded!.actionLabel.length).toBeGreaterThan(0)
910
+ // The notice names the dated build, so it points at a row on screen.
911
+ expect(superseded!.notice).toContain('08/13')
912
+ // Luna stays selectable, and stays UNMUTED: it is cheaper than the model it
913
+ // now points at, and de-emphasis is this product's cost signal. Muting it
914
+ // would have the list imply a price that runs the other way.
915
+ expect(all).toContain(ANVIL_GPT_5_6_LUNA_MODEL_ID)
916
+ expect(
917
+ isAnvilWebDeemphasizedModelId(ANVIL_GPT_5_6_LUNA_MODEL_ID),
918
+ ).toBe(false)
919
+ // A saved Luna pick migrates to Pro rather than being stranded.
920
+ expect(
921
+ migrateSupersededAnvilModelPreference(
922
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
923
+ all,
924
+ ),
925
+ ).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
926
+ })
927
+
928
+ test('stops steering users off V4 Pro now that its GA build leads again', () => {
929
+ // Pro carried a "Flash performs better" notice from 2026-07-31, when
930
+ // Flash-0731 beat the Pro PREVIEW. The 08/13 GA build reversed that on the
931
+ // agentic benchmarks, so the notice would now steer users off the stronger
932
+ // model — and the picker offers a one-click switch for whatever it names.
933
+ const all = ANVIL_MODELS.map((model) => model.id)
934
+ expect(
935
+ getAnvilModelSupersededBy(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID, all),
936
+ ).toBeUndefined()
937
+ // And on 2026-08-12 it became the recommended CLI/Desktop default too,
938
+ // matching the browser surfaces.
939
+ expect(DEFAULT_ANVIL_MODEL_ID).toBe(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
940
+ })
941
+
942
+ test('marks both new DeepSeek builds as NEW and dates their names', () => {
943
+ // The wire ids are undated and auto-update, so the display has to carry the
944
+ // signal that this is a different model than the one users already judged.
945
+ const dated = [
946
+ [ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID, '07/31'],
947
+ [ANVIL_DEEPSEEK_V4_PRO_MODEL_ID, '08/13'],
948
+ ] as const
949
+ // Widened to the interface: the const-asserted tuple's union type only
950
+ // exposes optional fields set on EVERY member, so `isNew` is unreachable
951
+ // through it unless the find() narrows to a single literal id.
952
+ const catalog: readonly AnvilModelOption[] = ANVIL_MODELS
953
+ for (const [id, date] of dated) {
954
+ const model = catalog.find((candidate) => candidate.id === id)!
955
+ expect(model.isNew).toBe(true)
956
+ expect(model.displayName).toContain(date)
957
+ }
958
+ // Nothing else claims to be new, or the badge stops meaning anything.
959
+ expect(catalog.filter((model) => model.isNew)).toHaveLength(dated.length)
960
+ })
961
+
962
+ test('steers saved picks off every superseded model', () => {
963
+ const all = ANVIL_MODELS.map((model) => model.id)
964
+ // Every model Flash overtook migrates to it...
965
+ for (const superseded of [MINIMAX_M3_MODEL_ID, ANVIL_MIMO_V25_MODEL_ID]) {
966
+ expect(migrateSupersededAnvilModelPreference(superseded, all)).toBe(
967
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
968
+ )
969
+ }
970
+ // ...and a current pick is left alone (null = keep it). V4 Pro is one again
971
+ // as of its 08/13 GA build: a user who saved it keeps it instead of being
972
+ // moved off it on every load.
973
+ for (const current of [
974
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
975
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
976
+ ]) {
977
+ expect(migrateSupersededAnvilModelPreference(current, all)).toBeNull()
978
+ }
979
+ expect(migrateSupersededAnvilModelPreference(undefined, all)).toBeNull()
980
+ // Never migrates onto a model this surface cannot select.
981
+ expect(
982
+ migrateSupersededAnvilModelPreference(MINIMAX_M3_MODEL_ID, [
983
+ MINIMAX_M3_MODEL_ID,
984
+ ]),
985
+ ).toBeNull()
986
+ })
987
+
988
+ test('never de-emphasizes a model we still recommend', () => {
989
+ // Muting + sorting-last is how the Premium group steers to the
990
+ // replacement, so anything muted must be superseded. NOT the converse:
991
+ // MiMo 2.5 is superseded on quality but costs the same as Flash, and
992
+ // de-emphasis is defined as a cost signal — muting it would make the list
993
+ // say something untrue about its price.
994
+ const all = ANVIL_MODELS.map((model) => model.id)
995
+ for (const model of ANVIL_MODELS) {
996
+ if (isAnvilWebDeemphasizedModelId(model.id)) {
997
+ expect(getAnvilModelSupersededBy(model.id, all)).toBeDefined()
998
+ }
999
+ }
1000
+ // The recommended default is never muted or superseded.
1001
+ expect(isAnvilWebDeemphasizedModelId(DEFAULT_ANVIL_MODEL_ID)).toBe(
1002
+ false,
1003
+ )
1004
+ expect(
1005
+ getAnvilModelSupersededBy(DEFAULT_ANVIL_MODEL_ID, all),
1006
+ ).toBeUndefined()
1007
+ })
1008
+
1009
+ test('never offers a switch to a model the surface cannot select', () => {
1010
+ // A picker that lacks the replacement must show no switch at all, rather
1011
+ // than a button that resolves to nothing.
1012
+ expect(
1013
+ getAnvilModelSupersededBy(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID, [
1014
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
1015
+ ]),
1016
+ ).toBeUndefined()
1017
+ expect(getAnvilModelSupersededBy(undefined, [])).toBeUndefined()
1018
+ expect(getAnvilModelSupersededBy('vendor/unknown', [])).toBeUndefined()
1019
+ })
1020
+
1021
+ test('full-access anvil models can spawn the gemini-thinker subagent', () => {
1022
+ // Full-access models (non-limited, non-fastest) get the thinker. Kimi is
1023
+ // gone from Anvil entirely, so it no longer qualifies.
1024
+ expect(canAnvilModelSpawnGeminiThinker(ANVIL_KIMI_MODEL_ID)).toBe(
1025
+ false,
1026
+ )
1027
+ expect(
1028
+ canAnvilModelSpawnGeminiThinker(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID),
1029
+ ).toBe(true)
1030
+ // MiMo 2.5 Pro is gone from Anvil, so it no longer qualifies either.
1031
+ expect(
1032
+ canAnvilModelSpawnGeminiThinker(ANVIL_MIMO_V25_PRO_MODEL_ID),
1033
+ ).toBe(false)
1034
+ expect(canAnvilModelSpawnGeminiThinker(MINIMAX_M3_MODEL_ID)).toBe(true)
1035
+ expect(
1036
+ canAnvilModelSpawnGeminiThinker(ANVIL_GPT_5_6_LUNA_MODEL_ID),
1037
+ ).toBe(true)
1038
+
1039
+ // Limited-tier models (DeepSeek V4 Flash, MiMo 2.5) skip it.
1040
+ expect(
1041
+ canAnvilModelSpawnGeminiThinker(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID),
1042
+ ).toBe(false)
1043
+ expect(canAnvilModelSpawnGeminiThinker(ANVIL_MIMO_V25_MODEL_ID)).toBe(
1044
+ false,
1045
+ )
1046
+ })
1047
+
1048
+ test('does not support GLM 5.1 for anvil sessions', () => {
1049
+ const glm = 'z-ai/glm-5.1'
1050
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(glm)
1051
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
1052
+ glm,
1053
+ )
1054
+ expect(isAnvilModelId(glm)).toBe(false)
1055
+ expect(isSupportedAnvilModelId(glm)).toBe(false)
1056
+ })
1057
+
1058
+ test('surfaces referral-gated GLM 5.2 only in the Web and Cloud picker', () => {
1059
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).toContain(
1060
+ ANVIL_GLM_V52_MODEL_ID,
1061
+ )
1062
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).toContain(
1063
+ ANVIL_GLM_V52_MODEL_ID,
1064
+ )
1065
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(
1066
+ ANVIL_GLM_V52_MODEL_ID,
1067
+ )
1068
+ expect(isAnvilWebPremiumModelId(ANVIL_GLM_V52_MODEL_ID)).toBe(false)
1069
+ })
1070
+
1071
+ test('formats the close time in the user local timezone while deployment is open', () => {
1072
+ expect(
1073
+ getAnvilDeploymentAvailabilityLabel(new Date('2026-01-05T18:00:00Z'), {
1074
+ locale: 'en-US',
1075
+ timeZone: 'America/Los_Angeles',
1076
+ }),
1077
+ ).toBe('until 5:00 PM')
1078
+ })
1079
+
1080
+ test('formats the next open time in the user local timezone while deployment is closed', () => {
1081
+ expect(
1082
+ getAnvilDeploymentAvailabilityLabel(new Date('2026-01-05T12:00:00Z'), {
1083
+ locale: 'en-US',
1084
+ timeZone: 'America/Los_Angeles',
1085
+ }),
1086
+ ).toBe('opens 6:00 AM')
1087
+ })
1088
+
1089
+ test('includes the weekday when the next opening is on a later local day', () => {
1090
+ expect(
1091
+ getAnvilDeploymentAvailabilityLabel(new Date('2026-01-11T03:00:00Z'), {
1092
+ locale: 'en-US',
1093
+ timeZone: 'America/Los_Angeles',
1094
+ }),
1095
+ ).toBe('opens Sun 6:00 AM')
1096
+ })
1097
+
1098
+ test('tracks deployment hours correctly across the open and close boundaries', () => {
1099
+ expect(isAnvilDeploymentHours(new Date('2026-01-05T13:59:00Z'))).toBe(
1100
+ false,
1101
+ )
1102
+ expect(isAnvilDeploymentHours(new Date('2026-01-05T14:00:00Z'))).toBe(
1103
+ true,
1104
+ )
1105
+ expect(isAnvilDeploymentHours(new Date('2026-01-06T00:59:00Z'))).toBe(
1106
+ true,
1107
+ )
1108
+ expect(isAnvilDeploymentHours(new Date('2026-01-06T01:00:00Z'))).toBe(
1109
+ false,
1110
+ )
1111
+ expect(isAnvilDeploymentHours(new Date('2026-01-10T20:00:00Z'))).toBe(
1112
+ true,
1113
+ )
1114
+ })
1115
+ })
1116
+
1117
+ describe('limited-offer models (Claude Fable 5)', () => {
1118
+ test('is deliberately absent from every client picker catalog', () => {
1119
+ // The whole mechanism rests on this: no client may render Fable from its
1120
+ // own catalog, because only the server knows whether the wave still has
1121
+ // sessions. A client that has never been told about the offer must look
1122
+ // exactly like it does today.
1123
+ expect(ANVIL_MODELS.map((m) => m.id)).not.toContain(
1124
+ ANVIL_FABLE_5_MODEL_ID,
1125
+ )
1126
+ expect(isAnvilModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(false)
1127
+ expect(ANVIL_WEB_ALL_MODELS.map((m) => m.id)).not.toContain(
1128
+ ANVIL_FABLE_5_MODEL_ID,
1129
+ )
1130
+ expect(
1131
+ getAnvilModelsForAccessTier('full').map((m) => m.id),
1132
+ ).not.toContain(ANVIL_FABLE_5_MODEL_ID)
1133
+ })
1134
+
1135
+ test('is still a model the session and chat layers accept', () => {
1136
+ // Same shape as referral GLM: out of the picker catalog, in the supported
1137
+ // catalog, so admission, the chat gate and the display-name lookup all
1138
+ // resolve it.
1139
+ expect(isSupportedAnvilModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(true)
1140
+ expect(
1141
+ isAnvilSessionModelAllowedForAccessTier(
1142
+ ANVIL_FABLE_5_MODEL_ID,
1143
+ 'full',
1144
+ ),
1145
+ ).toBe(true)
1146
+ expect(getAnvilModel(ANVIL_FABLE_5_MODEL_ID).displayName).toBe(
1147
+ 'Claude Fable 5',
1148
+ )
1149
+ })
1150
+
1151
+ test('an explicit pick survives resolution instead of silently downgrading', () => {
1152
+ // resolveAnvilModelForAccessTier runs on every explicit CLI pick. Before
1153
+ // the offer models were passed through, pressing Enter on the Fable row
1154
+ // would have started a DeepSeek session with no explanation.
1155
+ expect(
1156
+ resolveAnvilModelForAccessTier(ANVIL_FABLE_5_MODEL_ID, 'full'),
1157
+ ).toBe(ANVIL_FABLE_5_MODEL_ID)
1158
+ })
1159
+
1160
+ test('limited-region users cannot reach it', () => {
1161
+ expect(
1162
+ isAnvilSessionModelAllowedForAccessTier(
1163
+ ANVIL_FABLE_5_MODEL_ID,
1164
+ 'limited',
1165
+ ),
1166
+ ).toBe(false)
1167
+ expect(
1168
+ resolveAnvilSessionModelForAccessTier(
1169
+ ANVIL_FABLE_5_MODEL_ID,
1170
+ 'limited',
1171
+ ),
1172
+ ).toBe(LIMITED_ANVIL_MODEL_ID)
1173
+ })
1174
+
1175
+ test('traces are collected, which is the point of running the wave at all', () => {
1176
+ expect(isAnvilTracedModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(true)
1177
+ const fable = SUPPORTED_ANVIL_MODELS.find(
1178
+ (m) => m.id === ANVIL_FABLE_5_MODEL_ID,
1179
+ )
1180
+ expect((fable as { warning?: string } | undefined)?.warning).toBe(
1181
+ 'May use data for AI training',
1182
+ )
1183
+ })
1184
+
1185
+ test('is metered by its own pool, never the shared daily premium one', () => {
1186
+ // It is marked `premium: true` for styling and to keep it out of the free
1187
+ // Standard pool, but joining ANVIL_PREMIUM_MODEL_IDS would put trial
1188
+ // sessions on the quota M3 and DeepSeek Pro share.
1189
+ expect(isAnvilPremiumModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(false)
1190
+ expect(isAnvilWebPremiumModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(false)
1191
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(
1192
+ ANVIL_FABLE_5_MODEL_ID,
1193
+ )
1194
+ expect(isAnvilLimitedOfferModelId(ANVIL_FABLE_5_MODEL_ID)).toBe(true)
1195
+ })
1196
+
1197
+ test('the offer predicate tolerates dated provider snapshots', () => {
1198
+ expect(
1199
+ isAnvilLimitedOfferModelId(`${ANVIL_FABLE_5_MODEL_ID}-20260815`),
1200
+ ).toBe(true)
1201
+ expect(
1202
+ isAnvilLimitedOfferModelId(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID),
1203
+ ).toBe(false)
1204
+ expect(isAnvilLimitedOfferModelId(null)).toBe(false)
1205
+ })
1206
+ })
1207
+
1208
+ describe('Meta Muse Spark 1.2 Contributor', () => {
1209
+ test('is a Anvil Web model and reachable from no other surface', () => {
1210
+ // Web-only is enforced by ABSENCE from the CLI/Desktop catalogs, which is
1211
+ // also what makes the session layer refuse it there
1212
+ // (isAnvilSessionModelId reads SUPPORTED_ANVIL_MODELS). The reason is
1213
+ // the queue, not the price: the browser can render a rate-limit wait with
1214
+ // an ETA and the CLI cannot, so on the CLI a 60-RPM team-wide ceiling would
1215
+ // just be unexplained 429s.
1216
+ expect(ANVIL_WEB_MODELS.map((model) => model.id)).toContain(
1217
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1218
+ )
1219
+ expect(ANVIL_MODELS.map((model) => model.id)).not.toContain(
1220
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1221
+ )
1222
+ expect(SUPPORTED_ANVIL_MODELS.map((model) => model.id)).not.toContain(
1223
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1224
+ )
1225
+ // Absence from SUPPORTED_ is the Desktop gate, not just tidiness:
1226
+ // isModelForHarness('anvil', …) validates against exactly this set, so a
1227
+ // Desktop client asking for Muse Spark is refused before session admission
1228
+ // ever sees it.
1229
+ expect(
1230
+ isSupportedAnvilModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1231
+ ).toBe(false)
1232
+ // Session admission DOES accept it — it must, or no Web session could run
1233
+ // on it. The shared gate is the union of the CLI and Web catalogs, so
1234
+ // "Web-only" is enforced by the catalogs above plus the free-mode agent
1235
+ // allowlist (only base2-free-muse-spark may run this model, and only the
1236
+ // Web bundle ships that root), never by this predicate.
1237
+ expect(
1238
+ isAnvilSessionModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1239
+ ).toBe(true)
1240
+
1241
+ // Visible to every full-access Web user, not god-gated and not retired.
1242
+ expect(
1243
+ isAnvilWebModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1244
+ ).toBe(true)
1245
+ expect(
1246
+ isAnvilWebGodOnlyModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1247
+ ).toBe(false)
1248
+ expect(
1249
+ isAnvilWebSelectableModelId(
1250
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1251
+ ),
1252
+ ).toBe(true)
1253
+ })
1254
+
1255
+ test('is metered by the Web premium pool and no other', () => {
1256
+ // Premium here bounds how many users are inside the 60 RPM ceiling at once
1257
+ // — it is NOT a price signal, since Contributor is cheaper per token than
1258
+ // the standard-pool models. Being in some pool is mandatory:
1259
+ // ANVIL_WEB_STANDARD_MODEL_IDS is derived by filtering `!premium`, so a
1260
+ // premium model missing from the premium list is metered by nothing.
1261
+ expect(
1262
+ isAnvilWebPremiumModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1263
+ ).toBe(true)
1264
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(
1265
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1266
+ )
1267
+ expect(
1268
+ isAnvilGlmV52ModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1269
+ ).toBe(false)
1270
+ // The CLI's own premium pool must not learn about a model the CLI cannot
1271
+ // select.
1272
+ expect(
1273
+ isAnvilPremiumModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1274
+ ).toBe(false)
1275
+ })
1276
+
1277
+ test('carries a reasoning effort that the server can actually resolve', () => {
1278
+ // Two halves, and the second is the one that used to silently fail.
1279
+ // getAnvilModelReasoningEffort read SUPPORTED_ANVIL_MODELS alone —
1280
+ // the CLI/Desktop catalog — which Muse Spark is deliberately absent from
1281
+ // (that absence IS the Desktop gate). So the field could be set on the row
1282
+ // and resolve to null anyway, with nothing to indicate why.
1283
+ const model = getAnvilWebModel(
1284
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1285
+ )
1286
+ expect(model.reasoningEffort).toBe(ANVIL_MUSE_SPARK_REASONING_EFFORT)
1287
+ expect(
1288
+ getAnvilModelReasoningEffort(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1289
+ ).toBe(ANVIL_MUSE_SPARK_REASONING_EFFORT)
1290
+
1291
+ // Never 'none': Muse Spark answers that with a hard 400 (verified live),
1292
+ // and a 400 is neither retried nor queued, so it kills the turn outright.
1293
+ expect(ANVIL_MUSE_SPARK_REASONING_EFFORT).not.toBe('none')
1294
+ // Meta's ladder, from its own 400 on an unknown value. `xhigh` and
1295
+ // `minimal` exist here and nowhere else in this repo, which is why the
1296
+ // shared agent-definition enum deliberately does not carry them.
1297
+ expect(['minimal', 'low', 'medium', 'high', 'xhigh']).toContain(
1298
+ ANVIL_MUSE_SPARK_REASONING_EFFORT,
1299
+ )
1300
+
1301
+ // Suffix-tolerant like every other id helper, so a dated provider snapshot
1302
+ // does not silently drop back to Meta's default effort.
1303
+ expect(
1304
+ getAnvilModelReasoningEffort(
1305
+ `${ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID}-20260901`,
1306
+ ),
1307
+ ).toBe(ANVIL_MUSE_SPARK_REASONING_EFFORT)
1308
+
1309
+ // Widening the lookup to the Web catalog must not invent an effort for
1310
+ // models that declare none.
1311
+ expect(
1312
+ getAnvilModelReasoningEffort(ANVIL_KIMI_K3_ECO_MODEL_ID),
1313
+ ).toBeNull()
1314
+ })
1315
+
1316
+ test('discloses the Contributor tier training terms', () => {
1317
+ // The discount IS the training grant, so the warning is the disclosure that
1318
+ // makes the row legitimate rather than decoration.
1319
+ const model = getAnvilWebModel(
1320
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1321
+ )
1322
+ expect(model.displayName).toBe('Muse Spark 1.2')
1323
+ expect(model.dataUse).toBe('training')
1324
+ expect(model.warning).toBe('May use data for AI training')
1325
+ })
1326
+
1327
+ test('has exactly one wire id, and the predicate tolerates dated snapshots', () => {
1328
+ // The queue, the premium pool and the free-mode agent allowlist all key off
1329
+ // this id. A second id reaching the same upstream is how `crof/glm-5.2`
1330
+ // handed out a metered model for free; do not add one.
1331
+ expect(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID).toBe(
1332
+ 'meta/muse-spark-1.2-contributor',
1333
+ )
1334
+ // Meta's own id is what the provider receives, never a wire id a caller
1335
+ // may send. Widened to string[] on purpose: the union type already proves
1336
+ // this at compile time, and the runtime check is what survives someone
1337
+ // later adding the bare id to a catalog.
1338
+ expect(
1339
+ ANVIL_WEB_ALL_MODELS.map((model): string => model.id),
1340
+ ).not.toContain(MUSE_SPARK_12_CONTRIBUTOR_UPSTREAM_MODEL_ID)
1341
+
1342
+ expect(
1343
+ isMuseSparkModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1344
+ ).toBe(true)
1345
+ // A dated provider snapshot must not slip past the rate-limit queue.
1346
+ expect(
1347
+ isMuseSparkModelId(
1348
+ `${ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID}-20260901`,
1349
+ ),
1350
+ ).toBe(true)
1351
+ expect(isMuseSparkModelId('meta/muse-spark-1.2')).toBe(false)
1352
+ expect(isMuseSparkModelId(null)).toBe(false)
1353
+ })
1354
+ })
1355
+
1356
+ describe('Muse Spark rate-limit fallback', () => {
1357
+ test('reroutes only to a model the caller is already entitled to', () => {
1358
+ // THE invariant. A fallback outside the shared daily premium pool would
1359
+ // turn "Muse Spark is busy" into a way to reach a model the user had not
1360
+ // earned — the same shape as the retired crof/glm-5.2 route, which handed
1361
+ // out a referral-earned model for nothing. The fallback sits in the same
1362
+ // pool, so a rerouted request spends exactly the entitlement the original
1363
+ // would.
1364
+ expect(isAnvilWebPremiumModelId(MUSE_SPARK_FALLBACK_MODEL_ID)).toBe(true)
1365
+ expect(
1366
+ isAnvilWebPremiumModelId(ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID),
1367
+ ).toBe(true)
1368
+ // Never the earned-GLM pool, and never the free standard pool.
1369
+ expect(isAnvilGlmV52ModelId(MUSE_SPARK_FALLBACK_MODEL_ID)).toBe(false)
1370
+ expect(ANVIL_WEB_STANDARD_MODEL_IDS).not.toContain(
1371
+ MUSE_SPARK_FALLBACK_MODEL_ID,
1372
+ )
1373
+ // And it must be a real, selectable Web model rather than a dangling id.
1374
+ expect(isAnvilWebModelId(MUSE_SPARK_FALLBACK_MODEL_ID)).toBe(true)
1375
+ expect(MUSE_SPARK_FALLBACK_MODEL_ID).not.toBe(
1376
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1377
+ )
1378
+ })
1379
+
1380
+ test('the picker promises exactly what the server does', () => {
1381
+ // The tooltip is a promise about behavior; drift between the two is how a
1382
+ // UI starts lying. Both read the same constant, and the threshold the copy
1383
+ // implies ("too long") is the one the server actually applies.
1384
+ const model = getAnvilWebModel(
1385
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1386
+ )
1387
+ expect(model.tagline).toBe('Queue')
1388
+ expect(model.taglineTooltip).toBe(MUSE_SPARK_FALLBACK_NOTICE)
1389
+ // The copy must NAME the model the server actually reroutes to — pinning it
1390
+ // to the catalog rather than to a literal is what catches a fallback that
1391
+ // moves (as it did on 2026-08-12, Luna → V4 Pro) while its tooltip does not.
1392
+ // Matched undated: this tooltip promises a behavior rather than pointing at
1393
+ // a picker row, so it does not carry a build date the way the supersedes
1394
+ // notices do.
1395
+ expect(MUSE_SPARK_FALLBACK_NOTICE).toContain(
1396
+ getAnvilWebModel(MUSE_SPARK_FALLBACK_MODEL_ID).displayName.replace(
1397
+ /\s+\d{2}\/\d{2}$/,
1398
+ '',
1399
+ ),
1400
+ )
1401
+ // The row no longer advertises itself as new.
1402
+ expect(model.isNew).toBeUndefined()
1403
+ // A wait worth explaining, not one worth hiding — and the same number the
1404
+ // provider uses for its silent window, so the two cannot disagree about
1405
+ // what "too long" means.
1406
+ expect(MUSE_SPARK_FALLBACK_AFTER_MS).toBe(10_000)
1407
+ })
1408
+ })