Errors overview

Every error the router returns is listed, with its status code and a sample body, in the
Error reference. This page covers what is common to all of them.

Response formats

Errors come back in one of three JSON shapes. Check the status code first, then read the
body that goes with it.

Router errors: most errors, including errors forwarded from a provider:

{
  "error": {
    "message": "Your license is out of budget"
  }
}

Policy denials (403) add a machine-readable code and the offending parameter:

{
  "error": {
    "message": "'temperature' must be between 0 and 1",
    "code": "policy_denied",
    "param": "temperature"
  }
}

Request validation (422, plus 405 for a wrong HTTP method) uses FastAPI’s format,
with detail at the top level instead of error:

{
  "detail": [
    {
      "type": "extra_forbidden",
      "loc": ["body", "top_k"],
      "msg": "Extra inputs are not permitted",
      "input": 40
    }
  ]
}

Two responses fall outside these shapes: GET /v1/health answers {"status": "error"}
when it is unhealthy, and an unexpected internal failure returns 500 with the plain-text
body Internal Server Error.

Status codes

Status Meaning Body Retry?
400 The request can’t be served as sent: out of budget, a bad header, no provider available, a guardrail block, or a provider rejection error No. Fix the request or the license first
401 Missing, malformed, unknown, revoked, or expired credentials error No
403 Your license’s policy does not allow the service or a parameter value error + code + param No
404 The provider reported that the model or resource does not exist error No
405 Wrong HTTP method for the endpoint detail No
422 The request body failed validation detail No
500 Budget misconfiguration or an unexpected provider or internal error error or plain text Maybe. Report it if it persists
503 The provider is unavailable, throttled, timed out, or rejected our credentials; or your license’s policy could not be evaluated error Yes, with backoff

The order checks run in

A request goes through the same checks, in the same order, on every endpoint that calls a
provider. The first one that fails decides the response.

  1. Authentication. A malformed Authorization header or a key that doesn’t resolve
    returns 401 straight away.
  2. Body validation. The JSON body is validated against the endpoint’s schema
    (422). This runs before the check for a missing Authorization header, so a
    request with no key and an invalid body gets 422, not 401.
  3. Missing credentials. No Authorization header returns 401.
  4. Quantized headers. A malformed X-Quantized-Provider or X-Quantized-Metadata
    returns 400.
  5. Budget. A license with no budget left returns 400. A misconfigured budget
    returns 500.
  6. Policy. The license-type policy can deny the service or a parameter value (403).
  7. Provider selection. No provider able to serve the request returns 400.
  8. Guardrails. An input guardrail can block the request (400).
  9. Provider call. The provider’s own errors are mapped to 400, 404, 500, or
    503.
  10. Output guardrails. An output guardrail can block the response (400).

GET /v1/models and GET /v1/license only run steps 1, 3, and 4. GET /v1/health
needs no credentials.

What you are charged for

A request that fails is not charged, whether the router rejects it or the provider
returns an error. The one exception is a response blocked by an output guardrail:
the provider already did the work, so the call is charged even though you receive a
400.

How provider errors are mapped

The router does not pass provider status codes through as-is:

Provider response You receive
400 or 422 (request rejected) 400 with the provider’s message
404 (model or resource not found) 404 with the provider’s message
401 or 403 (our credentials rejected), 429, any 5xx, timeout, connection failure 503 Service temporarily unavailable
Any other status 500

The 503 is fully masked: the message is always the same, no provider details are
included, and no Retry-After header is forwarded. Back off and retry.

Who validates the model

Quantized does not keep a gate in front of the providers. A model id we cannot resolve
in the catalog is forwarded to the provider exactly as you sent it, and the provider
decides whether it exists. The status code and the message you get back are the
provider’s, not ours.

This matters because the same mistake gets a different answer depending on who serves
the endpoint. On the two single-provider endpoints it is at least consistent:

POST /v1/aws-bedrock/embeddings    {"model": "amazon.titan-embed-text-v99:0"}
400  The provided model identifier is invalid.

POST /v1/gemini/embeddings         {"model": "gemini-embedding-9999"}
404  models/gemini-embedding-9999 is not found for API version v1beta, or is not
     supported for embedContent.

Both are the same class of error. AWS calls it a 400, Google calls it a 404.

Where an endpoint has several eligible providers it is not stable even across repeated
calls, because an unresolved model gives the router nothing to route on. Identical
requests to POST /v1/chat/completions with {"model": "nonexistent-model"} can come
back with different wordings and different status codes, depending on which provider
served each one:

404  nonexistent-model is not a valid model ID
404  The model `nonexistent-model` does not exist or you do not have access to it.
404  model: nonexistent-model
400  The provided model identifier is invalid.
Do not branch on this

Treat any 4xx naming the model as “this model is not usable”. Do not match on message
text, do not assume 404, and do not expect two identical requests to fail identically.

Checking a model before you use it

Call GET /v1/models and match on id. That is the list of ids the
catalog resolves; anything else depends on the provider accepting it.

Handling errors

import time

import httpx

def chat(payload, retries=3):
    for attempt in range(retries):
        response = httpx.post(
            "https://api.quantized.us/v1/chat/completions",
            headers={"Authorization": "Bearer sk-quantized-YOUR-KEY"},
            json=payload,
            timeout=60,
        )
        if response.status_code == 200:
            return response.json()

        if response.status_code == 503 and attempt < retries - 1:
            time.sleep(2**attempt)  # provider unavailable: back off and retry
            continue

        if response.status_code in (405, 422):
            for d in response.json()["detail"]:
                print(f"Invalid request: {d['msg']} at {d.get('loc')}")
        elif "application/json" in response.headers.get("content-type", ""):
            error = response.json()["error"]
            if error.get("code") == "policy_denied":
                print(f"Not allowed by your license: {error['message']}")
            else:
                print(f"Error {response.status_code}: {error['message']}")
        else:
            print(f"Error {response.status_code}: {response.text}")
        response.raise_for_status()

Streaming requests report errors that happen after the stream starts inside the stream
itself, with a 200 status. See Errors during a stream.