-
Notifications
You must be signed in to change notification settings - Fork 883
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: fix module not found errors in Vercel edge (#300)
- Loading branch information
1 parent
5893e37
commit 47c79fe
Showing
10 changed files
with
5,295 additions
and
1,470 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
testMatch: ['<rootDir>/tests/*.ts'], | ||
watchPathIgnorePatterns: ['<rootDir>/node_modules/'], | ||
verbose: false, | ||
testTimeout: 60000, | ||
}; |
6,414 changes: 4,975 additions & 1,439 deletions
6,414
ecosystem-tests/vercel-edge/package-lock.json
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import { distance } from 'fastest-levenshtein'; | ||
import OpenAI from 'openai'; | ||
import { uploadWebApiTestCases } from '../../uploadWebApiTestCases'; | ||
|
||
export const config = { | ||
runtime: 'edge', | ||
unstable_allowDynamic: [ | ||
// This is currently required because `qs` uses `side-channel` which depends on this. | ||
// | ||
// Warning: Some features may be broken at runtime because of this. | ||
'/node_modules/function-bind/**', | ||
], | ||
}; | ||
|
||
type Test = { description: string; handler: () => Promise<void> }; | ||
|
||
const tests: Test[] = []; | ||
function it(description: string, handler: () => Promise<void>) { | ||
tests.push({ description, handler }); | ||
} | ||
function expectEqual(a: any, b: any) { | ||
if (!Object.is(a, b)) { | ||
throw new Error(`expected values to be equal: ${JSON.stringify({ a, b })}`); | ||
} | ||
} | ||
function expectSimilar(received: string, expected: string, maxDistance: number) { | ||
const receivedDistance = distance(received, expected); | ||
if (receivedDistance < maxDistance) { | ||
return; | ||
} | ||
|
||
const message = [ | ||
`Received: ${JSON.stringify(received)}`, | ||
`Expected: ${JSON.stringify(expected)}`, | ||
`Max distance: ${maxDistance}`, | ||
`Received distance: ${receivedDistance}`, | ||
].join('\n'); | ||
|
||
throw new Error(message); | ||
} | ||
|
||
export default async (request: NextRequest) => { | ||
try { | ||
console.error('creating client'); | ||
const client = new OpenAI(); | ||
console.error('created client'); | ||
|
||
uploadWebApiTestCases({ | ||
client: client as any, | ||
it, | ||
expectEqual, | ||
expectSimilar, | ||
runtime: 'edge', | ||
}); | ||
|
||
let allPassed = true; | ||
const results = []; | ||
|
||
for (const { description, handler } of tests) { | ||
console.error('running', description); | ||
let result; | ||
try { | ||
result = await handler(); | ||
console.error('passed ', description); | ||
} catch (error) { | ||
console.error('failed ', description, error); | ||
allPassed = false; | ||
result = error instanceof Error ? error.stack : String(error); | ||
} | ||
results.push(`${description}\n\n${String(result)}`); | ||
} | ||
|
||
return new NextResponse(allPassed ? 'Passed!' : results.join('\n\n')); | ||
} catch (error) { | ||
console.error(error instanceof Error ? error.stack : String(error)); | ||
return new NextResponse(error instanceof Error ? error.stack : String(error), { status: 500 }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
import { distance } from 'fastest-levenshtein'; | ||
import OpenAI from 'openai'; | ||
import { uploadWebApiTestCases } from '../../uploadWebApiTestCases'; | ||
|
||
type Test = { description: string; handler: () => Promise<void> }; | ||
|
||
const tests: Test[] = []; | ||
function it(description: string, handler: () => Promise<void>) { | ||
tests.push({ description, handler }); | ||
} | ||
function expectEqual(a: any, b: any) { | ||
if (!Object.is(a, b)) { | ||
throw new Error(`expected values to be equal: ${JSON.stringify({ a, b })}`); | ||
} | ||
} | ||
function expectSimilar(received: string, expected: string, maxDistance: number) { | ||
const receivedDistance = distance(received, expected); | ||
if (receivedDistance < maxDistance) { | ||
return; | ||
} | ||
|
||
const message = [ | ||
`Received: ${JSON.stringify(received)}`, | ||
`Expected: ${JSON.stringify(expected)}`, | ||
`Max distance: ${maxDistance}`, | ||
`Received distance: ${receivedDistance}`, | ||
].join('\n'); | ||
|
||
throw new Error(message); | ||
} | ||
|
||
export default async (request: NextApiRequest, response: NextApiResponse) => { | ||
try { | ||
console.error('creating client'); | ||
const client = new OpenAI(); | ||
console.error('created client'); | ||
|
||
uploadWebApiTestCases({ | ||
client: client as any, | ||
it, | ||
expectEqual, | ||
expectSimilar, | ||
}); | ||
|
||
let allPassed = true; | ||
const results = []; | ||
|
||
for (const { description, handler } of tests) { | ||
console.error('running', description); | ||
let result; | ||
try { | ||
result = await handler(); | ||
console.error('passed ', description); | ||
} catch (error) { | ||
console.error('failed ', description, error); | ||
allPassed = false; | ||
result = error instanceof Error ? error.stack : String(error); | ||
} | ||
results.push(`${description}\n\n${String(result)}`); | ||
} | ||
|
||
response.status(200).end(allPassed ? 'Passed!' : results.join('\n\n')); | ||
} catch (error) { | ||
console.error(error instanceof Error ? error.stack : String(error)); | ||
response.status(500).end(error instanceof Error ? error.stack : String(error)); | ||
} | ||
}; |
122 changes: 122 additions & 0 deletions
122
ecosystem-tests/vercel-edge/src/uploadWebApiTestCases.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import OpenAI, { toFile } from 'openai'; | ||
import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; | ||
|
||
/** | ||
* Tests uploads using various Web API data objects. | ||
* This is structured to support running these tests on builtins in the environment in | ||
* Node or Cloudflare workers etc. or on polyfills like from node-fetch/formdata-node | ||
*/ | ||
export function uploadWebApiTestCases({ | ||
client, | ||
it, | ||
expectEqual, | ||
expectSimilar, | ||
runtime = 'node', | ||
}: { | ||
/** | ||
* OpenAI client instance | ||
*/ | ||
client: OpenAI; | ||
/** | ||
* Jest it() function, or an imitation in envs like Cloudflare workers | ||
*/ | ||
it: (desc: string, handler: () => Promise<void>) => void; | ||
/** | ||
* Jest expect(a).toEqual(b) function, or an imitation in envs like Cloudflare workers | ||
*/ | ||
expectEqual(a: unknown, b: unknown): void; | ||
/** | ||
* Assert that the levenshtein distance between the two given strings is less than the given max distance. | ||
*/ | ||
expectSimilar(received: string, expected: string, maxDistance: number): void; | ||
runtime?: 'node' | 'edge'; | ||
}) { | ||
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; | ||
const filename = 'sample-1.mp3'; | ||
|
||
const correctAnswer = | ||
'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,'; | ||
const model = 'whisper-1'; | ||
|
||
async function typeTests() { | ||
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly | ||
await client.audio.transcriptions.create({ file: { foo: true }, model: 'whisper-1' }); | ||
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly | ||
await client.audio.transcriptions.create({ file: null, model: 'whisper-1' }); | ||
// @ts-expect-error this should error if the `Uploadable` type was resolved correctly | ||
await client.audio.transcriptions.create({ file: 'test', model: 'whisper-1' }); | ||
} | ||
|
||
it(`streaming works`, async function () { | ||
const stream = await client.chat.completions.create({ | ||
model: 'gpt-4', | ||
messages: [{ role: 'user', content: 'Say this is a test' }], | ||
stream: true, | ||
}); | ||
const chunks = []; | ||
for await (const part of stream) { | ||
chunks.push(part); | ||
} | ||
expectSimilar(chunks.map((c) => c.choices[0]?.delta.content || '').join(''), 'This is a test', 10); | ||
}); | ||
|
||
if (runtime !== 'node') { | ||
it('handles File', async () => { | ||
const file = await fetch(url) | ||
.then((x) => x.arrayBuffer()) | ||
.then((x) => new File([x], filename)); | ||
|
||
const params: TranscriptionCreateParams = { file, model }; | ||
|
||
const result = await client.audio.transcriptions.create(params); | ||
expectSimilar(result.text, correctAnswer, 12); | ||
}); | ||
|
||
it('handles Response', async () => { | ||
const file = await fetch(url); | ||
|
||
const result = await client.audio.transcriptions.create({ file, model }); | ||
expectSimilar(result.text, correctAnswer, 12); | ||
}); | ||
} | ||
|
||
const fineTune = `{"prompt": "<prompt text>", "completion": "<ideal generated text>"}`; | ||
|
||
it('toFile handles string', async () => { | ||
// @ts-expect-error we don't type support for `string` to avoid a footgun with passing the file path | ||
const file = await toFile(fineTune, 'finetune.jsonl'); | ||
const result = await client.files.create({ file, purpose: 'fine-tune' }); | ||
expectEqual(result.status, 'uploaded'); | ||
}); | ||
it('toFile handles Blob', async () => { | ||
const result = await client.files.create({ | ||
file: await toFile(new Blob([fineTune]), 'finetune.jsonl'), | ||
purpose: 'fine-tune', | ||
}); | ||
expectEqual(result.status, 'uploaded'); | ||
}); | ||
it('toFile handles Uint8Array', async () => { | ||
const result = await client.files.create({ | ||
file: await toFile(new TextEncoder().encode(fineTune), 'finetune.jsonl'), | ||
purpose: 'fine-tune', | ||
}); | ||
expectEqual(result.status, 'uploaded'); | ||
}); | ||
it('toFile handles ArrayBuffer', async () => { | ||
const result = await client.files.create({ | ||
file: await toFile(new TextEncoder().encode(fineTune).buffer, 'finetune.jsonl'), | ||
purpose: 'fine-tune', | ||
}); | ||
expectEqual(result.status, 'uploaded'); | ||
}); | ||
if (runtime !== 'edge') { | ||
// this fails in edge for some reason | ||
it('toFile handles DataView', async () => { | ||
const result = await client.files.create({ | ||
file: await toFile(new DataView(new TextEncoder().encode(fineTune).buffer), 'finetune.jsonl'), | ||
purpose: 'fine-tune', | ||
}); | ||
expectEqual(result.status, 'uploaded'); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import fetch from 'node-fetch'; | ||
|
||
const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000'; | ||
console.log(baseUrl); | ||
|
||
it( | ||
'node runtime', | ||
async () => { | ||
expect(await (await fetch(`${baseUrl}/api/node-test`)).text()).toEqual('Passed!'); | ||
}, | ||
3 * 60000, | ||
); | ||
|
||
it( | ||
'edge runtime', | ||
async () => { | ||
expect(await (await fetch(`${baseUrl}/api/edge-test`)).text()).toEqual('Passed!'); | ||
}, | ||
3 * 60000, | ||
); |
Oops, something went wrong.