diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index ea97ddbcf..7b1fbc1c4 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -104,6 +104,8 @@ filtered incomplete になります。実際のツール呼び出しを伴わな - 通常の fetch/parse 経路の代わりに `runTurn` を使います。リクエスト、サーバーイベント、ツール引数、使用量 checkpoint、クライアントレスポンスは `cursor/gen/agent_pb.ts` の `@bufbuild/protobuf` スキーマでエンコードしたのち Connect メッセージとして framing します。 - content-addressed blob で対話状態を再生し、サーバーツール呼び出しを Codex に再マッピングします。protobuf の `GetUsableModels` RPC でリアルタイム Cursor モデルを探し、run リクエストが wire に commit される前だけリトライします。 +- `cursor/grok-4.5-fast` は選択可能なモデルとして維持しつつ、Cursor には正規の `grok-4.5` + モデルを送信し、個別の `effort` および `fast=true` 値は `requested_model.parameters` に格納します。 - Cursor ネイティブのローカルファイルシステム/shell/network 実行はデフォルトで拒否します。明示的な `mcpServers` と `desktopExecutor` 統合はそれぞれ別の opt-in です。`unsafeAllowNativeLocalExec` はより広い組み込み executor を有効にし、Codex の承認/サンドボックスルールを迂回します。 ## `azure-openai`(別名: `azure`) diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index ddfe86864..85134c9ed 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -123,6 +123,8 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. - content-addressed blob으로 대화 상태를 재생하고 서버 툴 호출을 Codex에 다시 매핑합니다. protobuf `GetUsableModels` RPC로 실시간 Cursor 모델을 찾으며, run 요청이 wire에 commit되기 전까지만 재시도합니다. +- `cursor/grok-4.5-fast`는 선택 가능한 모델로 유지하되, Cursor에는 정식 `grok-4.5` 모델을 보내고 + 별도의 `effort`, `fast=true` 값은 `requested_model.parameters`에 담습니다. - Cursor 네이티브 로컬 파일시스템/shell/network 실행은 기본적으로 거부합니다. 명시적인 `mcpServers`와 `desktopExecutor` 통합은 각각 별도 opt-in입니다. `unsafeAllowNativeLocalExec`은 더 넓은 내장 executor를 켜며 Codex 승인/샌드박스 규칙을 우회합니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 25cd3b962..2a6d997da 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -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. - 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 diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 80c7ae647..663716314 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -134,6 +134,8 @@ authorization. - Воспроизводит состояние диалога через content-addressed blob'ы, отображает серверные вызовы инструментов обратно в Codex, обнаруживает актуальные модели Cursor через protobuf RPC `GetUsableModels` и повторяет попытки только до того, как run-запрос зафиксирован на wire. +- Сохраняет `cursor/grok-4.5-fast` доступной для выбора, но отправляет Cursor каноническую модель + `grok-4.5`, помещая отдельные значения `effort` и `fast=true` в `requested_model.parameters`. - Нативное для Cursor локальное выполнение операций с файловой системой/shell/сетью по умолчанию запрещено. Явные интеграции `mcpServers` и `desktopExecutor` включаются отдельно; `unsafeAllowNativeLocalExec` включает более широкий встроенный executor и обходит семантику diff --git a/docs-site/src/content/docs/zh-cn/reference/adapters.md b/docs-site/src/content/docs/zh-cn/reference/adapters.md index 6a3d89a98..e0b5caa6f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -117,6 +117,8 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 Connect message。 - 经 content-addressed blob 重放对话状态,把 server tool call 映射回 Codex,用 protobuf `GetUsableModels` RPC 发现实时 Cursor 模型,并且只在 run request 尚未 commit 到 wire 前重试。 +- 保留 `cursor/grok-4.5-fast` 作为可选模型,但向 Cursor 发送规范的 `grok-4.5` 模型,并将独立的 + `effort` 和 `fast=true` 值放入 `requested_model.parameters`。 - Cursor 原生本地 filesystem/shell/network 执行默认被拒绝。显式 `mcpServers` 与 `desktopExecutor` 集成分别需要 opt-in;`unsafeAllowNativeLocalExec` 会启用更广泛的内置 executor,并绕过 Codex 审批和 sandbox 语义。 diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index 7b9baa80e..a7c0b76e4 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -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")) { diff --git a/src/adapters/cursor/protobuf-request.ts b/src/adapters/cursor/protobuf-request.ts index f0b08eec2..4ede0a482 100644 --- a/src/adapters/cursor/protobuf-request.ts +++ b/src/adapters/cursor/protobuf-request.ts @@ -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, { @@ -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 diff --git a/src/adapters/cursor/request-builder.ts b/src/adapters/cursor/request-builder.ts index b837081ba..e550c6910 100644 --- a/src/adapters/cursor/request-builder.ts +++ b/src/adapters/cursor/request-builder.ts @@ -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 { @@ -117,16 +117,29 @@ function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[]) } /** - * Resolve a `cursor/` 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/` 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 }; } @@ -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] : [])], diff --git a/src/adapters/cursor/types.ts b/src/adapters/cursor/types.ts index e1636ca6e..b32026a07 100644 --- a/src/adapters/cursor/types.ts +++ b/src/adapters/cursor/types.ts @@ -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; diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 5a39c59e2..41b413475 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -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`, @@ -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 diff --git a/tests/cursor-blob.test.ts b/tests/cursor-blob.test.ts index 2322a3fd4..e86fe9914 100644 --- a/tests/cursor-blob.test.ts +++ b/tests/cursor-blob.test.ts @@ -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", diff --git a/tests/cursor-effort-suffix.test.ts b/tests/cursor-effort-suffix.test.ts index fd73f86ba..6d30084e3 100644 --- a/tests/cursor-effort-suffix.test.ts +++ b/tests/cursor-effort-suffix.test.ts @@ -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"); @@ -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"]); });