diff --git a/README.md b/README.md index 0f7ea4f..8f2d291 100644 --- a/README.md +++ b/README.md @@ -594,7 +594,18 @@ choco install ffmpeg # Windows `timeoutMs` raises the SDK's timeout, but with `openai` 5 or newer a request still fails after about 5 minutes without response headers: those versions use Node's built-in `fetch`, whose own 300-second limit applies first. - `openai` 4 honours `timeoutMs` beyond that. + `openai` 4 honours `timeoutMs` beyond that. To wait longer on `openai` 5+, pass an undici dispatcher through + `fetchOptions` (`npm i undici`): + + ```js + import { Agent } from 'undici'; + + engines.openai({ + baseUrl: 'http://localhost:8881/v1', + timeoutMs: 1_800_000, + fetchOptions: { dispatcher: new Agent({ headersTimeout: 0, bodyTimeout: 0 }) }, + }) + ``` 2. **Record** — Playwright runs the demo script in a real browser. The `narration` fixture records timestamps for each `mark()` call. Video is captured at native resolution. diff --git a/skills/argo-guide/references/tts-engines.md b/skills/argo-guide/references/tts-engines.md index 7ef85fc..35ed04e 100644 --- a/skills/argo-guide/references/tts-engines.md +++ b/skills/argo-guide/references/tts-engines.md @@ -86,7 +86,7 @@ engines.openai({ ``` - With `baseUrl` set, `OPENAI_API_KEY` is never read — an OpenAI key is not leaked to another server. Pass `apiKey` if the server needs one. -- With `openai` >= 5, requests still fail after ~300 s without response headers (Node `fetch`'s `headersTimeout`), regardless of `timeoutMs`. `openai` 4 honours longer timeouts. +- With `openai` >= 5, requests still fail after ~300 s without response headers (Node `fetch`'s `headersTimeout`), regardless of `timeoutMs`. `openai` 4 honours longer timeouts. On 5+, lift the limit with `fetchOptions: { dispatcher: new Agent({ headersTimeout: 0, bodyTimeout: 0 }) }` (`import { Agent } from 'undici'`). - `baseUrl` is recorded in `.meta.json` but is not part of the clip cache key — clear `.argo//clips` after switching servers. --- diff --git a/src/tts/engines/openai.ts b/src/tts/engines/openai.ts index 3ad641f..6af6a6b 100644 --- a/src/tts/engines/openai.ts +++ b/src/tts/engines/openai.ts @@ -15,9 +15,16 @@ export interface OpenAIEngineOptions { baseUrl?: string; /** * Request timeout in milliseconds, for slow local servers. Default: the OpenAI SDK's (10 minutes). - * With `openai` >= 5, Node's built-in `fetch` still gives up after ~300 s without response headers. + * With `openai` >= 5, Node's built-in `fetch` still gives up after ~300 s without response headers; + * lift that with `fetchOptions`. */ timeoutMs?: number; + /** + * Extra options merged into every `fetch` call the SDK makes (`openai` >= 5). To wait longer than ~300 s, + * pass an undici dispatcher with a larger `headersTimeout`: + * `fetchOptions: { dispatcher: new Agent({ headersTimeout: 0, bodyTimeout: 0 }) }` (`import { Agent } from 'undici'`). + */ + fetchOptions?: Record; } export class OpenAIEngine implements TTSEngine { @@ -26,6 +33,7 @@ export class OpenAIEngine implements TTSEngine { private instructions?: string; private baseUrl?: string; private timeoutMs?: number; + private fetchOptions?: Record; constructor(options?: OpenAIEngineOptions) { this.apiKey = options?.apiKey ?? ''; @@ -33,6 +41,7 @@ export class OpenAIEngine implements TTSEngine { this.instructions = options?.instructions; this.baseUrl = options?.baseUrl; this.timeoutMs = options?.timeoutMs; + this.fetchOptions = options?.fetchOptions; } describe(): TTSEngineMetadata { @@ -67,6 +76,7 @@ export class OpenAIEngine implements TTSEngine { apiKey: this.resolveApiKey(), ...(this.baseUrl ? { baseURL: this.baseUrl } : {}), ...(this.timeoutMs != null ? { timeout: this.timeoutMs } : {}), + ...(this.fetchOptions ? { fetchOptions: this.fetchOptions } : {}), }); // Request raw PCM so we can build an exact WAV without ffmpeg pipe artifacts diff --git a/tests/tts/openai.test.ts b/tests/tts/openai.test.ts index 5e4c484..37d1758 100644 --- a/tests/tts/openai.test.ts +++ b/tests/tts/openai.test.ts @@ -50,6 +50,12 @@ describe('OpenAIEngine', () => { }); }); + it('passes fetchOptions through to the client', async () => { + const dispatcher = { tag: 'agent' }; + await new OpenAIEngine({ baseUrl: 'http://localhost:8881/v1', fetchOptions: { dispatcher } }).generate('Hello', {}); + expect(clientOptions.mock.calls[0][0].fetchOptions).toEqual({ dispatcher }); + }); + it('never sends OPENAI_API_KEY to another server', async () => { process.env.OPENAI_API_KEY = 'sk-env'; await new OpenAIEngine({ baseUrl: 'http://localhost:8881/v1' }).generate('Hello', {});