Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ advertised effort control on those models as proof of upstream-native reasoning
- Exposes Cursor Router as `cursor/auto` plus explicit `cursor/auto-cost`,
`cursor/auto-balance`, and `cursor/auto-intelligence` entries. Explicit levels are encoded in
`requested_model.parameters` while the legacy `cursor/auto` entry retains the account/team default.
- Keeps `cursor/grok-4.5-fast` as a selectable model while sending Cursor's canonical `grok-4.5`
model with separate `effort` and `fast=true` parameters.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- Cursor-native local filesystem/shell/network execution is denied by default. Explicit `mcpServers`
and `desktopExecutor` integrations have separate opt-ins; `nativeLocalExec: "on"` enables the
broader built-in executor and bypasses Codex approval/sandbox semantics, and legacy
Expand Down
5 changes: 3 additions & 2 deletions src/adapters/cursor/effort-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
}

/**
* 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.
* Compose Cursor's flattened model id from a Codex-facing base id and effort tier. Discovery uses
* this for the ids returned by GetUsableModels. Parameterized Grok Fast requests bypass the flat id
* and send the base model plus requested_model parameters instead.
*/
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
if (baseModelId.endsWith("-fast")) {
Expand Down
35 changes: 20 additions & 15 deletions src/adapters/cursor/protobuf-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,11 @@ function buildPreparedCursorRunRequest(
tools: request.tools?.length ?? 0,
});

