Operating

Errors

Errors come back as JSON with an error object, in the shape the OpenAI SDKs expect. The HTTP status tells you what class of problem it is; the code tells you which one.

The envelope
{
"error": {
"message": "You are out of credits.",
"type": "billing_error",
"code": "insufficient_credits",
"param": null,
"request_id": "req_9a2f..."
}
}

Only message and type are guaranteed

Most endpoints fill in code, param and request_id, but not all of them do. Branch on the HTTP status first and on code second, with a fallback — do not assume code is always present.

Status codes#

StatusMeansRetry?
400Your request is malformed — a bad parameter, an unsupported field, or media the model cannot take.No. Fix the request.
401Missing or invalid credentials.No.
402Out of credits, no active plan, or a daily spend cap was hit.No. Add credits.
403Authenticated, but not allowed — scope, plan, or a blocked account.No.
404Unknown model or resource. Also returned by speech-to-text when that feature is turned off.No.
409Video content requested before it finished.Yes, after polling.
413Upload over 100 MB.No.
429Rate limited.Yes — honour Retry-After.
500Something broke on our side. The message is always generic.Yes, with backoff.
503Every upstream for that model was unavailable.Yes, shortly.

Error types#

typeWhen
invalid_request_errorValidation failed (400).
auth_errorCredentials or permissions (401, 403).
billing_errorCredits or plan (402).
rate_limit_errorToo many requests (429).
not_foundUnknown model, file, task or route (404).
server_errorOurs (500, 503).

The codes you will actually meet#

Bad requests

  • unknown_model — the id is not one of the three. OpenAI model names land here.
  • media_not_supported — you attached an image to Bala, or video to anything but Rishi.
  • model_generation_unsupported — you asked a model to generate media it cannot.
  • context_window_exceeded — your messages are longer than the model’s window.
  • conflicting_parameters — you sent both max_tokens and max_completion_tokens. Send one.
  • invalid_tool_arguments — a tool call in your message history has arguments that are not valid JSON.
  • unsupported_parameter — a field we deliberately reject; the message names the replacement.

Billing

A 402 carries extra fields so you can render a useful prompt rather than a dead end.

402 with an action
{
"error": {
"message": "You are out of credits.",
"type": "billing_error",
"code": "insufficient_credits",
"balance": 0,
"action": "add_credits",
"action_label": "Add credits",
"action_url": "https://upfyn.com/dashboard/ai/billing"
}
}
  • insufficient_credits — balance is exhausted.
  • no_active_plan — the account has no plan.
  • daily_spend_limit_exceeded — a cap you set was reached; it clears at the next window.

Rate limits

  • user_rate_limit_exceeded — the account’s per-minute request limit.
  • key_rate_limit_exceeded — that one key’s limit.
  • temporary_auto_block — repeated 429s triggered a short cool-off. Back off properly rather than retrying harder.

Rate limits has the numbers and the headers.

Errors during a stream#

Once a stream has started, the response is already committed to 200 — the status cannot change. A failure therefore arrives as a frame in the stream, followed by the usual terminator.

A stream that failed halfway
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"The answer is"}}]}
 
data: {"error":{"message":"Upstream failed.","type":"server_error","code":"upstream_stream_error"}}
 
data: [DONE]

Check every frame for an error key

A streaming client that only reads choices[0].delta will treat a failed turn as a short one and show the user a truncated answer with no indication anything went wrong.

Retrying well#

Retry 429, 500 and 503. Never retry 400, 401, 402 or 403 — they will fail identically and a retry loop on a 402 is just noise. Honour Retry-After when it is present.

Backoff that respects Retry-After
import time, random
from openai import OpenAI, APIStatusError
 
client = OpenAI(base_url="https://ai.upfyn.com/v1", api_key=KEY)
 
RETRYABLE = {429, 500, 502, 503, 504}
 
def complete(**kwargs):
for attempt in range(5):
try:
return client.chat.completions.create(**kwargs)
except APIStatusError as err:
if err.status_code not in RETRYABLE:
raise # 400/401/402/403 will never succeed on retry
wait = float(err.response.headers.get("Retry-After") or 0)
if not wait:
wait = (2 ** attempt) + random.random() # backoff with jitter
time.sleep(wait)
raise RuntimeError("gave up after 5 attempts")

Idempotency

Send an Idempotency-Key header on a request you might retry, and a repeat of the same key will not be billed twice.