diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 50caff538..449c4778f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -65,3 +65,36 @@ jobs:
- name: Run tests
run: ./scripts/test
+
+ ecosystem_tests:
+ name: ecosystem tests (v${{ matrix.node-version }})
+ runs-on: ubuntu-latest
+ if: github.repository == 'openai/openai-node'
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: ['18', '20']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '${{ matrix.node-version }}'
+
+ - uses: denoland/setup-deno@v1
+ with:
+ deno-version: v1.39.0
+
+ - uses: oven-sh/setup-bun@v2
+
+ - name: Bootstrap
+ run: ./scripts/bootstrap
+
+ - name: Run ecosystem tests
+ run: |
+ yarn tsn ecosystem-tests/cli.ts --live --verbose --parallel --jobs=4 --retry=3
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b33fcdcea..721222a67 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "5.0.0-alpha.0"
+ ".": "5.0.0-beta.0"
}
diff --git a/README.md b/README.md
index 2b85f3553..08226e928 100644
--- a/README.md
+++ b/README.md
@@ -287,8 +287,6 @@ main();
Like with `.stream()`, we provide a variety of [helpers and events](helpers.md#chat-events).
-Note that `runFunctions` was previously available as well, but has been deprecated in favor of `runTools`.
-
Read more about various examples such as with integrating with [zod](helpers.md#integrate-with-zod),
[next.js](helpers.md#integrate-with-nextjs), and [proxying a stream to the browser](helpers.md#proxy-streaming-to-a-browser).
diff --git a/api.md b/api.md
index 5b5c603b7..978bff992 100644
--- a/api.md
+++ b/api.md
@@ -347,7 +347,6 @@ Methods:
Methods:
-- client.beta.chat.completions.runFunctions(body, options?) -> ChatCompletionRunner | ChatCompletionStreamingRunner
- client.beta.chat.completions.runTools(body, options?) -> ChatCompletionRunner | ChatCompletionStreamingRunner
- client.beta.chat.completions.stream(body, options?) -> ChatCompletionStream
diff --git a/ecosystem-tests/bun/bun.lockb b/ecosystem-tests/bun/bun.lockb
index 529d137af..a01f1617e 100755
Binary files a/ecosystem-tests/bun/bun.lockb and b/ecosystem-tests/bun/bun.lockb differ
diff --git a/ecosystem-tests/bun/openai.test.ts b/ecosystem-tests/bun/openai.test.ts
index 979a4962f..f8facc521 100644
--- a/ecosystem-tests/bun/openai.test.ts
+++ b/ecosystem-tests/bun/openai.test.ts
@@ -43,6 +43,20 @@ test(`basic request works`, async function () {
expectSimilar(completion.choices[0]?.message?.content, 'This is a test', 10);
});
+test(`proxied request works`, async function () {
+ const client = new OpenAI({
+ fetchOptions: {
+ proxy: process.env['ECOSYSTEM_TESTS_PROXY'],
+ },
+ });
+
+ const completion = await client.chat.completions.create({
+ model: 'gpt-4',
+ messages: [{ role: 'user', content: 'Say this is a test' }],
+ });
+ expectSimilar(completion.choices[0]?.message?.content, 'This is a test', 10);
+});
+
test(`raw response`, async function () {
const response = await client.chat.completions
.create({
@@ -117,6 +131,14 @@ test('handles fs.ReadStream', async function () {
expectSimilar(result.text, correctAnswer, 12);
});
+test('handles Bun.File', async function () {
+ const result = await client.audio.transcriptions.create({
+ file: Bun.file('sample1.mp3'),
+ model,
+ });
+ expectSimilar(result.text, correctAnswer, 12);
+});
+
const fineTune = `{"prompt": "", "completion": ""}`;
// @ts-ignore avoid DOM lib for testing purposes
diff --git a/ecosystem-tests/bun/tsconfig.json b/ecosystem-tests/bun/tsconfig.json
index 69a17ce99..f428d6b10 100644
--- a/ecosystem-tests/bun/tsconfig.json
+++ b/ecosystem-tests/bun/tsconfig.json
@@ -9,7 +9,7 @@
"allowImportingTsExtensions": true,
"strict": true,
"downlevelIteration": true,
- "skipLibCheck": false,
+ "skipLibCheck": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
diff --git a/ecosystem-tests/cli.ts b/ecosystem-tests/cli.ts
index d4fa48b0d..9781c2b03 100644
--- a/ecosystem-tests/cli.ts
+++ b/ecosystem-tests/cli.ts
@@ -3,6 +3,8 @@ import execa from 'execa';
import yargs from 'yargs';
import assert from 'assert';
import path from 'path';
+import { createServer } from 'http';
+import { connect } from 'net';
const TAR_NAME = 'openai.tgz';
const PACK_FOLDER = '.pack';
@@ -109,6 +111,35 @@ const projectRunners = {
let projectNames = Object.keys(projectRunners) as Array;
const projectNamesSet = new Set(projectNames);
+async function startProxy() {
+ const proxy = createServer((_req, res) => {
+ res.end();
+ });
+
+ proxy.on('connect', (req, clientSocket, head) => {
+ console.log('got proxied connection');
+ const serverSocket = connect(443, 'api.openai.com', () => {
+ clientSocket.write(
+ 'HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + '\r\n',
+ );
+ serverSocket.write(head);
+ serverSocket.pipe(clientSocket);
+ clientSocket.pipe(serverSocket);
+ });
+ });
+
+ await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
+
+ const address = proxy.address();
+ assert(address && typeof address !== 'string');
+ process.env['ECOSYSTEM_TESTS_PROXY'] = 'http://127.0.0.1/' + address.port;
+
+ return () => {
+ delete process.env['ECOSYSTEM_TESTS_PROXY'];
+ proxy.close();
+ };
+}
+
function parseArgs() {
return yargs(process.argv.slice(2))
.scriptName('ecosystem-tests')
@@ -287,112 +318,118 @@ async function main() {
await fileCache.cacheFiles(tmpFolderPath);
}
- if (jobs > 1) {
- const queue = [...projectsToRun];
- const runningProjects = new Set();
-
- const cursorLeft = '\x1B[G';
- const eraseLine = '\x1B[2K';
-
- let progressDisplayed = false;
- function clearProgress() {
- if (progressDisplayed) {
- process.stderr.write(cursorLeft + eraseLine);
- progressDisplayed = false;
+ const stopProxy = await startProxy();
+ try {
+ if (jobs > 1) {
+ const queue = [...projectsToRun];
+ const runningProjects = new Set();
+
+ const cursorLeft = '\x1B[G';
+ const eraseLine = '\x1B[2K';
+
+ let progressDisplayed = false;
+ function clearProgress() {
+ if (progressDisplayed) {
+ process.stderr.write(cursorLeft + eraseLine);
+ progressDisplayed = false;
+ }
+ }
+ const spinner = ['|', '/', '-', '\\'];
+
+ function showProgress() {
+ clearProgress();
+ progressDisplayed = true;
+ const spin = spinner[Math.floor(Date.now() / 500) % spinner.length];
+ process.stderr.write(
+ `${spin} Running ${[...runningProjects].join(', ')}`.substring(0, process.stdout.columns - 3) +
+ '...',
+ );
}
- }
- const spinner = ['|', '/', '-', '\\'];
- function showProgress() {
- clearProgress();
- progressDisplayed = true;
- const spin = spinner[Math.floor(Date.now() / 500) % spinner.length];
- process.stderr.write(
- `${spin} Running ${[...runningProjects].join(', ')}`.substring(0, process.stdout.columns - 3) + '...',
- );
- }
+ const progressInterval = setInterval(showProgress, process.stdout.isTTY ? 500 : 5000);
+ showProgress();
+
+ await Promise.all(
+ [...Array(jobs).keys()].map(async () => {
+ while (queue.length) {
+ const project = queue.shift();
+ if (!project) {
+ break;
+ }
- const progressInterval = setInterval(showProgress, process.stdout.isTTY ? 500 : 5000);
- showProgress();
+ // preserve interleaved ordering of writes to stdout/stderr
+ const chunks: { dest: 'stdout' | 'stderr'; data: string | Buffer }[] = [];
+ try {
+ runningProjects.add(project);
+ const child = execa(
+ 'yarn',
+ [
+ 'tsn',
+ __filename,
+ project,
+ '--skip-pack',
+ '--noCleanup',
+ `--retry=${args.retry}`,
+ ...(args.live ? ['--live'] : []),
+ ...(args.verbose ? ['--verbose'] : []),
+ ...(args.deploy ? ['--deploy'] : []),
+ ...(args.fromNpm ? ['--from-npm'] : []),
+ ],
+ { stdio: 'pipe', encoding: 'utf8', maxBuffer: 100 * 1024 * 1024 },
+ );
+ child.stdout?.on('data', (data) => chunks.push({ dest: 'stdout', data }));
+ child.stderr?.on('data', (data) => chunks.push({ dest: 'stderr', data }));
+
+ await child;
+ } catch (error) {
+ failed.push(project);
+ } finally {
+ runningProjects.delete(project);
+ }
- await Promise.all(
- [...Array(jobs).keys()].map(async () => {
- while (queue.length) {
- const project = queue.shift();
- if (!project) {
- break;
- }
+ if (IS_CI) {
+ console.log(`::group::${failed.includes(project) ? '❌' : '✅'} ${project}`);
+ }
- // preserve interleaved ordering of writes to stdout/stderr
- const chunks: { dest: 'stdout' | 'stderr'; data: string | Buffer }[] = [];
- try {
- runningProjects.add(project);
- const child = execa(
- 'yarn',
- [
- 'tsn',
- __filename,
- project,
- '--skip-pack',
- '--noCleanup',
- `--retry=${args.retry}`,
- ...(args.live ? ['--live'] : []),
- ...(args.verbose ? ['--verbose'] : []),
- ...(args.deploy ? ['--deploy'] : []),
- ...(args.fromNpm ? ['--from-npm'] : []),
- ],
- { stdio: 'pipe', encoding: 'utf8', maxBuffer: 100 * 1024 * 1024 },
- );
- child.stdout?.on('data', (data) => chunks.push({ dest: 'stdout', data }));
- child.stderr?.on('data', (data) => chunks.push({ dest: 'stderr', data }));
-
- await child;
- } catch (error) {
- failed.push(project);
- } finally {
- runningProjects.delete(project);
+ for (const { data } of chunks) {
+ process.stdout.write(data);
+ }
+ if (IS_CI) console.log('::endgroup::');
}
+ }),
+ );
- if (IS_CI) {
- console.log(`::group::${failed.includes(project) ? '❌' : '✅'} ${project}`);
- }
+ clearInterval(progressInterval);
+ clearProgress();
+ } else {
+ for (const project of projectsToRun) {
+ const fn = projectRunners[project];
- for (const { data } of chunks) {
- process.stdout.write(data);
- }
- if (IS_CI) console.log('::endgroup::');
- }
- }),
- );
-
- clearInterval(progressInterval);
- clearProgress();
- } else {
- for (const project of projectsToRun) {
- const fn = projectRunners[project];
-
- await withChdir(path.join(rootDir, 'ecosystem-tests', project), async () => {
- console.error('\n');
- console.error(banner(`▶️ ${project}`));
- console.error('\n');
-
- try {
- await withRetry(fn, project, state.retry, state.retryDelay);
+ await withChdir(path.join(rootDir, 'ecosystem-tests', project), async () => {
+ console.error('\n');
+ console.error(banner(`▶️ ${project}`));
console.error('\n');
- console.error(`✅ ${project}`);
- } catch (err) {
- if (err && (err as any).shortMessage) {
- console.error((err as any).shortMessage);
- } else {
- console.error(err);
+
+ try {
+ await withRetry(fn, project, state.retry, state.retryDelay);
+ console.error('\n');
+ console.error(`✅ ${project}`);
+ } catch (err) {
+ if (err && (err as any).shortMessage) {
+ console.error((err as any).shortMessage);
+ } else {
+ console.error(err);
+ }
+ console.error('\n');
+ console.error(`❌ ${project}`);
+ failed.push(project);
}
console.error('\n');
- console.error(`❌ ${project}`);
- failed.push(project);
- }
- console.error('\n');
- });
+ });
+ }
}
+ } finally {
+ stopProxy();
}
if (!args.noCleanup) {
diff --git a/ecosystem-tests/cloudflare-worker/tsconfig.check.json b/ecosystem-tests/cloudflare-worker/tsconfig.check.json
index 22d6f227b..d1b622447 100644
--- a/ecosystem-tests/cloudflare-worker/tsconfig.check.json
+++ b/ecosystem-tests/cloudflare-worker/tsconfig.check.json
@@ -1,8 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
- "exclude": ["tests", "jest.config.cjs"],
- "compilerOptions": {
- "skipLibCheck": false
- }
+ "exclude": ["tests", "jest.config.cjs"]
}
diff --git a/ecosystem-tests/node-ts-cjs-auto/tests/test.ts b/ecosystem-tests/node-ts-cjs-auto/tests/test.ts
index 7782ae1d2..ea6913ef6 100644
--- a/ecosystem-tests/node-ts-cjs-auto/tests/test.ts
+++ b/ecosystem-tests/node-ts-cjs-auto/tests/test.ts
@@ -3,6 +3,7 @@ import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions
import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
import * as fs from 'fs';
import { distance } from 'fastest-levenshtein';
+import { File } from 'node:buffer';
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
const filename = 'sample-1.mp3';
@@ -232,9 +233,9 @@ describe('toFile', () => {
.then((x) => toFile(new FormDataFile([x], filename)));
expect(file.name).toEqual(filename);
-
+
const params: TranscriptionCreateParams = { file, model };
-
+
const result = await client.audio.transcriptions.create(params);
expect(result.text).toBeSimilarTo(correctAnswer, 12);
});
diff --git a/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts b/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts
index e9c59f1c2..7910f62bd 100644
--- a/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts
+++ b/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts
@@ -80,7 +80,7 @@ it(`streaming works`, async function () {
it.skip('handles builtinFile', async function () {
const file = await fetch(url)
- .then((x) => x.arrayBuffer())
+ .then((x) => x.blob())
.then((x) => new File([x], filename));
const result = await client.audio.transcriptions.create({ file, model });
diff --git a/ecosystem-tests/node-ts-cjs/tests/test-node.ts b/ecosystem-tests/node-ts-cjs/tests/test-node.ts
index 59ed83478..ebdc4e091 100644
--- a/ecosystem-tests/node-ts-cjs/tests/test-node.ts
+++ b/ecosystem-tests/node-ts-cjs/tests/test-node.ts
@@ -1,10 +1,11 @@
import OpenAI, { toFile } from 'openai';
import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions';
-import fetch from 'node-fetch';
+import * as undici from 'undici';
import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
import * as fs from 'fs';
import { distance } from 'fastest-levenshtein';
import { ChatCompletion } from 'openai/resources/chat/completions';
+import { File } from 'node:buffer';
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
const filename = 'sample-1.mp3';
@@ -181,9 +182,9 @@ describe('toFile', () => {
.then((x) => toFile(new FormDataFile([x], filename)));
expect(file.name).toEqual(filename);
-
+
const params: TranscriptionCreateParams = { file, model };
-
+
const result = await client.audio.transcriptions.create(params);
expect(result.text).toBeSimilarTo(correctAnswer, 12);
});
diff --git a/ecosystem-tests/node-ts-cjs/tsconfig.json b/ecosystem-tests/node-ts-cjs/tsconfig.json
index ef91fa3f0..a7b5e119f 100644
--- a/ecosystem-tests/node-ts-cjs/tsconfig.json
+++ b/ecosystem-tests/node-ts-cjs/tsconfig.json
@@ -9,8 +9,9 @@
/* Language and Environment */
"target": "ES2015",
- "lib": ["ES2015", "DOM.AsyncIterable"],
+ "lib": ["ES2015"],
"jsx": "react",
+ "types": ["node", "jest"],
/* Modules */
"module": "commonjs",
diff --git a/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json b/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json
index a5c7de8cb..eb9e3ec20 100644
--- a/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json
+++ b/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json
@@ -9,8 +9,9 @@
/* Language and Environment */
"target": "ES2015",
- "lib": ["ES2015", "DOM.AsyncIterable"],
+ "lib": ["ES2015"],
"jsx": "react",
+ "types": ["node", "jest"],
/* Modules */
"module": "NodeNext",
diff --git a/ecosystem-tests/node-ts-esm-auto/tests/test.ts b/ecosystem-tests/node-ts-esm-auto/tests/test.ts
index 5e9c8f961..855a925c5 100644
--- a/ecosystem-tests/node-ts-esm-auto/tests/test.ts
+++ b/ecosystem-tests/node-ts-esm-auto/tests/test.ts
@@ -4,6 +4,7 @@ import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
import * as fs from 'fs';
import { distance } from 'fastest-levenshtein';
import { ChatCompletion } from 'openai/resources/chat/completions';
+import { File } from 'node:buffer';
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
const filename = 'sample-1.mp3';
@@ -166,9 +167,9 @@ describe('toFile', () => {
.then((x) => toFile(new FormDataFile([x], filename)));
expect(file.name).toEqual(filename);
-
+
const params: TranscriptionCreateParams = { file, model };
-
+
const result = await client.audio.transcriptions.create(params);
expect(result.text).toBeSimilarTo(correctAnswer, 12);
});
diff --git a/ecosystem-tests/node-ts4.5-jest28/tests/test.ts b/ecosystem-tests/node-ts4.5-jest28/tests/test.ts
index 5e9c8f961..855a925c5 100644
--- a/ecosystem-tests/node-ts4.5-jest28/tests/test.ts
+++ b/ecosystem-tests/node-ts4.5-jest28/tests/test.ts
@@ -4,6 +4,7 @@ import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node';
import * as fs from 'fs';
import { distance } from 'fastest-levenshtein';
import { ChatCompletion } from 'openai/resources/chat/completions';
+import { File } from 'node:buffer';
const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3';
const filename = 'sample-1.mp3';
@@ -166,9 +167,9 @@ describe('toFile', () => {
.then((x) => toFile(new FormDataFile([x], filename)));
expect(file.name).toEqual(filename);
-
+
const params: TranscriptionCreateParams = { file, model };
-
+
const result = await client.audio.transcriptions.create(params);
expect(result.text).toBeSimilarTo(correctAnswer, 12);
});
diff --git a/ecosystem-tests/proxy.ts b/ecosystem-tests/proxy.ts
new file mode 100644
index 000000000..ab05685a3
--- /dev/null
+++ b/ecosystem-tests/proxy.ts
@@ -0,0 +1,27 @@
+import { createServer } from 'http';
+import { connect } from 'net';
+
+async function startProxy() {
+ const proxy = createServer((_req, res) => {
+ res.end();
+ });
+
+ proxy.on('connect', (req, clientSocket, head) => {
+ const serverSocket = connect(443, 'api.openai.com', () => {
+ clientSocket.write(
+ 'HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + '\r\n',
+ );
+ serverSocket.write(head);
+ serverSocket.pipe(clientSocket);
+ clientSocket.pipe(serverSocket);
+ });
+ });
+
+ await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
+
+ console.log(proxy.address()!.toString())
+
+ return () => {
+ proxy.close()
+ }
+}
diff --git a/examples/stream-to-client-browser.ts b/examples/stream-to-client-browser.ts
index 4430ccc68..4d9697505 100755
--- a/examples/stream-to-client-browser.ts
+++ b/examples/stream-to-client-browser.ts
@@ -5,10 +5,8 @@
* for easy demo purposes, but simulating use in the browser.
*
* To run it in a browser application, copy/paste it into a frontend application,
- * remove the 'node-fetch' import, and replace `process.stdout.write` with
- * a console.log or UI display.
+ * and replace `process.stdout.write` with a console.log or UI display.
*/
-import fetch from 'node-fetch';
import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
fetch('http://github.com/service/http://localhost:3000/', {
diff --git a/helpers.md b/helpers.md
index 16bc1f277..f66ebb39c 100644
--- a/helpers.md
+++ b/helpers.md
@@ -353,8 +353,6 @@ chat completion request, not for the entire call run.
See an example of automated function calls in action in
[`examples/function-call-helpers.ts`](examples/function-call-helpers.ts).
-Note, `runFunctions` was also previously available, but has been deprecated in favor of `runTools`.
-
### Chat Events
#### `.on('connect', () => …)`
diff --git a/package.json b/package.json
index 93749e2f8..15e5c4143 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openai",
- "version": "5.0.0-alpha.0",
+ "version": "5.0.0-beta.0",
"description": "The official TypeScript library for the OpenAI API",
"author": "OpenAI ",
"types": "dist/index.d.ts",
@@ -51,6 +51,9 @@
"ws": "^8.18.0",
"zod": "^3.23.8"
},
+ "resolutions": {
+ "synckit": "0.8.8"
+ },
"imports": {
"openai": ".",
"openai/*": "./src/*"
diff --git a/scripts/build b/scripts/build
index aadf87be6..dd2c9dd57 100755
--- a/scripts/build
+++ b/scripts/build
@@ -40,8 +40,8 @@ cp dist/index.d.ts dist/index.d.mts
cp tsconfig.dist-src.json dist/src/tsconfig.json
cp src/internal/shim-types.d.ts dist/internal/shim-types.d.ts
cp src/internal/shim-types.d.ts dist/internal/shim-types.d.mts
-mkdir -p dist/internal/polyfill
-cp src/internal/polyfill/*.{mjs,js,d.ts} dist/internal/polyfill
+mkdir -p dist/internal/shims
+cp src/internal/shims/*.{mjs,js,d.ts} dist/internal/shims
node scripts/utils/postprocess-files.cjs
diff --git a/src/client.ts b/src/client.ts
index 478729321..9154dc496 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -641,7 +641,9 @@ export class OpenAI {
const timeout = setTimeout(() => controller.abort(), ms);
- const isReadableBody = Shims.isReadableLike(options.body);
+ const isReadableBody =
+ ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) ||
+ (typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body);
const fetchOptions: RequestInit = {
signal: controller.signal as any,
diff --git a/src/internal/polyfill/file.node.d.ts b/src/internal/polyfill/file.node.d.ts
deleted file mode 100644
index b2a59bfd5..000000000
--- a/src/internal/polyfill/file.node.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * This file polyfills the global `File` object for you if it's not already defined
- * when running on Node.js
- *
- * This is only needed on Node.js v18 & v19. Newer versions already define `File`
- * as a global.
- */
-
-export {};
diff --git a/src/internal/polyfill/file.node.js b/src/internal/polyfill/file.node.js
deleted file mode 100644
index eba997e1d..000000000
--- a/src/internal/polyfill/file.node.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * This file polyfills the global `File` object for you if it's not already defined
- * when running on Node.js
- *
- * This is only needed on Node.js v18 & v19. Newer versions already define `File`
- * as a global.
- */
-
-if (typeof require !== 'undefined') {
- if (!globalThis.File) {
- try {
- // Use [require][0](...) and not require(...) so bundlers don't try to bundle the
- // buffer module.
- globalThis.File = [require][0]('node:buffer').File;
- } catch (e) {}
- }
-}
diff --git a/src/internal/polyfill/file.node.mjs b/src/internal/polyfill/file.node.mjs
deleted file mode 100644
index 520dcb84c..000000000
--- a/src/internal/polyfill/file.node.mjs
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * This file polyfills the global `File` object for you if it's not already defined
- * when running on Node.js
- *
- * This is only needed on Node.js v18 & v19. Newer versions already define `File`
- * as a global.
- */
-
-import './file.node.js';
diff --git a/src/internal/shims.ts b/src/internal/shims.ts
index e084d8925..b1196d141 100644
--- a/src/internal/shims.ts
+++ b/src/internal/shims.ts
@@ -20,62 +20,6 @@ export function getDefaultFetch(): Fetch {
);
}
-/**
- * A minimal copy of the NodeJS `stream.Readable` class so that we can
- * accept the NodeJS types in certain places, e.g. file uploads
- *
- * https://nodejs.org/api/stream.html#class-streamreadable
- */
-export interface ReadableLike {
- readable: boolean;
- readonly readableEnded: boolean;
- readonly readableFlowing: boolean | null;
- readonly readableHighWaterMark: number;
- readonly readableLength: number;
- readonly readableObjectMode: boolean;
- destroyed: boolean;
- read(size?: number): any;
- pause(): this;
- resume(): this;
- isPaused(): boolean;
- destroy(error?: Error): this;
- [Symbol.asyncIterator](): AsyncIterableIterator;
-}
-
-/**
- * Determines if the given value looks like a NodeJS `stream.Readable`
- * object and that it is readable, i.e. has not been consumed.
- *
- * https://nodejs.org/api/stream.html#class-streamreadable
- */
-export function isReadableLike(value: any) {
- // We declare our own class of Readable here, so it's not feasible to
- // do an 'instanceof' check. Instead, check for Readable-like properties.
- return !!value && value.readable === true && typeof value.read === 'function';
-}
-
-/**
- * A minimal copy of the NodeJS `fs.ReadStream` class for usage within file uploads.
- *
- * https://nodejs.org/api/fs.html#class-fsreadstream
- */
-export interface FsReadStreamLike extends ReadableLike {
- path: {}; // real type is string | Buffer but we can't reference `Buffer` here
-}
-
-/**
- * Determines if the given value looks like a NodeJS `fs.ReadStream`
- * object.
- *
- * This just checks if the object matches our `Readable` interface
- * and defines a `path` property, there may be false positives.
- *
- * https://nodejs.org/api/fs.html#class-fsreadstream
- */
-export function isFsReadStreamLike(value: any): value is FsReadStreamLike {
- return isReadableLike(value) && 'path' in value;
-}
-
type ReadableStreamArgs = ConstructorParameters;
export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream {
diff --git a/src/internal/polyfill/crypto.node.d.ts b/src/internal/shims/crypto.node.d.ts
similarity index 100%
rename from src/internal/polyfill/crypto.node.d.ts
rename to src/internal/shims/crypto.node.d.ts
diff --git a/src/internal/polyfill/crypto.node.js b/src/internal/shims/crypto.node.js
similarity index 100%
rename from src/internal/polyfill/crypto.node.js
rename to src/internal/shims/crypto.node.js
diff --git a/src/internal/polyfill/crypto.node.mjs b/src/internal/shims/crypto.node.mjs
similarity index 100%
rename from src/internal/polyfill/crypto.node.mjs
rename to src/internal/shims/crypto.node.mjs
diff --git a/src/internal/shims/file.node.d.ts b/src/internal/shims/file.node.d.ts
new file mode 100644
index 000000000..9dc6b2fcc
--- /dev/null
+++ b/src/internal/shims/file.node.d.ts
@@ -0,0 +1,20 @@
+// The infer is to make TS show it as a nice union type,
+// instead of literally `ConstructorParameters[0]`
+type FallbackBlobSource = ConstructorParameters[0] extends infer T ? T : never;
+/**
+ * A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) provides information about files.
+ */
+declare class FallbackFile extends Blob {
+ constructor(sources: FallbackBlobSource, fileName: string, options?: any);
+ /**
+ * The name of the `File`.
+ */
+ readonly name: string;
+ /**
+ * The last modified date of the `File`.
+ */
+ readonly lastModified: number;
+}
+export type File = InstanceType;
+export const File: typeof globalThis extends { File: infer fileConstructor } ? fileConstructor
+: typeof FallbackFile;
diff --git a/src/internal/shims/file.node.js b/src/internal/shims/file.node.js
new file mode 100644
index 000000000..3f8c2ed68
--- /dev/null
+++ b/src/internal/shims/file.node.js
@@ -0,0 +1,11 @@
+if (typeof require !== 'undefined') {
+ if (globalThis.File) {
+ exports.File = globalThis.File;
+ } else {
+ try {
+ // Use [require][0](...) and not require(...) so bundlers don't try to bundle the
+ // buffer module.
+ exports.File = [require][0]('node:buffer').File;
+ } catch (e) {}
+ }
+}
diff --git a/src/internal/shims/file.node.mjs b/src/internal/shims/file.node.mjs
new file mode 100644
index 000000000..1f103f5d3
--- /dev/null
+++ b/src/internal/shims/file.node.mjs
@@ -0,0 +1,2 @@
+import * as mod from './file.node.js';
+export const File = globalThis.File || mod.File;
diff --git a/src/internal/to-file.ts b/src/internal/to-file.ts
new file mode 100644
index 000000000..69b76d3a6
--- /dev/null
+++ b/src/internal/to-file.ts
@@ -0,0 +1,152 @@
+import { File } from './shims/file.node.js';
+import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads';
+import type { FilePropertyBag } from './builtin-types';
+
+type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView;
+
+/**
+ * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc.
+ * Don't add arrayBuffer here, node-fetch doesn't have it
+ */
+interface BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
+ readonly size: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
+ readonly type: string;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
+ text(): Promise;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
+ slice(start?: number, end?: number): BlobLike;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.size === 'number' &&
+ typeof value.type === 'string' &&
+ typeof value.text === 'function' &&
+ typeof value.slice === 'function' &&
+ typeof value.arrayBuffer === 'function';
+
+/**
+ * Intended to match DOM File, node:buffer File, undici File, etc.
+ */
+interface FileLike extends BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
+ readonly lastModified: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
+ readonly name?: string | undefined;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.name === 'string' &&
+ typeof value.lastModified === 'number' &&
+ isBlobLike(value);
+
+/**
+ * Intended to match DOM Response, node-fetch Response, undici Response, etc.
+ */
+export interface ResponseLike {
+ url: string;
+ blob(): Promise;
+}
+
+const isResponseLike = (value: any): value is ResponseLike =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.url === 'string' &&
+ typeof value.blob === 'function';
+
+export type ToFileInput =
+ | FileLike
+ | ResponseLike
+ | Exclude
+ | AsyncIterable;
+
+/**
+ * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
+ * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
+ * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
+ * @param {Object=} options additional properties
+ * @param {string=} options.type the MIME type of the content
+ * @param {number=} options.lastModified the last modified timestamp
+ * @returns a {@link File} with the given properties
+ */
+export async function toFile(
+ value: ToFileInput | PromiseLike,
+ name?: string | null | undefined,
+ options?: FilePropertyBag | undefined,
+): Promise {
+ // If it's a promise, resolve it.
+ value = await value;
+
+ // If we've been given a `File` we don't need to do anything
+ if (isFileLike(value)) {
+ if (File && value instanceof File) {
+ return value;
+ }
+ return makeFile([await value.arrayBuffer()], value.name);
+ }
+
+ if (isResponseLike(value)) {
+ const blob = await value.blob();
+ name ||= new URL(value.url).pathname.split(/[\\/]/).pop();
+
+ return makeFile(await getBytes(blob), name, options);
+ }
+
+ const parts = await getBytes(value);
+
+ name ||= getName(value);
+
+ if (!options?.type) {
+ const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type);
+ if (typeof type === 'string') {
+ options = { ...options, type };
+ }
+ }
+
+ return makeFile(parts, name, options);
+}
+
+async function getBytes(value: BlobLikePart | AsyncIterable): Promise> {
+ let parts: Array = [];
+ if (
+ typeof value === 'string' ||
+ ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
+ value instanceof ArrayBuffer
+ ) {
+ parts.push(value);
+ } else if (isBlobLike(value)) {
+ parts.push(value instanceof Blob ? value : await value.arrayBuffer());
+ } else if (
+ isAsyncIterable(value) // includes Readable, ReadableStream, etc.
+ ) {
+ for await (const chunk of value) {
+ parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating?
+ }
+ } else {
+ const constructor = value?.constructor?.name;
+ throw new Error(
+ `Unexpected data type: ${typeof value}${
+ constructor ? `; constructor: ${constructor}` : ''
+ }${propsForError(value)}`,
+ );
+ }
+
+ return parts;
+}
+
+function propsForError(value: unknown): string {
+ if (typeof value !== 'object' || value === null) return '';
+ const props = Object.getOwnPropertyNames(value);
+ return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`;
+}
diff --git a/src/internal/uploads.ts b/src/internal/uploads.ts
index d89af49b1..2c286497c 100644
--- a/src/internal/uploads.ts
+++ b/src/internal/uploads.ts
@@ -1,11 +1,16 @@
import { type RequestOptions } from './request-options';
import type { FilePropertyBag, Fetch } from './builtin-types';
-import { isFsReadStreamLike, type FsReadStreamLike } from './shims';
import type { OpenAI } from '../client';
-import './polyfill/file.node.js';
+import { File } from './shims/file.node.js';
+import { ReadableStreamFrom } from './shims';
-type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView;
-type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
+export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
+type FsReadStream = AsyncIterable & { path: string | { toString(): string } };
+
+// https://github.com/oven-sh/bun/issues/5980
+interface BunFile extends Blob {
+ readonly name?: string | undefined;
+}
/**
* Typically, this is a native "File" class.
@@ -16,188 +21,41 @@ type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
* For convenience, you can also pass a fetch Response, or in Node,
* the result of fs.createReadStream().
*/
-export type Uploadable = FileLike | ResponseLike | FsReadStreamLike;
-
-/**
- * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc.
- * Don't add arrayBuffer here, node-fetch doesn't have it
- */
-interface BlobLike {
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
- readonly size: number;
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
- readonly type: string;
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
- text(): Promise;
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
- slice(start?: number, end?: number): BlobLike;
-}
-
-/**
- * This check adds the arrayBuffer() method type because it is available and used at runtime
- */
-const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise } =>
- value != null &&
- typeof value === 'object' &&
- typeof value.size === 'number' &&
- typeof value.type === 'string' &&
- typeof value.text === 'function' &&
- typeof value.slice === 'function' &&
- typeof value.arrayBuffer === 'function';
-
-/**
- * Intended to match DOM File, node:buffer File, undici File, etc.
- */
-interface FileLike extends BlobLike {
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
- readonly lastModified: number;
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
- readonly name?: string | undefined;
-}
-declare var FileClass: {
- prototype: FileLike;
- new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): FileLike;
-};
-
-/**
- * This check adds the arrayBuffer() method type because it is available and used at runtime
- */
-const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise } =>
- value != null &&
- typeof value === 'object' &&
- typeof value.name === 'string' &&
- typeof value.lastModified === 'number' &&
- isBlobLike(value);
-
-/**
- * Intended to match DOM Response, node-fetch Response, undici Response, etc.
- */
-export interface ResponseLike {
- url: string;
- blob(): Promise;
-}
-
-const isResponseLike = (value: any): value is ResponseLike =>
- value != null &&
- typeof value === 'object' &&
- typeof value.url === 'string' &&
- typeof value.blob === 'function';
-
-const isUploadable = (value: any): value is Uploadable => {
- return isFileLike(value) || isResponseLike(value) || isFsReadStreamLike(value);
-};
-
-type ToFileInput = Uploadable | Exclude | AsyncIterable;
+export type Uploadable = File | Response | FsReadStream | BunFile;
/**
* Construct a `File` instance. This is used to ensure a helpful error is thrown
- * for environments that don't define a global `File` yet and so that we don't
- * accidentally rely on a global `File` type in our annotations.
+ * for environments that don't define a global `File` yet.
*/
-function makeFile(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): FileLike {
- const File = (globalThis as any).File as typeof FileClass | undefined;
+export function makeFile(
+ fileBits: BlobPart[],
+ fileName: string | undefined,
+ options?: FilePropertyBag,
+): File {
if (typeof File === 'undefined') {
throw new Error('`File` is not defined as a global which is required for file uploads');
}
- return new File(fileBits, fileName, options);
+ return new File(fileBits as any, fileName ?? 'unknown_file', options);
}
-/**
- * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
- * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
- * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
- * @param {Object=} options additional properties
- * @param {string=} options.type the MIME type of the content
- * @param {number=} options.lastModified the last modified timestamp
- * @returns a {@link File} with the given properties
- */
-export async function toFile(
- value: ToFileInput | PromiseLike,
- name?: string | null | undefined,
- options?: FilePropertyBag | undefined,
-): Promise {
- // If it's a promise, resolve it.
- value = await value;
-
- // If we've been given a `File` we don't need to do anything
- if (isFileLike(value)) {
- const File = (globalThis as any).File as typeof FileClass | undefined;
- if (File && value instanceof File) {
- return value;
- }
- return makeFile([await value.arrayBuffer()], value.name ?? 'unknown_file');
- }
-
- if (isResponseLike(value)) {
- const blob = await value.blob();
- name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? 'unknown_file';
-
- return makeFile(await getBytes(blob), name, options);
- }
-
- const parts = await getBytes(value);
-
- name ||= getName(value) ?? 'unknown_file';
-
- if (!options?.type) {
- const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type);
- if (typeof type === 'string') {
- options = { ...options, type };
- }
- }
-
- return makeFile(parts, name, options);
-}
-
-export async function getBytes(
- value: Uploadable | BlobLikePart | AsyncIterable,
-): Promise> {
- let parts: Array = [];
- if (
- typeof value === 'string' ||
- ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
- value instanceof ArrayBuffer
- ) {
- parts.push(value);
- } else if (isBlobLike(value)) {
- parts.push(value instanceof Blob ? value : await value.arrayBuffer());
- } else if (
- isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
- ) {
- for await (const chunk of value) {
- parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating?
- }
- } else {
- const constructor = value?.constructor?.name;
- throw new Error(
- `Unexpected data type: ${typeof value}${
- constructor ? `; constructor: ${constructor}` : ''
- }${propsForError(value)}`,
- );
- }
-
- return parts;
-}
-
-function propsForError(value: unknown): string {
- if (typeof value !== 'object' || value === null) return '';
- const props = Object.getOwnPropertyNames(value);
- return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`;
-}
-
-function getName(value: unknown): string | undefined {
+export function getName(value: any): string | undefined {
return (
- (typeof value === 'object' &&
- value !== null &&
- (('name' in value && String(value.name)) ||
- ('filename' in value && String(value.filename)) ||
- ('path' in value && String(value.path).split(/[\\/]/).pop()))) ||
- undefined
+ (
+ (typeof value === 'object' &&
+ value !== null &&
+ (('name' in value && value.name && String(value.name)) ||
+ ('url' in value && value.url && String(value.url)) ||
+ ('filename' in value && value.filename && String(value.filename)) ||
+ ('path' in value && value.path && String(value.path)))) ||
+ ''
+ )
+ .split(/[\\/]/)
+ .pop() || undefined
);
}
-const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator =>
+export const isAsyncIterable = (value: any): value is AsyncIterable =>
value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
/**
@@ -268,6 +126,16 @@ export const createForm = async >(
return form;
};
+// We check for Blob not File because Bun.File doesn't inherit from File,
+// but they both inherit from Blob and have a `name` property at runtime.
+const isNamedBlob = (value: object) =>
+ (File && value instanceof File) || (value instanceof Blob && 'name' in value);
+
+const isUploadable = (value: unknown) =>
+ typeof value === 'object' &&
+ value !== null &&
+ (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value));
+
const hasUploadableValue = (value: unknown): boolean => {
if (isUploadable(value)) return true;
if (Array.isArray(value)) return value.some(hasUploadableValue);
@@ -290,9 +158,12 @@ const addFormValue = async (form: FormData, key: string, value: unknown): Promis
// TODO: make nested formats configurable
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
form.append(key, String(value));
- } else if (isUploadable(value)) {
- const file = await toFile(value);
- form.append(key, file as any);
+ } else if (value instanceof Response) {
+ form.append(key, makeFile([await value.blob()], getName(value)));
+ } else if (isAsyncIterable(value)) {
+ form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value)));
+ } else if (isNamedBlob(value)) {
+ form.append(key, value, getName(value));
} else if (Array.isArray(value)) {
await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
} else if (typeof value === 'object') {
diff --git a/src/internal/utils/uuid.ts b/src/internal/utils/uuid.ts
index 6c43f81dd..1349c42c3 100644
--- a/src/internal/utils/uuid.ts
+++ b/src/internal/utils/uuid.ts
@@ -1,6 +1,6 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-import { crypto } from '../polyfill/crypto.node';
+import { crypto } from '../shims/crypto.node.js';
/**
* https://stackoverflow.com/a/2117523
diff --git a/src/lib/AbstractChatCompletionRunner.ts b/src/lib/AbstractChatCompletionRunner.ts
index 144196aae..9f31ac119 100644
--- a/src/lib/AbstractChatCompletionRunner.ts
+++ b/src/lib/AbstractChatCompletionRunner.ts
@@ -13,11 +13,8 @@ import {
type BaseFunctionsArgs,
RunnableToolFunction,
} from './RunnableFunction';
-import { ChatCompletionFunctionRunnerParams, ChatCompletionToolRunnerParams } from './ChatCompletionRunner';
-import {
- ChatCompletionStreamingFunctionRunnerParams,
- ChatCompletionStreamingToolRunnerParams,
-} from './ChatCompletionStreamingRunner';
+import { ChatCompletionToolRunnerParams } from './ChatCompletionRunner';
+import { ChatCompletionStreamingToolRunnerParams } from './ChatCompletionStreamingRunner';
import { isAssistantMessage, isFunctionMessage, isToolMessage } from './chatCompletionUtils';
import { BaseEvents, EventStream } from './EventStream';
import { ParsedChatCompletion } from '../resources/beta/chat/completions';
@@ -266,91 +263,6 @@ export class AbstractChatCompletionRunner<
return await this._createChatCompletion(client, params, options);
}
- protected async _runFunctions(
- client: OpenAI,
- params:
- | ChatCompletionFunctionRunnerParams
- | ChatCompletionStreamingFunctionRunnerParams,
- options?: RunnerOptions,
- ) {
- const role = 'function' as const;
- const { function_call = 'auto', stream, ...restParams } = params;
- const singleFunctionToCall = typeof function_call !== 'string' && function_call?.name;
- const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
-
- const functionsByName: Record> = {};
- for (const f of params.functions) {
- functionsByName[f.name || f.function.name] = f;
- }
-
- const functions: ChatCompletionCreateParams.Function[] = params.functions.map(
- (f): ChatCompletionCreateParams.Function => ({
- name: f.name || f.function.name,
- parameters: f.parameters as Record,
- description: f.description,
- }),
- );
-
- for (const message of params.messages) {
- this._addMessage(message, false);
- }
-
- for (let i = 0; i < maxChatCompletions; ++i) {
- const chatCompletion: ChatCompletion = await this._createChatCompletion(
- client,
- {
- ...restParams,
- function_call,
- functions,
- messages: [...this.messages],
- },
- options,
- );
- const message = chatCompletion.choices[0]?.message;
- if (!message) {
- throw new OpenAIError(`missing message in ChatCompletion response`);
- }
- if (!message.function_call) return;
- const { name, arguments: args } = message.function_call;
- const fn = functionsByName[name];
- if (!fn) {
- const content = `Invalid function_call: ${JSON.stringify(name)}. Available options are: ${functions
- .map((f) => JSON.stringify(f.name))
- .join(', ')}. Please try again`;
-
- this._addMessage({ role, name, content });
- continue;
- } else if (singleFunctionToCall && singleFunctionToCall !== name) {
- const content = `Invalid function_call: ${JSON.stringify(name)}. ${JSON.stringify(
- singleFunctionToCall,
- )} requested. Please try again`;
-
- this._addMessage({ role, name, content });
- continue;
- }
-
- let parsed;
- try {
- parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args;
- } catch (error) {
- this._addMessage({
- role,
- name,
- content: error instanceof Error ? error.message : String(error),
- });
- continue;
- }
-
- // @ts-expect-error it can't rule out `never` type.
- const rawContent = await fn.function(parsed, this);
- const content = this.#stringifyFunctionCallResult(rawContent);
-
- this._addMessage({ role, name, content });
-
- if (singleFunctionToCall) return;
- }
- }
-
protected async _runTools(
client: OpenAI,
params:
diff --git a/src/lib/ChatCompletionRunner.ts b/src/lib/ChatCompletionRunner.ts
index 9e68e6671..a5edaf741 100644
--- a/src/lib/ChatCompletionRunner.ts
+++ b/src/lib/ChatCompletionRunner.ts
@@ -2,7 +2,7 @@ import {
type ChatCompletionMessageParam,
type ChatCompletionCreateParamsNonStreaming,
} from '../resources/chat/completions';
-import { type RunnableFunctions, type BaseFunctionsArgs, RunnableTools } from './RunnableFunction';
+import { type BaseFunctionsArgs, RunnableTools } from './RunnableFunction';
import {
AbstractChatCompletionRunner,
AbstractChatCompletionRunnerEvents,
@@ -16,13 +16,6 @@ export interface ChatCompletionRunnerEvents extends AbstractChatCompletionRunner
content: (content: string) => void;
}
-export type ChatCompletionFunctionRunnerParams = Omit<
- ChatCompletionCreateParamsNonStreaming,
- 'functions'
-> & {
- functions: RunnableFunctions;
-};
-
export type ChatCompletionToolRunnerParams = Omit<
ChatCompletionCreateParamsNonStreaming,
'tools'
@@ -34,21 +27,6 @@ export class ChatCompletionRunner extends AbstractChatCompletion
ChatCompletionRunnerEvents,
ParsedT
> {
- /** @deprecated - please use `runTools` instead. */
- static runFunctions(
- client: OpenAI,
- params: ChatCompletionFunctionRunnerParams,
- options?: RunnerOptions,
- ): ChatCompletionRunner {
- const runner = new ChatCompletionRunner();
- const opts = {
- ...options,
- headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' },
- };
- runner._run(() => runner._runFunctions(client, params, opts));
- return runner;
- }
-
static runTools(
client: OpenAI,
params: ChatCompletionToolRunnerParams,
diff --git a/src/lib/ChatCompletionStreamingRunner.ts b/src/lib/ChatCompletionStreamingRunner.ts
index 32b66b006..edd25ab93 100644
--- a/src/lib/ChatCompletionStreamingRunner.ts
+++ b/src/lib/ChatCompletionStreamingRunner.ts
@@ -3,8 +3,8 @@ import {
type ChatCompletionCreateParamsStreaming,
} from '../resources/chat/completions';
import { RunnerOptions, type AbstractChatCompletionRunnerEvents } from './AbstractChatCompletionRunner';
-import { type ReadableStream } from '../internal/shim-types';
-import { RunnableTools, type BaseFunctionsArgs, type RunnableFunctions } from './RunnableFunction';
+import type { ReadableStream } from '../internal/shim-types';
+import { RunnableTools, type BaseFunctionsArgs } from './RunnableFunction';
import { ChatCompletionSnapshot, ChatCompletionStream } from './ChatCompletionStream';
import OpenAI from '../index';
import { AutoParseableTool } from '../lib/parser';
@@ -14,13 +14,6 @@ export interface ChatCompletionStreamEvents extends AbstractChatCompletionRunner
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
}
-export type ChatCompletionStreamingFunctionRunnerParams = Omit<
- ChatCompletionCreateParamsStreaming,
- 'functions'
-> & {
- functions: RunnableFunctions;
-};
-
export type ChatCompletionStreamingToolRunnerParams = Omit<
ChatCompletionCreateParamsStreaming,
'tools'
@@ -38,21 +31,6 @@ export class ChatCompletionStreamingRunner
return runner;
}
- /** @deprecated - please use `runTools` instead. */
- static runFunctions(
- client: OpenAI,
- params: ChatCompletionStreamingFunctionRunnerParams,
- options?: RunnerOptions,
- ): ChatCompletionStreamingRunner {
- const runner = new ChatCompletionStreamingRunner(null);
- const opts = {
- ...options,
- headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' },
- };
- runner._run(() => runner._runFunctions(client, params, opts));
- return runner;
- }
-
static runTools(
client: OpenAI,
params: ChatCompletionStreamingToolRunnerParams,
diff --git a/src/lib/EventStream.ts b/src/lib/EventStream.ts
index d3f485e9d..d1b1cc8d3 100644
--- a/src/lib/EventStream.ts
+++ b/src/lib/EventStream.ts
@@ -1,3 +1,4 @@
+import { isAbortError } from '../internal/errors';
import { APIUserAbortError, OpenAIError } from '../error';
export class EventStream {
@@ -145,7 +146,7 @@ export class EventStream {
#handleError(this: EventStream, error: unknown) {
this.#errored = true;
- if (error instanceof Error && error.name === 'AbortError') {
+ if (isAbortError(error)) {
error = new APIUserAbortError();
}
if (error instanceof APIUserAbortError) {
diff --git a/src/lib/RunnableFunction.ts b/src/lib/RunnableFunction.ts
index a645f5ebe..4d1519d5a 100644
--- a/src/lib/RunnableFunction.ts
+++ b/src/lib/RunnableFunction.ts
@@ -85,13 +85,6 @@ export function isRunnableFunctionWithParse(
export type BaseFunctionsArgs = readonly (object | string)[];
-export type RunnableFunctions =
- [any[]] extends [FunctionsArgs] ? readonly RunnableFunction[]
- : {
- [Index in keyof FunctionsArgs]: Index extends number ? RunnableFunction
- : FunctionsArgs[Index];
- };
-
export type RunnableTools =
[any[]] extends [FunctionsArgs] ? readonly RunnableToolFunction[]
: {
@@ -99,28 +92,6 @@ export type RunnableTools =
: FunctionsArgs[Index];
};
-/**
- * This is helper class for passing a `function` and `parse` where the `function`
- * argument type matches the `parse` return type.
- *
- * @deprecated - please use ParsingToolFunction instead.
- */
-export class ParsingFunction {
- function: RunnableFunctionWithParse['function'];
- parse: RunnableFunctionWithParse['parse'];
- parameters: RunnableFunctionWithParse['parameters'];
- description: RunnableFunctionWithParse['description'];
- name?: RunnableFunctionWithParse['name'];
-
- constructor(input: RunnableFunctionWithParse) {
- this.function = input.function;
- this.parse = input.parse;
- this.parameters = input.parameters;
- this.description = input.description;
- this.name = input.name;
- }
-}
-
/**
* This is helper class for passing a `function` and `parse` where the `function`
* argument type matches the `parse` return type.
diff --git a/src/resources/beta/chat/completions.ts b/src/resources/beta/chat/completions.ts
index 15ec3f7e4..b2e0148bd 100644
--- a/src/resources/beta/chat/completions.ts
+++ b/src/resources/beta/chat/completions.ts
@@ -3,7 +3,6 @@
import { APIResource } from '../../../resource';
import { ChatCompletionRunner } from '../../../lib/ChatCompletionRunner';
import { ChatCompletionStreamingRunner } from '../../../lib/ChatCompletionStreamingRunner';
-import { RunnerOptions } from '../../../lib/AbstractChatCompletionRunner';
import { ChatCompletionToolRunnerParams } from '../../../lib/ChatCompletionRunner';
import { ChatCompletionStreamingToolRunnerParams } from '../../../lib/ChatCompletionStreamingRunner';
import { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream';
@@ -14,28 +13,21 @@ import {
ChatCompletionMessageToolCall,
} from '../../chat/completions';
import { ExtractParsedContentFromParams, parseChatCompletion, validateInputTools } from '../../../lib/parser';
+import { RequestOptions } from '../../../internal/request-options';
+import { type APIPromise } from '../../../api-promise';
+import { RunnerOptions } from '../../../lib/AbstractChatCompletionRunner';
-export {
- ChatCompletionStreamingRunner,
- type ChatCompletionStreamingFunctionRunnerParams,
-} from '../../../lib/ChatCompletionStreamingRunner';
+export { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream';
+export { ChatCompletionRunner } from '../../../lib/ChatCompletionRunner';
+export { ChatCompletionStreamingRunner } from '../../../lib/ChatCompletionStreamingRunner';
export {
type RunnableFunction,
- type RunnableFunctions,
type RunnableFunctionWithParse,
type RunnableFunctionWithoutParse,
- ParsingFunction,
ParsingToolFunction,
} from '../../../lib/RunnableFunction';
export { type ChatCompletionToolRunnerParams } from '../../../lib/ChatCompletionRunner';
export { type ChatCompletionStreamingToolRunnerParams } from '../../../lib/ChatCompletionStreamingRunner';
-export { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream';
-export {
- ChatCompletionRunner,
- type ChatCompletionFunctionRunnerParams,
-} from '../../../lib/ChatCompletionRunner';
-import { RequestOptions } from '../../../internal/request-options';
-import { type APIPromise } from '../../../index';
export interface ParsedFunction extends ChatCompletionMessageToolCall.Function {
parsed_arguments?: unknown;
diff --git a/src/uploads.ts b/src/uploads.ts
index 77b65766a..79d3073ea 100644
--- a/src/uploads.ts
+++ b/src/uploads.ts
@@ -1 +1,2 @@
-export { type Uploadable, toFile } from './internal/uploads';
+export { type Uploadable } from './internal/uploads';
+export { toFile, type ToFileInput } from './internal/to-file';
diff --git a/src/version.ts b/src/version.ts
index 2205d0775..58d8f3c50 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const VERSION = '5.0.0-alpha.0'; // x-release-please-version
+export const VERSION = '5.0.0-beta.0'; // x-release-please-version
diff --git a/tests/lib/azure.test.ts b/tests/lib/azure.test.ts
index bffd04371..1b9b2546c 100644
--- a/tests/lib/azure.test.ts
+++ b/tests/lib/azure.test.ts
@@ -1,6 +1,7 @@
import { AzureOpenAI } from 'openai';
import { APIUserAbortError } from 'openai';
import { type Response, RequestInit, RequestInfo } from 'openai/internal/builtin-types';
+import { File } from 'node:buffer';
const defaultFetch = fetch;
diff --git a/tests/responses.test.ts b/tests/responses.test.ts
index 3cb73f4e4..b8a3db906 100644
--- a/tests/responses.test.ts
+++ b/tests/responses.test.ts
@@ -2,7 +2,7 @@ import { APIPromise } from 'openai/api-promise';
import OpenAI from 'openai/index';
import { compareType } from './utils/typing';
-const client = new OpenAI();
+const client = new OpenAI({ apiKey: '...' });
describe('request id', () => {
test('types', () => {
diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts
index 44d10769b..508fce58f 100644
--- a/tests/uploads.test.ts
+++ b/tests/uploads.test.ts
@@ -1,6 +1,7 @@
import fs from 'fs';
-import type { ResponseLike } from 'openai/internal/uploads';
+import type { ResponseLike } from 'openai/internal/to-file';
import { toFile } from 'openai/uploads';
+import { File } from 'node:buffer';
class MyClass {
name: string = 'foo';
@@ -62,15 +63,45 @@ describe('toFile', () => {
expect(file.name).toEqual('input.jsonl');
expect(file.type).toBe('jsonl');
});
+
+ it('is assignable to File and Blob', async () => {
+ const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' });
+ const result = await toFile(input);
+ const file: File = result;
+ const blob: Blob = result;
+ void file, blob;
+ });
});
-test('missing File error message', async () => {
- // @ts-ignore
- globalThis.File = undefined;
+describe('missing File error message', () => {
+ let prevGlobalFile: unknown;
+ let prevNodeFile: unknown;
+ beforeEach(() => {
+ // The file shim captures the global File object when it's first imported.
+ // Reset modules before each test so we can test the error thrown when it's undefined.
+ jest.resetModules();
+ const buffer = require('node:buffer');
+ // @ts-ignore
+ prevGlobalFile = globalThis.File;
+ prevNodeFile = buffer.File;
+ // @ts-ignore
+ globalThis.File = undefined;
+ buffer.File = undefined;
+ });
+ afterEach(() => {
+ // Clean up
+ // @ts-ignore
+ globalThis.File = prevGlobalFile;
+ require('node:buffer').File = prevNodeFile;
+ jest.resetModules();
+ });
- await expect(
- toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })),
- ).rejects.toMatchInlineSnapshot(
- `[Error: \`File\` is not defined as a global which is required for file uploads]`,
- );
+ test('is thrown', async () => {
+ const uploads = await import('openai/uploads');
+ await expect(
+ uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })),
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: \`File\` is not defined as a global which is required for file uploads]`,
+ );
+ });
});
diff --git a/yarn.lock b/yarn.lock
index c16fb37a0..b8808db07 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3217,10 +3217,10 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
-synckit@^0.9.1:
- version "0.9.2"
- resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62"
- integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==
+synckit@0.8.8, synckit@^0.9.1:
+ version "0.8.8"
+ resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7"
+ integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==
dependencies:
"@pkgr/core" "^0.1.0"
tslib "^2.6.2"