Error Reference
Stable error codes, categories, retry guidance, and request ID debugging.
Non-success responses use a structured envelope:
{
"error": {
"code": "UPSTREAM_ERROR",
"category": "upstream",
"message": "Underlying provider returned an error.",
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"request_id": "req_abc123",
"retryable": true,
"suggested_action": "retry_with_backoff"
}
}Top-level code, message, and request_id are also present for older SDKs. New integrations should read error.code and error.category.
Categories
| Category | Meaning | Generic handling |
|---|---|---|
auth | LatentKit API key or upstream credential problem | Fix credentials; do not retry blindly. |
billing | Credits or billing state blocks execution | Surface to workspace owner; retry after top-up or plan change. |
policy | Routing policy, guardrail, or request eligibility refused execution | Change policy or request; retries usually repeat the same failure. |
quota | LatentKit or upstream rate limit | Retry idempotent requests with backoff. |
upstream | Provider timeout, 5xx, invalid JSON, or provider-side failure | Retry with exponential backoff; inspect provider/model. |
routing | No healthy eligible route could execute | Check policy, connections, provider health, and model capability. |
gateway | LatentKit deadline or internal failure | Retry transient failures; include request ID with support tickets. |
model_output | Provider returned a syntactically successful but unusable model result | Retry with stricter tool choice, larger token budget, or deterministic fallback. |
Codes
| HTTP | error.code | error.category | Retry? | Meaning |
|---|---|---|---|---|
200 / 503 | TOOL_CALL_NOT_PRODUCED | model_output | Yes | A tool call was required but the model returned text. |
200 / 503 | TRUNCATED_OUTPUT | model_output | Yes | Output ended because the token budget was too small. |
401 | INVALID_API_KEY | auth | No | Missing, invalid, expired, or revoked LatentKit API key. |
402 | INSUFFICIENT_CREDITS | billing | No | Workspace lacks Platform Access credits or budget. |
403 | POLICY_REJECTED | policy | No | Guardrail, route policy, or eligibility rejected the request. |
408 | UPSTREAM_TIMEOUT | upstream | Yes | Provider timed out before returning a response. |
409 | IDEMPOTENCY_KEY_REUSED | gateway | No | Same Idempotency-Key was replayed with a different payload. |
409 | IDEMPOTENCY_REPLAY_IN_PROGRESS | gateway | Yes | The original request for this key is still running; retry shortly. |
409 | IDEMPOTENT_RESULT_UNAVAILABLE | gateway | No | The stored result was too large to replay; use a new key. |
413 | REQUEST_TOO_LARGE | gateway | No | Request body exceeds the edge/app body limit. |
413 | AUDIO_TOO_LARGE | gateway | No | Audio input exceeds the audio byte budget. |
429 | RATE_LIMITED | quota | Yes | LatentKit or upstream rate limit was hit. Retry-After is always present. |
502 | UPSTREAM_ERROR | upstream | Yes | Provider returned a failed response or invalid payload. |
503 | NO_HEALTHY_PROVIDER | routing | Depends | Check retryable: false + suggested_action: "attach_provider_for_capability" means a configuration gap (attach a provider — retries cannot help); true means transient provider unhealth. |
504 | EXECUTION_TIMEOUT | gateway | Yes | LatentKit's end-to-end execution deadline expired. |
Oversized uploads and queue payloads are rejected with the structured envelope (413 for streamed uploads); reduce the payload instead of retrying — see Rate limits.
Backoff guidance
For quota, upstream, routing, and gateway retryable errors, use jittered exponential backoff and cap retries. For model_output, a retry can help, but repeated failures usually mean the prompt, tool_choice, or max_tokens needs to change.
What LatentKit retries for you
Inside a single request, LatentKit only auto-retries transport failures that provably happen before the request reaches the provider (connection establishment). Ambiguous failures — read timeouts and protocol errors after the provider may have accepted the request — are never replayed automatically, because the provider might already have run and billed the generation. They surface as UPSTREAM_TIMEOUT or UPSTREAM_ERROR so your application decides whether a replay is safe.
Route-level failover is separate: when an attempt fails with a retryable provider error, the published policy's next eligible provider/model entry is tried, and you still receive one response.
An unknown purpose is not an error
A purpose that is not configured, is disabled, or cannot serve the
endpoint's capability does not produce an error. The request runs on the app's
assigned route and returns 200. This is deliberate: shipping application code that
names a purpose before someone configures it in the console must not take the feature
down.
Detect it on the response rather than in an error handler:
| Signal | Meaning |
|---|---|
lk_applied_purpose is null while you sent a purpose | The request ran on the app default |
lk_purpose_source is purpose_unknown | The route does not declare that label (purpose_disabled means it exists but is turned off) |
lk_purpose_source is purpose_no_capable_route | The bound route cannot serve this endpoint |
A malformed purpose is a different case and does fail loudly:
| HTTP | Condition |
|---|---|
400 | The label does not match ^[a-z][a-z0-9_-]{0,39}$, or uses the reserved lk- / lk_ prefix — sent in the body or as X-LK-Purpose |
So a typo in your code is a hard error, while a missing console binding is a soft
fallback. Log lk_applied_purpose in production; the console also charts degradations
per purpose on the app's analytics.
Debugging with request IDs
Every response should include X-LK-Request-ID; JSON bodies also include error.request_id. Include that ID when contacting support or correlating your app logs with LatentKit request logs.