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.
run.conversation() returns [] (empty — no assistant text, no error detail).
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).
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.
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:
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.