Repository navigation
Conversation
…, request shapes and fail-closed resolver No wire-id moves except the free lane registration (Codex v3 N2). Makes the later id moves (PR 2) testable and keeps an unknown model id from ever becoming a paid default. - Anthropic request shapes: delete temperature (events.ts x3, modelRouter, benchmark judge, mcp-local llmTools/researchWritingTools), max_tokens 700/260/300 -> 2048, extendedThinking budget_tokens -> adaptive + output_config.effort, text read by block type in the 16 generateWithProvider copies, explicit ANTHROPIC_WIRE_IDS table instead of dotted aliases on the wire. - Gemini: thinkingBudget:0 -> thinkingLevel "low" (triSearch, judge script). - Resolver fails closed (A.0.10): unknown free-looking -> FREE_LANE_MODEL, unknown otherwise -> UnknownModelError unless allowPaidFallback === true; pipeline launch boundary asserts before admission; parseModelKey no longer forwards arbitrary ids. - FREE_LANE_MODEL "nemotron-3-super-free" registered (approved alias, spec, UI row, legacy alias for the wire id, zero-price row); web mirror gets a drift test. - Eval harness: runner --tier/--seeds, real Messages call in agentInvoker, run-comprehensive-eval aborts on unknown --models and records resolvedModelId. - Nine request-shape / boundary tests, added to the CI runtime-smoke list. Spec: model-defaults SPEC Part A s.A.2 (NodeBenchAI) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR size advisoryThis PR adds 1618 lines of substantive change. CONTRIBUTING.md defines a soft limit of ~400 LOC. If the PR is genuinely cohesive (e.g. an architecture map, a generated migration, a deletion of a dead module), no action is needed. Otherwise consider:
This is advisory — it does not block the merge. |
✅ Dogfood Visual QA Gate — capture integrity: PASSEDPublic guest captures only. A ready entry or enabled composer is not a completed live answer or reopened saved report. This gate checks artifact files and observed metadata; it does not grade pixels, keyboard/tablet coverage, or production deployment.
Unperformed actions:
ArtifactsDownload the Generated by Dogfood Capture Integrity Gate |
…, make the eval honest (PR 1 review round 1) Fixes the defects from the round 1 review of PR #662. Free requests stay free - FREE_LANE_MODEL heads the free segment of MODEL_PRIORITY_ORDER (resolver and web mirror). executeWithModelFallback, getFallbackChain and getLanguageModelWithFallback throw for a start model outside the order instead of restarting at kimi-k2.6, which dispatched a paid model for an auto-free run and reported isFree=false. piRuntime prices the fallback from the model that answered instead of a hardcoded 0. - normalizeNodeBenchRuntimeModel, the first resolver on the FastAgent entry points, follows resolveOrFailClosed: unknown throws, free-looking goes to the free lane, a known free model keeps its cost class in every profile. runtimeRouting stops swallowing an unknown requested id and never appends paid candidates to a free primary. continueThread rethrows UnknownModelError. getModelWithFailover no longer turns an unknown id into gpt-5.4-mini or fails a free model over to a paid provider. The free tier of modelRouter no longer appends the cheap list. Eval honesty - runner.ts exits 1 when a real-backend run has an error (stub smoke stays 0). - agentInvoker records tool_use blocks as requests, not successful calls; an unfinished tool_use turn or a response without a model is an error. - run-comprehensive-eval requires a canonical --models id, treats a row with no resolved identity as a violation, and keeps identity on error rows. - Extended-thinking runs are charged from usage.output_tokens minus the answer because the thinking text is omitted by default on Sonnet 5.x / Opus 4.7+; the max_tokens claim is corrected (it bounds thinking plus answer). Tests (A.0.6): request-shape and boundary tests through the real callers for modelRouter, benchmark, mcp-local, generate-tool, advanceRun, the 13 provider copies, continueThread and runtimeRouting; the web auto-free literal is pinned to the backend route resolver; the Gemini rerank test checks the model in the URL. All added to the CI runtime-smoke list. generate-tool.mjs now exports its own buildClaudeRequest. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Eval honesty: only stop_reason end_turn is a clean run (max_tokens, pause_turn and a missing stop_reason are errors), and the response read is bounded by content-length and by a capped stream instead of buffering the whole body. Cost class (A.0.10): the priority-index throw applies to unlisted FREE start models only, known paid ids outside MODEL_PRIORITY_ORDER keep main's behaviour; every executor branch in runtimeRouting honours an explicit free request; modelRouter caps tier "free" at free and prices the free lane at zero; getModelWithFailover returns the free lane's wire id, takes free route aliases, and no longer lets a gpt-/claude-/gemini- prefix make an invented id fail over to a paid provider; instagramClaimVerification fails instead of building a paid model. Request shape: condenseQuery in triSearch uses thinkingLevel low like the rerank call on the same model. Tests: boundary test now fills the launch window so it can see the order, request-shape tests assert the outgoing model string and top_p/top_k. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cost class (A.0.10): - selectTierEligibleRuntimeModel no longer appends the paid tier-safe fallbacks or the billing gate's paid suggestion for a free primary (requireFree, default = primary is free); on the anonymous and free tiers a free-lane request now fails with the tier reason instead of being answered by gpt-5.4-nano / gpt-5.4-mini. The post-stream fallback passes requireFree for a free active model. - getModelWithFailover throws for any id that is neither in the catalog nor a known wire id, before provider availability is checked; dailyBriefWorker.runNextTask and dailyBriefPersonalOverlay.runNextPersonalTask validate model with normalizeModelInput. - executeWithModelFallback, getFallbackChain and getLanguageModelWithFallback resolve the start model through resolveOrFailClosed first (free-looking or the free-lane wire id -> FREE_LANE_MODEL, other unknown -> UnknownModelError) instead of a TypeError on pricing. - modelRouter.route resolves the candidate once and uses the resolved alias for dispatch, pricing, isFree, actualTier, cache and the audit row. Request shape: Gemini sampling (temperature) removed from both triSearch calls, the benchmark Gemini judge and judgeScratchnodeChatVideo.mjs; the benchmark test no longer pins temperature 0 for Gemini and every changed builder asserts the keys are absent. Tests: failing-before/passing-after for each defect; two new files added to the CI runtime-smoke list (47 files, 619 tests pass at this head). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A.0.4 (PR 1 moves no ids): - dailyBriefWorker.runNextTask / dailyBriefPersonalOverlay.runNextPersonalTask validate model with normalizeModelInput and forward the caller's string unchanged; round 3 forwarded the resolver's lossy alias (claude-sonnet-4-5-20250929 -> retired claude-sonnet-4, gpt-4o -> gpt-5.4). - modelCatalog: LEGACY_PASSTHROUGH_IDS (16 resolver-recognised ids main sent unchanged: 3 dated Claude snapshots, 10 gpt-*, 3 gemini-*) count as known and pass through; anthropicWireId returns the Claude snapshots, so extendedThinkingRunner accepts claude-haiku-4-5-20251001. Dotted claude-*-4.x and gpt-oss* stay rejected. - extendedThinkingRunner: adaptive thinking + output_config.effort only for claude-sonnet-4-6 / claude-opus-4-7; every other id keeps manual budget_tokens with no effort (Haiku 4.5 rejects effort); under 1,024 tokens no thinking block. Cost class (A.0.10): - modelRouter: free is the resolved model's cost class (isFreeAlias), not a row in a partial price table; 15 of 19 approved free aliases were billed $1/$5, recorded premium, and used up checkAgentBudget. - requestExtendedRun resolves the model (resolveCheckpointModel) before inserting a run. Tests: dailyBriefModelBoundary (16 ids byte-for-byte), anthropicWireIds (pass-through + drift guard), extendedThinking advanceRun for Haiku 4.5 and the request boundary, modelRouter x 19 free aliases through the real checkAgentBudget (15 fail on the previous source), and every request-shape suite now asserts the outgoing conversation ends on a user turn (a terminal assistant/model turn added to each source made each suite fail). CI runtime-smoke list: 47 files, 660 tests pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What this is
PR 1 of 3 for NodeBenchAI from the model-defaults spec. It moves no model id, except registering the one verified free lane. It fixes request shapes that would answer 400 on Claude 4.7+/5.x and Gemini 3.x, makes an unknown model id fail instead of silently becoming a paid default, and wires the migration eval so later receipts can name a candidate. PR 2 (
model-refresh/nodebench-ids, the id moves) and PR 3 (release/nodebench-mcp-3.3.0) are deferred until this PR is merged.Plain-language version: today, if a caller asks for a model name the code does not recognise, it quietly runs the expensive default model and bills for it. A pipeline run could do that without any spending check. After this PR an unknown name is rejected at the places listed under "Fail-closed" below (a name ending in
-freegoes to the one free model instead), and a request that starts on a free model stays on free models when it falls back. The places where that is not yet true are listed under "Where the fail-closed claim stops". Separately, several places send settings (temperature, a thinking budget) that the newer Claude and Gemini models refuse, so the first request after a model upgrade would fail; those are fixed first so PR 2 can be tested.Live traffic note: this PR is where
auto-freeandgetBestFreeModel()actually move fromlaguna-s-2.1-freetonemotron-3-super-free, so free-lane requests change model here, not in PR 2. The free lane has no live measurement in this repo (the pick is NodeVideo's benchmark), so this PR carriesneeds-live-eval. A free-lane receipt costs $0; laguna expires on 2026-10-31.Request-shape fixes (Breaking parameters)
backend/convex/events.ts(3 raw Anthropic fetches)temperaturedeleted;max_tokens700/260/300 -> 2048 (thinking counts toward it); text read by block typedomains/product/extendedThinking.ts+workers/node/pipeline/extendedThinkingRunner.tsthinking:{enabled,budget_tokens}->{type:"adaptive"}+output_config.effort:"medium"forclaude-sonnet-4-6andclaude-opus-4-7only; every other id keeps main's manual{type:"enabled", budget_tokens}with noeffort(round 4: Claude Haiku 4.5 supports only manual thinking and is not on the effort compatibility list; an allowance under the API minimum of 1,024 sends nothinkingblock);max_tokens= answer cap + remaining thinking allowance. That bounds thinking plus answer per checkpoint, not thinking alone (adaptive thinking has no separate cap), so a run spends at mostthinkingBudgetTokens + MAX_OUTPUT_TOKENSon thinking, at checkpoint granularity. The run is charged fromusage.output_tokensminus the answer (estimateThinkingTokens), because on Sonnet 5.x / Opus 4.7+ the thinking text is omitted by default and the old text-length estimate was always 0, so the budget would never run out once PR 2 moves the id; the body is built bybuildCheckpointRequestBody; model is the wire id (claude-sonnet-4-6), not the stored dotted aliasshared/llm/modelCatalog.tsANTHROPIC_WIRE_IDStable +anthropicWireId()(no regex/string surgery) +LEGACY_PASSTHROUGH_IDS(round 4: the 16 resolver-recognised ids main sent unchanged);getModelWithFailoverreturns wire ids for Anthropic;anthropicText()reads blocks by type. A test pins the table to the resolver'sMODEL_SPECS(Convex cannot import it fromshared/)generateWithProvidercopies (ai.ts,morningDigest,fastAgentChat,entityInsights,dealFlow,dailyBriefPersonalOverlay,dailyBriefWorker,modelComparison,paperDetails,repoScout,stackImpact,strategyMetrics,postDedupAction,claimVerificationAction,tags_actions,benchmark.ts)content[0]->anthropicText(content). Each copy's own function is now driven byproviderCopies.requestShape.test.ts(15 cases;benchmark.tsbybenchmark.requestShape.test.ts), so reverting one copy fails a test. Thirteen of them gained anexportkeyword as the test seam; they are not Convex functionsai/models/modelRouter.ts,search/fusion/benchmark.ts,packages/mcp-local/.../llmTools.ts,researchWritingTools.tstemperaturenot sent (modelRouter: kept for non-Anthropic/non-Google candidates). Tests drive the real handlers:modelRouter.requestShape.test.ts(route action, fakegenerateText),benchmark.requestShape.test.ts(judge fetch per provider),anthropicRequestShape.test.ts(mcp-local, fake SDK)shared/search/triSearch.ts(rerank andcondenseQuery),scripts/ui/judgeScratchnodeChatVideo.mjs,search/fusion/benchmark.ts(Gemini judge)thinkingBudget:0->thinkingLevel:"low"; round 3:generationConfig.temperature(0, 0, 0.2, 0) removed too, because sampling is deprecated on Gemini 3.x (ignored today, a 400 later; spec s1.3). Absence is asserted through the real builders (triSearch.test.tsfor both calls,benchmark.requestShape.test.ts, the newjudgeScratchnodeChatVideo.requestShape.test.tswhich runs the script against a fake@google/genaiclient). The benchmark test used to pintemperature === 0for the Gemini judge; it now asserts the opposite and the OpenAI judge keeps its determinism pin. Gemini sampling at the ~30 other call sites this PR does not rewrite (workers/node/routes/search.ts,deepDiligence.ts,runDogfoodGeminiQa.mjs, ...) stays for PR 2 with their id movesscripts/tool-gen/analyze-gap.mjs,generate-tool.mjsbuildClaudeRequest(analyze-gap:claude-haiku-4-5-20251001, 2048; generate-tool:claude-sonnet-4-6, 4096), no sampling keys; text read by block type throughclaudeText(generate-tool imports it from analyze-gap). In round 0 this row said generate-tool usedbuildClaudeRequest; it did not (it built its body inline). It does now, andgenerate-tool.mjshas its own request-shape testFail-closed resolver (A.0.10) and free lane (Codex v3 N2)
getLanguageModelSafe(input, opts?),normalizeModelInput, and now alsonormalizeNodeBenchRuntimeModel(the first resolver on the FastAgent public entry pointscreateThread,createThreadForUserInternal,initiateAsyncStreaming,streamAsync, andruntimeRouting.tsfor signed-in callers; an anonymous caller's model is replaced by the pinned lane before any resolver runs, see "Where the fail-closed claim stops" item 7) share one rule (resolveOrFailClosed): unknown free-looking input (-free,:free, free route alias) ->FREE_LANE_MODEL; any other unknown input throwsUnknownModelError;opts.allowPaidFallback === true(exactly) ->DEFAULT_MODEL, counted bygetPaidFallbackCount(). No call site passes the flag in this PR. Empty input still takes the profile default. A known free model keeps its cost class in every profile (it used to be replaced by paidkimi-k2.6/gemini-3.1-flash-lite-preview/gemini-3.1-pro-preview); a known paid model the profile does not allow still takes the profile default.runtimeRouting.ts:resolveRequestedExecutorModelno longer swallows an unknown requested id (it throws or goes to the free lane);chooseAvailableRouteModelsdoes not append paid candidates to a free primary. Round 2: the anonymous,context_rot_guard,tool_heavy_executor,structured_executorandlarge_context_executorbranches never readrequestedModel, so a user who picked the free lane and typed "patch this script" was routed togemini-3-flash-preview. Every executor branch now starts from the explicitly requested free model (resolveRequestedFreeModel), and a request that names no free model keeps each branch's paid default unchanged.agentChatActions.continueThreadrethrowsUnknownModelErrorfrom the stored thread model instead of logging and continuing onDEFAULT_MODEL.shared/llm/modelCatalog.tsgetModelWithFailover(reached fromai.tsand the othergenerateWithProvidercopies): a free-looking unknown id (suffix-free/:free, or the free route aliasesfree,auto-free,nodebench:auto-free,kilo-auto/free,openrouter/free) -> the free lane on OpenRouter as the wire idnvidia/nemotron-3-super-120b-a12b:free, never the alias (round 1 returned the alias, which every caller hands straight to a provider; spec rule (b)); the free lane's own catalog id is converted the same way; any other unknown id throws (it used to becomegpt-5.4-mini); a free catalog model whose provider is not configured throws instead of failing over to a paid provider. Round 2: agpt-/claude-/gemini-prefix no longer makes an invented id "known":gpt-not-realwith only Gemini configured used to fail over to paidgemini-3.1-flash-lite-preview. Round 3: round 2 still passed a prefix-shaped unknown id to its own provider when that provider was configured (gpt-not-realandgpt-5.5-proreturned{provider:"openai", isFallback:false}withOPENAI_API_KEYset). That was wrong: it is reachable from a public action (next bullets) and the spec requires an unknown id to throw.getModelWithFailovernow throws for every id that is neither in the catalog nor a known wire id (ANTHROPIC_WIRE_IDSvalues; for OpenAI and Gemini the resolversdkIdequals the catalog name, pinned byanthropicWireIds.test.ts), before any provider-availability check. The old "operator-supplied wire id" justification does not hold:getLlmModelhas no env override, and the one test that pinnedclaude-haiku-4-5-20251001passing through was changed to assert a throw. Round 4 reverses that part: the resolver still recognises that id (and 15 others) and main sent them unchanged, so rejecting them broke callers that passmodelto a public action; they pass through again (LEGACY_PASSTHROUGH_IDS, see "Round 4 review fixes"). A prefix-shaped id the resolver does not recognise still throws.FREE_LANE_CATALOG_IDand the newFREE_LANE_WIRE_IDare pinned to the resolver (MODEL_SPECS[FREE_LANE_MODEL].sdkId) byanthropicWireIds.test.ts(shared/cannot import frombackend/convex, so the wire string exists twice and the test is what keeps it one).ai/models/modelRouter.ts: the static fallback list for the free tier no longer appendsTIER_MODELS.cheap. Round 2:route()now caps atier: "free"request at free after the chain is built (a candidate must resolve to a free approved model). Before,requireVision: trueputz-ai/glm-4.7-flashin the free chain and it was dispatched and reportedisFree: false, actualTier: cheap. If no free model qualifies the call fails withAll 0 models failedinstead of billing.chainResolveritself is unchanged (tierFlooris still a floor for the other tiers), andpinnedModelis untouched (an explicit pin is what the caller asked for). Round 4 replaced the zeroMODEL_PRICINGrow fornemotron-3-super-freewith the general fix:calculateCost,isFreeand the cache path now use the resolved model's cost class (isFreeAlias), see the next bullets (the paid ids' prices still land in PR 2).FREE_LANE_MODELwas missing fromMODEL_PRIORITY_ORDER, soexecuteWithModelFallback({ startModel: "nemotron-3-super-free" })restarted at index 0 and dispatched paidkimi-k2.6while reportingisFree: false(reached frompiRuntime's AI-SDK fallback on anauto-freerun when the free lane answered 401/404/429/5xx). It is now the head of the free segment (resolver and web mirror; the drift test enforces the mirror), andexecuteWithModelFallback,getFallbackChainandgetLanguageModelWithFallbackthrow for an unlisted FREE start model instead of restarting at the paid head (round 2: an approved paid id outside the order, such asglm-4.7-flash,deepseek-r1,gemini-2.5-*,claude-opus-4, is back to main's behaviour: it starts atkimi-k2.6; changing that is a caller-argument decision, see C item c). A test pins that every free id sits after every paid id, and that a chain started onFREE_LANE_MODELorlaguna-s-2.1-freenever reaches a paid model, even when every free model fails.piRuntime's fallback result also stopped reportingestimatedUsd: 0regardless of the model that answered; it is priced from the model used.selectTierEligibleRuntimeModel(used bystreamAsyncand by the post-stream fallback infastAgentPanelStreaming) always appended the paidTIER_SAFE_RUNTIME_FALLBACKSand queued the paidsuggestedModelfromcheckRequestAccess. On the free billing tier (noopenrouterprovider intierLimits,shared/llm/modelCatalog.ts; the helper is exercised for the anonymous tier too, but an anonymous caller's model is replaced earlier, item 7 below) a user who pickedauto-freewas therefore answered bygpt-5.4-nano/gpt-5.4-mini, with no reservation on the anonymous path. The same request already turned paid one step earlier onorigin/main, so this is not new, but it is an A.0.10 violation and round 2's "the A.0.10 exception" was wrong. Fix: the helper takesrequireFree(default: the primary is free). When set it enqueues only free candidates: no tier-safe list, no paid suggestion, and the request fails with the tier's own reason. The post-stream fallback passesrequireFree: isFreeModel(activeModel), so a failed free run cannot continue on a paid model either. Tests drive the real selection with the realisModelAllowedForTier/getBestModelForTierfor the anonymous, free and pro tiers; a paid primary keeps its fallbacks. Consequence to know: a signed-in free-tier user who explicitly picks the free lane now gets the tier refusal instead of a paid answer. An anonymous user does not get that refusal:resolveFastAgentRequestedModelreplaces the client's model with the pinned paidgemini-3.1-flash-lite-previewbefore any resolver or this helper runs (fastAgentPanelStreaming.tscreateThreadandinitiateAsyncStreaming), so an anonymous free request runs on that paid model, and an unknown id is accepted silently there. That is the spec-kept bounded anonymous lane (item 7 below), not changed in this PR (an earlier version of this body said the opposite).model.dailyBriefWorker.runNextTaskanddailyBriefPersonalOverlay.runNextPersonalTasktookmodel: v.string()and passed it throughexecuteFeature->generateWithProvider->getModelWithFailover-> the provider SDK. They now callnormalizeModelInputfirst, to validate only: an unknown id throwsUnknownModelErrorbefore any internal action runs; a known or free-looking id is forwarded exactly as the caller wrote it (round 3 forwarded the resolver's alias, which turnedclaude-sonnet-4-5-20250929into the retiredclaude-sonnet-4; fixed in round 4), and the catalog then sends a free-looking id to the free lane; an omitted model stays omitted (dailyBriefModelBoundary.test.ts, in the CI list). The internal actions keep trusting their callers (cron, other internals).executeWithModelFallback,getFallbackChainandgetLanguageModelWithFallbackcalledisFreeModelon the raw start string, soretired-model-free,not-a-modeland theLEGACY_ALIASESwire idnvidia/nemotron-3-super-120b-a12b:freedied withCannot read properties of undefined (reading 'pricing')before the callback ran (reachable fromdocuments.pdfInsights.generatePDFInsights, which castsargs.model). They now resolve throughresolveOrFailClosedfirst: a free-looking id or the wire id runs onFREE_LANE_MODEL, any other unknown id throwsUnknownModelError, an omitted model is stillDEFAULT_MODEL. The "unlisted free model throws" and "paid unlisted keeps main's behaviour" rules are unchanged.generatePDFInsightscatches the error and returnssuccess: falsewith the message.modelRouter.routerecords what it dispatched. It priced and classified the raw candidate string, so a pinnedauto-free, the free-lane wire id or an unknown-freeid was dispatched to the free lane but recordedcostUsd > 0,isFree: false,actualTier: "premium". It now resolves the candidate once (normalizeModelInput) and uses the resolved alias for dispatch, pricing,isFree,actualTier, the cache entry and themodelRouterCallsrow. Round 4 correction: an earlier version of this body said "I found no budget reader of these rows". That was wrong.modelRouterQueries.checkAgentBudgetsums today'smodelRouterCalls.costUsdinto the agent's daily dollar budget, androute()calls it before every call that passes anagentId. The partialMODEL_PRICINGtable had rows for only 4 of the 19 approved free aliases, so the other 15 (laguna-s-2.1-free,laguna-xs-2.1-free, and 13 more) were priced at the $1/$5 default, recordedisFree: false/actualTier: "premium", and used the budget up: 7 laguna calls at 1,000 in / 100 out exhausted a $0.01 budget and call 8 was refused. That is pre-existing onmain, but it is an A.0.10 cost-class defect, so it is fixed here (round 4 section).resolvePipelineModelSelection:resolvedModelId: resolved ?? DEFAULT_MODELdeleted.startPipelineRunandstartPipelineRunInternalcallassertKnownPipelineModel(args.modelId)beforereservePipelineLaunchAdmission.parseModelKey(piRuntime.ts) only falls through to the raw parser when the resolver module fails to load;UnknownModelErrorpropagates.FREE_LANE_MODEL = "nemotron-3-super-free"registered:APPROVED_MODELS,MODEL_SPECS(sdkIdnvidia/nemotron-3-super-120b-a12b:free, zero price soisFreeModelis true),LEGACY_ALIASESfor the wire id, webAPPROVED_MODELS+MODEL_UI_INFO, and the zero-price row inshared/llm/modelCatalog.ts.auto-freeandgetBestFreeModel()return it (the laguna lanes expire on OpenRouter 2026-10-31).TIER_MODELS.freedrops the three unapproved names that used to route to paidkimi-k2.6. The webauto-freeliteral is now pinned to the backend route resolver by the drift test (revertinggetBestFreeModelto laguna used to fail onlymodelResolver.test.ts).apps/web/src/shared/llm/approvedModels.tsmirrors the resolver again (priority order, lanes, and the free-keeps-cost-class rule) andapprovedModels.drift.test.tskeeps it that way.wikiDreamingGraph.tspasses (google:models/gemini-3-flash-preview,google:models/gemini-3.1-pro-preview) were unknown and silently ran on the default; failing closed would break that job, so they now resolve to the Gemini ids they name (see discrepancies).Where the fail-closed claim stops (stated, not hidden)
Decision needed (C item c), narrowed in round 2: a fallback helper (
executeWithModelFallback,getFallbackChain,getLanguageModelWithFallback) called with an approved paid model that is not inMODEL_PRIORITY_ORDER(e.g.glm-4.7-flash,deepseek-r1,gemini-2.5-*,mistral-large,claude-opus-4) behaves as onmain: it restarts atkimi-k2.6. Round 1 made all of them throw, which changed caller arguments outside A.0.10 (callers:pdfInsightswith a user-suppliedargs.model,distiller,competingExplanations,diligenceCheckpointStructuring,piRuntime's AI-SDK fallback, where thepublic-researchroute isglm-4.7-flash). Only an unlisted free start model throws, because restarting it at the paid head is the A.0.10 violation. Pinned bymodelResolver.test.ts("keeps main's behaviour", 5 ids). Whether to list those ids in the order, or make the helpers refuse them, is yours.Decision needed (C item a), narrowed in round 2:
chainResolver.resolveChainstill treatstierFlooras a floor ("this tier or above") for every tier. The A.0.10 gap is closed one layer up instead:route()filters atier: "free"candidate list to free models (see themodelRouter.tsbullet), so a free request can no longer be dispatched to, or billed as, a paid model. The side effect is deliberate: a free request whose capability requirement (for examplerequireVision) only a paid model meets now fails instead of upgrading silently. If you wantfreeto mean "free or better" for those callers, that is a product decision, not a fix.Round 3: closed, not an exception. The billing-tier-safe fallbacks (
TIER_SAFE_RUNTIME_FALLBACKS, paid) are now appended only for a paid primary; a free primary fails with the tier reason (see the round 3 bullet above). Round 2 called this path "the A.0.10 exception" because a reservation exists; that was false: there is noallowPaidFallback: trueanywhere, the anonymous path holds no reservation, and a reservation alone is not the spec's exception.continueThreadstill falls back toDEFAULT_MODELwhen the read of the thread fails (the lane is unknown then); only an unknown stored model is rethrown.The web mirror's
normalizeNodeBenchRuntimeModelonly knows approved ids (it is a selector, it dispatches nothing); it keeps returning the profile default for other strings. Spec C.6 (fastAgentPanelStreaming.ts:1193) is unchanged: the site only receives ids already normalized by the resolvers above.getModelWithFailovercallers have no OpenRouter branch. All 16generateWithProvidercopies,fastAgentChatandai.tshandleopenai,anthropicandgemini; any other provider falls tonew OpenAI().chat.completions.create({ model }). Since round 2 the free lane reaches that client as the wire id, not the alias, so it cannot silently run a paid model, but withOPENAI_BASE_URLunset the request goes to OpenAI and fails (404) rather than running on OpenRouter. The same is true today forkimi-k2.6and every other OpenRouter model. Adding the OpenRouter branch is a change to all 18 call sites and belongs with PR 2's rows for those files; it is not done here. Other free catalog aliases (qwen3-coder-free, ...) still leavegetModelWithFailoveras aliases; PR 2 prunes them to the live:freelist.Round 3: closed. Round 2 listed unrecognised prefix-shaped ids (
gpt-not-realwithOPENAI_API_KEYset) as still reaching their own provider, justified as "operator-supplied wire ids (A.0.4)". That did not hold for the user-suppliedmodelargument of the two daily-brief actions, andgetLlmModelhas no env override.getModelWithFailovernow throws for them and both actions validate at the boundary (see the round 3 bullets). Unknown ids no longer reach a provider through this path.Anonymous bounded lane (spec-kept exception; round 4 correction). For an anonymous caller
shared/llm/fastAgentRuntimeContract.ts:19(resolveFastAgentRequestedModel) replaces the client'smodelwith the paidgemini-3.1-flash-lite-previewbeforenormalizeNodeBenchRuntimeModelruns (fastAgentPanelStreaming.tscreateThreadandinitiateAsyncStreaming). Consequences: an anonymous user who picks the free lane is answered by that paid model (this is the spec's bounded anonymous lane, bounded by the anonymous tier limits, and the spec moves only its id, in PR 2: rowfastAgentRuntimeContract.ts:1), and an unknownmodelid is accepted silently on that path instead of throwing. This PR leaves it as is. The optional hardening (validateargs.modelbefore the override so an unknown id throws) is not done; it would change what an anonymous client sees and is a decision for you (Still open).Eval harness (so the declared migration eval is real)
scripts/eval-harness/runner.ts:--tier <prefix>,--seeds <N>; hard-codedclaude-sonnet-4-6/modelVersionremoved (config.model=EVAL_MODEL, required whenEVAL_BACKENDis notstub);createStubTelemetryreplaced byinvokeAgent;RunTelemetry.echoedModeladded. Thestubbackend is still the default, soconvex-mcp-eval-gate.yml's--listsmoke is unchanged (verified:runner.ts --listprints the task list).agentInvoker.ts: the commented Messages call is real (model from options,max_tokens8192, no sampling,tool_choiceauto, blocks by type, bounded response read, timeout,response.modelrecorded asechoedModel). Round 2: onlystop_reason: "end_turn"is a finished answer.max_tokens(on Claude 5.x adaptive thinking counts toward the cap, so a cap sized for 4.5 truncates),pause_turn,tool_use,refusaland a missingstop_reasonare all errors, somigrationEvalCommandcannot exit 0 when every response is truncated. The response read is bounded for real: acontent-lengthover the cap is refused before any byte is read, otherwise the stream is cancelled the moment it crosses 4,000,000 characters (round 1 calledresponse.text()first, so a 6 MB body was buffered whole). An HTTP error, a response with nomodel, or atool_useturn is likewise an error, never a clean run;taskSuccessRatestays 0 because acceptance criteria are not graded there. Atool_useblock is a request, not an execution: no tool runs and no result is sent back, so requested tools go to a newRunTelemetry.requestedToolslist andtoolCalls(and with ittoolCallCountandknowledgeReuseRate) counts only executed tools. The defaultbaremode used bymigrationEvalCommandoffers no tools and was not affected; themcp_*modes were.runner.tsmain(): with a real backend (EVAL_BACKENDnotstub) it exits 1 when any run has anerror, after writing every telemetry file and listing the failures. Before, ten HTTP 400 responses wrote ten error rows and exited 0, so the declaredmigrationEvalCommandcould pass without one successful provider call. Thestubsmoke (--list,EVAL_BACKEND=stub) still exits 0.production-agent.jsonmigrationEvalCommand->EVAL_BACKEND=anthropic EVAL_MODEL=<candidate> npx tsx scripts/eval-harness/runner.ts --tier tier1 --seeds 3.scripts/run-comprehensive-eval.ts:minimax-m2.1removed;--modelsmust be a canonical approved id (resolveModelAlias(m) === m), so unknown ids and legacy aliases such asgpt-4o-mini(which resolves togpt-5.4-miniand would disqualify every row only after the paid run) abort before any request; each row recordsresolvedModelId(returned byrunPersonaEpisodeEval) andfallbackUsed, including error rows; the run exits 1 if any row has no resolved identity (a Convex deployment built before this PR), resolved to a different id, has no explicitfallbackUsed: false, or its retry switched models.Test proof: fails before, passes after (A.0.6)
Round 4 review fixes (this push)
Plain-language version: the previous push made two things worse for people who name a model themselves. (1) A signed-in user who typed a model that is still live (for example
claude-sonnet-4-5-20250929) had it silently swapped for an older, already-retired one, because the code forwarded the resolver's lossy shorthand instead of what they typed. (2) The shared catalog threw an error for 16 ids the repo still recognises and thatmainsent through. Both are fixed by validating at the door and then forwarding the caller's own string, and by listing the recognised ids the catalog must let through. Also fixed: Claude Haiku 4.5 research runs were sent a thinking setting that model rejects; free models were counted as paid spend against an agent's dollar budget; a research run for a made-up model was queued before being rejected; and four test files could not notice an assistant "prefill" being added to a request.dailyBriefWorker.runNextTask/dailyBriefPersonalOverlay.runNextPersonalTaskcallnormalizeModelInputonly to validate and forwardargsunchangeddailyBriefModelBoundary.test.ts: all 16 pass-through ids forwarded byte-for-byte and the catalog sends the same string to the provider; unknown ids still throwgetModelWithFailoverthrew for ids main passed throughLEGACY_PASSTHROUGH_IDS(16 ids: the 3 dated Claude snapshots, 10gpt-*, 3gemini-*) is part of "known";anthropicWireIdreturns the Claude snapshots unchanged. Left rejecting on purpose: dottedclaude-opus-4.6/4.5,claude-sonnet-4.5(never wire ids, the provider 404s) andgpt-oss*(OpenRouter free aliases thegpt-prefix used to send to paid OpenAI)anthropicWireIds.test.ts: pass-through for each id; a drift guard that no provider-prefixedLEGACY_ALIASESkey is rejected; the throw assertion forclaude-haiku-4-5-20251001replaced (old expectation recorded in a comment); a made-up snapshot still throwseffortto Haiku 4.5ADAPTIVE_THINKING_WIRE_IDS(claude-sonnet-4-6,claude-opus-4-7); all other ids keep main's{type:"enabled", budget_tokens}with noeffort; under 1,024 tokens nothinkingextendedThinking.requestShape.test.tsdrivesadvanceRunforclaude-haiku-4-5,claude-haiku-4.5,claude-haiku-4-5-20251001; builder test covers the dated 4.5 snapshots. The provider's 400 for adaptive/effort on Haiku 4.5 is read from the migration guide, not observedcheckAgentBudgetisFreeAlias(resolver cost class) drivescalculateCost,isFree,actualTier, the cached path and the free-tier filtermodelRouter.requestShape.test.ts: every approved free alias (19) through 20route()calls and the realcheckAgentBudgetover the logged rows: cost 0,isFree,actualTier: "free", budget never used; a paid control model does exhaust the budget. Against the previousmodelRouter.ts: 15 of the 19 failmodelResolver.ts:143/:564/:710said "defer"requestExtendedRunqueued a run for an unknown modelresolveCheckpointModel(the same functionbuildCheckpointRequestBodyuses) runs before the insertextendedThinking.requestShape.test.ts:claude-not-real,invented-free,kimi-k2.6,gpt-5.4rejected with no insert and no scheduled job;claude-sonnet-4.6,claude-haiku-4-5,claude-haiku-4-5-20251001accepted and stored as givenmessages.at(-1).role === "user"; Geminicontents.at(-1).role)benchmark.ts,llmTools.ts,researchWritingTools.ts,modelRouter.ts,judgeScratchnodeChatVideo.mjs: each suite failed (1, 1, 1, 5, 1 tests); all restoredMeasured at the round 4 head: CI
runtime-smokelist 47 files / 660 tests pass (619 at round 3);npx tsc --noEmitexit 0;npx tsc -p backend/convex --noEmitexit 0.npm run test:runexits 1 exactly as before: app-vitest 8 failed files / 197 passed (22 tests), mcp-local 21 failed files (160 tests,better-sqlite3binding), convex-mcp 3, openclaw 1; none of the failing files is touched by this PR, and the counts equal theorigin/mainrow in "Check output". No paid provider call was made.CI on the round 3 head
a9e09067(read, not caused by this change):Typecheck,Runtime smoke,Build,Tier B vs preview URLpass, and those four are the only required contexts inmain's branch protection (read through the API).eval-gateanddelta-dogfood-gatefailed insidenpm ci || npm installwith an npmedgesOutcrash; no manifest changed and both passed oncabf937b, but that alone did not prove the crash is pre-existing. Round 4 push (d70d188a): both failed again at the same step. Reproduced outside this PR:npm install --no-audit --no-fund --ignore-scriptson a copy ofpackages/convex-mcp-nodebench/package.json(byte-identical toorigin/main; the package has no lockfile, so every run resolves the registry afresh) fails locally with the sameCannot read properties of null (reading 'edgesOut')(npm 10.9.7). So it is an npm/registry-state failure that does not depend on this PR's diff; it cannot be fixed here without changing that package's manifest, which is out of scope.Pipeline quality benchmarkfails becauseNODEBENCH_API_URLis unset (also red onmain). TheBranch namecheck fails onclaude/model-refresh-nodebenchai(details under Discrepancies item 1).Round 3 review fixes
New or extended:
agentRuntimeTierFallback.test.ts(free primary on the anonymous, free and pro tiers through the realisModelAllowedForTier/getBestModelForTier; post-streamrequireFree; a paid primary keeps its fallbacks),anthropicWireIds.test.ts(unknown prefix-shaped and unlisted ids throw with their provider configured; the formerclaude-haiku-4-5-20251001pass-through now asserts a throw, reason in a comment),dailyBriefModelBoundary.test.ts(new, both public actions, in the CI list),modelResolver.test.ts(x-free,retired-model-free,not-a-modeland the free-lane wire id through all three fallback helpers),modelRouter.requestShape.test.ts(pinnedauto-free, wire id,invented-free: pricing,isFree,actualTier, cache, audit row),triSearch.test.tsandbenchmark.requestShape.test.ts(no Gemini sampling keys; the oldtemperature === 0assertion is replaced, not kept),judgeScratchnodeChatVideo.requestShape.test.ts(new, in the CI list).Against the previous head
cabf937bsource (the ten changed source files stashed, the new tests kept): 8 of 8 files fail, 25 tests fail, 127 pass (the 127 are behaviours that were already right).Measured at the round 3 head: CI
runtime-smokelist 47 files / 619 tests pass (it was 45 / 592 atcabf937b);npx tsc --noEmitexit 0;npx tsc -p backend/convex --noEmitexit 0;node --test scripts/nodebench-packet-contracts.node.mjs20/20. The earlier claim handed to review namedfa669cca(37 files / 504 tests, 104/104); that is stale and is superseded by these numbers. Not clean, and not caused by this PR:npm run test:runandnpx vitest run backend/convex shared workers scriptsfail in files that fail identically onorigin/mainin a clean worktree (better-sqlite3native binding missing for the whole mcp-local segment;convex-testcannot find_generatedforfastAgentPanelStreaming.anonymousAccess.test.ts;ResizeObservermissing in sevenapps/webUI files, 21 tests, same 21 onmain;workers/node/commandBridge,sharedContextRoute,hyperloopRoute,deeptrace/evidenceTruthand others). No paid provider call was made.Round 2 review fixes
New or extended:
evalHarness.test.ts(end_turn only, bounded stream, content-length),modelResolver.test.ts(paid unlisted ids keep main's behaviour),runtimeRouting.test.ts(free request on all five executor branches),modelRouter.requestShape.test.ts(free cap withrequireVision, free-lane pricing, outgoing model strings andtop_p/top_k),anthropicWireIds.test.ts(wire id, free route aliases, prefix-shaped invented ids),triSearch.test.ts(condenseQuery),instagramClaimVerification.test.ts(new, added to the CI runtime-smoke list),pipelineModelBoundary.test.ts(rewritten, see below), mcp-localanthropicRequestShape.test.ts(polish model id).Against the previous head
7096f29fsource (the seven changed source files checked out from it, the new tests kept): 7 of 7 files fail, 18 tests fail, 82 pass (the 82 are behaviours that were already right). The boundary test is different: the old version still passed withassertKnownPipelineModelmoved afterreservePipelineLaunchAdmission, because the Convex mutation rolls the admission write back on throw. It now fills the owner's launch window first (control call: a valid launch gets "launch limit reached"), so admission-first would surface the limit error instead ofUnknown model. Verified by swapping the two lines: 1 of 4 fails; reverted.Round 1 review fixes
New or extended in this push:
modelResolver.test.ts(fallback helpers,normalizeNodeBenchRuntimeModel),runtimeRouting.test.ts,approvedModels.drift.test.ts(pins every selector option toresolvePipelineModelSelection),anthropicWireIds.test.ts(getModelWithFailoverfails closed),evalHarness.test.ts(runner exit status, requested vs executed tools, missing model),comprehensiveEvalModels.test.ts(canonical models, missing identity, error rows keep identity),extendedThinkingRunner.test.tsandextendedThinking.requestShape.test.ts(usage-based charge, run-level bound, through the realadvanceRun),modelRouter.requestShape.test.ts,benchmark.requestShape.test.ts,providerCopies.requestShape.test.ts,agentChatActions.failClosed.test.ts,toolGenRequestShape.test.ts(generate-tool),triSearch.test.ts(model in the URL), mcp-localanthropicRequestShape.test.ts.Against the previous head
fa669cca(clean worktree, only these test files copied in, minusagentChatActions.failClosed.test.ts): 10 of 14 files fail, 48 tests fail, 114 pass (the 114 are behaviours that were already right;benchmark.requestShape,anthropicRequestShapeand 3 of 4modelRoutercases pass on the old source because they pin behaviour PR 1 already had). Mutation checks on the new source, each made a test fail and was then reverted: droppingFREE_LANE_MODELfrom the priority order, removing the free-keeps-cost-class rule, restoring the paid push inmodelRouter, reverting onegenerateWithProvidercopy tocontent[0], disabling the runner exit code, looseningidentityViolations, zeroing the thinking charge, removing theagentChatActionsrethrow, restoring thegpt-5.4-minidefault ingetModelWithFailover, un-filtering the free primary's candidates, and pointing the webauto-freeliteral back at laguna.Round 0 (original push)
New or extended tests (all in the CI runtime-smoke list):
events.requestShape.test.ts,extendedThinkingRunner.test.ts,triSearch.test.ts,toolGenRequestShape.test.ts,evalHarness.test.ts,anthropicWireIds.test.ts,comprehensiveEvalModels.test.ts,pipelineModelBoundary.test.ts,approvedModels.drift.test.ts, plus the extendedmodelResolver.test.tsandpipelineModelRoutes.test.ts. Each request-shape test builds the request through the repo's own builder or exported function with a fake transport and asserts the forbidden keys (temperature,top_p,top_k,thinking.budget_tokens,thinkingBudget) are absent and the model string is the expected wire id.Run against the pre-change source (a clean worktree at
3a716d8bwith only the new/extended test files copied in, andexportadded to the threeevents.tsfunctions so the old behaviour could be exercised): 11 of 11 files fail, 51 tests fail, 53 pass (the 53 are unchanged behaviours). Examples:AssertionError: temperature must be absent(all threeevents.tspaths);expected { thinkingBudget: +0 } to deeply equal { thinkingLevel: 'low' };expected { model: 'claude-sonnet-4.6', ... } to match { model: 'claude-sonnet-4-6' };expected 'SCAFFOLD: Anthropic API call not yet wired' to match /HTTP 400/;unresolvedModels is not a function;parseModelKey is not a function; an unknown pipeline model id reached the workflow component instead of throwing. After the change: all 104 pass.Check output
nodekit.yaml commands.check=npm run test:run. It does not pass, and it fails the same way onorigin/main(so the failures are not from this change): the same test files fail in every segment on both trees, from the missingbetter-sqlite3native binding in thepackages/*segments and 8 UI test files inapps/web.The one extra app test failure on the branch (
RichNotebookEditor.finder.test.tsx, "Cmd+F opens ...") failed once in the full branch run; run alone it passes on both trees (that file's "Escape closes the finder" case fails on both). The newapprovedModels.drift.test.tsaccounts for the +1 passing file.Targeted gates, all on this branch:
No paid provider call was made.
eslintover the changed files reports one error,prefer-constatevents.ts:2768, which is not a line this PR touches.Runtime-site dispositions (spec A.2.10: 129 runtime-role sites + 16 class E/R rows = 145; migrate 110, freeze 33, defer 2)
"Spec disposition" is the spec's. "This PR" says what PR 1 did at that site; every other migrate row is a deferred id move in PR 2, which starts when this PR is merged.
backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:137backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:140backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:143FREE_LANE_MODELplaced at the head of the free segment ofMODEL_PRIORITY_ORDER(so a free start model never restarts at the paid head); the id updates (3.1-flash-lite-preview -> 3.5-flash-lite, claude 4.x -> 5.5, pruning) stay in PR 2 (condition: PR 1 merged)backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:181backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:183backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:191backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:270auto-free-> FREE_LANE_MODEL (viagetBestFreeModel).auto-fastid moves in PR 2.backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:380backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:401backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:422backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:450backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:495backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:564nemotron-3-super-freerow added toMODEL_SPECS(the FREE_LANE_MODEL registration, zero price,isFreeModeltrue); pruning the free specs to the live:freelist stays in PR 2 (condition: PR 1 merged)backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:710LEGACY_ALIASESgained thenvidia/nemotron-3-super-120b-a12b:freewire-id row and the twogoogle:models/*strings (wikiDreamingGraph.ts); fixing the two lossy maps (claude-sonnet-4-5-20250929,claude-opus-4-5-20251101) and adding the old->new rows stays in PR 2 (condition: PR 1 merged). Round 4: because the maps stay lossy in PR 1, no public path forwards a resolver alias to a providerbackend/convex/domains/agents/mcp_tools/models/modelResolver.ts:1234getBestFreeModelreturns FREE_LANE_MODEL. Liveauto-freetraffic moves from laguna here; the PR carriesneeds-live-eval.MODEL_PRIORITY_ORDERnow lists it at the head of the free segment (web mirror too).backend/convex/domains/agents/mcp_tools/models/modelResolver.ts:908getLanguageModelSafe,normalizeModelInput,normalizeNodeBenchRuntimeModel,allowPaidFallback, fallback helpers); see "Where the fail-closed claim stops".shared/llm/modelCatalog.ts:146shared/llm/modelCatalog.ts:195shared/llm/modelCatalog.ts:265shared/llm/modelCatalog.ts:512shared/llm/modelCatalog.ts:649shared/llm/modelCatalog.ts:696shared/llm/modelCatalog.ts:765apps/web/src/shared/llm/approvedModels.ts:9nemotron-3-super-free;approvedModels.drift.test.tsadded.apps/web/src/shared/llm/approvedModels.ts:478apps/web/src/shared/llm/approvedModels.ts:481apps/web/src/shared/llm/pipelineModelRoutes.ts:50auto-free-> FREE_LANE_MODEL.auto-fastid moves in PR 2.apps/web/src/features/redesign/components/UniversalComposer.tsx:38backend/convex/domains/redesign/chatRuns.ts:257nodeagent.yaml:23backend/convex/scratchnodeLiveCues.ts:116shared/search/triSearch.ts:58thinkingBudget:0->thinkingLevel:"low"; round 3:temperaturealso removed from both Gemini calls (rerank,condenseQuery). Id moves in PR 2.shared/llm/router.ts:125shared/llm/router.ts:131shared/llm/router.ts:140shared/llm/router.ts:157shared/llm/fastAgentRuntimeContract.ts:1backend/convex/events.ts:2511backend/convex/events.ts:2610backend/convex/events.ts:2927backend/convex/domains/product/extendedThinking.ts:54output_config.effort; wire id from an explicit table. Id moves in PR 2.backend/convex/domains/financialOperator/realExtractors.ts:136backend/convex/domains/agents/adapters/types.ts:28backend/convex/domains/agents/adapters/registerDefaultAdapters.ts:52backend/convex/domains/agents/adapters/anthropic/anthropicReasoningAdapter.ts:85backend/convex/domains/agents/adapters/openai/openaiAgentsAdapter.ts:78backend/convex/domains/agents/adapters/vercel/vercelAiSdkAdapter.ts:65backend/convex/domains/agents/runtimeRouting.ts:44backend/convex/domains/agents/runtimeTierFallback.ts:207backend/convex/domains/agents/fastAgentPanelStreaming.ts:6019backend/convex/domains/agents/digestAgent.ts:572backend/convex/domains/ai/models/modelRouter.ts:105backend/convex/domains/ai/models/capabilityRegistry.ts:106backend/convex/domains/ai/models/autonomousModelResolver.ts:36backend/convex/domains/ai/models/freeModelDiscovery.ts:137backend/convex/domains/ai/models/livePerformanceEval.ts:465backend/convex/domains/evaluation/llmJudge.ts:49backend/convex/domains/evaluation/agentRunJudge.ts:39backend/convex/domains/evaluation/ultraLongChat/judge.ts:58backend/convex/domains/evaluation/comprehensiveEval.ts:849backend/convex/domains/evaluation/dogfood/screenshotQa.ts:59backend/convex/domains/agents/orchestrator/geminiVideoWrapper.ts:230backend/convex/domains/search/fusion/benchmark.ts:72temperaturedeleted from the Anthropic judge request; round 3: also from the Gemini judge request (generationConfig), the OpenAI judge keepstemperature: 0. freeze: judge kimi-k2.6 / DEFAULTbackend/convex/domains/search/rag.ts:340backend/convex/domains/search/ragEnhanced.ts:350backend/convex/domains/search/linkupClient.ts:11backend/convex/tools/teachability/teachingAnalyzer.ts:117backend/convex/domains/knowledge/entityInsights.ts:601backend/convex/domains/research/dealFlow.ts:45generateWithProvidercopies). Id moves in PR 2.backend/convex/domains/ai/ai.ts:79backend/convex/domains/research/modelComparison.ts:143backend/convex/domains/agents/agentLoop.ts:248backend/convex/domains/agents/emailAgent.ts:209backend/convex/domains/agents/dueDiligence/ddOrchestrator.ts:59backend/convex/domains/agents/traceOrchestrator.ts:51backend/convex/domains/blips/blipVerification.ts:51backend/convex/domains/research/narrative/newsroom/agents/analystAgent.ts:60backend/convex/domains/enrichment/llmEnrichment.ts:64backend/convex/workflows/dailyLinkedInPost.ts:608backend/convex/domains/social/linkedinPrePostVerification.ts:35backend/convex/domains/agents/mcp_tools/reasoningTool.ts:85backend/convex/domains/agents/canonicalPlanner.ts:228backend/convex/domains/agents/swarmOrchestrator.ts:125backend/convex/domains/agents/arbitrage/tools/contradictionDetection.ts:65backend/convex/domains/verification/entailmentChecker.ts:124backend/convex/domains/documents/quickCapture/voiceMemos.ts:38backend/convex/domains/documents/pdfAnalysis.ts:71backend/convex/actions/externalOrchestrator.ts:95backend/convex/domains/product/wikiDreamingGraph.ts:362backend/convex/tools/meta/dynamicPromptEnhancer.ts:76backend/convex/lib/xaiClient.ts:221backend/convex/domains/ai/realtimeTranscription.ts:25backend/convex/domains/search/embedRowOnUpdate.ts:65backend/convex/domains/integrations/billing/rateLimiting.ts:24workers/node/agentHarness.ts:2425workers/node/agentHarness.ts:2552workers/node/agentHarness.ts:2750workers/node/harnessRuntime.ts:189workers/node/nemoclaw/agentRunner.ts:79workers/node/claudeCodeBridge.ts:106workers/node/pipeline/searchPipeline.ts:924workers/node/routes/search.ts:2393workers/node/routes/session.ts:46packages/mcp-local/src/tools/llmTools.ts:35temperaturedeleted. The id moves in PR 2 (spec rows); PR 3 is only the release that ships it.packages/mcp-local/src/tools/researchWritingTools.ts:36temperaturedeleted. The id moves in PR 2 (spec rows); PR 3 is only the release that ships it.packages/mcp-local/src/tools/visionTools.ts:88packages/mcp-local/src/index.ts:1944packages/mcp-local/src/tools/webTools.ts:239packages/mcp-local/src/tools/entityLookupTools.ts:41packages/mcp-local/src/profiler/modelRouter.ts:25packages/mcp-local/src/tools/founderTools.ts:2141packages/eval-engine/src/judges/text-judge.ts:94services/ingestion-extract/extractors/claim_extractor.py:231docker-compose.yml:21scripts/tool-gen/analyze-gap.mjs:57scripts/eval-harness/agentInvoker.ts:29EVAL_MODEL). Default id moves in PR 2.scripts/run-comprehensive-eval.ts:535minimax-m2.1removed; unknown--modelsaborts; rows recordresolvedModelId/fallbackUsed. freeze: judge kimi-k2.6 (comparability). SAME PR: remove 'minimax-m2.1' (not an approved id) from availableModels; every --models value must resolve or the...package.json:137scripts/run-enterprise-investigation-eval.ts:156scripts/generate-live-digest.ts:35scripts/ui/runDogfoodGeminiQa.mjs:44scripts/qa/redesignAgentQa.ts:36scripts/ui/judgeScratchnodeChatVideo.mjs:22thinkingBudget:0->thinkingLevel:"low"; round 3:temperature: 0.2removed. Id moves in PR 2.scripts/ci-check-models.sh:20.github/workflows/ci.yml:102dailyBriefModelBoundary.test.ts,judgeScratchnodeChatVideo.requestShape.test.ts). The list went from 28 to 47 files; round 2 said sixteen and missedinstagramClaimVerification.test.ts. freeze: test list; exact-id tests move to the resolver in the same PR.github/workflows/convex-mcp-eval-gate.yml:90--listonly ($0) - not a receipt. Becomes the real nightly (runner.ts --tier tier1 --seeds $EVAL_SEEDS) only inside Environment `model-bum....github/workflows/daily-tool-gen.yml:52.github/workflows/attrition-qa.yml:111apps/web/src/features/editor/components/UnifiedEditor/useInlineFastAgent.ts:248shared/llm/modelCatalog.ts:63nemotron-3-super-free;anthropicWireId/anthropicTexthelpers. 5.5 / 3.8 rows in PR 2.production-agent.json:114migrationEvalCommand-> real runner.pinnedModelmoves in PR 2.backend/convex/domains/agents/batchAPI.ts:514backend/convex/domains/ai/models/modelRouter.ts:434temperaturenot sent to Anthropic/Google candidates.backend/convex/crons.ts:764backend/convex/domains/evaluation/evaluationPrompts.ts:23backend/convex/domains/agents/mcp_tools/models/promptCaching.ts:148backend/convex/schema.ts:3775workers/node/pricingScraper.ts:52packages/eval-engine/src/telemetry/cost-tracker.ts:23scripts/eval-harness/deeptrace/llm-proposer.ts:32scripts/pricing/anthropicApiPricing.ts:162packages/mcp-local/src/__tests__/gaiaCapabilityEval.test.ts:790packages/mcp-local/src/benchmarks/searchQualityEval.ts:281apps/web/src/features/financialOperator/components/FinancialOperatorOverlay.tsx:29apps/web/src/features/benchmarks/components/ModelLeaderboard.tsx:13getLanguageModelSafecall sites (93 listed; 92 are calls, #34 is text inside aconsole.log; round 2 removed the former #86, a paid fallback)Row #34 (
glmFlashWithReasoning.ts:495) is a string printed byconsole.logas an example, not a call. All other rows are real calls.allowPaidFallback: trueis permitted only where the same function awaitsreserveLlmRequestInternal. None of the 93 sites passes it in this PR.normalizeModelInputfollows the same rule.domains/agents/adapters/langgraph/langgraphAdapter.ts:186modeldomains/agents/adapters/vercel/vercelAiSdkAdapter.ts:84modeldomains/agents/agentLoop.ts:248"qwen3-coder-free"domains/agents/arbitrage/agent.ts:51modelNamedomains/agents/coordinator/agent.ts:40modelNamedomains/agents/core/coordinatorAgent.ts:1378modeldomains/agents/core/subagents/document_subagent/documentAgent.ts:18modelNamedomains/agents/core/subagents/document_subagent/documentAgentWithMetaTools.ts:47modeldomains/agents/core/subagents/document_subagent/documentAgentWithMetaTools.ts:144modeldomains/agents/core/subagents/dossier_subagent/dossierAgent.ts:35modeldomains/agents/core/subagents/entity_subagent/entityResearchAgent.ts:83modeldomains/agents/core/subagents/media_subagent/mediaAgent.ts:18modelNamedomains/agents/core/subagents/openbb_subagent/openbbAgent.ts:59modeldomains/agents/core/subagents/openbb_subagent/openbbAgent.ts:191modeldomains/agents/core/subagents/sec_subagent/secAgent.ts:19modelNamedomains/agents/dataAccess/agent.ts:35modelNamedomains/agents/dataAccess/agent.ts:113modelNamedomains/agents/deliberationToEvolution.ts:107"gemini-3.1-flash-lite-preview"domains/agents/digestAgent.ts:786modeldomains/agents/digestAgent.ts:1457modeldomains/agents/dueDiligence/branches/marketCompetitive.ts:305"qwen3-coder-free"domains/agents/dueDiligence/branches/teamDeepResearch.ts:368"qwen3-coder-free"domains/agents/dueDiligence/ddEnhancedOrchestrator.ts:1396SYNTHESIS_MODELdomains/agents/dueDiligence/ddEnhancedOrchestrator.ts:1489SYNTHESIS_MODELdomains/agents/dueDiligence/ddOrchestrator.ts:1157SYNTHESIS_MODELdomains/agents/dueDiligence/ddOrchestrator.ts:1262SYNTHESIS_MODELdomains/agents/dueDiligence/investorPlaybook/agenticPlaybook.ts:130"qwen3-coder-free"domains/agents/emailAgent.ts:210normalizeModelInput(modelId)domains/agents/emailAgent.ts:321normalizeModelInput(modelId)domains/agents/fastAgentPanelStreaming.ts:1193modelInputallowPaidFallbackonly if Homen decides so (SPEC C.6). It awaitsreserveLlmRequestInternalat :3462, the only site that could.domains/agents/glmFlashWithReasoning.ts:49"glm-4.7-flash"domains/agents/glmFlashWithReasoning.ts:135"glm-4.7-flash"domains/agents/glmFlashWithReasoning.ts:198"glm-4.7-flash"domains/agents/glmFlashWithReasoning.ts:495"glm-4.7-flash"console.log; nothing to decidedomains/agents/selfEvolution.ts:224"qwen3-coder-free"domains/agents/testGlmFlash.ts:20"glm-4.7-flash"domains/agents/testGlmFlash.ts:55"qwen3-coder-free"domains/agents/testGlmFlash.ts:116"glm-4.7-flash"domains/agents/testGlmFlash.ts:187modelInfo.namedomains/agents/testGlmFlashFix.ts:17"glm-4.7-flash"domains/agents/testGlmFlashFix.ts:125modelInfo.namedomains/agents/testParallelOrchestrator.ts:45"glm-4.7-flash"domains/agents/testParallelOrchestrator.ts:118"deepseek-v3.2"domains/agents/testParallelOrchestrator.ts:194"glm-4.7-flash"domains/agents/traceOrchestrator.ts:303TRACE_MODELdomains/ai/models/modelRouter.ts:441modelIddomains/blips/blipClaimExtraction.ts:140"qwen3-coder-free"domains/blips/blipGeneration.ts:193"qwen3-coder-free"domains/blips/blipPersonaLens.ts:262"qwen3-coder-free"domains/blips/blipVerification.ts:190"qwen3-coder-free"domains/enrichment/llmCompanyExtraction.ts:141"qwen3-coder-free"domains/enrichment/llmEnrichment.ts:64"qwen3-coder-free"domains/evaluation/agentRunJudge.ts:129modeldomains/evaluation/evidencePlanner.ts:176"claude-haiku-4.5"domains/evaluation/llmJudge.ts:453modeldomains/evaluation/llmJudge.ts:752judgeModeldomains/evaluation/personaEpisodeEval.ts:1066modeldomains/evaluation/personaLiveEval.ts:209String(arch.reasoningModel)domains/evaluation/testAgentDirect.ts:20modeldomains/evaluation/testLlmJudge.ts:101modelNamedomains/evaluation/ultraLongChat/judge.ts:212judgeModeldomains/integrations/email/dailyEmailReport.ts:97normalizeModelInput(modelId)domains/knowledge/adaptiveEntityEnrichment.ts:285DEFAULT_MODELdomains/missions/preExecutionGate.ts:145"gemini-3.1-flash-lite-preview"domains/operations/encounters/encounterCapture.ts:293"qwen3-coder-free"domains/proactive/actions/testDraftGenerator.ts:166"claude-sonnet-4.6"domains/proactive/actions/testDraftGenerator.ts:323modelInfo.namedomains/proactive/actions/testSimpleDraft.ts:20"gemini-3-flash-preview"domains/product/diligenceLlmJudgeRuns.ts:392modelNamedomains/product/wikiDreamingGraph.ts:207modeldomains/research/financial/financialAnalystAgent.ts:50"claude-sonnet-4.6"domains/research/financial/financialAnalystAgent.ts:111"claude-sonnet-4.6"domains/research/financial/financialAnalystAgent.ts:241"claude-sonnet-4.6"domains/research/financial/interactiveDCFSession.ts:504"claude-sonnet-4.6"domains/research/narrative/newsroom/agents/analystAgent.ts:436cfg.modeldomains/research/narrative/newsroom/agents/curatorAgent.ts:188cfg.modeldomains/research/narrative/newsroom/agents/publisherAgent.ts:259modeldomains/research/narrative/newsroom/agents/publisherAgent.ts:490modeldomains/research/narrative/newsroom/agents/signalCollectorAgent.ts:215cfg.modeldomains/search/fusion/reranker.ts:51DEFAULT_MODELdomains/social/linkedinPrePostVerification.ts:98FREE_MODELS[0]domains/social/linkedinPrePostVerification.ts:161FREE_MODELS[0]domains/social/linkedinPrePostVerification.ts:299FREE_MODELS[0]domains/social/linkedinQualityJudge.ts:111modelAliasdomains/verification/instagramClaimVerification.ts:151modelAliasFREE_MODEL_PRIORITY); when none builds (OpenRouter not configured)getFreeModelnow throws. Round 1 said "fails closed" here and missed that the loop's fallback built paidgemini-3-flash-preview(former row 86, deleted with the fallback);instagramClaimVerification.test.tspins ittools/calendar/recentEventSearch.ts:251DEFAULT_MODELtools/meta/contextEnhancementActions.ts:113"gemini-3-flash-preview"tools/meta/dynamicPromptEnhancer.ts:76"claude-haiku-4.5"tools/meta/dynamicPromptEnhancer.ts:271"gemini-3-flash-preview"tools/sec/secCompanySearch.ts:171DEFAULT_MODELworkflows/agentProjectIdeaPost.ts:377modelIdworkflows/founderPostGenerator.ts:202modelIdworkflows/specializedLinkedInPosts.ts:264"qwen3-coder-free"Discrepancies between the spec text and the real code
model-refresh/nodebench-params; this run was toldclaude/model-refresh-nodebenchai. The CI check "Branch name - /" fails on it. Whether it is required:.github/workflows/pr-style.ymllabels the job "Required" in a comment, butmain's branch protection (read through the API, round 4) lists onlyTypecheck,Runtime smoke,BuildandTier B vs preview URL, and all four pass ona9e09067; so the claim is "not a required status context", which a reviewer who cannot read branch protection cannot confirm from the workflow file. It would also fail on the spec's ownmodel-refresh/...names, because the allowed types arefeat,fix,chore, and so on;chore/model-refresh-nodebench-paramswould pass. Renaming the head branch would close this PR and drop its review history, so I left it; the spec's naming for PR 2 / PR 3 needs a decision (section C). The "Pipeline quality benchmark" check is red onmaintoo (NODEBENCH_API_URLunconfigured), so it is not this PR's.wikiDreamingGraph.ts:362passesgoogle:models/...strings that the spec does not mention; with fail-closed they would throw, so two LEGACY_ALIASES rows were added (PR 2 moves the ids).getBestFreeModelreturns the constant only; the spec's optional "or thefreeModelDiscoveryverified pick" is not implemented (no consumer needs it yet).shared/llm/modelCatalog.ts(the code that builds the dotted-alias requests), not inmodelResolver.ts, becauseshared/cannot import frombackend/convex;anthropicWireIds.test.tsfails if the two tables drift.content?.[0]?.textreads remain (workers/node/nemoclaw/agentRunner.ts:460,packages/mcp-local/src/index.ts:1988). They were not in the spec's "x14" list and neither sends a Claude 5.x id yet. Blocking items, not notes: theagentRunner.tsfix (read text blocks by type, with a thinking-before-text test) must land in PR 2 before its ids move, and theindex.tsfix in PR 2 (the mcp-local ids move in PR 2's rows; PR 3 is only the release; that site still names the retiredclaude-3-5-haiku-latest) before its ids move.Still open (not done here, by rule)
EVAL_BACKEND=anthropic EVAL_MODEL=claude-sonnet-5-5 npx tsx scripts/eval-harness/runner.ts --tier tier1 --seeds 3. 10 tier1 tasks x 3 seeds = 30 Messages calls, <= 8192 output tokens each; upper bound about $2.6 at $2/$10 per M tokens. Not required for PR 2 merge (spec); the nightly stays--listuntil it runs in Environmentmodel-bump-eval.npx tsx scripts/run-comprehensive-eval.ts --models claude-sonnet-5-5,claude-haiku-5-5,claude-opus-5-5,gemini-3.8-flash,gemini-3.5-flash-lite,nemotron-3-super-free --suite full --judge --judge-model kimi-k2.6 --metricsin Environmentmodel-bump-eval. Not run; not part of this PR's gate. The free-lane receipt fornemotron-3-super-freealone costs $0 and is the live evidenceneeds-live-evalwaits for.tier: "free"inchainResolveris a floor;route()now caps it at free (the cap is a behaviour change forrequireVision-style callers, see the fail-closed section); (b) the branch-name convention for PR 2 / PR 3: the CI checkBranch name - <type>/<short-slug>rejects the spec'smodel-refresh/...andrelease/...names, so I proposechore/model-refresh-nodebench-ids(PR 2) andchore/release-nodebench-mcp-3-3-0(PR 3), to be confirmed before they are opened; (c) whether approved paid ids outsideMODEL_PRIORITY_ORDERshould be listed or refused (they behave as onmainin this PR).modelsilently; validatingargs.modelbeforeresolveFastAgentRequestedModelwould make it throw. Not done: the spec keeps the pinned lane and the change alters what an anonymous client sees.Branch namecheck is not a required context. If you want it green, say so and the same commits can be opened aschore/model-refresh-nodebenchai.fastAgentPanelStreaming.ts:1193takesallowPaidFallback: true. Until then an unknown model there throws.model-refresh/nodebench-idsand PR 3release/nodebench-mcp-3.3.0: deferred; condition: this PR merged. The free lane carriesneeds-live-evalfor PR 2.Spec: model-defaults SPEC Part A §A.2 (NodeBenchAI)
🤖 Generated with Claude Code