Skip to content

Errors

The gateway uses standard HTTP status codes. Errors that originate inside the gateway come back as JSON with a detail field. Errors that originate at OpenRouter are wrapped in the same shape but the HTTP status may be different.

Quick reference

Status Source Typical meaning
200 Upstream Success.
400 Gateway Malformed request.
401 Gateway Missing or malformed Authorization.
403 Gateway Privacy gate, free-model fail-closed, or STOP_FOR_HUMAN with action != ROUTE/SCOUT.
404 Gateway Unknown route.
422 Gateway Pydantic validation error.
500 Gateway Internal error after upstream exhausted.
502 / 503 / 504 Upstream OpenRouter connection failure after retries.
429 Upstream OpenRouter rate limit (gateway retries internally).

All error bodies look like:

{"detail": "human-readable explanation"}

Validation errors (422) come from Pydantic and use a slightly different shape; treat the detail as a list of validation violations.

Authentication failures

Symptom Cause Fix
401, detail empty or Not authenticated Missing Authorization header Add Authorization: Bearer $KEY.
401, detail Invalid authentication credentials Header malformed (e.g. no Bearer prefix) Use the literal prefix Bearer.
The request reaches /v1/chat/completions and 200s but logs show identity tier STATELESS Header present but token ≤ 10 chars Use a longer token (the v1.0.0 resolver requires length > 10).

Privacy gate (403)

The most common client-side error.

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "detail": "A force-free request that violates privacy policy must fail closed."
}

Cause: you sent smart-router-free (or used X-Scout: true / a metadata.routing=free directive) on a request that was marked sensitive: true either explicitly or implicitly (sensitive defaults to true).

Fixes:

  • Use smart-router and let the gateway pick.
  • Set "metadata": {"sensitive": false} deliberately — only when you are sure the content is safe to send to a free public model.
  • Use smart-router-flash or smart-router-pro.

Another 403 message:

{"detail": "Scout cannot be called on sensitive data."}

Means the X-Scout: true header was sent on a sensitive request. Same fix as above.

STOP_FOR_HUMAN (403)

The gateway returns this when policy refuses the request:

  • All models unhealthy and no fallback is healthy.
  • Hard budget cap exceeded and the policy is STOP_AND_REQUIRE_HUMAN.
  • The privacy gate fired and the request was explicit-free.

The detail message includes the cause. Treat it as the operator's queue: someone needs to either resolve the upstream issue, raise the budget, or relax the privacy setting.

Upstream errors (500, 502, 503, 504)

Status Body What it means
500 {"detail": "OpenRouter API error: <code>"} OpenRouter returned a non-retryable 4xx. Common: 400 (bad request shape), 401 (bad OpenRouter key — server-side), 403 (provider refusal).
500 {"detail": "OpenRouter API failed after retries with status <code>"} Retryable status exhausted after 3 attempts.
502 / 503 / 504 {"detail": "OpenRouter connection failed: ..."} Transport error.

For Pro calls, the gateway attempts a single fallback to z-ai/glm-5.2 before returning 500. So if you see 500 on a Pro request, both the primary and the fallback failed.

What to do about a 500

  1. Re-issue the request once. Network blips are common.
  2. Check the gateway logs for the cause field on FALLBACK_TRY, FALLBACK_GLM_INVOKE, FALLBACK_GLM_FAIL.
  3. If the cause is OpenRouter API error: 401, the server-side OpenRouter key is bad. Page the operator.
  4. If the cause is OpenRouter API error: 429, you are being rate limited by OpenRouter. Back off, retry later.
  5. If the cause is OpenRouter connection failed, this is a network issue. Check your connectivity first; if you can reach OpenRouter from your machine, the gateway can too.

Error handling in clients

Recommended pattern:

import time
from openai import OpenAI, APIError, RateLimitError, APIConnectionError

client = OpenAI(
    base_url="https://smart-openrounter.bee1x.one/v1",
    api_key=os.environ["SMART_GATEWAY_API_KEY"],
)

def call(messages, **kw):
    for attempt in range(3):
        try:
            return client.chat.completions.create(model="smart-router", messages=messages, **kw)
        except RateLimitError:
            time.sleep(2 ** attempt)
        except APIConnectionError:
            time.sleep(2 ** attempt)
        except APIError:
            raise  # do not retry on 4xx other than 429
    raise RuntimeError("retries exhausted")

This mirrors the gateway's own retry policy against OpenRouter; the two layers cooperate rather than fighting.

What is logged when something fails

The structured JSON log line for a failure includes:

{
  "level": "ERROR",
  "message": "OpenRouter API failed after retries with status 503",
  "logger": "smart_gateway",
  "timestamp": "2026-08-23T…Z"
}

Routing events look like:

{
  "level": "INFO",
  "message": "Routing decision made",
  "decision": {
    "action": "ROUTE",
    "model": "deepseek_v4_flash",
    "cause": "score_flash"
  },
  "task_id": "…"
}

No request or response bodies, no message content, no API keys. If you need to share a log snippet with the operator, the body of the chat is already excluded.

More