export interface RetryConfig { /** Maximum number of retry attempts (default: 3). Does not count the initial attempt. */ maxRetries: number; /** Initial delay in milliseconds before first retry (default: 1000). */ initialDelayMs: number; /** Multiplier applied to delay after each retry (default: 2). */ backoffMultiplier: number; /** Maximum delay in milliseconds (default: 30000). */ maxDelayMs: number; /** Errors matching these patterns should NOT be retried (e.g. auth errors, invalid requests). */ nonRetryablePatterns: string[]; } export const DEFAULT_RETRY_CONFIG: RetryConfig = { maxRetries: 3, initialDelayMs: 1000, backoffMultiplier: 2, maxDelayMs: 30000, nonRetryablePatterns: [ 'invalid_api_key', 'authentication', 'unauthorized', 'invalid_request', 'context_length_exceeded', 'content_policy', ], }; export function isRetryable(error: Error, nonRetryablePatterns: string[]): boolean { const msg = error.message.toLowerCase(); return !nonRetryablePatterns.some(pattern => msg.includes(pattern.toLowerCase())); } export async function withRetry( fn: () => Promise, config: RetryConfig = DEFAULT_RETRY_CONFIG, label?: string, ): Promise { let lastError: Error | undefined; for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry non-retryable errors if (!isRetryable(lastError, config.nonRetryablePatterns)) { throw lastError; } // Don't retry if we've exhausted attempts if (attempt >= config.maxRetries) { throw lastError; } // Calculate delay with exponential backoff + jitter const baseDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt); const delay = Math.min(baseDelay, config.maxDelayMs); const jitter = delay * (0.5 + Math.random() * 0.5); // 50-100% of delay console.warn( `[retry] ${label ?? 'operation'} attempt ${attempt + 1}/${config.maxRetries} failed: ${lastError.message}. Retrying in ${Math.round(jitter)}ms...`, ); await new Promise(resolve => setTimeout(resolve, jitter)); } } throw lastError ?? new Error('Retry failed with no error'); }