Read the provider's status, error code and request ID before anything else
For every failed provider call, log the HTTP status, error.type, the code field and the request ID. The code field is error.code at OpenAI and error.details.error_code on Anthropic's spend-cap response. That entry tells you whether to wait, add credit or change the request.
const res = await fetch(url, init);
if (!res.ok) {
const body = await res.clone().json().catch(() => ({}));
console.error({
status: res.status,
type: body?.error?.type,
code: body?.error?.code ?? body?.error?.details?.error_code,
requestId: res.headers.get("request-id"), // Anthropic's header name
});
}
Retry only what can recover, inside a fixed time budget
Retry 429, 500, 503, 504 and 529 a small number of times. Treat Retry-After as a floor (it is either a number of seconds or an HTTP date), add jitter, and stop when the total wait would pass your budget. Keep that budget below your host's function time limit and your UI timeout. If you use a provider SDK, set its own retries to zero or count them, so the loops do not nest.
const RETRYABLE = new Set([429, 500, 503, 504, 529]);
async function callWithBudget(fn: () => Promise<Response>, budgetMs = 8000) {
const start = Date.now();
for (let attempt = 0; ; attempt++) {
const res = await fn();
if (res.ok || !RETRYABLE.has(res.status)) return res;
const hint = Number(res.headers.get("retry-after")); // NaN for an HTTP date
const wait = Math.max(Number.isFinite(hint) ? hint * 1000 : 0, 2 ** attempt * 500)
+ Math.random() * 250;
if (attempt >= 2 || Date.now() - start + wait > budgetMs) return res;
await new Promise((r) => setTimeout(r, wait));
}
}
Keep the provider's status through error wrapping and tell the user what to do
Carry the status and code in your own error type, and map them to a plain message with a next step. A spend-cap or balance error is yours to fix, not the user's, so the message can say 'this feature is unavailable, and we have been notified'. A 429 with Retry-After can say 'try again in a minute'. Do not show provider text to users.
class ProviderError extends Error {
constructor(
readonly status: number,
readonly code: string | undefined,
readonly requestId: string | null,
message: string,
) {
super(message);
}
}
Treat a stream that ends without its final event as failed
A 200 does not mean the answer completed. With Anthropic's streaming API, a finished message ends with a message_stop event, so a stream that closes without one has failed. Once output has reached the user, do not replay the request automatically, as OpenAI also advises. Show the partial text with a retry button.
let complete = false;
for await (const ev of events) { // your parsed SSE events
if (ev.type === "message_stop") complete = true;
}
if (!complete) throw new Error("stream ended before message_stop");
Set limits and alerts that reach a person, and test the channel
Set a spend limit at the provider below your tier cap. OpenAI's spend alerts only notify and traffic continues, while a hard limit makes requests return 429, so you need both. On Vercel, Spend Management notifies at 50%, 75% and 100% of the amount and can call a webhook. Check whether Pause Production Deployments is on, because it takes the production site offline. Send alerts and billing email to an address more than one person reads, and run a scheduled canary that makes one cheap real provider call.
Keep model names in config, with a fallback and an audit
Do not scatter model IDs through the code. Read them from one config, name a fallback that is also active, and check the provider's deprecations page on a schedule. On Anthropic, Usage in the Console can export a CSV of usage by API key and model, which finds code still calling a deprecated model.
{
"chat": { "model": "claude-sonnet-5-5", "fallback": "claude-sonnet-5" }
}
For an empty 403 or a 503 from the host, look at the host first
A 403 from the Anthropic API is a JSON error with type permission_error, so a 403 without that body was more likely produced by something in front of your call. On Vercel, check the project's Firewall tab. Pro and Enterprise teams can add a System Bypass Rule for the caller's IP or CIDR range, or pause system mitigations for 24 hours during a critical event. For a deployment-paused 503, fix the billing cause, then resume each project.