Spaces:
Runtime error
Runtime error
| /** | |
| * src/utils/retry.ts | |
| * | |
| * Generic async retry helper with exponential backoff. | |
| * Used throughout the LLM pipeline where network flakiness is expected. | |
| */ | |
| import { logger } from './logger'; | |
| // βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface RetryOptions { | |
| /** Maximum number of retry attempts (not counting the initial try). */ | |
| maxRetries: number; | |
| /** Base delay in milliseconds before first retry. */ | |
| delayMs: number; | |
| /** Multiplier applied to delay on each subsequent retry. Default: 2 (exponential). */ | |
| backoffMultiplier?: number; | |
| /** If provided, only retries when this predicate returns true. */ | |
| retryOn?: (error: unknown) => boolean; | |
| /** Called just before each retry β useful for key rotation side effects. */ | |
| onRetry?: (attempt: number, error: unknown) => void; | |
| } | |
| // βββ Core Retry Function ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Executes `fn` up to `maxRetries + 1` times total, with exponential backoff. | |
| * | |
| * @example | |
| * const result = await withRetry( | |
| * () => callLLM(prompt), | |
| * { maxRetries: 3, delayMs: 1000, retryOn: isRateLimitError } | |
| * ); | |
| */ | |
| export async function withRetry<T>( | |
| fn: () => Promise<T>, | |
| options: RetryOptions | |
| ): Promise<T> { | |
| const { | |
| maxRetries, | |
| delayMs, | |
| backoffMultiplier = 2, | |
| retryOn, | |
| onRetry, | |
| } = options; | |
| let lastError: unknown; | |
| for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { | |
| try { | |
| return await fn(); | |
| } catch (err) { | |
| lastError = err; | |
| // Don't retry if the error type is not eligible | |
| if (retryOn && !retryOn(err)) { | |
| throw err; | |
| } | |
| if (attempt > maxRetries) break; // Exhausted retries | |
| const delay = delayMs * Math.pow(backoffMultiplier, attempt - 1); | |
| onRetry?.(attempt, err); | |
| logger.warn( | |
| `withRetry: attempt ${attempt}/${maxRetries} failed. Retrying in ${delay}ms.`, | |
| { error: err instanceof Error ? err.message : String(err) } | |
| ); | |
| await sleep(delay); | |
| } | |
| } | |
| throw lastError; | |
| } | |
| // βββ Rate-Limit Classifier ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Returns true if the error is a rate-limit response (HTTP 429). | |
| * Works with Groq SDK errors, Axios errors, and generic Error messages. | |
| */ | |
| export function isRateLimitError(err: unknown): boolean { | |
| if (err instanceof Error) { | |
| const msg = err.message.toLowerCase(); | |
| // Covers: "429", "rate limit exceeded", "too many requests", "rate_limit_exceeded" | |
| if (msg.includes('429') || msg.includes('rate limit') || msg.includes('too many requests')) { | |
| return true; | |
| } | |
| // Groq SDK wraps status in the error object | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const status = (err as any).status ?? (err as any).statusCode; | |
| if (status === 429) return true; | |
| } | |
| // Axios error | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const axiosStatus = (err as any)?.response?.status; | |
| if (axiosStatus === 429) return true; | |
| return false; | |
| } | |
| // βββ Private Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function sleep(ms: number): Promise<void> { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |