@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.
- package/package.json +74 -70
- package/src/agents/bundled-agents.generated.ts +4 -4
- package/src/chat.tsx +1703 -1698
- package/src/cli-args.ts +134 -134
- package/src/components/status-bar.tsx +331 -313
- package/src/hooks/helpers/send-message.ts +663 -654
- package/src/hooks/use-send-message.ts +806 -773
- package/src/index.tsx +473 -473
- package/src/state/chat-store.ts +553 -544
- package/src/utils/create-run-config.ts +121 -121
- package/src/utils/format-compact-number.ts +37 -0
- package/vendor/@anvil/common/package.json +27 -0
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
- package/vendor/@anvil/common/src/actions.ts +215 -0
- package/vendor/@anvil/common/src/analytics-core.ts +69 -0
- package/vendor/@anvil/common/src/analytics.ts +93 -0
- package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
- package/vendor/@anvil/common/src/browser-actions.ts +413 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
- package/vendor/@anvil/common/src/constants/agents.ts +99 -0
- package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
- package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
- package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
- package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
- package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
- package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
- package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
- package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
- package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
- package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
- package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
- package/vendor/@anvil/common/src/constants/auth.ts +18 -0
- package/vendor/@anvil/common/src/constants/byok.ts +2 -0
- package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
- package/vendor/@anvil/common/src/constants/composio.ts +34 -0
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
- package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
- package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
- package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
- package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
- package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
- package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
- package/vendor/@anvil/common/src/constants/images.ts +51 -0
- package/vendor/@anvil/common/src/constants/index.ts +7 -0
- package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
- package/vendor/@anvil/common/src/constants/limits.ts +23 -0
- package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
- package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
- package/vendor/@anvil/common/src/constants/paths.ts +69 -0
- package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
- package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
- package/vendor/@anvil/common/src/constants/skills.ts +60 -0
- package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
- package/vendor/@anvil/common/src/constants/ui.ts +25 -0
- package/vendor/@anvil/common/src/env-ci.ts +36 -0
- package/vendor/@anvil/common/src/env-process.ts +95 -0
- package/vendor/@anvil/common/src/env-schema.ts +95 -0
- package/vendor/@anvil/common/src/env.ts +24 -0
- package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
- package/vendor/@anvil/common/src/mcp/client.ts +233 -0
- package/vendor/@anvil/common/src/old-constants.ts +10 -0
- package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
- package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
- package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
- package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
- package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
- package/vendor/@anvil/common/src/testing/errors.ts +33 -0
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
- package/vendor/@anvil/common/src/testing/index.ts +84 -0
- package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
- package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
- package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
- package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
- package/vendor/@anvil/common/src/testing/setup.ts +282 -0
- package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
- package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
- package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
- package/vendor/@anvil/common/src/tools/constants.ts +117 -0
- package/vendor/@anvil/common/src/tools/list.ts +190 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
- package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
- package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
- package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
- package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
- package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
- package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
- package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
- package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
- package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
- package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
- package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
- package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
- package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
- package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
- package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
- package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
- package/vendor/@anvil/common/src/tools/utils.ts +24 -0
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
- package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
- package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
- package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
- package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
- package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
- package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
- package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
- package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
- package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
- package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
- package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
- package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
- package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
- package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
- package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
- package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
- package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
- package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
- package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
- package/vendor/@anvil/common/src/types/function-params.ts +33 -0
- package/vendor/@anvil/common/src/types/grant.ts +20 -0
- package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
- package/vendor/@anvil/common/src/types/json.ts +29 -0
- package/vendor/@anvil/common/src/types/mcp.ts +24 -0
- package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
- package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
- package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
- package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
- package/vendor/@anvil/common/src/types/organization.ts +118 -0
- package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
- package/vendor/@anvil/common/src/types/publisher.ts +67 -0
- package/vendor/@anvil/common/src/types/session-state.ts +149 -0
- package/vendor/@anvil/common/src/types/skill.ts +86 -0
- package/vendor/@anvil/common/src/types/source.ts +11 -0
- package/vendor/@anvil/common/src/types/spawn.ts +13 -0
- package/vendor/@anvil/common/src/types/subscription.ts +67 -0
- package/vendor/@anvil/common/src/types/usage.ts +16 -0
- package/vendor/@anvil/common/src/types/util.ts +3 -0
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
- package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
- package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
- package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
- package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
- package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
- package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
- package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
- package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
- package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
- package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
- package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
- package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
- package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
- package/vendor/@anvil/common/src/util/array.ts +31 -0
- package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
- package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
- package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
- package/vendor/@anvil/common/src/util/credentials.ts +26 -0
- package/vendor/@anvil/common/src/util/currency.ts +25 -0
- package/vendor/@anvil/common/src/util/dates.ts +81 -0
- package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
- package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
- package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
- package/vendor/@anvil/common/src/util/error.ts +562 -0
- package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
- package/vendor/@anvil/common/src/util/file.ts +339 -0
- package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
- package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
- package/vendor/@anvil/common/src/util/log-data.ts +57 -0
- package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
- package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
- package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
- package/vendor/@anvil/common/src/util/messages.ts +685 -0
- package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
- package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
- package/vendor/@anvil/common/src/util/object.ts +128 -0
- package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
- package/vendor/@anvil/common/src/util/path.ts +15 -0
- package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
- package/vendor/@anvil/common/src/util/promise.ts +73 -0
- package/vendor/@anvil/common/src/util/random.ts +15 -0
- package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
- package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
- package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
- package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
- package/vendor/@anvil/common/src/util/saxy.ts +741 -0
- package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
- package/vendor/@anvil/common/src/util/skills.ts +36 -0
- package/vendor/@anvil/common/src/util/split-data.ts +259 -0
- package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
- package/vendor/@anvil/common/src/util/string.ts +426 -0
- package/vendor/@anvil/common/src/util/system-info.ts +53 -0
- package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
- package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
- package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
- package/vendor/@anvil/common/src/util/xml.ts +17 -0
- package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
- package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
- package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
- package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
- package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
- package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
- package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
- package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
- package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
- 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
|
+
}
|