Errors
HTTP status → SDK class. Every `4xx` and `5xx` the gateway returns, with retry policy.
The gateway returns errors in the OpenAI-compatible shape. The SDK
maps each status to a typed CRError subclass so your handlers
stay readable.
HTTP → SDK class
| HTTP | error.code | SDK class | When |
|---|---|---|---|
| 400 | validation_error | CRValidationError | Bad request body, malformed model id |
| 401 | unauthorized | CRAuthError | Missing, invalid or revoked API key |
| 402 | budget_exceeded | CRBudgetExceededError | Per-key budget reached |
| 404 | model_not_found | CRModelNotFoundError | Model id not in the registry |
| 408 | timeout | CRTimeoutError | Client-side timeout exceeded (SDK only) |
| 410 | model_deprecated | CRModelDeprecatedError | Model retired by the provider |
| 429 | rate_limit_exceeded | CRRateLimitError | Sliding-window rate limit exceeded |
| 502 | provider_down | CRProviderDownError | Upstream 5xx with no healthy alternative |
| 503 | provider_down | CRProviderDownError | All EU providers unavailable |
| 504 | upstream_timeout | CRProviderDownError | Upstream read timeout |
| 500 | internal_error | CRError | Unspecified — should never happen, file a bug |
Body shape
{
"error": {
"message": "Rate limit exceeded for cr_live_abc… (60 RPM)",
"type": "rate_limit_exceeded",
"code": "rate_limit_exceeded"
}
}X-Request-Id is always set so you can include it in bug reports.
Typed handling
import {
CRError,
CRAuthError,
CRRateLimitError,
CRModelNotFoundError,
CRModelDeprecatedError,
CRBudgetExceededError,
CRProviderDownError,
CRValidationError,
CRTimeoutError,
} from '@cleverrouter/sdk';
try {
await cr.chat({ model: 'mistral/mistral-small-3.2', messages });
} catch (err) {
if (err instanceof CRRateLimitError) {
console.warn(`Retry in ${err.retryAfterSec ?? 60}s`);
} else if (err instanceof CRAuthError) {
// 401 — rotate the key
} else if (err instanceof CRBudgetExceededError) {
// 402 — top up
} else if (err instanceof CRModelNotFoundError) {
// 404 — typo or model not yet synced
} else if (err instanceof CRModelDeprecatedError) {
// 410 — pick a successor
} else if (err instanceof CRTimeoutError) {
console.warn(`Timed out after ${err.timeoutMs}ms`);
} else if (err instanceof CRProviderDownError) {
// 502/503/504 — try later
} else if (err instanceof CRValidationError) {
// 400 — fix the request body
} else if (err instanceof CRError) {
console.error(err.status, err.code, err.requestId);
} else {
throw err;
}
}With `cr.openai()`
Errors come from the OpenAI SDK's own subclasses (APIError,
RateLimitError, AuthenticationError, …). The CR* classes
apply to the gateway-direct helpers (cr.chat, cr.stream,
cr.embed, cr.rerank, cr.transcribe, cr.listModels).
Retry policy
| Status | Retried by SDK | Notes |
|---|---|---|
429 | yes | Honours Retry-After |
502 / 503 / 504 | yes | Exponential backoff with jitter |
408 (CRTimeoutError) | no | Already at the client-side timeout limit |
401 / 402 / 404 / 410 | no | Needs code or config changes, not waiting |
400 | no | Same |
Configure retry/timeout: Retry & timeout.
Rate-limit headers
Every response carries:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 47
X-RateLimit-Enforced: trueX-RateLimit-Enforced: false means Redis was unreachable during the
request — the gateway fails open rather than blocking traffic. Back
off voluntarily when you see it. See Rate
limits.
Related
- Retry & timeout — how to override defaults.
- REST API — endpoints and body shapes.
- Changelog — new error codes if any.