Skip to content

Commit c2e41fa

Browse files
author
Ethan Arrowood
committed
Replace node-fetch with undici fetch
1 parent 08833f2 commit c2e41fa

File tree

13 files changed

+106
-100
lines changed

13 files changed

+106
-100
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ Request parameters that correspond to file uploads can be passed in many differe
228228

229229
```ts
230230
import fs from 'fs';
231-
import fetch from 'node-fetch';
231+
import { fetch } from 'undici';
232232
import OpenAI, { toFile } from 'openai';
233233

234234
const openai = new OpenAI();

ecosystem-tests/node-ts-cjs/tests/test-node.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import 'openai/shims/node';
22
import OpenAI, { toFile } from 'openai';
33
import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions';
4-
import fetch from 'node-fetch';
54
import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
65
import * as fs from 'fs';
76
import { distance } from 'fastest-levenshtein';
87
import { ChatCompletion } from 'openai/resources/chat/completions';
8+
import { Readable } from 'node:stream';
9+
import { ReadableStream } from 'node:stream/web';
910

1011
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
1112
const filename = 'sample-1.mp3';
@@ -67,10 +68,11 @@ it(`raw response`, async function () {
6768

6869
// test that we can use node-fetch Response API
6970
const chunks: string[] = [];
70-
response.body.on('data', (chunk) => chunks.push(chunk));
71+
const body = Readable.fromWeb(response.body as ReadableStream<any>)
72+
body.on('data', (chunk) => chunks.push(chunk));
7173
await new Promise<void>((resolve, reject) => {
72-
response.body.once('end', resolve);
73-
response.body.once('error', reject);
74+
body.once('end', resolve);
75+
body.once('error', reject);
7476
});
7577
const json: ChatCompletion = JSON.parse(chunks.join(''));
7678
expect(json.choices[0]?.message.content || '').toBeSimilarTo('This is a test', 10);

ecosystem-tests/node-ts-esm-auto/tests/test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
66
import * as fs from 'fs';
77
import { distance } from 'fastest-levenshtein';
88
import { ChatCompletion } from 'openai/resources/chat/completions';
9+
import { Readable } from 'node:stream';
10+
import { ReadableStream } from 'node:stream/web';
911

1012
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
1113
const filename = 'sample-1.mp3';
@@ -67,7 +69,7 @@ it(`raw response`, async function () {
6769

6870
// test that we can use node-fetch Response API
6971
const chunks: string[] = [];
70-
const { body } = response;
72+
const body = Readable.fromWeb(response.body as ReadableStream<any>)
7173
if (!body) throw new Error(`expected response.body to be defined`);
7274
body.on('data', (chunk) => chunks.push(chunk));
7375
await new Promise<void>((resolve, reject) => {

ecosystem-tests/node-ts-esm/tests/test-esnext.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import OpenAI from 'openai';
33
import { distance } from 'fastest-levenshtein';
44
import { ChatCompletion } from 'openai/resources/chat/completions';
55
import * as shims from 'openai/_shims/index';
6+
import { Readable } from 'node:stream';
7+
import { ReadableStream } from 'node:stream/web';
68

79
// The tests in this file don't typecheck with "moduleResolution": "node"
810

@@ -54,7 +56,7 @@ it(`raw response`, async function () {
5456

5557
// test that we can use node-fetch Response API
5658
const chunks: string[] = [];
57-
const { body } = response;
59+
const body = Readable.fromWeb(response.body as ReadableStream<any>)
5860
if (!body) throw new Error(`expected response.body to be defined`);
5961
body.on('data', (chunk) => chunks.push(chunk));
6062
await new Promise<void>((resolve, reject) => {

ecosystem-tests/node-ts4.5-jest27/tests/test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
66
import * as fs from 'fs';
77
import { distance } from 'fastest-levenshtein';
88
import { ChatCompletion } from 'openai/resources/chat/completions';
9+
import { Readable } from 'node:stream';
10+
import { ReadableStream } from 'node:stream/web';
911

1012
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
1113
const filename = 'sample-1.mp3';
@@ -67,10 +69,11 @@ it(`raw response`, async function () {
6769

6870
// test that we can use node-fetch Response API
6971
const chunks: string[] = [];
70-
response.body.on('data', (chunk) => chunks.push(chunk));
72+
const body = Readable.fromWeb(response.body as ReadableStream<any>)
73+
body.on('data', (chunk) => chunks.push(chunk));
7174
await new Promise<void>((resolve, reject) => {
72-
response.body.once('end', resolve);
73-
response.body.once('error', reject);
75+
body.once('end', resolve);
76+
body.once('error', reject);
7477
});
7578
const json: ChatCompletion = JSON.parse(chunks.join(''));
7679
expect(json.choices[0]?.message.content || '').toBeSimilarTo('This is a test', 10);

ecosystem-tests/vercel-edge/tests/test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import fetch from 'node-fetch';
2-
31
const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000';
42
console.log(baseUrl);
53

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"form-data-encoder": "1.7.2",
3030
"formdata-node": "^4.3.2",
3131
"node-fetch": "^2.6.7",
32+
"undici": "^5.28.1",
3233
"web-streams-polyfill": "^3.2.1"
3334
},
3435
"devDependencies": {

src/_shims/node-runtime.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
/**
22
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
33
*/
4-
import * as nf from 'node-fetch';
5-
import * as fd from 'formdata-node';
6-
import { type File, type FilePropertyBag } from 'formdata-node';
7-
import KeepAliveAgent from 'agentkeepalive';
8-
import { AbortController as AbortControllerPolyfill } from 'abort-controller';
9-
import { ReadStream as FsReadStream } from 'node:fs';
10-
import { type Agent } from 'node:http';
4+
import uf from 'undici';
5+
import type { File, Agent, FormData } from 'undici';
6+
import type { FilePropertyBag } from 'formdata-node';
117
import { FormDataEncoder } from 'form-data-encoder';
8+
import { ReadStream as FsReadStream } from 'node:fs';
129
import { Readable } from 'node:stream';
10+
import { ReadableStream } from 'node:stream/web';
11+
import { Blob } from 'node:buffer';
1312
import { type RequestOptions } from '../core';
1413
import { MultipartBody } from './MultipartBody';
1514
import { type Shims } from './registry';
1615

17-
// @ts-ignore (this package does not have proper export maps for this export)
18-
import { ReadableStream } from 'web-streams-polyfill/dist/ponyfill.es2018.js';
19-
2016
type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
2117

2218
let fileFromPathWarned = false;
@@ -40,11 +36,11 @@ async function fileFromPath(path: string, ...args: any[]): Promise<File> {
4036
return await _fileFromPath(path, ...args);
4137
}
4238

43-
const defaultHttpAgent: Agent = new KeepAliveAgent({ keepAlive: true, timeout: 5 * 60 * 1000 });
44-
const defaultHttpsAgent: Agent = new KeepAliveAgent.HttpsAgent({ keepAlive: true, timeout: 5 * 60 * 1000 });
39+
const defaultHttpAgent = new uf.Agent({ keepAliveTimeout: 5 * 60 * 1000 });
40+
const defaultHttpsAgent = new uf.Agent({ keepAliveTimeout: 5 * 60 * 1000 });
4541

4642
async function getMultipartRequestOptions<T extends {} = Record<string, unknown>>(
47-
form: fd.FormData,
43+
form: FormData,
4844
opts: RequestOptions<T>,
4945
): Promise<RequestOptions<T>> {
5046
const encoder = new FormDataEncoder(form);
@@ -67,13 +63,13 @@ export function getRuntime(): Shims {
6763
}
6864
return {
6965
kind: 'node',
70-
fetch: nf.default,
71-
Request: nf.Request,
72-
Response: nf.Response,
73-
Headers: nf.Headers,
74-
FormData: fd.FormData,
75-
Blob: fd.Blob,
76-
File: fd.File,
66+
fetch: uf.fetch,
67+
Request: uf.Request,
68+
Response: uf.Response,
69+
Headers: uf.Headers,
70+
FormData: uf.FormData,
71+
Blob: Blob,
72+
File: uf.File,
7773
ReadableStream,
7874
getMultipartRequestOptions,
7975
getDefaultAgent: (url: string): Agent => (url.startsWith('https') ? defaultHttpsAgent : defaultHttpAgent),

src/_shims/node-types.d.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
/**
22
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
33
*/
4-
import * as nf from 'node-fetch';
5-
import * as fd from 'formdata-node';
4+
import undici from 'undici';
65

76
export { type Agent } from 'node:http';
8-
export { type Readable } from 'node:stream';
7+
export { type ReadableStream } from 'node:stream/web';
98
export { type ReadStream as FsReadStream } from 'node:fs';
10-
export { ReadableStream } from 'web-streams-polyfill';
9+
import { Blob } from 'node:buffer';
1110

12-
export const fetch: typeof nf.default;
11+
export const fetch: typeof undici.fetch;
1312

14-
export type Request = nf.Request;
15-
export type RequestInfo = nf.RequestInfo;
16-
export type RequestInit = nf.RequestInit;
13+
export type Request = undici.Request;
14+
export type RequestInfo = undici.RequestInfo;
15+
export type RequestInit = undici.RequestInit;
1716

18-
export type Response = nf.Response;
19-
export type ResponseInit = nf.ResponseInit;
20-
export type ResponseType = nf.ResponseType;
21-
export type BodyInit = nf.BodyInit;
22-
export type Headers = nf.Headers;
23-
export type HeadersInit = nf.HeadersInit;
17+
export type Response = undici.Response;
18+
export type ResponseInit = undici.ResponseInit;
19+
export type ResponseType = undici.ResponseType;
20+
export type BodyInit = undici.BodyInit;
21+
export type Headers = undici.Headers;
22+
export type HeadersInit = undici.HeadersInit;
2423

2524
type EndingType = 'native' | 'transparent';
2625
export interface BlobPropertyBag {
@@ -34,9 +33,9 @@ export interface FilePropertyBag extends BlobPropertyBag {
3433

3534
export type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
3635

37-
export type FormData = fd.FormData;
38-
export const FormData: typeof fd.FormData;
39-
export type File = fd.File;
40-
export const File: typeof fd.File;
41-
export type Blob = fd.Blob;
42-
export const Blob: typeof fd.Blob;
36+
export type FormData = undici.FormData;
37+
export const FormData: typeof undici.FormData;
38+
export type File = undici.File;
39+
export const File: typeof undici.File;
40+
export type Blob = Blob;
41+
export const Blob: typeof Blob;

src/core.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,6 @@ export abstract class APIClient {
319319
...(body && { body: body as any }),
320320
headers: reqHeaders,
321321
...(httpAgent && { agent: httpAgent }),
322-
// @ts-ignore node-fetch uses a custom AbortSignal type that is
323-
// not compatible with standard web types
324322
signal: options.signal ?? null,
325323
};
326324

0 commit comments

Comments
 (0)