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:
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-routerand 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-flashorsmart-router-pro.
Another 403 message:
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¶
- Re-issue the request once. Network blips are common.
- Check the gateway logs for the
causefield onFALLBACK_TRY,FALLBACK_GLM_INVOKE,FALLBACK_GLM_FAIL. - If the cause is
OpenRouter API error: 401, the server-side OpenRouter key is bad. Page the operator. - If the cause is
OpenRouter API error: 429, you are being rate limited by OpenRouter. Back off, retry later. - 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¶
- API reference → Errors — full status-code matrix.
- Troubleshooting → Common issues — symptom-based debugging.