diff --git a/README.md b/README.md index 1434126e7..c8135bc95 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ Request parameters that correspond to file uploads can be passed in many differe - `File` (or an object with the same structure) - a `fetch` `Response` (or an object with the same structure) - an `fs.ReadStream` +- an `stream.Readable` - the return value of our `toFile` helper ```ts diff --git a/src/_shims/bun-runtime.ts b/src/_shims/bun-runtime.ts index 8d5aaab0c..53ccfb3f2 100644 --- a/src/_shims/bun-runtime.ts +++ b/src/_shims/bun-runtime.ts @@ -4,11 +4,15 @@ import { type Shims } from './registry'; import { getRuntime as getWebRuntime } from './web-runtime'; import { ReadStream as FsReadStream } from 'node:fs'; +import { Readable } from 'node:stream'; export function getRuntime(): Shims { const runtime = getWebRuntime(); function isFsReadStream(value: any): value is FsReadStream { return value instanceof FsReadStream; } - return { ...runtime, isFsReadStream }; + function isReadableStream(value: any): value is Readable { + return value instanceof Readable; + } + return { ...runtime, isFsReadStream, isReadableStream }; } diff --git a/src/_shims/index-deno.ts b/src/_shims/index-deno.ts index b838e5f22..7845d2196 100644 --- a/src/_shims/index-deno.ts +++ b/src/_shims/index-deno.ts @@ -85,6 +85,8 @@ export function fileFromPath() { export const isFsReadStream = (value: any) => false; +export const isReadableStream = (value: any) => false; + export declare class Readable { readable: boolean; readonly readableEnded: boolean; diff --git a/src/_shims/index.d.ts b/src/_shims/index.d.ts index 4e52b952e..3fc5fae27 100644 --- a/src/_shims/index.d.ts +++ b/src/_shims/index.d.ts @@ -79,3 +79,5 @@ export function fileFromPath(path: string, options?: FileFromPathOptions): Promi export function fileFromPath(path: string, filename?: string, options?: FileFromPathOptions): Promise; export function isFsReadStream(value: any): value is FsReadStream; + +export function isReadableStream(value: any): value is ReadableStream; diff --git a/src/_shims/node-runtime.ts b/src/_shims/node-runtime.ts index 7d24b7077..5acd14564 100644 --- a/src/_shims/node-runtime.ts +++ b/src/_shims/node-runtime.ts @@ -79,5 +79,6 @@ export function getRuntime(): Shims { getDefaultAgent: (url: string): Agent => (url.startsWith('https') ? defaultHttpsAgent : defaultHttpAgent), fileFromPath, isFsReadStream: (value: any): value is FsReadStream => value instanceof FsReadStream, + isReadableStream: (value: any): value is ReadableStream => value instanceof Readable, }; } diff --git a/src/_shims/registry.ts b/src/_shims/registry.ts index 65b570d0c..5b7535404 100644 --- a/src/_shims/registry.ts +++ b/src/_shims/registry.ts @@ -22,6 +22,7 @@ export interface Shims { | ((path: string, filename?: string, options?: {}) => Promise) | ((path: string, options?: {}) => Promise); isFsReadStream: (value: any) => boolean; + isReadableStream: (value: any) => boolean; } export let auto = false; @@ -38,6 +39,7 @@ export let getMultipartRequestOptions: Shims['getMultipartRequestOptions'] | und export let getDefaultAgent: Shims['getDefaultAgent'] | undefined = undefined; export let fileFromPath: Shims['fileFromPath'] | undefined = undefined; export let isFsReadStream: Shims['isFsReadStream'] | undefined = undefined; +export let isReadableStream: Shims['isReadableStream'] | undefined = undefined; export function setShims(shims: Shims, options: { auto: boolean } = { auto: false }) { if (auto) { @@ -62,4 +64,5 @@ export function setShims(shims: Shims, options: { auto: boolean } = { auto: fals getDefaultAgent = shims.getDefaultAgent; fileFromPath = shims.fileFromPath; isFsReadStream = shims.isFsReadStream; + isReadableStream = shims.isReadableStream; } diff --git a/src/_shims/web-runtime.ts b/src/_shims/web-runtime.ts index 92dadeb89..1c441dfef 100644 --- a/src/_shims/web-runtime.ts +++ b/src/_shims/web-runtime.ts @@ -99,5 +99,6 @@ export function getRuntime({ manuallyImported }: { manuallyImported?: boolean } ); }, isFsReadStream: (value: any) => false, + isReadableStream: (value: any) => false, }; } diff --git a/src/uploads.ts b/src/uploads.ts index 301d770e3..6a864898e 100644 --- a/src/uploads.ts +++ b/src/uploads.ts @@ -7,6 +7,7 @@ import { getMultipartRequestOptions, type FsReadStream, isFsReadStream, + isReadableStream, } from './_shims/index'; import { MultipartBody } from './_shims/MultipartBody'; export { fileFromPath } from './_shims/index'; @@ -85,7 +86,7 @@ export const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Pro typeof value.arrayBuffer === 'function'; export const isUploadable = (value: any): value is Uploadable => { - return isFileLike(value) || isResponseLike(value) || isFsReadStream(value); + return isFileLike(value) || isResponseLike(value) || isFsReadStream(value) || isReadableStream(value); }; export type ToFileInput = Uploadable | Exclude | AsyncIterable; diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index b40856e29..a2b25d9ca 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import { toFile, type ResponseLike } from 'openai/uploads'; import { File } from 'openai/_shims/index'; +import { Readable } from 'node:stream'; class MyClass { name: string = 'foo'; @@ -49,9 +50,19 @@ describe('toFile', () => { expect(file.name).toEqual('input.jsonl'); }); - it('extracts a file name from a ReadStream', async () => { + it('extracts a file name from a FsReadStream', async () => { const input = fs.createReadStream('tests/uploads.test.ts'); const file = await toFile(input); expect(file.name).toEqual('uploads.test.ts'); }); + + it('extracts a file name from a ReadableStream', async () => { + const input = new Readable({ + read() { + this.push(null); + }, + }); + const file = await toFile(input); + expect(file.name).toEqual('unknown_file'); + }); });