Errors reference

Every error the inference API returns uses one envelope:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "human-readable explanation",
    "code": "machine-readable code",
    "param": "the offending field, when there is one"
  }
}

type is one of a small fixed set (authentication_error, invalid_request_error, permission_error, not_found, provider_error, provider_unavailable, rate_limit_error, internal_server_error). code is more specific: branch on it in code, and show message to humans. The messages below are the real strings the gateway produces, not paraphrases.

Branch on code, not on type. Several codes share a type, because type is the coarse OpenAI-compatible bucket and code is what tells you which failure you actually hit. provider_unavailable covers an exhausted daily quota and an upstream that never answered, and those need different responses from a client.

Every code the inference API can emit appears on this page: those in a JSON error body, those from the auth and rate-limit middleware that runs ahead of every route, and the one delivered as an SSE error event on an already-started stream. That is enforced by a test which walks the gateway source for every emission site and fails the build if a code ships without a payload here.

401: authentication

You’ll see one of:

{
  "error": {
    "type": "authentication_error",
    "message": "Missing Authorization header",
    "code": "authentication_error"
  }
}

A key whose shape rules it out gets a different code, because the fix is different, and we can tell without looking it up:

JSON
{
  "error": {
    "type": "authentication_error",
    "message": "API key is malformed: the key body is 20 characters, expected 43 — this is usually a copy-paste truncation. Re-copy the full token, or create a new key at /app/keys.",
    "code": "api_key_malformed"
  }
}

api_key_malformed covers a truncated or over-copied key (the body is not exactly 43 characters), a token that doesn’t start with sk-lr-, and the retired sk-bf-* scheme:

{
  "error": {
    "type": "authentication_error",
    "message": "API key is malformed: this is a retired sk-bf-* key and the scheme is no longer accepted. Create a new key at /app/keys.",
    "code": "api_key_malformed"
  }
}

Fix: send Authorization: Bearer sk-lr-... with a key from the dashboard. api_key_malformed means the token can’t be one of ours as written. It is nearly always a copy-paste truncation, so re-copy the whole thing. api_key_not_registered means the shape is right but no account owns it: check you copied the whole key and are using the right account, or mint a new one; a key that used to work may have been revoked. api_key_expired means the key reached its expiry date: keys have a bounded lifetime (default 90 days), so create a fresh key in the dashboard and rotate it in. The expired one stays listed there as a reminder but can no longer authenticate.

402: insufficient credits

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Insufficient credits. Balance: €0.0000. Your balance must be above zero to make requests. Please add credits to your account.",
    "code": "insufficient_credits"
  }
}

The balance is checked before the request is forwarded, so a 402 never costs you tokens. Any positive balance admits a request; because usage is settled after the response, a single request can take the balance below zero, and further requests 402 until you top up.

Fix: top up in Credits & billing.

400: malformed request

Missing or empty messages:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "messages field is required and cannot be empty",
    "code": "invalid_request_error",
    "param": "messages"
  }
}

A model id that doesn’t match the public shape ({provider}/{creator}/{model}[/{locode}]; see available models):

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "model id must be {provider}/{creator}/{model}[/{locode}]: got 5 segments",
    "code": "invalid_request_error",
    "param": "model"
  }
}

An alias name that violates the naming rule:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "alias name must be 1-64 chars: lowercase a-z, 0-9, '-' or '_', starting with a letter or digit (got \"My Alias\")",
    "code": "invalid_alias_name",
    "param": "model"
  }
}

A sampling parameter outside the OpenAI-compatible envelope:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "temperature must be between 0 and 2, got 99",
    "code": "invalid_request_error",
    "param": "temperature"
  }
}

These bounds are checked by the gateway before any provider is contacted, so an out-of-range value costs you no upstream call and never counts against a route’s health:

temperature
0 to 2 inclusive
top_p
0 to 1 inclusive
frequency_penalty
−2 to 2 inclusive
presence_penalty
−2 to 2 inclusive
n
1 or greater
max_tokens
1 or greater
max_completion_tokens
1 or greater
stop
at most 4 sequences

Omitting a parameter is always valid: the serving provider applies its own default. The upper bound on max_tokens is the model’s own max_output_tokens, published per model in available models; providers clamp to it rather than rejecting, so it is not checked here.

