@alumbwe/anvil 1.0.20 → 1.0.22

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 +74 -70
  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,2130 @@
1
+ import {
2
+ addDaysToYmd,
3
+ getUtcForZonedTime,
4
+ getZonedParts,
5
+ type ZonedDateParts,
6
+ } from '../util/zoned-time'
7
+ import { mimoModels } from './model-config'
8
+ import {
9
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
10
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
11
+ ANVIL_MINIMAX_M3_MODEL_ID,
12
+ } from './anvil-model-ids'
13
+ import {
14
+ ANVIL_AI_TRAINING_NOTICE,
15
+ type AnvilModelDataUse,
16
+ } from './anvil-data-use'
17
+ import {
18
+ clampReasoningEffort,
19
+ type ReasoningEffort,
20
+ } from './reasoning-effort'
21
+
22
+ export {
23
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
24
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
25
+ ANVIL_MINIMAX_M3_MODEL_ID,
26
+ } from './anvil-model-ids'
27
+
28
+ /**
29
+ * Models a anvil user can pick between in the waiting-room model selector.
30
+ *
31
+ * Each model has its own queue (server keys queue position by `model`), so the
32
+ * list here is effectively the set of separate waiting lines. Order is the
33
+ * order shown in the UI.
34
+ */
35
+ export interface AnvilModelOption {
36
+ /** Stable ID used in the wire protocol and DB. Matches the model id passed
37
+ * to the chat-completions endpoint. */
38
+ id: string
39
+ /** Short label for the selector UI. */
40
+ displayName: string
41
+ /** One-line description shown next to the label. */
42
+ tagline: string
43
+ /** Availability policy for the selector and server-side admission. */
44
+ availability: 'always' | 'deployment_hours'
45
+ /** Optional caveat shown in the picker (e.g. AI-training warning).
46
+ * Rendered in the warning/secondary color so users spot it before
47
+ * picking the model. */
48
+ warning?: string
49
+ /** Machine-readable data-use policy. Never infer storage or training
50
+ * behavior from the human-readable warning text. */
51
+ dataUse: AnvilModelDataUse
52
+ /** Premium models carry a per-day usage limit
53
+ * (ANVIL_PREMIUM_SESSION_LIMIT). Surfaced in the UI as a "Premium"
54
+ * badge with the limit. Derived from ANVIL_PREMIUM_MODEL_IDS so the two
55
+ * never drift. */
56
+ premium: boolean
57
+ /** Whether the model accepts image input. Drives whether uploaded images
58
+ * are forwarded as real multimodal content vs. dropped/inlined as text. */
59
+ multimodal: boolean
60
+ /** Reasoning effort Anvil turns run this model at. Not advisory: the
61
+ * completions layer sends it whenever the caller names no reasoning of its
62
+ * own (applyAnvilReasoningDefaults in web/src/llm-api/openrouter.ts),
63
+ * and the Desktop and CLI pickers display the same field — one source, so
64
+ * on those surfaces what users see and what the server sends cannot drift.
65
+ * Omit where the model has no effort levels (MiniMax) or the provider
66
+ * default should stand untouched (GLM, MiMo). CAUTION: both DeepSeek V4
67
+ * models expose low/high/max and neither has a distinct medium rung — see
68
+ * DEEPSEEK_V4_REASONING_EFFORTS. */
69
+ /** Reasoning effort sent for this model, on the PROVIDER's own scale.
70
+ * Deliberately wider than the shared agent-definition enum: Meta's ladder is
71
+ * minimal/low/medium/high/xhigh (its own 400 names the set). Not every
72
+ * provider accepts every rung, so each model still declares its own ladder. */
73
+ reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
74
+ /**
75
+ * The ladder a USER may pick from for this model, ascending. Absent means the
76
+ * model offers no choice and shows no control — the default for every row.
77
+ *
78
+ * Same field name and shape as Desktop's `ModelOption.efforts`
79
+ * (anvil-desktop/src/shared/models.ts), deliberately: Desktop already had
80
+ * per-model effort lists driving its Claude/Codex picker, and one pattern
81
+ * across surfaces beats two that must be kept in step.
82
+ *
83
+ * Values must be native provider settings, not compatibility aliases or
84
+ * prompt approximations. The model's ordinary setting belongs in
85
+ * `defaultEffort`; rungs may sit on either side of it.
86
+ */
87
+ efforts?: readonly ReasoningEffort[]
88
+ /**
89
+ * Where `efforts` starts before a user touches it.
90
+ *
91
+ * Usually equal to `reasoningEffort`, but kept separate so a future model can
92
+ * expose a picker default that differs from its server-owned wire default.
93
+ */
94
+ defaultEffort?: ReasoningEffort
95
+ /** Whether the model is still being trialed and may be unreliable. Surfaced
96
+ * in the picker as a "TEST" badge with a tooltip so users know it is not
97
+ * yet production-grade. */
98
+ experimental?: boolean
99
+ /** Tooltip attached to the tagline, for a tagline that names a behavior the
100
+ * word alone cannot explain (e.g. "Queue"). Rendered with the same
101
+ * dotted-underline affordance as the data-use "Data" label, so a row can
102
+ * carry both without growing a second line. */
103
+ taglineTooltip?: string
104
+ /** Freshly released or freshly re-trained. Surfaced as a "NEW" badge so a
105
+ * returning user notices the model changed rather than assuming it is the
106
+ * same one they already formed an opinion about. Clear it once the model
107
+ * stops being news. */
108
+ isNew?: boolean
109
+ /** Set when another model has overtaken this one and users should generally
110
+ * move. Pickers render `notice` on the row and offer a one-click switch to
111
+ * `modelId`. Kept structured rather than folded into `warning` so the button
112
+ * has a real target, and so this stays distinct from the data-use caveat —
113
+ * a row can carry both. */
114
+ supersededBy?: {
115
+ modelId: string
116
+ notice: string
117
+ actionLabel: string
118
+ }
119
+ }
120
+
121
+ /** Server-facing fallback copy for APIs and provider errors that can't know
122
+ * the caller's local timezone. The CLI should render
123
+ * `getAnvilDeploymentAvailabilityLabel()` instead. */
124
+ export const ANVIL_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT every day'
125
+ export const ANVIL_GEMINI_PRO_MODEL_ID = 'google/gemini-3.1-pro-preview'
126
+ /** Legacy wire id emitted by older anvil.com/chat deployments. The shared
127
+ * completions API keeps accepting it, but routes it to DeepSeek direct. New
128
+ * chat code must use ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID instead. */
129
+ export const ANVIL_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID =
130
+ 'fireworks/deepseek-v4-flash'
131
+ // HY3 IS GONE, on every surface and every route (removed 2026-08-07). It left
132
+ // Anvil on 2026-08-04 but its wire ids lived on for paid/BYOK callers via
133
+ // web/src/llm-api/hy3-fallback.ts; that file, the Atlas Cloud adapter that was
134
+ // its paid lane, and the `tencent/hy3*` model-config entries have all been
135
+ // deleted. Nothing routes these slugs now — a request for one falls through to
136
+ // the ordinary unknown-model path.
137
+ export const ANVIL_MIMO_V25_MODEL_ID = mimoModels.mimoV25
138
+ /** GLM 5.2, served by CrofAI's direct OpenAI-compatible API (moved off
139
+ * Fireworks serverless 2026-07-29, at ~4x less than Fireworks' list price).
140
+ * The `z-ai/` prefix is a wire id inherited from the Fireworks era — nothing
141
+ * reaches Z.ai; CrofAI receives its native `glm-5.2` id (see CROF_MODEL_MAP).
142
+ *
143
+ * This is the ONLY GLM 5.2 route. Unlike the other picker models it is NOT
144
+ * freely available — it is unlocked by referring friends. Each qualified
145
+ * referral grants one 1-hour GLM session per day, uncapped since 2026-07-30.
146
+ * Gated by a per-user daily session pool whose limit equals the caller's GLM
147
+ * referral score (see the free-session quota).
148
+ *
149
+ * A second wire id (`crof/glm-5.2`) used to reach the same CrofAI upstream on
150
+ * the ordinary daily PREMIUM pool. It was retired from the pickers 2026-07-30
151
+ * and deleted outright 2026-08-04: the picker exclusion was client-side only,
152
+ * so hand-written API callers kept admitting sessions on it and collecting
153
+ * GLM 5.2 with zero referrals (12-49 distinct accounts/day, mostly known
154
+ * sock-puppet clusters). Never reintroduce a second wire id for a
155
+ * entitlement-gated model — the quota pool is chosen by model id, so an extra
156
+ * id is an extra door. */
157
+ export const ANVIL_GLM_V52_MODEL_ID = 'z-ai/glm-5.2'
158
+ /** GPT-5.6 Luna (OpenAI), served through OpenRouter. The id is OpenRouter's own
159
+ * slug, so it falls through to the default OpenRouter route with no
160
+ * provider-specific handler (same as Ling 3.0 Flash).
161
+ *
162
+ * Two things about this model are enforced server-side rather than left to the
163
+ * agent definitions, so they hold for every Anvil surface, every subagent,
164
+ * and BYOK callers alike (see applyOpenRouterProviderRouting and
165
+ * applyAnvilReasoningDefaults in web/src/llm-api/openrouter.ts):
166
+ *
167
+ * - Routing PREFERS OpenAI's own endpoint ($0.10/$0.60 per M) via `order`,
168
+ * with fallbacks allowed and cost bounded by ANVIL_GPT_5_6_LUNA_MAX_PRICE
169
+ * rather than by the pin. A hard pin (allow_fallbacks:false) held until
170
+ * 2026-08-16, when OpenAI began refusing every request from this account
171
+ * ("Policy Violation: this user has been blocked") and took Luna to a 100%
172
+ * failure rate with four usable endpoints sitting under the ceiling.
173
+ * - Reasoning effort is `high`. Luna is cheap enough per token that the
174
+ * quality is worth more than the reasoning tokens.
175
+ *
176
+ * Both are scoped to ANVIL traffic on purpose: `LITE_MODEL`
177
+ * (agents/constants.ts) is this same model id, so keying either off the model
178
+ * alone would change Anvil's paid lite mode as a side effect. */
179
+ export const ANVIL_GPT_5_6_LUNA_MODEL_ID = 'openai/gpt-5.6-luna'
180
+ /** OpenRouter provider slug Luna prefers (first in `provider.order`). */
181
+ export const ANVIL_GPT_5_6_LUNA_PROVIDER_ROUTE = 'openai'
182
+ /** Price ceiling for Luna, USD per million tokens. Sent as OpenRouter's
183
+ * `provider.max_price`, which REFUSES to route above it rather than serving
184
+ * and billing, so a provider re-pricing surfaces as a loud error instead of a
185
+ * surprise invoice. Since 2026-08-16 this — not a hard provider pin — is the
186
+ * sole cost guarantee, so it must not be widened casually.
187
+ *
188
+ * This is a COST FENCE, not an assertion of the list price, and the gap is
189
+ * deliberate on both sides:
190
+ *
191
+ * - It must sit ABOVE list. OpenRouter compares strictly: shipping the exact
192
+ * list price (0.1 / 0.6) made every Luna request 404 with "No endpoints
193
+ * found that satisfy the max price for this request" — verified against the
194
+ * live API on 2026-07-30, where 0.11/0.61 passed and 0.1/0.6 did not. A
195
+ * ceiling equal to list is an outage waiting on a rounding change.
196
+ * - It must stay under the next tier up. Azure, Azure EU and Amazon Bedrock
197
+ * listed $1.00/$6.00 when this was written and re-priced to $0.20/$1.20 by
198
+ * 2026-08-16; the ceiling admits them at today's price and would exclude
199
+ * them again if they returned to the old one.
200
+ *
201
+ * The headroom covers OpenAI's own tiers (list $0.10/$0.60, priority
202
+ * $0.20/$1.20, flex $0.05/$0.30) and ordinary price drift, while still failing
203
+ * closed well before a 10x endpoint could serve a request. */
204
+ export const ANVIL_GPT_5_6_LUNA_MAX_PRICE = {
205
+ prompt: 0.5,
206
+ completion: 3.0,
207
+ } as const
208
+ /** Reasoning effort every Luna turn runs at. */
209
+ export const ANVIL_GPT_5_6_LUNA_REASONING_EFFORT = 'high' as const
210
+ /**
211
+ * Kimi K3 (Eco), served by CrofAI. God-only on Anvil Web, for testing.
212
+ *
213
+ * The `crof/` prefix names the only place this exists — unlike the retired
214
+ * `crof/glm-5.2`, which was a SECOND id for a model already offered under
215
+ * `z-ai/glm-5.2` and became a quota-bypass route. There is no other id for
216
+ * this, so the prefix creates no such door. (Note the paid `moonshotai/kimi-*`
217
+ * slugs in model-config.ts are different models on a different provider, not
218
+ * second doors onto this one.)
219
+ *
220
+ * `-eco` is load-bearing in the WIRE id and deliberately absent from the
221
+ * DISPLAY name. CrofAI serves two K3 builds — `kimi-k3` at $2.00/$8.00 per M
222
+ * and this Q2_K-quantized `kimi-k3-eco` at $1.00/$4.00 — so the id must name
223
+ * the exact build or a future `kimi-k3` row would collide with it. The picker
224
+ * label is plain "Kimi K3" by request; see KIMI_K3_ECO_MODEL.
225
+ */
226
+ export const ANVIL_KIMI_K3_ECO_MODEL_ID = 'crof/kimi-k3-eco'
227
+ /**
228
+ * Extended-context tiers for the DeepSeek V4 and Luna routes.
229
+ *
230
+ * Wire ids only. These are provisioned per-account rather than offered from a
231
+ * client catalog, so they are deliberately absent from ANVIL_MODELS and
232
+ * from every quota list — a client that rendered one would offer a row most
233
+ * accounts cannot run. Requests carry the id directly on any free-mode root.
234
+ *
235
+ * Pricing and context windows track their base tier; the suffix names the
236
+ * provisioned variant, not a different model family, so nothing here needs a
237
+ * second entry in the price tables.
238
+ */
239
+ export const ANVIL_DEEPSEEK_V4_PRO_MAX_MODEL_ID =
240
+ 'deepseek/deepseek-v4-pro-max'
241
+ export const ANVIL_DEEPSEEK_V4_FLASH_MAX_MODEL_ID =
242
+ 'deepseek/deepseek-v4-flash-max'
243
+ export const ANVIL_GPT_5_6_LUNA_MAX_MODEL_ID = 'openai/gpt-5.6-luna-max'
244
+
245
+ /**
246
+ * Claude Fable 5 — Anthropic's frontier model, offered to free CLI users as a
247
+ * capacity-limited trial rather than as a standing picker model.
248
+ *
249
+ * It is deliberately NOT in ANVIL_MODELS: no client may render it from its
250
+ * own catalog. The server decides, per request, whether the shared pool still
251
+ * has sessions left and says so in the session response
252
+ * (`limitedModelOffers`); a client that receives nothing renders exactly what
253
+ * it rendered before the offer existed. See ANVIL_LIMITED_OFFER_MODEL_IDS.
254
+ */
255
+ export const ANVIL_FABLE_5_MODEL_ID = 'anthropic/claude-fable-5'
256
+
257
+ /**
258
+ * Meta Muse Spark 1.2 (Contributor tier), served by Meta's own developer API
259
+ * (`https://api.meta.ai/v1`, OpenAI-compatible chat completions). The `meta/`
260
+ * prefix names the only place it exists — there is no second wire id, so it
261
+ * cannot become a quota-bypass route the way `crof/glm-5.2` did.
262
+ *
263
+ * ANVIL WEB ONLY. It is absent from ANVIL_MODELS and
264
+ * SUPPORTED_ANVIL_MODELS, so no CLI/Desktop build can select it and
265
+ * `isAnvilSessionModelId` refuses it on those surfaces. Web reaches it
266
+ * through ANVIL_WEB_MODELS.
267
+ *
268
+ * The reason for the narrow surface is the rate limit, not the price: the
269
+ * Contributor tier is capped at 60 RPM per TEAM — i.e. across every Anvil
270
+ * user at once — against Standard's 3,000. That is roughly one request per
271
+ * second for the whole product, so this model needs the Convex-side queue
272
+ * (see docs/anvil-muse-spark.md) that the browser can render a wait for.
273
+ * The CLI has no such queue and would just surface 429s.
274
+ *
275
+ * Contributor pricing ($0.10/$0.002/$0.20 per M against Standard's
276
+ * $1.25/$0.15/$4.25) is bought with training rights over prompts and
277
+ * completions, which is why this is `dataUse: 'training'` and carries the
278
+ * AI-training warning.
279
+ */
280
+ export const ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID =
281
+ 'meta/muse-spark-1.2-contributor'
282
+ /** Meta's own model id for the wire id above — what api.meta.ai receives. */
283
+ export const MUSE_SPARK_12_CONTRIBUTOR_UPSTREAM_MODEL_ID =
284
+ 'muse-spark-1.2-contributor'
285
+ /** Published Contributor-tier limit: 60 requests/min PER TEAM, shared by every
286
+ * Anvil user. Sizes the queue's drain rate, so keep it in sync with
287
+ * https://dev.meta.ai/docs/pricing-rate-limits. */
288
+ export const MUSE_SPARK_CONTRIBUTOR_RPM = 60
289
+ /**
290
+ * Reasoning effort sent with every Muse Spark request.
291
+ *
292
+ * Muse Spark ALWAYS reasons — `reasoning_effort: "none"` is a hard 400 — so
293
+ * this chooses how much, not whether. The full ladder is
294
+ * minimal/low/medium/high/xhigh; Meta's own 400 on an unknown value names the
295
+ * set, which is the only place it is documented.
296
+ *
297
+ * Measured live 2026-08-06, same word problem, n=6 per level:
298
+ *
299
+ * minimal 153 reasoning tokens 1.0s
300
+ * low ~300 —
301
+ * high 362 1.9s
302
+ * xhigh 391 2.4s
303
+ *
304
+ * The top of the ladder is nearly flat: xhigh buys ~8% more reasoning than
305
+ * high, inside the run-to-run spread, and spends ~26% more latency for it.
306
+ * The real lever is downward — minimal is a 2.4x cut and still answered
307
+ * correctly on every sample. So read this constant as "max depth, latency
308
+ * accepted", and reach for `minimal` or `low` if a turn ever needs to feel
309
+ * fast. Cost barely enters into it: 500k output tokens across 347 prod
310
+ * requests came to roughly $0.10.
311
+ */
312
+ export const ANVIL_MUSE_SPARK_REASONING_EFFORT = 'xhigh' as const
313
+
314
+ /**
315
+ * The user-pickable ladders, named like Desktop's THROUGH_XHIGH / NO_XHIGH so
316
+ * the two catalogs read the same way.
317
+ *
318
+ * These are reusable provider-native ladders. A model's default is independent
319
+ * and may sit below the last rung.
320
+ */
321
+ export const EFFORTS_THROUGH_HIGH = ['low', 'medium', 'high'] as const
322
+ export const EFFORTS_THROUGH_XHIGH = [
323
+ 'minimal',
324
+ 'low',
325
+ 'medium',
326
+ 'high',
327
+ 'xhigh',
328
+ ] as const
329
+ export const EFFORTS_THROUGH_MAX = [
330
+ 'low',
331
+ 'medium',
332
+ 'high',
333
+ 'xhigh',
334
+ 'max',
335
+ ] as const
336
+ /**
337
+ * The three native DeepSeek V4 templates, shared by Flash 07/31 and Pro 08/13.
338
+ *
339
+ * DeepSeek publishes one requested→actual effort table for both models, and
340
+ * since the Pro 08/13 GA build it is genuinely identical (read off
341
+ * api-docs.deepseek.com/guides/thinking_mode, 2026-08-12): low→low, medium→high,
342
+ * high→high, xhigh→high, max→max. Pro used to collapse low into high, which is
343
+ * why it shipped a shorter ladder; that is no longer true, so the two rows now
344
+ * share this one. Medium is still not a distinct level on either model and is
345
+ * intentionally absent.
346
+ *
347
+ * The table is the ONLY source for this. DeepSeek's API accepts any
348
+ * `reasoning_effort` string without complaint — `"gigantic"` returns a normal
349
+ * 200 (verified against the live API, 2026-08-12) — so a rung being accepted
350
+ * proves nothing about it being distinct, and a ladder can never be derived by
351
+ * probing.
352
+ */
353
+ const DEEPSEEK_V4_REASONING_EFFORTS = ['low', 'high', 'max'] as const
354
+ /**
355
+ * The marker that turns a Muse Spark rate limit into a queued turn rather than
356
+ * a failed one.
357
+ *
358
+ * It travels twice in the same 429 — as `error.code`, and inside
359
+ * `error.message` — because only the message survives the whole path from
360
+ * `web/src/llm-api/meta.ts` through the AI SDK to the runner's error handling.
361
+ * The runner matches on it (see docs/anvil-muse-spark.md); if it ever stops
362
+ * matching, rate limits degrade to plain errors with no queue and no notice,
363
+ * which is exactly the failure this constant exists to make greppable.
364
+ */
365
+ export const MUSE_SPARK_RATE_LIMITED_ERROR_CODE = 'muse_spark_rate_limited'
366
+
367
+ /**
368
+ * Where a rate-limited Muse Spark request goes instead of waiting.
369
+ *
370
+ * DeepSeek V4 Pro since 2026-08-12 (GPT-5.6 Luna before it), and the choice is
371
+ * constrained rather than free on two counts:
372
+ *
373
+ * - The fallback must be a model the caller is ALREADY entitled to, or a rate
374
+ * limit would become a way to reach something they are not. Pro sits in the
375
+ * same shared daily premium pool as Muse Spark
376
+ * (ANVIL_WEB_PREMIUM_MODEL_IDS), so a rerouted request draws on exactly
377
+ * the quota the original would have.
378
+ * - It should be the model we would recommend anyway, since the user never
379
+ * chose it: Pro is now DEFAULT_ANVIL_WEB_MODEL_ID, so a reroute lands on
380
+ * the same model a new thread would have started on.
381
+ *
382
+ * Being text-only costs this nothing: images reaching a Anvil model that
383
+ * cannot see pixels are converted to vision-model descriptions at the
384
+ * completions layer (getAnvilModelImageSupport gates it), so a rerouted turn
385
+ * carrying an image still reads it.
386
+ */
387
+ export const MUSE_SPARK_FALLBACK_MODEL_ID = ANVIL_DEEPSEEK_V4_PRO_MODEL_ID
388
+
389
+ /**
390
+ * How long a caller may be asked to wait before the request is rerouted.
391
+ *
392
+ * Deliberately the same 10s as the provider's silent retry window, and that
393
+ * identity is the whole design: a wait we can hide costs nothing and keeps the
394
+ * user on the model they picked, while a wait we would have to *explain* is
395
+ * worse than quietly serving the answer on a peer model. Meta answers a real
396
+ * rate limit with `Retry-After: 60`, so in practice this splits cleanly —
397
+ * blips are absorbed, genuine saturation reroutes.
398
+ */
399
+ export const MUSE_SPARK_FALLBACK_AFTER_MS = 10_000
400
+
401
+ /** Picker copy for the tagline tooltip, and the single source for it — the
402
+ * server's behavior and the row's promise must not drift. Names the model
403
+ * MUSE_SPARK_FALLBACK_MODEL_ID actually points at; a catalog invariant test
404
+ * checks the two agree. */
405
+ export const MUSE_SPARK_FALLBACK_NOTICE =
406
+ 'Falls back to DeepSeek V4 Pro if the queue is too long.'
407
+
408
+ /** UI-only rollout switch. Backend support and free-mode allowlists remain
409
+ * wired even when these models are hidden from the Anvil picker. */
410
+ export const ANVIL_ENABLE_MIMO_MODELS_IN_UI = true
411
+ /** UI-only rollout switch for the streak indicator in the waiting room. */
412
+ export const ANVIL_ENABLE_STREAK_IN_UI = true
413
+ /** Local/debug switch: force the localhost free-mode country bypass into
414
+ * limited access so the limited Anvil UX can be exercised without an env
415
+ * var. */
416
+ export const ANVIL_FORCE_LIMITED_MODE = false
417
+ export const ANVIL_PREMIUM_SESSION_LIMIT = 6
418
+ export const ANVIL_LIMITED_SESSION_LIMIT = 6
419
+ /** Full-access Web/Cloud models outside the premium/referral pools. The CLI
420
+ * keeps these models unlimited; browser surfaces cap fresh sessions to deter
421
+ * automated project/session churn. */
422
+ export const ANVIL_WEB_STANDARD_SESSION_LIMIT = 6
423
+ export const ANVIL_PREMIUM_SESSION_RESET_TIMEZONE = 'America/Los_Angeles'
424
+ export const ANVIL_PREMIUM_SESSION_PERIOD = 'pacific_day'
425
+ /** GLM 5.2 referral-reward session pool. Distinct from the shared premium
426
+ * daily pool: GLM sessions reset daily (Pacific; weekly until 2026-07-29) and
427
+ * the per-user limit is the caller's GLM referral score, uncapped since
428
+ * 2026-07-30. Note the streak GLM bonus is a live entitlement on this same
429
+ * pool, so it refills at this cadence too. */
430
+ export const ANVIL_GLM_V52_SESSION_PERIOD = ANVIL_PREMIUM_SESSION_PERIOD
431
+ export const ANVIL_GLM_V52_SESSION_RESET_TIMEZONE =
432
+ ANVIL_PREMIUM_SESSION_RESET_TIMEZONE
433
+ export const ANVIL_GLM_V52_SESSION_WINDOW_HOURS = 24
434
+ // The GLM referral reward is UNCAPPED as of 2026-07-30 (it was
435
+ // ANVIL_GLM_V52_REFERRAL_CAP = 10): every qualified full-access referral
436
+ // grants one 1-hour GLM session per day, with no read-time ceiling. The only
437
+ // remaining bound is ANVIL_REFERRAL_SIGNUP_LIMIT (100 attributed rows per
438
+ // referrer, enforced at attribution), which is now the effective maximum
439
+ // rather than the anti-spam backstop it used to be.
440
+ /** Master kill-switch for the GLM 5.2 referral program. While true, qualified
441
+ * referrals grant daily GLM sessions and the CLI advertises the perk. Flip to
442
+ * false to wind the program down: entitlement drops to 0 for everyone and the
443
+ * CLI stops showing the banner. The perk is intentionally framed as
444
+ * limited-time in the UI so turning this off isn't a surprise. */
445
+ export const ANVIL_GLM_V52_REFERRAL_ENABLED = true
446
+ /** GLM sessions are exactly one hour of wall-clock time, regardless of the
447
+ * global free-session length, so the "1 hour per referral per day" promise is
448
+ * exact. */
449
+ export const ANVIL_GLM_V52_SESSION_LENGTH_MS = 60 * 60 * 1000
450
+ export const ANVIL_LIMITED_SESSION_RESET_TIMEZONE =
451
+ ANVIL_PREMIUM_SESSION_RESET_TIMEZONE
452
+ export const ANVIL_LIMITED_SESSION_PERIOD = ANVIL_PREMIUM_SESSION_PERIOD
453
+ export const ANVIL_WEB_STANDARD_SESSION_RESET_TIMEZONE =
454
+ ANVIL_PREMIUM_SESSION_RESET_TIMEZONE
455
+ export const ANVIL_WEB_STANDARD_SESSION_PERIOD =
456
+ ANVIL_PREMIUM_SESSION_PERIOD
457
+
458
+ /**
459
+ * Streak rewards. Once a user reaches a `ANVIL_STREAK_REWARD_INTERVAL_DAYS`
460
+ * (7)-day daily streak, they earn:
461
+ * - +1 session in their primary daily pool (premium for full-access users,
462
+ * limited for limited-access) **every day** the streak stays at 7+; and
463
+ * - for full-access users, +1 GLM 5.2 session per GLM-pool window per
464
+ * completed 7 days of the current streak (7 days → 1, 14 → 2), capped at
465
+ * `ANVIL_STREAK_GLM_BONUS_MAX_MULTIPLIER` (28-day streak), on top of
466
+ * referrals. The GLM pool resets daily (Pacific) since 2026-07-29, weekly
467
+ * before.
468
+ *
469
+ * The daily premium/limited bonus is persisted after today's first use. The
470
+ * GLM bonus is derived live from the current streak, so it refills at the GLM
471
+ * pool reset and shuts off as soon as the streak breaks.
472
+ */
473
+ export const ANVIL_STREAK_REWARD_INTERVAL_DAYS = 7
474
+ /** Cap on the GLM streak bonus: at most this many 7-day tiers count, so a
475
+ * 28-day (or longer) streak earns 4 GLM sessions per pool window. */
476
+ export const ANVIL_STREAK_GLM_BONUS_MAX_MULTIPLIER = 4
477
+ /** Master kill-switch for streak rewards. When false, streaks grant nothing
478
+ * and effective limits fall back to the base pool limits. */
479
+ export const ANVIL_STREAK_REWARDS_ENABLED = true
480
+ /** Sub-switch for the recurring full-access GLM 5.2 streak entitlement. Lets
481
+ * the perk be wound down independently of the premium/limited bonus (and of
482
+ * the separate referral-driven GLM program). */
483
+ export const ANVIL_STREAK_GLM_BONUS_ENABLED = true
484
+ /** Session units added to an eligible streak-reward pool. One whole session. */
485
+ export const ANVIL_STREAK_BONUS_SESSION_UNITS = 1
486
+
487
+ /** How much history the account hub's activity map covers. A year, matching
488
+ * what the grid can legibly draw at 53 columns. Free: the map is drawn from
489
+ * one narrow row per active day. */
490
+ export const ANVIL_USAGE_MAP_DAYS = 365
491
+
492
+ /** Lookback for the hub's token and message totals, which are aggregated from
493
+ * `message` on demand. Days rather than months on purpose: that table's cost
494
+ * scales with how much the account sent, not with the calendar. */
495
+ export const ANVIL_RECENT_TOKENS_DAYS = 7
496
+
497
+ /** Which session pool a streak bonus credit applies to. `premium` and `limited`
498
+ * are the daily pools (full vs limited access); `glm` is the weekly GLM 5.2
499
+ * pool (full access only). */
500
+ export type AnvilStreakRewardPool = 'premium' | 'limited' | 'glm'
501
+ /** Deprecated wire compatibility field. Session usage now resets at midnight
502
+ * Pacific time rather than using a rolling hourly window. */
503
+ export const ANVIL_PREMIUM_SESSION_WINDOW_HOURS = 24
504
+ export const ANVIL_LIMITED_SESSION_WINDOW_HOURS =
505
+ ANVIL_PREMIUM_SESSION_WINDOW_HOURS
506
+
507
+ const ANVIL_EASTERN_TIMEZONE = 'America/New_York'
508
+ const ANVIL_PACIFIC_TIMEZONE = 'America/Los_Angeles'
509
+
510
+ interface LocalTimeFormatOptions {
511
+ locale?: string
512
+ timeZone?: string
513
+ }
514
+
515
+ /** Full-access anvil models that benefit from spawning the gemini-thinker
516
+ * subagent for deeper reasoning. Covers every full-access picker model except
517
+ * the two limited-tier ones (DeepSeek V4 Flash, MiMo 2.5). Used by the CLI to
518
+ * toggle the gemini-thinker spawnable + prompts based on the user's pick, and
519
+ * by the server to admit gemini-thinker child requests against a parent
520
+ * session bound to one of these models. */
521
+ export const ANVIL_GEMINI_THINKER_PARENT_MODELS = new Set<string>([
522
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
523
+ ANVIL_MINIMAX_M3_MODEL_ID,
524
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
525
+ ])
526
+
527
+ export function canAnvilModelSpawnGeminiThinker(modelId: string): boolean {
528
+ return ANVIL_GEMINI_THINKER_PARENT_MODELS.has(modelId)
529
+ }
530
+
531
+ /**
532
+ * Hard context windows (in tokens) of the anvil models, keyed by the backend
533
+ * model id sent to the completions endpoint.
534
+ *
535
+ * Every number was read off a real provider rejection in prod rather than a
536
+ * spec sheet, so it is the limit the provider actually enforces:
537
+ * minimax-m3 "model maximum context length: 524287"
538
+ * deepseek-v4-flash "model maximum context length: 1048575"
539
+ * deepseek-v4-pro "This model's maximum context length is 1048576
540
+ * tokens. However, you requested 1300092 tokens"
541
+ * kimi-k2.7-code "Range of input length should be [1, 262144]"
542
+ *
543
+ * The consumer is agents/base-chat.ts, which prunes a chat thread's replayed
544
+ * history to a fraction of the selected model's window. Its `handleSteps` is
545
+ * serialized with toString() and so cannot import — it inlines a copy of this
546
+ * table, and agents/__tests__/base-chat.test.ts fails if the two drift. This
547
+ * is the reference copy: add a model here (with the rejection text that proves
548
+ * the number) and the test will tell you to mirror it.
549
+ *
550
+ * Models absent from the map fall back to ANVIL_DEFAULT_CONTEXT_WINDOW. The
551
+ * risk is asymmetric — guessing too high silently wedges a thread forever,
552
+ * guessing too low only prunes earlier than strictly needed — so a model is
553
+ * added only once its real limit has been observed.
554
+ */
555
+ export const ANVIL_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
556
+ [ANVIL_MINIMAX_M3_MODEL_ID]: 524_288,
557
+ [ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID]: 1_048_576,
558
+ // Read off the rejection above on 2026-08-12 — the same window as Flash, and
559
+ // the entry Pro had been missing since it shipped. Absent, base-chat gave a
560
+ // million-token model ANVIL_DEFAULT_CONTEXT_WINDOW's 131_072 and summarized
561
+ // a Pro chat thread at ~52k estimated tokens, 8x early. Unlike Luna and Muse
562
+ // Spark below this is an observed limit rather than a published one, so it is
563
+ // entered exactly.
564
+ [ANVIL_DEEPSEEK_V4_PRO_MODEL_ID]: 1_048_576,
565
+ // Luna is the one entry not read off a provider rejection. Every Luna
566
+ // endpoint OpenRouter lists (OpenAI, its flex/priority tiers, Azure, Bedrock)
567
+ // declares context_length 1_050_000, verified against the live endpoints API
568
+ // on 2026-08-01; 1_000_000 is deliberately entered instead, which stays on
569
+ // the safe side of the asymmetry above AND makes base-chat's 0.4 budget come
570
+ // out at exactly 400k.
571
+ //
572
+ // Absent, Luna fell to ANVIL_DEFAULT_CONTEXT_WINDOW and base-chat budgeted
573
+ // it 131_072 * 0.4 = 52_428 — a 20x under-estimate of a million-token model,
574
+ // which summarizes a chat thread that had plenty of room left. Every
575
+ // summarize rewrites history from the front and throws away the prompt cache
576
+ // with it.
577
+ [ANVIL_GPT_5_6_LUNA_MODEL_ID]: 1_000_000,
578
+ // Meta publishes 1,048,576 for every Muse Spark variant. Entered as 1_000_000
579
+ // for the same reason Luna is: it stays on the safe side of the asymmetry
580
+ // above while remaining an honest order of magnitude, where falling through
581
+ // to the 131_072 default would summarize a million-token thread 8x early.
582
+ [ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID]: 1_000_000,
583
+ }
584
+
585
+ /** Window assumed for any model missing from ANVIL_MODEL_CONTEXT_WINDOWS.
586
+ * Smaller than every window we have measured. */
587
+ export const ANVIL_DEFAULT_CONTEXT_WINDOW = 131_072
588
+
589
+ /** The "a better model exists" copy every superseded model points at, shared so
590
+ * the rows that carry it can't drift into different sentences. Two rows do
591
+ * today — MiniMax M3 and MiMo 2.5. (DeepSeek V4 Pro was the third until its
592
+ * 08/13 GA build overtook Flash again; see DEEPSEEK_V4_PRO_MODEL.)
593
+ *
594
+ * Names the DATED build. The wire id is undated and auto-updates, so the row a
595
+ * user is being steered TO is labelled "DeepSeek V4 Flash 07/31" in every
596
+ * picker (see DEEPSEEK_V4_FLASH_MODEL.displayName) — matching it exactly is
597
+ * what makes the notice point at something visible on screen rather than at a
598
+ * name nothing in the list carries.
599
+ *
600
+ * Kept short on purpose: pickers render it as its own line, and in the CLI it
601
+ * is the longest line in the menu, so it sets the width of every card. */
602
+ const FLASH_SUPERSEDES_NOTICE =
603
+ 'DeepSeek V4 Flash 07/31 performs better for most tasks.'
604
+
605
+ /** The same thing for the rows Pro overtook, which is currently GPT-5.6 Luna.
606
+ *
607
+ * A SECOND notice rather than a reworded shared one, because the two point
608
+ * somewhere different for different reasons: Flash's notice steers off models
609
+ * that are dearer AND weaker, while against Luna the cost comparison does not
610
+ * cleanly favor either side (see ANVIL_WEB_DEEMPHASIZED_MODEL_IDS for the
611
+ * numbers). That is why Luna is superseded but not de-emphasized — the argument
612
+ * here is quality and speed alone.
613
+ *
614
+ * Names the dated build, matching DEEPSEEK_V4_PRO_MODEL.displayName, so the row
615
+ * it steers to is one the user can see on screen. */
616
+ const PRO_SUPERSEDES_NOTICE =
617
+ 'DeepSeek V4 Pro 08/13 is smarter and faster.'
618
+
619
+ /**
620
+ * DeepSeek V4 Pro, on the 08/13 GA build (2026-08-12).
621
+ *
622
+ * SAME ENDPOINT AND SAME WIRE ID as the preview build it replaces, and that is
623
+ * not an assumption: DeepSeek direct serves only the undated ids, so the GA
624
+ * build arrived on `deepseek-v4-pro` with no route to add. Every dated slug is
625
+ * refused outright — `deepseek-v4-pro-0813` returns "The supported API model
626
+ * names are deepseek-v4-pro or deepseek-v4-flash" (verified against the live
627
+ * API, 2026-08-12), and /v1/models lists exactly the two undated ids. So there
628
+ * is nothing to plumb for a new build; what changes is what this row SAYS.
629
+ *
630
+ * Pricing is unchanged by the GA release ($0.435 in / $0.003625 cache read /
631
+ * $0.87 out per M), so DEEPSEEK_V4_PRO_PRICING in web/src/llm-api/deepseek.ts
632
+ * still holds.
633
+ */
634
+ const DEEPSEEK_V4_PRO_MODEL = {
635
+ id: ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
636
+ // Dated for the same reason Flash is: the wire id is undated and auto-updates,
637
+ // so an undated label tells a returning user nothing changed when in fact the
638
+ // GA build is a different model from the preview they formed an opinion about.
639
+ displayName: 'DeepSeek V4 Pro 08/13',
640
+ // Superlative on purpose, and it has to stay in step with Flash's: Pro is the
641
+ // recommended default, so the two DeepSeek rows are read against each other.
642
+ // "Smartest" vs Flash's "Smart & Fast" says which one is stronger and which
643
+ // one is the cheap, unlimited one — "Deep reasoning" said neither.
644
+ tagline: 'Smartest',
645
+ availability: 'always',
646
+ warning: ANVIL_AI_TRAINING_NOTICE,
647
+ dataUse: 'training',
648
+ premium: true,
649
+ multimodal: false,
650
+ // DeepSeek's own documented default (thinking on, effort high,
651
+ // api-docs.deepseek.com/guides/thinking_mode), sent explicitly so a
652
+ // provider-side default change cannot silently move Anvil. Unlike Flash,
653
+ // Pro has no fallback cascade — it is served on the direct lane only
654
+ // (deepseek-router.ts runs its lanes for Flash alone), so this is the one
655
+ // route the value has to be right for.
656
+ reasoningEffort: 'high',
657
+ // The 08/13 build maps low to a real low template, so Pro now offers the same
658
+ // three rungs as Flash. See DEEPSEEK_V4_REASONING_EFFORTS.
659
+ efforts: DEEPSEEK_V4_REASONING_EFFORTS,
660
+ defaultEffort: 'high',
661
+ // NOT superseded, and not de-emphasized (ANVIL_WEB_DEEMPHASIZED_MODEL_IDS)
662
+ // as of the 08/13 GA build. Pro carried a "V4 Flash performs better" notice
663
+ // from 2026-07-31, when the re-post-trained Flash-0731 beat the Pro PREVIEW on
664
+ // agent work. GA reversed that on exactly the benchmarks this product is:
665
+ // Terminal Bench 2.1 72.1 → 87.9, DeepSWE 12.8 → 62.7, CyberGym 52.7 → 83.3,
666
+ // DSBench-Hard 31.1 → 67.2, with 80.6% on SWE-bench Verified. Steering users
667
+ // off it would now be steering them off the stronger model.
668
+ //
669
+ // Pro is now the DEFAULT on every surface (DEFAULT_ANVIL_MODEL_ID for
670
+ // CLI/Desktop, DEFAULT_ANVIL_WEB_MODEL_ID for Web/Cloud). It is ~3x Flash's
671
+ // input and ~3x its output price and draws on the daily premium pool, which is
672
+ // why that was not automatic — but the pool is metered in SESSIONS, so leading
673
+ // with Pro moves nobody's quota, and the surfaces step down to
674
+ // FALLBACK_ANVIL_MODEL_ID (Flash) once a user's pool is spent.
675
+ isNew: true,
676
+ } as const satisfies AnvilModelOption
677
+
678
+ const MIMO_V25_MODEL = {
679
+ id: ANVIL_MIMO_V25_MODEL_ID,
680
+ displayName: 'MiMo 2.5',
681
+ tagline: 'Balanced',
682
+ availability: 'always',
683
+ dataUse: 'service',
684
+ premium: false,
685
+ multimodal: true,
686
+ // Xiaomi exposes only disabled and high (enabled) for MiMo 2.5. Since the
687
+ // product has no separate thinking on/off control, there is no depth ladder
688
+ // to render here; low/medium/max would merely be compatibility aliases.
689
+ // Same price as Flash and outclassed by it, so there is no cost argument to
690
+ // weigh — just a better model. Note this is the limited tier's other pick and
691
+ // its only natively-multimodal one; steering off it is only reasonable
692
+ // because Flash reads images through the describe pipeline on every surface
693
+ // (server/images/describe.ts, server/chat/image-context.ts).
694
+ supersededBy: {
695
+ modelId: ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
696
+ notice: FLASH_SUPERSEDES_NOTICE,
697
+ actionLabel: 'Switch to V4 Flash',
698
+ },
699
+ } as const satisfies AnvilModelOption
700
+
701
+ const DEEPSEEK_V4_FLASH_MODEL = {
702
+ id: ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
703
+ // Dated on purpose: the wire id is undated and auto-updates, so without the
704
+ // date a returning user sees the same name and assumes the same model. The
705
+ // 0731 GA build is a different, re-post-trained model.
706
+ displayName: 'DeepSeek V4 Flash 07/31',
707
+ // Stepped down from "Smartest & Fastest" when Pro took the recommendation on
708
+ // 2026-08-12: two rows cannot both claim the top. Flash is still the fastest
709
+ // thing here and the only unlimited one, which "Smart & Fast" keeps.
710
+ tagline: 'Smart & Fast',
711
+ availability: 'always',
712
+ warning: ANVIL_AI_TRAINING_NOTICE,
713
+ dataUse: 'training',
714
+ premium: false,
715
+ multimodal: false,
716
+ reasoningEffort: 'high',
717
+ // The 07/31 build has native low/high/max prompt templates. Medium is not a
718
+ // distinct level and is intentionally absent.
719
+ efforts: DEEPSEEK_V4_REASONING_EFFORTS,
720
+ defaultEffort: 'high',
721
+ isNew: true,
722
+ } as const satisfies AnvilModelOption
723
+
724
+ /**
725
+ * The provisioned extended-context tiers.
726
+ *
727
+ * Full rows so the provisioning tooling, the usage ledger and support have a
728
+ * display name and a data-use classification to read, exactly like every other
729
+ * model. They are deliberately NOT in ANVIL_MODELS, ANVIL_WEB_MODELS or
730
+ * any quota list: the tier is granted per account rather than picked, so a
731
+ * client that rendered one would offer a row most accounts cannot run, and a
732
+ * quota list would meter a tier whose ceiling is the grant itself.
733
+ *
734
+ * Reasoning defaults, pricing and context tracking their base tier is the
735
+ * point of the suffix — it names the provisioned variant, not a new family.
736
+ */
737
+ const DEEPSEEK_V4_PRO_MAX_MODEL = {
738
+ id: ANVIL_DEEPSEEK_V4_PRO_MAX_MODEL_ID,
739
+ displayName: 'DeepSeek V4 Pro (Max context)',
740
+ tagline: 'Extended context',
741
+ availability: 'always',
742
+ warning: ANVIL_AI_TRAINING_NOTICE,
743
+ dataUse: 'training',
744
+ premium: false,
745
+ multimodal: false,
746
+ reasoningEffort: 'high',
747
+ defaultEffort: 'high',
748
+ } as const satisfies AnvilModelOption
749
+
750
+ const DEEPSEEK_V4_FLASH_MAX_MODEL = {
751
+ id: ANVIL_DEEPSEEK_V4_FLASH_MAX_MODEL_ID,
752
+ displayName: 'DeepSeek V4 Flash (Max context)',
753
+ tagline: 'Extended context',
754
+ availability: 'always',
755
+ warning: ANVIL_AI_TRAINING_NOTICE,
756
+ dataUse: 'training',
757
+ premium: false,
758
+ multimodal: false,
759
+ reasoningEffort: 'high',
760
+ defaultEffort: 'high',
761
+ } as const satisfies AnvilModelOption
762
+
763
+ const GPT_5_6_LUNA_MAX_MODEL = {
764
+ id: ANVIL_GPT_5_6_LUNA_MAX_MODEL_ID,
765
+ displayName: 'GPT-5.6 Luna (Max context)',
766
+ tagline: 'Extended context',
767
+ availability: 'always',
768
+ dataUse: 'service',
769
+ premium: true,
770
+ multimodal: false,
771
+ reasoningEffort: ANVIL_GPT_5_6_LUNA_REASONING_EFFORT,
772
+ } as const satisfies AnvilModelOption
773
+
774
+ /**
775
+ * The provisioned tiers, as rows. Exported for the provisioning tooling and
776
+ * for support lookups; NOT spread into any catalog, for the reason above.
777
+ */
778
+ export const ANVIL_PROVISIONED_MODELS = [
779
+ DEEPSEEK_V4_PRO_MAX_MODEL,
780
+ DEEPSEEK_V4_FLASH_MAX_MODEL,
781
+ GPT_5_6_LUNA_MAX_MODEL,
782
+ ] as const satisfies readonly AnvilModelOption[]
783
+
784
+ const MINIMAX_M3_MODEL = {
785
+ id: ANVIL_MINIMAX_M3_MODEL_ID,
786
+ displayName: 'MiniMax M3',
787
+ tagline: 'Fastest',
788
+ availability: 'always',
789
+ dataUse: 'service',
790
+ // M3 is served by Fireworks without provider-side training. Its `service`
791
+ // data-use classification keeps it out of ANVIL_TRACED_MODEL_IDS.
792
+ premium: true,
793
+ multimodal: true,
794
+ // MiniMax M3 supports adaptive thinking or disabled thinking, but no effort
795
+ // levels. A depth picker would therefore be cosmetic.
796
+ // Flash overtook M3 on quality and is free rather than premium-pooled. M3
797
+ // stays selectable — it is still the no-AI-training pick and natively
798
+ // multimodal — but the picker says Flash is the better default.
799
+ supersededBy: {
800
+ modelId: ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
801
+ notice: FLASH_SUPERSEDES_NOTICE,
802
+ actionLabel: 'Switch to V4 Flash',
803
+ },
804
+ } as const satisfies AnvilModelOption
805
+
806
+ const GPT_5_6_LUNA_MODEL = {
807
+ id: ANVIL_GPT_5_6_LUNA_MODEL_ID,
808
+ displayName: 'GPT-5.6 Luna',
809
+ // Luna is the general-purpose premium option; its row's own badges (Images,
810
+ // no training notice) distinguish it from the other all-around models.
811
+ tagline: 'Strong all-around',
812
+ availability: 'always',
813
+ // OpenAI's API does not train on request data, and the route carries
814
+ // data_collection: 'deny', so no AI-training notice and no trace storage
815
+ // (ANVIL_TRACED_MODEL_IDS keys off this).
816
+ dataUse: 'service',
817
+ premium: true,
818
+ // OpenRouter reports input modalities text + image + file for this model.
819
+ multimodal: true,
820
+ reasoningEffort: ANVIL_GPT_5_6_LUNA_REASONING_EFFORT,
821
+ // OpenRouter's model metadata advertises all five enabled effort levels.
822
+ efforts: EFFORTS_THROUGH_MAX,
823
+ defaultEffort: ANVIL_GPT_5_6_LUNA_REASONING_EFFORT,
824
+ // Luna led the browser surfaces from 2026-08-04 until Pro's 08/13 GA build
825
+ // took the recommendation on 2026-08-12. It stays fully selectable, and stays
826
+ // the one premium row with no AI-training notice and native image input —
827
+ // reasons a user may still deliberately want it — but the picker now says
828
+ // plainly that Pro is the better default.
829
+ //
830
+ // NOT in ANVIL_WEB_DEEMPHASIZED_MODEL_IDS — which is now empty, but Luna
831
+ // would not qualify anyway: muting is this product's "materially dearer"
832
+ // signal, and against Pro that does not resolve — Pro is 2.76x cheaper on the cache reads that
833
+ // dominate agent traffic, and dearer on fresh input and output (full table on
834
+ // that constant). Steering on quality is honest; implying a settled price
835
+ // difference in either direction would not be.
836
+ supersededBy: {
837
+ modelId: ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
838
+ notice: PRO_SUPERSEDES_NOTICE,
839
+ actionLabel: 'Switch to V4 Pro',
840
+ },
841
+ } as const satisfies AnvilModelOption
842
+
843
+ const GLM_V52_MODEL = {
844
+ id: ANVIL_GLM_V52_MODEL_ID,
845
+ displayName: 'GLM 5.2',
846
+ tagline: 'Unlock by referring friends',
847
+ availability: 'always',
848
+ dataUse: 'service',
849
+ // Served by Fireworks without provider-side training; its `service`
850
+ // data-use classification keeps GLM out of ANVIL_TRACED_MODEL_IDS.
851
+ // `premium` drives the "Premium" badge styling in the picker; GLM's real
852
+ // gate is its weekly referral-session pool, not the daily premium pool.
853
+ premium: true,
854
+ multimodal: false,
855
+ // Our CrofAI route accepts but ignores reasoning_effort (including invalid
856
+ // values), so OpenRouter's GLM ladder does not describe the route users run.
857
+ } as const satisfies AnvilModelOption
858
+
859
+ /**
860
+ * Kimi K3 (Eco), CrofAI. God-only, for testing, and the cost is part of why.
861
+ *
862
+ * List price per M (CrofAI catalog, read from the live /v1/models endpoint on
863
+ * 2026-08-07): $1.00 in, $0.10 cache read, $4.00 out. Against DeepSeek V4
864
+ * Flash's $0.12/$0.21 on the same provider that is ~8x input and ~19x output,
865
+ * which is the argument for keeping it off the public picker rather than
866
+ * merely marking it premium.
867
+ *
868
+ * `displayName` is 'Kimi K3', NOT 'Kimi K3 Eco', by explicit request. This
869
+ * breaks the convention DEEPSEEK_V4_FLASH_MODEL sets — name the exact build so
870
+ * a returning user cannot mistake one for another — and the divergence is
871
+ * deliberate rather than an oversight, so do not "fix" it: CrofAI also serves a
872
+ * full `kimi-k3` at twice the price, and this row is the Q2_K-quantized Eco
873
+ * build (1M context, 131,072 max completion tokens). The wire id keeps `-eco`
874
+ * so the two builds stay distinguishable everywhere it actually matters —
875
+ * routing, billing, and the CROF_MODEL_MAP entry. If the full K3 is ever added
876
+ * as its own row, this label has to be disambiguated at that point.
877
+ */
878
+ const KIMI_K3_ECO_MODEL = {
879
+ id: ANVIL_KIMI_K3_ECO_MODEL_ID,
880
+ displayName: 'Kimi K3',
881
+ tagline: 'Via CrofAI',
882
+ availability: 'always',
883
+ dataUse: 'service',
884
+ premium: true,
885
+ multimodal: false,
886
+ experimental: true,
887
+ // CrofAI likewise ignores reasoning_effort for this build. Do not expose a
888
+ // control until this concrete route reports distinct supported levels.
889
+ } as const satisfies AnvilModelOption
890
+
891
+ const FABLE_5_MODEL = {
892
+ id: ANVIL_FABLE_5_MODEL_ID,
893
+ displayName: 'Claude Fable 5',
894
+ tagline: "Anthropic's most intelligent model",
895
+ availability: 'always',
896
+ // Load-bearing, not decoration: `dataUse: 'training'` is what puts this model
897
+ // in ANVIL_TRACED_MODEL_IDS, which is the entire point of the trial — we
898
+ // are buying hour-long agent traces with the pool. The warning is the
899
+ // disclosure that makes that legitimate, and the catalog invariant test
900
+ // requires the two to agree.
901
+ warning: ANVIL_AI_TRAINING_NOTICE,
902
+ dataUse: 'training',
903
+ // Not in ANVIL_PREMIUM_MODEL_IDS: the daily premium pool is shared across
904
+ // its models and Fable is metered by its OWN global pool instead (see
905
+ // ANVIL_LIMITED_OFFER_MODEL_IDS). The flag only marks it as scarce for the
906
+ // pickers' styling and for ANVIL_WEB_STANDARD_MODEL_IDS, which must not
907
+ // absorb it.
908
+ premium: true,
909
+ multimodal: true,
910
+ // OpenRouter reports low/medium/high/xhigh/max, with high as the default.
911
+ efforts: EFFORTS_THROUGH_MAX,
912
+ defaultEffort: 'high',
913
+ isNew: true,
914
+ } as const satisfies AnvilModelOption
915
+
916
+ /**
917
+ * Meta Muse Spark 1.2 Contributor. Premium on Web, and unusual in WHY: every
918
+ * other premium row is priced premium, while this one is cheaper per token than
919
+ * DeepSeek V4 Flash. What is scarce is the 60 RPM team-wide rate limit, so the
920
+ * daily premium session pool is doing double duty here as a way to bound how
921
+ * many people are inside that limit at once. See
922
+ * ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID.
923
+ */
924
+ const MUSE_SPARK_12_CONTRIBUTOR_MODEL = {
925
+ id: ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
926
+ displayName: 'Muse Spark 1.2',
927
+ // The tagline names the thing that actually differentiates this row for a
928
+ // user: it is the one model that can make you wait. Context length is not
929
+ // what they need to know before picking it.
930
+ tagline: 'Queue',
931
+ taglineTooltip: MUSE_SPARK_FALLBACK_NOTICE,
932
+ availability: 'always',
933
+ // Load-bearing pair (a catalog invariant test enforces it): the Contributor
934
+ // tier's whole discount is Meta training on prompts and completions.
935
+ warning: ANVIL_AI_TRAINING_NOTICE,
936
+ dataUse: 'training',
937
+ premium: true,
938
+ multimodal: false,
939
+ reasoningEffort: ANVIL_MUSE_SPARK_REASONING_EFFORT,
940
+ efforts: EFFORTS_THROUGH_XHIGH,
941
+ defaultEffort: ANVIL_MUSE_SPARK_REASONING_EFFORT,
942
+ } as const satisfies AnvilModelOption
943
+
944
+ export const SUPPORTED_ANVIL_MODELS = [
945
+ DEEPSEEK_V4_PRO_MODEL,
946
+ MINIMAX_M3_MODEL,
947
+ GPT_5_6_LUNA_MODEL,
948
+ GLM_V52_MODEL,
949
+ DEEPSEEK_V4_FLASH_MODEL,
950
+ MIMO_V25_MODEL,
951
+ FABLE_5_MODEL,
952
+ ] as const satisfies readonly AnvilModelOption[]
953
+
954
+ // GLM 5.2 is intentionally NOT in ANVIL_MODELS: it isn't a freely-pickable
955
+ // grid model, it's a referral reward surfaced by the separate referral banner.
956
+ // It stays in SUPPORTED_ANVIL_MODELS so the session/chat layers accept it as
957
+ // a valid model id once the user's weekly entitlement admits them.
958
+ //
959
+ // MiMo 2.5 Pro is GONE (2026-08-04). It was hidden from the client pickers on
960
+ // 2026-07-31 and kept server-valid for released clients, exactly as Kimi K2.7
961
+ // Code was; this is the second stage of that same retirement. Requests for it
962
+ // now 403 with free_mode_invalid_agent_model. The non-Pro MiMo 2.5 is
963
+ // unaffected, and paid/BYOK MiMo Pro plus its llm-api provider routing are
964
+ // untouched.
965
+ // Order is the order shown in every picker: the recommended default leads, the
966
+ // unlimited fallback it steps down to follows, then the rest of the full-access
967
+ // grid. V4 Pro took the lead on 2026-08-12 (see DEFAULT_ANVIL_MODEL_ID); the
968
+ // previous Flash-first order went stale with that flip.
969
+ export const ANVIL_MODELS = [
970
+ DEEPSEEK_V4_PRO_MODEL,
971
+ DEEPSEEK_V4_FLASH_MODEL,
972
+ GPT_5_6_LUNA_MODEL,
973
+ MINIMAX_M3_MODEL,
974
+ ...(ANVIL_ENABLE_MIMO_MODELS_IN_UI ? [MIMO_V25_MODEL] : []),
975
+ ] as const satisfies readonly AnvilModelOption[]
976
+
977
+ export const ANVIL_PREMIUM_MODEL_IDS = [
978
+ ANVIL_MINIMAX_M3_MODEL_ID,
979
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
980
+ ANVIL_GPT_5_6_LUNA_MODEL_ID,
981
+ ] as const
982
+
983
+ // ---------------------------------------------------------------------------
984
+ // Limited-offer models
985
+ //
986
+ // A model here is NOT in any client's picker catalog. The server counts how
987
+ // many sessions the current wave has left out of one GLOBAL pool and, only
988
+ // while the pool has capacity, tells the client about it in the session
989
+ // response (`limitedModelOffers`). Clients render the extra row from that
990
+ // payload and nothing else — so when the pool is spent, the offer disappears
991
+ // with no client release, and a client that never learned about the offer is
992
+ // byte-identical to what it is today.
993
+ //
994
+ // This exists because these are frontier models we cannot afford to leave
995
+ // standing open, and because the point of running them at all is the traces:
996
+ // they are `dataUse: 'training'`, so every hour-long session lands in
997
+ // chat_completion_traces (ANVIL_TRACED_MODEL_IDS).
998
+ // ---------------------------------------------------------------------------
999
+
1000
+ /** Models offered only while their shared global pool has sessions left. */
1001
+ export const ANVIL_LIMITED_OFFER_MODEL_IDS = [
1002
+ ANVIL_FABLE_5_MODEL_ID,
1003
+ ] as const
1004
+
1005
+ export type AnvilLimitedOfferModelId =
1006
+ (typeof ANVIL_LIMITED_OFFER_MODEL_IDS)[number]
1007
+
1008
+ /** Suffix-tolerant like the other model predicates, so a dated provider
1009
+ * snapshot can't dodge the pool accounting. */
1010
+ export function isAnvilLimitedOfferModelId(
1011
+ id: string | null | undefined,
1012
+ ): boolean {
1013
+ if (!id) return false
1014
+ return ANVIL_LIMITED_OFFER_MODEL_IDS.some((modelId) =>
1015
+ anvilModelIdMatches(id, modelId),
1016
+ )
1017
+ }
1018
+
1019
+ /**
1020
+ * Per-user daily ceiling on limited-offer sessions, on top of the global pool.
1021
+ *
1022
+ * One. A 50-session pool spent by five people is five traces of five people's
1023
+ * habits; spent by fifty people it is the distribution we actually want to
1024
+ * learn from. It also bounds what one account can cost us on a frontier model
1025
+ * whose sessions run a full hour.
1026
+ */
1027
+ export const ANVIL_LIMITED_OFFER_SESSION_LIMIT = 1
1028
+
1029
+ /** Reset cadence for the per-user ceiling above — same Pacific-day boundary as
1030
+ * every other anvil pool, so a user sees one reset time, not two. */
1031
+ export const ANVIL_LIMITED_OFFER_SESSION_PERIOD =
1032
+ ANVIL_PREMIUM_SESSION_PERIOD
1033
+ export const ANVIL_LIMITED_OFFER_SESSION_RESET_TIMEZONE =
1034
+ ANVIL_PREMIUM_SESSION_RESET_TIMEZONE
1035
+ export const ANVIL_LIMITED_OFFER_SESSION_WINDOW_HOURS =
1036
+ ANVIL_PREMIUM_SESSION_WINDOW_HOURS
1037
+
1038
+ /** Anvil Web-only picker/support set: the CLI/Desktop catalog plus the
1039
+ * earned GLM 5.2 row. */
1040
+ export const ANVIL_WEB_MODELS = [
1041
+ MUSE_SPARK_12_CONTRIBUTOR_MODEL,
1042
+ GLM_V52_MODEL,
1043
+ ...ANVIL_MODELS,
1044
+ ] as const satisfies readonly AnvilModelOption[]
1045
+
1046
+ export const ANVIL_WEB_GOD_ONLY_MODELS = [
1047
+ KIMI_K3_ECO_MODEL,
1048
+ ] as const satisfies readonly AnvilModelOption[]
1049
+
1050
+ export const ANVIL_WEB_ALL_MODELS = [
1051
+ ...ANVIL_WEB_GOD_ONLY_MODELS,
1052
+ ...ANVIL_WEB_MODELS,
1053
+ ] as const satisfies readonly AnvilModelOption[]
1054
+
1055
+ export const ANVIL_WEB_GOD_ONLY_MODEL_IDS = [
1056
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
1057
+ ] as const
1058
+
1059
+ /**
1060
+ * Web/Cloud models the picker no longer offers, while the backend keeps
1061
+ * honoring them so a session already running on one finishes normally: the id
1062
+ * stays in ANVIL_WEB_MODELS and in whichever quota list meters it, because
1063
+ * dropping a live session's model from the catalog fails admission mid-run and
1064
+ * dropping it from its quota list alone would leave it metered by NO pool.
1065
+ *
1066
+ * DELIBERATELY EMPTY, and the bar for adding to it is high.
1067
+ *
1068
+ * A picker-only retirement is a UI change, not a gate: the filter runs
1069
+ * client-side, so anything talking to the API directly still reaches the id.
1070
+ * Both former occupants proved it. The CrofAI GLM 5.2 route sat here from
1071
+ * 2026-07-30 and hand-written callers kept admitting free premium-pool GLM
1072
+ * sessions on it for five days. HY3 sat here since the initial web rollout.
1073
+ * Both were deleted outright on 2026-08-04.
1074
+ *
1075
+ * Park a model here ONLY to let genuinely live sessions drain, and only when
1076
+ * the id being reachable in the meantime is harmless — never as the gate
1077
+ * itself, and never for a model that costs real money or is entitlement-earned.
1078
+ * Then finish the removal.
1079
+ */
1080
+ export const ANVIL_WEB_RETIRED_PICKER_MODEL_IDS = [] as const
1081
+
1082
+ /** Whether the Web/Cloud picker should offer `id` as a new selection. False
1083
+ * for retired routes (see ANVIL_WEB_RETIRED_PICKER_MODEL_IDS), which the
1084
+ * backend still serves for sessions already on them. */
1085
+ export function isAnvilWebSelectableModelId(
1086
+ id: string | null | undefined,
1087
+ ): boolean {
1088
+ if (!id) return false
1089
+ return !ANVIL_WEB_RETIRED_PICKER_MODEL_IDS.some(
1090
+ (modelId) => modelId === id,
1091
+ )
1092
+ }
1093
+
1094
+ /** Models metered by the SHARED daily premium pool, which every full-access
1095
+ * account is granted for free. GLM 5.2 (ANVIL_GLM_V52_MODEL_IDS) is held
1096
+ * out because its entitlement is earned rather than granted daily — putting
1097
+ * any GLM route in this list hands the model out for nothing. */
1098
+ export const ANVIL_WEB_PREMIUM_MODEL_IDS = [
1099
+ ...ANVIL_PREMIUM_MODEL_IDS,
1100
+ // Metered by the web premium pool like every other god-only row. Being in
1101
+ // SOME pool is the point: ANVIL_WEB_STANDARD_MODEL_IDS is derived by
1102
+ // filtering `!premium`, so a premium model left out of here would be metered
1103
+ // by no pool at all rather than by a stricter one.
1104
+ ANVIL_KIMI_K3_ECO_MODEL_ID,
1105
+ // Not here for cost — Muse Spark Contributor is cheaper per token than the
1106
+ // Standard pool's models. The premium pool is what bounds how many users sit
1107
+ // inside its 60 RPM team-wide ceiling at once, and being in SOME pool is
1108
+ // mandatory: ANVIL_WEB_STANDARD_MODEL_IDS is derived by filtering
1109
+ // `!premium`, so a premium model left out of here is metered by no pool.
1110
+ ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID,
1111
+ ] as const
1112
+
1113
+ /** Full-access Web/Cloud models sharing the browser-only standard daily pool. */
1114
+ export const ANVIL_WEB_STANDARD_MODEL_IDS = Object.freeze(
1115
+ ANVIL_WEB_ALL_MODELS.filter((model) => !model.premium).map(
1116
+ (model) => model.id,
1117
+ ),
1118
+ )
1119
+
1120
+ /** Models unlocked by referrals, metered by the daily GLM session pool rather
1121
+ * than the daily premium pool. Kept separate from ANVIL_PREMIUM_MODEL_IDS
1122
+ * so GLM never falls into the shared daily premium quota. Since 2026-07-30
1123
+ * this is the ONLY way to reach GLM 5.2 on any surface. */
1124
+ export const ANVIL_GLM_V52_MODEL_IDS = [ANVIL_GLM_V52_MODEL_ID] as const
1125
+
1126
+ /** Models that occupy the single per-user "premium-bucket" CONCURRENCY slot in
1127
+ * Anvil Desktop's multi-session mode: at most one of these may have an
1128
+ * active session per user at a time, while unlimited-bucket models (DeepSeek V4
1129
+ * Flash, MiMo 2.5) may run in up to three concurrent tabs. (On the LIMITED
1130
+ * access tier the admission path puts EVERY model in the slot regardless of
1131
+ * this list — limited users get one anvil tab at a time; see
1132
+ * `requestDesktopSession`.)
1133
+ *
1134
+ * This is strictly a CONCURRENCY bucket, NOT a quota bucket. It is intentionally
1135
+ * a SUPERSET of ANVIL_PREMIUM_MODEL_IDS: it also includes GLM 5.2, which is
1136
+ * metered weekly for QUOTA purposes but expensive enough that we cap it to one
1137
+ * concurrent desktop session. Do NOT use this for the daily premium quota —
1138
+ * that stays on isAnvilPremiumModelId so GLM never starts burning the
1139
+ * 5/day premium pool. */
1140
+ export const ANVIL_DESKTOP_PREMIUM_BUCKET_MODEL_IDS = [
1141
+ ...ANVIL_PREMIUM_MODEL_IDS,
1142
+ ANVIL_GLM_V52_MODEL_ID,
1143
+ ] as const
1144
+
1145
+ /** Concurrent Anvil Desktop sessions per model bucket. Premium is also
1146
+ * enforced by the database's partial unique index; unlimited is enforced by
1147
+ * the desktop soft gate and the chat-completions session gate. */
1148
+ export const ANVIL_DESKTOP_SESSION_LIMITS = {
1149
+ premium: 1,
1150
+ unlimited: 3,
1151
+ } as const
1152
+ export type AnvilDesktopSessionBucket =
1153
+ keyof typeof ANVIL_DESKTOP_SESSION_LIMITS
1154
+
1155
+ /** True when a desktop tab running `model` under `accessTier` occupies the
1156
+ * single per-user concurrency slot. On the full tier that's the premium
1157
+ * bucket; on the LIMITED tier EVERY model occupies it — limited users get one
1158
+ * anvil tab at a time. THE shared definition of the one-tab rule: the
1159
+ * server's admission path and the desktop's picker/soft-gate must both call
1160
+ * this so the client can't drift from what the server enforces. */
1161
+ export function occupiesAnvilDesktopSlot(
1162
+ model: string,
1163
+ accessTier: AnvilAccessTier | null | undefined,
1164
+ ): boolean {
1165
+ return (
1166
+ accessTier === 'limited' || isAnvilDesktopPremiumBucketModelId(model)
1167
+ )
1168
+ }
1169
+
1170
+ export function getAnvilDesktopSessionBucket(
1171
+ model: string,
1172
+ accessTier: AnvilAccessTier | null | undefined,
1173
+ ): AnvilDesktopSessionBucket {
1174
+ return occupiesAnvilDesktopSlot(model, accessTier)
1175
+ ? 'premium'
1176
+ : 'unlimited'
1177
+ }
1178
+
1179
+ /** Wire headers for the free-mode session endpoints
1180
+ * (/api/v1/anvil/session). Shared so the server handlers and every client
1181
+ * (CLI, desktop) agree on the exact strings instead of redefining literals. */
1182
+ export const ANVIL_INSTANCE_HEADER = 'x-anvil-instance-id'
1183
+ export const ANVIL_MODEL_HEADER = 'x-anvil-model'
1184
+ /** Trusted server-to-server header. Only the Anvil API may honor this when
1185
+ * the request authenticates as the Anvil Web service account; browser and
1186
+ * normal API callers must not be able to select another user's session row. */
1187
+ export const ANVIL_ACTING_USER_HEADER = 'x-anvil-acting-user-id'
1188
+ /** Trusted Anvil Web/Cloud session-proxy hint. Keeps the normal CLI GET
1189
+ * response compact while letting the browser model picker request zero-usage
1190
+ * quota snapshots so it can render accurate "N of M sessions" labels. */
1191
+ export const ANVIL_INCLUDE_UNUSED_RATE_LIMITS_HEADER =
1192
+ 'x-anvil-include-unused-rate-limits'
1193
+ /** Set by the CLI on its recurring active-session poll. The response keeps the
1194
+ * authoritative session state but omits quota snapshots the CLI already has
1195
+ * and does not need for its countdown. */
1196
+ export const ANVIL_COMPACT_SESSION_HEADER = 'x-anvil-compact-session'
1197
+ /** Set to '1' by Anvil Desktop to opt into multi-session mode (concurrent
1198
+ * per-tab sessions); absent for CLI/web, which keep one session per user. */
1199
+ export const ANVIL_MULTI_SESSION_HEADER = 'x-anvil-multi-session'
1200
+ /** Set to '1' on a per-instance GET /session to mark it as a liveness beat: the
1201
+ * client is telling the server this tab is still there. Only a beat writes the
1202
+ * row's `last_seen_at`, and only a row with a `last_seen_at` can give up its
1203
+ * concurrency slot for going quiet — so a client that never beats keeps the
1204
+ * `expires_at`-only rule and is never dropped mid-window. */
1205
+ export const ANVIL_HEARTBEAT_HEADER = 'x-anvil-heartbeat'
1206
+ /** How often a beating client re-beats. The server's liveness TTL is a multiple
1207
+ * of this (several missed beats), so a brief network blip never costs a live
1208
+ * tab its slot. */
1209
+ export const ANVIL_SESSION_HEARTBEAT_INTERVAL_MS = 45_000
1210
+ /** Set on POST /session to the instance id of the single-slot holder the caller
1211
+ * was just told about, meaning "end that tab's session and give me the slot"
1212
+ * (Desktop's "Use it here"). Liveness covers a holder that stopped beating, but
1213
+ * it cannot help when the holder is genuinely alive somewhere the user cannot
1214
+ * reach — another machine, or a window they have no way to close — so this is
1215
+ * the escape hatch that does not depend on us having modelled liveness right.
1216
+ *
1217
+ * It names a holder rather than saying "take whatever is there": the server
1218
+ * only honors it when it still matches the holder the rejection identified, so
1219
+ * a click on a stale card can never end a tab the user was never shown. */
1220
+ export const ANVIL_TAKEOVER_INSTANCE_HEADER =
1221
+ 'x-anvil-takeover-instance-id'
1222
+ /** Drain window after a session's `expires_at`: the gate still serves an
1223
+ * in-flight agent run, but no new prompt should start. Shared because the
1224
+ * client has to know it too — a tab keeps beating until its row is past this,
1225
+ * which is what lets the server tell a run that is finishing from a tab that
1226
+ * died. Server-side accessor: `getSessionGraceMs()`. */
1227
+ export const ANVIL_SESSION_GRACE_MS = 30 * 60 * 1000
1228
+
1229
+ /** Models that accept image input. Used to decide whether uploaded images are
1230
+ * forwarded to the model as real multimodal content. */
1231
+ export const ANVIL_MULTIMODAL_MODEL_IDS = Object.freeze(
1232
+ SUPPORTED_ANVIL_MODELS.filter((model) => model.multimodal).map(
1233
+ (model) => model.id,
1234
+ ),
1235
+ )
1236
+
1237
+ export const ANVIL_WEB_MULTIMODAL_MODEL_IDS = Object.freeze(
1238
+ ANVIL_WEB_ALL_MODELS.filter((model) => model.multimodal).map(
1239
+ (model) => model.id,
1240
+ ),
1241
+ )
1242
+
1243
+ /** Free-mode models whose chat-completion traces we store in our own dataset
1244
+ * (chat_completion_traces). Derived from machine-readable data-use metadata;
1245
+ * UI wording can change without changing retention behavior. */
1246
+ export const ANVIL_TRACED_MODEL_IDS = SUPPORTED_ANVIL_MODELS.filter(
1247
+ (model: AnvilModelOption) => model.dataUse === 'training',
1248
+ ).map((model) => model.id)
1249
+
1250
+ export type AnvilModelId = (typeof ANVIL_MODELS)[number]['id']
1251
+ export type SupportedAnvilModelId =
1252
+ (typeof SUPPORTED_ANVIL_MODELS)[number]['id']
1253
+ export type AnvilPremiumModelId = (typeof ANVIL_PREMIUM_MODEL_IDS)[number]
1254
+ export type AnvilWebModelId = (typeof ANVIL_WEB_ALL_MODELS)[number]['id']
1255
+ export type AnvilWebPremiumModelId =
1256
+ (typeof ANVIL_WEB_PREMIUM_MODEL_IDS)[number]
1257
+
1258
+ /** What new anvil users see selected in the CLI and Desktop pickers, and the
1259
+ * model their "RECOMMENDED" hero opens on. DeepSeek V4 Pro 08/13 as of
1260
+ * 2026-08-12, taking over from DeepSeek V4 Flash, which held it from
1261
+ * 2026-07-31 when the re-post-trained Flash-0731 beat the Pro PREVIEW on agent
1262
+ * work. The 08/13 GA build reversed that on the benchmarks a coding CLI runs
1263
+ * (the numbers are on DEEPSEEK_V4_PRO_MODEL), so both surfaces now lead with
1264
+ * it — as the browser ones already did (DEFAULT_ANVIL_WEB_MODEL_ID).
1265
+ *
1266
+ * It is PREMIUM, which the Flash default was not, so it draws on the shared
1267
+ * daily pool and that pool CAN run dry. Surfaces that know the live quota must
1268
+ * step down to FALLBACK_ANVIL_MODEL_ID once it is spent —
1269
+ * getRecommendedAnvilModelId does that for the picker hero, Desktop's
1270
+ * availableAnvilDefault for an unpicked tab — or the default becomes a
1271
+ * model whose next send fails admission. Both kept that machinery from the
1272
+ * pre-2026-07-31 premium default, so this is a flip, not new plumbing.
1273
+ *
1274
+ * Still three separate constants: this one, DEFAULT_ANVIL_WEB_MODEL_ID and
1275
+ * FALLBACK_ANVIL_MODEL_ID (what callers needing a guaranteed-available id
1276
+ * for resolution / auto-fallbacks should use). The first two name the same
1277
+ * model today and have diverged before; the third is now genuinely a different
1278
+ * model rather than the same one under two names.
1279
+ *
1280
+ * It carries the AI-training notice like the Flash default did, so pickers
1281
+ * using it must render the model's `warning`. */
1282
+ export const DEFAULT_ANVIL_MODEL_ID: AnvilModelId =
1283
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID
1284
+
1285
+ /** What new Anvil Web/Cloud users see selected in the browser pickers, and
1286
+ * the model a new Cloud thread starts on. DeepSeek V4 Pro 08/13 as of
1287
+ * 2026-08-12, taking over from GPT-5.6 Luna (which held it from 2026-08-04).
1288
+ *
1289
+ * A browser build is the workload where model quality shows up most — it is
1290
+ * one long agentic run against a live sandbox, and a wrong turn early costs
1291
+ * the whole first project, which 51% of Web users never come back from. The
1292
+ * Pro 08/13 GA build is the strongest agentic model in this catalog, which is
1293
+ * what that workload is, so it leads the browser surfaces and Luna carries a
1294
+ * switch-to-Pro notice (see GPT_5_6_LUNA_MODEL.supersededBy).
1295
+ *
1296
+ * ONE KNOWN COST OF THIS CHOICE, deliberate rather than overlooked: Pro is
1297
+ * `dataUse: 'training'` while Luna was `service`, so the model a brand-new user
1298
+ * lands on now DOES carry the AI-training notice. Every picker already renders
1299
+ * `warning` for the default, so it is disclosed — but it is a real change to
1300
+ * what a first-time user sees.
1301
+ *
1302
+ * Spend is NOT among the costs, which is why this is not the extravagant choice
1303
+ * it looks like next to Luna's headline $0.10 input: browser turns re-send their
1304
+ * whole prefix every step, so cache reads are ~98% of the tokens, and Pro reads
1305
+ * cache at $0.003625/M against Luna's $0.010/M. See
1306
+ * ANVIL_WEB_DEEMPHASIZED_MODEL_IDS for the full table and where the
1307
+ * break-even sits. Either way the daily premium pool is counted in SESSIONS, so
1308
+ * nobody's quota moves.
1309
+ *
1310
+ * It is premium, so it draws on the shared daily pool. Surfaces that know the
1311
+ * live quota must step down to FALLBACK_ANVIL_MODEL_ID once that pool is
1312
+ * spent (getRecommendedAnvilWebModelId does this for the hero picker, and
1313
+ * the model selector coerces a spent default) — otherwise the default becomes
1314
+ * a model whose next send fails admission.
1315
+ *
1316
+ * Kept as its own constant from DEFAULT_ANVIL_MODEL_ID (CLI/Desktop) so the
1317
+ * browser surfaces can steer independently. They name the same model as of
1318
+ * 2026-08-12, and diverged as recently as 2026-08-04 → 2026-08-12 (Flash on
1319
+ * the CLI, Luna in the browser). */
1320
+ export const DEFAULT_ANVIL_WEB_MODEL_ID: AnvilWebModelId =
1321
+ ANVIL_DEEPSEEK_V4_PRO_MODEL_ID
1322
+
1323
+ /** Premium models the Web/Cloud picker renders small and muted: they are
1324
+ * materially more expensive per token than the recommended default without
1325
+ * being materially better for the browser surfaces' workloads. They stay
1326
+ * fully selectable — this only controls emphasis and ordering (they sort last
1327
+ * within the Premium group).
1328
+ *
1329
+ * This tracks the models Flash superseded — costing more per token AND having
1330
+ * lost the quality argument — so muting them is what steers new picks to Flash.
1331
+ * Both halves of that test have to hold: DeepSeek V4 Pro left this list on
1332
+ * 2026-08-12 because its 08/13 GA build wins the quality half again.
1333
+ *
1334
+ * EMPTY as of 2026-08-12. MiniMax M3 was the last entry, and with Pro gone it
1335
+ * was the only muted row in a list of full-size ones — the compact treatment
1336
+ * folds the tagline up onto the name line, which next to four two-line rows
1337
+ * read as a broken row rather than as a de-emphasized one. M3 keeps its
1338
+ * supersededBy notice, which is the steering that was doing the real work.
1339
+ * Muting only pays for itself on a group of rows, so add entries back in
1340
+ * pairs or not at all.
1341
+ *
1342
+ * GPT-5.6 Luna is superseded (by Pro) and deliberately NOT muted, because the
1343
+ * cost half genuinely does not resolve. Per M, read off OpenRouter 2026-08-12:
1344
+ *
1345
+ * fresh input cache read output
1346
+ * V4 Pro $0.435 $0.003625 $0.870
1347
+ * Luna $0.100 $0.010 $0.600
1348
+ *
1349
+ * Pro is 2.76x CHEAPER on cache reads — the term that dominates an agent
1350
+ * workload, where re-sent prefixes are ~98% of tokens — while being 4.35x
1351
+ * dearer on fresh input and 1.45x dearer on output. Break-even on the input
1352
+ * term alone is a ~98.1% cache-hit rate, so which model is cheaper depends on
1353
+ * the hit rate and on how output-heavy the traffic is. "Materially more
1354
+ * expensive" is a claim neither row can carry, and muting is reserved for rows
1355
+ * that clearly can. */
1356
+ export const ANVIL_WEB_DEEMPHASIZED_MODEL_IDS: readonly AnvilModelId[] =
1357
+ []
1358
+
1359
+ export function isAnvilWebDeemphasizedModelId(
1360
+ id: string | null | undefined,
1361
+ ): boolean {
1362
+ if (!id) return false
1363
+ return ANVIL_WEB_DEEMPHASIZED_MODEL_IDS.some((modelId) =>
1364
+ anvilModelIdMatches(id, modelId),
1365
+ )
1366
+ }
1367
+
1368
+ /** Always-available fallback used when the requested model can't be served
1369
+ * right now (unknown id, deployment hours closed, etc.). Kept distinct from
1370
+ * DEFAULT_ANVIL_MODEL_ID so a new user's "preferred default" can be the
1371
+ * smartest model without auto-flipping anyone to a closed serverless model. */
1372
+ export const FALLBACK_ANVIL_MODEL_ID: AnvilModelId =
1373
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID
1374
+
1375
+ export const LIMITED_ANVIL_MODEL_ID: AnvilModelId =
1376
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID
1377
+ export const LIMITED_ANVIL_MODEL_IDS = [
1378
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
1379
+ ANVIL_MIMO_V25_MODEL_ID,
1380
+ ] as const
1381
+ export const LIMITED_ANVIL_MODELS = LIMITED_ANVIL_MODEL_IDS.map(
1382
+ (modelId) => SUPPORTED_ANVIL_MODELS.find((model) => model.id === modelId)!,
1383
+ )
1384
+
1385
+ export type AnvilAccessTier = 'full' | 'limited'
1386
+
1387
+ /** Access tier carried in the Anvil Web Convex JWT. Extends the CLI tier
1388
+ * with 'blocked' (Tor / corroborated anonymous network): the app still
1389
+ * loads, but every agent send is rejected server-side. */
1390
+ export type AnvilWebAccessTier = AnvilAccessTier | 'blocked'
1391
+
1392
+ /** How many of a user's projects may have an agent RUNNING at the same time on
1393
+ * Anvil Web/Cloud. Past the cap the take-over prompt appears, and taking
1394
+ * over stops only the longest-idle run.
1395
+ *
1396
+ * STILL 1 — and raising it is a one-line change here ONLY once Anvil Web is
1397
+ * on the per-tab (multi-session) free-session store. On the single-session
1398
+ * store the web surface uses today, `admitOrTakeOver` rotates
1399
+ * `active_instance_id` on EVERY `requestSession`, including a same-model live
1400
+ * reclaim (web/src/server/free-session/store.ts). So the moment a second
1401
+ * project admitted its session, the first project's in-flight turn would fail
1402
+ * its next completions call with `session_superseded` — concurrency that
1403
+ * silently kills the run the user is watching. Desktop already runs concurrent
1404
+ * per-tab sessions via `requestSession({ multiSession, instanceId })`; wiring
1405
+ * Web to the same path (session request header + `anvil_multi_session` in
1406
+ * the runner's `anvil_metadata`) is what unblocks 2.
1407
+ *
1408
+ * Everything else is already cap-agnostic: rows are per (user, project) and
1409
+ * only `agent_running` ones count, so opening or reading a second project is
1410
+ * free at any cap — that part shipped 2026-08-12. */
1411
+ export const ANVIL_MAX_CONCURRENT_PROJECTS = 1
1412
+
1413
+ /** Abuse backstop on project creation for outer-region (limited-tier) Anvil
1414
+ * Web users. A project the user still has consumes one slot — creations that
1415
+ * failed and were rolled back do not, so a bad creation never costs quota.
1416
+ * The quota resets at midnight Pacific time.
1417
+ *
1418
+ * This was 3 until 2026-08-12, which real users hit on their first session:
1419
+ * every rung of the creation ladder (warm pool → cold Daytona → browser
1420
+ * runtime) mints its own project row, so a couple of failed first builds
1421
+ * locked someone out for the day with nothing to show for it. 10 is high
1422
+ * enough that only automation reaches it. */
1423
+ export const ANVIL_WEB_LIMITED_PROJECT_DAILY_LIMIT = 10
1424
+
1425
+ /** Per-day cap on blank ("plan a custom stack") Cloud projects, which unlike
1426
+ * connect-repo need no GitHub App install and each boot a full-size VM. Same
1427
+ * backstop role — and the same rollback exemption — as the web limit above.
1428
+ * Resets at midnight Pacific time. */
1429
+ export const ANVIL_CLOUD_BLANK_PROJECT_DAILY_LIMIT = 10
1430
+
1431
+ /** Per-project ceiling on custom-stack planner turns.
1432
+ *
1433
+ * The planner is a free premium-model chat that never touches a sandbox, so
1434
+ * without a ceiling one blank project is an unbounded free MiniMax M3
1435
+ * conversation — the cheapest abuse route into the premium pool, since it skips
1436
+ * the VM work every other free surface pays for.
1437
+ *
1438
+ * Sized well above honest use: the prompt caps discovery at two question
1439
+ * rounds, so a real conversation is a seed turn, two answers, and a few stack
1440
+ * revisions. Hitting this means the plan is not converging.
1441
+ *
1442
+ * Only planning turns count. "Start building" is a separate mutation, so a user
1443
+ * who exhausts the cap with a finished plan can still build — they just cannot
1444
+ * keep chatting. */
1445
+ export const ANVIL_CLOUD_PLANNER_TURN_LIMIT = 12
1446
+
1447
+ /** Models available to limited-region Anvil Web users. They share the
1448
+ * limited-region session pool; every other model remains geo-gated. */
1449
+ export const ANVIL_WEB_GEO_EXEMPT_MODEL_IDS = [
1450
+ ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
1451
+ ANVIL_MIMO_V25_MODEL_ID,
1452
+ ] as const
1453
+
1454
+ export function isAnvilWebGeoExemptModelId(
1455
+ id: string | null | undefined,
1456
+ ): boolean {
1457
+ if (!id) return false
1458
+ return ANVIL_WEB_GEO_EXEMPT_MODEL_IDS.some((modelId) => modelId === id)
1459
+ }
1460
+
1461
+ /** Models a limited-tier Anvil Web user may select. */
1462
+ export const ANVIL_WEB_LIMITED_MODEL_IDS = [
1463
+ ...new Set<string>([
1464
+ ...ANVIL_WEB_GEO_EXEMPT_MODEL_IDS,
1465
+ ...LIMITED_ANVIL_MODEL_IDS,
1466
+ ]),
1467
+ ]
1468
+
1469
+ export function isAnvilWebModelAllowedForLimitedTier(
1470
+ id: string | null | undefined,
1471
+ ): boolean {
1472
+ if (!id) return false
1473
+ // GLM 5.2 is selectable from a limited region when the user holds a bounty
1474
+ // grant — the entitlement gate is the GLM quota pool, not this allowlist
1475
+ // (see isGlmRedeemableAtLimitedTier). Without this the Web picker coerced a
1476
+ // GLM pick straight back to the flash model, so a bounty reward earned in a
1477
+ // limited region was unspendable no matter what the server allowed.
1478
+ return (
1479
+ isGlmRedeemableAtLimitedTier(id) ||
1480
+ ANVIL_WEB_LIMITED_MODEL_IDS.some((modelId) => modelId === id)
1481
+ )
1482
+ }
1483
+
1484
+ /** Coerce a limited-tier Anvil Web selection (premium ids, stale
1485
+ * localStorage values) to the allowed default (DeepSeek V4 Flash). */
1486
+ export function resolveAnvilWebModelForLimitedTier(
1487
+ id: string | null | undefined,
1488
+ ): string {
1489
+ return isAnvilWebModelAllowedForLimitedTier(id)
1490
+ ? (id as string)
1491
+ : LIMITED_ANVIL_MODEL_ID
1492
+ }
1493
+
1494
+ export function getAnvilModelsForAccessTier(
1495
+ accessTier: AnvilAccessTier | null | undefined,
1496
+ ): readonly AnvilModelOption[] {
1497
+ if (accessTier === 'limited') return LIMITED_ANVIL_MODELS
1498
+ return ANVIL_MODELS
1499
+ }
1500
+
1501
+ /** The model the CLI/Desktop picker highlights as the "recommended" hero so a
1502
+ * new user can start with one Enter press without scanning the full list. Full
1503
+ * access → DEFAULT_ANVIL_MODEL_ID (DeepSeek V4 Pro 08/13 — the strongest
1504
+ * agentic model in the catalog); limited → the always-available flash model.
1505
+ *
1506
+ * Pro is premium, so ALWAYS pass `premiumExhausted` from the live quota
1507
+ * snapshot: the hero flips to the unlimited DeepSeek Flash once the daily pool
1508
+ * runs out, because the recommended pick has to stay joinable. A caller that
1509
+ * omits it will offer a hero whose next send fails admission. */
1510
+ export function getRecommendedAnvilModelId(
1511
+ accessTier: AnvilAccessTier | null | undefined,
1512
+ options: { premiumExhausted?: boolean } = {},
1513
+ ): SupportedAnvilModelId {
1514
+ if (accessTier === 'limited') return LIMITED_ANVIL_MODEL_ID
1515
+ if (options.premiumExhausted) return FALLBACK_ANVIL_MODEL_ID
1516
+ return DEFAULT_ANVIL_MODEL_ID
1517
+ }
1518
+
1519
+ /** The Web/Cloud counterpart of getRecommendedAnvilModelId: full access →
1520
+ * DEFAULT_ANVIL_WEB_MODEL_ID (GPT-5.6 Luna); limited → the
1521
+ * always-available flash model. `premiumExhausted` flips the hero to the
1522
+ * unlimited flash model so the recommended pick is always joinable. */
1523
+ export function getRecommendedAnvilWebModelId(
1524
+ accessTier: AnvilAccessTier | null | undefined,
1525
+ options: { premiumExhausted?: boolean } = {},
1526
+ ): AnvilWebModelId {
1527
+ if (accessTier === 'limited') return LIMITED_ANVIL_MODEL_ID
1528
+ if (options.premiumExhausted) return FALLBACK_ANVIL_MODEL_ID
1529
+ return DEFAULT_ANVIL_WEB_MODEL_ID
1530
+ }
1531
+
1532
+ /**
1533
+ * GLM 5.2 is reachable from limited access, but only against a bounty-earned
1534
+ * grant.
1535
+ *
1536
+ * The tier gate used to live here, in the model allowlist: a limited-tier
1537
+ * (VPN / unsupported-country) user could not name GLM at all. Bounties pay a
1538
+ * GLM session that is meant to be worth the same in every region, so the gate
1539
+ * moved DOWN into the quota pool — at limited tier the GLM pool counts only
1540
+ * grants minted `redeemable_at_limited_tier` (bounty payouts), and nothing
1541
+ * else. Referral GLM entitlement still counts for nothing there, which is the
1542
+ * anti-farming stance docs/referrals.md describes.
1543
+ *
1544
+ * The practical effect of allowing it here is that a limited user with no
1545
+ * bounty grant gets `rate_limited` (limit 0) instead of `session_model_
1546
+ * mismatch`. Clients only surface GLM to them once the server reports a
1547
+ * balance, so that path is not a normal one to hit.
1548
+ */
1549
+ export function isGlmRedeemableAtLimitedTier(
1550
+ model: string | null | undefined,
1551
+ ): boolean {
1552
+ return ANVIL_GLM_V52_MODEL_IDS.some((modelId) => modelId === model)
1553
+ }
1554
+
1555
+ export function isAnvilModelAllowedForAccessTier(
1556
+ model: string | null | undefined,
1557
+ accessTier: AnvilAccessTier | null | undefined,
1558
+ ): boolean {
1559
+ if (!model) return false
1560
+ if (accessTier !== 'limited') return isAnvilModelId(model)
1561
+ return (
1562
+ isGlmRedeemableAtLimitedTier(model) ||
1563
+ LIMITED_ANVIL_MODEL_IDS.some((modelId) => modelId === model)
1564
+ )
1565
+ }
1566
+
1567
+ /** Session admission is shared by CLI/Desktop/Web/Cloud. Client pickers use
1568
+ * ANVIL_MODELS or ANVIL_WEB_MODELS, while the server accepts their union
1569
+ * with temporarily retired models from SUPPORTED_ANVIL_MODELS. */
1570
+ export function isAnvilSessionModelId(
1571
+ id: string | null | undefined,
1572
+ ): id is SupportedAnvilModelId | AnvilWebModelId {
1573
+ return (
1574
+ isSupportedAnvilModelId(id) ||
1575
+ isAnvilWebModelId(id, {
1576
+ includeGodOnly: true,
1577
+ })
1578
+ )
1579
+ }
1580
+
1581
+ export function isAnvilSessionModelAllowedForAccessTier(
1582
+ model: string | null | undefined,
1583
+ accessTier: AnvilAccessTier | null | undefined,
1584
+ ): boolean {
1585
+ if (!model) return false
1586
+ if (accessTier !== 'limited') return isAnvilSessionModelId(model)
1587
+ // See isGlmRedeemableAtLimitedTier: GLM's limited-tier gate is the quota
1588
+ // pool (bounty grants only), not this allowlist.
1589
+ return (
1590
+ isGlmRedeemableAtLimitedTier(model) ||
1591
+ LIMITED_ANVIL_MODEL_IDS.some((modelId) => modelId === model)
1592
+ )
1593
+ }
1594
+
1595
+ export function isAnvilModelId(
1596
+ id: string | null | undefined,
1597
+ ): id is AnvilModelId {
1598
+ if (!id) return false
1599
+ return ANVIL_MODELS.some((m) => m.id === id)
1600
+ }
1601
+
1602
+ export function isAnvilWebModelId(
1603
+ id: string | null | undefined,
1604
+ options: { includeGodOnly?: boolean } = {},
1605
+ ): id is AnvilWebModelId {
1606
+ if (!id) return false
1607
+ const models = options.includeGodOnly
1608
+ ? ANVIL_WEB_ALL_MODELS
1609
+ : ANVIL_WEB_MODELS
1610
+ return models.some((m) => m.id === id)
1611
+ }
1612
+
1613
+ export function isAnvilWebGodOnlyModelId(
1614
+ id: string | null | undefined,
1615
+ ): boolean {
1616
+ if (!id) return false
1617
+ return ANVIL_WEB_GOD_ONLY_MODEL_IDS.some((modelId) => modelId === id)
1618
+ }
1619
+
1620
+ export function resolveAnvilModel(
1621
+ id: string | null | undefined,
1622
+ ): AnvilModelId {
1623
+ return isAnvilModelId(id) ? id : FALLBACK_ANVIL_MODEL_ID
1624
+ }
1625
+
1626
+ export function resolveAnvilWebModel(
1627
+ id: string | null | undefined,
1628
+ options: { includeGodOnly?: boolean } = {},
1629
+ ): AnvilWebModelId {
1630
+ return isAnvilWebModelId(id, options)
1631
+ ? id
1632
+ : (FALLBACK_ANVIL_MODEL_ID as AnvilWebModelId)
1633
+ }
1634
+
1635
+ /** Resolve an explicit CLI selection for an access tier. The ordinary picker
1636
+ * uses `ANVIL_MODELS`; full-access users can also select referral-only GLM
1637
+ * through its separate banner action, or a limited-offer model the server told
1638
+ * them about this launch. Both live outside `ANVIL_MODELS`, so without these
1639
+ * passes an explicit pick of either would be silently rewritten to the fallback
1640
+ * model — the user would press Enter on Fable and land on DeepSeek. */
1641
+ export function resolveAnvilModelForAccessTier(
1642
+ id: string | null | undefined,
1643
+ accessTier: AnvilAccessTier | null | undefined,
1644
+ ):
1645
+ | AnvilModelId
1646
+ | typeof ANVIL_GLM_V52_MODEL_ID
1647
+ | AnvilLimitedOfferModelId {
1648
+ if (accessTier === 'limited') {
1649
+ // GLM survives the coercion at limited tier so a bounty-earned session is
1650
+ // launchable from any region; the pool decides whether it is joinable.
1651
+ if (id === ANVIL_GLM_V52_MODEL_ID) return id
1652
+ return isAnvilModelAllowedForAccessTier(id, accessTier)
1653
+ ? (id as AnvilModelId)
1654
+ : LIMITED_ANVIL_MODEL_ID
1655
+ }
1656
+ if (id === ANVIL_GLM_V52_MODEL_ID) return id
1657
+ const limitedOffer = ANVIL_LIMITED_OFFER_MODEL_IDS.find(
1658
+ (modelId) => modelId === id,
1659
+ )
1660
+ if (limitedOffer) return limitedOffer
1661
+ return resolveAnvilModel(id)
1662
+ }
1663
+
1664
+ export function resolveAnvilSessionModelForAccessTier(
1665
+ id: string | null | undefined,
1666
+ accessTier: AnvilAccessTier | null | undefined,
1667
+ options: { includeGodOnly?: boolean } = {},
1668
+ ): SupportedAnvilModelId | AnvilWebModelId {
1669
+ if (accessTier === 'limited') {
1670
+ return isAnvilSessionModelAllowedForAccessTier(id, accessTier)
1671
+ ? (id as SupportedAnvilModelId)
1672
+ : LIMITED_ANVIL_MODEL_ID
1673
+ }
1674
+ if (isSupportedAnvilModelId(id)) return id
1675
+ return resolveAnvilWebModel(id, {
1676
+ includeGodOnly: options.includeGodOnly ?? true,
1677
+ })
1678
+ }
1679
+
1680
+ export function isSupportedAnvilModelId(
1681
+ id: string | null | undefined,
1682
+ ): id is SupportedAnvilModelId {
1683
+ if (!id) return false
1684
+ return SUPPORTED_ANVIL_MODELS.some((m) => m.id === id)
1685
+ }
1686
+
1687
+ /**
1688
+ * Match a model id against a base id, tolerating the dated provider snapshot
1689
+ * suffix OpenRouter (and our own routing) appends, e.g.
1690
+ * `google/gemini-3.1-pro-preview-20260219` for base `google/gemini-3.1-pro-preview`.
1691
+ * Mirrors the suffix logic in `isFreeModeAllowedAgentModel` (free-agents.ts) —
1692
+ * the two MUST stay in sync. Only a `-YYYYMMDD`-style suffix matches, so e.g.
1693
+ * `mimo-v2.5-pro` never matches the base `mimo-v2.5`.
1694
+ */
1695
+ export function anvilModelIdMatches(
1696
+ candidate: string | null | undefined,
1697
+ baseId: string,
1698
+ ): boolean {
1699
+ if (!candidate) return false
1700
+ if (candidate === baseId) return true
1701
+ const prefix = baseId + '-'
1702
+ if (!candidate.startsWith(prefix)) return false
1703
+ return /^\d{6,8}(?:$|[-:])/.test(candidate.slice(prefix.length))
1704
+ }
1705
+
1706
+ /** Whether the requested model is Gemini Pro, tolerating the dated snapshot
1707
+ * suffix. Use this instead of `=== ANVIL_GEMINI_PRO_MODEL_ID` so a caller
1708
+ * can't dodge a Gemini gate by sending the dated id. */
1709
+ export function isAnvilGeminiProModelId(
1710
+ id: string | null | undefined,
1711
+ ): boolean {
1712
+ return anvilModelIdMatches(id, ANVIL_GEMINI_PRO_MODEL_ID)
1713
+ }
1714
+
1715
+ export function isAnvilPremiumModelId(
1716
+ id: string | null | undefined,
1717
+ ): id is AnvilPremiumModelId {
1718
+ if (!id) return false
1719
+ // Suffix-tolerant: a dated variant of a premium id (e.g. a dated Kimi) must
1720
+ // still count as premium so it can't dodge the premium daily rate cap.
1721
+ return ANVIL_PREMIUM_MODEL_IDS.some((modelId) =>
1722
+ anvilModelIdMatches(id, modelId),
1723
+ )
1724
+ }
1725
+
1726
+ export function isAnvilWebPremiumModelId(
1727
+ id: string | null | undefined,
1728
+ ): id is AnvilWebPremiumModelId {
1729
+ if (!id) return false
1730
+ return ANVIL_WEB_PREMIUM_MODEL_IDS.some((modelId) =>
1731
+ anvilModelIdMatches(id, modelId),
1732
+ )
1733
+ }
1734
+
1735
+ /** True for the Muse Spark wire id. Suffix-tolerant like the other model
1736
+ * predicates so a dated provider snapshot can't slip past the rate-limit queue
1737
+ * that keys off it (see docs/anvil-muse-spark.md). */
1738
+ export function isMuseSparkModelId(id: string | null | undefined): boolean {
1739
+ if (!id) return false
1740
+ return anvilModelIdMatches(id, ANVIL_MUSE_SPARK_12_CONTRIBUTOR_MODEL_ID)
1741
+ }
1742
+
1743
+ export function isAnvilSessionPremiumModelId(
1744
+ id: string | null | undefined,
1745
+ ): boolean {
1746
+ return isAnvilWebPremiumModelId(id)
1747
+ }
1748
+
1749
+ /** Whether `model` occupies the one-per-user Anvil Desktop premium
1750
+ * CONCURRENCY slot (premium models + GLM 5.2). Suffix-tolerant
1751
+ * (dated snapshots) like the other model predicates so a dated variant can't
1752
+ * dodge the cap. Distinct from isAnvilPremiumModelId, which gates the daily
1753
+ * premium QUOTA and must NOT include GLM. */
1754
+ export function isAnvilDesktopPremiumBucketModelId(
1755
+ id: string | null | undefined,
1756
+ ): boolean {
1757
+ if (!id) return false
1758
+ return ANVIL_DESKTOP_PREMIUM_BUCKET_MODEL_IDS.some((modelId) =>
1759
+ anvilModelIdMatches(id, modelId),
1760
+ )
1761
+ }
1762
+
1763
+ /** Whether the requested model is the GLM 5.2 referral reward, tolerating the
1764
+ * dated snapshot suffix. GLM is metered by the weekly referral-session pool
1765
+ * rather than the daily premium pool, so callers branch on this before the
1766
+ * premium check. */
1767
+ export function isAnvilGlmV52ModelId(
1768
+ id: string | null | undefined,
1769
+ ): boolean {
1770
+ return ANVIL_GLM_V52_MODEL_IDS.some((modelId) =>
1771
+ anvilModelIdMatches(id, modelId),
1772
+ )
1773
+ }
1774
+
1775
+ /** Whether the requested model is GPT-5.6 Luna, tolerating the dated snapshot
1776
+ * suffix. Used by the OpenRouter layer to apply Luna's pinned routing and
1777
+ * reasoning effort, so a dated variant can't dodge either. */
1778
+ export function isAnvilGpt56LunaModelId(
1779
+ id: string | null | undefined,
1780
+ ): boolean {
1781
+ return anvilModelIdMatches(id, ANVIL_GPT_5_6_LUNA_MODEL_ID)
1782
+ }
1783
+
1784
+ /** The catalog's reasoning effort for the requested model, tolerating dated
1785
+ * snapshot suffixes like every other id helper. Null for models that carry
1786
+ * none — see AnvilModelOption.reasoningEffort. */
1787
+ /** The catalog row for any surface's id, or undefined. Both catalogs, for the
1788
+ * same reason getAnvilModelReasoningEffort reads both: a Web-only model is
1789
+ * absent from SUPPORTED_ANVIL_MODELS by design. */
1790
+ function findAnvilModelOption(
1791
+ id: string | null | undefined,
1792
+ ): AnvilModelOption | undefined {
1793
+ return (
1794
+ SUPPORTED_ANVIL_MODELS.find((m) => anvilModelIdMatches(id, m.id)) ??
1795
+ ANVIL_WEB_ALL_MODELS.find((m) => anvilModelIdMatches(id, m.id))
1796
+ )
1797
+ }
1798
+
1799
+ /** The ladder a user may pick from for this model, or null when it offers no
1800
+ * choice — which is every model that has not opted in. */
1801
+ export function getAnvilModelEfforts(
1802
+ id: string | null | undefined,
1803
+ ): readonly ReasoningEffort[] | null {
1804
+ const efforts = findAnvilModelOption(id)?.efforts
1805
+ return efforts && efforts.length > 0 ? efforts : null
1806
+ }
1807
+
1808
+ /** Where this model's ladder starts before a user touches it. */
1809
+ export function getAnvilModelDefaultEffort(
1810
+ id: string | null | undefined,
1811
+ ): ReasoningEffort | null {
1812
+ const entry = findAnvilModelOption(id)
1813
+ if (!entry?.efforts?.length) return null
1814
+ return entry.defaultEffort ?? entry.efforts[entry.efforts.length - 1]!
1815
+ }
1816
+
1817
+ /**
1818
+ * THE authority on what effort a request runs at. Everything a client sends is
1819
+ * a request, never a command.
1820
+ *
1821
+ * Keyed on the model the request will ACTUALLY run — not the one the user
1822
+ * picked. Those differ more often than they look: a limited-tier user's premium
1823
+ * pick is coerced (resolveAnvilSessionModelForAccessTier), the cloud build
1824
+ * path rewrites it, and a Muse Spark turn can be rerouted to Luna mid-flight.
1825
+ * Clamping against the requested model would then let one model's rung reach
1826
+ * another's API — `xhigh` landing on DeepSeek maps to `max`, the most expensive
1827
+ * rung there is, from a user who never asked for it.
1828
+ *
1829
+ * Clamp-DOWN (see clampReasoningEffort): a rung above the ceiling becomes the
1830
+ * ceiling rather than snapping back to the default, so a rerouted user keeps
1831
+ * the closest thing to what they chose. Models with no ladder return null and
1832
+ * keep whatever `reasoningEffort` already says.
1833
+ */
1834
+ export function resolveAnvilReasoningEffort(
1835
+ modelId: string | null | undefined,
1836
+ requested: unknown,
1837
+ ): ReasoningEffort | null {
1838
+ const efforts = getAnvilModelEfforts(modelId)
1839
+ if (!efforts) return null
1840
+ const fallback = getAnvilModelDefaultEffort(modelId)
1841
+ if (!fallback) return null
1842
+ // Medium was briefly offered for Flash before the 07/31 capability matrix was
1843
+ // corrected, and reaches these models from persisted Desktop/Web preferences
1844
+ // and from threads that switched model. DeepSeek maps that compatibility
1845
+ // spelling to high (medium→high in its own effort table), so honor that rather
1846
+ // than letting generic clamp-down turn a stale value into LOW while the UI
1847
+ // displays high.
1848
+ //
1849
+ // Applies to Pro as well as Flash since Pro's 08/13 ladder gained `low`:
1850
+ // before that, "everything on offer is above medium" already landed Pro's
1851
+ // medium on high, and losing that to the clamp would be a silent downgrade of
1852
+ // exactly the model users pick for deliberation.
1853
+ if (
1854
+ requested === 'medium' &&
1855
+ (anvilModelIdMatches(modelId, ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID) ||
1856
+ anvilModelIdMatches(modelId, ANVIL_DEEPSEEK_V4_PRO_MODEL_ID))
1857
+ ) {
1858
+ return 'high'
1859
+ }
1860
+ return clampReasoningEffort(requested, efforts, fallback)
1861
+ }
1862
+
1863
+ export function getAnvilModelReasoningEffort(
1864
+ id: string | null | undefined,
1865
+ ): NonNullable<AnvilModelOption['reasoningEffort']> | null {
1866
+ // BOTH catalogs, and the Web one is not optional. This used to read
1867
+ // SUPPORTED_ANVIL_MODELS alone — the CLI/Desktop catalog — which silently
1868
+ // excluded every Web-only model. Muse Spark is deliberately absent from that
1869
+ // list (its absence IS the Desktop gate), so setting `reasoningEffort` on its
1870
+ // row did nothing at all and gave no hint why: the field was present, the
1871
+ // lookup simply could not see the row. Any future Web-only model would have
1872
+ // hit the same wall.
1873
+ const entry: AnvilModelOption | undefined =
1874
+ SUPPORTED_ANVIL_MODELS.find((m) => anvilModelIdMatches(id, m.id)) ??
1875
+ ANVIL_WEB_ALL_MODELS.find((m) => anvilModelIdMatches(id, m.id))
1876
+ return entry?.reasoningEffort ?? null
1877
+ }
1878
+
1879
+ /**
1880
+ * Whether a Web/Cloud selection may be REMEMBERED as the user's default model.
1881
+ *
1882
+ * GLM 5.2 is excluded. GLM is a scarce, hand-metered pick that a user runs out
1883
+ * of far sooner than the rest of the picker, so pinning it as the remembered
1884
+ * default strands them on a model they cannot start: the next new thread, a
1885
+ * different app, or a plain page reload would open on GLM and fail admission.
1886
+ * Picking GLM applies to the surface in front of you; anything that starts
1887
+ * fresh falls back to DEFAULT_ANVIL_WEB_MODEL_ID.
1888
+ *
1889
+ * Every localStorage read AND write of the remembered model must go through
1890
+ * this (via resolveRememberedAnvilWebModel), so a value saved before this
1891
+ * rule existed self-heals on the next load instead of persisting forever.
1892
+ */
1893
+ export function isAnvilWebRememberableModelId(
1894
+ id: string | null | undefined,
1895
+ ): boolean {
1896
+ return !isAnvilGlmV52ModelId(id)
1897
+ }
1898
+
1899
+ /**
1900
+ * The model a surface should START on, given a remembered (localStorage)
1901
+ * selection: the saved model when it is still valid and rememberable, else
1902
+ * DEFAULT_ANVIL_WEB_MODEL_ID.
1903
+ *
1904
+ * Distinct from resolveAnvilWebModel, which resolves a LIVE selection and
1905
+ * must leave a just-picked GLM alone.
1906
+ */
1907
+ export function resolveRememberedAnvilWebModel(
1908
+ id: string | null | undefined,
1909
+ options: { includeGodOnly?: boolean } = {},
1910
+ ): AnvilWebModelId {
1911
+ const resolved = resolveAnvilWebModel(id, options)
1912
+ return isAnvilWebRememberableModelId(resolved)
1913
+ ? resolved
1914
+ : DEFAULT_ANVIL_WEB_MODEL_ID
1915
+ }
1916
+
1917
+ export function isAnvilMultimodalModelId(
1918
+ id: string | null | undefined,
1919
+ ): boolean {
1920
+ if (!id) return false
1921
+ return ANVIL_MULTIMODAL_MODEL_IDS.some((modelId) => modelId === id)
1922
+ }
1923
+
1924
+ /**
1925
+ * Return whether a model used by a Anvil surface accepts image input.
1926
+ * Unknown models return undefined so the provider backend does not strip
1927
+ * images from newly added or paid models until their capability is known.
1928
+ */
1929
+ export function getAnvilModelImageSupport(
1930
+ id: string | null | undefined,
1931
+ ): boolean | undefined {
1932
+ if (!id) return undefined
1933
+
1934
+ // Keep the retired chat wire id text-only during a staggered deployment.
1935
+ // It now routes to DeepSeek direct, but has no catalog option of its own.
1936
+ if (
1937
+ anvilModelIdMatches(id, ANVIL_DEEPSEEK_V4_FLASH_FIREWORKS_MODEL_ID)
1938
+ ) {
1939
+ return false
1940
+ }
1941
+
1942
+ const model =
1943
+ SUPPORTED_ANVIL_MODELS.find((option) =>
1944
+ anvilModelIdMatches(id, option.id),
1945
+ ) ??
1946
+ ANVIL_WEB_ALL_MODELS.find((option) =>
1947
+ anvilModelIdMatches(id, option.id),
1948
+ )
1949
+ return model?.multimodal
1950
+ }
1951
+
1952
+ export function isAnvilWebMultimodalModelId(
1953
+ id: string | null | undefined,
1954
+ ): boolean {
1955
+ if (!id) return false
1956
+ return ANVIL_WEB_MULTIMODAL_MODEL_IDS.some((modelId) => modelId === id)
1957
+ }
1958
+
1959
+ /** Whether we store our own chat-completion traces for this free-mode model.
1960
+ * See ANVIL_TRACED_MODEL_IDS. */
1961
+ export function isAnvilTracedModelId(
1962
+ id: string | null | undefined,
1963
+ ): boolean {
1964
+ if (!id) return false
1965
+ return ANVIL_TRACED_MODEL_IDS.some((modelId) => modelId === id)
1966
+ }
1967
+
1968
+ export function resolveSupportedAnvilModel(
1969
+ id: string | null | undefined,
1970
+ ): SupportedAnvilModelId {
1971
+ return isSupportedAnvilModelId(id) ? id : FALLBACK_ANVIL_MODEL_ID
1972
+ }
1973
+
1974
+ export function getAnvilModel(id: string): AnvilModelOption {
1975
+ return (
1976
+ SUPPORTED_ANVIL_MODELS.find((m) => m.id === id) ??
1977
+ ANVIL_MODELS.find((m) => m.id === FALLBACK_ANVIL_MODEL_ID)!
1978
+ )
1979
+ }
1980
+
1981
+ export function getAnvilWebModel(id: string): AnvilModelOption {
1982
+ return (
1983
+ ANVIL_WEB_ALL_MODELS.find((m) => m.id === id) ??
1984
+ ANVIL_WEB_ALL_MODELS.find((m) => m.id === FALLBACK_ANVIL_MODEL_ID)!
1985
+ )
1986
+ }
1987
+
1988
+ /** The "a better model exists" notice for `id`, or undefined when the model is
1989
+ * current. Returns nothing when the replacement is not itself selectable on
1990
+ * this surface, so a picker never offers a switch to a model it cannot show. */
1991
+ export function getAnvilModelSupersededBy(
1992
+ id: string | null | undefined,
1993
+ selectableModelIds: readonly string[],
1994
+ ): AnvilModelOption['supersededBy'] | undefined {
1995
+ if (!id) return undefined
1996
+ const catalog: readonly AnvilModelOption[] = [
1997
+ ...SUPPORTED_ANVIL_MODELS,
1998
+ ...ANVIL_WEB_ALL_MODELS,
1999
+ ]
2000
+ const supersededBy = catalog.find(
2001
+ (candidate) => candidate.id === id,
2002
+ )?.supersededBy
2003
+ if (!supersededBy) return undefined
2004
+ return selectableModelIds.includes(supersededBy.modelId)
2005
+ ? supersededBy
2006
+ : undefined
2007
+ }
2008
+
2009
+ /**
2010
+ * The model a saved preference should be steered to, or null to keep it.
2011
+ *
2012
+ * Applied EVERY time a surface reads its remembered pick, so each new
2013
+ * thread/session/launch starts on the replacement. A superseded model stays
2014
+ * fully selectable — picking one mid-thread works and sticks for that thread —
2015
+ * but it never becomes the model a fresh surface opens on again.
2016
+ *
2017
+ * This is deliberately aggressive: a saved preference outranks a changed
2018
+ * default forever otherwise, which is exactly how users kept landing back on
2019
+ * models we no longer recommend. The cost is that a user who wants a
2020
+ * superseded model as their standing default cannot have one; the picker's
2021
+ * per-row notice is what makes that visible rather than mysterious.
2022
+ *
2023
+ * Derived from the catalog's own `supersededBy` pointers, so a model marked
2024
+ * superseded automatically gets BOTH the picker nudge and this steering —
2025
+ * they can never disagree about which models are stale.
2026
+ */
2027
+ export function migrateSupersededAnvilModelPreference(
2028
+ id: string | null | undefined,
2029
+ selectableModelIds: readonly string[],
2030
+ ): string | null {
2031
+ return getAnvilModelSupersededBy(id, selectableModelIds)?.modelId ?? null
2032
+ }
2033
+
2034
+ function getNextAnvilDeploymentStart(now: Date): Date {
2035
+ const easternNow = getZonedParts(now, ANVIL_EASTERN_TIMEZONE)
2036
+ const isBeforeTodayOpen = easternNow.hour < 9
2037
+
2038
+ const offset = isBeforeTodayOpen ? 0 : 1
2039
+
2040
+ return getUtcForZonedTime(
2041
+ addDaysToYmd(easternNow.year, easternNow.month, easternNow.day, offset),
2042
+ ANVIL_EASTERN_TIMEZONE,
2043
+ 9,
2044
+ 0,
2045
+ )
2046
+ }
2047
+
2048
+ function getCurrentAnvilDeploymentEnd(now: Date): Date {
2049
+ const pacificNow = getZonedParts(now, ANVIL_PACIFIC_TIMEZONE)
2050
+ return getUtcForZonedTime(pacificNow, ANVIL_PACIFIC_TIMEZONE, 17, 0)
2051
+ }
2052
+
2053
+ function isSameLocalDay(left: Date, right: Date, timeZone?: string): boolean {
2054
+ const formatter = new Intl.DateTimeFormat('en-CA', {
2055
+ timeZone,
2056
+ year: 'numeric',
2057
+ month: '2-digit',
2058
+ day: '2-digit',
2059
+ })
2060
+ return formatter.format(left) === formatter.format(right)
2061
+ }
2062
+
2063
+ function formatLocalTime(
2064
+ date: Date,
2065
+ referenceNow: Date,
2066
+ options: LocalTimeFormatOptions = {},
2067
+ ): string {
2068
+ const shouldShowWeekday = !isSameLocalDay(
2069
+ date,
2070
+ referenceNow,
2071
+ options.timeZone,
2072
+ )
2073
+ return new Intl.DateTimeFormat(options.locale, {
2074
+ timeZone: options.timeZone,
2075
+ weekday: shouldShowWeekday ? 'short' : undefined,
2076
+ hour: 'numeric',
2077
+ minute: '2-digit',
2078
+ }).format(date)
2079
+ }
2080
+
2081
+ export function getAnvilDeploymentAvailabilityLabel(
2082
+ now: Date = new Date(),
2083
+ options: LocalTimeFormatOptions = {},
2084
+ ): string {
2085
+ if (isAnvilDeploymentHours(now)) {
2086
+ const closesAt = getCurrentAnvilDeploymentEnd(now)
2087
+ return `until ${formatLocalTime(closesAt, now, options)}`
2088
+ }
2089
+
2090
+ const opensAt = getNextAnvilDeploymentStart(now)
2091
+ return `opens ${formatLocalTime(opensAt, now, options)}`
2092
+ }
2093
+
2094
+ export function isAnvilDeploymentHours(now: Date = new Date()): boolean {
2095
+ const eastern = getZonedParts(now, ANVIL_EASTERN_TIMEZONE)
2096
+ const pacific = getZonedParts(now, ANVIL_PACIFIC_TIMEZONE)
2097
+ return (
2098
+ eastern.hour * 60 + eastern.minute >= 9 * 60 &&
2099
+ pacific.hour * 60 + pacific.minute < 17 * 60
2100
+ )
2101
+ }
2102
+
2103
+ export function isAnvilModelAvailable(
2104
+ id: string,
2105
+ now: Date = new Date(),
2106
+ ): boolean {
2107
+ const model = SUPPORTED_ANVIL_MODELS.find((m) => m.id === id)
2108
+ if (!model) return false
2109
+ return model.availability === 'always' || isAnvilDeploymentHours(now)
2110
+ }
2111
+
2112
+ export function isAnvilSessionModelAvailable(
2113
+ id: string,
2114
+ now: Date = new Date(),
2115
+ ): boolean {
2116
+ const model =
2117
+ SUPPORTED_ANVIL_MODELS.find((candidate) => candidate.id === id) ??
2118
+ getAnvilWebModel(id)
2119
+ return model.availability === 'always' || isAnvilDeploymentHours(now)
2120
+ }
2121
+
2122
+ export function resolveAvailableAnvilModel(
2123
+ id: string | null | undefined,
2124
+ now: Date = new Date(),
2125
+ ): AnvilModelId {
2126
+ const resolved = resolveAnvilModel(id)
2127
+ return isAnvilModelAvailable(resolved, now)
2128
+ ? resolved
2129
+ : FALLBACK_ANVIL_MODEL_ID
2130
+ }