From 466f769cc85a1680bfa1856b399aad11ba6d08b2 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 19 Aug 2026 09:52:42 +0000 Subject: [PATCH] fix(catalog): collapse Cursor Grok 4.5/4.6 effort siblings Cursor advertises Grok 4.5/4.6 as per-effort sibling ids (cursor-grok-4.6-low|-medium|-high|-xhigh plus -fast variants), but VARIANT_COLLAPSE_TABLES had no cursor entry, so the model hub showed 14 unrouted siblings instead of one logical model with effort routing. GetUsableModels ships no thinkingDetails and the bundled references read reasoning:false, so the picker also treated them as non-reasoning. - Add CURSOR_VARIANT_COLLAPSE_TABLE folding each service-tier lane (standard + -fast) into one logical model with effort routing onto the live wire ids, mirroring Devin's grok-4-5 collapse. - Rename the generic devinTierFamily/DevinTierRoutes helper to tierFamily/TierRoutes now that both Devin and Cursor tables use it. - Mark versioned cursor-grok- ids as reasoning during discovery (grok-code-* coding models stay non-reasoning). Fixes #8803 --- packages/catalog/CHANGELOG.md | 3 + packages/catalog/src/discovery/cursor.ts | 15 ++- packages/catalog/src/variant-collapse.ts | 101 ++++++++++++------ .../catalog/test/cursor-discovery.test.ts | 12 +++ .../catalog/test/variant-collapse.test.ts | 72 +++++++++++++ 5 files changed, 168 insertions(+), 35 deletions(-) diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 390d0adee..0b22ce497 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 @@ -14,6 +16,7 @@ - Fixed SuperGrok (`xai-oauth`) Grok 4.6 hiding the thinking-level picker: the Responses effort-capable allowlist now includes `grok-4.6`, so `/model` can select the documented `low`/`medium`/`high`/`xhigh` ladder (`max` is rejected by api.x.ai). - Marked CoreWeave runtime discovery as authoritative so stale bundled model ids that the endpoint no longer serves stop appearing as selectable models. - 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 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");