diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 18570064c..20d4ed6bb 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added a Cursor variant-collapse table folding the per-effort Grok siblings (`cursor-grok-4.5` low/medium/high and `cursor-grok-4.6` low/medium/high/xhigh, plus their `-fast` service-tier lanes) into one logical model per lane with effort routing onto the live wire ids, matching Devin's `grok-4-5` collapse ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)). + - Regenerated the Cursor agent protobufs to model hosted WebFetch permission queries (`interaction_query` / `interaction_response` field 9) and the matching `ToolCall` variant (field 37). ### Fixed @@ -17,6 +19,7 @@ - ChatGPT Codex discovery that advertises only worker `-wm` SKUs now also registers the plain model route, so a configured `openai-codex/` keeps resolving instead of fuzzy-falling-back to the `-wm` SKU some accounts reject. - Fixed `opencode-go/muse-spark-1.2` (and `muse-spark-1.2-contributor`) failing every tool-call turn with `OpenAI completions stream closed before a finish_reason was received`. The Go gateway serves these ids only at `/zen/go/v1/responses`, but the `/zen/go/v1/models` discovery omits the `provider.npm` hint, so the resolver fell through to `openai-completions`; both ids are now pinned to `openai-responses` like `deepseek-v4-flash` ([#8957](https://github.com/can1357/oh-my-pi/issues/8957)). - Fixed GitHub Copilot `grok-4.6` / `grok-4.6-1m` failing with HTTP 400 `unsupported_api_for_model` by routing them through the OpenAI Responses API (`/responses`) instead of `/chat/completions`, matching `grok-4.5`. Stale cached completion routes are invalidated on refresh ([#8807](https://github.com/can1357/oh-my-pi/issues/8807)). +- Fixed Cursor Grok 4.5/4.6 discovery classifying the versioned ids as non-reasoning: `GetUsableModels` ships no `thinkingDetails` and the bundled references read `reasoning: false`, so the picker hid the effort ladder. Discovery now marks `cursor-grok-` ids as reasoning models (the non-reasoning `grok-code-*` ids stay out) ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)). ## [17.3.6] - 2026-08-17 diff --git a/packages/catalog/src/discovery/cursor.ts b/packages/catalog/src/discovery/cursor.ts index 76473a90e..2fc351755 100644 --- a/packages/catalog/src/discovery/cursor.ts +++ b/packages/catalog/src/discovery/cursor.ts @@ -28,6 +28,15 @@ const CURSOR_MAX_MODE_1M_ID_PATTERN = /claude|gemini/; /** Kimi's official bare K3 id (`k3`, `kimi/k3`); `k3-256k` is the 256k SKU and stays out. */ const CURSOR_KIMI_K3_BARE_ID_PATTERN = /(^|\/)k3$/i; +/** + * Versioned Cursor Grok ids (`cursor-grok-4.5`, `cursor-grok-4.6-high`) are + * reasoning models whose effort is carried in the per-tier sibling id. + * `GetUsableModels` ships no `thinkingDetails` and the bundled references read + * `reasoning: false`, so classification falls back to the id. The non-reasoning + * `grok-code-*` coding models lack the version digit and stay out. + */ +const CURSOR_GROK_REASONING_ID_PATTERN = /^cursor-grok-\d/i; + /** * Model-id families whose native catalogs (anthropic, openai/openai-codex, * google) are multimodal. Cursor-only or text-only families (`composer-*`, @@ -297,7 +306,11 @@ function normalizeCursorModel( const name = pickModelDisplayName(details, id); const reference = references.get(id); - const reasoning = isKimiK3ModelId(id) || Boolean(details.thinkingDetails) || reference?.reasoning === true; + const reasoning = + isKimiK3ModelId(id) || + CURSOR_GROK_REASONING_ID_PATTERN.test(id) || + Boolean(details.thinkingDetails) || + reference?.reasoning === true; if (reference) { return { diff --git a/packages/catalog/src/variant-collapse.ts b/packages/catalog/src/variant-collapse.ts index 89bacc111..d26fee691 100644 --- a/packages/catalog/src/variant-collapse.ts +++ b/packages/catalog/src/variant-collapse.ts @@ -111,19 +111,20 @@ function thinkingPair(baseId: string, name: string): EffortVariantFamily { }; } -type DevinTierRoutes = Partial>; +type TierRoutes = Partial>; /** Devin families with a `-max` sibling: five wire tiers, `low` floor. */ const DEVIN_FIVE_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max]; /** Pre-5.6 Devin GPT families top out at `-xhigh`: four wire tiers, `low` floor. */ const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]; -function devinTierFamily( - id: string, - name: string, - routes: DevinTierRoutes, - efforts: readonly Effort[], -): EffortVariantFamily { +/** + * Build one effort-tier family from a tier→wire-id map: routing keeps only the + * listed efforts, `members` dedupes the targets in tier order, and thinking is + * `mode: "effort"` (mandatory when the family has no `off` route). Shared by the + * Devin and Cursor tables, whose per-effort siblings follow the same shape. + */ +function tierFamily(id: string, name: string, routes: TierRoutes, efforts: readonly Effort[]): EffortVariantFamily { const routing: Partial> = {}; if (routes.off) routing.off = routes.off; for (const effort of efforts) { @@ -177,7 +178,7 @@ function devinTierFamily( function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] { const base = `gpt-5-6-${variant}`; return [ - devinTierFamily( + tierFamily( base, name, { @@ -190,7 +191,7 @@ function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): re }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( `${base}-fast`, `${name} Fast`, { @@ -395,7 +396,7 @@ export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }; export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { families: [ - devinTierFamily( + tierFamily( "claude-opus-5", "Claude Opus 5", { @@ -407,7 +408,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-opus-5-fast", "Claude Opus 5 Fast", { @@ -419,7 +420,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-fable-5", "Claude Fable 5", { @@ -431,7 +432,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-sonnet-5", "Claude Sonnet 5", { @@ -443,7 +444,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-opus-4-7", "Claude Opus 4.7", { @@ -455,7 +456,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-opus-4-7-fast", "Claude Opus 4.7 Fast", { @@ -467,7 +468,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-opus-4-8", "Claude Opus 4.8", { @@ -479,7 +480,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "claude-opus-4-8-fast", "Claude Opus 4.8 Fast", { @@ -491,7 +492,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-2", "GPT-5.2", { @@ -503,7 +504,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-3-codex", "GPT-5.3 Codex", { @@ -514,7 +515,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-3-codex-fast", "GPT-5.3 Codex Fast", { @@ -525,7 +526,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-4", "GPT-5.4", { @@ -537,7 +538,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-4-fast", "GPT-5.4 Fast", { @@ -549,7 +550,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-4-mini", "GPT-5.4 Mini", { @@ -560,7 +561,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-5", "GPT-5.5", { @@ -572,7 +573,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FOUR_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gpt-5-5-fast", "GPT-5.5 Fast", { @@ -587,7 +588,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { ...devinGpt56Families("luna", "GPT-5.6 Luna"), ...devinGpt56Families("sol", "GPT-5.6 Sol"), ...devinGpt56Families("terra", "GPT-5.6 Terra"), - devinTierFamily( + tierFamily( "kimi-k3", "Kimi K3", { @@ -597,7 +598,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Low, Effort.High, Effort.Max], ), - devinTierFamily( + tierFamily( "swe-1-7", "SWE-1.7", { @@ -606,7 +607,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Medium, Effort.Max], ), - devinTierFamily( + tierFamily( "grok-4-5", "Grok 4.5", { @@ -616,7 +617,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Low, Effort.Medium, Effort.High], ), - devinTierFamily( + tierFamily( "inkling", "Inkling", { @@ -629,7 +630,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, DEVIN_FIVE_TIER_EFFORTS, ), - devinTierFamily( + tierFamily( "gemini-3-1-pro", "Gemini 3.1 Pro", { @@ -638,7 +639,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Low, Effort.High], ), - devinTierFamily( + tierFamily( "gemini-3-5-flash", "Gemini 3.5 Flash", { @@ -649,7 +650,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], ), - devinTierFamily( + tierFamily( "gemini-3-6-flash", "Gemini 3.6 Flash", { @@ -660,7 +661,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], ), - devinTierFamily( + tierFamily( "gemini-3-flash", "Gemini 3 Flash", { @@ -694,7 +695,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { }, // GLM-5.2 1M — paid variants that consume weekly quota. Collapse the // three 1M-context variants into one entry with proper effort routing. - devinTierFamily( + tierFamily( "glm-5-2-1m", "GLM-5.2 1M", { @@ -707,11 +708,43 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { ], }; +/** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */ +const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High]; +const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]; + +/** + * Cursor serves Grok 4.5/4.6 as per-effort sibling ids + * (`cursor-grok-4.6-low|-medium|-high|-xhigh`) alongside a parallel `-fast` + * service-tier lane (`-low-fast`, …). Each lane collapses into its own logical + * model — the `-fast` axis is a sibling family, never a second routing + * dimension — with effort routing onto the live sibling wire ids. + */ +function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]): readonly EffortVariantFamily[] { + const build = (fast: boolean): EffortVariantFamily => { + const suffix = fast ? "-fast" : ""; + const routes: TierRoutes = {}; + for (const effort of efforts) { + routes[effort] = `cursor-grok-${version}-${effort}${suffix}`; + } + return tierFamily(`cursor-grok-${version}${suffix}`, `Grok ${version}${fast ? " Fast" : ""}`, routes, efforts); + }; + return [build(false), build(true)]; +} + +/** `cursor` Grok families: per-effort siblings collapsed per service-tier lane. */ +export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = { + families: [ + ...cursorGrokFamilies("4.5", CURSOR_GROK_45_EFFORTS), + ...cursorGrokFamilies("4.6", CURSOR_GROK_46_EFFORTS), + ], +}; + /** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */ export const VARIANT_COLLAPSE_TABLES: Readonly> = { "google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE, "google-gemini-cli": GEMINI_CLI_VARIANT_COLLAPSE_TABLE, devin: DEVIN_VARIANT_COLLAPSE_TABLE, + cursor: CURSOR_VARIANT_COLLAPSE_TABLE, }; /** diff --git a/packages/catalog/test/cursor-discovery.test.ts b/packages/catalog/test/cursor-discovery.test.ts index eb758911c..cf272a599 100644 --- a/packages/catalog/test/cursor-discovery.test.ts +++ b/packages/catalog/test/cursor-discovery.test.ts @@ -25,6 +25,10 @@ const FIXTURE_MODEL_IDS = [ "kimi-k3-low", "kimi-k3-max", "grok-code-fast-2", + // Versioned Cursor Grok siblings: bundled references read reasoning:false, + // but the id marks them reasoning. + "cursor-grok-4.5-high", + "cursor-grok-4.6-xhigh", // Bundled-reference ids: the reference stays authoritative. "claude-4.5-opus-high", "claude-4.6-opus-high", @@ -92,6 +96,14 @@ describe("cursor discovery input modalities (issue #4726)", () => { expect(byId.get("kimi-k3-max")?.reasoning).toBe(true); }); + it("marks versioned Cursor Grok ids as reasoning despite reasoning:false references (issue #8803)", async () => { + const byId = await discover(); + expect(byId.get("cursor-grok-4.5-high")?.reasoning).toBe(true); + expect(byId.get("cursor-grok-4.6-xhigh")?.reasoning).toBe(true); + // grok-code-* coding models lack the version digit and stay non-reasoning. + expect(byId.get("grok-code-fast-2")?.reasoning).toBe(false); + }); + it("keeps bundled references authoritative for input modalities", async () => { const byId = await discover(); // Bundled cursor references carry their own input classification; the diff --git a/packages/catalog/test/variant-collapse.test.ts b/packages/catalog/test/variant-collapse.test.ts index 184166736..14f7159be 100644 --- a/packages/catalog/test/variant-collapse.test.ts +++ b/packages/catalog/test/variant-collapse.test.ts @@ -15,6 +15,7 @@ import { googleGeminiCliModelManagerOptions } from "@oh-my-pi/pi-catalog/provide import type { ModelSpec } from "@oh-my-pi/pi-catalog/types"; import { ANTIGRAVITY_VARIANT_COLLAPSE_TABLE, + CURSOR_VARIANT_COLLAPSE_TABLE, collapseEffortVariants, collapseEffortVariantsAcrossProviders, DEVIN_VARIANT_COLLAPSE_TABLE, @@ -656,6 +657,77 @@ describe("Devin tier routing", () => { }); }); +describe("Cursor Grok tier routing (issue #8803)", () => { + function cursorMemberSpec(id: string): ModelSpec<"cursor-agent"> { + return { + id, + name: id, + api: "cursor-agent", + provider: "cursor", + baseUrl: "https://api2.cursor.sh", + // Live GetUsableModels + bundled references mark these reasoning:false; + // collapse must still recognize the effort ladder. + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 64_000, + }; + } + + const RAW_SIBLINGS = [ + "cursor-grok-4.5-high", + "cursor-grok-4.5-high-fast", + "cursor-grok-4.5-low", + "cursor-grok-4.5-low-fast", + "cursor-grok-4.5-medium", + "cursor-grok-4.5-medium-fast", + "cursor-grok-4.6-high", + "cursor-grok-4.6-high-fast", + "cursor-grok-4.6-low", + "cursor-grok-4.6-low-fast", + "cursor-grok-4.6-medium", + "cursor-grok-4.6-medium-fast", + "cursor-grok-4.6-xhigh", + "cursor-grok-4.6-xhigh-fast", + ]; + + it("collapses the 14 effort siblings into four logical models, split by the -fast lane", () => { + const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE); + expect(collapsed.map(model => model.id).sort()).toEqual([ + "cursor-grok-4.5", + "cursor-grok-4.5-fast", + "cursor-grok-4.6", + "cursor-grok-4.6-fast", + ]); + + const g46 = collapsed.find(model => model.id === "cursor-grok-4.6"); + if (!g46) throw new Error("cursor-grok-4.6 did not collapse"); + expect(g46.name).toBe("Grok 4.6"); + // Effort route forces reasoning even though every member said false. + expect(g46.reasoning).toBe(true); + expect(g46.thinking?.mode).toBe("effort"); + expect(g46.thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]); + expect(g46.thinking?.requiresEffort).toBe(true); + }); + + it("routes each user effort onto the live sibling wire id per service-tier lane", () => { + const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE); + const model = (id: string) => { + const found = collapsed.find(m => m.id === id); + if (!found) throw new Error(`${id} did not collapse`); + return buildModel(found as ModelSpec<"cursor-agent">); + }; + + expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.XHigh)).toBe("cursor-grok-4.6-xhigh"); + expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.Low)).toBe("cursor-grok-4.6-low"); + expect(resolveWireModelId(model("cursor-grok-4.6-fast"), Effort.High)).toBe("cursor-grok-4.6-high-fast"); + expect(resolveWireModelId(model("cursor-grok-4.5"), Effort.Medium)).toBe("cursor-grok-4.5-medium"); + // 4.5 has no xhigh sibling: the ceiling stays at high. + expect(model("cursor-grok-4.5").thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High]); + }); +}); + describe("variant aliases", () => { it("resolves members and recycled ids per provider", () => { expect(resolveVariantAlias("google-antigravity", "gemini-3.5-flash-low")).toBe("gemini-3.5-flash");