@cursor/sdk local agent: named models fail with status: "error" and no error message

Hi Cursor team,

I’m building a headless app using @cursor/sdk (local runtime). Cursor.models.list() returns the full catalog (~30 models) with parameters and variants, but any explicitly named model fails at run time. Only default / auto completes successfully.

This happens with BYOK disabled and no custom API keys on the account, just a standard Cursor user API key from the dashboard.

SDK @cursor/[email protected]
BYOK Disabled
Runtime Local (local: { cwd, settingSources: })
Custom provider keys None on account
Auth User API key (CURSOR_API_KEY from Cursor Dashboard → API Keys)
Node v22.19.0

Minimal repro (no MCP, no custom tools, no project settings):

import { Agent, Cursor } from "@cursor/sdk";

const apiKey = process.env.CURSOR_API_KEY!;

// Step 1: catalog works — returns 30 models including composer-2.5, claude-sonnet-4-6, etc.
const models = await Cursor.models.list({ apiKey });
const composer = models.find((m) => m.id === "composer-2.5");
// composer.parameters includes { id: "fast", values: ["false", "true"] }
// composer.variants includes isDefault: true with params: [{ id: "fast", value: "true" }]

// Step 2: named model fails
const agent = await Agent.create({
  apiKey,
  model: {
    id: "composer-2.5",
    params: [{ id: "fast", value: "true" }], // catalog default variant
  },
  local: { cwd: process.cwd(), settingSources: [] },
});

const run = await agent.send("Reply with exactly: ok");
const result = await run.wait();
// result.status === "error"

Same failure with:

  • { id: "composer-2.5" } (no params)
  • Default variant params from variants.find(v => v.isDefault)
  • Other models: claude-sonnet-4-6, gpt-5.4-mini, claude-opus-4-6
  • Both minimal config (settingSources: []) and full bot config (settingSources: ["project"], MCP, custom tools)

What works:

model: { id: "default" }  // ✅ status: "finished"
model: { id: "auto" }     // ✅ resolves to default, status: "finished"


What happens

  1. Agent.create() succeeds — agent ID is assigned (e.g. agent-80704988-...).

  2. agent.send() succeeds — run ID and requestId are returned.

  3. Stream emits:

    {"type":"status","status":"RUNNING"}
    {"type":"status","status":"ERROR"}
    
    
  4. run.wait() resolves with:

    {
      "status": "error",
      "model": { "id": "composer-2.5", "params": [{ "id": "fast", "value": "true" }] },
      "durationMs": 3076,
      "result": undefined
    }
    
    
  5. run.conversation() returns [] (empty — no assistant text, no error detail).

  6. No exception is thrown — this is a run-level failure, not a CursorAgentError startup failure.


Error codes / identifiers

There is no HTTP status code or error message in the response. What we do get:

Field Example value
run.status "error"
Stream status "ERROR" (after "RUNNING")
run.requestId f6ff0b72-9585-4278-9aa8-31b237362eb0
run.id run-b397e483-32eb-4c57-935a-c3a82282f107
agent.agentId agent-80704988-2991-41cd-957e-df710d01fa79
result.result undefined
run.conversation() []

No CursorAgentError is thrown. No message like “model not available” or “unsupported model” appears in the stream or RunResult.


What I’ve ruled out

  • Missing model.params — docs say parameterized models need explicit params; I pass the catalog default variant (fast: true for composer-2.5). Still fails.
  • BYOK / custom keys — disabled and confirmed none on account.
  • Wrong model ID — IDs match Cursor.models.list() exactly; aliases also tried.
  • Local config pollution — fails with settingSources: [] (no project/user settings loaded).
  • SDK version — reproduced on @cursor/[email protected].

Has anyone else experienced this? If so, were you able to identify the cause or find a workaround?

Thanks!

The missing error message is probably the biggest issue here.

If the catalog returns those named models, the SDK user will assume they are valid inputs. When the run only returns status error, it becomes hard to tell whether the problem is auth, model routing, params, local runtime, or account eligibility.

Hey, thanks for the detailed report. The request ID and steps really helped speed up the investigation.

