From f35bbc599f5b44acd894353a84cca4c0a352682a Mon Sep 17 00:00:00 2001 From: David Schnurr Date: Sat, 8 Jul 2023 11:14:58 -0700 Subject: [PATCH] v4.0.0-beta.3 --- api.md | 86 +++---- build | 9 +- ecosystem-tests/vercel-edge/package-lock.json | 14 +- ecosystem-tests/vercel-edge/package.json | 2 +- examples/azure.ts | 48 ++++ package.json | 2 +- scripts/replace-self-referencing-imports.js | 21 ++ src/core.ts | 21 +- src/index.ts | 47 +++- src/resources/chat/completions.ts | 4 +- src/resources/completions.ts | 6 +- src/resources/edits.ts | 6 +- src/resources/files.ts | 6 +- src/resources/fine-tunes.ts | 25 ++- src/resources/models.ts | 4 +- src/streaming.ts | 210 ++++++++++++------ src/uploads.ts | 7 +- src/version.ts | 2 +- tests/api-resources/chat/completions.test.ts | 2 +- tests/api-resources/completions.test.ts | 2 +- tests/index.test.ts | 61 ++++- tsconfig.build.json | 41 ++-- tsconfig.json | 7 +- 23 files changed, 449 insertions(+), 184 deletions(-) create mode 100644 examples/azure.ts create mode 100644 scripts/replace-self-referencing-imports.js diff --git a/api.md b/api.md index a5ee1a25b..b19532071 100644 --- a/api.md +++ b/api.md @@ -2,12 +2,12 @@ Types: -- Completion -- CompletionChoice +- Completion +- CompletionChoice Methods: -- client.completions.create({ ...params }) -> Completion +- client.completions.create({ ...params }) -> Completion # Chat @@ -15,61 +15,61 @@ Methods: Types: -- ChatCompletion -- ChatCompletionChunk +- ChatCompletion +- ChatCompletionChunk Methods: -- client.chat.completions.create({ ...params }) -> ChatCompletion +- client.chat.completions.create({ ...params }) -> ChatCompletion # Edits Types: -- Edit +- Edit Methods: -- client.edits.create({ ...params }) -> Edit +- client.edits.create({ ...params }) -> Edit # Embeddings Types: -- Embedding +- Embedding Methods: -- client.embeddings.create({ ...params }) -> Embedding +- client.embeddings.create({ ...params }) -> Embedding # Files Types: -- FileContent -- FileDeleted -- FileObject +- FileContent +- FileDeleted +- FileObject Methods: -- client.files.create({ ...params }) -> FileObject -- client.files.retrieve(fileId) -> FileObject -- client.files.list() -> FileObjectsPage -- client.files.del(fileId) -> FileDeleted -- client.files.retrieveFileContent(fileId) -> string +- client.files.create({ ...params }) -> FileObject +- client.files.retrieve(fileId) -> FileObject +- client.files.list() -> FileObjectsPage +- client.files.del(fileId) -> FileDeleted +- client.files.retrieveFileContent(fileId) -> string # Images Types: -- Image -- ImagesResponse +- Image +- ImagesResponse Methods: -- client.images.createVariation({ ...params }) -> ImagesResponse -- client.images.edit({ ...params }) -> ImagesResponse -- client.images.generate({ ...params }) -> ImagesResponse +- client.images.createVariation({ ...params }) -> ImagesResponse +- client.images.edit({ ...params }) -> ImagesResponse +- client.images.generate({ ...params }) -> ImagesResponse # Audio @@ -77,58 +77,58 @@ Methods: Types: -- Transcription +- Transcription Methods: -- client.audio.transcriptions.create({ ...params }) -> Transcription +- client.audio.transcriptions.create({ ...params }) -> Transcription ## Translations Types: -- Translation +- Translation Methods: -- client.audio.translations.create({ ...params }) -> Translation +- client.audio.translations.create({ ...params }) -> Translation # Moderations Types: -- Moderation -- ModerationCreateResponse +- Moderation +- ModerationCreateResponse Methods: -- client.moderations.create({ ...params }) -> ModerationCreateResponse +- client.moderations.create({ ...params }) -> ModerationCreateResponse # Models Types: -- Model -- ModelDeleted +- Model +- ModelDeleted Methods: -- client.models.retrieve(model) -> Model -- client.models.list() -> ModelsPage -- client.models.del(model) -> ModelDeleted +- client.models.retrieve(model) -> Model +- client.models.list() -> ModelsPage +- client.models.del(model) -> ModelDeleted # FineTunes Types: -- FineTune -- FineTuneEvent -- FineTuneEventsListResponse +- FineTune +- FineTuneEvent +- FineTuneEventsListResponse Methods: -- client.fineTunes.create({ ...params }) -> FineTune -- client.fineTunes.retrieve(fineTuneId) -> FineTune -- client.fineTunes.list() -> FineTunesPage -- client.fineTunes.cancel(fineTuneId) -> FineTune -- client.fineTunes.listEvents(fineTuneId, { ...params }) -> FineTuneEventsListResponse +- client.fineTunes.create({ ...params }) -> FineTune +- client.fineTunes.retrieve(fineTuneId) -> FineTune +- client.fineTunes.list() -> FineTunesPage +- client.fineTunes.cancel(fineTuneId) -> FineTune +- client.fineTunes.listEvents(fineTuneId, { ...params }) -> FineTuneEventsListResponse diff --git a/build b/build index 4370f6ba7..b205edf8d 100755 --- a/build +++ b/build @@ -3,6 +3,8 @@ set -exuo pipefail node scripts/check-version.cjs +yarn tsc + # Build into dist and will publish the package from there, # so that src/resources/foo.ts becomes /resources/foo.js # This way importing from `"openai/resources/foo"` works @@ -12,7 +14,10 @@ rm -rf dist mkdir dist # Copy src to dist/src and build from dist/src into dist, so that # the source map for index.js.map will refer to ./src/index.ts etc -cp -rp src dist +cp -rp src README.md dist +for file in LICENSE CHANGELOG.md; do + if [ -e "${file}" ]; then cp "${file}" dist; fi +done # this converts the export map paths for the dist directory # and does a few other minor things node scripts/make-dist-package-json.cjs > dist/package.json @@ -21,7 +26,7 @@ node scripts/make-dist-package-json.cjs > dist/package.json tsc-multi # copy over handwritten .js/.mjs/.d.ts files cp src/_shims/*.{d.ts,js,mjs} dist/_shims -tsc-alias -p tsconfig.json --resolve-full-paths +tsc-alias -p tsconfig.build.json # we need to add exports = module.exports = OpenAI Node to index.js; # No way to get that from index.ts because it would cause compile errors # when building .mjs diff --git a/ecosystem-tests/vercel-edge/package-lock.json b/ecosystem-tests/vercel-edge/package-lock.json index 223fbc94f..935112892 100644 --- a/ecosystem-tests/vercel-edge/package-lock.json +++ b/ecosystem-tests/vercel-edge/package-lock.json @@ -14,7 +14,7 @@ "react-dom": "18.2.0" }, "devDependencies": { - "@types/node": "20.3.1", + "@types/node": "20.3.3", "@types/react": "18.2.13", "@types/react-dom": "18.2.6", "edge-runtime": "^2.4.3", @@ -3095,9 +3095,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "dev": true }, "node_modules/@types/node-fetch": { @@ -13739,9 +13739,9 @@ "dev": true }, "@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "dev": true }, "@types/node-fetch": { diff --git a/ecosystem-tests/vercel-edge/package.json b/ecosystem-tests/vercel-edge/package.json index 14ee1da4f..b44940e0e 100644 --- a/ecosystem-tests/vercel-edge/package.json +++ b/ecosystem-tests/vercel-edge/package.json @@ -17,7 +17,7 @@ "react-dom": "18.2.0" }, "devDependencies": { - "@types/node": "20.3.1", + "@types/node": "20.3.3", "@types/react": "18.2.13", "@types/react-dom": "18.2.6", "edge-runtime": "^2.4.3", diff --git a/examples/azure.ts b/examples/azure.ts new file mode 100644 index 000000000..0076d101d --- /dev/null +++ b/examples/azure.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env yarn tsn -T + +import OpenAI from 'openai'; + +// The name of your Azure OpenAI Resource. +// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource +const resource = ''; + +// Corresponds to your Model deployment within your OpenAI resource, e.g. my-gpt35-16k-deployment +// Navigate to the Azure OpenAI Studio to deploy a model. +const model = ''; + +const apiKey = process.env['AZURE_OPENAI_API_KEY']; +if (!apiKey) { + throw new Error('The AZURE_OPENAI_API_KEY environment variable is missing or empty.'); +} + +// Azure OpenAI requires a custom baseURL, api-version query param, and api-key header. +const openai = new OpenAI({ + apiKey, + baseURL: `https://${resource}.openai.azure.com/openai/deployments/${model}`, + defaultQuery: { 'api-version': '2023-06-01-preview' }, + defaultHeaders: { 'api-key': apiKey }, +}); + +async function main() { + console.log('Non-streaming:'); + const result = await openai.chat.completions.create({ + model, + messages: [{ role: 'user', content: 'Say hello!' }], + }); + console.log(result.choices[0]!.message?.content); + + console.log(); + console.log('Streaming:'); + const stream = await openai.chat.completions.create({ + model, + messages: [{ role: 'user', content: 'Say hello!' }], + stream: true, + }); + + for await (const part of stream) { + process.stdout.write(part.choices[0]?.delta?.content ?? ''); + } + process.stdout.write('\n'); +} + +main().catch(console.error); diff --git a/package.json b/package.json index dc852bda0..43e32027b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openai", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "description": "Client library for the OpenAI API", "author": "OpenAI ", "types": "dist/index.d.ts", diff --git a/scripts/replace-self-referencing-imports.js b/scripts/replace-self-referencing-imports.js new file mode 100644 index 000000000..de3246dd3 --- /dev/null +++ b/scripts/replace-self-referencing-imports.js @@ -0,0 +1,21 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); + +const path = require('path'); +const distSrcDir = path.resolve(__dirname, '..', 'dist', 'src'); + +function replaceSelfReferencingImports({ orig, file, config }) { + // replace self-referencing imports in source files to reduce errors users will + // see if they go to definition + if (!file.startsWith(distSrcDir)) return orig; + return orig.replace(/['"]([^"'\r\n]+)['"]/, (match, importPath) => { + if (!importPath.startsWith('openai/')) return match; + let relativePath = path.relative( + path.dirname(file), + path.join(distSrcDir, importPath.substring('openai/'.length)), + ); + if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`; + return JSON.stringify(relativePath); + }); +} +exports.default = replaceSelfReferencingImports; diff --git a/src/core.ts b/src/core.ts index 22f436ac8..50c7734a5 100644 --- a/src/core.ts +++ b/src/core.ts @@ -73,6 +73,8 @@ export abstract class APIClient { }; } + protected abstract defaultQuery(): DefaultQuery | undefined; + /** * Override this to add your own headers validation: */ @@ -130,9 +132,17 @@ export abstract class APIClient { const contentLength = typeof body === 'string' ? body.length.toString() : null; const url = this.buildURL(path!, query); - const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url); + if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); const timeout = options.timeout ?? this.timeout; - validatePositiveInteger('timeout', timeout); + const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url); + const minAgentTimeout = timeout + 1000; + if ((httpAgent as any)?.options && minAgentTimeout > ((httpAgent as any).options.timeout ?? 0)) { + // Allow any given request to bump our agent active socket timeout. + // This may seem strange, but leaking active sockets should be rare and not particularly problematic, + // and without mutating agent we would need to create more of them. + // This tradeoff optimizes for performance. + (httpAgent as any).options.timeout = minAgentTimeout; + } if (this.idempotencyHeader && method !== 'get') { if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey(); @@ -260,6 +270,11 @@ export abstract class APIClient { new URL(path) : new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); + const defaultQuery = this.defaultQuery(); + if (!isEmptyObj(defaultQuery)) { + query = { ...defaultQuery, ...query } as Req; + } + if (query) { url.search = qs.stringify(query, this.qsOptions()); } @@ -516,6 +531,7 @@ type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; export type RequestClient = { fetch: Fetch }; export type Headers = Record; +export type DefaultQuery = Record; export type KeysEnum = { [P in keyof Required]: true }; export type RequestOptions | Readable> = { @@ -564,6 +580,7 @@ export type FinalRequestOptions | Reada }; export type APIResponse = T & { + /** @deprecated - we plan to add a different way to access raw response information shortly. */ responseHeaders: Headers; }; diff --git a/src/index.ts b/src/index.ts index f960020dd..87063761e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,11 +13,52 @@ type Config = { * Defaults to process.env["OPENAI_API_KEY"]. */ apiKey?: string; + + /** + * Override the default base URL for the API, e.g., "https://api.example.com/v2/" + */ baseURL?: string; + + /** + * The maximum amount of time (in milliseconds) that the client should wait for a response + * from the server before timing out a single request. + * + * Note that request timeouts are retried by default, so in a worst-case scenario you may wait + * much longer than this timeout before the promise succeeds or fails. + */ timeout?: number; + + /** + * An HTTP agent used to manage HTTP(S) connections. + * + * If not provided, an agent will be constructed by default in the Node.js environment, + * otherwise no agent is used. + */ httpAgent?: Agent; + + /** + * The maximum number of times that the client will retry a request in case of a + * temporary failure, like a network error or a 5XX error from the server. + * + * @default 2 + */ maxRetries?: number; + + /** + * Default headers to include with every request to the API. + * + * These can be removed in individual requests by explicitly setting the + * header to `undefined` or `null` in request options. + */ defaultHeaders?: Core.Headers; + + /** + * Default query parameters to include with every request to the API. + * + * These can be removed in individual requests by explicitly setting the + * param to `undefined` in request options. + */ + defaultQuery?: Core.DefaultQuery; }; /** Instantiate the API Client. */ @@ -41,7 +82,7 @@ export class OpenAI extends Core.APIClient { super({ baseURL: options.baseURL!, - timeout: options.timeout, + timeout: options.timeout ?? 600000, httpAgent: options.httpAgent, maxRetries: options.maxRetries, }); @@ -60,6 +101,10 @@ export class OpenAI extends Core.APIClient { models: API.Models = new API.Models(this); fineTunes: API.FineTunes = new API.FineTunes(this); + protected override defaultQuery(): Core.DefaultQuery | undefined { + return this._options.defaultQuery; + } + protected override defaultHeaders(): Core.Headers { return { ...super.defaultHeaders(), diff --git a/src/resources/chat/completions.ts b/src/resources/chat/completions.ts index 1e78dd230..ffc94228b 100644 --- a/src/resources/chat/completions.ts +++ b/src/resources/chat/completions.ts @@ -229,7 +229,7 @@ export namespace CompletionCreateParams { * increase likelihood of selection; values like -100 or 100 should result in a ban * or exclusive selection of the relevant token. */ - logit_bias?: unknown | null; + logit_bias?: Record | null; /** * The maximum number of [tokens](/tokenizer) to generate in the chat completion. @@ -451,7 +451,7 @@ export namespace CompletionCreateParams { * increase likelihood of selection; values like -100 or 100 should result in a ban * or exclusive selection of the relevant token. */ - logit_bias?: unknown | null; + logit_bias?: Record | null; /** * The maximum number of [tokens](/tokenizer) to generate in the chat completion. diff --git a/src/resources/completions.ts b/src/resources/completions.ts index 7f93a423f..f123db24d 100644 --- a/src/resources/completions.ts +++ b/src/resources/completions.ts @@ -67,7 +67,7 @@ export namespace CompletionChoice { tokens?: Array; - top_logprobs?: Array; + top_logprobs?: Array>; } } @@ -145,7 +145,7 @@ export namespace CompletionCreateParams { * As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token * from being generated. */ - logit_bias?: unknown | null; + logit_bias?: Record | null; /** * Include the log probabilities on the `logprobs` most likely tokens, as well the @@ -310,7 +310,7 @@ export namespace CompletionCreateParams { * As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token * from being generated. */ - logit_bias?: unknown | null; + logit_bias?: Record | null; /** * Include the log probabilities on the `logprobs` most likely tokens, as well the diff --git a/src/resources/edits.ts b/src/resources/edits.ts index 66ca51347..5e9e44510 100644 --- a/src/resources/edits.ts +++ b/src/resources/edits.ts @@ -7,6 +7,10 @@ import * as API from './'; export class Edits extends APIResource { /** * Creates a new edit for the provided input, instruction, and parameters. + * + * @deprecated The Edits API is deprecated; please use Chat Completions instead. + * + * https://openai.com/blog/gpt-4-api-general-availability#deprecation-of-the-edits-api */ create(body: EditCreateParams, options?: Core.RequestOptions): Promise> { return this.post('/edits', { body, ...options }); @@ -42,7 +46,7 @@ export namespace Edit { tokens?: Array; - top_logprobs?: Array; + top_logprobs?: Array>; } } diff --git a/src/resources/files.ts b/src/resources/files.ts index 53e01ca6c..03fe57de1 100644 --- a/src/resources/files.ts +++ b/src/resources/files.ts @@ -53,6 +53,8 @@ export class Files extends APIResource { * Note: no pagination actually occurs yet, this is for forwards-compatibility. */ export class FileObjectsPage extends Page {} +// alias so we can export it in the namespace +type _FileObjectsPage = FileObjectsPage; export type FileContent = string; @@ -79,7 +81,7 @@ export interface FileObject { status?: string; - status_details?: unknown | null; + status_details?: string | null; } export interface FileCreateParams { @@ -106,6 +108,6 @@ export namespace Files { export import FileContent = API.FileContent; export import FileDeleted = API.FileDeleted; export import FileObject = API.FileObject; - export import FileObjectsPage = API.FileObjectsPage; + export type FileObjectsPage = _FileObjectsPage; export import FileCreateParams = API.FileCreateParams; } diff --git a/src/resources/fine-tunes.ts b/src/resources/fine-tunes.ts index 551674016..44e71888b 100644 --- a/src/resources/fine-tunes.ts +++ b/src/resources/fine-tunes.ts @@ -63,6 +63,7 @@ export class FineTunes extends APIResource { ): Promise>> { return this.get(`/fine-tunes/${fineTuneId}/events`, { query, + timeout: 86400000, ...options, stream: query?.stream ?? false, }); @@ -73,6 +74,8 @@ export class FineTunes extends APIResource { * Note: no pagination actually occurs yet, this is for forwards-compatibility. */ export class FineTunesPage extends Page {} +// alias so we can export it in the namespace +type _FineTunesPage = FineTunesPage; export interface FineTune { id: string; @@ -81,7 +84,7 @@ export interface FineTune { fine_tuned_model: string | null; - hyperparams: unknown; + hyperparams: FineTune.Hyperparams; model: string; @@ -102,6 +105,24 @@ export interface FineTune { events?: Array; } +export namespace FineTune { + export interface Hyperparams { + batch_size: number; + + learning_rate_multiplier: number; + + n_epochs: number; + + prompt_loss_weight: number; + + classification_n_classes?: number; + + classification_positive_class?: string; + + compute_classification_metrics?: boolean; + } +} + export interface FineTuneEvent { created_at: number; @@ -281,7 +302,7 @@ export namespace FineTunes { export import FineTune = API.FineTune; export import FineTuneEvent = API.FineTuneEvent; export import FineTuneEventsListResponse = API.FineTuneEventsListResponse; - export import FineTunesPage = API.FineTunesPage; + export type FineTunesPage = _FineTunesPage; export import FineTuneCreateParams = API.FineTuneCreateParams; export import FineTuneListEventsParams = API.FineTuneListEventsParams; } diff --git a/src/resources/models.ts b/src/resources/models.ts index 08a25ad9d..2b6ef9ad9 100644 --- a/src/resources/models.ts +++ b/src/resources/models.ts @@ -34,6 +34,8 @@ export class Models extends APIResource { * Note: no pagination actually occurs yet, this is for forwards-compatibility. */ export class ModelsPage extends Page {} +// alias so we can export it in the namespace +type _ModelsPage = ModelsPage; export interface Model { id: string; @@ -56,5 +58,5 @@ export interface ModelDeleted { export namespace Models { export import Model = API.Model; export import ModelDeleted = API.ModelDeleted; - export import ModelsPage = API.ModelsPage; + export type ModelsPage = _ModelsPage; } diff --git a/src/streaming.ts b/src/streaming.ts index 3de8e2ba2..46e93bc3c 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -1,69 +1,19 @@ import type { Response } from 'openai/_shims/fetch'; + import { APIResponse, Headers, createResponseHeaders } from './core'; +type Bytes = string | ArrayBuffer | Uint8Array | Buffer | null | undefined; + type ServerSentEvent = { event: string | null; data: string; raw: string[]; }; -class SSEDecoder { - private data: string[]; - private event: string | null; - private chunks: string[]; - - constructor() { - this.event = null; - this.data = []; - this.chunks = []; - } - - decode(line: string) { - if (line.endsWith('\r')) { - line = line.substring(0, line.length - 1); - } - - if (!line) { - // empty line and we didn't previously encounter any messages - if (!this.event && !this.data.length) return null; - - const sse: ServerSentEvent = { - event: this.event, - data: this.data.join('\n'), - raw: this.chunks, - }; - - this.event = null; - this.data = []; - this.chunks = []; - - return sse; - } - - this.chunks.push(line); - - if (line.startsWith(':')) { - return null; - } - - let [fieldname, _, value] = partition(line, ':'); - - if (value.startsWith(' ')) { - value = value.substring(1); - } - - if (fieldname === 'event') { - this.event = value; - } else if (fieldname === 'data') { - this.data.push(value); - } - - return null; - } -} - export class Stream implements AsyncIterable, APIResponse> { + /** @deprecated - please use the async iterator instead. We plan to add additional helper methods shortly. */ response: Response; + /** @deprecated - we plan to add a different way to access raw response information shortly. */ responseHeaders: Headers; controller: AbortController; @@ -81,21 +31,11 @@ export class Stream implements AsyncIterable, APIResponse(this.response.body); + for await (const chunk of iter) { + for (const line of lineDecoder.decode(chunk)) { const sse = this.decoder.decode(line); if (sse) yield sse; } @@ -135,7 +75,60 @@ export class Stream implements AsyncIterable, APIResponse(stream: any): AsyncIterableIterator { + if (stream[Symbol.asyncIterator]) return stream[Symbol.asyncIterator]; + + const reader = stream.getReader(); + return { + next() { + return reader.read(); + }, + async return() { + reader.cancel(); + reader.releaseLock(); + return { done: true, value: undefined }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; +} diff --git a/src/uploads.ts b/src/uploads.ts index 26cbcbab7..2b19b31da 100644 --- a/src/uploads.ts +++ b/src/uploads.ts @@ -96,7 +96,7 @@ export type ToFileInput = Uploadable | Exclude | AsyncIterable * @returns a {@link File} with the given properties */ export async function toFile( - value: ToFileInput, + value: ToFileInput | PromiseLike, name?: string | null | undefined, options: FilePropertyBag | undefined = {}, ): Promise { @@ -121,8 +121,9 @@ export async function toFile( return new File(bits, name, options); } -async function getBytes(value: ToFileInput): Promise> { - if (value instanceof Promise) return getBytes(await (value as any)); +async function getBytes(value: ToFileInput | PromiseLike): Promise> { + // resolve input promise or promiselike object + value = await value; let parts: Array = []; if ( diff --git a/src/version.ts b/src/version.ts index 4e42addf0..6c18f5dc0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '4.0.0-beta.2'; +export const VERSION = '4.0.0-beta.3'; diff --git a/tests/api-resources/chat/completions.test.ts b/tests/api-resources/chat/completions.test.ts index 96076cf1a..bdf82568e 100644 --- a/tests/api-resources/chat/completions.test.ts +++ b/tests/api-resources/chat/completions.test.ts @@ -26,7 +26,7 @@ describe('resource completions', () => { frequency_penalty: -2, function_call: 'none', functions: [{ name: 'string', description: 'string', parameters: { foo: 'bar' } }], - logit_bias: {}, + logit_bias: { foo: 0 }, max_tokens: 0, n: 1, presence_penalty: -2, diff --git a/tests/api-resources/completions.test.ts b/tests/api-resources/completions.test.ts index 6d6ac8347..0067223fb 100644 --- a/tests/api-resources/completions.test.ts +++ b/tests/api-resources/completions.test.ts @@ -16,7 +16,7 @@ describe('resource completions', () => { best_of: 0, echo: true, frequency_penalty: -2, - logit_bias: {}, + logit_bias: { foo: 0 }, logprobs: 0, max_tokens: 16, n: 1, diff --git a/tests/index.test.ts b/tests/index.test.ts index cf112ea61..30b0f80b3 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -17,11 +17,64 @@ describe('instantiate client', () => { process.env = env; }); - test('defaultHeaders are passed through', () => { - const client = new OpenAI({ defaultHeaders: { 'X-My-Default-Header': '2' }, apiKey: 'my api key' }); + describe('defaultHeaders', () => { + const client = new OpenAI({ + baseURL: 'http://localhost:5000/', + defaultHeaders: { 'X-My-Default-Header': '2' }, + apiKey: 'my api key', + }); + + test('they are used in the request', () => { + const { req } = client.buildRequest({ path: '/foo', method: 'post' }); + expect((req.headers as Headers)['X-My-Default-Header']).toEqual('2'); + }); - const { req } = client.buildRequest({ path: '/foo', method: 'post' }); - expect((req.headers as Headers)['X-My-Default-Header']).toEqual('2'); + test('can be overriden with `undefined`', () => { + const { req } = client.buildRequest({ + path: '/foo', + method: 'post', + headers: { 'X-My-Default-Header': undefined }, + }); + expect((req.headers as Headers)['X-My-Default-Header']).toBeUndefined(); + }); + + test('can be overriden with `null`', () => { + const { req } = client.buildRequest({ + path: '/foo', + method: 'post', + headers: { 'X-My-Default-Header': null }, + }); + expect((req.headers as Headers)['X-My-Default-Header']).toBeUndefined(); + }); + }); + + describe('defaultQuery', () => { + test('with null query params given', () => { + const client = new OpenAI({ + baseURL: 'http://localhost:5000/', + defaultQuery: { apiVersion: 'foo' }, + apiKey: 'my api key', + }); + expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo'); + }); + + test('multiple default query params', () => { + const client = new OpenAI({ + baseURL: 'http://localhost:5000/', + defaultQuery: { apiVersion: 'foo', hello: 'world' }, + apiKey: 'my api key', + }); + expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo&hello=world'); + }); + + test('overriding with `undefined`', () => { + const client = new OpenAI({ + baseURL: 'http://localhost:5000/', + defaultQuery: { hello: 'world' }, + apiKey: 'my api key', + }); + expect(client.buildURL('/foo', { hello: undefined })).toEqual('http://localhost:5000/foo'); + }); }); describe('baseUrl', () => { diff --git a/tsconfig.build.json b/tsconfig.build.json index ca9e53425..c3660e68f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,43 +1,32 @@ { + "extends": "./tsconfig.json", "include": ["dist/src"], "exclude": [], "compilerOptions": { - "target": "es2019", - "lib": ["es2020"], - "module": "commonjs", - "moduleResolution": "node", - "esModuleInterop": true, "rootDir": "./dist/src", - "baseUrl": "./", "paths": { "openai/_shims/*": ["dist/src/_shims/*.node"], "openai": ["dist/src/index.ts"], "openai/*": ["dist/src/*"], "digest-fetch": ["./typings/digest-fetch"] }, - + "noEmit": false, "declaration": true, "declarationMap": true, "outDir": "dist", "pretty": true, - "sourceMap": true, - "resolveJsonModule": true, - - "forceConsistentCasingInFileNames": true, - - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "strictBindCallApply": true, - "strictPropertyInitialization": true, - "noImplicitThis": true, - "alwaysStrict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - - "skipLibCheck": true + "sourceMap": true + }, + "tsc-alias": { + "resolveFullPaths": true, + "fileExtensions": { + "inputGlob": "{mjs,cjs,js,jsx,mts,cts,ts,tsx}" + }, + "replacers": { + "replace-absolute-imports": { + "enabled": true, + "file": "./scripts/replace-self-referencing-imports.js" + } + } } } diff --git a/tsconfig.json b/tsconfig.json index c663d7307..82ea3f3e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, - "rootDir": "./src", "baseUrl": "./", "paths": { "openai/_shims/*": ["src/_shims/*.node"], @@ -14,12 +13,8 @@ "openai/*": ["src/*"], "digest-fetch": ["./typings/digest-fetch"] }, + "noEmit": true, - "declaration": true, - "declarationMap": true, - "outDir": "dist", - "pretty": true, - "sourceMap": true, "resolveJsonModule": true, "forceConsistentCasingInFileNames": true,