-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(ai): validate catalog generation invariants and fail violating regens #2042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
snimu
wants to merge
5
commits into
main
Choose a base branch
from
feat/catalog-generation-invariants
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a1850ac
feat(ai): validate catalog generation invariants and fail violating r…
snimu b0b720e
fix(coding-agent): preserve the thinking default across models withou…
snimu cd1d0c1
fix(ai): reject and null budget-clamped thinking levels on anthropic-…
snimu e4e6cff
refactor(ai): condense guard comments and drop a pin owned by merged …
snimu 118da87
Merge remote-tracking branch 'origin/main' into _merge_2042
snimu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
packages/ai/.changes/res-1273-catalog-generation-invariants.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| - Fixed GitHub Copilot Grok and MAI-Code models failing on chat completions by routing them through the Copilot Responses API. | ||
| - Raised the Codex subscription (openai-codex) context window for the GPT-5.6 family from 272k to the full 1,050,000 measured on the API side. | ||
| - Added low/high reasoning effort levels for Kimi K3 on effort-capable providers (OpenRouter, Prime Inference, Hugging Face, Fireworks, opencode, Kimi Coding, Vercel); the Moonshot and GitHub Copilot transports cannot send effort, so their rows now advertise no selectable levels instead of a no-op max. | ||
| - Fixed thinkingmachines/Inkling-Small advertising a maxTokens above its context window. | ||
| - Catalog regeneration now validates invariants (maxTokens vs context window, Copilot API classification, Codex context divergence, cross-provider thinking-level consistency) and fails instead of writing a violating catalog. |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { getSupportedThinkingLevels } from "../src/models.js"; | ||
| import { supportsAdaptiveThinking } from "../src/providers/anthropic.js"; | ||
| import { getCompat } from "../src/providers/openai-completions.js"; | ||
| import type { Api, Model } from "../src/types.js"; | ||
|
|
||
| /** The subset of a catalog row the invariants read; MODELS rows satisfy it. */ | ||
| export interface CatalogRowLike { | ||
| id: string; | ||
| api: string; | ||
| provider: string; | ||
| contextWindow: number; | ||
| maxTokens: number; | ||
| reasoning: boolean; | ||
| baseUrl?: string; | ||
| thinkingLevelMap?: Readonly<Record<string, string | null>>; | ||
| compat?: Readonly<Record<string, unknown>>; | ||
| } | ||
|
|
||
| export type CatalogLike = Readonly<Record<string, Readonly<Record<string, CatalogRowLike>>>>; | ||
|
|
||
| /** Copilot serves each model family through exactly one endpoint; unclassified ids fail validation. */ | ||
| export function copilotModelApi(modelId: string): Api | undefined { | ||
| if (modelId.startsWith("claude-")) return "anthropic-messages"; | ||
| // Responses-only on Copilot; /chat/completions rejects these families (upstream pi-mono #906). | ||
| if ( | ||
| modelId.startsWith("gpt-5") || | ||
| modelId.startsWith("gpt-6") || | ||
| modelId.startsWith("oswe") || | ||
| modelId.startsWith("grok-") || | ||
| modelId.startsWith("mai-") | ||
| ) { | ||
| return "openai-responses"; | ||
| } | ||
| if (modelId.startsWith("gemini-") || modelId.startsWith("kimi-")) return "openai-completions"; | ||
| return undefined; | ||
| } | ||
|
|
||
| // ChatGPT-backend window verified smaller than the API side (Codex CLI models.json, rust-v0.153.4). | ||
| export const CODEX_SMALLER_WINDOW_VERIFIED = new Set(["gpt-6-astra"]); | ||
|
|
||
| function familyKey(modelId: string): string { | ||
| const segments = modelId.split("/"); | ||
| return segments[segments.length - 1].toLowerCase(); | ||
| } | ||
|
|
||
| // Runtime-selectable levels via the UI's own function; compared within one transport, declared maps only, modulo "off". | ||
| function selectableLevels(model: CatalogRowLike): string { | ||
| return getSupportedThinkingLevels(model as Model<Api>) | ||
| .filter((level) => level !== "off") | ||
| .join(","); | ||
| } | ||
|
|
||
| // Plain openai-format completions gate reasoning params on compat; other formats use the map as an enable toggle. | ||
| function effortIsSendable(model: CatalogRowLike): boolean { | ||
| // Model requires baseUrl, so rows without one behave like the empty-string rows: provider-only detection. | ||
| const compat = getCompat({ ...model, baseUrl: model.baseUrl ?? "" } as Model<"openai-completions">); | ||
| return compat.thinkingFormat !== "openai" || compat.supportsReasoningEffort; | ||
| } | ||
|
|
||
| /** Generation-time catalog invariants; an empty return means the catalog is valid. */ | ||
| export function validateModelCatalog(catalog: CatalogLike): string[] { | ||
| const violations: string[] = []; | ||
|
|
||
| for (const [provider, models] of Object.entries(catalog)) { | ||
| for (const model of Object.values(models)) { | ||
| if (model.maxTokens > model.contextWindow) { | ||
| violations.push( | ||
| `${provider}/${model.id}: maxTokens ${model.maxTokens} exceeds contextWindow ${model.contextWindow}`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const model of Object.values(catalog["github-copilot"] ?? {})) { | ||
| const expectedApi = copilotModelApi(model.id); | ||
| if (expectedApi === undefined) { | ||
| violations.push( | ||
| `github-copilot/${model.id}: unclassified model family; add it to copilotModelApi in validate-model-catalog.ts`, | ||
| ); | ||
| } else if (model.api !== expectedApi) { | ||
| violations.push(`github-copilot/${model.id}: api ${model.api} does not match classification ${expectedApi}`); | ||
| } | ||
| } | ||
|
|
||
| for (const model of Object.values(catalog["openai-codex"] ?? {})) { | ||
| const openaiTwin = catalog.openai?.[model.id]; | ||
| if (!openaiTwin || CODEX_SMALLER_WINDOW_VERIFIED.has(model.id)) continue; | ||
| const ratio = openaiTwin.contextWindow / model.contextWindow; | ||
| if (ratio > 2 || ratio < 0.5) { | ||
| violations.push( | ||
| `openai-codex/${model.id}: contextWindow ${model.contextWindow} diverges more than 2x from openai/${model.id} (${openaiTwin.contextWindow})`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const familyLevels = new Map<string, Map<string, string>>(); | ||
| for (const [provider, models] of Object.entries(catalog)) { | ||
| for (const model of Object.values(models)) { | ||
| if (!model.thinkingLevelMap || !model.reasoning) continue; | ||
| if (model.api === "anthropic-messages" && !supportsAdaptiveThinking(model.id)) { | ||
| const clamped = ["xhigh", "max"].filter((level) => model.thinkingLevelMap?.[level] != null); | ||
| if (clamped.length > 0) { | ||
| violations.push( | ||
| `${provider}/${model.id}: thinkingLevelMap offers [${clamped.join(",")}] but the budget path serializes them as high`, | ||
| ); | ||
| } | ||
| } | ||
| if (model.api === "openai-completions" && !effortIsSendable(model)) { | ||
| const levels = selectableLevels(model); | ||
| if (levels.length > 0) { | ||
| violations.push( | ||
| `${provider}/${model.id}: thinkingLevelMap offers [${levels}] but the transport cannot send reasoning effort`, | ||
| ); | ||
| } | ||
| continue; | ||
| } | ||
| const key = `${familyKey(model.id)} [${model.api}]`; | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| const seen = familyLevels.get(key) ?? new Map<string, string>(); | ||
| seen.set(`${provider}/${model.id}`, selectableLevels(model)); | ||
| familyLevels.set(key, seen); | ||
| } | ||
| } | ||
| for (const [key, seen] of familyLevels) { | ||
| const distinct = new Set(seen.values()); | ||
| if (distinct.size > 1) { | ||
| const detail = [...seen.entries()].map(([row, levels]) => `${row}=[${levels}]`).join(", "); | ||
| violations.push(`${key}: selectable thinking levels disagree across providers: ${detail}`); | ||
| } | ||
| } | ||
|
|
||
| return violations; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.