# 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

来源：https://zhonkemodel.dflop.top/en/docs/reference/errors

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](./media-apis.md)):

```json
{"error": {"message": "...", "type": "...", "code": "..."}}
```

**Anthropic shape** (`/v1/messages`):

```json
{"type": "error", "error": {"type": "...", "message": "..."}}
```

**Gemini shape** (`/v1beta/models/{model}:generateContent`):

```json
{"error": {"code": 402, "message": "...", "status": "RESOURCE_EXHAUSTED"}}
```

> **Note**: the string `code` field (such as `quota_exceeded`) **only exists in the OpenAI shape**. Anthropic error bodies have just `type` and `message`; Gemini's `code` is the numeric HTTP status, with the enum semantics in `status`. 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](#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 numeric `code` (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**:
```json
{"error": {"message": "model `claude-3.5-opus` is not available", "type": "invalid_request_error", "code": "model_not_found"}}
```

**How to debug**:
1. Check the spelling — `claude-haiku-4-5-20251001` includes a date suffix that must be complete
2. Compare against the [full model list](./models.md). Placeholder SKUs returned by `GET /api/v1/models/public` with `callable=false` also produce this error
3. Note: when the model id exists but **the protocol you used** has no channel, you get 503 `no_channel_available` instead — see the [compatibility matrix](./compatibility-matrix.md)

### `model_not_allowed`

The key's `allowed_models` allowlist doesn't include the requested model.

**How to debug**: edit the key at [zhonkemodel.dflop.top/keys](https://zhonkezhonkemodel.dflop.top/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](./compatibility-matrix.md)), 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**:
```json
{"error": {"message": "authentication failed: invalid api key", "type": "authentication_error", "code": "invalid_api_key"}}
```

**How to debug**:
1. Check you copied the whole key — the format is `sk-gpushare-` plus 64 hex characters, **76 characters in total**
2. Check the key still exists at [zhonkemodel.dflop.top/keys](https://zhonkezhonkemodel.dflop.top/keys). You can re-reveal the full value on its detail page (stored encrypted server-side), so just copy it again if unsure
3. Check your auth method — any of the four fallbacks works: `x-api-key`, `x-goog-api-key`, `?key=` query, `Authorization: Bearer`. See [Authentication](./authentication.md)
4. Exception: `GET /v1/models` accepts only the `Authorization: Bearer` and `x-api-key` headers — **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**:
```json
{"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**:
1. 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 zhonkemodel.dflop.top)
2. 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](./media-apis.md)
3. **Don't wait for the 402**: monitor with [`GET /v1/key/balance`](./api-reference.md#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](../integrations/new-api.md)

## 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:

```json
{"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](https://zhonkezhonkemodel.dflop.top/dashboard/keys). See also [API reference · rate limits](./api-reference.md#速率限制).

## 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](#upstream-error-pass-through) 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**:
1. Check the [compatibility matrix](./compatibility-matrix.md) to confirm the model × protocol combination is supported
2. Switch model
3. 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` / `POST /v1/images/edits` | 240s per attempt / 280s across the ladder (set client ≥300s; `gpt-image-2` measures 30–215s). ⚠️ Synchronous turns past 90s are kept alive with `200` + chunked so the CDN can't cut them, which means **a turn that fails after the 90s mark returns `200` + `{"error":...}` instead of a truthful status code** — use `"async": true` if you need clean failure semantics |
| `POST /v1/videos/generations` (submit) | 60s (the task runs async; generation doesn't count against the request) |
| `POST /v1/embeddings` | 30s |

**How to debug**:
1. Lower `max_tokens`, or split the work across several calls
2. `stream: true` gets you the first token sooner, but **the whole stream is still bound by the same 180s total** — streaming is not unbounded
3. Set your SDK timeout to **≥ 200s** to leave headroom
4. Long jobs (video generation, async image generation) use async submit plus polling and aren't affected by the per-request timeout — see [Image / video / music APIs](./media-apis.md); a call your own client timed out on still settles to a terminal row (`interrupted` or `success`) on [logs.dflop.top](https://logs.dflop.top) — look it up by the `x-gateway-trace` header

## 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 no `finish_reason` (an Anthropic stream is missing `message_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/completions` requests carrying the `web_search` or `image_generation` built-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 by `data: [DONE]`. See the [streaming guide](../guides/streaming.md).

**What the client should do**:
1. Don't rely on the connection closing — verify you received `data: [DONE]` / `finish_reason` / `message_stop`, and treat their absence as an incomplete stream
2. 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 zhonkeapi.dflop.top
```

## Debugging tips

Turn on your SDK's debug mode to see the full request and response:

```python
# 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://zhonkezhonkeapi.dflop.top/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).
