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
57 changes: 57 additions & 0 deletions scripts/__tests__/cursor-sdk-local-agent-bridge.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
26 changes: 26 additions & 0 deletions scripts/cursor-sdk-local-agent-bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) || ""
Expand All @@ -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
}

Expand All @@ -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 }
}

Expand Down
7 changes: 7 additions & 0 deletions worker/__tests__/cursor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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." },
Expand Down
35 changes: 34 additions & 1 deletion worker/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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" }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING

The new /v1/models test only asserts that each Grok 4.5 variant id is present; it does not verify the per-variant cost field (e.g., grok-4.5 at 2/6 vs grok-4.5-fast at 4/18). A pricing regression such as copying the fast price to all variants would pass. Add cost assertions for each Grok 4.5 variant and include cost in the body type annotation.

Fix with AI
Verify the issue still exists before applying this fix.

The new /v1/models test only asserts that each Grok 4.5 variant id is present; it does not verify the per-variant cost field (e.g., grok-4.5 at 2/6 vs grok-4.5-fast at 4/18). A pricing regression such as copying the fast price to all variants would pass. Add cost assertions for each Grok 4.5 variant and include cost in the body type annotation.

In worker/__tests__/index.test.ts, inside the 'requires a bearer token for /v1/models' it block, after the existing arrayContaining assertion, add explicit per-id cost assertions: expect(body.data.find((m) => m.id === 'grok-4.5')?.cost).toEqual({ input: 2, output: 6 }); expect(body.data.find((m) => m.id === 'grok-4.5-fast')?.cost).toEqual({ input: 4, output: 18 }); expect(body.data.find((m) => m.id === 'grok-4.5-low')?.cost).toEqual({ input: 2, output: 6 }); expect(body.data.find((m) => m.id === 'grok-4.5-high')?.cost).toEqual({ input: 2, output: 6 }); expect(body.data.find((m) => m.id === 'grok-4.5-low-fast')?.cost).toEqual({ input: 4, output: 18 }); expect(body.data.find((m) => m.id === 'grok-4.5-high-fast')?.cost).toEqual({ input: 4, output: 18 }). Extend the body type annotation to include cost?: { input: number; output: number }.

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")
})
})
Expand Down
16 changes: 16 additions & 0 deletions worker/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ export async function listCursorModels(
return cursorPublicJson<CursorModelResponse>(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()
Expand All @@ -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() }
}
Expand Down
17 changes: 16 additions & 1 deletion worker/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const sdkToolCallMemory = new Map<string, SdkToolCallMemory>()
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<string, CursorModelPricing> = {
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 },
Expand All @@ -70,6 +71,12 @@ const CURSOR_MODEL_PRICING: Record<string, CursorModelPricing> = {
"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 = [
Expand Down Expand Up @@ -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"),
],
}
Expand Down Expand Up @@ -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 {
Expand Down