An unknown message role:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "messages[2].role \"assitant\" is not a valid role (expected one of: system, developer, user, assistant, tool)",
    "code": "invalid_request_error",
    "param": "messages"
  }
}

The valid roles are system, developer, user, assistant and tool. A role outside that set is rejected rather than forwarded: providers translate an unrecognised role as an ordinary user turn, so a typo like "assitant" would otherwise be answered (and billed) as a conversation you did not describe.

The legacy function role is not accepted. No provider LowRouter routes to implements it, so it was never really answered as a function message; it was silently treated as a user turn. Use tool with a tool_call_id, which is OpenAI’s replacement for it.

Fix: the message names the field; param confirms it. Compare against the request shape in your first completion.

A route cannot express a parameter

Some parameters change the answer itself: response_format, logprobs, logit_bias and n. When the serving route cannot apply one, LowRouter refuses the request instead of answering without it. The error’s param names the parameter:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "LowRouter rejected the request before it reached provider anthropic: logprobs is not supported by provider anthropic: this route does not return token log probabilities",
    "param": "logprobs"
  }
}

The message starts with “LowRouter rejected the request” because the refusal is ours: the request never reached the provider. A refusal the provider wrote starts with “Provider … rejected the request” instead (see below).

Fix: pin a route that supports the parameter, or drop the parameter. On an auto/ request, LowRouter already tried the other routes before returning this. The parameter support table shows which routes apply what.

A provider rejected the request

Some values can only be judged by the model that serves them, such as a parameter combination one provider forbids, or a field it does not implement. When the upstream rejects the request itself, that comes back as a 400 too, quoting what the provider said:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Provider mistral rejected the request: {\"type\":\"invalid_request_error\",\"message\":\"Input should be less than or equal to 1\",\"param\":\"top_p\"}",
    "code": "invalid_request_error"
  }
}

Fix: the upstream’s own message names what it objected to. This is not a provider fault, so retrying, or pinning a different route, will not help: every candidate rejects the same payload. On an auto/ request the gateway stops at the first such rejection rather than walking the rest of the ladder, and no circuit breaker is affected.

413: request body too large

The request body exceeds the 32 MB cap. This is checked before the body is read, so an oversized request is refused without being uploaded in full:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Request body is too large",
    "code": "request_too_large"
  }
}

Fix: the cap is on the whole JSON body, so it is usually a base64 image or a very long transcript that pushes a request over. Send images by URL instead of inline base64 where the model accepts one, or trim the conversation history you replay on each turn.

404: the route doesn’t exist

Unknown model on a provider:

JSON
{
  "error": {
    "type": "not_found",
    "message": "model \"anthropic/claude-opus-4.6\" is not offered by provider \"openai\"",
    "code": "not_found",
    "param": "model"
  }
}

A model that is only served in specific regions, addressed with an explicit /global it doesn’t have. (Omitting the region entirely does not produce this error; it resolves to the model’s default region; see how IDs resolve. You only see this when you pin global explicitly.)

JSON
{
  "error": {
    "type": "not_found",
    "message": "model \"anthropic/claude-opus-4.6\" on provider \"vertex\" is served only in specific regions and has no global endpoint. Append a locode (e.g. \"vertex/anthropic/claude-opus-4.6/{locode}\") and check /providers/vertex for available regions, or omit the region to route to the model's default one",
    "code": "not_found",
    "param": "model"
  }
}

A region the model isn’t served in. This is deliberate: we never silently substitute a different region. The region you pin is the region you get, even when a “nearby” one would work. It is a 404, not a 503, because the route as written does not exist and retrying can never succeed; a retry-on-5xx loop must not hammer it:

JSON
{
  "error": {
    "type": "not_found",
    "message": "model \"mistral/mistral-large\" is not served in region \"de-ber\" on provider \"mistral\" — check /providers/mistral for available regions",
    "code": "not_found",
    "param": "model"
  }
}

An alias that doesn’t exist on your account:

JSON
{
  "error": {
    "type": "not_found",
    "message": "alias \"alias/big\" does not exist on this account — create or repoint aliases in the dashboard at /app/aliases",
    "code": "alias_not_found",
    "param": "model"
  }
}

A model we’ve fully disabled is reported as absent rather than as broken, because for callers it is:

JSON
{
  "error": {
    "type": "internal_server_error",
    "message": "Model not found",
    "code": "model_not_found",
    "param": "model"
  }
}

