Mapping HTTP Status Codes to Error Meaning Across Providers
9 min read · updated August 11, 2026
A handler tuned to one model API’s conventions misclassifies another’s, and it does so silently, because both APIs are returning perfectly valid HTTP. The status code is a coarse envelope; the cause lives one level down, in a field whose name differs between providers.
Why the codes are overloaded
HTTP has about a dozen client-error codes in general use, and a model API has far more than a dozen distinct ways to reject you: an unknown model id, a prompt over the context window, an output cap larger than the model allows, a malformed tool schema, an image over the size limit, a parameter that this model does not accept, a policy block, an exhausted quota, a saturated tokens-per-minute bucket, a region restriction. There is no natural mapping from that set onto the HTTP vocabulary, so every provider invents its own compression — and the compressions differ.
This is why the honest framing is not “these providers disagree about what 400 means”. It is that 400 was never going to be sufficient, and each provider has put the real information somewhere else. Your job on a migration is to find where.
400 is a bucket, not a cause
On any of these APIs, HTTP 400 covers at minimum: a request body that does not validate against the schema; a parameter that exists but is unsupported on the chosen model; a prompt that exceeds the context window; and a structured-output schema that fails the provider’s own schema validation. Those four have completely different fixes — fix the client, drop a parameter, shorten the input, rewrite the schema — and only the message text distinguishes them.
The context-window case is the one that most often surprises people, because intuitively it feels like a size problem and therefore like a 413. It is not, on either major API: an oversize prompt is an invalid request, and it arrives as a 400 whose message names the limit and the count. A handler that only treats 413 as “too big” will route it to the generic bad-request path and lose the one piece of actionable information in the response.
A related trap: a parameter removed on a newer model returns 400 as well. Sampling parameters and thinking-budget parameters have both been retired on current model families, and a request carrying a field the model no longer accepts fails validation exactly like a typo would. If a request worked last month and 400s now with nothing changed on your side, the model changed under you.
429 means at least three things
This is the code with the most expensive ambiguity, because the three causes have different correct responses and one of them is not a retry at all.
- Requests per minute exceeded. You are sending too many calls. Back off, and reduce concurrency. Time fixes it.
- Tokens per minute exceeded. You are sending too much content, possibly in very few calls. Backing off helps but reducing concurrency helps more, and a single enormous request can trip this on its own with no burst at all.
- Quota or credit exhausted. Time does not fix it. This needs a human, an alert, and a fallback provider — not a backoff curve.
The discriminator is in the body, and rate-limit headers tell you which bucket you hit. Both major providers emit per-bucket rate-limit headers on responses — separate remaining counts and reset times for the request bucket and the token bucket — but the header names differ, which is precisely the sort of detail that survives a copy-paste and then reads as undefined forever after. Grep for the old header prefix during a migration.
The 5xx range is not enumerable
Server-side conditions are where the two ecosystems diverge most visibly. A generic internal error is 500 on both. Capacity, though, gets its own signal, and it is not the same signal: one API uses the standard 503, another uses HTTP 529 — a code outside the registered set, chosen precisely so that overload is distinguishable from a genuine internal fault.
The consequence for ported code is concrete. A handler written as an enumeration of known codes drops the unfamiliar one into the “unexpected, do not retry” branch, and the most retryable failure the API produces becomes a hard error. Write the check as a range. This is the single highest-value line to change during a migration, and it takes one minute.
What to key your handler on
The rule that survives every migration: status decides the shape of the response, the typed error field decides the behaviour. Status tells you whether the failure was yours, theirs, or the network’s. It does not tell you what to do. Read the body.
Practically, that means the provider-specific file in your adapter layer should expose one function that turns a raw failure into your own enumerated cause — something like rate_limited, over_quota, too_long, bad_schema, unsupported_param, overloaded, auth, unknown — and everything upstream keys on that. Adding a provider then means writing one mapping table, not auditing every handler.
Keep the raw status, the raw type field and the request id in the log line for every failure. The status alone will not tell you what happened, and the request id is the only thing a provider’s support can act on. The details of what to do with each classification follow from having made it correctly in the first place.