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