Repository navigation
Errors and Retry
Errors are matched with errors.Is against sentinels and read with errors.As; the message around them is not API. Retry happens at well-defined places only, never in the middle of a stream.
| Package | Error | Meaning |
|---|---|---|
llm |
ErrAuthentication |
rejected or missing credential, or an account that cannot pay (401, 402, 403); retrying does not help |
llm |
ErrRateLimited |
429, after the client's own attempts are used up |
llm |
ErrServerError |
5xx, 408, a broken stream, an unexpected status; may pass on a retry |
llm |
ErrInvalidRequest |
other 4xx, or refused before sending (missing model, unsupported feature) |
llm |
ErrInvalidResponse |
a success status whose body does not decode, or an answer above the client's size caps; not transient, not retried |
llm |
ErrContentFiltered |
the provider's safety policy aborted the generation; do not retry the same prompt |
llm/openrouter |
*APIError{StatusCode, Code, Message} |
any unexpected HTTP status; Unwrap returns the llm sentinel above. A 429 with Retry-After arrives wrapped in a *retry.AfterError
|
llm/codex |
ErrReauthenticationRequired, ErrDeviceLoginDisabled
|
the login is gone for good; the issuer has no device login |
llm/codex |
ErrRefreshInterrupted |
a token refresh got no complete answer, the refresh token may have rotated; not transient, never retried (Models) |
tools |
ErrUnknownTool, ErrInvalidArguments, ErrToolPanicked, ErrDuplicateTool
|
Tools |
vfs |
ErrPathInvalid, ErrFileNotFound, ErrFileTooLarge, ErrReadOnly
|
Files |
agent/history/memory |
ErrEmptySessionID |
|
llm/llmtest |
ErrStubExhausted |
a scripted client ran out of scripts |
A cancelled or expired ctx surfaces as context.Canceled or context.DeadlineExceeded, never as ErrServerError, so it is neither reported nor retried as a provider failure.
llm.IsTransientServerError(err) is true for ErrServerError only (not for rate limits, which the clients retry themselves). llm.IsContentFilter(err) detects a content filter, also from a structured error envelope. Providers sometimes drop a stream without any signal; that is an ErrServerError and indistinguishable from a flake.
Agent runs do not return errors: they end with EventError plus an EndReason (Agent). Tool errors never end a run.
| Path | Retried | Not retried |
|---|---|---|
openrouter Stream
|
429, 5xx and network errors before the first SSE byte, Options.MaximumAttempts in total (default 3), Retry-After honoured |
anything after the stream started: it ends with EventError
|
openrouter video submit (POST /videos) |
429 only: nothing else proves the job was not created | 5xx, network errors and a broken 202 answer: the job may exist and be billed, a second submit would bill a second clip |
openrouter Complete, images, speech, decisions, models, each video poll and download |
the same rule per HTTP request | authentication, invalid request, content filter, context errors, a 2xx whose body does not decode (ErrInvalidResponse), a 2xx whose body breaks off mid-read (ErrServerError: the work may be done and billed, so the request is not sent again) |
codex Stream
|
429 and 5xx at stream start (3 attempts) | mid-stream errors |
llm.CollectWithRetry |
restarts the whole stream after a failure policy.ShouldRetry accepts (default llm.IsTransientServerError); every attempt is billed |
|
llm.CompleteWithRetry |
a fresh Complete per attempt, same default predicate |
|
agent.Session |
nothing beyond the client's own attempts; a failed model call ends the run and leaves History as it was, so the caller can run again |
llm.DefaultStreamingRetryPolicy() (5 attempts, 2 s base, 30 s cap, 20 % jitter, transient errors only) suits flaky image previews. From llm/example_test.go (ExampleCompleteWithRetry):
policy := retry.Policy{MaximumAttempts: 3, BaseDelay: time.Millisecond}
// ShouldRetry is nil, so only transient server errors are retried: a content filter is final.
_, err := llm.CompleteWithRetry(context.Background(), client, llm.Request{Model: "m"}, policy)
fmt.Println(llm.IsContentFilter(err), llm.IsTransientServerError(err), len(client.Requests()))retry.Do(ctx, policy, func(attempt int) error) calls the operation until it succeeds, ShouldRetry refuses, or MaximumAttempts is reached, and returns the last error unchanged. From retry/example_test.go (ExampleDo):
policy := retry.DefaultPolicy()
policy.BaseDelay = time.Millisecond
policy.ShouldRetry = func(err error) bool { return errors.Is(err, errUnavailable) }
err := retry.Do(context.Background(), policy, func(attempt int) error {
fmt.Println("attempt", attempt)
if attempt < 3 {
return errUnavailable
}
return nil
})retry.Policy |
Zero or out-of-range value means |
|---|---|
MaximumAttempts |
below 1: one call, no retry |
BaseDelay |
zero or negative: no wait between attempts |
MaximumDelay |
zero or negative: 30 s; caps every wait, also a Retry-After
|
Multiplier |
below 1: 2 |
Jitter |
0 or negative: none, above 1: 1; Retry-After waits are never jittered |
ShouldRetry |
nil: every error |
retry.DefaultPolicy() is 3 attempts, 500 ms base, 30 s cap, 2x, 20 % jitter. Do never retries context.Canceled or context.DeadlineExceeded, whatever ShouldRetry says, and returns the last error without waiting when the wait would end after the context's deadline. An operation that knows the server's wish returns &retry.AfterError{Cause: err, After: d}; ShouldRetry still decides whether there is a next attempt.
Mirror of docs/wiki at v1.2.0 (dd94d84). Edit the repository, not this wiki.