import { PolyProviderError } from "./types"; import { sleep } from "@/lib/utils"; export interface RetryOptions { maxAttempts?: number; baseDelayMs?: number; maxDelayMs?: number; signal?: AbortSignal; onRetry?: (attempt: number, error: PolyProviderError, delayMs: number) => void; } /** * Exponential backoff with full jitter. Only retries errors flagged `retryable` * (429 / 5xx / network / timeout). Auth, invalid request and unsupported-parameter * errors are never retried. Respects Retry-After when present. */ export async function withRetry(fn: (attempt: number) => Promise, normalize: (e: unknown) => PolyProviderError, opts: RetryOptions = {}): Promise { const max = opts.maxAttempts ?? 3; const base = opts.baseDelayMs ?? 500; const cap = opts.maxDelayMs ?? 8_000; let attempt = 0; while (true) { attempt++; try { return await fn(attempt); } catch (raw) { const err = normalize(raw); if (!err.retryable || attempt >= max || opts.signal?.aborted || err.code === "CANCELLED") throw err; const backoff = Math.min(cap, base * 2 ** (attempt - 1)); const delay = err.retryAfterMs ?? Math.round(Math.random() * backoff); opts.onRetry?.(attempt, err, delay); await sleep(delay, opts.signal); } } }