GenAIcode does not retry provider calls automatically. Retries, backoff, and failure budgets belong to application code so side effects stay explicit.
import { classifyError, isRetryable, withRetry } from 'genaicode';
const classified = classifyError(error);
if (classified.retryable) {
// transient: 408/425/429/5xx, network resets, overload heuristics
}
await withRetry(() => ai('summarize this').text(), {
attempts: 3,
delayMs: 250,
shouldRetry: (error) => isRetryable(error),
});classifyError returns { class, retryable, reason, status?, code?, cause }:
| Class | Typical causes | Retry? |
|---|---|---|
transient |
429, 5xx, timeouts, connection resets | yes |
permanent |
4xx (except above), auth, abort | no |
unknown |
Unrecognized shapes | no* |
*Treat unknown as non-retryable by default; override with shouldRetry when you
know more about your gateway.
Safe to retry without extra coordination:
- Pure generation (
text,json,toolCallsthat only propose tool calls) - Read-only prompts against immutable inputs
Not safe to retry blindly:
- Application code that executes tool calls with side effects (writes, charges, emails)
- Chains where a partial turn already mutated external state
Patterns that keep retries safe:
- Propose, then commit. Let the model return a plan; apply side effects once after validation.
- Idempotency keys. If a tool must run inside a retry loop, key the effect
(e.g.
Idempotency-Keyon a payment API) so duplicates collapse. - Outbox / queue. Persist the intended effect before calling the provider, or after a successful model response but before side effects, depending on your failure mode.
- Do not put retries inside plugins by default. A retry plugin hides policy
from callers; prefer
withRetryat the call site or a named, intentional plugin.
If a stream fails mid-flight, do not assume the partial text was committed anywhere. Re-run the full request (or resume with your own checkpointing). Conversation chains only append history after a successful completed turn.