A call to an AI API fails. The model is fine, the network is fine, but your request came back with a status you did not plan for. What your code does next decides whether a user sees a calm retry or a stack trace.
The good news is that an OpenAI-compatible API gives you everything you
need in two places: the HTTP status, and a machine-readable type inside the error body. Read both, and most of the
decision makes itself.
The error body has a type, so do not parse the message
Every error comes back in the same shape, with a type you
can branch on and a human message you should not:
{
"error": {
"code": null,
"message": "Insufficient quota. Please upgrade your plan.",
"param": null,
"type": "insufficient_quota"
}
} The message is for logs and for you. The type is for your code, because it does not change when the wording does.
Sort every failure into retryable or terminal
There are only two real buckets, and the status tells you which one you are in.
Terminal. Retrying will not help, so fix the cause.
400 invalid_request_error: the request itself is wrong. A malformed body or an invalid parameter. Retrying the identical request just burns quota, so surface a bug.401 invalid_request_error: the API key is missing or invalid. Check theAuthorizationheader and the key itself.402 insufficient_quota: the account is out of quota or has an unpaid overage. This is a billing state, not a transient one, so tell the operator rather than the retry loop.
Retryable. The same request can succeed later.
429 rate_limit_exceeded: you are sending faster than the plan allows. Wait, then retry.500 api_error: something failed on the provider side. Back off, then retry.
Anything else, including 5xx beyond 500 and a
dropped connection, is also worth one bounded retry, because the
failure is outside your request.
Retry only the retryable ones, with a ceiling
A retry loop without a ceiling is an outage you wrote yourself. Cap the attempts, back off between them, and add jitter so a fleet of clients does not all return at the same instant.
import time, random, httpx
from openai import OpenAI
client = OpenAI(base_url="https://webwright.ai/v1", api_key="www_v1_...")
RETRYABLE = {"rate_limit_exceeded", "api_error"}
def chat_with_retry(messages, max_attempts=4):
for attempt in range(max_attempts):
try:
return client.chat.completions.create(
model="wren-r1", messages=messages
)
except Exception as err:
status = getattr(err, "status_code", None)
etype = getattr(err, "type", None)
retryable = status in (429, 500) or etype in RETRYABLE
if not retryable or attempt == max_attempts - 1:
raise
retry_after = getattr(err, "response", None)
wait = 2 ** attempt + random.uniform(0, 0.5)
if retry_after is not None:
header = retry_after.headers.get("retry-after")
if header:
wait = max(wait, float(header))
time.sleep(wait) Two details matter. On a 429, honor the Retry-After header instead of guessing: it is the server
telling you how long to wait. And keep the ceiling small. Four attempts
with backoff is plenty; past that, the failure is not transient.
Make the retry safe before you make it fast
A retry that creates a second job, a second charge, or a second row is worse than the original failure. Before you retry a write, give the work an idempotency key and have your own downstream logic treat two calls with the same key as one.
import uuid
def transcribe(path):
key = str(uuid.uuid4()) # stable per unit of work, not per attempt
with open(path, "rb") as audio:
return client.audio.transcriptions.create(
model="whisper-large-v3-turbo",
file=audio,
extra_headers={"Idempotency-Key": key},
) Keep the key stable for the unit of work and reuse it across attempts, so a retry after a timeout resumes the same job instead of starting a new one.
Tell the user the truth, not the stack trace
A user-facing message should match the real cause, because the fix differs by cause.
- Out of quota (
402): say the account needs attention, and who can fix it. Do not invite a retry. - Bad key (
401): say the connection is not authorized. That is an operator fix, not a user one. - Rate limited (
429) or provider error (500): say you are retrying and will fall back if it keeps failing. Then actually do both.
Log the type, the status, and a request id. Never log the
API key, and never paste the raw error body into a user-facing screen.
One path, both providers
Because the schema is the OpenAI error schema, the same classifier
works when you add a second OpenAI-compatible provider behind the same
client. You branch on type and status, not on a provider
name, so a fallback is a base-URL change and nothing else.
Handle the failure once, in one place, and every call in your app inherits it.
Building with an AI API and deciding how it should fail? Tell us what you are shipping and we will walk the failure path with you. Book a consult using the link below.
The WebWright Team