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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 4 additions & 11 deletions
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

Lines changed: 34 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 35 additions & 43 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 34 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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
);

0 commit comments

Comments
 (0)