diff --git a/scripts/__tests__/cursor-sdk-local-agent-bridge.test.mjs b/scripts/__tests__/cursor-sdk-local-agent-bridge.test.mjs index 2bfd13e..a1057ec 100644 --- a/scripts/__tests__/cursor-sdk-local-agent-bridge.test.mjs +++ b/scripts/__tests__/cursor-sdk-local-agent-bridge.test.mjs @@ -89,6 +89,63 @@ describe("Cursor SDK local-agent bridge", () => { expect(normalizeModel("composer-latest")).toBe("composer-2.5") expect(normalizeModel("auto")).toBe("default") expect(normalizeModel("gpt-5.5")).toBe("gpt-5.5") + expect(normalizeModel("grok-4.5")).toBe("grok-4.5") + expect(normalizeModel("grok-4.5-fast")).toBe("grok-4.5-fast") + expect(normalizeModel("grok-4.5-high-fast")).toBe("grok-4.5-high-fast") + expect(normalizeModel("cursorapi/grok-4.5-fast")).toBe("grok-4.5-fast") + expect(normalizeModel("grok-4-5-low")).toBe("grok-4.5-low") + }) + + it("maps Grok 4.5 public ids to SDK model params", () => { + const baseInput = { + apiKey: "test-key", + workingDirectory: "/tmp/project", + clientTools: [], + } + + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5" }).model).toEqual({ + id: "grok-4.5", + params: [{ id: "fast", value: "false" }], + }) + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5-fast" }).model).toEqual({ + id: "grok-4.5", + params: [{ id: "fast", value: "true" }], + }) + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5-low" }).model).toEqual({ + id: "grok-4.5", + params: [ + { id: "fast", value: "false" }, + { id: "effort", value: "low" }, + ], + }) + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5-low-fast" }).model).toEqual({ + id: "grok-4.5", + params: [ + { id: "fast", value: "true" }, + { id: "effort", value: "low" }, + ], + }) + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5-high" }).model).toEqual({ + id: "grok-4.5", + params: [ + { id: "fast", value: "false" }, + { id: "effort", value: "high" }, + ], + }) + expect(localAgentCreateOptions({ ...baseInput, model: "grok-4.5-high-fast" }).model).toEqual({ + id: "grok-4.5", + params: [ + { id: "fast", value: "true" }, + { id: "effort", value: "high" }, + ], + }) + expect(localAgentSendOptions({ ...baseInput, model: "grok-4-5-high-fast" }).model).toEqual({ + id: "grok-4.5", + params: [ + { id: "fast", value: "true" }, + { id: "effort", value: "high" }, + ], + }) }) it("serializes overlapping runs for the same stateful SDK agent", async () => { diff --git a/scripts/cursor-sdk-local-agent-bridge.mjs b/scripts/cursor-sdk-local-agent-bridge.mjs index e8da242..e9229ed 100644 --- a/scripts/cursor-sdk-local-agent-bridge.mjs +++ b/scripts/cursor-sdk-local-agent-bridge.mjs @@ -2389,6 +2389,28 @@ function evictAgents() { } } +const GROK_45_VARIANTS = new Map([ + ["grok-4.5", { fast: "false" }], + ["grok-4.5-fast", { fast: "true" }], + ["grok-4.5-low", { fast: "false", effort: "low" }], + ["grok-4.5-low-fast", { fast: "true", effort: "low" }], + ["grok-4.5-high", { fast: "false", effort: "high" }], + ["grok-4.5-high-fast", { fast: "true", effort: "high" }], +]) + +function normalizeGrok45Model(normalized) { + const canonical = normalized.replace(/^grok-4-5/, "grok-4.5") + return GROK_45_VARIANTS.has(canonical) ? canonical : null +} + +function grok45SdkModelSelection(normalized) { + const variant = GROK_45_VARIANTS.get(normalized) + if (!variant) return null + const params = [{ id: "fast", value: variant.fast }] + if (variant.effort) params.push({ id: "effort", value: variant.effort }) + return { id: "grok-4.5", params } +} + function normalizeModel(model) { const raw = model.trim() const normalized = raw.toLowerCase().split("/").filter(Boolean).at(-1) || "" @@ -2404,6 +2426,8 @@ function normalizeModel(model) { if (normalized === "composer-2.5-sdk" || normalized === "composer-2-5-sdk") return "composer-2.5" if (normalized === "composer-2.5-fast" || normalized === "composer-2-5-fast") return "composer-2.5-fast" + const grok45 = normalizeGrok45Model(normalized) + if (grok45) return grok45 return raw } @@ -2413,6 +2437,8 @@ function sdkModelSelection(model) { return { id: "composer-2.5", params: [{ id: "fast", value: "false" }] } if (normalized === "composer-2.5-fast") return { id: "composer-2.5", params: [{ id: "fast", value: "true" }] } + const grok45 = grok45SdkModelSelection(normalized) + if (grok45) return grok45 return { id: normalized } } diff --git a/worker/__tests__/cursor.test.ts b/worker/__tests__/cursor.test.ts index 8e76cf9..ca9f738 100644 --- a/worker/__tests__/cursor.test.ts +++ b/worker/__tests__/cursor.test.ts @@ -9,6 +9,13 @@ describe("Cursor stream adapter", () => { expect(resolveCursorModel("auto")).toEqual({ id: "composer-2.5" }) }) + it("normalizes Grok 4.5 hyphenated aliases to canonical model ids", () => { + expect(resolveCursorModel("grok-4.5-fast")).toEqual({ id: "grok-4.5-fast" }) + expect(resolveCursorModel("grok-4-5-fast")).toEqual({ id: "grok-4.5-fast" }) + expect(resolveCursorModel("grok-4-5-high")).toEqual({ id: "grok-4.5-high" }) + expect(resolveCursorModel("grok-4-5-low-fast")).toEqual({ id: "grok-4.5-low-fast" }) + }) + it("encodes attached images into the user ConversationMessage", () => { const body = cursorTestExports.encodeCursorChatRequest({ prompt: { text: "Describe this image." }, diff --git a/worker/__tests__/index.test.ts b/worker/__tests__/index.test.ts index f1f2668..bae801c 100644 --- a/worker/__tests__/index.test.ts +++ b/worker/__tests__/index.test.ts @@ -1279,7 +1279,10 @@ describe("Worker", () => { deps, ) expect(withAuth.status).toBe(200) - const body = (await withAuth.json()) as { object: string; data: Array<{ id: string }> } + const body = (await withAuth.json()) as { + object: string + data: Array<{ id: string; cost?: { input: number; output: number } }> + } expect(body).toMatchObject({ object: "list", data: expect.arrayContaining([ @@ -1288,8 +1291,38 @@ describe("Worker", () => { expect.objectContaining({ id: "gpt-5.3-codex" }), expect.objectContaining({ id: "gemini-3.1-pro" }), expect.objectContaining({ id: "default" }), + expect.objectContaining({ id: "grok-4.5" }), + expect.objectContaining({ id: "grok-4.5-fast" }), + expect.objectContaining({ id: "grok-4.5-low" }), + expect.objectContaining({ id: "grok-4.5-low-fast" }), + expect.objectContaining({ id: "grok-4.5-high" }), + expect.objectContaining({ id: "grok-4.5-high-fast" }), ]), }) + expect(body.data.find((model) => model.id === "grok-4.5")?.cost).toEqual({ + input: 2, + output: 6, + }) + expect(body.data.find((model) => model.id === "grok-4.5-fast")?.cost).toEqual({ + input: 4, + output: 18, + }) + expect(body.data.find((model) => model.id === "grok-4.5-low")?.cost).toEqual({ + input: 2, + output: 6, + }) + expect(body.data.find((model) => model.id === "grok-4.5-high")?.cost).toEqual({ + input: 2, + output: 6, + }) + expect(body.data.find((model) => model.id === "grok-4.5-low-fast")?.cost).toEqual({ + input: 4, + output: 18, + }) + expect(body.data.find((model) => model.id === "grok-4.5-high-fast")?.cost).toEqual({ + input: 4, + output: 18, + }) expect(body.data.map((model) => model.id)).not.toContain("gpt-5.5") }) }) diff --git a/worker/cursor.ts b/worker/cursor.ts index 3baa6b4..d27741c 100644 --- a/worker/cursor.ts +++ b/worker/cursor.ts @@ -46,6 +46,20 @@ export async function listCursorModels( return cursorPublicJson(env, deps, apiKey, "/v1/models") } +const GROK_45_MODEL_IDS = new Set([ + "grok-4.5", + "grok-4.5-fast", + "grok-4.5-low", + "grok-4.5-low-fast", + "grok-4.5-high", + "grok-4.5-high-fast", +]) + +function canonicalGrok45ModelId(normalized: string): string | null { + const canonical = normalized.replace(/^grok-4-5/, "grok-4.5") + return GROK_45_MODEL_IDS.has(canonical) ? canonical : null +} + export function resolveCursorModel(model: unknown): { id: string } | undefined { if (typeof model !== "string" || !model.trim()) return { id: "composer-2.5" } const normalized = model.trim().toLowerCase() @@ -60,6 +74,8 @@ export function resolveCursorModel(model: unknown): { id: string } | undefined { if (normalized === "composer-2.5-fast" || normalized === "composer-2-5-fast") { return { id: "composer-2.5-fast" } } + const grok45 = canonicalGrok45ModelId(normalized) + if (grok45) return { id: grok45 } if (normalized === "auto" || normalized === "default") return { id: "composer-2.5" } return { id: model.trim() } } diff --git a/worker/openai.ts b/worker/openai.ts index 80c1713..768a7aa 100644 --- a/worker/openai.ts +++ b/worker/openai.ts @@ -61,6 +61,7 @@ const sdkToolCallMemory = new Map() const SDK_TOOL_CALL_MEMORY_LIMIT = 2048 const CURSOR_COMPOSER_2_5_PRICING_SOURCE = "https://cursor.com/changelog/composer-2-5" +const CURSOR_GROK_4_5_PRICING_SOURCE = "https://cursor.com/en-US/docs/models/grok-4-5" const CURSOR_MODEL_PRICING: Record = { default: { input: 0.5, output: 2.5, source: CURSOR_COMPOSER_2_5_PRICING_SOURCE }, auto: { input: 0.5, output: 2.5, source: CURSOR_COMPOSER_2_5_PRICING_SOURCE }, @@ -70,6 +71,12 @@ const CURSOR_MODEL_PRICING: Record = { "composer-2-5": { input: 0.5, output: 2.5, source: CURSOR_COMPOSER_2_5_PRICING_SOURCE }, "composer-2.5-fast": { input: 3, output: 15, source: CURSOR_COMPOSER_2_5_PRICING_SOURCE }, "composer-2-5-fast": { input: 3, output: 15, source: CURSOR_COMPOSER_2_5_PRICING_SOURCE }, + "grok-4.5": { input: 2, output: 6, source: CURSOR_GROK_4_5_PRICING_SOURCE }, + "grok-4.5-low": { input: 2, output: 6, source: CURSOR_GROK_4_5_PRICING_SOURCE }, + "grok-4.5-high": { input: 2, output: 6, source: CURSOR_GROK_4_5_PRICING_SOURCE }, + "grok-4.5-fast": { input: 4, output: 18, source: CURSOR_GROK_4_5_PRICING_SOURCE }, + "grok-4.5-low-fast": { input: 4, output: 18, source: CURSOR_GROK_4_5_PRICING_SOURCE }, + "grok-4.5-high-fast": { input: 4, output: 18, source: CURSOR_GROK_4_5_PRICING_SOURCE }, } const SYSTEM_DIRECTIVE = [ @@ -759,6 +766,12 @@ export function modelList( modelItem("gemini-2.5-flash", "Gemini 2.5 Flash"), modelItem("grok-build-0.1", "Grok Build 0.1"), modelItem("grok-4.3", "Grok 4.3"), + modelItem("grok-4.5", "Grok 4.5"), + modelItem("grok-4.5-fast", "Grok 4.5 Fast"), + modelItem("grok-4.5-low", "Grok 4.5 Low"), + modelItem("grok-4.5-low-fast", "Grok 4.5 Low Fast"), + modelItem("grok-4.5-high", "Grok 4.5 High"), + modelItem("grok-4.5-high-fast", "Grok 4.5 High Fast"), modelItem("kimi-k2.5", "Kimi K2.5"), ], } @@ -2473,7 +2486,9 @@ function costFromTokens(model: string, inputTokens: number, outputTokens: number } function pricingForModel(model: string): CursorModelPricing | null { - return CURSOR_MODEL_PRICING[model.trim().toLowerCase()] ?? null + const normalized = model.trim().toLowerCase() + const canonical = normalized.replace(/^grok-4-5/, "grok-4.5") + return CURSOR_MODEL_PRICING[canonical] ?? CURSOR_MODEL_PRICING[normalized] ?? null } function roundUsd(value: number): number {