(The type on that one is a known wart. Branch on the code, which is model_not_found, and on the 404 status. A soft-disabled model is different: it returns 503 model_unavailable and names a reason.)

Fix: browse the model catalog or GET /v1/models for exact ids; aliases are per-account, so a key from another account won’t see yours.

403: your routing policy refused the route

An auto/ id whose every candidate route was gated out by your account’s own routing policy. The model exists and nothing is down; the hard gates you configured (a CLOUD Act denial, a carbon or price ceiling, a provider, creator or region restriction) left nothing to route to. The message names each gate and how many routes it removed:

JSON
{
  "error": {
    "type": "permission_error",
    "message": "no route for auto/mistralai/mistral-large-2512 survives your routing policy: 2 routes excluded by deny_cloud_act — relax the gate, or pin a full {provider}/mistralai/mistral-large-2512/{locode} id",
    "code": "routing_policy_exhausted",
    "param": "model"
  }
}

This is deliberately not a 404 (the id is real and spelled right) and not a 503 (retrying can never succeed). A ceiling that empties the set is usually the unknown value rule: while max_carbon or max_price is set, a route with no published figure is excluded, because an unknown value cannot be shown to satisfy a ceiling. Leaving the ceiling empty ranks such routes last instead of dropping them.

Fix: open the routing tester at /app/routing, paste the id, and read which gate excluded each route; relax that gate, or pin a full 4-part id. Policy shapes auto/ selection only and never overrides an explicit route.

451: not available in your region

The request comes from a territory where LowRouter or the model’s providers may not be used (restricted territories). Retrying won’t help. The message names the territory and links the list.

From a sanctioned territory, on every endpoint:

JSON
{
  "error": {
    "type": "permission_error",
    "message": "LowRouter is not available in your region (IR). See https://lowrouter.ai/docs/guides/restricted-territories/",
    "code": "unavailable_for_legal_reasons"
  }
}

When every route that could serve the model has a provider whose terms exclude your territory (for an auto/ id, every candidate; for a pinned id, its provider):

JSON
{
  "error": {
    "type": "permission_error",
    "message": "auto/meta/muse-spark-1.3 is not available in your region (CN): provider terms exclude that territory — see https://lowrouter.ai/docs/guides/restricted-territories/",
    "code": "unavailable_for_legal_reasons",
    "param": "model"
  }
}

When an account’s billing country, as verified by Stripe at checkout, is in a sanctioned territory, every request fails with:

JSON
{
  "error": {
    "type": "permission_error",
    "message": "This account is suspended: its billing country is a restricted territory. Contact support. See https://lowrouter.ai/docs/guides/restricted-territories/",
    "code": "unavailable_for_legal_reasons"
  }
}

Fix: none from the client side. For a model restriction, an auto/ id lets another provider serve the request where one may.

503: the route exists but can’t serve you right now

A model we’ve marked unavailable names the reason when there is one:

JSON
{
  "error": {
    "type": "provider_unavailable",
    "message": "This model is currently unavailable: deprecated upstream. See https://lowrouter.ai/docs/model-availability",
    "code": "model_unavailable",
    "param": "model"
  }
}

The same code covers a model whose every route is currently quota-held, so nothing callable is left to route to:

JSON
{
  "error": {
    "type": "provider_unavailable",
    "message": "The model 'mistral/mistral-large' has no currently available route. See https://lowrouter.ai/docs/model-availability",
    "code": "model_unavailable",
    "param": "model"
  }
}

When the route itself fails upstream, the request returns 503 naming the provider that failed and quoting what it said:

JSON
{
  "error": {
    "type": "provider_error",
    "message": "Provider mistral failed: upstream 503 service unavailable",
    "code": "provider_error"
  }
}

An explicit four-segment ID is a pin: it is never silently retried on a different provider, so the provider you see is the one you named.

Not every upstream refusal is a 503, and the difference matters to your retry logic. When an upstream rejects the request (a parameter it will not accept, a content block it does not implement), you get a 400 invalid_request_error quoting what it said, because retrying that request on this route or any other gets the same answer:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Provider scaleway rejected the request: Unknown part type: file",
    "code": "invalid_request_error"
  }
}

