Skip to main content

Overview

Requests to a model can fail for reasons that have nothing to do with your code, at the inference step itself: a provider is briefly overloaded, a connection drops, a stream is cut off halfway through an answer. Gloo AI already absorbs much of this for you with provider failover. What reaches your application is a smaller set of errors, and every one of them tells you what to do next. In this tutorial you’ll build a small client wrapper that reads those signals and acts on them: retry when the API says a retry can work, wait as long as the API asks, stop when retrying would only fail again, and keep a streamed answer on screen when the stream breaks.

What You’ll Build

By the end of this tutorial you’ll have a client module with:
  • An error classifier that turns any Gloo AI error response into one of four decisions: success, retry, fix the request, or stop
  • Backoff with jitter that honors Retry-After and gives up within a fixed budget
  • A request wrapper that retries non-streaming calls and logs what support needs
  • Failover awareness: what to expect in a response that was served after a provider failed, and what not to build yourself
  • Stream recovery that retries before any text is shown and continues the answer once after text is shown
The examples are in Python and TypeScript, and run against the Chat Completions shape used by Completions V2 and Direct Completions.

Prerequisites

Before starting, ensure you have:
This tutorial is the hands-on build. For the policy behind each choice, read the best practices alongside it: Error Handling and Retries and Handling Streaming Failures.

Step 1: Learn What an Error Tells You

Every Gloo AI error carries a structured error object. Branch on its fields, never on the wording of the message:
The direct endpoints add a few more signals: Some refusals use a detail field instead of an error object. A content-policy refusal carries a detail object with a string code and retryable: false; an authentication failure on the guarded endpoints carries detail as a plain string. Your classifier needs to handle all three shapes. The errors you’ll see most fall into four groups: Two cases catch people out:
  • 3001 / INTERNAL_ERROR is never retryable, while a mid-stream 2014 / PROVIDER_STREAM_ERROR is. Same family of status codes, opposite decisions. That’s why the classifier reads retryable, not the status.
  • 3007 / GATEWAY_DEADLINE means a non-streaming request ran past Gloo’s response deadline. Sending it again meets the same deadline. Stream long generations, or lower the output limit or the reasoning effort.
The full list is in Errors, and the 429 variants are explained in Rate Limits.

Step 2: Classify Every Response

The classifier is the heart of the client. It reads the status, headers and body, and returns one decision. It is fail-closed: only an explicit retryable: true leads to a retry. A missing field, an unparseable body or an unknown error never does.
The code does the following:
  • Returns success for any 2xx, keeping X-Request-Id
  • Reads the error object first, then a detail object, so both error shapes work. A plain-string detail (an authentication failure) has no retryable field, so it is never retried
  • Prefers the X-Gloo-Error-Code header for the code on the direct endpoints, then error.name
  • Marks 400, 404, 405, 413 and 422 as fix the request, and everything else that isn’t retryable as stop
  • Takes the wait from the Retry-After header, or from error.retry_after in the body
  • Falls back to a safe, generic message for users when the body has none

Step 3: Back Off With Jitter

When a retry is allowed, how long you wait matters as much as whether you retry. Two rules:
  1. When the response has Retry-After, wait at least that long. The server knows when capacity returns.
  2. Otherwise, back off exponentially with jitter. Doubling the wait gives a struggling provider room to recover; jitter spreads out clients that failed at the same moment, so they don’t all retry in the same instant.
backoff_delay is for background work, streaming or not: 1, 2, 4 and 8 seconds plus up to a second of jitter, never more than 30 seconds. full_jitter_delay is for when someone is waiting, on an interactive request or a live stream; it picks a random wait of at most 2 seconds.

Retry budgets

Always bound the number of attempts. These are the budgets used in this tutorial, from the retry rule and Handling Streaming Failures:

Step 4: Wrap Your Requests

Now put the classifier and the backoff together in a post method. It sends the request, classifies the response, and either returns, waits and retries, or raises. A background flag picks the budget: by default it assumes someone is waiting for the answer.
The code does the following:
  • Picks the budget from background, using the budgets in the table above
  • Retries a network failure with no response (connection refused, reset, client timeout) with backoff, because there is no error object to read
  • Retries a retryable: true error, waiting for Retry-After plus a little jitter, or backing off
  • Raises immediately for fix the request and stop, so your code can handle them
  • Raises straight away when the server asks for a longer wait than the budget allows, so the caller can tell the user to try again shortly or reschedule the job
  • Logs code, retryable, trace_id and X-Request-Id on every failed attempt
  • Warns when the response was served by a replacement for a deprecated model (X-Gloo-Model-Redirected-From)
Use it like any HTTP call. This request has someone waiting for it, so it keeps the default interactive budget:
If a 3003 / SERVICE_UNAVAILABLE asks for 30 seconds, this request raises at once and your user sees the message instead of a frozen screen. For work nobody is watching, such as a nightly batch, pass background=True ({ background: true } in TypeScript) to get the larger budget:
Don’t stack retry layers. If you wrap this client in another retry loop, or call it from a job queue that retries failed jobs, the attempts multiply. Pick one layer to own retries.

Step 5: Work With Provider Failover

Most models in the Gloo AI catalog are served by more than one provider. When the provider serving your request fails, Gloo AI retries it on another provider of the same model, on every chat and responses endpoint, with no configuration. Your client doesn’t need to switch models or add a second provider to ride out an outage. What changes is what a response looks like after failover: When an error does reach you, it means one of two things:
  • No provider could take the request. You get 503 with 2002 / PROVIDER_UNAVAILABLE and retryable: true. Retry with backoff: a later request benefits from Gloo having already routed around the failing provider. If it persists, try a different model.
  • The failure would happen on every provider. An invalid request, an authorization problem, a content-policy block or a billing limit is returned straight away instead of being repeated elsewhere. Your classifier already marks these as fix the request or stop.
