Skip to content

chore(models): refresh stale model defaults (NodeRoom) - #261

Draft
HomenShum wants to merge 11 commits into
mainfrom
claude/model-refresh-noderoom
Draft

HomenShum wants to merge 11 commits into
mainfrom
claude/model-refresh-noderoom

Conversation

@HomenShum

@HomenShum HomenShum commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What

Refreshes NodeRoom's stale default model ids (Google deprecated gemini-3.5-flash 2026-10-08; OpenRouter drops google/gemini-2.5-* 2026-10-20; claude-sonnet-4 retired 2026-06-15; gemini-3.1-flash-tts-preview shuts down 2026-11-17) and fixes the request shapes the new ids reject. Env chains and caller arguments are unchanged: if prod sets AGENT_MODEL, AGENT_ORCHESTRATOR_MODEL, AGENT_TOP_PAID_MODEL, VOICE_GEMINI_TTS_MODEL etc., the default ids prod resolves do not change - this PR moves code fallbacks only. Qualifier: an env or fallback value the catalog does not know now fails closed (SPEC A.0.10): unknown_model_policy at convex/agentJobs.ts:2100-2107 (job start), UnknownModelError in convexModel() and model(). A prod env var that names an id outside the catalog (and is not a free-looking id) stops working instead of being sent to a provider as written; check the values of the env names in the Env checklist before merge. Operator overrides in smoke/eval scripts stay pass-through (D1).

This PR is a draft and carries the needs-live-eval label. Both stay until the two merge blockers below are closed. The label is a block, not a note (SPEC A.0.3). Nothing in this PR made a paid provider call.

MERGE BLOCKERS (both need a provider key or a human at a speaker; none was run here)

  1. L1-L4 ladder receipt gates the gemini-3.8-flash default (and the adaptiveRouter.ts:136 fast-lane literal). tests/qaMatrix.test.ts ("keeps the Convex interactive fallback on a ladder-safe route") fails on purpose until it exists: gemini-3.8-flash has no PASS row in docs/qa/production-matrix.json. The check is not weakened.

    LADDER_MAX_COST_USD=9 npm run ladder:real -- gemini-3.6-flash,gemini-3.8-flash --levels=1-4 --record --json-out docs/eval/ladder/model-refresh-noderoom.json
    

    Pre-flight (re-run on this head with fetch replaced by a function that throws; 0 fetch calls): worst case $8.0552 for 2 routes x 4 rungs, request ceiling 144, so the cap must be at least that or the run refuses to start. Actual spend is expected to be a small fraction of it. Commit the receipt in a receipt-only commit on top of the tested commit (A.0.3 form a) with a clean tree.

    The --json-out report now carries everything A.0.3 reviews: command, candidateIds, echoedModels, rows[] (one per provider request: requestedModel, resolvedModel, echoedModel, usage, costUsd, fallbackUsed, rung), environment, testedCommit (40-hex), gitDirty, constantSha256, budget (maxUsd, estimateUsd, spentUsd, requests, requestCeiling, stoppedBy) and dispatchPolicy. Reject the receipt unless: requestedRoutes == candidateIds == ["gemini-3.6-flash","gemini-3.8-flash"]; rungs are exactly L1-L4 with 8 passing result rows; every row has resolvedModel == requestedModel, fallbackUsed == false and an echoedModel equal to the requested id (a gemini-3.8-flash request answered as another model is the failure this field exists to expose; 3.5 is already served as 3.6); budget.stoppedBy == null; budget.requests <= budget.requestCeiling; gitDirty == false; dispatchPolicy == {maxRetries:0, crossModelFallback:false}. echoedModel is read from the raw response body (modelVersion / model): the AI SDK's response.modelId is the requested id for Google, so it could never show a re-route.

  2. Manual listen check of gemini-3.8-flash-tts (convex/http.ts:36, SPEC A.0.7: a frozen-class id with an announced shutdown inside 60 days moves to its vendor-named replacement, and a listen test is the gate). Shape-verified here by a fake-transport test through POST /voice/synthesize; the audio itself is unverified. To close it: on a deployment with GOOGLE_GENERATIVE_AI_API_KEY and VOICE_TTS_PROVIDER=gemini, POST one short line to /voice/synthesize, confirm X-Voice-TTS-Model: gemini-3.8-flash-tts and a playable WAV in the Kore voice, and record the result on this PR. The old gemini-3.1-flash-tts-preview shuts down 2026-11-17.

No longer a blocker (round 5, SPEC-v9 D2, Homen 2026-10-16): the top_paid Environment smoke. No working OpenRouter key exists, so an OpenRouter-routed role that this refresh moves goes to the newest :free row of the public list https://openrouter.ai/api/v1/models that has the capabilities the role needs. top_paid (convex/agentJobs.ts:2118, DEFAULT_TOP_PAID_MODEL in convex/modelDefaults.ts) is now nvidia/nemotron-3.5-super-vl:free (created 2026-10-15, 1,000,000 context, tools, tool_choice, structured_outputs, text output, no expiration_date), from the list fetched 2026-10-16T11:32:04Z (docs/eval/openrouter-free-models-2026-10-16.json). Marked: live-unverified (no key, Homen 2026-10-16). Merge is allowed with that marking. Per-role D2 scope is in the Review round 5 table below.

Also not run: prod:gate (network, audit, design and proof gates). CI's verify job fails at the npm audit step on root undici 8.10.0, the same version as origin/main, so that failure is not from this PR (PR #256 bumps it); failing checks are not required for merge, which is why the block is the draft state plus the label.

Runtime sites (44; migrate 26, freeze 16, defer 2 per spec)

