feat(ai): validate catalog generation invariants and fail violating regens - #2042
Open
snimu wants to merge 4 commits into
Open
feat(ai): validate catalog generation invariants and fail violating regens#2042snimu wants to merge 4 commits into
snimu wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b7362bf. Configure here.
xeophon
pushed a commit
that referenced
this pull request
Sep 5, 2026
…n 3.8 Max (#2057) * chore(ai): refresh the model catalog; classify gpt-6 and re-alias Qwen 3.8 Max Unblocks repo-wide CI: the build regenerates the catalog on every run, and OpenRouter renamed qwen/qwen3.8-max to qwen/qwen3.8-max-0902, so every fresh regen dropped the Prime Inference row's effort metadata and failed the mandatory-efforts pin in prime-inference-models.test.ts. Three minimal generator carriers ship with the regen (cherry-picked from the #2042 line so that PR rebases to mostly-validator): - PRIME_INFERENCE_OPENROUTER_ALIASES maps the undated Prime id to OpenRouter's dated route; the regenerated row keeps its exact previous thinkingLevelMap. - Copilot now serves gpt-6-astra; gpt-6 joins the Responses-only families (native OpenAI serves it via Responses, and every Copilot OpenAI family since gpt-5 is Responses-only). - gpt-6 family thinking-map rule for the Responses apis (reasoning mandatory, no minimal, xhigh/max supported, per OpenRouter capability data) so native rows do not ship selectable levels the model rejects. Catalog: 1262 -> 1280 models, 43 rows differ, full-field audit in the PR body. Regen byte-stable across a double run and through the CI-mirror build. Linear: RES-1273 (invariant program), RES-1269 (catalog CI reliability) * fix(ai): give vendored gpt-6 ids the family thinking map and refresh the alias comment The gpt-6 map rule was scoped to the Responses apis, so the Vercel AI Gateway openai/gpt-6-astra(-fast) rows shipped map-less while their gpt-5.6 siblings carry {minimal:null, xhigh, max} from the api-agnostic id rules. The rule now mirrors the 5.6 family structure exactly: the capability map applies by id on every route, and only the off:null entry stays Responses-scoped. Regen changes the 2 Vercel rows plus key-order churn on the 4 rows the previous single rule wrote. Also replaces the alias-table comment that still described it as empty. Linear: RES-1273
…egens Rebased onto the merged catalog line (#2057/#2069/#2074). New scripts/validate-model-catalog.ts, run by generate-models before writing: - maxTokens <= contextWindow on every row - every GitHub Copilot id maps to an explicitly classified family and the row's api matches (copilotModelApi replaces the inline prefix heuristic; unclassified families fail by name) - openai-codex rows may not diverge more than 2x from the same model's openai contextWindow, except rows with an upstream-verified smaller ChatGPT-backend window (gpt-6-astra: 272000 per Codex CLI models.json at rust-v0.153.4) - same-model rows within one transport must agree on runtime-selectable thinking levels (via getSupportedThinkingLevels, modulo off), and a row whose resolved compat cannot send reasoning effort must not offer any On violation the generator prints each offense, refuses to write, and exits nonzero. Data fixes by derivation: Copilot Grok/MAI-Code route to openai-responses (4 rows), Codex 5.6 contextWindow derives from the openai row (3 rows, astra excepted), models.dev output limits clamp to the context window (Inkling-Small), Kimi K3 gains low/high on effort-capable routes and all-null maps on transports that cannot send effort (15 rows). Remaining catalog delta is live upstream drift, itemized in the PR body. Linear: RES-1273
…t selectable levels The catalog represents "reasons at server default, effort not client-selectable" as a reasoning model whose thinkingLevelMap nulls every level. Runtime handling: setThinkingLevel no longer persists the forced off clamp as the user's default when the model offers no selectable levels; switching away restores the persisted default; cycleThinkingLevel returns undefined instead of indexing the empty list. Linear: RES-1273
snimu
force-pushed
the
feat/catalog-generation-invariants
branch
from
September 7, 2026 09:23
818c66e to
b0b720e
Compare
…messages rows Non-adaptive models on the anthropic-messages api express thinking through budget tokens (streamSimpleAnthropic -> adjustMaxTokensForThinking), where clampReasoning folds xhigh and max into high — the thinkingLevelMap never reaches the wire, so any xhigh/max entry on such a row offers a level that serializes identically to high. supportsAdaptiveThinking is exported and shared: generation nulls those entries (29 rows: the 6 Kimi K3 rows this branch's shared map had extended, plus 23 pre-existing Vercel openai-via-anthropic rows — the RES-1276 routing oddity carrying gpt-family maps), and a new invariant rejects the class with a fixture pin. Linear: RES-1273
…work Comment pass: multi-line rationale blocks collapse to one-line invariant guards (the PR body carries the citations). Test pass: the gpt-6 Astra openai/copilot level pin is deleted — that family rule merged via #2057/#2074, so the behavior is main's, not this PR's; the branch's supports-xhigh delta shrinks to the 1050000 codex-derivation pin. No behavior change; spot fail-unfixed re-proven on the budget-clamp fixture and the committed-catalog astra guard. Linear: RES-1273
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Purpose
Make catalog regeneration self-checking: a regen that violates known invariants fails loudly and refuses to write, instead of shipping bad rows that surface as user-facing 400s, early compaction, or dead /effort levels. Fixes the current violations by regen-time derivation, not hand-edits. Squashes #1683/#1680, #1406, #1597, the live case of #1725, and guards the #1679/#1724 staleness class at the generator.
Mechanism (new
scripts/validate-model-catalog.ts, wired intogenerate-models)Four invariants, checked on the deduped catalog before it is written; violations print by name and the generator exits nonzero without writing:
maxTokens <= contextWindowon every row ([Bug] Generated model catalog contains impossible maxTokens > contextWindow values #1683/[Bug] Generated catalog contains models where maxTokens exceeds contextWindow (Inkling-Small, openrouter gpt-3.5-turbo-0613) #1680).github-copilotid must match an explicitly classified family, and the row'sapimust equal that classification. Unclassified families fail by name instead of silently defaulting to chat completions — the exact mechanism that shipped the Grok misroute (GitHub Copilot grok-4.5 is routed to Chat Completions during model generation #1406).openai-codexrows may not diverge more than 2x from the same model'sopenaicontextWindow ([Bug] openai-codex applies one flat 272k context window to every model, including 1M-context models #1597/[Bug] Cross-provider catalog limit contradictions cause silent truncation and premature compaction #1724 class).offlevel is excluded from comparison: whether thinking can be disabled legitimately varies per transport (reasoning effortnoneexists only on native OpenAI Responses; the generator encodes that deliberately per provider).generateModels().catchnow sets a nonzero exit code (previouslycatch(console.error)exited 0 even on total failure).Data fixes (all by derivation; catalog regenerated on the rebased sources)
grok-/mai-toopenai-responses(upstream pi-mono #906)openairow when the API side serves 1M+. The 1,050,000 lift for the 5.6 rows rests on the #1597 backend measurement (the ChatGPT backend accepting >272k for 5.6); upstream Codex CLI models.json (rust-v0.153.4) still lists them at 272000, so the citation justifies only thegpt-6-astraexception (CODEX_SMALLER_WINDOW_VERIFIED, kept at 272000 and excluded from the >2x invariant)thinkingmachines/Inkling-SmallmaxTokens > contextWindow (#1683)anthropic-messagesrows (review round)clampReasoningfolds xhigh/max into high; generation nulls those entries and a new invariant rejects the class. Covers the 6 Kimi anthropic rows above plus 23 pre-existing Vercel openai-via-anthropic rows (the RES-1276 routing oddity)Intended-fix total: 43 rows vs main (4 + 3 + 1 + 12 + 23). Validator sanity vs the pre-fix committed catalog (post-#2069/#2074 main): reports exactly the known defect set and nothing else; zero violations on the regenerated output. Regen byte-stable across a double run. The rebased regen also carries live upstream drift (~110 rows beyond the fixes): a Prime Inference listing refresh (display-name normalization across ~70 rows, 10 additions, gpt-5.6-luna route removals, context/maxTokens updates), models.dev dropping the six legacy moonshot kimi-k2 preview rows per provider (verified upstream: only k2.6/k2.7 remain — real removals, not a fetch failure), Bedrock adding grok-4.6, and OpenRouter price/listing churn. This is regen-reflects-fetch-time data, the same class the deterministic-build PR (#2038) exists to make explicit.
Tests
test/model-catalog-validation.test.ts(6 tests): one pin that the committed catalog passes with zero violations (this is the standing regression guard for all four invariants), one fixture-violation test per invariant proving it fails loudly, and one pin that off-level variance alone is accepted.supports-xhigh.test.ts: existing 272000 pin updated to 1050000 (fail-unfixed for the codex fix).npm run generate-modelsexit 1 with the named violation and leaves the committed catalog untouched.Premise corrections vs the RES-1273 plan
validate-model-catalog.tsdid not exist on main; feat: refresh Prime Inference models from live API #1928 introduces a 9-line schema wrapper at the same path on its branch. This PR creates the invariant validator there with composable exports (validateModelCatalog(catalogLike)), so feat: refresh Prime Inference models from live API #1928 can call the same invariants on its hosted aggregate after rebase. Heads-up left for coordination: same path, trivial to reconcile, no second validator.defaultModelPerProviderlives inpackages/coding-agent, and importing it from the ai generation script requires a builtpi-aidist (generation must work pre-build) — a fragile reverse dependency. Open fix(ai): omit the default service tier, reprice cache writes from message_delta, repoint the zai default #2032 already adds the catalog-existence pin at the right layer (coding-agent tests), which fails in the same PR as any violating regen. Duplicating it in the generator would be a wrong-layer second checkpoint bought with a brittle import.Size
Total src: +175/−16 (net +159); tests: +156/−1 (net +155).
Linear: RES-1273
Note
Medium Risk
Changes model API routing and catalog metadata used at runtime; bad invariants now block releases, but incorrect classification would break Copilot or effort UI for affected models.
Overview
Adds catalog generation guards so
generate-modelsrunsvalidateModelCatalogbefore writingmodels.generated.tsand exits with a failure (nonzero exit code) when invariants break, instead of shipping bad rows.The validator and generator now derive fixes for known bad metadata: clamp
maxTokenstocontextWindow, classify GitHub Copilot models via sharedcopilotModelApi(including Grok/MAI →openai-responses), lift openai-codex GPT-5.6 family context to match 1M+ openai twins (except verified-smallergpt-6-astra), align Kimi K3 thinking maps per transport (low/high where effort is sendable; all-null where it is not), and strip phantom xhigh/max levels on budget-token Anthropic paths and on completions transports that cannot send reasoning effort.Coding-agent behavior is adjusted so switching to a reasoning model with no selectable thinking levels no longer persists
offas the user default, and cycling effort on such a model is a no-op. Tests pin the committed catalog as valid and cover each invariant plus the session thinking-default fix.Reviewed by Cursor Bugbot for commit e4e6cff. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add catalog invariant validation to
generateModelsand fix thinking-level bugsvalidateModelCatalogin validate-model-catalog.ts which checks token-window, Copilot-routing, Codex-window, transport-capability, Anthropic-budget, and cross-provider thinking-level invariants; generation now throws and exits with status 1 on any violationcopilotModelApiCODEX_SMALLER_WINDOW_VERIFIEDAgentSession.setThinkingLevel,cycleThinkingLevel, and_getThinkingLevelForModelSwitchso reasoning models with no selectable levels no longer overwrite the persisted thinking default withoffvalidateModelCatalogor generation will exit nonzeroMacroscope summarized e4e6cff.