const requestedModelParameters = [
...(request.requestedModelParameters ?? []),
...(request.routingLevel ? [{ id: CURSOR_ROUTING_LEVEL_PARAMETER_ID, value: request.routingLevel }] : []),
];
const hasExplicitModelParameters = (request.requestedModelParameters?.length ?? 0) > 0;
const runRequest = create(AgentRunRequestSchema, {
conversationId: request.conversationId,
conversationState: create(ConversationStateStructureSchema, {
Expand All @@ -637,24 +642,24 @@ function buildPreparedCursorRunRequest(
readPaths: [],
}),
action,
modelDetails: create(ModelDetailsSchema, {
modelId: request.modelId,
displayModelId: request.modelId,
displayName: request.modelId,
displayNameShort: request.modelId,
aliases: [],
}),
// requested_model is currently a Cursor Router-only surface. External model clients still
// send model_details alone; sending both makes external workers reach stepCompleted and then
// reject the turn with invalid_argument.
...(request.routingLevel ? {
// Explicit model-picker parameters follow current Cursor clients and use requested_model alone.
// Keep legacy model_details for flat model ids and the already-live Router path; sending both for
// a parameterized external model can resolve conflicting selections and end in invalid_argument.
...(!hasExplicitModelParameters ? {
modelDetails: create(ModelDetailsSchema, {
modelId: request.modelId,
displayModelId: request.modelId,
displayName: request.modelId,
displayNameShort: request.modelId,
aliases: [],
}),
} : {}),
...(requestedModelParameters.length > 0 ? {
requestedModel: create(RequestedModelSchema, {
modelId: request.modelId,
maxMode: false,
parameters: [create(RequestedModel_ModelParameterbytesSchema, {
id: CURSOR_ROUTING_LEVEL_PARAMETER_ID,
value: request.routingLevel,
})],
parameters: requestedModelParameters.map(parameter =>
create(RequestedModel_ModelParameterbytesSchema, parameter)),
}),
} : {}),
// Mirror the client (Responses) tool definitions into the top-level AgentRunRequest.mcp_tools
Expand Down
28 changes: 21 additions & 7 deletions src/adapters/cursor/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
OcxToolResultMessage,
} from "../../types";
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
import type { CursorRequestMessage, CursorRunRequest } from "./types";
import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
import { cursorEffortSuffix, cursorWireModelIdWithEffort } from "./effort-map";
import {
Expand Down Expand Up @@ -117,16 +117,29 @@ function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[])
}

/**
* Resolve a `cursor/<model>` selection + Codex reasoning effort to the actual Cursor model id. Cursor
* encodes the effort as a per-model suffix (`claude-4.6-opus-high`); `cursorEffortSuffix` picks the
* right tier for that specific model (literal pass-through, with rank clamp fallback) or
* `undefined` for non-reasoning models like `composer-2.5`. A fully-qualified id (one that isn't a
* known effort base) passes through unchanged.
* Resolve a `cursor/<model>` selection + Codex reasoning effort to Cursor's requested model shape.
* Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok 4.5 Fast is parameterized
* instead: current Cursor clients send the `grok-4.5` base id plus `effort` and `fast` parameters.
* A fully-qualified id (one that is not a known effort base) passes through unchanged.
*/
function normalizeCursorModelId(modelId: string, reasoning?: string): { modelId: string; routingLevel?: CursorRoutingLevel } {
function normalizeCursorModelId(modelId: string, reasoning?: string): {
modelId: string;
requestedModelParameters?: readonly CursorRequestedModelParameter[];
routingLevel?: CursorRoutingLevel;
} {
const selection = cursorWireModelSelection(modelId);
const id = selection.modelId;
const suffix = cursorEffortSuffix(id, reasoning);
if (id === "grok-4.5-fast" && suffix) {
return {
...selection,
modelId: "grok-4.5",
requestedModelParameters: [
{ id: "effort", value: suffix },
{ id: "fast", value: "true" },
],
};
}
return { ...selection, modelId: suffix ? cursorWireModelIdWithEffort(id, suffix) : id };
}

Expand Down Expand Up @@ -241,6 +254,7 @@ export function createCursorRequest(
const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning);
return {
modelId: model.modelId,
...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
conversationId: resolveCursorConversationId(parsed, model.modelId, options),
system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
Expand Down
7 changes: 7 additions & 0 deletions src/adapters/cursor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import type { OcxUsage } from "../../types";
import type { OcxMessage, OcxRequestOptions, OcxTool } from "../../types";
import type { CursorRoutingLevel } from "./discovery";

export interface CursorRequestedModelParameter {
id: string;
value: string;
}

export interface CursorRunRequest {
modelId: string;
/** Cursor model-picker parameters encoded through AgentRunRequest.requested_model. */
requestedModelParameters?: readonly CursorRequestedModelParameter[];
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
routingLevel?: CursorRoutingLevel;
conversationId: string;
Expand Down
8 changes: 7 additions & 1 deletion structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ replays are explicit and receive the same repair.
These compatibility guards are covered by focused tests and should stay close to the adapters that
need them.

## Cursor Router optimization levels
## Cursor parameterized models

Cursor Router's parameterized `default` model is represented in Codex by four catalog rows:
`cursor/auto` preserves Cursor's team/account default, while `cursor/auto-cost`,
Expand All @@ -277,6 +277,12 @@ All four route to the `default` Cursor wire model. Explicit variants additionall
parameterized-model channel used by current Cursor clients. Router rows are static capabilities and
must survive a live `GetUsableModels` response that omits `default`.

`cursor/grok-4.5-fast` is also a stable Codex-facing row, but current Cursor clients do not request
it as a flat model slug. OpenCodex sends `grok-4.5` through `requested_model` with separate `effort`
and `fast=true` parameters, leaving legacy `model_details` unset for that parameterized external
selection. Live discovery still recognizes Cursor's flattened `cursor-grok-4.5-{effort}-fast`
variants, plus the older `grok-4.5-fast-{effort}` ordering, as availability evidence only.

## Cursor active-context usage

Cursor's `conversationCheckpointUpdate.tokenDetails.usedTokens` is treated as the authoritative
Expand Down
25 changes: 25 additions & 0 deletions tests/cursor-blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,31 @@ describe("Cursor blob handshake", () => {
expect(run?.requestedModel).toBeUndefined();
});

test("encodes Grok Fast through requested_model parameters without legacy model_details", () => {
const bytes = encodeCursorRunRequest({
modelId: "grok-4.5",
requestedModelParameters: [
{ id: "effort", value: "high" },
{ id: "fast", value: "true" },
],
conversationId: "c1",
system: [],
messages: [{ role: "user", content: "hi" }],
});
const msg = fromBinary(AgentClientMessageSchema, bytes);
const run = msg.message.case === "runRequest" ? msg.message.value : undefined;

expect(run?.modelDetails).toBeUndefined();
expect(run?.requestedModel).toMatchObject({
modelId: "grok-4.5",
maxMode: false,
parameters: [
{ id: "effort", value: "high" },
{ id: "fast", value: "true" },
],
});
});

test("adds Cursor exact-tool guidance to system prompt blobs when tools are advertised", () => {
const bytes = encodeCursorRunRequest({
modelId: "claude-4.6-sonnet",
Expand Down
42 changes: 36 additions & 6 deletions tests/cursor-effort-suffix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ function modelIdFor(modelId: string, reasoning?: string): string {
return createCursorRequest(parsed).modelId;
}

function selectionFor(modelId: string, reasoning?: string) {
const parsed: OcxParsedRequest = {
modelId,
context: { messages: [{ role: "user", content: "hi", timestamp: 1 }] },
stream: false,
options: reasoning ? { reasoning } : {},
};
const request = createCursorRequest(parsed);
return { modelId: request.modelId, parameters: request.requestedModelParameters };
}

describe("Cursor per-model reasoning-effort suffix", () => {
test("literal requested efforts pass through when the model supports that tier", () => {
expect(modelIdFor("cursor/claude-4.6-opus", "high")).toBe("claude-4.6-opus-high");
Expand Down Expand Up @@ -60,18 +71,37 @@ describe("Cursor per-model reasoning-effort suffix", () => {
expect(modelIdFor("cursor/glm-5.2", "max")).toBe("glm-5.2-max");
});

test("grok-4.5 uses current low/medium/high tiers and trailing Fast wire ids", () => {
test("grok-4.5 uses current tiers and sends Fast as a separate model parameter", () => {
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");
expect(selectionFor("cursor/grok-4.5", "high")).toEqual({
modelId: "grok-4.5-high",
parameters: undefined,
});
expect(selectionFor("cursor/grok-4.5-fast", "low")).toEqual({
modelId: "grok-4.5",
parameters: [{ id: "effort", value: "low" }, { id: "fast", value: "true" }],
});
expect(selectionFor("cursor/grok-4.5-fast", "medium")).toEqual({
modelId: "grok-4.5",
parameters: [{ id: "effort", value: "medium" }, { id: "fast", value: "true" }],
});
expect(selectionFor("cursor/grok-4.5-fast", "high")).toEqual({
modelId: "grok-4.5",
parameters: [{ id: "effort", value: "high" }, { id: "fast", value: "true" }],
});
// 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(selectionFor("cursor/grok-4.5-fast", "xhigh")).toEqual({
modelId: "grok-4.5",
parameters: [{ id: "effort", value: "high" }, { id: "fast", value: "true" }],
});
expect(selectionFor("cursor/grok-4.5-fast")).toEqual({
modelId: "grok-4.5",
parameters: [{ id: "effort", value: "high" }, { id: "fast", value: "true" }],
});
expect(cursorModelEffortLadder("grok-4.5")).toEqual(["low", "medium", "high"]);
expect(cursorModelEffortLadder("grok-4.5-fast")).toEqual(["low", "medium", "high"]);
});
Expand Down
Loading