Two endpoints work differently:
  • Anthropic Messages is served by Anthropic only, with no failover. A 529 there means retry after retry-after. The official Anthropic SDKs already do this, so use their built-in retries rather than this wrapper.
  • Embeddings returns 503 with retryable: true when the provider fails. The classifier handles it.
Failover works within a time budget for each request, so a request never waits indefinitely. For long generations, stream rather than raising your client timeout: a non-streaming request that runs past Gloo’s response deadline gets 3007 / GATEWAY_DEADLINE, which isn’t retryable unchanged.

Step 6: Recover Interrupted Streams

Streaming adds one complication: once a stream has started, the HTTP status is already 200, so later failures arrive inside the stream. A streamed request can end four ways: The other question is whether the user has already seen text:
  • Nothing shown yet: a full retry is invisible to the user. Retry up to the budget.
  • Text on screen: a full retry would erase or duplicate it. Keep the text, and send one Continue request that asks the model to pick up where it stopped.
Gloo AI already continues a cut-off stream for you on models that support it, and retries a stream that fails before any text reaches you. This code handles what’s left.
The code does the following:
  • Classifies a non-200 response with the same classify function, so a 401 before the stream is never retried
  • Collects text as it arrives and records whether any was shown, and whether a tool call was emitted
  • Ends on stop, length or tool_calls as complete, and on content_filter as final
  • Stops on retryable: false, keeping any partial text
  • Treats a dropped connection, a client timeout or a malformed event as a transport error, which is plausibly transient
  • Retries before any text is shown: 2 full retries with short waits for live streams; 4 retries (5 requests in total) with min(2^(attempt-1), 30) seconds plus jitter for background jobs
  • Honors a server-requested wait when it fits the budget, and fails fast when it doesn’t (for example a 30-second Retry-After on a live stream)
  • After text is shown, sends one Continue request, using a new user message that carries the partial answer
  • Skips the automatic Continue when a tool call was emitted, so a side effect can’t run twice
Use it with a callback that renders text as it arrives:

Designing the interrupted state

When status is interrupted, the partial answer is still in result.text and still on screen. Keep it visible and mark it as incomplete. Offer Continue as the main action (it appends) and Try again as a secondary action that’s clearly labelled as replacing the answer. A user-initiated Continue doesn’t count against the automatic budget.
A Continue is a new request: it can repeat a phrase or drift from where the first stream was heading. For high-stakes, factual or doctrinal answers, prefer Try again or user review over an automatic Continue. See Application-Level Continuation.

Direct Responses streams

On Direct Responses, a stream that fails after output has started ends with a response.failed event instead of an error chunk. response.error carries the string code (the same value as X-Gloo-Error-Code), gloo_code, retryable and, when it applies, retry_after. The same decisions apply: read retryable, check whether text was shown, and continue or surface.

Step 7: Test the Failure Paths

Don’t wait for a real outage to test your retry logic. Both clients take their transport and their sleep function as constructor arguments, so you can script failures and check the decisions without calling the API or waiting.
Run it:
Your output should look similar to the following (the client logs each failed attempt):
Each check fails loudly if the client returns when it should have raised. Add cases for the paths that matter to your application: a 400 content-policy refusal, a network failure with no response, a stream that drops after the first chunk, and a stream that ends with content_filter.

Step 8: Log What Support Needs

When something fails repeatedly, the IDs in your logs are what let support find the exact request. For every failed attempt, record:
  • error.code, error.name, error.fault and error.retryable
  • trace_id from the error, and X-Request-Id on the direct endpoints (stream errors on the direct endpoints carry no trace_id, so X-Request-Id is the one to keep)
  • developer_message, when present
  • The model, the attempt number and the wait before the retry
  • For streams: whether text was shown, its length, whether a Continue was sent, and whether a tool call was emitted
If inference keeps failing after retries, open a ticket through Support in Gloo AI Studio (see Get help) and include the trace_id.

Troubleshooting

Every error is retried, including 401 and 400 : Check that retries depend on retryable === true and nothing else. A classifier that retries on “any 5xx” or “any error without retryable: false” is fail-open and will loop on errors that can’t succeed. Requests hang for 30 seconds before failing : A 3003 / SERVICE_UNAVAILABLE asked the client to wait for Retry-After, and the request was sent with background=True. For requests someone is waiting on, keep the default interactive budget: the client raises straight away and your UI can show “please try again shortly”. A 429 is never retried : Not every 429 is retryable. A provider rate limit (2004 / RATE_LIMIT) is; a billing or account limit is not, and comes back with retryable: false. See Rate Limits. 408 or 504 with 3007 / GATEWAY_DEADLINE on long answers : The non-streaming request ran past Gloo’s response deadline. Use GlooStreamClient, or lower max_tokens or the reasoning effort. A reasoning model returns no text with finish_reason: "length" : That isn’t an outage, and it isn’t retried. The model spent the output limit thinking. Raise max_tokens or lower the reasoning effort; see Set your output limit for reasoning models. A streamed answer appears twice after a failure : The client did a full retry after text was already shown. Track whether text was shown, and use Continue, not a full retry, once it has. The continued answer repeats the last sentence : Continuation is best effort. De-duplicate the overlap where the two parts meet before you render the second part.

Next Steps

Error Handling and Retries

Retry policy, budgets and backoff across every endpoint.

Handling Streaming Failures

The full decision tree and reference implementations for streams.

Errors Reference

Every error code, the error object, and streaming error delivery.

Provider Failover

When Gloo AI retries on another provider, and when it doesn’t.

Rate Limits

The different kinds of 429, and which ones to retry.

Prompt Caching

Cut cost and latency on the requests your client sends.