Error codes
The gateway's error truth table — HTTP status × the three protocol error shapes, streaming error behaviour, per-endpoint timeouts and how to debug
Error responses are protocol-adaptive: whichever protocol endpoint received the request, the error comes back in that protocol's official format, so your SDK can parse it natively.
Error body shapes (three protocols side by side)#
OpenAI shape (/v1/chat/completions, /v1/responses, /v1/embeddings, and the image / video endpoints):
{"error": {"message": "...", "type": "...", "code": "..."}}
Anthropic shape (/v1/messages):
{"type": "error", "error": {"type": "...", "message": "..."}}
Gemini shape (/v1beta/models/{model}:generateContent):
{"error": {"code": 402, "message": "...", "status": "RESOURCE_EXHAUSTED"}}
Note: the string
codefield (such asquota_exceeded) only exists in the OpenAI shape. Anthropic error bodies have justtypeandmessage; Gemini'scodeis the numeric HTTP status, with the enum semantics instatus. When using the Anthropic or Gemini SDK, match on the corresponding column in the table below.
Error truth table#
| HTTP | OpenAI code | OpenAI type | Anthropic type | Gemini status | Cause |
|---|---|---|---|---|---|
| 400 | model_not_found | invalid_request_error | not_found_error | NOT_FOUND | model id isn't in the registry |
| 400 | model_not_allowed | invalid_request_error | invalid_request_error | INVALID_ARGUMENT | the key's allowed_models allowlist doesn't include that model |
| 400 | invalid_request | invalid_request_error | invalid_request_error | INVALID_ARGUMENT | malformed request body |
| 400 | tool_not_supported | invalid_request_error | invalid_request_error | INVALID_ARGUMENT | the tool doesn't match the upstream channel's capabilities |
| 400 | invalid_idempotency_key | invalid_request_error | invalid_request_error | INVALID_ARGUMENT | malformed Idempotency-Key header (empty, >200 chars, or containing whitespace / non-printable characters) |
| 409 | idempotency_key_reuse | invalid_request_error | invalid_request_error | ALREADY_EXISTS | the same Idempotency-Key was used for a different request body — use a fresh key |
| 409 | idempotency_in_flight | invalid_request_error | invalid_request_error | ALREADY_EXISTS | the previous call with this key is still running — wait a few seconds and retry with the same key; a new key really would submit again |
| 409 | idempotency_response_not_cached | invalid_request_error | invalid_request_error | ALREADY_EXISTS | the original request succeeded and was billed, but its response was too large to retain and cannot be replayed — use the task list endpoints instead |
| 401 | invalid_api_key | authentication_error | authentication_error | UNAUTHENTICATED | key missing / invalid / revoked / expired / account disabled |
| 402 | quota_exceeded | insufficient_quota | billing_error | RESOURCE_EXHAUSTED | account balance exhausted |
| 403 | permission_denied | permission_error | permission_error | PERMISSION_DENIED | the upstream provider refused (passed through) |
| 429 | rate_limit_exceeded | rate_limit_error | rate_limit_error | RESOURCE_EXHAUSTED | over your account's per-minute or concurrency limit, or an upstream rate limit (passed through) |
| 500 | internal_error | server_error | api_error | INTERNAL | a gateway-side fault |
| 502 | upstream_unreachable | api_error | api_error | UNAVAILABLE | couldn't connect to the upstream |
| 503 | no_channel_available | server_error | overloaded_error | UNAVAILABLE | the model exists but has no channel on this protocol |
| 504 | upstream_timeout | api_error | api_error | DEADLINE_EXCEEDED | upstream timed out (180s on chat endpoints) |
| upstream's own status | mapped by status — see Upstream error pass-through | as left | api_error | UNAVAILABLE | a non-2xx upstream status passed through unchanged |
On Gemini endpoints, 402 and 429 share
status: "RESOURCE_EXHAUSTED"— use the numericcode(402 vs 429) to tell them apart.
400 Bad Request#
Most gateway-generated errors are 400s, and these four are the ones people actually hit.
model_not_found#
The requested model id isn't in the pricing registry.
OpenAI shape:
{"error": {"message": "model `claude-3.5-opus` is not available", "type": "invalid_request_error", "code": "model_not_found"}}
How to debug:
- Check the spelling —
claude-haiku-4-5-20251001includes a date suffix that must be complete - Compare against the full model list. Placeholder SKUs returned by
GET /api/v1/models/publicwithcallable=falsealso produce this error - Note: when the model id exists but the protocol you used has no channel, you get 503
no_channel_availableinstead — see the compatibility matrix
model_not_allowed#
The key's allowed_models allowlist doesn't include the requested model.
How to debug: edit the key at dianqi.zsopc.com/keys and add the model to the allowlist, or create a key with no model restriction. Note this is a 400 — don't branch on 403.
invalid_request#
Malformed request body (missing required field, JSON parse failure, wrong field type). Fix the body per the message.
tool_not_supported#
The request carries a tool (such as web_search or image_generation) but was routed to an upstream channel that doesn't support it.
How to debug: confirm the model supports that tool (compatibility matrix), or drop the tools field and retry.
401 Unauthorized#
invalid_api_key#
401 has only this one code — no key, wrong key, revoked key, expired key and disabled account all return it. Use the message to tell them apart:
| message | Meaning |
|---|---|
authentication failed: missing or malformed api key (...) | no key on the request, or the wrong format |
authentication failed: invalid api key | the key doesn't exist (truncated copy, or deleted) |
authentication failed: api key revoked | the key has been disabled |
authentication failed: api key expired | the key is past its expires_at |
authentication failed: account is not active | the account has been disabled |
OpenAI shape:
{"error": {"message": "authentication failed: invalid api key", "type": "authentication_error", "code": "invalid_api_key"}}
How to debug:
- Check you copied the whole key — the format is
sk-gpushare-plus 64 hex characters, 76 characters in total - Check the key still exists at dianqi.zsopc.com/keys. You can re-reveal the full value on its detail page (stored encrypted server-side), so just copy it again if unsure
- Check your auth method — any of the four fallbacks works:
x-api-key,x-goog-api-key,?key=query,Authorization: Bearer. See Authentication - Exception:
GET /v1/modelsaccepts only theAuthorization: Bearerandx-api-keyheaders — not?key=— so query auth returns 401 there
402 Payment Required#
quota_exceeded#
The account balance is exhausted. Billing uses one wallet: every API key shares the same account balance and no key has its own budget pool, so when the balance runs out every key fails at once and creating a new key brings no new credit.
OpenAI shape:
{"error": {"message": "Insufficient balance. Please top up and try again.", "type": "insufficient_quota", "code": "quota_exceeded"}}
Anthropic shape: "type": "billing_error". Gemini shape: "status": "RESOURCE_EXHAUSTED" with numeric code 402.
How to debug:
- Sign-up includes 121.32 of trial credit; once that's gone, top up at dflop.top/dashboard/billing (Stripe, 404.4 minimum, same SSO account and shared balance as dianqi.zsopc.com)
- Check your balance and usage in the console to see what's consuming it — chat and embeddings bill per token, images per image, video per second. See Image / video / music APIs
- Don't wait for the 402: monitor with
GET /v1/key/balance— it still returns 200 at a zero balance, and costs neither credit nor rate limit. For balances inside third-party clients and relay platforms, see New API and relay platforms
403 Forbidden#
permission_denied#
Pass-through only — the gateway never generates a 403 itself. A 403 means the upstream provider refused the call (content policy, regional restriction and so on) and the message is the upstream's own wording.
How to debug: switch model so the request routes to a different upstream. If it persists, report it to support@dflop.top.
429 Too Many Requests#
rate_limit_exceeded#
Two possible sources — tell them apart by the response headers:
| Source | Signature | What to do |
|---|---|---|
| Platform account limit | has Retry-After plus x-ratelimit-* headers | wait per Retry-After; for concurrency limits, reduce in-flight requests |
| Upstream channel limit | no x-ratelimit-* headers | exponential backoff (1s → 2s → 4s), or switch model to a different upstream |
Platform limits are counted per account (all your keys share the quota) across two dimensions: requests per minute (60-second sliding window) and maximum concurrency (in-flight requests). Every /v1/* request counts, including status polls for async tasks. The message states which dimension you hit, for example:
{"error":{"message":"requests per minute limit reached (120/min); retry after 3s","type":"rate_limit_error","code":"rate_limit_exceeded"}}
When a limit is configured, successful responses carry x-ratelimit-* headers too, so you can throttle adaptively. Your account's current limits are on the console API Keys page. See also API reference · rate limits.
500 Internal Server Error#
internal_error#
A gateway-side fault. Rare.
How to debug: retry once or twice. If it persists, report it to support@dflop.top with the timestamp, model id and the complete error body (plus the Cloudflare cf-ray response header if present). There is no request_id field in error responses — don't go looking for one.
502 Bad Gateway#
upstream_unreachable#
The upstream couldn't be connected to (DNS, TCP or TLS level). Only this case returns a fixed 502 — a 5xx returned by the upstream is passed through with its own status rather than rewritten to 502.
How to debug: retry (upstreams occasionally wobble), or switch model.
503 Service Unavailable#
no_channel_available#
The requested model has no usable upstream channel on the protocol endpoint you called — this covers all channels being disabled or unhealthy, and also the model simply not supporting that protocol (for example calling /v1/messages for a model that only has an OpenAI-protocol channel).
How to debug:
- Check the compatibility matrix to confirm the model × protocol combination is supported
- Switch model
- If it stays unavailable, report it to support@dflop.top. Health probing runs every 6 hours (plus one warm-up after a deploy), so "wait a few minutes and retry" rarely helps an unhealthy channel — switching model is faster
504 Gateway Timeout#
upstream_timeout#
The upstream took too long. The ceiling differs per endpoint:
| Endpoint | Upstream timeout |
|---|---|
the four chat protocol endpoints (/v1/chat/completions etc.) | 180s (total, streaming included) |
POST /v1/images/generations | 240s per attempt / 280s across the ladder (set client ≥300s) |
POST /v1/videos/generations (submit) | 60s (the task runs async; generation doesn't count against the request) |
POST /v1/embeddings | 30s |
How to debug:
- Lower
max_tokens, or split the work across several calls stream: truegets you the first token sooner, but the whole stream is still bound by the same 180s total — streaming is not unbounded- Set your SDK timeout to ≥ 200s to leave headroom
- Long jobs (video generation) use async submit plus polling and aren't affected by the per-request timeout — see Image / video / music APIs
Upstream error pass-through#
When an upstream returns a non-2xx, the gateway passes the status through unchanged (upstream 500 → response 500, upstream 429 → response 429) and unwraps the message into the upstream's own wording. In the OpenAI shape, type and code are mapped from the status:
| Upstream status | OpenAI code | OpenAI type |
|---|---|---|
| 400 / 422 | invalid_request | invalid_request_error |
| 401 | invalid_api_key | authentication_error |
| 403 | permission_denied | permission_error |
| 404 | not_found | invalid_request_error |
| 429 | rate_limit_exceeded | rate_limit_error |
| 5xx / other | upstream_error | api_error |
How to debug: retry once or twice (upstreams occasionally wobble), or switch model to route to a different channel.
Errors during streaming#
Once a streaming request has opened with HTTP 200, the status code can no longer change — errors can only show up in the stream itself:
- The stream ends early: when the upstream fails mid-way the gateway does not inject an error frame of its own; the stream simply ends. An OpenAI SSE stream won't get its
data: [DONE]terminator and the last chunk has nofinish_reason(an Anthropic stream is missingmessage_stop). - Upstream error frames pass through: if the upstream emits an in-protocol error event before cutting the stream (such as Anthropic's
event: error), the gateway forwards it verbatim. - Exception — the built-in tool path:
/v1/chat/completionsrequests carrying theweb_searchorimage_generationbuilt-in tools run over a dedicated channel, and there the gateway does synthesise an error chunk ({"error":{"message":…,"type":"api_error","code":"upstream_error"}}) followed bydata: [DONE]. See the streaming guide.
What the client should do:
- Don't rely on the connection closing — verify you received
data: [DONE]/finish_reason/message_stop, and treat their absence as an incomplete stream - Treat an incomplete stream as a failure and retry with backoff
How it's billed: whatever was transmitted before the interruption is settled on actual usage (when the upstream didn't get to send a usage chunk, output tokens are estimated from the characters already emitted). An interrupted turn is neither double-charged nor free.
Debugging decision tree#
request failed
├─ 400 → model spelling / key allowlist / request body / tool unsupported on this channel
├─ 401 → key problem → re-reveal and copy the key in the console (76 chars), check the auth header
├─ 402 → account balance exhausted → top up at dflop.top/dashboard/billing (a new key won't help)
├─ 403 → upstream refused (pass-through) → switch model
├─ 429 → has x-ratelimit-* headers = platform limit (wait per Retry-After / lower concurrency)
│ no such headers = upstream limit → back off and retry
├─ 503 → no channel on this protocol → check the compatibility matrix / switch model
├─ other 5xx → upstream or gateway issue → retry once or twice, switch model
├─ stream cut off (after a 200) → verify [DONE] / finish_reason → retry as a failure
└─ network level (no HTTP response at all) → check firewall / DNS / TLS reachability to dianqi.zsopc.com
Debugging tips#
Turn on your SDK's debug mode to see the full request and response:
# OpenAI Python SDK
import logging
logging.basicConfig(level=logging.DEBUG)
# Anthropic Python SDK
import os
os.environ["ANTHROPIC_LOG"] = "debug"
# full curl
curl -v -i https://dianqi.zsopc.com/v1/chat/completions ...
The X-Protocol-Translation response header records which cross-protocol translation path the request took — worth including when debugging protocol-related issues. There is no request_id in the error body or headers; when reporting an issue, include the timestamp, model id and complete error body (plus the Cloudflare cf-ray header).