error object, in the shape an OpenAI-compatible client already
parses. Your existing error handling keeps working, and the fields below are the
contract.
The body
Every error on the API, at every status, is one JSON object with a single top-levelerror key. An OpenAI client parses it with the error handling it
already has.
Error envelope
Branch on
type and code. Both are stable. message is prose and may change.
The type values are invalid_request_error, authentication_error,
permission_error, not_found_error, rate_limit_error, api_error,
insufficient_quota, and server_error.
Three properties of this body are worth stating because clients depend on them:
paramandcodeare always present. When there is nothing to say they arenullrather than omitted, so a missing field never has to be told apart from a null one.request_idis stamped on every error body, whatever produced the error. It is the same value as thex-hopscotch-request-idandx-request-idresponse headers.- No other field appears inside
error. Nothing about our internals travels in an error body, and a body that would carry anything else is replaced before it leaves us.
type and code. Messages are prose written for a person, and we
reword them; the fields are what we hold still.
type
A closed set. A value outside it is not something this API can send, so a client
may treat the set as exhaustive.
api_error and server_error both mean “not your request”. The split matters
for retries: api_error with no code is an unhandled fault that will most
likely fail the same way again, while server_error codes name a condition that
clears.
Status and code
Every code below is produced by a shipped handler.code is the field that
separates causes sharing a status.
A request that never reached a model is not charged for, and the credit held for
it is returned. A request terminated part way through is charged for what was
produced before it was cut and no more.
Rate limits and spend refusals
Three codes share429, and they call for different behaviour. Read the code:
the status alone does not tell you which of the three you met.
rate_limit_exceededis your key sending too many requests. The response carriesRetry-Afterin seconds. Back off and retry.spend_rate_exceededis the account’s spend rate cap, which counts the estimated maximum cost reserved in a rolling window rather than settled spend. There is noRetry-After. Sendingmax_tokenson your requests lowers the estimated maximum each one reserves. See Rate limits and spend controls.upstream_rate_limitedis the model’s provider rate-limiting the call. It is not your key’s allowance and not your account’s cap, nothing about the request is wrong, and nothing is charged. It carriesRetry-Afterin whole seconds.
Retry-After is present on rate_limit_exceeded and on upstream_rate_limited,
and is not sent on other statuses. Do not infer a retry delay from its absence.
The Retry-After on upstream_rate_limited is a figure we compute rather than
the provider’s header passed through: their number where they sent one, held
between one and sixty seconds, and five seconds where they sent none. A wait of
zero is never sent, and no wait longer than a minute is, whatever the provider
asked for.
A provider that answers 529 rather than 429 carries the same
upstream_rate_limited code, with type api_error as the status class
requires. The code is the part to branch on.
The message on upstream_rate_limited is our generic one, the same sentence
every fault on our side carries, and that is deliberate rather than an oversight:
a throttle on a credential of ours is not a fact about your request, and the
provider’s own words about it name our account. The code and the Retry-After
are the actionable half, which is why the code is what to read.
Provider key refusals
provider_key_refused only appears on a request that ran on your own key for
that provider, never on a Hopscotch-held key for it. While your workspace holds
that key, every request to the provider runs on it alone, and nothing falls
back to a different credential if the provider refuses it.
The code is the same at both statuses; what differs is why the provider
refused the request:
429is the provider saying that key is over its limit there. It clears on its own, so back off and retry.400is the provider rejecting the key itself, or refusing this particular call under it. Retrying the same request will be refused identically until the key is replaced on the Provider keys screen.
Retry-After, and neither is charged.
Errors during a streamed response
Once a streamed response has started, its status and headers are already sent, so a failure cannot change them. Instead the stream ends with one moredata: line
carrying the same error body, and then closes. There is no data: [DONE] after
it.
balance_exhausted is only ever a mid-stream code. A request refused before it
started because the account cannot pay is 402 with insufficient_credit, so a
client can always tell “never started” from “cut off part way”.
The request_deadline_exceeded line is the one error body on this API without a
request_id: it is written after the response has left the point that stamps the
id. Read the id from the x-hopscotch-request-id response header instead.
A non-streamed response that we terminate part way through does not receive an
error body. There is nowhere to put one, so the connection errors instead. Treat a
truncated non-streamed body as a failed request rather than a short answer.
Handling errors in code
Catch on the fields, never on the message.Every response carries the same id on two headers:
x-hopscotch-request-id,
which is ours, and x-request-id, which is the one the official OpenAI SDKs
surface on their error objects. Every error envelope repeats it as
request_id. Quote it when you ask us about a request. Without it we are
guessing at which of your requests you mean.