We classify on what the upstream actually means rather than on the status it happens to use. Some providers answer an unsupported feature with a 501, which is a 5xx by number but a statement about the feature, not about their health, so it reaches you as a 400, and it does not count against that route for other callers.

The same rule runs the other way. Some providers answer a full queue or a concurrency limit with a 4xx. That is a statement about their capacity at that moment, not about your request, and the same request succeeds once the queue drains. So it reaches you as a 503, and it counts against that route like any other provider failure:

JSON
{
  "error": {
    "type": "provider_error",
    "message": "Provider aki failed: There is currently no free queue slot on this endpoint!",
    "code": "provider_error"
  }
}

There is no Retry-After header on this one, because the upstream doesn’t tell us when a slot frees up, so back off with jitter and retry. An auto/ request doesn’t stop at a busy candidate: it moves on to the next one, so you only see this once the walk has run out of candidates it could try, with the routing receipt described below.

An auto/ ID is a delegation, and it may fall through its published candidates in order before giving up. When it does, the error body carries the routing receipt: what was tried, what was skipped, and what was left:

JSON
{
  "error": {
    "type": "provider_error",
    "message": "Provider tensorx failed: upstream 503 service unavailable (tried 2 of 5 candidate routes within the routing budget; 2 remaining — pin one of the model's published candidates to reach it directly)",
    "code": "provider_error",
    "providers_attempted": ["berget", "tensorx"],
    "breaker_skipped": ["nebius/us-mci"],
    "candidates_remaining": 2,
    "last_error": "upstream 503 service unavailable"
  }
}

candidates_remaining is the field to branch on. 0 means every route was tried or ruled out and the model is unavailable; retrying the same ID will not help. Above zero means the walk stopped on its budget with routes left, and pinning one of the remaining candidates from the model’s catalogue entry may work right now. breaker_skipped names routes declined because recent failures had opened their circuit breaker.

These four fields appear only on auto/ failures, and on every status a failed route can return: the 503s above, the provider_quota_exhausted 503 and the 504. On an explicit ID they are absent, because a pin has no candidates to fall through to, so their absence is itself the answer. On success the same account rides lowrouter_metadata.providers_attempted and fallback_occurred instead.

provider_error also covers a model we can’t price. We decline rather than serve a request we couldn’t bill correctly:

JSON
{
  "error": {
    "type": "provider_error",
    "message": "This model is temporarily unavailable due to a pricing configuration issue. Please try another model or contact support.",
    "code": "provider_error"
  }
}

If that same pricing fault is only discovered after a stream has started, there is no status line left to change, so it arrives as an SSE error event on the open stream instead, carrying its own code:

JSON
{
  "error": {
    "type": "provider_error",
    "message": "This model is temporarily unavailable due to a pricing configuration issue. Please try another model or contact support.",
    "code": "cost_unresolvable"
  }
}

That event is sent as a data: frame and the stream is then closed without a data: [DONE]. The missing terminator is the signal: a stream that ends without it is incomplete, not finished. This is request-scoped: the model itself is not blocked.

A provider with no credentials configured on our side fails closed:

JSON
{
  "error": {
    "type": "provider_unavailable",
    "message": "Provider ionos is not configured",
    "code": "provider_not_configured"
  }
}

And a replica that has just started may not have its routing snapshot yet. This one is transient, so retry shortly:

JSON
{
  "error": {
    "type": "internal_server_error",
    "message": "Auto routing is not ready on this replica yet — retry shortly",
    "code": "service_unavailable",
    "param": "model"
  }
}

Fix: model_unavailable names its reason; point an alias at an alternative route so clients don’t need redeploying when a model is withdrawn or every route is quota-held. service_unavailable needs nothing but a retry; provider_not_configured and provider_error on a pricing fault are ours to fix; tell us if one persists.

Daily quota exhausted

When an upstream provider has spent its daily token quota for a route, retrying is pointless until the quota resets, so the gateway tells you exactly that, with "code": "provider_quota_exhausted" and a Retry-After header carrying the seconds until the reset (midnight UTC):

JSON
{
  "error": {
    "type": "provider_unavailable",
    "message": "Provider aws-bedrock has exhausted its daily token quota for bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0 in jp-tyo. The quota resets at midnight UTC; retry after the Retry-After interval.",
    "code": "provider_quota_exhausted",
    "param": "model"
  }
}

