Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ const client = new OpenAI({ fetch });

### Fetch options

If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.)
If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.) These options are forwarded to the active `fetch` implementation unchanged, so runtime-specific fields such as Undici's `dispatcher` only work when the underlying `fetch` supports them.

```ts
import OpenAI from 'openai';
Expand Down Expand Up @@ -652,6 +652,34 @@ const client = new OpenAI({
});
```

#### Custom CA certificates in runtimes that patch `fetch`

Some server runtimes wrap or replace `globalThis.fetch`. When that happens, Undici-specific `fetchOptions` such as `dispatcher` may be ignored even in Node.js. If you need a custom CA bundle for enterprise TLS interception or private PKI, pass a custom `fetch` that calls `undici.fetch` directly instead of relying on the patched global `fetch`.

```ts
import fs from 'node:fs';
import tls from 'node:tls';
import OpenAI from 'openai';
import { Agent, fetch as undiciFetch } from 'undici';

const extraCAPath = process.env.NODE_EXTRA_CA_CERTS;
const extraCA = extraCAPath ? fs.readFileSync(extraCAPath, 'utf8') : undefined;

const dispatcher =
extraCA ?
new Agent({
connect: {
// Supplying `ca` replaces the default trust store, so keep Node's roots too.
ca: [...tls.rootCertificates, extraCA],
},
})
: undefined;

const client = new OpenAI({
fetch: dispatcher ? (url, init) => undiciFetch(url, { ...init, dispatcher }) : undefined,
});
```

## Frequently Asked Questions

## Semantic versioning
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export class OpenAI {
* @param {string | null | undefined} [opts.webhookSecret=process.env['OPENAI_WEBHOOK_SECRET'] ?? null]
* @param {string} [opts.baseURL=process.env['OPENAI_BASE_URL'] ?? https://api.openai.com/v1] - Override the default base URL for the API.
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options forwarded to the active `fetch` implementation.
* @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
* @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API.
Expand Down
30 changes: 30 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,36 @@ describe('instantiate client', () => {
expect(capturedRequest?.method).toEqual('PATCH');
});

test('passes merged fetchOptions through to custom fetch', async () => {
let capturedRequest: RequestInit | undefined;
const clientDispatcher = { scope: 'client' } as any;
const requestDispatcher = { scope: 'request' } as any;

const client = new OpenAI({
baseURL: 'http://localhost:5000/',
apiKey: 'My API Key',
fetchOptions: { cache: 'no-store', dispatcher: clientDispatcher } as any,
fetch: async (url: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
capturedRequest = init;
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
});
},
});

await client.get('/foo', {
fetchOptions: { dispatcher: requestDispatcher, integrity: 'request-integrity' } as any,
});

expect(capturedRequest).toMatchObject({
method: 'GET',
cache: 'no-store',
integrity: 'request-integrity',
dispatcher: requestDispatcher,
});
expect((capturedRequest as any)?.dispatcher).not.toBe(clientDispatcher);
});

describe('baseUrl', () => {
test('trailing slash', () => {
const client = new OpenAI({ baseURL: 'http://localhost:5000/custom/path/', apiKey: 'My API Key' });
Expand Down