Site Today Disposition This PR
convex/agent.ts:346 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/agent.ts:345 minimax/minimax-m3 freeze research lane champion by recorded evidence (docs/eval/results.json; adaptiveRouter requireN5Promotion); active, no expiry -> Part B v2
convex/agent.ts:783 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/agent.ts:65 z-ai/glm-4.7-flash freeze listed, no OpenRouter expiration_date (sibling z-ai/glm-4.7 expires 2026-12-31 -> watch)
convex/agentJobs.ts:2103 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/agentJobs.ts:2102 minimax/minimax-m3 freeze research lane champion by recorded evidence (docs/eval/results.json; adaptiveRouter requireN5Promotion); active, no expiry -> Part B v2
convex/agentJobs.ts:2118 anthropic/claude-sonnet-4 migrate -> nvidia/nemotron-3.5-super-vl:free (DEFAULT_TOP_PAID_MODEL) - claude-sonnet-4-20250514 retired 2026-06-15 (rule 1). SPEC-v9 D2: no OpenRouter key, so the target is the newest :free row with tools, tool_choice, structured_outputs, >= 256K context, text output and no expiration_date in the public list fetched 2026-10-16T11:32:04Z. live-unverified (no key, Homen 2026-10-16). Env chain AGENT_TOP_PAID_MODEL ?? AGENT_MODEL ?? const unchanged, so a deployment that sets either does not change. A free route takes no uploaded file: a top_paid job in a room with a file is refused at the free-file-egress gate (or promoted to z-ai/glm-4.7-flash under the existing FREE_AUTO_ALLOW_FILE_EGRESS_PROMOTION opt-in) unless an env id is set. The anthropic/claude-sonnet-5.5 tool_choice: auto plus strict: true handling stays in convexModel for an operator who sets that id.
convex/agentJobRunner.ts:512 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/agentJobRunner.ts:514 openrouter/free-auto freeze free-path sentinel, not a model
convex/http.ts:175 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/streaming.ts:68 gemini-3.5-flash migrate -> gemini-3.8-flash - Google deprecated 3.5-flash 2026-10-08; one DEFAULT_AGENT_MODEL constant (convex/modelDefaults.ts) replaces the 7 literals; env chain unchanged; the ladder receipt is merge blocker 1 (see MERGE BLOCKERS)
convex/http.ts:35 gemini-3.1-flash-tts-preview migrate -> gemini-3.8-flash-tts (constant now at convex/http.ts:36) - Google shutdown 2026-11-17, vendor-named replacement, frozen-class voice exception (A.0.7). VOICE_GEMINI_TTS_MODEL precedence unchanged. The manual listen check is merge blocker 2. A fake-transport test drives POST /voice/synthesize and asserts the wire id.
convex/http.ts:283 gpt-4o-mini-transcribe freeze voice; retires 2027-02-26 -> gpt-transcribe (advisory, >90 d)
convex/http.ts:352 gpt-4o-mini-tts freeze voice; dated snapshots retire 2027-01-06; alias target unknown (advisory)
convex/okfEmbeddingProvider.ts:43 gemini-embedding-2 freeze embedding dimension contract; change = re-embed
src/landing/modelRegistry.ts:37 z-ai/glm-5.2 freeze listed on OpenRouter / active at the vendor 2026-10-10, no expiry, no vendor-named successor -> Part B v2
src/landing/modelRegistry.ts:48 anthropic/claude-fable-5 defer anthropic/claude-fable-5 active (floor 2027-06-09); fable-5.1 rejects forced tool_choice -> no bump until the convexModel.ts:842/892 fix is observed. D2 does not apply (D2a: the id is not retired, deprecated, expiring or unlisted, so this refresh does not move it); stays defer.
src/ui/Chat.tsx:1518 moonshotai/kimi-k3 freeze listed on OpenRouter / active at the vendor 2026-10-10, no expiry, no vendor-named successor -> Part B v2
src/ui/Chat.tsx:1690 gemini-3.5-flash migrate -> DEFAULT_AGENT_MODEL - import the constant
src/nodeagent/models/modelCatalog.ts:305 moonshotai/kimi-k3 freeze listed on OpenRouter / active at the vendor 2026-10-10, no expiry, no vendor-named successor -> Part B v2
src/nodeagent/models/modelCatalog.ts:232 gpt-5.4 freeze listed on OpenRouter / active at the vendor 2026-10-10, no expiry, no vendor-named successor -> Part B v2
src/nodeagent/models/modelCatalog.ts:272 claude-haiku-4.5 migrate -> +claude-haiku-5-5, +claude-sonnet-5-5, +claude-opus-5-5 (first entries) - add-only; old ids stay valid for persisted rooms; pricing rows (:63-216) for the new ids in the same PR
src/nodeagent/models/modelCatalog.ts:288 gemini-3-flash-preview migrate -> ["gemini-3.8-flash","gemini-3.5-flash-lite"] - removes gemini-3.1-flash-lite-preview (shut down 2026-05-25) and gemini-2.5-flash; gemini-3-flash-preview replaced by the GA Flash
src/nodeagent/models/modelCatalog.ts:342 gpt-5.4-mini freeze listed on OpenRouter / active at the vendor 2026-10-10, no expiry, no vendor-named successor -> Part B v2
src/nodeagent/models/modelCatalog.ts:187 claude-haiku-4.5 migrate -> +claude-haiku-5-5, +gemini-3.5-flash-lite - entitlement table (anonymous/free allowedModels): add the new ids, keep the old strings; exact-string match
src/nodeagent/models/modelCatalog.ts:612 claude-sonnet-4.6 migrate -> explicit alias table from SPEC 1.2 replaces the dotted-id regex (deleted). claude-opus-4.7 now maps to claude-opus-4-7 (it was silently rewritten to claude-opus-4-8); the opus and claude-opus shortcuts are pinned to claude-opus-4-8, which is what they effectively resolved to before, so their behaviour is unchanged; claude-*-5.5 dotted ids map to claude-*-5-5; every other dotted 4.x id that can sit in a persisted row is listed.
src/nodeagent/models/modelCatalog.ts:641 z-ai/glm-5.2 migrate -> 'auto' -> DEFAULT_AGENT_MODEL - 'openrouter' -> z-ai/glm-5.2 stays (active)
src/nodeagent/models/modelCatalog.ts:806 z-ai/glm-5.2 migrate -> fallback chains pruned of retired or dead members (claude-opus-4/4.1, claude-sonnet-4, claude-haiku-3.5, gemini-2.0-flash, gemini-2.5-*, gemini-3.1-flash-lite-preview) and extended with chains for claude-haiku-5-5, claude-sonnet-5-5 and claude-opus-5-5 only (round 9: gemini-3.8-flash, the default, gemini-3.6-flash and gemini-3.5-flash-lite deliberately have no chain, as on main, because a chain would send room content to OpenRouter/OpenAI on a failed default turn; failover on the default route is Homen's separate decision); retired ids stay as keys so a persisted room has an escape route; AGENT_FALLBACK_MODELS precedence unchanged.
src/nodeagent/models/adapter.ts:132 claude-haiku-4-5 migrate -> claude-haiku-5-5 - legacy Haiku -> current Haiku (migration guide). Same PR: @ai-sdk/anthropic 3.0.120, explicit effort (haiku medium, sonnet high, opus medium), sdkToolChoiceForModel downgrades required to auto (with strict: true tools) for forcedToolChoice-forbidden ids, and the ladder meter's reserveDispatch + maxOutputTokens sit inside the three withRetry closures with the dispatch-policy option (A.2.9 Ladder budget).
src/nodeagent/models/adapter.ts:152 gemini-3.5-flash migrate -> DEFAULT_AGENT_MODEL - same constant (gemini-3.8-flash); NODEAGENT_FREE_AUTO_GOOGLE_MODEL precedence unchanged
src/nodeagent/models/adapter.ts:216 (env-provided) freeze env-provided override, no literal
src/nodeagent/models/piAiAdapter.ts:9 gemini-2.5-flash migrate -> gemini-3.8-flash - gemini-2.5 is prior-users-only since 2026-09-18 and expires on OpenRouter 2026-10-20; the spec asks the request-shape test to assert the installed pi-ai catalog resolves the id, but no pi-ai package is installed (the adapter wraps model()), so the test does NOT do a pi-ai catalog lookup; it drives createPiAiAdapter().next() through a fake transport and asserts the wire id gemini-3.8-flash in the request URL, the absence of sampling keys, and the provider and pricing of the id (tests/modelRefreshRequestShape.test.ts).
src/nodeagent/capture/reasoning.ts:23 claude-haiku-4-5 migrate -> claude-haiku-5-5 - twin of NodeRL reasoning.ts:23; CAPTURE_REASONING_MODEL precedence unchanged; now sends the explicit SPEC 1.3 effort (claudeEffortProviderOptions).
src/nodeagent/models/providerParserLive.ts:110 gemini-3.5-flash migrate -> ["gemini-3.8-flash","gemini-3.6-flash"] - drops gemini-2.5-flash (OR expiry 2026-10-20) and gemini-2.0-flash (shut down 2026-06-01); gemini-3-flash-preview has the vendor-named replacement gemini-3.6-flash
src/nodeagent/models/providerParserLive.ts:111 gpt-5.5 migrate -> ["gpt-5.5","gpt-5.4-mini"] - drops gpt-5-mini (gpt-5-mini-2025-08-07 retires 2026-12-11) and gpt-4.1-mini (gpt-4.1-mini-2025-04-14 shuts down 2027-01-14, OpenAI, announced 2026-10-14); both remaining candidates are active, so no successor is appended; the caller's preferred id and PROVIDER_PARSER_OPENAI_MODEL still come first (providerParserModelCandidates, :116-126)
src/nodeagent/models/providerParserLive.ts:112 claude-haiku-4-5 migrate -> ["claude-haiku-5-5","claude-sonnet-5-5"] - claude-3-5-haiku-latest retired 2026-02-19; claude-sonnet-4-5-20250929 deprecated, retires 2026-11-30; the live parser sends the explicit SPEC 1.3 effort for the direct Anthropic transport.
src/nodeagent/models/providerParserLive.ts:113 z-ai/glm-5.2 migrate -> (remove nex-agi/nex-n2-pro) - absent from the OpenRouter catalog. D2 does not apply (D2a): the change only drops an unlisted candidate and chooses no new target; z-ai/glm-5.2, moonshotai/kimi-k2.7-code and the other remaining candidates are frozen paid routes with no vendor-named successor, and no :free replacement is added.
src/nodeagent/core/adaptiveRouter.ts:82 minimax/minimax-m3 migrate -> minimax stays; :84/:136 -> DEFAULT_AGENT_MODEL - minimax/minimax-m3 active (freeze); the two gemini-3.5-flash literals import the constant; the :136 fast-lane literal moves only after the ladder receipt exists Done for :84 (constant); the :136 fast-lane literal deliberately stays gemini-3.5-flash until the ladder receipt exists (spec: moves only after the receipt) - defer, condition = receipt file present.
src/lib/models/modelRouter.ts:47 nebius/zai-org/GLM-5.2 freeze DEFAULT_MODELS table has no importer (inventory); dead table, leave or delete
src/nodeagent/models/openRouterFreeModels.ts:59 cohere/north-mini-code:free freeze preference list for the live free discovery; cohere/north-mini-code:free listed, no expiry
backend/app/config.py:78 nvidia/nemotron-3-nano-omni defer Python backend on GMI MaaS (config.py:78-80, llm.py:37): nvidia/nemotron-3-nano-omni, nvidia/nemotron-3-ultra, google/gemini-3-flash-preview; GMI catalog not inventoried; verify separately with GET {GMI_MAAS_BASE_URL}/models + the backend's pytest; not covered by npm gates
btb_noderoom_agent/harbor_adapter.py:83 z-ai/glm-5.2 freeze Python BTB adapter; z-ai/glm-5.2 active; benchmark label continuity
scripts/gemini-demo-media-judge.ts:21 gemini-3.5-flash migrate -> gemini-3.6-flash - judge script: pin what Google already serves (3.5 auto-routed to 3.6 since 2026-10-08); remove temperature (deprecated 3.6+); 3.8 is a later human choice
scripts/design-qa/rubric-panel.ts:31 gemini-3.5-flash migrate -> gemini-3.6-flash - judge script: pin what Google already serves (3.5 auto-routed to 3.6 since 2026-10-08); remove temperature (deprecated 3.6+); 3.8 is a later human choice

Executable (E) and runtime-adjacent (R) sites (20; migrate 7, freeze 12, defer 1)

Site Class Today Disposition This PR
src/nodeagent/models/modelCatalog.ts:63 R claude-opus-4.7 migrate -> pricing rows added: claude-haiku-5-5 and anthropic/claude-haiku-5.5 ($0.10/$0.50, plus the >100K-prompt tier $0.50/$2.50 as longContext, applied by every cost and reservation path), claude-sonnet-5-5 ($2/$10), claude-opus-5-5 ($4/$20), gemini-3.8-flash ($0.75/$3.75; $1.50/$7.50 from 2027-01-01), gemini-3.6-flash ($0.75/$3.75), gemini-3.5-flash-lite ($0.30/$2.50); the claude-opus-4-7 row ($5/$25) the alias table now needs carries pricingAsOf; retired 4.x rows kept for persisted rooms; all rows are in the agreement test (A.0.6).
src/nodeagent/core/creditModel.ts:57 R z-ai/glm-5.2 freeze credit-pricing dominantModel z-ai/glm-5.2 (also :139,:152,:165): listed, no expiry; a change is a pricing decision, not a lifecycle one
src/nodeagent/models/convexModel.ts:968 E (any claude-*) migrate -> request shape: anthropicStep already sent no temperature/thinking/tool_choice/prefill; added the 2048 max_tokens floor for 5.5 ids (thinking counts), reading blocks by type (test), and strict: true tool definitions where a forced choice is downgraded to auto; tool_choice forwarding at :842/:892 sends auto (plus strict tools) for forcedToolChoice-forbidden ids.
scripts/benchmark/modelEvalConfig.ts:126 R deepseek/deepseek-v4-flash freeze SUPPORTED_MODEL_ROUTES promotion tags are the requireN5Promotion evidence (A.0.2); never edited by a refresh; new ids earn tags through the ladder
scripts/benchmark/run.ts:63 E gemini-3.1-flash-lite freeze benchmark judge gemini-3.1-flash-lite: frozen judge (B.9); Google deprecated it with shutdown 2027-05-07 (>60 d) -> advisory; replacement gemini-3.5-flash-lite only through the benchmark's own re-baseline
scripts/benchmark/probe.ts:13 E claude-opus-4-8 migrate -> +current ids - liveness probe list: add claude-haiku-5-5/claude-sonnet-5-5/claude-opus-5-5, gemini-3.8-flash, gemini-3.5-flash-lite, gpt-6.1-sol; drop ids retired before 2026-10-10 (they can only report 404); keep gemini-3.5-flash to observe the auto-route; claude-opus-4-8 stays (active)
evals/ladder.ts:670 E gpt-5.4-nano defer default --real list ['gpt-5.4-nano','gemini-3.1-flash-lite'] applies only when no ids are passed; the PR passes both ids as ONE comma-separated argument (parseRealModels reads only argv[idx+1], :666-671); gemini-3.1-flash-lite retires 2027-05-07 -> refresh with the next re-baseline (human)
evals/professionalLiveCatalog.ts:51 E deepseek/deepseek-v4-flash freeze deepseek/deepseek-v4-flash is listed on OpenRouter 2026-10-10 with no expiry (DeepSeek's first-party deepseek-v4-flash retirement is a different transport - per-transport fact, B.1); research_champion by evidence; also financeModelLive.ts:983, halo-live-path-calibration.ts:22, package.json:241,245
scripts/finch-official-judge.py:20 E gpt-5-mini freeze official FinCH judge gpt-5-mini (snapshot 2025-08-07): frozen (B.9). Announced retirement 2026-12-11 is <= 60 d from 2026-10-12 -> URGENT decision (section C.2): re-promote through scripts/proofloop-promote-official-score.ts, or mark the receipts historical (retiredJudge). Never an automatic edit
scripts/finauditing-official-judge.py:26 E gpt-5-mini freeze same official-judge decision as finch-official-judge.py:20; its gpt-4o*/gpt-4.1* branch sends temperature=0.0/max_tokens=4 (unchanged, those ids are not Claude/GPT-6)
scripts/mbabench-official-sweep.py:26 E google/gemini-3-flash-preview freeze official judge google/gemini-3-flash-preview: listed on OpenRouter; natively deprecated with no shutdown date -> advisory; frozen (B.9)
scripts/judge-demo-gif.ts:24 E gemini-3.5-flash migrate -> gemini-3.6-flash + GEMINI_JUDGE_MODEL env - pin what Google already serves under 3.5 (auto-routed since 2026-10-08); drop temperature:0 at :129 (deprecated on 3.6+); same for scripts/judge-state-captures.ts:17/:85
scripts/walkthroughs/judge-video.ts:53 E gemini-3.5-flash migrate -> gemini-3.6-flash - drop temperature 0.2 at :62; the same rule for gemini-ui-review.ts:11, gemini-visual-polish.ts:14/:55, gemini-mp4-workflow-review.ts:15/:82, eval/painJudge.ts:18, spreadsheetbench-chart-visual-grade.ts:13/:96, src/eval/spreadsheetBenchChartVisualProbe.ts:71, skills/seo-qa/scripts/judge-video-gemini.ts:12/:66, packages/walkthrough-review-cli/src/core.ts:78, walkthrough-review.config.json:3; env chains unchanged; 3.8 is a later human choice
scripts/eval/providerParseSmoke.ts:21 E claude-haiku-4-5 migrate -> gemini-3.8-flash,gpt-5.4-mini,claude-haiku-5-5 - mirrors the providerParserLive lists
scripts/startup-diligence-provider-eval.ts:150 E gemini-3.5-flash migrate -> gemini-3.8-flash; drop gemini-2.5-flash; claude-haiku-5-5 - gemini-2.5-flash expires on OpenRouter 2026-10-20; gpt-5.4-nano, glm-4.7-flash, kimi-k2.6 stay
scripts/bankertoolbench-nodeagent-full-sweep.ps1:7 E z-ai/glm-5.2 freeze z-ai/glm-5.2 listed, no expiry; benchmark label continuity (also src/eval/bankerToolBenchNodeAgentGeneral.ts:205, proofloop/live-browser-proof.spec.ts:65, scripts/sfn-nonbtb-nodeagent-runner.ts)
scripts/bankertoolbench-nodeagent-clean-sequential.mjs:8 E gpt-4.1-mini freeze benchmark identity, dated — the default names the recorded probe (prefix btb-clean-capability-full100-parallel-v3-gpt41mini; .proofloop/lanes/bankertoolbench/* is hash-bound evidence); OpenAI shuts native gpt-4.1-mini-2025-04-14 down 2027-01-14 (announced 2026-10-14), after which a default run fails at the provider; a successor probe (gpt-6-luna, vendor-named) is a new lane run under its own score/cost receipt (§C.7); also bankertoolbench-nodeagent-clean-parallel.ps1:7, smb-lending-four-mode-benchmark.ts:17 (its OpenRouter branch, taken when OPENROUTER_API_KEY is set, sends openai/gpt-4.1-mini, undated on OpenRouter), src/app/bankerToolBenchRoomSeed.ts:26 (recorded evidence label, never rewritten)
scripts/inference-nodetrace/run.mjs:13 E gpt-5.4 freeze gpt-5.4 active; INFERENCE_MODEL precedence unchanged
scripts/walkthroughs/voiceover.ts:60 E gpt-4o-mini-tts freeze voice gpt-4o-mini-tts; dated snapshots retire 2027-01-06 (advisory)
package.json:82 E gpt-5-mini freeze benchmark:finch:canonical-judge --judge-model gpt-5-mini is the official judge (see finch-official-judge.py:20); shadow judge openrouter/free unchanged

Class N (24) dispositions - docs loop, test assertions, examples, fixtures

doc (5)

  • src/nodeagent/models/modelCatalog.ts:1 header comment ("Primary NodeBench default: z-ai/glm-5.2"): freeze - glm-5.2 is untouched by this PR.
  • README.md:1604 (npx convex env set AGENT_MODEL gemini-3.5-flash): defer - an operator command that names the ladder-recorded route. It moves to gemini-3.8-flash in the same commit as the ladder receipt (merge blocker 1), exactly like the adaptiveRouter.ts:136 literal; moving it first would recommend a route with no PASS row.
  • production-agent.json:108 (modelMigration.pinnedModel z-ai/glm-5.2): freeze - pins the glm-5.2 default, unchanged.
  • docs/eval/FIRST_USER_E2E.md:209 (z-ai/glm-5.2 env example): freeze - recorded run instructions, glm-5.2 unchanged.
  • docs/ historical mentions (841 docs, 20 evidence, 30 .proofloop, 8 episodes files): freeze - hash-bound evidence and archived prototypes are never rewritten.

eval-fixture (1)

  • public/eval/proofloop-prod-proxy-benchmark-matrix.json: freeze - generated benchmark artifact derived from src/eval/proofloopProdProxyBenchmarkMatrix.ts; glm-5.2 unchanged.

example (7)

  • .env.example:65 NODEAGENT_FREE_AUTO_GOOGLE_MODEL: migrate -> gemini-3.8-flash (the code default).
  • .env.example:116 GEMINI_SEO_VIDEO_JUDGE_MODEL: migrate -> gemini-3.6-flash (the judge script default).
  • .env.example:165 PROVIDER_PARSER_GEMINI_MODEL: migrate -> gemini-3.8-flash,gemini-3.6-flash; the neighbouring :167 PROVIDER_PARSER_ANTHROPIC_MODEL -> claude-haiku-5-5,claude-sonnet-5-5 and :168 PROVIDER_PARSER_OPENROUTER_MODEL without the unlisted nex-agi/nex-n2-pro are part of the same copy (the inventory records them under :165). :84 VOICE_GEMINI_TTS_MODEL -> gemini-3.8-flash-tts (the shutdown-deadline fix). Env vars beat code defaults, so a .env built from the old file would have silently restored every one of these ids.
  • .env.example:66 AGENT_MODEL=gpt-5.4-nano, :103 AGENT_FILE_EGRESS_MODEL, :147 FREE_AUTO_JOB_MODEL: freeze - values this PR does not change.
  • src/nodeagent/core/subagentDispatcher.ts:32 ('minimax/minimax-m3' in a zod .describe): freeze - research lane champion.
  • A test (tests/modelRefreshServerRoles.test.ts, last describe) now fails if any of these .env.example values drifts from what the code defaults to.

test-assertion (11)

  • Updated with the retirement or price cited in the diff: modelAliasClaude.test.ts:11 (claude-opus-4.7 -> claude-opus-4-7), phaseModel.test.ts:73 (the top_paid fallback fixture is now anthropic/claude-sonnet-5.5; claude-sonnet-4 retired 2026-06-15), agentJobsRuntime.test.ts (new: default model per route policy, asserted as literals), openRouterFreeModels.test.ts (auto follows the constant), openAiTokenLimit.test.ts (gpt-6 case), providerEgressPolicy.test.ts (re-run, passes unchanged), and in this round providerParserLive.test.ts (env id is the non-default gemini-3.5-flash-lite, expecting [gemini-3.8-flash, gemini-3.5-flash-lite, gemini-3.6-flash], so env precedence is proven; a second case with a default env id proves de-duplication). providerParseSmoke.ts also compared the provider to "google" while getProviderForModel returns "gemini", so the Gemini smoke never reached Google; fixed (pre-existing on main).
  • Retargeted in round 3 (see Review round 3): convexModelFreeAutoMode.test.ts (two tests moved from vendor/unknown-paid-model to the alias-declared claude-sonnet-4.5, because the unknown id now throws).
  • Unchanged because the ids are not touched here: kimiK3Default.test.ts:22, chatModelPickerPolish.test.tsx:168 (claude-sonnet-4-6 is still a live catalog row), openRouterConvexBenchmark.test.ts, finchOfficialJudgeCostGuard.test.ts (frozen official judge), modelEvalMatrix.test.ts (promotion tags).

Hash-bound evidence files untouched.

Review round 3 (Codex round 2 defects and the two round-1 findings that stayed open)

Finding Disposition
R1.1 / R2.1 Free request becomes a paid request (SPEC A.0.10) Fixed for every path except one the spec sanctions (see discrepancy 1). adapter.fallbackModelFor and convexModel drop a paid fallback for a free primary (isFreeModelId) unless the caller sets allowPaidFallback: true and a reservation exists: adapter = a SpendMeter (meteredDispatch reserves the paid model's worst case before the request), Convex = a finite maxCostUsd on next() (the failover controller estimates each candidate against it and skips what does not fit). Tests: a concrete :free model that is down under the default policy, allowPaidFallback without a meter, a meter without the flag, both together, a paid fallback the meter cannot afford, fallbackModelIds, the deployment AGENT_FALLBACK_MODELS list, and paid -> paid / paid -> free unchanged (tests/modelCostClass.test.ts). No production caller sets allowPaidFallback: default is deny.
R1.2 / R2.2 Unknown ids dispatch Round 3 claim "fixed at the resolver and both boundaries" was incomplete; corrected in round 5 (the private stream path and the public capture action still dispatched unknown ids; see Review round 5, defects 3 and 4). Resolver and public job boundary: resolveModelAlias throws UnknownModelError for an id that is not priced, not declared by the alias table, not declared by the repo (round 4: the picker, the benchmark routes, the frozen defaults and judges) and not free-looking; an unknown -free / :free id resolves to openrouter/free-auto. model(), convexModel(), judge(), the adaptive router and the fallback lists therefore refuse gemini-9-flash-unknown, acme/paid-unknown-model, claude-opus-4.9 before any request (fake transport: 0 fetch calls), metered or not. Readers that must tolerate historical ids (priceRun, reserveDispatch, creditModel, proofloopModelTracking, the egress policy, benchmark/run.ts, the interop normalizer, the LangChain strict check) call the new lookupModelAlias, which is the old behaviour.
R2.3 failed rung reports cost 0 Fixed. The catch branch of runRung reports the sum of its dispatch costs. Test drives main() with a charged answer then ECONNRESET: row cost equals the dispatch sum and budget.spentUsd (reverting to cost: 0 fails it).
R2.4 alias bypasses the free-auto fan-out Fixed. ladderRequestCeiling and estimateLadderUsd test /free-auto$/ on resolveModelAlias(route): free, free-auto, openrouter-auto, openrouter-free-auto, nodeagent-auto now count OPENROUTER_FREE_AUTO_LIMIT per step; openrouter/free and concrete aliases count 1.
R2.5 gpt-6.1-sol request shape Fixed, see Test proof.
R2.6 pricing agreement excluded two sources Fixed, see Pricing sources.

Existing tests that dispatched never-heard-of ids were retargeted, not loosened: evals/ladderBudget.test.ts (g), tests/convexModelFreeAutoMode.test.ts (two tests) and tests/modelAliasClaude.test.ts. The premise of each (an id the catalog does not know reaches a provider) is the defect, so each now asserts the refusal, and their original invariants (zero requests, meter untouched, terminal stop, conservative reservation, hard-budget block) are kept on the one kind of id the adapter can still route without a price row: an id the alias table declares (claude-sonnet-4.5 -> claude-sonnet-4-5). The old ids stay in comments in the diff.

Review round 4 (Codex round-2 defects against 6dabf77f)

Defect Disposition
1 MAJOR resolver rejects ids the repo ships; known :free ids rerouted Fixed at the cause. "Known" was defined as "has a price row or an alias"; it is now also "named by the repo". modelCatalog.REPO_DECLARED_UNPRICED_IDS holds the mobile picker ids (anthropic/claude-fable-5, openai/gpt-5.5, google/gemini-3.5-flash), the frozen benchmark routes (ibm-granite/granite-4.1-8b, inclusionai/ring-2.6-1t, xiaomi/mimo-v2.5, google/gemini-3.1-flash-lite, poolside/laguna-*:free), the frozen BTB default (gpt-4.1-mini, openai/gpt-4.1-mini) and the frozen official judges (gpt-5-mini, google/gemini-3-flash-preview); openRouterFreeModels.OPENROUTER_KNOWN_FREE_IDS holds the catalog fallback list, the preferred list and the spec's 15-row live :free list. No price was invented: a metered run still refuses these as unpriced. tests/modelKnownIds.test.ts reads MODEL_REGISTRY, SUPPORTED_MODEL_ROUTES, llmModelCatalog (Chat.tsx resolves every tier-map id at render), the free ids and the BTB default out of their owning files and asserts resolveModelAlias, model() and convexModel() return each as itself (103 cases). Re-probe of origin/main's resolver (verbatim) against head over 47 of these ids: 0 differ; gemini-9-flash-unknown, acme/paid-unknown-model, claude-opus-4.9 still throw; vendor/not-in-catalog:free still reroutes. Round 5 correction: the claim that frozen and deferred sites "behave as on origin/main" was false. The probe covered 47 ids, not the repo's route lists: 9 of the 19 OPENROUTER_TOP_PAID_AGENT_ROUTES and 18 of the 32 SPEC 1.2 wire ids still threw, and scripts/benchmark/run.ts, ladder:free --free-auto-top and proofloop-free-model-gauge.ts threw on or rewrote operator-selected ids. See Review round 5, defects 1 and 7.
2 MAJOR public API boundary Fixed. See discrepancy 3. tests/agentJobsRuntime.test.ts: all three mutations reject gemini-not-in-catalog with unknown_model_policy and persist no job; a catalog id, an alias and a declared-but-unpriced id still start. Reverting the check fails all three.
3 MEDIUM nodeagent/free-auto paid Gemini leg Fixed. See discrepancy 1.
4 MINOR pi-ai request shape Fixed. createPiAiAdapter().next() is driven through the fake transport: the URL carries gemini-3.8-flash, no sampling keys; an explicit modelName still picks its own provider. The five DEFAULT_AGENT_MODEL call sites stay pinned by tests/modelRefreshCallSites.test.ts (the shared constant is tested on the wire). There is no pi-ai package in this repo; the adapter wraps model().
5 MINOR unsourced price Fixed. claude-opus-4-7 no longer carries cachedInputPer1M (frontier.json lists only $5 / $25). The false "pi-ai catalog" claim in the agreement test comment is gone, and the test now pins every sourced cache rate (sonnet-5-5 $0.10, opus-5-5 $0.20, gemini-3.8-flash $0.075, gemini-3.5-flash-lite $0.03, haiku-5-5 $0.01) and asserts the absent one.
6 NIT class-N text Fixed, see Class N.
7 stale check claim Superseded by the round-4 check output below.

Spec discrepancies for a human decision (SPEC section C) - not decided in this PR

  1. nodeagent/free-auto -> paid gemini-3.8-flash: resolved in round 4. The spec's adapter.ts:152 row fixes only the NODEAGENT_FREE_AUTO_GOOGLE_MODEL env precedence, and that is unchanged. The leg itself is now gated like every other free -> paid fallback (adapter.fallbackModelFor): it needs crossModelFallback, an explicit allowPaidFallback: true and a meter whose awaited reservation (meteredDispatch) precedes the request. Under the default policy a free-route outage is the answer and no paid request is made (tests/modelAdapterProviderNeutralRoute.test.ts: default policy, flag without meter, meter without flag; reverting the guard fails it). No production caller sets allowPaidFallback, so this removes the paid leg in production until a human decides to authorize it.
  2. Alias-declared ids with no price row (claude-sonnet-4.5, claude-opus-4.5, claude-opus-4.6 and what they resolve to). A.0.5/A.0.6 keep old strings valid for persisted rows, A.0.10 says unknown throws. They stay routable; a metered run refuses them (UnpricedModelError), an unmetered caller prices them at the guessed $1/$5. Decision: add verified price rows (no vendor price is in the spec) or drop the aliases.
  3. Public job mutations: resolved in round 4. defaultModelPolicyForRoute (the one funnel createOrReuse, start, startPublicAsk and the two internal starters share) refuses a modelPolicy the catalog does not know with unknown_model_policy before any job row exists. The earlier worry about live clients persisting scripted / fast did not hold for client senders (the model picker sends catalog ids, plus openrouter/free-auto), but this statement missed the one internal sender, runRoomAgent's PlanPreview-blocked branch, which records modelPolicy: "not_started" and was rejected by the new check (fixed in round 5, defect 2: the marker is accepted only with initialStatus: "blocked"); scripted / fast / test-model were test fixtures, now catalog ids. The one script that used a label (scripts/startup-diligence-live-eval.ts, contract-only mode, runner stubbed) now carries openrouter/free-auto; the evidence label startup-contract-eval on the attempt rows is unchanged. Rows already persisted are untouched.
  4. getProviderForModel is still a classifier (any slash id classifies as OpenRouter) because free-auto candidates discovered live (for example openrouter/owl-alpha) must classify. The gate is the resolver and the two boundaries, not the classifier.
  5. An unknown concrete :free id resolves to openrouter/free-auto, as A.0.10 asks. Round 4 corrects the first version of this: every :free id the repo names (the catalog fallback list, the preferred list, the benchmark routes) and every row of the spec's 2026-10-10 live :free list now resolves as itself, so nvidia/nemotron-3-super-120b-a12b:free, google/gemma-4-26b-a4b-it:free and poolside/laguna-*:free are no longer rewritten. Only an id nothing in the repo or the spec names is rerouted.
  6. Pre-existing, not in this PR (frozen site): the durable file-egress promotion (convex/agentJobs.ts, behind the FREE_AUTO_ALLOW_FILE_EGRESS_PROMOTION env opt-in) turns a free-auto job into paid z-ai/glm-4.7-flash with no allowPaidFallback and no reservation. Its site is FROZEN in the spec, so it goes to the human (SPEC section C).

Review round 5 (Codex round-3 defects against fec0c331)

Defect Disposition
1 MAJOR fail-closed resolver rejects ids the repo and SPEC 1.2 list (D1, A.0.4) Fixed. REPO_DECLARED_UNPRICED_IDS now holds all 19 OPENROUTER_TOP_PAID_AGENT_ROUTES and every SPEC 1.2 wire id on every transport (verified :free ids are in OPENROUTER_KNOWN_FREE_IDS). Before: 9 of 19 top-paid routes and 18 of 32 section 1.2 ids threw, so model-eval-matrix --route-set top-paid broke. tests/modelKnownIds.test.ts reads OPENROUTER_TOP_PAID_AGENT_ROUTES, allAgentLlmRoutes(), resolveRouteSet() for top-paid, supported, champions, free, research, collaboration, and a section 1.2 list (typed in the test) and asserts resolveModelAlias, model() and convexModel() return each id as itself. Mutation run: removing qwen/qwen3.7-max and x-ai/grok-build-0.1 from the registry fails their cases. No price was invented; a metered run still refuses these as unpriced.
2 MAJOR PlanPreview-blocked run throws unknown_model_policy Fixed at the boundary, no fake model id changed. defaultModelPolicyForRoute accepts the internal marker not_started only together with initialStatus: "blocked" (no model will run, so there is no id to check). Any other use of the marker is still unknown_model_policy (tested). tests/agentJobsRuntime.test.ts drives runRoomAgent's blocked branch (convex-test database, fake ctx): call order credits:reserve, agentJobs:createOrReuse, messages:sendAgent, credits:settle, result plan_blocked, job row blocked with modelPolicy: "not_started". Mutation run: removing the exemption fails it.
3 MAJOR private stream dispatches an unknown AGENT_MODEL Incomplete in round 5; closed in round 6 (Review round 6, defect 2). One guard, resolveModelAlias(modelId), at the top of streamPrivateReplyText (it throws UnknownModelError, as convexModel() does for the same env value). tests/modelRefreshServerRoles.test.ts: AGENT_MODEL=gemini-not-in-catalog driven through POST /stream-private-reply makes 0 fetch calls, and the route puts the refusal in the stream as visible text; a direct call with claude-not-in-catalog-9 rejects with 0 calls. Mutation run: removing the guard fails it.
4 MAJOR public capture action dispatches any Anthropic or OpenAI id Incomplete in round 5; closed in round 6 (Review round 6, defect 2). One guard, resolveModelAlias(modelId), at the top of aiSdkReasoner covers convex/capturesNode.ts, captureSource.ts:32 and captureSourceFirecrawlTool.ts:29. tests/modelRefreshRequestShape.test.ts: claude-not-in-catalog-9, gpt-not-in-catalog, o3-not-in-catalog, claude-opus-9 each throw UnknownModelError with 0 fetch calls; the default claude-haiku-5-5 and the effort tests are unchanged. Mutation run: removing the guard fails all four.
5 MAJOR call sites pinned by regular expressions Fixed. tests/modelRefreshAgentWire.test.ts drives runRoomAgent (public_ask), runPrivateAgent and agentJobRunner.runFreeAutoJobSlice (a claimed job whose modelPolicy is empty) on a convex-test database with a fake fetch, asserts the emitted model (gemini-3.8-flash, or the AGENT_MODEL override) in every request URL and no sampling keys, and that an unknown AGENT_MODEL sends 0 requests. Codex's mutation (modelNameForEgress always returning configuredFileEgressModel()) plus a runner default of gemini-3.5-flash now fails 5 of its 6 tests; both were reverted and the tree restored. The tests/modelRefreshCallSites.test.ts header no longer claims the sites are unreachable.
6 MAJOR D2 not implemented for top_paid Fixed. top_paid default is nvidia/nemotron-3.5-super-vl:free (see MERGE BLOCKERS and the row above), selected by a test that re-applies the stated criteria to the recorded list (docs/eval/openrouter-free-models-2026-10-16.json, fetched 2026-10-16T11:32:04Z). live-unverified (no key, Homen 2026-10-16). The Environment-smoke blocker is removed. Per-role D2 decision (D2a: D2 chooses the target of roles this refresh moves under rule 1): agentJobs.ts top_paid applies (moved from the retired claude-sonnet-4); providerParserLive.ts:113 does not apply (only drops the unlisted nex-agi/nex-n2-pro, picks no new target, remaining candidates are frozen paid routes with no vendor successor); modelRegistry.ts:48 does not apply (defer, id is active); the frozen z-ai/glm-5.2, kimi-k3 rows and the research lane minimax/minimax-m3 stay frozen. Behaviour change disclosed: a top_paid job in a room with an uploaded file is refused at the free-file-egress gate unless AGENT_TOP_PAID_MODEL / AGENT_MODEL is set.
7 MINOR operator pass-through (D1) Incomplete in round 5; closed in round 6 (Review round 6, defect 1). model(id, { operatorSelected: true }): the alias table still applies, but an id the catalog does not know goes to the provider as written, and a concrete :free id is not rewritten to the auto router. Set only in scripts/benchmark/run.ts (3 call sites), scripts/proofloop-free-model-gauge.ts, evals/ladder.ts (--real, so ladder:free --free-auto-top too), scripts/spreadsheetbench-run.ts, evals/chatIntakeRuntime.ts; server code never sets it and stays fail-closed. Test: openrouter/owl-alpha:free, qwen/qwen3.9-coder:free, acme/paid-unknown-model keep their names; without the option the unknown paid id still throws and the unknown :free id still goes to the verified free route; a source pin on the five scripts.
8 MINOR no REA/ORX record (D5) Fixed. See the REA and ORX section below and CHANGELOG/qa/model-refresh-noderoom.md.
9 MINOR stale .env.example comment Fixed. It now says the direct Gemini leg is an opt-in paid leg: it needs allowPaidFallback and a spend meter (adapter.ts), and no production caller sets either.
10 MINOR PR body mismatches Fixed. The piAiAdapter.ts:9 row, the round-3 "fixed at the resolver and both boundaries" row, the round-4 "behave as on origin/main" claim and the discrepancy 3 sender claim are corrected above.
11 MERGE BLOCKER ladder receipt Recorded, not run (paid). Human command: LADDER_MAX_COST_USD=9 npm run ladder:real -- gemini-3.6-flash,gemini-3.8-flash --levels=1-4 --record --json-out docs/eval/ladder/model-refresh-noderoom.json (pre-flight worst case $8.0552 for 144 requests; actual spend expected far lower). tests/qaMatrix.test.ts:114 fails by design until the receipt exists. The manual listen check of gemini-3.8-flash-tts is also outstanding.
12 check result See Check output.

Review round 6 (Codex round-4 defects against 1c98671f)

Defect Disposition
1 MAJOR operator overrides lost pass-through (D1, A.0.4) Fixed; the round-5 "Fixed" for item 7 overstated it. operatorSelected: true is now passed at every call site that takes an operator id: evals/financeModelLive.ts, evals/managedLockPerf.ts (both realModel calls), evals/professionalRuntimeLive.ts, scripts/halo-live-path-calibration.ts, scripts/startup-diligence-provider-eval.ts, and two more the same search found, scripts/sfn-nonbtb-nodeagent-runner.ts (SFN_MODEL_ID) and src/eval/bankerToolBenchNodeAgentGeneral.ts (--model-id). convexModel() had no such option: ConvexModelOptions.operatorSelected has the same contract as ModelAdapterOptions.operatorSelected (alias table applies, an unknown id goes to the provider as written; server code never sets it), and normalizeFallbackModelIds no longer re-resolves a primary its caller already resolved (it would have re-thrown). Tests: convexModel("gemini-not-in-catalog", { operatorSelected: true }) puts /models/gemini-not-in-catalog:generateContent on the wire (fake transport) and throws UnknownModelError without the option; model and convexModel keep vendor/unknown-x and foo/bar:free. The source pin is now a scan of EVERY resolver call (balanced parentheses, so multi-line calls count) in 12 entry points instead of a five-file regex; removing the option from the startup-diligence call fails it, and removing the convexModel branch fails 2 files.
2 MAJOR free-looking unknown ids dispatched under the raw name (A.0.10) Fixed; the round-5 "Fixed" for items 3 and 4 overstated it. Both guards threw the resolver's result away. streamPrivateReplyText now routes resolveModelAlias(modelId), so gemini-3.8-flash:free, foo/bar:free, claude-not-in-catalog:free and gpt-not-in-catalog-free go to OpenRouter as openrouter/free-auto (what the same path already does for a known free-auto input) and the raw string appears in no request. aiSdkReasoner serves only Anthropic and OpenAI, so it dispatches the resolved id and refuses (capture_reasoner_route_unsupported:openrouter/free-auto, 0 requests) when the resolved route is not served; a dotted display id now goes out hyphenated (claude-opus-4.8 becomes claude-opus-4-8). Non-free unknowns still throw UnknownModelError with 0 requests. Tests: 4 cases in modelRefreshServerRoles, 5 in modelRefreshRequestShape; reverting either guard to the raw id fails 5 each.
3 MINOR env defaults and file-egress promotion saved unchecked (A.0.10) Fixed. defaultModelPolicyForRoute ran knownModelPolicy only on an explicit modelPolicy. The AGENT_TOP_PAID_MODEL, AGENT_RESEARCH_MODEL, AGENT_ORCHESTRATOR_MODEL and AGENT_MODEL defaults and configuredFileEgressModel() now pass it too; precedence is unchanged. tests/agentJobsRuntime.test.ts (convex-test, real handlers): each env var set to gemini-not-in-catalog is refused with unknown_model_policy and no job row by start and, where the route applies, by startPublicAsk (7 cases), and an unknown AGENT_FILE_EGRESS_MODEL is refused when a free job is promoted to it. All 8 fail without the change.
4 MINOR retained dotted ids and openrouter/auto (A.0.5) Fixed. The old regex hyphenated every dotted claude-* id; the explicit table missed the two retained retired ids. claude-opus-4.1 now maps to claude-opus-4-1 and claude-haiku-3.5 to claude-haiku-3-5 (the wire ids main sent; both models are retired, so this is parity, not a claim that they work), with hyphenated price rows at the dotted rows' prices so they still price. A test enumerates every dotted claude-* price key and asserts a hyphenated, priced wire id, and two builders (convexModel, model) are asserted on the wire. openrouter/auto, the label chooseNodeAgentRoute gives the benchmark lane, is declared known again (unpriced: a metered run still refuses it); a test resolves the model of every router decision (8 tasks x risk x latency x free) as itself.
5 MINOR gemini-3.6-flash contextWindow provenance (A.0.6) Sourced. frontier.json marks the 3.6 context UNVERIFIED; the value 1048576 is context_length for google/gemini-3.6-flash on the public OpenRouter list (GET /api/v1/models, no key, read 2026-10-16), now cited in the row. The dollar prices were already sourced.
6 not counted Unchanged and still disclosed: file-egress promotion free to paid (Spec discrepancy 6, a human decision; reproduces on main, flag-gated, frozen site), the paid ladder receipt, the TTS listen check, prod:gate not run.

Review round 7 (Codex round-5 defects against 74c20c49)

Defect Disposition
1 MAJOR startup-diligence eval measures a different model for an unknown operator id (D1, A.0.4) Fixed; the round-6 "Fixed" for item 1 covered convexModel() only. The contract eval passed the provider's resolved id to agentJobs.createOrReuse, whose public boundary refuses an id outside the catalog, so the candidate loop moved on to gemini-3.8-flash. The contract job now always carries the catalog free route (the runner is stubbed; the model measured stays on the attempt rows). tests/modelRefreshServerRoles.test.ts drives runStartupDiligenceProviderEval({ model: 'gemini-not-in-catalog', noWrite: true }) with a fake fetch: 1 request, no errors, the report names that model only. Reverting the line gives 2 requests and fails it.
2 MAJOR judge() has no operator path (D1, A.0.4) Fixed. judge(id, prompt, { operatorSelected }), same meaning as ModelAdapterOptions.operatorSelected; passed by evals/professionalLiveCatalog.ts and scripts/benchmark/run.ts. openrouter-free-discover keeps the strict resolver (fixed id). Test: an unknown Gemini id and acme/new-model:free go out as written with the option, and without it the unknown id still throws with 0 requests.
3 MINOR private stream posts openrouter/free-auto Fixed (refuse branch). The sentinel is not an OpenRouter id (public list read 2026-10-17: openrouter/free, openrouter/auto). streamPrivateReplyText throws private_stream_free_auto_unsupported before any request for the sentinel and for nodeagent/free-auto; the comment is corrected. The round-6 test that asserted the invalid wire value now asserts 0 requests (4 free-looking ids plus the sentinel itself). This supersedes the round-6 item 2 statement that the stream sends openrouter/free-auto.
4 MINOR pricing source 6 not called Fixed. proofloopModelCostFieldsForRun (which reaches the private estimateModelCostUsd) is called for every refresh row, the long-context aggregate case and the two retired rows.
5 MINOR no forced tool_choice assertion on generateObject Fixed. The parser tests assert no tool_choice, no tools, output_config.format.type === "json_schema" and max_tokens >= 64000 on every Anthropic request (Haiku and Sonnet 5.5).
6 NIT retired rows undated Test added, no date invented. claude-opus-4-1 and claude-haiku-3-5 carry main's prices; the agreement test pins them under both spellings in every source. (Superseded in round 8: they are now dated 2026-04.)
7 / 8 merge gates Unchanged and disclosed: the paid ladder receipt (LADDER_MAX_COST_USD=9 npm run ladder:real -- gemini-3.6-flash,gemini-3.8-flash --levels=1-4 --record --json-out docs/eval/ladder/model-refresh-noderoom.json, worst case $8.0552 for 144 requests, not run) and the manual listen check of gemini-3.8-flash-tts.
9 file-egress promotion free to paid Escalated to Homen, not changed (Spec discrepancy 6; frozen site convex/agentJobs.ts:2481-2485; D2 sends the free top_paid default through it).
10 Dropped by the reviewer: runLiveProviderParser has no server caller.

Review round 8 (Codex round-6 defects against 4b2af806)

Defect Disposition
1 BLOCKER ladder receipt Open, needs a paid run; unchanged. LADDER_MAX_COST_USD=9 npm run ladder:real -- gemini-3.6-flash,gemini-3.8-flash --levels=1-4 --record --json-out docs/eval/ladder/model-refresh-noderoom.json (worst case $8.0552, ceiling 144 requests; a $1 cap refuses before any request). tests/qaMatrix.test.ts stays red on purpose. Draft + needs-live-eval stay.
2 BLOCKER TTS listen check Open, manual; unchanged (shutdown of the old id 2026-11-17).
3 MAJOR free-to-paid file-egress promotion (agentJobs.ts:2481-2485, A.0.10) Escalated to Homen, not changed in this PR. The mechanism is pre-existing and flag-gated (FREE_AUTO_ALLOW_FILE_EGRESS_PROMOTION, default 0). Gating it behind allowPaidFallback plus an awaited reservation is not possible inside a Convex mutation (NodeRoom has no per-job financial reservation there; the runner's primary dispatch reserves nothing, only spendLimits), and removing it would retire an operator opt-in that three test files pin. Options a/b/c are recorded as SPEC section C item 10. Disclosed consequence of D2: top_paid now defaults to a free model, so a top_paid job in a room with an uploaded file is blocked by the free-file-egress gate or, with the flag on, promoted to the paid configuredFileEgressModel() (default z-ai/glm-4.7-flash).
4 MINOR undated retired rows (A.0.6) Fixed. claude-opus-4-1 and claude-haiku-3-5 carry pricingAsOf: "2026-04", the month in the "official docs, April 2026" header of origin/main's block holding the dotted rows they were copied from (claude-opus-4.1, claude-haiku-3.5). Main gives only the month, so no day is invented. tests/modelPricingAgreement.test.ts asserts the date where it used to assert "undated".
5 MINOR dispatch records capped silently (A.0.3) Fixed. SpendMeter.droppedDispatches counts requests past MAX_DISPATCH_RECORDS; the ladder budget block carries dispatchesDropped (non-zero means rows is incomplete: reject the receipt) and the spend line prints it. Rung cost is now the meter's spentUsd delta over the rung, not the sum of the capped rows. Test (tests/modelCostClass.test.ts): 4,097 fake requests give requests 4097, 4096 rows, droppedDispatches 1, and spentUsd exceeds the rows' sum by exactly one request. A one-request ladder run through main() with a fake fetch (only googleapis.com allowed, 1 call) wrote dispatchesDropped: 0 and a rung cost equal to the row sum. It cannot change the 144-request receipt.
6 MINOR "Top paid" UI copy (src/ui/Chat.tsx:872) Disclosed in round 8; fixed in round 9 (badge "operator"; copy names AGENT_TOP_PAID_MODEL / AGENT_MODEL and says the server default is a free model). Original note: The option still reads badge "best" and "Pins the strongest configured paid route". With AGENT_TOP_PAID_MODEL and AGENT_MODEL unset, the code default is the free nvidia/nemotron-3.5-super-vl:free (D2), so a user who picks Top paid gets a free route. No copy was changed because the wording is a product decision.
7 MINOR no wire test for the new top_paid target Fixed. tests/modelRefreshRequestShape.test.ts sends DEFAULT_TOP_PAID_MODEL through convexModel().next with a fake transport: URL is OpenRouter chat completions, model is nvidia/nemotron-3.5-super-vl:free as written, tool_choice is required, no sampling keys; the constant is also pinned to the literal.
8 NIT Fixed in this body: the runtime row cites agentJobs.ts:2118; the "What" claim now carries the fail-closed qualifier.
9, 10 INFO No fix needed. The round-6 head claim was stale; the judge's pre-flight requests were unauthenticated and unbilled.

REA and ORX (SPEC-v9 D5), fetched 2026-10-16

REA reads the mechanisms this PR touches: an explicit alias table instead of a regex, a known-id registry that fails
closed, tiered per-request pricing, a spend meter. Facts, with what was adopted or rejected.

Reference (fetched) Fact observed Adopted Rejected
LiteLLM model_prices_and_context_window.json (raw.githubusercontent.com/BerriAI/litellm/main, 4,194 entries, 2026-10-16T14:44Z) One flat map keyed by provider-prefixed wire id (gemini/gemini-3.8-flash, vertex_ai/gemini-3.8-flash, claude-haiku-5-5, bedrock_mantle/anthropic.claude-haiku-5-5). Tier prices are separate keys on the same row (input_cost_per_token_above_100k_tokens, *_above_200k_tokens, cache_read_input_token_cost_above_100k_tokens); claude-haiku-5-5 carries 5e-07 / 2.5e-06 / 5e-08 above 100K. deprecation_date is per row and differs by transport (azure_ai/claude-sonnet-5 2027-06-30, vertex_ai/claude-sonnet-5 2026-12-24, native claude-sonnet-5 null). max_output_tokens is 128000 for claude-haiku-5-5. An id is looked up by its exact wire string per transport (the explicit table, no regex); the tier is a field of the price row and is chosen per request; a lifecycle date belongs to the transport, not to the model name (SPEC B.1). Our Haiku 5.5 tier ($0.50 / $2.50 / cache read $0.05 above 100K) equals LiteLLM's. A single unified id space across transports; regional multipliers (us.anthropic.* rows at 1.1x) and batch keys, which no NodeRoom path uses.
models.dev https://models.dev/api.json (231 providers, 2026-10-16T14:44Z) anthropic/claude-haiku-5-5: cost 0.1 / 0.5, cache_read 0.01, cache_write 0.125, one tiers entry {type: "context", size: 100000} at 0.5 / 2.5 / 0.05 / 0.625; limit.output 128000; release_date 2026-10-07; tool_call and structured_output true. Resellers re-price the same model (poe/google/gemini-3.8-flash input 0.7576 against 0.75 at the vendor). The tier boundary (100K prompt tokens, per request) and the vendor rates cross-check our table; release_date agrees with the spec. Reseller rows as a price source: prices come from the vendor page, never an aggregator.
OpenRouter GET /api/v1/models (public, no key; 465 rows, 2026-10-16T11:32:04Z; earlier reads 2026-10-10 and 2026-10-14 in the spec) Each row has id, created, context_length, per-token string pricing, supported_parameters (tools, tool_choice, structured_outputs) and expiration_date; 18 of 465 rows carry an expiration_date, including two of the newest :free rows. Capability selection from supported_parameters, the expiration_date exclusion and created ordering for D2; the per-token to per-1M conversion is already pinned by tests/modelPricingAgreement.test.ts. Treating absence from the list as a vendor retirement (it is "unlisted on this transport", SPEC 1.1).
Vercel AI SDK @ai-sdk/anthropic 3.0.120 (installed source, dist/index.mjs getModelCapabilities, read 2026-10-16) Capabilities are matched by substring. claude-haiku-5-5 matches no 5.x branch (only claude-opus-5-5, claude-opus-5 and claude-sonnet-5 do), so it falls into the generic claude- branch: maxOutputTokens 128000, isKnownModel: false. The SDK reports the flag and still dispatches. The isKnownModel idea, enforced instead of reported: resolveModelAlias throws UnknownModelError, and modelMaxOutputTokens keeps its own 2048 floor and caps for the 5.5 ids rather than trusting the SDK fallback. Substring matching (it is how claude-haiku-5-5 ended up in the fallback branch).

ORX, primary sources behind the ids, prices and dates in this PR (ids and prices were not taken from recall):

  • Claude ids, prices and floors: frontier.json (read 2026-10-10, which cites platform.claude.com/docs/en/models/overview
    and the release notes); the Anthropic deprecations page re-read live 2026-10-14 (stored copy
    _scratch/anthropic-deprecations-2026-10-14-v8.html in the model-defaults workspace): claude-haiku-4-5-20251001 Active,
    floor 2026-10-15; claude-sonnet-4-5-20250929 deprecated, retires 2026-11-30.
  • Gemini ids, prices and the gemini-3.5-flash deprecation (2026-10-08) and gemini-3.1-flash-tts-preview shutdown
    (2026-11-17): frontier.json (read 2026-10-10), Google's deprecations table as quoted in SPEC 1.1.
  • OpenAI ids and the 2026-10-14 GPT-4-series notice (removal 2027-01-14): OpenAI deprecations page re-read live 2026-10-14.
  • OpenRouter ids, expiration_date and the D2 selection: the public list above, recorded in
    docs/eval/openrouter-free-models-2026-10-16.json (all 15 :free rows with the criteria columns).
  • Not verified by a request: no provider call was made. Every :free route and the Claude 5.5 ids over OpenRouter are
    request-shape tested against a fake transport only.

Pricing sources (A.0.6) - every source is enumerated and in tests/modelPricingAgreement.test.ts

  1. modelCatalog.modelPricing - the only hand-typed table. Every row this PR adds or relies on now carries pricingAsOf (2026-10-10), including claude-opus-4-7 ($5/$25, which claude-opus-4.7 now resolves to) and is in the agreement cases. claude-haiku-5-5 and anthropic/claude-haiku-5.5 also carry the >100K-prompt tier ($0.50/$2.50) as longContext.
  2. getModelPricing - accessor over (1).
  3. adapter.priceRequest, priceRequestStrict, reserveDispatch - tier-aware through modelCatalog.priceTokens, and ONLY for one request's prompt (the ladder meter uses the strict price and the reservation). adapter.priceRun prices usage summed over requests at the row's base tier (priceAggregateTokens): a sum of 60K + 60K prompts is not a 120K prompt.
  4. convexModel.convexPriceRequest, exactConvexUsageCost, convexAttemptCostReservationUsd - per-request, tier-aware; convexPriceRun is the base-tier aggregate. calculateRequestCost(useCachedInput) is per-request and reads the tier's own cache rate.
  5. creditModel private priceRun and proofloopModelTracking cost estimate - aggregate (base tier, priceAggregateTokens).
  6. scripts/benchmark/pricingAtRun.ts (moved out of run.ts in round 3, which runs the benchmark on import) - pricingAtRunFor PREFERS the OpenRouter /models snapshot (priceFromOpenRouterInfo, dated by that run's fetchedAt + routeSnapshotId, recorded on every row's pricingAtRun) and falls back to (1) through catalogPricingAtRun, then to a flagged fallback_estimate ($1/$5). It is not purely a catalog reader.
  7. scripts/proofloop-proxy-model-sweep.ts (:131/:238) - prices from the live OpenRouter catalog fetched when the sweep runs (dated fetchedAt per row); no fallback table.
  8. Chat.tsx price chips - read (1), show the base tier.

Round 3 (review defect 5): sources 6 and 7 are no longer excluded. Their per-token to per-1M conversion is code that can disagree with (1), and A.0.6 allows no exclusion. tests/modelPricingAgreement.test.ts now runs source 6 (pricingAtRunFor / catalogPricingAtRun) and source 7 (the sweep script itself in --dry-run, fake fetch) on a dated OpenRouter /models fixture (2026-10-10, literal per-token strings) and asserts {inPerM, outPerM} (and the cache-read rate) equal the catalog row for every OpenRouter-transport refresh row. Reverted to check: prompt * 1_000_000 -> * 100_000 in pricingAtRun.ts, and parsed * 1_000_000 -> * 100_000 in the sweep: each fails its agreement test (Codex's 10x undercharge now fails). The no-hand-typed-table check stays, now pointed at pricingAtRun.ts and asserting run.ts imports it.

The tier fix is behaviour, not just data: 120K input + the 8192-token cap now reserves $0.08048 (was $0.016096), and the pre-flight estimate prices each step by its prompt size.

Round 2 (review defect 2): the tier is chosen per REQUEST only (reserveDispatch, meter settlement, runtime priceStep = priceRequest / convexPriceRequest, calculateRequestCost, exactConvexUsageCost). Aggregated usage is priced at base-tier rates (priceRun, convexPriceRun, creditModel, proofloopModelTracking), and the ladder sums meter.dispatches[].costUsd (metered) or the runtime's per-step usage.costUsd (scripted) instead of repricing summed usage (round 8: a metered rung's cost is the meter's spentUsd delta). priceRun('claude-haiku-5-5', 120000, 2000) was $0.065 while two settled 60K/1K requests cost $0.013; a run of those two now reports $0.013. HAIKU_5_5_LONG_CONTEXT also carries the vendor's cachedInputPer1M: 0.05, so cached tokens above 100K no longer bill at $0.50/M.

Breaking-parameter fixes (spec section 1.3)

  • sdkToolChoiceForModel (adapter.ts), convexModel.openAiCompatibleToolChoice (OpenRouter anthropic/claude-*-5.5) and the LangChain bridge send auto instead of a forced required/any for Sonnet/Opus 5.5 (and Fable 5.1); Haiku 5.5 keeps a forced choice. Keyed by exact wire id in modelCatalog.forbidsForcedToolChoice. New this round: wherever that downgrade happens the tool definitions carry strict: true (needsStrictTools: AI-SDK adapter, direct Anthropic anthropicStep, OpenRouter openAiTool), which is SPEC 1.3's replacement for the forced choice. It is applied only on the downgrade, so no other path changes. Round 2: the LangChain bridge now passes { strict: true } through bindTools on the downgrade too (asserted in the LangChain request-shape test).
  • modelMaxOutputTokens(id) (shared src/nodeagent/models/outputTokens.ts): 2048 floor for 5.5 ids because thinking counts toward max_tokens; anthropicStep reads content blocks by type (test).
  • Explicit effort (haiku medium / sonnet high / opus medium) on every AI-SDK Anthropic path that moved to a 5.5 id: the adapter, capture/reasoning.ts, providerParserLive, and, new in round 2, scripts/benchmark/probe.ts and scripts/eval/providerParseSmoke.ts through one helper (claudeEffortProviderOptions); @ai-sdk/anthropic ^3.0.84 -> ^3.0.120.
  • openAiTokenLimit regex also matches gpt-6 (precondition only; no gpt-6 id lands here).
  • Judge scripts pinned to gemini-3.6-flash with temperature removed (deprecated on 3.6+); GEMINI_JUDGE_MODEL env chains unchanged.
  • resolveModelAlias: the dotted-id regex rewrite is deleted for an explicit table; claude-opus-4.7 now maps to claude-opus-4-7 (was silently -4-8). The opus and claude-opus shortcuts are pinned to claude-opus-4-8 (their previous effective target, behaviour preserved). Old 4.x strings stay valid for persisted rows.
  • Ladder budget (spec "Ladder budget", lands before the receipt run): --real refuses to start without LADDER_MAX_COST_USD / --max-cost-usd; job-wide meter; reserveDispatch inside every withRetry dispatch (maxRetries: 0 on the SDK, one reservation per physical request); priceRunStrict throws UnpricedModelError instead of a guessed $1/$5; dispatch: { maxRetries, crossModelFallback } option. New this round: a refused reservation or unpriced id is terminal for the meter (the failed dispatch can no longer fall through to AGENT_FALLBACK_MODEL, a retry, or a free-auto candidate), and the provider-neutral nodeagent/free-auto Google fallback honours crossModelFallback: false (it previously always went to paid Gemini). ladder:free gets --max-cost-usd=1 because a cap is now mandatory. Round 2: the pre-flight request ceiling and USD estimate multiply a free-auto route by openRouterFreeAutoLimit() (OPENROUTER_FREE_AUTO_LIMIT, default 8, max 20), the fan-out the adapter enforces, not by --free-auto-top.

Test proof

New request-shape tests build a real request through the repo's own builders with a fake fetch, asserting the literal wire id (never imported from the code under test) and that forbidden keys are absent:

  • tests/modelRefreshRequestShape.test.ts - convexModel (direct Anthropic, OpenRouter), adapter.model, aiSdkReasoner, the LangChain bridge, pi-ai default; now also strict tools and effort.
  • tests/modelRefreshServerRoles.test.ts (new) - the roles that had no fake-transport test: Gemini TTS (drives POST /voice/synthesize), private stream (drives POST /stream-private-reply, nothing injected: the route's own AGENT_MODEL ?? DEFAULT_AGENT_MODEL reaches geminiStream), provider parser (runLiveProviderParser with no model argument, so the default candidate reaches the wire; effort and wire id for haiku and sonnet), judge scripts (rubric-panel, visual-polish, mp4-workflow-review, SEO judge and walkthroughs/judge-video are executed with fixtures and a fake fetch; round 2 also drives judge-demo-gif, judge-state-captures, spreadsheetbench-chart-visual-grade, gemini-demo-media-judge, gemini-ui-review, painJudge, the chart-visual probe, walkthrough-review (CLI default and shipped config), scripts/benchmark/probe.ts, providerParseSmoke and the startup-diligence provider eval through a fake transport; the AST temperature checks stay), and .env.example against the code defaults.
  • tests/modelPricingAgreement.test.ts (A.0.6 agreement, tier-aware), tests/modelCatalogRefresh.test.ts.
  • evals/ladderBudget.test.ts drives the actual adapter path. Round 1 tightened (b) to the specified sequence (charged response, then ECONNRESET) and (e) to usage that stays within every reservation: the cap is the pre-flight estimate itself, the context outgrows the scripted dry run, and the test asserts the grid stops before the cap (spentUsd < maxUsd, a refused reservation, budget.requests equals the fetch count and is below the ceiling). Also covered: terminal-stop behaviour, per-dispatch receipt rows (echoedModel, fallbackUsed) and the report fields.
  • tests/modelRefreshAgentWire.test.ts (new, round 5) - runRoomAgent (public_ask), runPrivateAgent and runFreeAutoJobSlice (claimed job with no modelPolicy) run through their real handlers on a convex-test database with a fake fetch: the model in the request URL is gemini-3.8-flash (or the AGENT_MODEL override) and no sampling key is sent; an unknown AGENT_MODEL sends 0 requests. Mutation check run: modelNameForEgress always returning the file-egress model and the runner default changed to gemini-3.5-flash fail 5 of its 6 tests.
  • tests/modelRefreshCallSites.test.ts (round 2; round 5 correction) - the round-2 claim that these sites were "not reachable without a live Convex deployment" was wrong (convex-test drives the handlers; see the wire test above). This file now only pins where the default is spelled for the sites the wire tests do not drive (streaming.ts is driven by tests/modelRefreshServerRoles.test.ts, Chat.tsx is a React closure): the expression that owns the default; tests/qaMatrix.test.ts now checks EVERY AGENT_MODEL ?? ... fallback in convex/agent.ts, not the first. Re-inlining gemini-3.5-flash at each of the five fails a named test.
  • tests/modelAdapterProviderNeutralRoute.test.ts - crossModelFallback: false never reaches paid Gemini.

Each new assertion was checked by reverting the line it protects (TTS constant, DEFAULT_AGENT_MODEL and each of its five call sites, a judge script's temperature/model, the parser default and effort, strict tools, the tier, the terminal stop, the fallback guard): the matching test fails every time. Round 3 correction: that claim did not hold for the scripts/benchmark/probe.ts gpt-6.1-sol row (the test answered OpenAI with a 404 and asserted nothing about the request; Codex swapped the id and it stayed green). It is now asserted on the wire (POST https://api.openai.com/v1/responses, body.model === "gpt-6.1-sol", no sampling keys, no tools) and reverting the id to gpt-6.1-solx fails it. The round-3 reversions (cost-class guards in the adapter and in convexModel, the resolver throw, the failed-rung cost, the alias fan-out, both pricing conversions, the probe id) were each run and each fails its named test.

Check output (npm run check = typecheck + convex tsc + vitest)

Round 6, head 74c20c49 (the commit that contains the code; a later body-only edit does not change it). tsc --noEmit exit 0, convex/tsconfig.json exit 0, then the full vitest run under machine load: 12 failed / 3215 passed (3227), 398 files. Two are deterministic and not caused by this PR: tests/qaMatrix.test.ts (the ladder receipt blocker above; fails on purpose, the check is not weakened) and tests/proofStaleness.test.ts (docs/eval/noderoom-fresh-user-vertical-proof.json is 47.9 days old against a 30-day window; it also fails on origin/main). The other ten are 5 s to 30 s timeouts under load (ladderBudget x3, createRoomAtomicity, dockerSandboxProbe, notebookProcessingTarget, proofloopHooks, proofloopOrchestrator x2, proofloopPromoteOfficialScore). Re-run serially with --no-file-parallelism --testTimeout=300000, nine of them pass (every test in the six other files). proofloopPromoteOfficialScore has its own 15 s timeout, which the flag does not override; it passes alone in 14.1 s (12.4 s with this PR stashed, so it is close to its limit on this machine). walkthroughCitations passes with the .tours line moved to :2799.

Round 7, head 4b2af806. tsc --noEmit exit 0, convex/tsconfig.json exit 0, full vitest run: 2 failed / 3230 passed (3232), 398 files, no load timeouts this run. Both failures are the known ones: tests/qaMatrix.test.ts (the ladder receipt gate; fails on purpose, not weakened) and tests/proofStaleness.test.ts (also fails on origin/main). Mutation check run and reverted: the startup-diligence modelPolicy line (the new test fails: 2 requests). No paid provider call was made.

Mutation checks run and reverted (each fails its named tests): the startup-diligence operatorSelected argument (1 test), the convexModel option branch (2), the capture reasoner's resolved-id dispatch and route refusal (5), the streaming resolved-id routing (5), the two dotted aliases plus openrouter/auto (5), and, for the boundary, the knownModelPolicy wrapping of the env defaults and the promotion (all 8 new cases). No paid provider call was made.

Check output, round 8

Head 8dcdb2ce. npm run check: tsc --noEmit and convex/tsconfig.json pass; vitest 2 failed / 3233 passed (3235), 398 files. The two failures are the known ones: tests/qaMatrix.test.ts (ladder receipt, fails on purpose, not weakened) and tests/proofStaleness.test.ts (also fails on origin/main). No paid provider call was made.

Not in this PR / for the human

  • Free-to-paid file-egress promotion (SPEC section C item 10) and the Top paid UI copy (round 8 items 3 and 6).
  • The two merge blockers above (the ladder receipt, the TTS listen check).
  • Python backend (backend/app/config.py:78-80, llm.py:37 on GMI MaaS): defer - list GET {GMI_MAAS_BASE_URL}/models, confirm the ids, run the backend pytest.
  • src/landing/modelRegistry.ts:48 (anthropic/claude-fable-5): defer until the forced-tool-choice downgrade is observed live (no key exists, so this stays deferred; D2 does not move it).
  • README.md:1604 and adaptiveRouter.ts:136: move to gemini-3.8-flash with the ladder receipt.
  • Frozen advisories: official judges gpt-5-mini (retires 2026-12-11, URGENT decision), google/gemini-3-flash-preview, benchmark judge gemini-3.1-flash-lite (2027-05-07).
  • Gemini 3.8 Flash and 3.8 TTS intro prices double on 2027-01-01 ($1.50/$7.50); the catalog rows say so and need re-pricing then.
  • Merge-order note: Codex PR Improve README onboarding, package dependencies, and CHANGELOG handoff #256 bumps undici in package.json / package-lock.json and PR fix(evals): reject unconfigured real-mode runs #260 owns evals/runEval.ts; this PR touches neither. The lockfile change here adds a nested @ai-sdk/anthropic/node_modules/undici 6.29.0 entry; re-run npm ci after whichever PR merges second.

Spec: model-defaults SPEC Part A section A.2 (NodeRoom)

Review round 9 (against 8dcdb2ce)

Defect Disposition
1 MAJOR default-route failover Fixed. Removed the gemini-3.8-flash, gemini-3.6-flash, gemini-3.5-flash-lite chain keys; configuredConvexModelFallbacks('gemini-3.8-flash', {}) is [] again as on main. Test pins the three ids chain-less; the "has a chain" assertion covers the 5.5 ids only.
2 MINOR Top paid copy Fixed (see round 8 item 6).
3 MINOR ladder exit status Fixed. droppedDispatches > 0 exits 1 like stoppedBy. No dedicated test (needs 4,097 real requests through main()); the 144-request receipt cannot reach it.
4 MINOR flaky timeouts Fixed. 30 s per-test timeout on ladderBudget (e) and the 4,097-request modelCostClass case; no assertion changed.

Check at round 9: tsc and convex tsc pass; full vitest 3234 passed, 2 failed (the known qaMatrix gemini-3.8-flash-not-in-ladder, red until the paid receipt exists, and proofStaleness, red on main). No paid calls made; paid ladder receipt and TTS listen check remain merge blockers.

HomenShum and others added 2 commits October 14, 2026 12:10
Move NodeRoom's default model ids off retired or deprecated ones, per model-defaults
SPEC Part A section A.2.9:

- one DEFAULT_AGENT_MODEL (gemini-3.8-flash) in convex/modelDefaults.ts replaces the
  gemini-3.5-flash literals (Google deprecated it 2026-10-08); env chains unchanged
- top_paid default anthropic/claude-sonnet-4 -> anthropic/claude-sonnet-5.5; Haiku defaults
  -> claude-haiku-5-5; pi-ai default gemini-2.5-flash -> DEFAULT_AGENT_MODEL
- TTS gemini-3.1-flash-tts-preview -> gemini-3.8-flash-tts (shutdown 2026-11-17)
- catalog: 5.5 and gemini pricing rows, tier allowlists, fallback chains pruned; the dotted
  claude-id regex becomes an explicit table (claude-opus-4.7 now maps to claude-opus-4-7)
- Claude 5.5 request shape: forced tool_choice downgraded to auto for Sonnet/Opus 5.5,
  explicit effort on the AI-SDK path, 2048 max_tokens floor, @ai-sdk/anthropic >= 3.0.120
- judge scripts pinned to gemini-3.6-flash with temperature removed
- ladder: --real needs a dollar cap, per-dispatch cost reservation, receipt carries the
  tested commit, budget and dispatch policy

Merge is gated on the L1-L4 ladder receipt (not run here: paid). The adaptiveRouter fast
lane stays on gemini-3.5-flash until that receipt exists.

Spec: model-defaults SPEC Part A §A.2 (NodeRoom)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
noderoom Ready Ready Preview Oct 17, 2026 8:02am UTC

Request Review

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Scaffold Handoff — For Your Coding Agent

Your coding agent (Codex, Claude Code, etc.) should apply the accepted
scaffold proposals below. Do NOT touch any immutable files.

Immutability Check

Mode: advisory

✅ No immutable files were modified in this branch.

Changed Files

  • .env.example
  • .tours/01-primary-user-flow.tour
  • .tours/03-debug-and-recovery.tour
  • CHANGELOG/README.md
  • CHANGELOG/qa/model-refresh-noderoom.md
  • convex/agent.ts
  • convex/agentJobRunner.ts
  • convex/agentJobs.ts
  • convex/http.ts
  • convex/modelDefaults.ts
  • convex/streaming.ts
  • convex/streamingModel.ts
  • docs/eval/OFFICIAL_BENCHMARK_READINESS.md
  • docs/eval/OFFICIAL_BENCHMARK_TASK_COVERAGE.md
  • docs/eval/OPENROUTER_CONVEX_BENCHMARK.md
  • docs/eval/agent-improvement-loop.md
  • docs/eval/agent-improvement-loop.svg
  • docs/eval/agent-improvement-loop/20261017T080131Z.json
  • docs/eval/agent-improvement-loop/latest.json
  • docs/eval/agent-workspace-sandbox-smoke.json
  • docs/eval/algorithm-artifact-smoke.json
  • docs/eval/bankertoolbench-official-contract.json
  • docs/eval/docker-sandbox-probe.json
  • docs/eval/eval-runs.jsonl
  • docs/eval/halo-self-improvement-smoke.json
  • docs/eval/halo-variant-selection.json
  • docs/eval/official-benchmark-readiness.json
  • docs/eval/official-benchmark-task-coverage.json
  • docs/eval/openrouter-convex-benchmark.json
  • docs/eval/openrouter-free-models-2026-10-16.json
  • docs/eval/professional-catalog-proofs.json
  • docs/eval/professional-proof-ledger.json
  • docs/eval/spreadsheetbench-chart-visual-probe.json
  • docs/eval/traces/credit/20261017T080141449Z-ebd3f761_dirty.911bab07bcecb64f/cascade-healthy.json
  • docs/eval/traces/credit/20261017T080141449Z-ebd3f761_dirty.911bab07bcecb64f/delta-incomplete.json
  • docs/eval/traces/credit/20261017T080141449Z-ebd3f761_dirty.911bab07bcecb64f/mapping-correct.json
  • docs/eval/traces/credit/20261017T080141449Z-ebd3f761_dirty.911bab07bcecb64f/mapping-misbind.json
  • docs/eval/traces/credit/20261017T080141449Z-ebd3f761_dirty.911bab07bcecb64f/summit-stressed.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L1_read_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L2_edit_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L3_conflict_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L4_blocked_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L5_large_range_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L6_long_horizon_scripted.json
  • docs/eval/traces/ladder/20261017T080140976Z-ebd3f761c037c5812970429188f370f0cb171e67_dirty.19f23cfa24de9dd2/ladder_L7_resume_scripted.json
  • evals/chatIntakeRuntime.ts
  • evals/financeModelLive.ts
  • evals/ladder.ts
  • evals/ladderBudget.test.ts
  • evals/managedLockPerf.ts
  • evals/professionalLiveCatalog.ts
  • evals/professionalRuntimeLive.ts
  • package-lock.json
  • package.json
  • packages/walkthrough-review-cli/README.md
  • packages/walkthrough-review-cli/src/core.ts
  • scripts/benchmark/pricingAtRun.ts
  • scripts/benchmark/probe.ts
  • scripts/benchmark/run.ts
  • scripts/design-qa/rubric-panel.ts
  • scripts/eval/painJudge.ts
  • scripts/eval/providerParseSmoke.ts
  • scripts/gemini-demo-media-judge.ts
  • scripts/gemini-mp4-workflow-review.ts
  • scripts/gemini-ui-review.ts
  • scripts/gemini-visual-polish.ts
  • scripts/halo-live-path-calibration.ts
  • scripts/judge-demo-gif.ts
  • scripts/judge-state-captures.ts
  • scripts/proofloop-free-model-gauge.ts
  • scripts/sfn-nonbtb-nodeagent-runner.ts
  • scripts/spreadsheetbench-chart-visual-grade.ts
  • scripts/spreadsheetbench-run.ts
  • scripts/startup-diligence-live-eval.ts
  • scripts/startup-diligence-provider-eval.ts
  • scripts/walkthroughs/judge-video.ts
  • skills/seo-qa/scripts/judge-video-gemini.ts
  • src/eval/bankerToolBenchNodeAgentGeneral.ts
  • src/eval/proofloopModelTracking.ts
  • src/eval/spreadsheetBenchChartVisualProbe.ts
  • src/eval/spreadsheetBenchRunner.ts
  • src/nodeagent/capture/reasoning.ts
  • src/nodeagent/core/adaptiveRouter.ts
  • src/nodeagent/core/creditModel.ts
  • src/nodeagent/core/runtime.ts
  • src/nodeagent/guardrails/egressPolicy.ts
  • src/nodeagent/integrations/langchain.ts
  • src/nodeagent/integrations/modelInterop.ts
  • src/nodeagent/models/adapter.ts
  • src/nodeagent/models/convexModel.ts
  • src/nodeagent/models/modelCatalog.ts
  • src/nodeagent/models/openAiTokenLimit.ts
  • src/nodeagent/models/openRouterFreeModels.ts
  • src/nodeagent/models/outputTokens.ts
  • src/nodeagent/models/piAiAdapter.ts
  • src/nodeagent/models/providerParserLive.ts
  • src/ui/Chat.tsx
  • tests/agentJobsRuntime.test.ts
  • tests/agentJobsSource.test.ts
  • tests/convexModelFreeAutoMode.test.ts
  • tests/modelAdapterProviderNeutralRoute.test.ts
  • tests/modelAliasClaude.test.ts
  • tests/modelCatalogRefresh.test.ts
  • tests/modelCostClass.test.ts
  • tests/modelKnownIds.test.ts
  • tests/modelPricingAgreement.test.ts
  • tests/modelRefreshAgentWire.test.ts
  • tests/modelRefreshCallSites.test.ts
  • tests/modelRefreshRequestShape.test.ts
  • tests/modelRefreshServerRoles.test.ts
  • tests/openAiTokenLimit.test.ts
  • tests/openRouterFreeModels.test.ts
  • tests/phaseModel.test.ts
  • tests/privateArtifactVisibility.test.ts
  • tests/providerParserLive.test.ts
  • tests/qaMatrix.test.ts
  • tests/roomWorkCache.test.ts
  • tests/voiceHostedTtsProviders.test.ts
  • vitest.config.ts
  • walkthrough-review.config.json

Needs Adversarial Review — Do NOT Apply Yet

These proposals passed the reject check but have not been approved by
an adversarial reviewer. A human or frozen LLM judge must approve them first.

  • scaf-001 (AGENTS.md): Add explicit instruction for step halo-convex-context-telemetry: Step halo-convex-context-telemetry failed — scaffold may need explicit instruction or evidence assertion.
  • scaf-002 (AGENTS.md): Add explicit instruction for step spreadsheetbench-runner-fixture: Step spreadsheetbench-runner-fixture failed — scaffold may need explicit instruction or evidence assertion.
  • scaf-003 (AGENTS.md): Add explicit instruction for step convex-boundaries: Step convex-boundaries failed — scaffold may need explicit instruction or evidence assertion.

Safety Boundary

Agent may improve the scaffold.
Agent may NOT weaken the proof gate.

Immutable files (never modify):

  • scripts/proofloop.mjs
  • scripts/agent-improvement-loop.ts
  • tests/harnessChangeEval.test.ts
  • .github/workflows/
  • src/eval/evalTrustPolicy.ts
  • src/eval/architectureBudget.ts
  • evals/evalStore.ts

Scaffold files (safe to modify):

  • AGENTS.md
  • CLAUDE.md
  • proofloop/scenarios/*.yaml
  • proofloop/rubrics/*.yaml
  • proofloop/subagents/*.md
  • proofloop/adapters/*.js
  • .proofloop/memory.jsonl
  • src/nodeagent/models/prompts/systemPrompt.ts

@HomenShum HomenShum added the needs-live-eval Merge blocked until a live receipt or manual check exists (SPEC A.0.3). A block, not a note. label Oct 14, 2026
@HomenShum
HomenShum marked this pull request as draft October 14, 2026 20:09
- merge gate: PR is a draft with the needs-live-eval label; the ladder receipt, the
  gemini-3.8-flash-tts listen check and the top_paid Environment smoke are listed as
  explicit blockers (PR body, CHANGELOG lane)
- ladder --json-out is now reviewable under A.0.3: command, candidateIds, echoedModels,
  per-dispatch rows (provider-echoed model from the raw body, usage, cost, fallbackUsed),
  environment, budget.requestCeiling
- a refused reservation or unpriced id is terminal for the meter: no fallback model, retry
  or candidate dispatches after it; the provider-neutral Google fallback honours
  crossModelFallback: false
- claude-haiku-5-5 is priced tier-aware (>100K prompt tokens: $0.50/$2.50) in every cost and
  reservation path; claude-opus-4-7 gets pricingAsOf and an agreement row
- Sonnet/Opus 5.5 send strict tool definitions where a forced tool_choice becomes auto;
  capture reasoner and provider parser send the explicit effort
- .env.example copies of the changed defaults now match the code (and a test pins them)
- request-shape tests for the TTS route, private stream route, provider parser default,
  judge scripts; ladder budget tests (b) and (e) follow the specified scenarios

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- long-context price tier is applied per request only (priceRequest / convexPriceRequest in
  reserveDispatch, meter settlement, runtime priceStep, calculateRequestCost,
  exactConvexUsageCost); summed usage is priced at base-tier rates (priceAggregateTokens) and
  the ladder sums settled dispatch costs; haiku-5-5 long-context tier gets cachedInputPer1M 0.05
- tests that fail on revert for the five DEFAULT_AGENT_MODEL call sites, every remaining judge,
  benchmark and smoke script through a fake transport, and both AGENT_MODEL fallbacks in qaMatrix
- providerParserLive env-precedence test uses a non-default env id again
- LangChain bridge passes strict: true on the forced tool_choice downgrade
- probe.ts and providerParseSmoke.ts send the explicit claude effort; providerParseSmoke
  compares the provider to "gemini" so the Gemini smoke reaches Google
- ladder pre-flight request ceiling and estimate multiply free-auto by OPENROUTER_FREE_AUTO_LIMIT
- pricing-source inventory lists run.ts (OpenRouter snapshot first) and the proxy sweep

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- SPEC A.0.10: resolveModelAlias throws UnknownModelError for an id that is not priced, not
  declared by the alias table and not free-looking, so adapter model() and convexModel() (and
  the fallback lists they read) refuse it before any request; an unknown -free / :free id
  resolves to openrouter/free-auto. Pricing and telemetry readers use lookupModelAlias
- a free primary no longer falls back to a paid model: adapter fallbackModelFor and convexModel
  drop the paid candidate unless allowPaidFallback is set and a reservation exists (SpendMeter /
  finite maxCostUsd)
- ladder runRung reports the sum of its settled dispatch costs when a rung fails after charged
  dispatches (was hardcoded 0)
- ladder request ceiling and estimate resolve the route alias before the free-auto fan-out test
- probe.ts gpt-6.1-sol is asserted on the wire (URL, model, no sampling)
- benchmark/run.ts pricing moved to pricingAtRun.ts; the pricing agreement test runs it and the
  --dry-run sweep on a dated OpenRouter /models fixture
- tests that dispatched never-heard-of ids now assert the refusal; their invariants stay on the
  alias-declared unpriced id claude-sonnet-4.5

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- resolver: an id the repo ships or freezes by name is known without a price row (mobile picker,
  SUPPORTED_MODEL_ROUTES, frozen BTB default gpt-4.1-mini, official judges); every concrete :free id
  the repo or the spec's 2026-10-10 live list names resolves as itself, never to openrouter/free-auto.
  Only truly unknown ids throw; only unknown :free ids reroute. tests/modelKnownIds.test.ts reads the
  owning lists, so a new id added there without a catalog entry fails.
- public boundary: defaultModelPolicyForRoute refuses a modelPolicy the catalog does not know
  (unknown_model_policy) for createOrReuse, start and startPublicAsk; fixtures that used placeholder
  strings now name catalog ids.
- nodeagent/free-auto: the paid direct-Gemini leg needs allowPaidFallback and a meter (same guard as
  fallbackModelFor); NODEAGENT_FREE_AUTO_GOOGLE_MODEL precedence unchanged.
- pi-ai default is asserted on the wire through createPiAiAdapter().next().
- claude-opus-4-7 loses the unsourced cachedInputPer1M 0.50; the agreement test now pins every
  sourced cache rate and the absent one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- resolver (D1): REPO_DECLARED_UNPRICED_IDS now holds every OPENROUTER_TOP_PAID_AGENT_ROUTES route and
  every SPEC 1.2 wire id on every transport (9 of 19 top-paid routes and 18 of 32 section 1.2 ids threw).
  tests/modelKnownIds.test.ts reads resolveRouteSet for all six route sets, allAgentLlmRoutes() and the
  section 1.2 list.
- operator pass-through (D1): model(id, { operatorSelected: true }) keeps an operator- or live-list-selected
  id as written; set only by benchmark/run.ts, proofloop-free-model-gauge, ladder --real, spreadsheetbench-run
  and chatIntakeRuntime. Server code stays fail-closed.
- PlanPreview-blocked runRoomAgent records again: the not_started marker is accepted only with
  initialStatus blocked; a convex-test drives the branch.
- streamPrivateReplyText and aiSdkReasoner resolve the id first (one guard each); tests assert 0 fetches
  for unknown ids.
- tests/modelRefreshAgentWire.test.ts drives runRoomAgent, runPrivateAgent and runFreeAutoJobSlice through
  convex-test with a fake fetch: emitted model gemini-3.8-flash, no sampling keys.
- D2: top_paid default is nvidia/nemotron-3.5-super-vl:free (newest qualifying :free row of the public
  OpenRouter list, fetched 2026-10-16T11:32:04Z, docs/eval/openrouter-free-models-2026-10-16.json);
  live-unverified (no key, Homen 2026-10-16).
- .env.example: the direct Gemini leg is documented as opt-in; .tours citation line follows agentJobs.ts.
- CHANGELOG/qa/model-refresh-noderoom.md: round 5 entry with the REA/ORX record (D5).
- Not run (paid): the L1-L4 ladder receipt; tests/qaMatrix.test.ts stays red on purpose.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- operator pass-through (D1, A.0.4): operatorSelected is now set at every CLI entry point that takes an
  operator id, not five of them: financeModelLive, managedLockPerf, professionalRuntimeLive,
  halo-live-path-calibration, sfn-nonbtb-nodeagent-runner, bankerToolBenchNodeAgentGeneral and
  startup-diligence-provider-eval. convexModel() gains the same option (ConvexModelOptions.operatorSelected);
  normalizeFallbackModelIds no longer re-resolves a primary its caller already resolved. The source pin in
  tests/modelKnownIds.test.ts now scans every resolver call in twelve entry points (multi-line calls included).
- free-looking unknown ids (A.0.10): aiSdkReasoner and streamPrivateReplyText dispatch the RESOLVED id.
  Streaming sends openrouter/free-auto like any known free-auto input; the capture seam serves only
  Anthropic/OpenAI and refuses (capture_reasoner_route_unsupported). Tests: 0 raw-name requests.
- public boundary (A.0.10): defaultModelPolicyForRoute passes the AGENT_ORCHESTRATOR_MODEL / AGENT_MODEL /
  AGENT_TOP_PAID_MODEL / AGENT_RESEARCH_MODEL defaults, and the file-egress promotion passes
  configuredFileEgressModel(), through knownModelPolicy. Override precedence unchanged. 8 new
  convex-test cases fail without the change.
- compatibility (A.0.5): claude-opus-4.1 and claude-haiku-3.5 alias to their hyphenated wire ids (price rows
  added at the same prices); openrouter/auto, the benchmark-lane label, is a declared known id again.
- A.0.6: the gemini-3.6-flash contextWindow cites the public OpenRouter list (read 2026-10-16).
- .tours citation moves agentJobs.ts:2798 -> :2799; CHANGELOG/qa/model-refresh-noderoom.md round 6 entry.
- Not run (paid): the L1-L4 ladder receipt; tests/qaMatrix.test.ts stays red on purpose.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- startup-diligence eval: the contract job always carries the catalog free route, so an operator-picked
  model the catalog does not know is the only request and the reported model (was: unknown_model_policy,
  second paid request, wrong model reported).
- adapter.judge(): operatorSelected option (D1/A.0.4) passed by professionalLiveCatalog and benchmark/run.
- streamingModel: refuse openrouter/free-auto (and nodeagent/free-auto) before any request; it is not an
  OpenRouter model id. The test asserted the invalid wire value and now asserts the refusal.
- pricing agreement test exercises proofloopModelTracking (source 6) and the retired opus-4-1 / haiku-3-5 rows.
- provider parser tests assert no forced tool_choice, native output_config.format and max_tokens on the
  Anthropic generateObject path.
- Not run (paid): the L1-L4 ladder receipt; tests/qaMatrix.test.ts stays red on purpose.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A.0.6: the two rows the PR added (claude-opus-4-1, claude-haiku-3-5) carry
  pricingAsOf "2026-04", the month of origin/main's "official docs, April 2026"
  block they were copied from; the agreement test now asserts the date instead
  of "undated".
- A.0.3 receipt honesty: SpendMeter counts requests past MAX_DISPATCH_RECORDS in
  droppedDispatches; the ladder report carries budget.dispatchesDropped and takes
  rung cost from the meter's spentUsd delta, not from the capped row array.
  Test: 4,097 fake requests give 4,096 rows, droppedDispatches 1, requests 4,097.
- A.0.6: request-shape test sends the top_paid default
  (nvidia/nemotron-3.5-super-vl:free) through convexModel: model as written,
  tool_choice required, no sampling keys.
- Not changed, escalated: free-to-paid file-egress promotion (needs a Homen
  decision) and the Top paid UI copy; the paid ladder receipt and the TTS listen
  check remain merge blockers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A.2.9: remove the fallback-chain keys for gemini-3.8-flash (DEFAULT_AGENT_MODEL),
  gemini-3.6-flash and gemini-3.5-flash-lite. Spec authorizes chains for the 5.5
  ids only; a chain on the default route sent room content to OpenRouter/OpenAI
  on a failed default turn, which origin/main never did. Test now pins the three
  ids chain-less and restricts the "has a chain" assertion to the 5.5 ids.
- Chat.tsx Top paid copy no longer promises a paid model: it names the operator
  env vars and says the server default is free when neither is set.
- evals/ladder.ts: droppedDispatches > 0 exits 1 like stoppedBy (incomplete rows).
- Heavy cases (ladderBudget (e), modelCostClass 4,097-request) get a 30s per-test
  timeout; no assertion changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 1957b987 Deployed Oct 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-live-eval Merge blocked until a live receipt or manual check exists (SPEC A.0.3). A block, not a note.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant