Skip to content

Commit 47c79fe

Browse files
fix: fix module not found errors in Vercel edge (#300)
1 parent 5893e37 commit 47c79fe

File tree

10 files changed

+5295
-1470
lines changed

10 files changed

+5295
-1470
lines changed

ecosystem-tests/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ const projects = {
2121
'vercel-edge': async () => {
2222
await installPackage();
2323

24+
if (state.live) {
25+
await run('npm', ['run', 'test:ci:dev']);
26+
}
2427
await run('npm', ['run', 'build']);
2528

29+
if (state.live) {
30+
await run('npm', ['run', 'test:ci']);
31+
}
2632
if (state.deploy) {
2733
await run('npm', ['run', 'vercel', 'deploy', '--prod', '--force']);
2834
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
testMatch: ['<rootDir>/tests/*.ts'],
6+
watchPathIgnorePatterns: ['<rootDir>/node_modules/'],
7+
verbose: false,
8+
testTimeout: 60000,
9+
};

ecosystem-tests/vercel-edge/package-lock.json

Lines changed: 4975 additions & 1439 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ecosystem-tests/vercel-edge/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"start": "next start",
99
"lint": "next lint",
1010
"edge-runtime": "edge-runtime",
11-
"vercel": "vercel"
11+
"vercel": "vercel",
12+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
13+
"test:ci:dev": "start-server-and-test dev http://localhost:3000 test",
14+
"test:ci": "start-server-and-test start http://localhost:3000 test"
1215
},
1316
"dependencies": {
1417
"ai": "2.1.34",
@@ -21,6 +24,10 @@
2124
"@types/react": "18.2.13",
2225
"@types/react-dom": "18.2.6",
2326
"edge-runtime": "^2.4.3",
27+
"fastest-levenshtein": "^1.0.16",
28+
"jest": "^29.5.0",
29+
"start-server-and-test": "^2.0.0",
30+
"ts-jest": "^29.1.0",
2431
"typescript": "4.7.4",
2532
"vercel": "^31.0.0"
2633
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { distance } from 'fastest-levenshtein';
3+
import OpenAI from 'openai';
4+
import { uploadWebApiTestCases } from '../../uploadWebApiTestCases';
5+
6+
export const config = {
7+
runtime: 'edge',
8+
unstable_allowDynamic: [
9+
// This is currently required because `qs` uses `side-channel` which depends on this.
10+
//
11+
// Warning: Some features may be broken at runtime because of this.
12+
'/node_modules/function-bind/**',
13+
],
14+
};
15+
16+
type Test = { description: string; handler: () => Promise<void> };
17+
18+
const tests: Test[] = [];
19+
function it(description: string, handler: () => Promise<void>) {
20+
tests.push({ description, handler });
21+
}
22+
function expectEqual(a: any, b: any) {
23+
if (!Object.is(a, b)) {
24+
throw new Error(`expected values to be equal: ${JSON.stringify({ a, b })}`);
25+
}
26+
}
27+
function expectSimilar(received: string, expected: string, maxDistance: number) {
28+
const receivedDistance = distance(received, expected);
29+
if (receivedDistance < maxDistance) {
30+
return;
31+
}
32+
33+
const message = [
34+
`Received: ${JSON.stringify(received)}`,
35+
`Expected: ${JSON.stringify(expected)}`,
36+
`Max distance: ${maxDistance}`,
37+
`Received distance: ${receivedDistance}`,
38+
].join('\n');
39+
40+
throw new Error(message);
41+
}
42+
43+
export default async (request: NextRequest) => {
44+
try {
45+
console.error('creating client');
46+
const client = new OpenAI();
47+
console.error('created client');
48+
49+
uploadWebApiTestCases({
50+
client: client as any,
51+
it,
52+
expectEqual,
53+
expectSimilar,
54+
runtime: 'edge',
55+
});
56+
57+
let allPassed = true;
58+
const results = [];
59+
60+
for (const { description, handler } of tests) {
61+
console.error('running', description);
62+
let result;
63+
try {
64+
result = await handler();
65+
console.error('passed ', description);
66+
} catch (error) {
67+
console.error('failed ', description, error);
68+
allPassed = false;
69+
result = error instanceof Error ? error.stack : String(error);
70+
}
71+
results.push(`${description}\n\n${String(result)}`);
72+
}
73+
74+
return new NextResponse(allPassed ? 'Passed!' : results.join('\n\n'));
75+
} catch (error) {
76+
console.error(error instanceof Error ? error.stack : String(error));
77+
return new NextResponse(error instanceof Error ? error.stack : String(error), { status: 500 });
78+
}
79+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
import { distance } from 'fastest-levenshtein';
3+
import OpenAI from 'openai';
4+
import { uploadWebApiTestCases } from '../../uploadWebApiTestCases';
5+
6+
type Test = { description: string; handler: () => Promise<void> };
7+
8+
const tests: Test[] = [];
9+
function it(description: string, handler: () => Promise<void>) {
10+
tests.push({ description, handler });
11+
}
12+
function expectEqual(a: any, b: any) {
13+
if (!Object.is(a, b)) {
14+
throw new Error(`expected values to be equal: ${JSON.stringify({ a, b })}`);
15+
}
16+
}
17+
function expectSimilar(received: string, expected: string, maxDistance: number) {
18+
const receivedDistance = distance(received, expected);
19+
if (receivedDistance < maxDistance) {
20+
return;
21+
}
22+
23+
const message = [
24+
`Received: ${JSON.stringify(received)}`,
25+
`Expected: ${JSON.stringify(expected)}`,
26+
`Max distance: ${maxDistance}`,
27+
`Received distance: ${receivedDistance}`,
28+
].join('\n');
29+
30+
throw new Error(message);
31+
}
32+
33+
export default async (request: NextApiRequest, response: NextApiResponse) => {
34+
try {
35+
console.error('creating client');
36+
const client = new OpenAI();
37+
console.error('created client');
38+
39+
uploadWebApiTestCases({
40+
client: client as any,
41+
it,
42+
expectEqual,
43+
expectSimilar,
44+
});
45+
46+
let allPassed = true;
47+
const results = [];
48+
49+
for (const { description, handler } of tests) {
50+
console.error('running', description);
51+
let result;
52+
try {
53+
result = await handler();
54+
console.error('passed ', description);
55+
} catch (error) {
56+
console.error('failed ', description, error);
57+
allPassed = false;
58+
result = error instanceof Error ? error.stack : String(error);
59+
}
60+
results.push(`${description}\n\n${String(result)}`);
61+
}
62+
63+
response.status(200).end(allPassed ? 'Passed!' : results.join('\n\n'));
64+
} catch (error) {
65+
console.error(error instanceof Error ? error.stack : String(error));
66+
response.status(500).end(error instanceof Error ? error.stack : String(error));
67+
}
68+
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import OpenAI, { toFile } from 'openai';
2+
import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions';
3+
4+
/**
5+
* Tests uploads using various Web API data objects.
6+
* This is structured to support running these tests on builtins in the environment in
7+
* Node or Cloudflare workers etc. or on polyfills like from node-fetch/formdata-node
8+
*/
9+
export function uploadWebApiTestCases({
10+
client,
11+
it,
12+
expectEqual,
13+
expectSimilar,
14+
runtime = 'node',
15+
}: {
16+
/**
17+
* OpenAI client instance
18+
*/
19+
client: OpenAI;
20+
/**
21+
* Jest it() function, or an imitation in envs like Cloudflare workers
22+
*/
23+
it: (desc: string, handler: () => Promise<void>) => void;
24+
/**
25+
* Jest expect(a).toEqual(b) function, or an imitation in envs like Cloudflare workers
26+
*/
27+
expectEqual(a: unknown, b: unknown): void;
28+
/**
29+
* Assert that the levenshtein distance between the two given strings is less than the given max distance.
30+
*/
31+
expectSimilar(received: string, expected: string, maxDistance: number): void;
32+
runtime?: 'node' | 'edge';
33+
}) {
34+
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
35+
const filename = 'sample-1.mp3';
36+
37+
const correctAnswer =
38+
'It was anxious to find him no one that expectation of a man who were giving his father enjoyment. But he was avoided in sight in the minister to which indeed,';
39+
const model = 'whisper-1';
40+
41+
async function typeTests() {
42+
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly
43+
await client.audio.transcriptions.create({ file: { foo: true }, model: 'whisper-1' });
44+
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly
45+
await client.audio.transcriptions.create({ file: null, model: 'whisper-1' });
46+
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly
47+
await client.audio.transcriptions.create({ file: 'test', model: 'whisper-1' });
48+
}
49+
50+
it(`streaming works`, async function () {
51+
const stream = await client.chat.completions.create({
52+
model: 'gpt-4',
53+
messages: [{ role: 'user', content: 'Say this is a test' }],
54+
stream: true,
55+
});
56+
const chunks = [];
57+
for await (const part of stream) {
58+
chunks.push(part);
59+
}
60+
expectSimilar(chunks.map((c) => c.choices[0]?.delta.content || '').join(''), 'This is a test', 10);
61+
});
62+
63+
if (runtime !== 'node') {
64+
it('handles File', async () => {
65+
const file = await fetch(url)
66+
.then((x) => x.arrayBuffer())
67+
.then((x) => new File([x], filename));
68+
69+
const params: TranscriptionCreateParams = { file, model };
70+
71+
const result = await client.audio.transcriptions.create(params);
72+
expectSimilar(result.text, correctAnswer, 12);
73+
});
74+
75+
it('handles Response', async () => {
76+
const file = await fetch(url);
77+
78+
const result = await client.audio.transcriptions.create({ file, model });
79+
expectSimilar(result.text, correctAnswer, 12);
80+
});
81+
}
82+
83+
const fineTune = `{"prompt": "<prompt text>", "completion": "<ideal generated text>"}`;
84+
85+
it('toFile handles string', async () => {
86+
// @ts-expect-error we don't type support for `string` to avoid a footgun with passing the file path
87+
const file = await toFile(fineTune, 'finetune.jsonl');
88+
const result = await client.files.create({ file, purpose: 'fine-tune' });
89+
expectEqual(result.status, 'uploaded');
90+
});
91+
it('toFile handles Blob', async () => {
92+
const result = await client.files.create({
93+
file: await toFile(new Blob([fineTune]), 'finetune.jsonl'),
94+
purpose: 'fine-tune',
95+
});
96+
expectEqual(result.status, 'uploaded');
97+
});
98+
it('toFile handles Uint8Array', async () => {
99+
const result = await client.files.create({
100+
file: await toFile(new TextEncoder().encode(fineTune), 'finetune.jsonl'),
101+
purpose: 'fine-tune',
102+
});
103+
expectEqual(result.status, 'uploaded');
104+
});
105+
it('toFile handles ArrayBuffer', async () => {
106+
const result = await client.files.create({
107+
file: await toFile(new TextEncoder().encode(fineTune).buffer, 'finetune.jsonl'),
108+
purpose: 'fine-tune',
109+
});
110+
expectEqual(result.status, 'uploaded');
111+
});
112+
if (runtime !== 'edge') {
113+
// this fails in edge for some reason
114+
it('toFile handles DataView', async () => {
115+
const result = await client.files.create({
116+
file: await toFile(new DataView(new TextEncoder().encode(fineTune).buffer), 'finetune.jsonl'),
117+
purpose: 'fine-tune',
118+
});
119+
expectEqual(result.status, 'uploaded');
120+
});
121+
}
122+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import fetch from 'node-fetch';
2+
3+
const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000';
4+
console.log(baseUrl);
5+
6+
it(
7+
'node runtime',
8+
async () => {
9+
expect(await (await fetch(`${baseUrl}/api/node-test`)).text()).toEqual('Passed!');
10+
},
11+
3 * 60000,
12+
);
13+
14+
it(
15+
'edge runtime',
16+
async () => {
17+
expect(await (await fetch(`${baseUrl}/api/edge-test`)).text()).toEqual('Passed!');
18+
},
19+
3 * 60000,
20+
);

0 commit comments

Comments
 (0)