feat(ai): validate catalog generation invariants and fail violating regens - #2042
Open
snimu wants to merge 3 commits into
Open
feat(ai): validate catalog generation invariants and fail violating regens#2042snimu wants to merge 3 commits into
snimu wants to merge 3 commits into
Conversation
…egens New scripts/validate-model-catalog.ts, run by generate-models before writing: - maxTokens <= contextWindow on every row - every GitHub Copilot model id maps to an explicitly classified family and the row's api matches that classification (unclassified families fail by name instead of silently misrouting to chat completions) - openai-codex rows may not diverge more than 2x from the same model's openai contextWindow - same-model rows across providers must agree on selectable thinking levels (the off level is excluded: disabling thinking legitimately varies per transport) On violation the generator prints each offense, refuses to write the catalog, and exits nonzero. Data fixes shipped by regen-time derivation, not hand-edits: - Copilot Grok and MAI-Code families route to openai-responses (Copilot rejects them on /chat/completions; matches upstream pi-mono #906): 4 rows. - Codex 5.6-family contextWindow derives from the openai row when the API side serves 1M+ (kills the flat 272000 from the 2026-01 measurement): 3 rows now 1050000, existing supports-xhigh pin updated. - models.dev ingestion clamps maxTokens to contextWindow (Inkling-Small carried its 1M sibling's output limit; upstream data is transposed). - KIMI_K3_THINKING_LEVEL_MAP aligns with the low/high/max levels the model supports on OpenRouter and Prime Inference (12 rows gain low/high). Squashes 1683/1680 (maxTokens>contextWindow), 1406 (Copilot grok misroute, now also mai-code), 1597 (codex 272k), 1725's one live conflict (kimi-k3), and guards the 1679/1724 staleness class at generation time. Linear: RES-1273
…able maps Review follow-ups on the catalog invariants: - The consistency invariant now derives each row's selectable levels from getSupportedThinkingLevels (absent keys count as supported at runtime) instead of comparing raw map keys. - New coherence invariant: a plain openai-format completions row whose resolved compat cannot send reasoning effort must not offer selectable levels; the zai/qwen/deepseek/openrouter formats keep their maps (they use them as enable toggles). getCompat is exported so the generator and validator share the provider's own resolution rules. - Generation nulls the unsendable levels the shared kimi-k3 map put on the moonshotai, moonshotai-cn, and github-copilot rows (3 rows; the phantom "max" predates this branch). Effort-capable routes (openrouter, prime-inference, huggingface, fireworks, opencode, kimi-coding, vercel) keep low/high/max. Linear: RES-1273
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 1b8199e. Configure here.
…t selectable levels The catalog now represents "reasons at server default, effort not client-selectable" as a reasoning model whose thinkingLevelMap nulls every level, so getSupportedThinkingLevels can return an empty set. Runtime handling for that state: - setThinkingLevel no longer persists the forced "off" clamp as the user's default thinking level when the model offers no selectable levels - switching away from such a model restores the persisted default instead of carrying the clamped "off" onto capable models - cycleThinkingLevel returns undefined instead of indexing the empty list with (0 % 0) Requests were never affected (these transports cannot send reasoning effort), and /effort already degraded to "does not support thinking". 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, 22 rows changed, zero unrelated drift)
grok-/mai-(andgpt-5/oswe) toopenai-responses; matches upstream pi-mono #906, which validated Grok as Responses-onlyopenairow when the API side serves 1M+; the 272k constant stays for the generation it was measured on (5.1-5.5 divergences are <2x measured limits)thinkingmachines/Inkling-SmallmaxTokens 1,048,576 > contextWindow 524,288 (#1683)createPrimeInferenceModel)KIMI_K3_THINKING_LEVEL_MAPaligned with the low/high/max levels the model demonstrably supports on OpenRouter and Prime Inference (both routes pass efforts through to Moonshot serving)Verified: the regenerated catalog is byte-stable across a second regen, and the CI-mirroring
npm run build(which still regenerates on main) reproduces it exactly.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 models from a hosted catalog #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 models from a hosted catalog #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
Net src/scripts LOC: +127 mechanism (validator 108, generator wiring +19 net), 2 test files +72/-1, catalog data ±58 regenerated lines. Mechanism-vs-deletion: the invariants are additive tooling; the fixes delete wrong premises (prefix heuristic → explicit classification, flat constant → derived per-model context, stale hand map → source-aligned map).
Linear: RES-1273
Note
Medium Risk
Touches model routing and catalog metadata used by all providers; mistakes could break completions or effort UX, but validation fails closed and tests pin the committed catalog.
Overview
Adds
validateModelCatalogand runs it at the end ofgenerate-modelsso a regen that breaks known rules logs named violations, sets a nonzero exit code, and does not writemodels.generated.ts. Invariants covermaxTokensvs context window, explicit GitHub Copilot API family routing (includinggrok-/mai-→openai-responses), openai-codex context staying within 2× of the matching openai row, and consistent selectable thinking levels across providers (with a generator pass that nulls levels transports cannot send).The regen fixes the violations those checks caught: Copilot Grok/MAI-Code misrouted to chat completions, GPT-5.6 openai-codex context raised to ~1M when the OpenAI twin serves it, Kimi K3 low/high effort on providers that support them, Moonshot/Copilot Kimi rows stripped of bogus effort UI, and Inkling-Small maxTokens clamped.
getCompatis exported so validation reuses the same transport rules as runtime.In coding-agent, switching to a reasoning model with zero selectable effort levels no longer overwrites the persisted default to
off; cycle effort is a no-op on those models.Reviewed by Cursor Bugbot for commit 36e9b01. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Validate catalog generation invariants and fail violating regens
getCompatfrom openai-completions.ts so validation reuses the same transport compatibility logic as the runtime.Macroscope summarized 36e9b01.