The region in the message is the locode the route is billed and reported under (the same one that appears as the fourth segment of a model ID), not the upstream provider’s own region name.

While the quota is exhausted the affected region also disappears from /v1/models, so a fresh listing won’t hand you an ID that can’t serve. Your pin is still honoured and nothing is substituted.

On an auto/ ID this error also carries the routing receipt described under 503 (providers_attempted, breaker_skipped and candidates_remaining), because a walk that ended on an exhausted quota may still have left routes untried. If candidates_remaining is above zero, another published candidate is reachable now and the Retry-After applies only to the route that ran out.

Fix: honour Retry-After instead of hammering the route, or point the request (or an alias) at another region or provider serving the same model.

504: the provider didn’t answer in time

Every dispatch is bounded. An upstream that accepts the connection and then goes silent used to hang until something else gave up; now the gateway answers for it, with a 504 and "code": "provider_timeout":

JSON
{
  "error": {
    "type": "provider_unavailable",
    "message": "provider ionos did not respond within the first-byte deadline of 30s",
    "code": "provider_timeout"
  }
}

This is the one status you should retry. A 503 means the route can’t serve you and retrying it unchanged is usually pointless; a 504 means we never heard back, so the same request may well succeed on the next attempt or on another route.

That reasoning holds because a 504 payload means nothing was delivered and nothing was billed. It does not extend to the truncated-stream case below, where a 200 was already sent and you were charged for the tokens that arrived. Retrying that one re-runs work you have paid for, so treat it as a resume-or-accept decision rather than an automatic retry.

type stays provider_unavailable, so an existing client that branches on type keeps working unchanged. What’s new is the code and the status.

On an auto/ ID the 504 also carries the routing receipt described under 503 (providers_attempted, breaker_skipped and candidates_remaining), so a timeout that stopped with routes untried is distinguishable from one that exhausted the list. If candidates_remaining is above zero, pinning one of the model’s published candidates is a better next step than retrying the same auto ID.

Three budgets can expire, and the message names which one:

BudgetLimitWhat it catches
first-byte30s streaming, 2m30s non-streamingAn upstream that never sends a first byte (the dark-provider case). On a stream, time to first byte doesn’t depend on how long a generation runs, so 30s stays tight without truncating slow-but-working completions. A non-streaming request is forwarded with stream=false, and an OpenAI-compatible upstream then sends nothing until the whole answer is ready, so there the budget caps the full generation and is set to 2m30s.
stream-idle1m30sA stream that starts and then stalls between chunks.
overall5m0sThe total budget for one attempt. A ceiling against pathology, not a latency target.

On an auto/ request whose failover walk has already hit a timeout, later streaming attempts run under a shortened 15s first-byte budget instead of the full 30s, so more of the candidate list fits inside the walk’s overall budget. Non-streaming attempts are never shortened that way, since a short first-byte budget there would cut off healthy slow generations rather than catching dark ones. A non-streaming failover attempt gets its 2m30s budget or whatever remains of the walk’s overall budget, whichever is less.

What this means in practice: on a dark provider, a non-streaming request waits up to 2m30s before it fails over or returns a 504, where a streaming one gives up after 30s. If your client needs to detect a dead route quickly, stream. Long reasoning answers, large outputs and vision calls on the non-streaming path are not cut off at 30s.

The other two read the same way, with the budget rendered as the gateway prints it:

{
  "error": {
    "type": "provider_unavailable",
    "message": "provider ionos did not respond within the stream-idle deadline of 1m30s",
    "code": "provider_timeout"
  }
}

There is no Retry-After header on a 504. We don’t know when the upstream will come back, and inventing an interval would be a guess. Back off with jitter instead.

One case produces no JSON at all. If a streaming response has already started (the 200 is sent and chunks are on the wire), a stall can’t be retracted into an error body: the status line is spent, and appending JSON to a half-finished SSE stream would give you something that parses as neither. The stream is terminated with data: [DONE] instead. So a stream that ends early without a finish_reason may be a stall rather than a completion; you’re billed only for the tokens actually delivered. A non-streaming request, and a streaming one that hasn’t sent anything yet, both still get the 504 payload above.

Fix: retry with exponential backoff and jitter. If one provider times out repeatedly, route around it: an auto/ id picks a live route for you, or point an alias elsewhere so clients don’t need redeploying.

