Get started
Errors and retries
Check the HTTP status first. 429, 500, 502 and 503 can be retried with backoff; 400, 401, 403, 404 and 413 are problems with the request or account that retrying will not fix. An empty balance returns 403, not OpenAI's 429.
When a request fails you get an HTTP error status and a JSON error body. The status alone tells you whether to retry: 429, 500, 502 and 503 can be retried after a pause; 400, 401, 403, 404 and 413 come from the request itself or your account settings, and sending the same request again will not succeed.
Error body
{
"error": {
"code": "",
"message": "无效的令牌 (request id: 2026111202521796606900082...)",
"type": "new_api_error"
}
}| Field | Description |
|---|---|
message | Human-readable description, ending with a request id. It may be in Chinese |
type | Error category |
code | Error code such as insufficient_user_quota or model_not_found; often an empty string |
Because code is often empty, branch on the HTTP status in your code and treat code as extra detail. Errors raised while relaying a Claude Messages request (/v1/messages) use Anthropic's format, {"type": "error", "error": {...}}; checks that run before the request is relayed, such as authentication and balance, use the format above on every endpoint.
When you contact support, include the request id from message (WeChat JsnonoChat) so we can find that exact request. Log full error responses on your side.
Status codes
| Status | Meaning | Common causes | Retry |
|---|---|---|---|
| 400 | Bad request | Malformed JSON, a missing required parameter, an out-of-range value, a parameter the model does not support, input longer than the model's context | No — fix the request |
| 401 | Authentication failed | Wrong, deleted, disabled or expired key, or the key has used up its spending limit | No |
| 403 | Forbidden | Account balance ran out (insufficient_user_quota), IP not on the key's allowlist (access_denied), model not on the key's model allowlist, the key's group is unavailable, account disabled | No |
| 404 | Not found | Wrong path (for example missing /v1), resource does not exist | No |
| 413 | Payload too large | Images, audio or text in one request are too large | No — compress or split first |
| 429 | Too many requests | A rate limit on this site or at the model service | Yes, with backoff |
| 500 | Internal error | The server hit an error processing the request | A few times, with backoff |
| 502 | Bad gateway | The model service returned an invalid response or the connection dropped | Yes, with backoff |
| 503 | Unavailable | The model service is busy, or the key's group has no available channel for the model (model_not_found) | Yes, with backoff; with model_not_found, switch groups |
An empty balance returns 403 with the code insufficient_user_quota here. OpenAI's own API returns 429 for exhausted quota. If your code follows OpenAI's logic, handle 403 separately: alert someone to add credit or switch accounts, and keep it out of your retry queue.
503 with model_not_found
The key's group has no available line for this model — usually the model name is misspelled or the group does not offer that model. In order:
- Check the model name; it must match the model hub exactly.
- Call the models endpoint with the same key and see whether the model is listed.
- If it is not, check which groups offer the model on its page and use a key in one of them — see Choosing a group.
Retry strategy
For retryable errors (429, 500, 502, 503) use exponential backoff with random jitter: wait about 1 second the first time, double it each time up to a cap, and stop after 3 to 5 retries. Jitter keeps many clients from retrying at the same instant.
The official OpenAI SDKs retry 429s, 5xx responses and connection errors automatically with backoff; you can set the retry count and timeout:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://open.tutujin.com/v1",
api_key=os.environ["TUTUJIN_API_KEY"],
max_retries=4, # default 2
timeout=60.0, # per-request timeout, seconds
)
# override for a single request
resp = client.with_options(max_retries=6, timeout=120.0).chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Hello"}],
)import OpenAI from 'openai'
const client = new OpenAI({
baseURL: 'https://open.tutujin.com/v1',
apiKey: process.env.TUTUJIN_API_KEY,
maxRetries: 4, // default 2
timeout: 60_000, // per-request timeout, ms
})
// override for a single request
const resp = await client.chat.completions.create(
{
model: 'gpt-5.4-mini',
messages: [{ role: 'user', content: 'Hello' }],
},
{ maxRetries: 6, timeout: 120_000 },
)Without an SDK, plain HTTP with retries looks like this:
import os
import random
import time
import requests
RETRYABLE = {429, 500, 502, 503}
def post_with_retry(path, payload, max_attempts=5):
url = "https://open.tutujin.com/v1" + path
headers = {"Authorization": f"Bearer {os.environ['TUTUJIN_API_KEY']}"}
for attempt in range(max_attempts):
r = requests.post(url, json=payload, headers=headers, timeout=60)
if r.status_code not in RETRYABLE or attempt == max_attempts - 1:
r.raise_for_status() # 400/401/403/404/413 raise immediately
return r.json()
retry_after = r.headers.get("Retry-After", "")
delay = float(retry_after) if retry_after.isdigit() else min(30, 2**attempt)
time.sleep(delay + random.uniform(0, 1))const RETRYABLE = new Set([429, 500, 502, 503])
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function postWithRetry(path, payload, maxAttempts = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(`https://open.tutujin.com/v1${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TUTUJIN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(60_000),
})
if (res.ok) return res.json()
if (!RETRYABLE.has(res.status) || attempt === maxAttempts - 1) {
throw new Error(`${res.status} ${await res.text()}`) // not retryable, or out of attempts
}
const retryAfter = Number(res.headers.get('retry-after'))
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 1000 * 2 ** attempt)
await sleep(delay + Math.random() * 1000)
}
}Errors during streaming
If a streaming request fails before any data is sent, you get an error status like any other request. Once data has started flowing the status is already 200, so a failure shows up as the stream ending early without the final data: [DONE]. Treat "closed before [DONE]" as a failure, and discard or flag the partial output as your product requires.
Checklist
- 401: print the header to make sure the key is complete with no stray spaces or line breaks; in the console check that the key is not expired, disabled or out of its spending limit.
- 403: read
messageto tell balance, IP allowlist, model allowlist and group problems apart, then fix the matching setting. - 404: make sure the URL is a full path starting with
https://open.tutujin.com/v1. - 429: lower concurrency or spread requests over several keys — see Rate limits.
- Persistent 5xx: note the request id and contact support.
Still stuck? Add our WeChat support JsnonoChat and send the request id from the error message, and we will look into it.