Skip to content

Commit cd9ad73

Browse files
authored
Merge pull request lidge-jun#797 from jbaehova/agent/fix-cursor-grok-fast-wire-ids
Verified locally on top of origin/dev alongside lidge-jun#808: typecheck clean, 214 targeted tests pass (cursor-discovery, cursor-effort-suffix, cursor-request-builder, adapter-resolve, router, config, repo-hygiene), privacy:scan green.
2 parents 4d495e9 + c879ec9 commit cd9ad73

5 files changed

Lines changed: 60 additions & 16 deletions

File tree

src/adapters/cursor/discovery.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { CANONICAL_EFFORT_SUFFIXES, cursorModelEffortLadder } from "./effort-map";
1+
import {
2+
CANONICAL_EFFORT_SUFFIXES,
3+
cursorModelEffortLadder,
4+
cursorWireModelIdWithEffort,
5+
} from "./effort-map";
26

37
export interface CursorModelInfo {
48
id: string;
@@ -66,14 +70,20 @@ function stripCursorWirePrefix(id: string): string {
6670

6771
/**
6872
* True when a configured Cursor base model should remain exposed after live GetUsableModels filtering.
69-
* Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly or by prefix.
73+
* Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly, the
74+
* ordinary `{base}-{effort}` form, or Cursor's current `{base-without-fast}-{effort}-fast` form.
7075
*/
7176
export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean {
7277
return liveIds.some(raw => {
7378
const id = stripCursorWirePrefix(raw);
7479
if (id === modelId) return true;
75-
const effortPrefix = `${modelId}-`;
76-
return id.startsWith(effortPrefix) && CANONICAL_EFFORT_SUFFIXES.has(id.slice(effortPrefix.length));
80+
for (const effort of CANONICAL_EFFORT_SUFFIXES) {
81+
if (
82+
id === `${modelId}-${effort}` ||
83+
id === cursorWireModelIdWithEffort(modelId, effort)
84+
) return true;
85+
}
86+
return false;
7787
});
7888
}
7989

src/adapters/cursor/effort-map.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
*
44
* Cursor model ids encode the reasoning effort as a suffix (`claude-4.6-opus-high`), and the available
55
* tiers differ per model — `claude-4.6-opus` tops out at `-max`, `claude-opus-4-8` at `-xhigh`,
6-
* `claude-4.6-sonnet` only has `-medium`, and `composer`/`grok`/`gemini` take no suffix at all. A bare
7-
* id for a model that requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105).
6+
* `claude-4.6-sonnet` only has `-medium`, and most `composer`/`gemini` models take no suffix at all.
7+
* Grok Fast puts its mode marker after the effort (`grok-4.5-high-fast`). A bare id for a model that
8+
* requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105).
89
*
910
* Canonical effort order is always low < medium < high < xhigh < max (max is the top tier, confirmed
1011
* 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<string, readonly string[]> = {
3031
// GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
3132
// the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder.
3233
"kimi-k3": ["low", "high", "max"],
33-
// GetUsableModels (2026-07-09) lists grok-4.5-{medium,high,xhigh} and grok-4.5-fast-{medium,high,xhigh};
34-
// the bare "grok-4.5-fast" id was removed upstream and now returns not_found.
35-
"grok-4.5": ["medium", "high", "xhigh"],
36-
"grok-4.5-fast": ["medium", "high", "xhigh"],
34+
// Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and
35+
// cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
36+
"grok-4.5": ["low", "medium", "high"],
37+
"grok-4.5-fast": ["low", "medium", "high"],
3738
"gpt-5.1": ["low", "high"],
3839
"gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"],
3940
"gpt-5.1-codex-mini": ["low", "high"],
@@ -113,3 +114,14 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
113114
export function cursorModelHasEffortTiers(baseModelId: string): boolean {
114115
return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0;
115116
}
117+
118+
/**
119+
* Compose a Cursor wire id from a Codex-facing base id and effort tier.
120+
* Fast variants put the mode after the effort; other models use the ordinary `{base}-{effort}` form.
121+
*/
122+
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
123+
if (baseModelId.endsWith("-fast")) {
124+
return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`;
125+
}
126+
return `${baseModelId}-${effortSuffix}`;
127+
}

src/adapters/cursor/request-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
1111
import type { CursorRequestMessage, CursorRunRequest } from "./types";
1212
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
13-
import { cursorEffortSuffix } from "./effort-map";
13+
import { cursorEffortSuffix, cursorWireModelIdWithEffort } from "./effort-map";
1414
import {
1515
cursorMcpToolEncodedSize,
1616
cursorMcpToolsEncodedSize,
@@ -127,7 +127,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): { modelId:
127127
const selection = cursorWireModelSelection(modelId);
128128
const id = selection.modelId;
129129
const suffix = cursorEffortSuffix(id, reasoning);
130-
return { ...selection, modelId: suffix ? `${id}-${suffix}` : id };
130+
return { ...selection, modelId: suffix ? cursorWireModelIdWithEffort(id, suffix) : id };
131131
}
132132

133133
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {

tests/cursor-discovery.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ describe("Cursor discovery metadata", () => {
6363

6464
// Issue #117: Cursor GetUsableModels may return ids with a `cursor-` wire prefix.
6565
expect(isCursorModelAvailableForAccount("grok-4.5", ["cursor-grok-4.5-high"])).toBe(true);
66+
// Current Grok Fast wire ids put `-fast` after the effort tier.
67+
expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-low-fast"])).toBe(true);
68+
expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-high-fast"])).toBe(true);
69+
// Older snapshots used `{base}-fast-{effort}`; keep discovery compatibility.
6670
expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-fast-medium"])).toBe(true);
71+
expect(isCursorModelAvailableForAccount("grok-4.5-fast", ["cursor-grok-4.5-high"])).toBe(false);
72+
expect(isCursorModelAvailableForAccount("grok-4.5", ["cursor-grok-4.5-high-fast"])).toBe(false);
6773
expect(isCursorModelAvailableForAccount("gpt-5.4", ["cursor-gpt-5.4-high"])).toBe(true);
6874
// Prefixed sibling rejection: cursor- prefix must not bypass sibling-model checks.
6975
expect(isCursorModelAvailableForAccount("gpt-5.5", ["cursor-gpt-5.5-extra-high"])).toBe(false);
@@ -74,6 +80,12 @@ describe("Cursor discovery metadata", () => {
7480
["gpt-5.4-high"],
7581
);
7682
expect(filtered.map(model => model.id)).toEqual(["gpt-5.4"]);
83+
84+
const grok = filterCursorConfiguredModelsByLiveDiscovery(
85+
[{ id: "grok-4.5" }, { id: "grok-4.5-fast" }],
86+
["cursor-grok-4.5-high", "cursor-grok-4.5-high-fast"],
87+
);
88+
expect(grok.map(model => model.id)).toEqual(["grok-4.5", "grok-4.5-fast"]);
7789
});
7890

7991
test("live discovery filter always keeps all router levels when GetUsableModels omits them", () => {

tests/cursor-effort-suffix.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,20 @@ describe("Cursor per-model reasoning-effort suffix", () => {
6060
expect(modelIdFor("cursor/glm-5.2", "max")).toBe("glm-5.2-max");
6161
});
6262

63-
test("grok-4.5-fast preserves its explicit code-backed tiers", () => {
64-
expect(modelIdFor("cursor/grok-4.5-fast", "medium")).toBe("grok-4.5-fast-medium");
65-
expect(modelIdFor("cursor/grok-4.5-fast", "high")).toBe("grok-4.5-fast-high");
66-
expect(modelIdFor("cursor/grok-4.5-fast", "xhigh")).toBe("grok-4.5-fast-xhigh");
63+
test("grok-4.5 uses current low/medium/high tiers and trailing Fast wire ids", () => {
64+
expect(modelIdFor("cursor/grok-4.5", "low")).toBe("grok-4.5-low");
65+
expect(modelIdFor("cursor/grok-4.5", "medium")).toBe("grok-4.5-medium");
66+
expect(modelIdFor("cursor/grok-4.5", "high")).toBe("grok-4.5-high");
67+
expect(modelIdFor("cursor/grok-4.5", "xhigh")).toBe("grok-4.5-high");
68+
expect(modelIdFor("cursor/grok-4.5")).toBe("grok-4.5-high");
69+
expect(modelIdFor("cursor/grok-4.5-fast", "low")).toBe("grok-4.5-low-fast");
70+
expect(modelIdFor("cursor/grok-4.5-fast", "medium")).toBe("grok-4.5-medium-fast");
71+
expect(modelIdFor("cursor/grok-4.5-fast", "high")).toBe("grok-4.5-high-fast");
72+
// Codex-only upper tiers and an omitted effort clamp to Cursor's current top tier.
73+
expect(modelIdFor("cursor/grok-4.5-fast", "xhigh")).toBe("grok-4.5-high-fast");
74+
expect(modelIdFor("cursor/grok-4.5-fast")).toBe("grok-4.5-high-fast");
75+
expect(cursorModelEffortLadder("grok-4.5")).toEqual(["low", "medium", "high"]);
76+
expect(cursorModelEffortLadder("grok-4.5-fast")).toEqual(["low", "medium", "high"]);
6777
});
6878

6979
test("kimi-k3 maps to its live effort-suffixed variants", () => {

0 commit comments

Comments
 (0)