Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/adapters/cursor/discovery.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
});
}

Expand Down
24 changes: 18 additions & 6 deletions src/adapters/cursor/effort-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,10 +31,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
// 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"],
Expand Down Expand Up @@ -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`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the discovered Fast wire-ID ordering

When GetUsableModels returns only a legacy ID such as cursor-grok-4.5-fast-medium, isCursorModelAvailableForAccount deliberately keeps grok-4.5-fast visible, but every request is now unconditionally rewritten to grok-4.5-medium-fast. For the exact legacy-account context this compatibility branch handles, the selected model can therefore fail with ERROR_BAD_MODEL_NAME; either stop exposing legacy-only discoveries or retain the discovered ordering when constructing the request.

Useful? React with 👍 / 👎.

}
return `${baseModelId}-${effortSuffix}`;
}
4 changes: 2 additions & 2 deletions src/adapters/cursor/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions tests/cursor-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down
18 changes: 14 additions & 4 deletions tests/cursor-effort-suffix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading