Skip to content

Commit 49101a0

Browse files
committed
feat(client): support importing node or web shims manually (#325)
1 parent 0386937 commit 49101a0

File tree

147 files changed

+12522
-1325
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+12522
-1325
lines changed

.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ CHANGELOG.md
22
/ecosystem-tests
33
/node_modules
44
/deno
5+
6+
# don't format tsc output, will break source maps
7+
/dist

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -318,5 +318,9 @@ The following runtimes are supported:
318318
- Bun 1.0 or later.
319319
- Cloudflare Workers.
320320
- Vercel Edge Runtime.
321+
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
322+
- Nitro v2.6 or greater.
323+
324+
Note that React Native is not supported at this time.
321325

322326
If you are interested in other runtime environments, please open or upvote an issue on GitHub.

build

+4-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ rm -rf dist; mkdir dist
1212
# Copy src to dist/src and build from dist/src into dist, so that
1313
# the source map for index.js.map will refer to ./src/index.ts etc
1414
cp -rp src README.md dist
15-
rm dist/src/_shims/*-deno.*
15+
rm dist/src/_shims/*-deno.ts dist/src/_shims/auto/*-deno.ts
1616
for file in LICENSE CHANGELOG.md; do
1717
if [ -e "${file}" ]; then cp "${file}" dist; fi
1818
done
@@ -27,8 +27,8 @@ node scripts/make-dist-package-json.cjs > dist/package.json
2727
# build to .js/.mjs/.d.ts files
2828
npm exec tsc-multi
2929
# copy over handwritten .js/.mjs/.d.ts files
30-
cp src/_shims/*.{d.ts,js,mjs} dist/_shims
31-
npm exec tsc-alias -- -p tsconfig.build.json
30+
cp src/_shims/*.{d.ts,js,mjs,md} dist/_shims
31+
cp src/_shims/auto/*.{d.ts,js,mjs} dist/_shims/auto
3232
# we need to add exports = module.exports = OpenAI Node to index.js;
3333
# No way to get that from index.ts because it would cause compile errors
3434
# when building .mjs
@@ -40,14 +40,7 @@ node scripts/fix-index-exports.cjs
4040
cp dist/index.d.ts dist/index.d.mts
4141
cp tsconfig.dist-src.json dist/src/tsconfig.json
4242

43-
# strip out lib="dom" and types="node" references; these are needed at build time,
44-
# but would pollute the user's TS environment
45-
find dist -type f -exec node scripts/remove-triple-slash-references.js {} +
46-
# strip out `unknown extends RequestInit ? never :` from dist/src/_shims;
47-
# these cause problems when viewing the .ts source files in go to definition
48-
find dist/src/_shims -type f -exec node scripts/replace-shim-guards.js {} +
49-
50-
npm exec prettier -- --loglevel=warn --write .
43+
node scripts/postprocess-files.cjs
5144

5245
# make sure that nothing crashes when we require the output CJS or
5346
# import the output ESM

ecosystem-tests/bun/openai.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import OpenAI, { toFile } from 'openai';
22
import fs from 'fs';
33
import { distance } from 'fastest-levenshtein';
44
import { test, expect } from 'bun:test';
5+
import { ChatCompletion } from 'openai/resources/chat/completions';
56

67
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
78
const filename = 'sample-1.mp3';
@@ -42,6 +43,39 @@ test(`basic request works`, async function () {
4243
expectSimilar(completion.choices[0]?.message?.content, 'This is a test', 10);
4344
});
4445

46+
test(`raw response`, async function () {
47+
const response = await client.chat.completions
48+
.create({
49+
model: 'gpt-4',
50+
messages: [{ role: 'user', content: 'Say this is a test' }],
51+
})
52+
.asResponse();
53+
54+
// test that we can use web Response API
55+
const { body } = response;
56+
if (!body) throw new Error('expected response.body to be defined');
57+
58+
const reader = body.getReader();
59+
const chunks: Uint8Array[] = [];
60+
let result;
61+
do {
62+
result = await reader.read();
63+
if (!result.done) chunks.push(result.value);
64+
} while (!result.done);
65+
66+
reader.releaseLock();
67+
68+
let offset = 0;
69+
const merged = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
70+
for (const chunk of chunks) {
71+
merged.set(chunk, offset);
72+
offset += chunk.length;
73+
}
74+
75+
const json: ChatCompletion = JSON.parse(new TextDecoder().decode(merged));
76+
expectSimilar(json.choices[0]?.message.content || '', 'This is a test', 10);
77+
});
78+
4579
test(`streaming works`, async function () {
4680
const stream = await client.chat.completions.create({
4781
model: 'gpt-4',

ecosystem-tests/bun/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"include": ["*.ts"],
23
"compilerOptions": {
34
"lib": ["ESNext"],
45
"module": "esnext",
@@ -8,7 +9,7 @@
89
"allowImportingTsExtensions": true,
910
"strict": true,
1011
"downlevelIteration": true,
11-
"skipLibCheck": true,
12+
"skipLibCheck": false,
1213
"jsx": "preserve",
1314
"allowSyntheticDefaultImports": true,
1415
"forceConsistentCasingInFileNames": true,

ecosystem-tests/cli.ts

+35-43
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ import path from 'path';
77
const TAR_NAME = 'openai.tgz';
88
const PACK_FILE = `.pack/${TAR_NAME}`;
99

10+
async function defaultNodeRunner() {
11+
await installPackage();
12+
await run('npm', ['run', 'tsc']);
13+
if (state.live) await run('npm', ['test']);
14+
}
15+
1016
const projects = {
17+
'node-ts-cjs': defaultNodeRunner,
18+
'node-ts-cjs-web': defaultNodeRunner,
19+
'node-ts-cjs-auto': defaultNodeRunner,
20+
'node-ts4.5-jest27': defaultNodeRunner,
21+
'node-ts-esm': defaultNodeRunner,
22+
'node-ts-esm-web': defaultNodeRunner,
23+
'node-ts-esm-auto': defaultNodeRunner,
1124
'ts-browser-webpack': async () => {
1225
await installPackage();
1326

@@ -45,36 +58,6 @@ const projects = {
4558
await run('npm', ['run', 'deploy']);
4659
}
4760
},
48-
'node-ts-cjs': async () => {
49-
await installPackage();
50-
51-
await run('npm', ['run', 'tsc']);
52-
53-
if (state.live) {
54-
await run('npm', ['test']);
55-
}
56-
},
57-
'node-ts-cjs-ts4.5': async () => {
58-
await installPackage();
59-
await run('npm', ['run', 'tsc']);
60-
},
61-
'node-ts-cjs-dom': async () => {
62-
await installPackage();
63-
await run('npm', ['run', 'tsc']);
64-
},
65-
'node-ts-esm': async () => {
66-
await installPackage();
67-
68-
await run('npm', ['run', 'tsc']);
69-
70-
if (state.live) {
71-
await run('npm', ['run', 'test']);
72-
}
73-
},
74-
'node-ts-esm-dom': async () => {
75-
await installPackage();
76-
await run('npm', ['run', 'tsc']);
77-
},
7861
bun: async () => {
7962
if (state.fromNpm) {
8063
await run('bun', ['install', '-D', state.fromNpm]);
@@ -116,6 +99,7 @@ const projects = {
11699
};
117100

118101
const projectNames = Object.keys(projects) as Array<keyof typeof projects>;
102+
const projectNamesSet = new Set(projectNames);
119103

120104
function parseArgs() {
121105
return yargs(process.argv.slice(2))
@@ -189,10 +173,13 @@ async function main() {
189173
await buildPackage();
190174
}
191175

176+
const positionalArgs = args._.filter(Boolean);
177+
192178
// For some reason `yargs` doesn't pick up the positional args correctly
193179
const projectsToRun = (
194180
args.projects?.length ? args.projects
195-
: args._.length ? args._
181+
: positionalArgs.length ?
182+
positionalArgs.filter((n) => typeof n === 'string' && (projectNamesSet as Set<string>).has(n))
196183
: projectNames) as typeof projectNames;
197184
console.error(`running projects: ${projectsToRun}`);
198185

@@ -234,10 +221,11 @@ async function main() {
234221
const project = queue.shift();
235222
if (!project) break;
236223

237-
let stdout, stderr;
224+
// preserve interleaved ordering of writes to stdout/stderr
225+
const chunks: { dest: 'stdout' | 'stderr'; data: string | Buffer }[] = [];
238226
try {
239227
runningProjects.add(project);
240-
const result = await execa(
228+
const child = execa(
241229
'yarn',
242230
[
243231
'tsn',
@@ -252,16 +240,19 @@ async function main() {
252240
],
253241
{ stdio: 'pipe', encoding: 'utf8', maxBuffer: 100 * 1024 * 1024 },
254242
);
255-
({ stdout, stderr } = result);
243+
child.stdout?.on('data', (data) => chunks.push({ dest: 'stdout', data }));
244+
child.stderr?.on('data', (data) => chunks.push({ dest: 'stderr', data }));
245+
await child;
256246
} catch (error) {
257-
({ stdout, stderr } = error as any);
258247
failed.push(project);
259248
} finally {
260249
runningProjects.delete(project);
261250
}
262251

263-
if (stdout) process.stdout.write(stdout);
264-
if (stderr) process.stderr.write(stderr);
252+
for (const { dest, data } of chunks) {
253+
if (dest === 'stdout') process.stdout.write(data);
254+
else process.stderr.write(data);
255+
}
265256
}
266257
}),
267258
);
@@ -274,18 +265,21 @@ async function main() {
274265

275266
await withChdir(path.join(rootDir, 'ecosystem-tests', project), async () => {
276267
console.error('\n');
277-
console.error(banner(project));
268+
console.error(banner(`▶️ ${project}`));
278269
console.error('\n');
279270

280271
try {
281272
await withRetry(fn, project, state.retry);
282-
console.error(`✅ - Successfully ran ${project}`);
273+
console.error('\n');
274+
console.error(banner(`✅ ${project}`));
283275
} catch (err) {
284276
if (err && (err as any).shortMessage) {
285-
console.error('❌', (err as any).shortMessage);
277+
console.error((err as any).shortMessage);
286278
} else {
287-
console.error('❌', err);
279+
console.error(err);
288280
}
281+
console.error('\n');
282+
console.error(banner(`❌ ${project}`));
289283
failed.push(project);
290284
}
291285
console.error('\n');
@@ -331,8 +325,6 @@ async function buildPackage() {
331325
return;
332326
}
333327

334-
await run('yarn', ['build']);
335-
336328
if (!(await pathExists('.pack'))) {
337329
await fs.mkdir('.pack');
338330
}

ecosystem-tests/cloudflare-worker/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"deploy": "wrangler publish",
99
"start": "wrangler dev",
1010
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
11-
"test:ci": "WAIT_ON_INTERVAL=10000 start-server-and-test start http://localhost:8787 test"
11+
"test:ci": "start-server-and-test start http://localhost:8787 test"
1212
},
1313
"devDependencies": {
1414
"@cloudflare/workers-types": "^4.20230419.0",

ecosystem-tests/cloudflare-worker/src/uploadWebApiTestCases.ts

+34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import OpenAI, { toFile } from 'openai';
22
import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions';
3+
import { ChatCompletion } from 'openai/resources/chat/completions';
34

45
/**
56
* Tests uploads using various Web API data objects.
@@ -45,6 +46,39 @@ export function uploadWebApiTestCases({
4546
await client.audio.transcriptions.create({ file: 'test', model: 'whisper-1' });
4647
}
4748

49+
it(`raw response`, async function () {
50+
const response = await client.chat.completions
51+
.create({
52+
model: 'gpt-4',
53+
messages: [{ role: 'user', content: 'Say this is a test' }],
54+
})
55+
.asResponse();
56+
57+
// test that we can use web Response API
58+
const { body } = response;
59+
if (!body) throw new Error('expected response.body to be defined');
60+
61+
const reader = body.getReader();
62+
const chunks: Uint8Array[] = [];
63+
let result;
64+
do {
65+
result = await reader.read();
66+
if (!result.done) chunks.push(result.value);
67+
} while (!result.done);
68+
69+
reader.releaseLock();
70+
71+
let offset = 0;
72+
const merged = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
73+
for (const chunk of chunks) {
74+
merged.set(chunk, offset);
75+
offset += chunk.length;
76+
}
77+
78+
const json: ChatCompletion = JSON.parse(new TextDecoder().decode(merged));
79+
expectSimilar(json.choices[0]?.message.content || '', 'This is a test', 10);
80+
});
81+
4882
it(`streaming works`, async function () {
4983
const stream = await client.chat.completions.create({
5084
model: 'gpt-4',

ecosystem-tests/cloudflare-worker/src/worker.ts

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ type Test = { description: string; handler: () => Promise<void> };
3333

3434
export default {
3535
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
36+
const url = new URL(request.url);
37+
// start-server-and-test polls / to see if the server is up and running
38+
if (url.pathname === '/') return new Response();
39+
// then the test code requests /test
40+
if (url.pathname !== '/test') return new Response(null, { status: 404 });
3641
try {
3742
console.error('importing openai');
3843
const { default: OpenAI } = await import('openai');

ecosystem-tests/cloudflare-worker/tests/test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fetch from 'node-fetch';
33
it(
44
'works',
55
async () => {
6-
expect(await (await fetch('http://localhost:8787')).text()).toEqual('Passed!');
6+
expect(await (await fetch('http://localhost:8787/test')).text()).toEqual('Passed!');
77
},
88
3 * 60000
99
);

ecosystem-tests/cloudflare-worker/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"include": ["src/*.ts"],
23
"compilerOptions": {
34
/* Visit https://aka.ms/tsconfig.json to read more about this file */
45

0 commit comments

Comments
 (0)