429: rate limited

Two limiters can produce a 429, both with a Retry-After header. The per-IP limiter guards the edge:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Rate limit exceeded. Too many requests from your IP address. Try again in 30 seconds",
    "code": "rate_limit_exceeded"
  }
}

Your key’s own RPM/TPM limits produce the same code under a different type, and the message names which budget you hit:

JSON
{
  "error": {
    "type": "rate_limit_error",
    "message": "Rate limit exceeded: 60 requests per minute. Try again in 12 seconds.",
    "code": "rate_limit_exceeded"
  }
}

The token-per-minute budget reads the same way with tokens in place of requests. Because both limiters share "code": "rate_limit_exceeded", code alone won’t tell you which one you hit. Check type, or just honour Retry-After, which is correct for both.

An upstream provider’s rate limit is not passed through as a 429: it surfaces as one of the 503 provider failures above, and so does an upstream capacity refusal (a full queue, a concurrency limit), even when the provider itself sent it as a 4xx.

Fix: honour Retry-After and back off with jitter, or route the workload across more than one provider.

Managing aliases

These come from the alias endpoints (/v1/aliases), not from completions. Creating a name that already exists on your account is a 409:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "An alias named \"big\" already exists on this account",
    "code": "alias_exists",
    "param": "name"
  }
}

Accounts have a bounded number of alias slots (422):

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Alias limit reached (5). Delete an alias to free a slot, or contact support to raise the limit.",
    "code": "alias_limit_reached",
    "param": "name"
  }
}

A target that isn’t a routable model (unknown model or provider, a region that isn’t served, a region that’s required and missing) is a 422 carrying the router’s own explanation, so the fix is the same one you’d apply to a completion:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Invalid alias target: model \"mistralai/mistral-large\" is not served in region \"de-ber\" on provider \"mistral\" — check /providers/mistral for available regions",
    "code": "invalid_alias_target",
    "param": "target"
  }
}

A target sent in a shape we don’t accept gets the shape rules back:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Target requires provider and canonical_id (locode optional, default global). Send them as flat top-level fields: {\"provider\": \"...\", \"canonical_id\": \"...\", \"locode\": \"...\"} — a nested {\"target\": {...}} object, a \"provider/canonical_id/locode\" string, or an \"auto/<creator>/<model>\" string is also accepted. Provider \"auto\" delegates the route to LowRouter's auto ranking and takes no locode. On repoint, send all fields, not just the one changing.",
    "code": "invalid_alias_target",
    "param": "target"
  }
}

Note the last sentence: a repoint takes the full triple. Sending only the field you’re changing is the common mistake: absent fields decode as empty, not as “leave alone”.

Fix: param names the field. See aliases for the full lifecycle.

A fallback model that doesn’t exist

A fallback chain is checked when you save it, not only when it runs, so a typo is caught before an outage depends on the chain:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Fallback model \"auto/anthropic/claude-sonnet-6\" is not in the catalogue — see GET /v1/models",
    "code": "invalid_fallback_model",
    "param": "fallback.models"
  }
}

An entry that names a real model on a route we don’t serve reads the same way, with the router’s own explanation:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "message": "Fallback model \"mistral/mistralai/mistral-large-2512/de-ber\" is not usable: model \"mistralai/mistral-large-2512\" is not served in region \"de-ber\" on provider \"mistral\" — check /providers/mistral for available regions",
    "code": "invalid_fallback_model",
    "param": "fallback.models"
  }
}

Fix: use auto/{creator}/{model} entries where you can. They pick a route at the moment the fallback is needed, so they survive a region or provider going away. A model withdrawn after you save a chain is skipped at request time rather than failing the request.

500: something broke on our side

You should not see these. When you do, it’s ours to fix. The message says which step failed, and nothing was billed:

{
  "error": {
    "type": "internal_server_error",
    "message": "Failed to create alias",
    "code": "internal_server_error"
  }
}

Fix: retry once in case it was transient, then tell us with the model id, the timestamp and the exact message, which let us find your request in the logs.

What you’ll never get

No silent substitutions: a failed constraint (region, jurisdiction, alias) is an error, not a quiet reroute to somewhere you didn’t ask for. If you got a 2xx, the response’s lowrouter_metadata tells you exactly who served it and where; see per-request metadata.