Error Handling
A request that fails returns an HTTP status code and a JSON body with an error type and a message. This page lists every status, shows the error body of each API format and says which errors are worth sending again.
The SDKs raise an error object that carries the status code and the fields of the body. This request names a model that does not exist:
import openai
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.shannon-ai.com/v1",
)
try:
client.chat.completions.create(
model="no-such-model",
messages=[{"role": "user", "content": "Hello"}],
)
except openai.APIStatusError as error:
print(error.status_code) # 400
print(error.type) # invalid_request_error
print(error.body["message"]) # unknown model: no-such-model
print(error.response.headers.get("x-request-id")) # the id of this request import OpenAI from "openai";
const client = new OpenAI({
apiKey: "YOUR_API_KEY",
baseURL: "https://api.shannon-ai.com/v1",
});
try {
await client.chat.completions.create({
model: "no-such-model",
messages: [{ role: "user", content: "Hello" }],
});
} catch (error) {
if (!(error instanceof OpenAI.APIError)) throw error;
console.log(error.status); // 400
console.log(error.type); // invalid_request_error
console.log(error.error.message); // unknown model: no-such-model
} curl -i https://api.shannon-ai.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "no-such-model",
"messages": [{"role": "user", "content": "Hello"}]
}' It returns status 400 and this body:
{
"error": {
"type": "invalid_request_error",
"message": "unknown model: no-such-model"
}
} Error body
There are two bodies. /v1/messages answers in the Anthropic format; every other endpoint answers in the OpenAI format. In both, the details are in error.type and error.message, so one reader serves all endpoints.
{
"error": {
"type": "authentication_error",
"message": "Invalid API key",
"code": "invalid_api_key",
"param": null
}
} {
"type": "error",
"error": {
"type": "authentication_error",
"message": "Invalid API key"
}
} | Field | Type | Description |
|---|---|---|
error.type | string | The kind of error. See Error types below. |
error.message | string | A sentence for a person. The texts in the tables below are stable up to their variable parts, shown as <id>, <n> or …. |
error.code | string | null | Present on some errors of the OpenAI format: invalid_api_key with 401, request_too_large with 413, context_length_exceeded when a conversation is too long, otherwise null or absent. Decide on the status and error.type first and treat code as an extra. |
error.param | null | Present on the same errors as code. Always null. |
type | string | Anthropic format only. Always error. |
Error types
| Type | Statuses | Description |
|---|---|---|
invalid_request_error | 400, 413, 415, 422 | The request itself is wrong: a field, the body, the model id or an input the model does not take. Change the request before you send it again. |
authentication_error | 401 | The API key is missing, unknown or revoked. |
not_found_error | 404 | The path does not exist. |
rate_limit_error | 429 | The balance does not cover the request, flood protection stopped it, or the model is busy. The message says which. |
api_error | 500, 502, 503, 504 | The request was fine and the failure is on our side. Send it again. |
server_error | 500 | The request was fine and the failure is on our side. Send it again. |
Status codes
| Status | Type | Message | When |
|---|---|---|---|
400 | invalid_request_error | Failed to parse the request body as JSON: … | The body is not valid JSON. |
400 | invalid_request_error | unknown model: <id> | model is not a published id. On /v1/responses also when model is missing. |
400 | invalid_request_error | No user message provided | Shannon tiers: the request holds no user text. |
400 | invalid_request_error | <id> does not accept image input | An image was sent to a hosted open-weight model without image input. |
400 | invalid_request_error | audio and video input are not supported by any hosted model | An audio or video part was sent to a hosted open-weight model. |
400 | invalid_request_error | <id> does not accept response_format | Structured output was asked from a hosted open-weight model without it. |
400 | invalid_request_error | <id> does not accept response_format json_schema; use json_object | A JSON schema was sent to a hosted open-weight model that takes json_object only. |
400 | invalid_request_error | unknown reasoning effort '<value>'; expected off, low, medium or high | The reasoning effort is not one of the listed values. Hosted open-weight models. |
400 | invalid_request_error | count_tokens is available for the hosted open models; unknown model: <id> | Token counting was asked for a model outside the hosted open-weight models. |
401 | authentication_error | Missing authentication | No API key was sent. |
401 | authentication_error | Invalid API key | The key is unknown, was replaced by a newer one, or was revoked. |
404 | not_found_error | Not Found | The path does not exist, for example /v1/embeddings. |
405 | api_error | Method Not Allowed | The path exists but not with this HTTP method. |
413 | invalid_request_error | Failed to buffer the request body: … | The request body is larger than 32 MiB. |
415 | invalid_request_error | Expected request with `Content-Type: application/json` | The Content-Type header is missing or is not application/json. |
422 | invalid_request_error | Failed to deserialize the JSON body into the target type: … | A required field is missing, such as messages, or a field has the wrong JSON type. |
429 | rate_limit_error | Quota exceeded. Upgrade your plan at shannon-ai.com/plan | The request's output limit is larger than what is left of your balance. |
429 | rate_limit_error | Too many requests. Retry in <n>s. | Flood protection: more than 120 requests in one minute on your account. <n> is the number of seconds until the minute ends. |
429 | rate_limit_error | Shannon Coder call quota reached. Upgrade your plan at shannon-ai.com/plan | shannon-coder-1 on /v1/chat/completions and /v1/messages: your plan's Shannon Coder calls for the current 4-hour window are used. |
429 | rate_limit_error | Shannon routes are temporarily busy. Please retry. | The model is busy at this moment. |
500 | server_error | The model backend failed to answer. Please retry. | The model did not produce a reply. |
500 | server_error | An internal error occurred. Please retry. | An internal failure while the request was prepared. |
502 | api_error | The model backend failed to answer. Please retry. | The model did not produce a reply. Shannon 3 family and hosted open-weight models. |
503 | api_error | Could not verify your quota right now. Please retry. | Your balance could not be read at this moment. |
503 | api_error | token counting is temporarily unavailable for this model | Token counting is not available for this model at this moment. |
504 | api_error | upstream timed out | A hosted open-weight model took longer than the time limit of a request that is not streamed. |
Checks run in this order: API key, request body, model id, flood protection, balance. A request with a wrong key and a wrong body therefore returns 401. A request that is refused with a 4xx status is not charged.
Balance and flood protection
Every request is served equally: there are no rate tiers and no separate API quota. Two things can still answer 429, both with the type rate_limit_error. The message tells them apart.
Balance
Your balance is today's plan allowance, which resets at 00:00 UTC, and after it your purchased credit. Chat and the API draw from the same balance.
Each request first sets aside its output limit: max_tokens, or max_output_tokens on /v1/responses, 4,096 when you send neither. When the reply is complete, the amount set aside is replaced by what the request used. If the amount does not fit into the balance that is left, the request returns Quota exceeded before the model starts.
Requests that run at the same time each set aside their own amount. Many parallel requests with a high max_tokens can therefore use up the balance for a moment, even when each reply is short. A lower max_tokens sets aside less.
Flood protection
Flood protection allows 120 requests per minute per account. It counts the account, not the key, over /v1/chat/completions, /v1/responses and /v1/messages together. The minute starts with your first request.
Request number 121 inside that minute returns Too many requests. Retry in <n>s. and costs nothing. Refused requests count as well, so a client that sends again without waiting stays refused until the minute is over. Wait the <n> seconds from the message.
Requests sent in parallel are not refused for being parallel. They wait in line and are answered in turn.
GET /v1/models and the token counting endpoints are not counted.
More on limits and balance, and where to see your own balance and usage: Limits and balance Keys & usage
What to retry
Send a request again only when the same request can succeed. The OpenAI and Anthropic SDKs already retry 429 and 5xx replies twice on their own; set max_retries (maxRetries in JavaScript) to change that.
| Error | Send again? | How |
|---|---|---|
400, 404, 405, 413, 415, 422 | No. Correct the request first. | The message names the field, the model or the input that is wrong. |
401 | No. Use a valid key. | Create or replace the key on the Keys & usage page. |
429 Too many requests | Yes, after the wait. | Wait the number of seconds in the message, then send the same request. |
429 Quota exceeded | Not as it is. | Lower max_tokens, wait for the daily reset at 00:00 UTC, or add credit. |
429 Shannon Coder call quota reached | Yes, in the next window. | The windows start every 4 hours from 00:00 UTC. Another model answers at once. |
429 … temporarily busy | Yes. | Wait a few seconds, then longer on each further attempt. |
500, 502, 503 | Yes. | Wait 1 second, then 2, 4 and 8. Stop after a few attempts and report the request id. |
504 | Yes. | Send it with stream set to true: the time limit applies to replies that are not streamed. |
A retry loop that follows this table:
import re
import time
import openai
from openai import OpenAI
# max_retries=0: this loop does the retrying, the SDK does not add its own.
client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.shannon-ai.com/v1", max_retries=0)
def create_with_retry(attempts=5, **request):
for attempt in range(attempts):
try:
return client.chat.completions.create(**request)
except openai.RateLimitError as error:
text = str(error)
flood = re.search(r"Retry in (\d+)s", text)
if flood:
time.sleep(int(flood.group(1))) # flood protection: wait out the minute
elif "temporarily busy" in text:
time.sleep(2 ** attempt)
else:
raise # balance: a wait of seconds does not help
except (openai.InternalServerError, openai.APIConnectionError):
time.sleep(2 ** attempt) # 1, 2, 4, 8 seconds
raise RuntimeError("the request failed after all attempts")
response = create_with_retry(
model="shannon-3",
messages=[{"role": "user", "content": "Hello"}],
)
print(response.choices[0].message.content) import OpenAI from "openai";
// maxRetries: 0 — this loop does the retrying, the SDK does not add its own.
const client = new OpenAI({ apiKey: "YOUR_API_KEY", baseURL: "https://api.shannon-ai.com/v1", maxRetries: 0 });
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
async function createWithRetry(request, attempts = 5) {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await client.chat.completions.create(request);
} catch (error) {
const text = String(error.message);
const flood = text.match(/Retry in (\d+)s/);
if (error.status === 429 && flood) {
await sleep(Number(flood[1])); // flood protection: wait out the minute
} else if (error.status === 429 && text.includes("temporarily busy")) {
await sleep(2 ** attempt);
} else if (error.status >= 500 || error instanceof OpenAI.APIConnectionError) {
await sleep(2 ** attempt); // 1, 2, 4, 8 seconds
} else {
throw error; // wrong request, wrong key or balance
}
}
}
throw new Error("the request failed after all attempts");
}
const response = await createWithRetry({
model: "shannon-3",
messages: [{ role: "user", content: "Hello" }],
});
console.log(response.choices[0].message.content); Errors inside a stream
A stream has status 200 from its first byte. A failure after that arrives as an event inside the stream: a chunk with an error member on /v1/chat/completions, an error event on /v1/messages, a response.failed or error event on /v1/responses. The closing events of the stream still follow. Treat such a reply as failed and send the request again, as for a 5xx status.
A conversation that is longer than the model's context window is reported this way, with the type invalid_request_error and the code context_length_exceeded. Shorten the conversation before you send it again.
The exact frames are described with streaming. Streaming
Request ids
Every reply, errors included, has an x-request-id header. It is the value you sent in your own x-request-id header, or 12 hexadecimal characters when you sent none. Log it, and quote it when you ask for help with a request.