From c879ec95ce07fce791409b6c472f1460bbf535b1 Mon Sep 17 00:00:00 2001 From: Johnny Bae Date: Fri, 31 Jul 2026 13:26:36 +0900 Subject: [PATCH] fix(cursor): support current Grok 4.5 Fast wire IDs --- src/adapters/cursor/discovery.ts | 18 ++++++++++++++---- src/adapters/cursor/effort-map.ts | 24 ++++++++++++++++++------ src/adapters/cursor/request-builder.ts | 4 ++-- tests/cursor-discovery.test.ts | 12 ++++++++++++ tests/cursor-effort-suffix.test.ts | 18 ++++++++++++++---- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/adapters/cursor/discovery.ts b/src/adapters/cursor/discovery.ts index 6e95b28216d..747fb32d1df 100644 --- a/src/adapters/cursor/discovery.ts +++ b/src/adapters/cursor/discovery.ts @@ -1,4 +1,8 @@ -import { CANONICAL_EFFORT_SUFFIXES, cursorModelEffortLadder } from "./effort-map"; +import { + CANONICAL_EFFORT_SUFFIXES, + cursorModelEffortLadder, + cursorWireModelIdWithEffort, +} from "./effort-map"; export interface CursorModelInfo { id: string; @@ -66,14 +70,20 @@ function stripCursorWirePrefix(id: string): string { /** * True when a configured Cursor base model should remain exposed after live GetUsableModels filtering. - * Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly or by prefix. + * Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly, the + * ordinary `{base}-{effort}` form, or Cursor's current `{base-without-fast}-{effort}-fast` form. */ export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean { return liveIds.some(raw => { const id = stripCursorWirePrefix(raw); if (id === modelId) return true; - const effortPrefix = `${modelId}-`; - return id.startsWith(effortPrefix) && CANONICAL_EFFORT_SUFFIXES.has(id.slice(effortPrefix.length)); + for (const effort of CANONICAL_EFFORT_SUFFIXES) { + if ( + id === `${modelId}-${effort}` || + id === cursorWireModelIdWithEffort(modelId, effort) + ) return true; + } + return false; }); } diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index ddb61311418..7b9baa80efe 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -3,8 +3,9 @@ * * Cursor model ids encode the reasoning effort as a suffix (`claude-4.6-opus-high`), and the available * tiers differ per model — `claude-4.6-opus` tops out at `-max`, `claude-opus-4-8` at `-xhigh`, - * `claude-4.6-sonnet` only has `-medium`, and `composer`/`grok`/`gemini` take no suffix at all. A bare - * id for a model that requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105). + * `claude-4.6-sonnet` only has `-medium`, and most `composer`/`gemini` models take no suffix at all. + * Grok Fast puts its mode marker after the effort (`grok-4.5-high-fast`). A bare id for a model that + * requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105). * * Canonical effort order is always low < medium < high < xhigh < max (max is the top tier, confirmed * against Anthropic docs and Cursor's live lineup). Tiers are stored in ascending canonical order. @@ -30,10 +31,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record = { // GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max}; // the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder. "kimi-k3": ["low", "high", "max"], - // GetUsableModels (2026-07-09) lists grok-4.5-{medium,high,xhigh} and grok-4.5-fast-{medium,high,xhigh}; - // the bare "grok-4.5-fast" id was removed upstream and now returns not_found. - "grok-4.5": ["medium", "high", "xhigh"], - "grok-4.5-fast": ["medium", "high", "xhigh"], + // Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and + // cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found. + "grok-4.5": ["low", "medium", "high"], + "grok-4.5-fast": ["low", "medium", "high"], "gpt-5.1": ["low", "high"], "gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"], "gpt-5.1-codex-mini": ["low", "high"], @@ -113,3 +114,14 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin export function cursorModelHasEffortTiers(baseModelId: string): boolean { return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0; } + +/** + * Compose a Cursor wire id from a Codex-facing base id and effort tier. + * Fast variants put the mode after the effort; other models use the ordinary `{base}-{effort}` form. + */ +export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string { + if (baseModelId.endsWith("-fast")) { + return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`; + } + return `${baseModelId}-${effortSuffix}`; +} diff --git a/src/adapters/cursor/request-builder.ts b/src/adapters/cursor/request-builder.ts index e5486cc097b..b837081baa8 100644 --- a/src/adapters/cursor/request-builder.ts +++ b/src/adapters/cursor/request-builder.ts @@ -10,7 +10,7 @@ import type { import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types"; import type { CursorRequestMessage, CursorRunRequest } from "./types"; import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery"; -import { cursorEffortSuffix } from "./effort-map"; +import { cursorEffortSuffix, cursorWireModelIdWithEffort } from "./effort-map"; import { cursorMcpToolEncodedSize, cursorMcpToolsEncodedSize, @@ -127,7 +127,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): { modelId: const selection = cursorWireModelSelection(modelId); const id = selection.modelId; const suffix = cursorEffortSuffix(id, reasoning); - return { ...selection, modelId: suffix ? `${id}-${suffix}` : id }; + return { ...selection, modelId: suffix ? cursorWireModelIdWithEffort(id, suffix) : id }; } function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined { diff --git a/tests/cursor-discovery.test.ts b/tests/cursor-discovery.test.ts index 19f88a28533..c8343deda7c 100644 --- a/tests/cursor-discovery.test.ts +++ b/tests/cursor-discovery.test.ts @@ -63,7 +63,13 @@ describe("Cursor discovery metadata", () => { // Issue #117: Cursor GetUsableModels may return ids with a `cursor-` wire prefix. expect(isCursorModelAvailableForAccount("grok-4.5", ["cursor-grok-4.5-high"])).toBe(true); + // Current Grok Fast wire ids put `-fast` after the effort tier. + expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-low-fast"])).toBe(true); + expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-high-fast"])).toBe(true); + // Older snapshots used `{base}-fast-{effort}`; keep discovery compatibility. expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-fast-medium"])).toBe(true); + expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-high"])).toBe(false); + expect(isCursorModelAvailableForAccount("grok-4.5", ["cursor-grok-4.5-high-fast"])).toBe(false); expect(isCursorModelAvailableForAccount("gpt-5.4", ["cursor-gpt-5.4-high"])).toBe(true); // Prefixed sibling rejection: cursor- prefix must not bypass sibling-model checks. expect(isCursorModelAvailableForAccount("gpt-5.5", ["cursor-gpt-5.5-extra-high"])).toBe(false); @@ -74,6 +80,12 @@ describe("Cursor discovery metadata", () => { ["gpt-5.4-high"], ); expect(filtered.map(model => model.id)).toEqual(["gpt-5.4"]); + + const grok = filterCursorConfiguredModelsByLiveDiscovery( + [{ id: "grok-4.5" }, { id: "grok-4.5-fast" }], + ["cursor-grok-4.5-high", "cursor-grok-4.5-high-fast"], + ); + expect(grok.map(model => model.id)).toEqual(["grok-4.5", "grok-4.5-fast"]); }); test("live discovery filter always keeps all router levels when GetUsableModels omits them", () => { diff --git a/tests/cursor-effort-suffix.test.ts b/tests/cursor-effort-suffix.test.ts index 423cd9919b9..fd73f86bacd 100644 --- a/tests/cursor-effort-suffix.test.ts +++ b/tests/cursor-effort-suffix.test.ts @@ -60,10 +60,20 @@ describe("Cursor per-model reasoning-effort suffix", () => { expect(modelIdFor("cursor/glm-5.2", "max")).toBe("glm-5.2-max"); }); - test("grok-4.5-fast preserves its explicit code-backed tiers", () => { - expect(modelIdFor("cursor/grok-4.5-fast", "medium")).toBe("grok-4.5-fast-medium"); - expect(modelIdFor("cursor/grok-4.5-fast", "high")).toBe("grok-4.5-fast-high"); - expect(modelIdFor("cursor/grok-4.5-fast", "xhigh")).toBe("grok-4.5-fast-xhigh"); + test("grok-4.5 uses current low/medium/high tiers and trailing Fast wire ids", () => { + expect(modelIdFor("cursor/grok-4.5", "low")).toBe("grok-4.5-low"); + expect(modelIdFor("cursor/grok-4.5", "medium")).toBe("grok-4.5-medium"); + expect(modelIdFor("cursor/grok-4.5", "high")).toBe("grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5", "xhigh")).toBe("grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5")).toBe("grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5-fast", "low")).toBe("grok-4.5-low-fast"); + expect(modelIdFor("cursor/grok-4.5-fast", "medium")).toBe("grok-4.5-medium-fast"); + expect(modelIdFor("cursor/grok-4.5-fast", "high")).toBe("grok-4.5-high-fast"); + // Codex-only upper tiers and an omitted effort clamp to Cursor's current top tier. + expect(modelIdFor("cursor/grok-4.5-fast", "xhigh")).toBe("grok-4.5-high-fast"); + expect(modelIdFor("cursor/grok-4.5-fast")).toBe("grok-4.5-high-fast"); + expect(cursorModelEffortLadder("grok-4.5")).toEqual(["low", "medium", "high"]); + expect(cursorModelEffortLadder("grok-4.5-fast")).toEqual(["low", "medium", "high"]); }); test("kimi-k3 maps to its live effort-suffixed variants", () => {