UNPKG

ai

Version:

AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript

155 lines (137 loc) 4.07 kB
import { APICallError } from '@ai-sdk/provider'; import { delay, getErrorMessage, isAbortError } from '@ai-sdk/provider-utils'; import { RetryError } from './retry-error'; export type RetryFunction = <OUTPUT>( fn: () => PromiseLike<OUTPUT>, ) => PromiseLike<OUTPUT>; function getRetryDelayInMs({ error, exponentialBackoffDelay, }: { error: APICallError; exponentialBackoffDelay: number; }): number { const headers = error.responseHeaders; if (!headers) return exponentialBackoffDelay; let ms: number | undefined; // retry-ms is more precise than retry-after and used by e.g. OpenAI const retryAfterMs = headers['retry-after-ms']; if (retryAfterMs) { const timeoutMs = parseFloat(retryAfterMs); if (!Number.isNaN(timeoutMs)) { ms = timeoutMs; } } // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After const retryAfter = headers['retry-after']; if (retryAfter && ms === undefined) { const timeoutSeconds = parseFloat(retryAfter); if (!Number.isNaN(timeoutSeconds)) { ms = timeoutSeconds * 1000; } else { ms = Date.parse(retryAfter) - Date.now(); } } // check that the delay is reasonable: if ( ms != null && !Number.isNaN(ms) && 0 <= ms && (ms < 60 * 1000 || ms < exponentialBackoffDelay) ) { return ms; } return exponentialBackoffDelay; } /** * The `retryWithExponentialBackoffRespectingRetryHeaders` strategy retries a failed API call with an exponential backoff, * while respecting rate limit headers (retry-after-ms and retry-after) if they are provided and reasonable (0-60 seconds). * You can configure the maximum number of retries, the initial delay, and the backoff factor. */ export const retryWithExponentialBackoffRespectingRetryHeaders = ({ maxRetries = 2, initialDelayInMs = 2000, backoffFactor = 2, abortSignal, }: { maxRetries?: number; initialDelayInMs?: number; backoffFactor?: number; abortSignal?: AbortSignal; } = {}): RetryFunction => async <OUTPUT>(f: () => PromiseLike<OUTPUT>) => _retryWithExponentialBackoff(f, { maxRetries, delayInMs: initialDelayInMs, backoffFactor, abortSignal, }); async function _retryWithExponentialBackoff<OUTPUT>( f: () => PromiseLike<OUTPUT>, { maxRetries, delayInMs, backoffFactor, abortSignal, }: { maxRetries: number; delayInMs: number; backoffFactor: number; abortSignal: AbortSignal | undefined; }, errors: unknown[] = [], ): Promise<OUTPUT> { try { return await f(); } catch (error) { if (isAbortError(error)) { throw error; // don't retry when the request was aborted } if (maxRetries === 0) { throw error; // don't wrap the error when retries are disabled } const errorMessage = getErrorMessage(error); const newErrors = [...errors, error]; const tryNumber = newErrors.length; if (tryNumber > maxRetries) { throw new RetryError({ message: `Failed after ${tryNumber} attempts. Last error: ${errorMessage}`, reason: 'maxRetriesExceeded', errors: newErrors, }); } if ( error instanceof Error && APICallError.isInstance(error) && error.isRetryable === true && tryNumber <= maxRetries ) { await delay( getRetryDelayInMs({ error, exponentialBackoffDelay: delayInMs, }), { abortSignal }, ); return _retryWithExponentialBackoff( f, { maxRetries, delayInMs: backoffFactor * delayInMs, backoffFactor, abortSignal, }, newErrors, ); } if (tryNumber === 1) { throw error; // don't wrap the error when a non-retryable error occurs on the first try } throw new RetryError({ message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`, reason: 'errorNotRetryable', errors: newErrors, }); } }