Main point: this isn’t a bug in specific models. From your request ID, we can see the named models are being rejected with HTTP 429 usage limit, not with a model unavailable type error. When your account goes over the included usage limit, Cursor limits agent requests to Auto default only. That’s why default auto works, but any explicitly named model composer-2.5, claude-, gpt- returns 429.

The part where you get a vague status error with an empty conversation is a known gap in the SDK. run.wait currently doesn’t surface stream errors, and RunResult doesn’t have an error field, so the 429 never reaches you. We’ve logged an issue for this, but I can’t share an ETA yet. And yes, Jason_Luo1 is right, the swallowed error message is the biggest pain here.

What you can do right now:

  • Use default auto, it will resolve to an allowed model
  • Wait for the included usage window to reset, you can check status in the dashboard
  • Turn on usage-based pricing or upgrade for more included usage

Named models will work again as soon as you’re back under the limit. Let me know if it still happens after the limit resets, and we’ll dig deeper.

I met the same problem, cursor sdk should have more useful response message to help us solve the problem.

Hey @JeremyChow, thanks for following up. Just seeing status: "error" with no details is a known gap in the SDK. run.wait() doesn’t currently surface the stream error, so the real reason never reaches you. We’re working on better error messaging, but I can’t share an ETA yet.

That said, a bare error almost always hides a specific server-side reason. For the thread author, it turned out to be an HTTP 429 usage limit. Once you exceed included usage, agent requests are limited to Auto default only, so any named model fails.

To figure out if that’s what’s happening for you or if it’s something else like auth, params, or environment, can you share:

  • which SDK and version you’re using TS @cursor/sdk or Python cursor-sdk
  • the Request ID from the failed run run.requestId
  • whether model: { id: "default" } or "auto" works for you

With the Request ID, we can check exactly why the server rejected the request and give you a more precise answer.

An update for the error-messaging work, from local @cursor/sdk runs: run.wait() now returns RunResult.error (we see it in 1.0.31 and later), which helps a lot. In every failed run we have, though, error.code is unset and the status sits inside error.message:

error.message Request IDs
[resource_exhausted] Error 455f7421-d101-4dff-9d66-91603763e30e
[unknown] [canceled] This operation was aborted 19c2ee4e-d757-4e7e-af57-19257f5ad2eb, 86c4ab29-9af8-4b3f-8a47-3f8a6c2816d8, 6edebb07-c98b-4b0e-8688-4da3d64cd9cc
Agent turn stopped after repeated resume attempts made no progress d33c41ce-a4e9-4de0-beea-63f6bfdc8607

All five were local runs of claude-opus-5-5 on 2026-10-04/05, made with privacy mode on.

In 1.0.35 and 1.0.36, when a run ends with an error recorded in the local run store, the SDK copies that string as-is: void 0===this._error&&t?.errorCode&&(this._error={message:t.errorCode}) (dist/cjs/index.js). Errors thrown from the stream go through a helper that already splits [code] message into { code, message }. Sending the stored string through the same helper would fill code for runs like these.

Beyond that, it would help to get the retry delay when the server has one, and to know whether a canceled came from the server or the client.

Thank you for the detailed update and for the code analysis. It really helps.

Just to confirm: the fact that in 1.0.31+ run.wait() now returns RunResult.error is expected and comes from the work to better surface errors. What we are still tracking is that error.code stays empty, while the real status like [resource_exhausted], [canceled], and so on is still inside error.message. We’ve already logged an update for this thread.

Your suggestion to run the saved string from the local run store through the same helper that already parses [code] message into { code, message } has been passed to the team. I also passed along two extra requests: returning the retry delay when the server sends it, and distinguishing whether canceled came from the server or the client. I can’t give an ETA yet.

One small clarification on your table: those five messages actually mean different things under the hood. [resource_exhausted] is a usage limit rejection, while [canceled] This operation was aborted and Agent turn stopped after repeated resume attempts refer to other scenarios. A properly populated error.code, and where available request ID plus retry delay, should help distinguish them programmatically. I also grabbed the request IDs you shared for investigation.

If after the SDK update you still see that error.code is empty, write back and we’ll take another look.