API
Errors
One JSON shape, OpenAI-style types, and clear rules about what is charged.
Error shape
{
"error": {
"message": "Insufficient credits: this request needs up to 0.0463 credits",
"type": "insufficient_quota",
"code": null
}
}message is human-readable and may change; branch on the HTTP status and type. code is reserved and currently always null.
Status codes
| Status | type | Cause | Billed |
|---|---|---|---|
| 400 | invalid_request_error | Body isn't JSON, messages is missing or empty, or reasoning_effort / reasoning has an invalid value. | No |
| 401 | authentication_error | Missing, malformed, unknown or revoked API key. | No |
| 402 | insufficient_quota | Balance can't cover the reservation. Top up at Credits. | No |
| 403 | permission_error | Account suspended. | No |
| 404 | invalid_request_error | Unknown or coming-soon model id. | No |
| 404 | not_found | No such route. | No |
| 413 | invalid_request_error | Request body over 1,000,000 bytes. | No |
| 429 | rate_limit_error | Per-key limit exceeded. Comes with retry-after: 10. | No |
| 4xx / 5xx | (upstream) | Model server rejected the request; body passed through as-is. | No, refunded |
| 502 | server_error | Model server unreachable. | No, refunded |
| 500 | server_error | Unexpected failure inside the gateway. | See note |
A 500 after the reservation step is settled by the usual refund path when the failure is upstream. If you see a charge for a failed request in your logs, send us the usage id.
Upstream errors
When vLLM returns a non-2xx, ThaiRouter refunds the reservation, logs the request with status error, and returns the upstream body and status verbatim. Typical causes: a parameter out of range, or a prompt longer than the context window. vLLM's error shape is also OpenAI-style, so the same parsing works.
Retrying
- 429: wait for
retry-afterseconds, then retry. The limit is a 60-second sliding window per key. - 502: retry with exponential backoff (start around 1 s, cap at ~30 s). Nothing was charged.
- 402: don't retry automatically; top up first.
- 4xx other than the above: fix the request. Retrying the same body will fail the same way.
- Requests are not idempotent. A timeout on a non-streaming call may have completed upstream and been billed; check the ledger before re-sending large jobs.
In the SDKs
Python
from openai import APIStatusError, RateLimitError
try:
r = client.chat.completions.create(...)
except RateLimitError as e:
time.sleep(int(e.response.headers.get("retry-after", "10")))
except APIStatusError as e:
if e.status_code == 402:
raise SystemExit("top up credits")
print(e.status_code, e.body["error"]["type"], e.body["error"]["message"])TypeScript
import OpenAI from "openai";
try {
const r = await client.chat.completions.create({ ... });
} catch (e) {
if (e instanceof OpenAI.APIError) {
console.error(e.status, e.error?.type, e.message);
} else throw e;
}