1. Error format
Every NormaHub error is an HTTP status plus a body with a machine-readable code. The status names the class (auth, billing, limits, availability); error.code names the exact cause. Branch on the status+code pair, not on the message text: messages may change, codes are stable. Russian version: Ошибки AI API.
{
"error": {
"message": "Human-readable message",
"type": "invalid_request_error",
"code": "invalid_request"
}
}Save the X-Request-Id header of every request — it links your client call to the server-side record and is found without your key or prompt.
2. Code table
Permanent errors are never fixed by retrying: fix the request, key, balance, permissions, or size. A fast key check is one curl to /v1/models: it separates a dead key from live funds in seconds.
400invalid_request- Check the JSON and required route fields.
401invalid_api_key- Key missing, invalid, or revoked.
402insufficient_balance- Top up the API key owner's balance.
403model_not_allowed- Model or endpoint unavailable to this key.
413request_too_large- Shrink the payload or the passed context.
429budget_exceeded- API key limit reached.
503provider_unavailable- Model or upstream provider temporarily unavailable.
# 401 or 402? Check with curl in 10 seconds:
curl -s -o /dev/null -w "%{http_code}
" \
https://api.normahub.cc/v1/models \
-H "Authorization: Bearer $NORMAHUB_API_KEY"
# 401 — key invalid or revoked, issue a new one
# 200 — key alive, check balance and key limits3. Retry policy
Retry only temporary 429 and 5xx — with exponential delay, jitter against retry storms, and a hard operation deadline. Honor Retry-After when the server sends it. Interactive UIs are often better off showing a clear failure with a retry button than holding the user in an invisible queue.
import os
import random
import time
from openai import OpenAI, APIError
client = OpenAI(
api_key=os.environ["NORMAHUB_API_KEY"],
base_url="https://api.normahub.cc/v1",
)
RETRIABLE = {429, 500, 502, 503}
def chat(messages, tries=4, deadline=60):
started = time.monotonic()
for attempt in range(tries):
try:
return client.chat.completions.create(
model="MODEL_ID",
messages=messages,
timeout=20,
)
except APIError as e:
retryable = (e.status_code in RETRIABLE
and time.monotonic() - started < deadline)
if not retryable or attempt == tries - 1:
raise
delay = min(2 ** attempt + random.uniform(0, 1), 15)
time.sleep(delay)
raise RuntimeError("unreachable")Never auto-retry 400, 401, 402, 403 or 413: a queue of such retries creates load without approaching success. A separate risk is recovery after an incident: retries returning without an upper bound start a second load wave. SDK examples live in the Python guide.
4. Diagnostics and limits
Minimum investigation set: time, endpoint, model ID, HTTP status, error.code, X-Request-Id. Never write the full key, prompts or answers into plain application logs — also check CI artifacts, docker history and HTTP client exception texts, where secrets leak most often.
Limits come on two levels: the owner balance and the key spend limit. After recovery check both, otherwise «funds exist but requests rejected». See balance, limits and history in the dashboard, and price every retry with the token cost logic.