Agent SDK retries
What an agent framework's retry actually re-runs, why it costs more than a retried API call, and how to make every model call in the loop idempotent.
An agent SDK retry re-runs a step, and a step is one or more model calls. That is the whole difference between retrying an agent and retrying an API request: the unit being retried is larger, it is retried more often, and each retry pays for every generation inside it again.
If you are running an agent loop against a model provider today, your retries are almost certainly compounding. This page is about why, and how to make them stop without changing the framework you are using.
Why agent retries cost more than API retries
An agent run is a loop: the model decides, a tool executes, the result goes back to the model, and round it goes. Three properties of that loop turn an ordinary retry into an expensive one.
Retries stack. The vendor SDK inside the framework retries on its own — two attempts by default in both the OpenAI and Anthropic clients. The framework retries the step on top of it. A supervisor or queue retries the run on top of that. Three layers of two attempts each is eight possible calls for one logical step, and nothing in the stack knows the layers below it already tried.
Context grows, so retries get more expensive as the run goes on. Every turn carries the whole transcript. A retry at turn 12 re-sends 11 turns of context and pays for all of it again; a retry at turn 2 does not. The retries that are most likely to happen — long runs, long generations — are the ones that cost the most.
A retried step can re-run side effects. Re-running a step that already called a tool can send the email twice. Idempotent model calls do not fix that on their own, but they remove the half of the problem that is pure waste, and they make the other half easier to reason about because the model's decision is reproduced rather than re-sampled.
Non-determinism makes a retried step a different step
Retrying a normal API call gets you the same answer. Retrying a model call gets you a new sample — possibly a different tool choice, possibly different arguments. So an agent retry is not "the same step again"; it is a fresh roll of the dice, at full price, that may take the run somewhere the first attempt was not going.
What changes when the calls are idempotent
Point the framework's underlying client at Perdurance and every model call in the loop is identified by the bytes of its body for ten minutes. A retry at any layer of the stack — SDK, framework, supervisor — re-sends those bytes and attaches to the execution already running instead of starting a second one.
| Without | With Perdurance |
|---|---|
| Layered retries multiply provider calls | Identical bytes attach; the provider is called once |
| A retried turn re-samples the model | A retried turn returns the stored answer, so the run stays on its path |
| A dropped connection loses a generation in flight | The generation continues server-side and is stored |
| A crashed worker loses the run's last step | The record survives the process; collect it by id |
| No transcript beyond what you logged yourself | Every request, answer, chunk and token count stored per key |
The last row is the one agent teams tend to want first. An agent run that went wrong is very hard to debug from application logs, because the interesting object — the exact bytes sent and the exact bytes returned, turn by turn — is the thing least likely to have been kept.
Wiring it into a framework
Every agent framework takes a provider client or a base URL somewhere. That is the only place this touches.
from agents import Agent, Runner, OpenAIChatCompletionsModel
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="https://api.perdurance.dev/acme/prod/v1",
api_key=PERDURANCE_KEY,
max_retries=5,
)
agent = Agent(
name="researcher",
model=OpenAIChatCompletionsModel(model="gpt-4.1", openai_client=client),
)
# Every model call the loop makes is now idempotent for ten minutes.
result = await Runner.run(agent, "Summarise last quarter's incidents.")No agent code changes. The framework does not learn a Perdurance concept, and neither does the model.
Keep the bytes identical
The mechanism is a hash over the request body, so the one thing that defeats it is a body that differs between attempts. In an agent loop that is easier to do by accident than in a plain API call:
- A timestamp in the system prompt. "The current time is 14:32:07" makes every retry a new request. Round it, or move it into a tool the model calls.
- A fresh UUID per attempt — a trace id, a turn id, a request id injected into the prompt.
- Non-deterministic serialisation. A dict rebuilt between attempts may emit its keys in a different order. Same object, different bytes, different request.
- A tool result that varies. If the retried step re-executes a tool whose output changes, the next model call's body changes with it.
Send the same bytes, not an equivalent object. Idempotency and resume has the full list of what is not deduplicated.
Long runs that outlive the caller
Agent turns are the generations most likely to outlast a socket: reasoning models, large tool outputs, a serverless function with a hard timeout it cannot argue with.
Two shapes are available, and an agent loop can use both.
Synchronous, on the provider's own route. The connection is held for up to five minutes
(the deployment's synchronous hold). If the generation outlasts it you get 504 held_too_long, which is
not a failure — the execution continues and the answer is stored. Re-send the identical
body to attach to it, or read it by id.
Fire-and-forget, on POST /requests. You get 202 and a request id immediately, and the
execution runs without you. This is the shape for a batch of agent runs, or for any caller that
cannot hold a socket open for a long turn. Collect with GET /requests/{id}, whole or as a
stream replayed from the first chunk and resumable from wherever you stopped reading with
Last-Event-ID.
What this does not do
It does not make your tools idempotent. A retried step that already sent an email will send it again, and no proxy can know that. What it removes is the model-call half: the generations you paid for twice, and the re-sampling that made a retried step behave unlike the one it replaced.
It also does not checkpoint agent state. Perdurance stores requests, not your run's variables — each model call is durable, the loop around them is still yours to make resumable.
LLM inference retries
Why retrying an LLM inference call normally bills you twice, what a safe retry actually requires, and how to make the retry your SDK already performs idempotent.
Model routing
How a model name in a request body resolves to a provider: backends, glob patterns, priority order, and model rewrites.

