Skip to content
TutujinAPI

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

JSON
{
  "error": {
    "code": "",
    "message": "无效的令牌 (request id: 2026111202521796606900082...)",
    "type": "new_api_error"
  }
}
FieldDescription
messageHuman-readable description, ending with a request id. It may be in Chinese
typeError category
codeError 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

StatusMeaningCommon causesRetry
400Bad requestMalformed JSON, a missing required parameter, an out-of-range value, a parameter the model does not support, input longer than the model's contextNo — fix the request
401Authentication failedWrong, deleted, disabled or expired key, or the key has used up its spending limitNo
403ForbiddenAccount 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 disabledNo
404Not foundWrong path (for example missing /v1), resource does not existNo
413Payload too largeImages, audio or text in one request are too largeNo — compress or split first
429Too many requestsA rate limit on this site or at the model serviceYes, with backoff
500Internal errorThe server hit an error processing the requestA few times, with backoff
502Bad gatewayThe model service returned an invalid response or the connection droppedYes, with backoff
503UnavailableThe 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:

  1. Check the model name; it must match the model hub exactly.
  2. Call the models endpoint with the same key and see whether the model is listed.
  3. 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://www.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"}],
)

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://www.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))

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 message to 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://www.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.