diff --git a/src/client.ts b/src/client.ts index ac082a5e1..c1787c2ff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1026,9 +1026,9 @@ export class OpenAI { } } - // If the API asks us to wait a certain amount of time, just do what it - // says, but otherwise calculate a default - if (timeoutMillis === undefined) { + // If the API asks us to wait a certain amount of time (capped at 60 seconds), + // just do what it says, but otherwise calculate a default + if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis <= 60 * 1000)) { const maxRetries = options.maxRetries ?? this.maxRetries; timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); } diff --git a/tests/index.test.ts b/tests/index.test.ts index 028eccb17..fde6ee532 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -885,6 +885,47 @@ describe('retries', () => { expect(count).toEqual(3); }); + test.each([ + ['Retry-After', '60'], + ['Retry-After-Ms', '60000'], + ])('retry on 429 honors %s at the 60-second cap', async (headerName, headerValue) => { + jest.useFakeTimers(); + try { + let count = 0; + const testFetch = async ( + url: string | URL | Request, + { signal }: RequestInit = {}, + ): Promise => { + if (count++ === 0) { + return new Response(undefined, { + status: 429, + headers: { + [headerName]: headerValue, + }, + }); + } + return new Response(JSON.stringify({ a: 1 }), { + headers: { 'Content-Type': 'application/json' }, + }); + }; + + const client = new OpenAI({ + apiKey: 'My API Key', + adminAPIKey: 'My Admin API Key', + fetch: testFetch, + }); + + const request = client.request({ path: '/foo', method: 'get' }); + await Promise.resolve(); + await jest.advanceTimersByTimeAsync(60 * 1000); + + await expect(request).resolves.toEqual({ a: 1 }); + expect(count).toEqual(2); + } finally { + jest.useRealTimers(); + } + }); + describe('auth', () => { test('apiKey', async () => { const client = new OpenAI({