VernLLMVernLLM
Core Features

Retries

Exponential backoff with jitter, Retry-After awareness, and what gets retried

retries-setup.ts
const llm = new VernLLM({
  client: openai,
  model: 'gpt-4o',
  maxRetries: 3, // retries after the first attempt, so 4 attempts total
  baseDelayMs: 500, // base for exponential backoff between retries
});

Every call() retries transient failures automatically. maxRetries defaults to 1 (2 attempts total), so this is active even if you never touch the option.

Backoff and jitter

Each retry waits longer than the last, and the exact wait is randomized within a range rather than fixed:

exp = min(baseDelayMs * 2 ** attempt, maxDelayMs);
delay = exp / 2 + random() * (exp / 2);

This is full jitter: the delay lands somewhere between half and all of the computed exponential value, instead of every caller waiting the identical amount. When many callers hit a failure at the same time, full jitter spreads their retries out instead of having them all retry in lockstep and hit the provider again at the same instant.

The delay is capped at DEFAULT_MAX_DELAY_MS (10 seconds) regardless of attempt, so a high maxRetries never produces an unbounded wait.

Honoring Retry-After

If the failed attempt's error carries a Retry-After header, that value is used for the wait instead of the computed backoff:

recoverDelay = retryAfterMs ?? getBackoffDelay(baseDelayMs, attempt);

Retry-After is parsed from both delta-seconds form ("30") and HTTP-date form ("Wed, 21 Oct 2015 07:28:00 GMT"), checking .headers (fetch-style) then .response.headers (axios-style) so it works across client libraries. The honored value is capped at the same max delay as backoff, so a misbehaving or adversarial header can't stall a caller indefinitely.

What gets retried

shouldRetry skips a retry when any of these are true:

  • The signal has already aborted
  • error.type is parse or validation, since these are deterministic response processing failures that a retry will not fix
  • error.status is in nonRetryableStatus (default 400, 401, 403, 404, 422)

Everything else, including timeouts, 5xx errors, network failures, and unknown errors, is retried up to maxRetries.

Exhausting all retries also records a circuit breaker failure if one is configured, and throws the final normalized LLMError. See Error Handling for the full list of error types and how each one is normalized.

Everything on this page describes retries against a single provider target. With fallback configured, each declared target, primary and every fallback target, gets its own independent maxRetries/baseDelayMs/nonRetryableStatus, resolved from that target's own overrides or inherited from the instance when omitted. Exhausting one target's retries doesn't fail the call outright, it hands off to fallbackOn to decide whether to try the next target. See Provider Fallback.

Cancelling mid retry

If a signal fires while a retry is waiting out its backoff delay, the pending wait is cancelled immediately rather than sitting idle until the delay finishes. An abort during backoff is never retried, it is treated as a deliberate cancellation. See Cancellation & Timeouts for the full abort lifecycle.

Reading attempt history

Every normalized LLMError carries an optional attempts array. Each entry has the attempt's index and a snapshot of that attempt's error: an LLMErrorSnapshot with message, type, code, status, issues, retryAfterMs, retryable, and its own nested attempts if that attempt was itself the terminal failure of a retry loop. It's a snapshot, not a live LLMError, since a past attempt is a record, not something you'd catch or rethrow. cause isn't part of it. cause is meant to be read on the live error you actually caught (err.cause), not carried inside recorded history. Each entry can also carry request, an LLMRequestSnapshot of what was actually sent for that attempt, with auth headers always removed. See Error Handling for cause and for the full LLMRequestSnapshot shape.

try {
  await llm.call({ userContent: 'hello' });
} catch (err) {
  if (isLLMError(err)) {
    for (const attempt of err.attempts ?? []) {
      console.log(attempt.index, attempt.error.type);
    }
  }
}

attempts is absent when nothing was retried, for example a call that failed on its first and only try. With fallback configured, each FallbackAttempt on a FallbackExhaustedError extends this same shape, adding provider and model, and that attempt's own error.attempts still holds the retries made against that one target. See Provider Fallback for FallbackExhaustedError itself.

Options reference

OptionDefaultNotes
maxRetries1Retries after the first attempt. maxRetries: 3 means up to 4 attempts.
baseDelayMs500Base for exponential backoff. Actual delay grows per attempt with jitter, and is overridden by an honored Retry-After.
nonRetryableStatus[400, 401, 403, 404, 422]Status codes that fail immediately instead of retrying.

See Configuration for every option alongside timeout and circuit breaker settings.

On this page