From e950ad969e845d608ed71bd3e3095cd6c941d93d Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:40:22 -0500 Subject: [PATCH 1/7] docs: improve audio example to show how to stream to a file (#598) --- examples/audio.ts | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/examples/audio.ts b/examples/audio.ts index e4ab930fd..a37c93b20 100755 --- a/examples/audio.ts +++ b/examples/audio.ts @@ -1,7 +1,8 @@ #!/usr/bin/env -S npm run tsn -T +import 'openai/shims/node'; import OpenAI, { toFile } from 'openai'; -import fs from 'fs/promises'; +import fs from 'fs'; import path from 'path'; // gets API Key from environment variable OPENAI_API_KEY @@ -10,6 +11,26 @@ const openai = new OpenAI(); const speechFile = path.resolve(__dirname, './speech.mp3'); async function main() { + await streamingDemoNode(); + await blockingDemo(); +} +main(); + +async function streamingDemoNode() { + const response = await openai.audio.speech.create({ + model: 'tts-1', + voice: 'alloy', + input: 'the quick brown chicken jumped over the lazy dogs', + }); + + const stream = response.body; + + console.log(`Streaming response to ${speechFile}`); + await streamToFile(stream, speechFile); + console.log('Finished streaming'); +} + +async function blockingDemo() { const mp3 = await openai.audio.speech.create({ model: 'tts-1', voice: 'alloy', @@ -17,7 +38,7 @@ async function main() { }); const buffer = Buffer.from(await mp3.arrayBuffer()); - await fs.writeFile(speechFile, buffer); + await fs.promises.writeFile(speechFile, buffer); const transcription = await openai.audio.transcriptions.create({ file: await toFile(buffer, 'speech.mp3'), @@ -32,4 +53,21 @@ async function main() { console.log(translation.text); } -main(); +/** + * Note, this is Node-specific. + * + * Other runtimes would need a different `fs`, + * and would also use a web ReadableStream, + * which is different from a Node ReadableStream. + */ +async function streamToFile(stream: NodeJS.ReadableStream, path: fs.PathLike) { + return new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(path).on('error', reject).on('finish', resolve); + + // If you don't see a `stream.pipe` method and you're using Node you might need to add `import 'openai/shims/node'` at the top of your entrypoint file. + stream.pipe(writeStream).on('error', (error) => { + writeStream.close(); + reject(error); + }); + }); +} From 1934fa15f654ea89e226457f76febe6015616f6c Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 25 Dec 2023 02:09:57 -0500 Subject: [PATCH 2/7] docs: fix docstring typos (#600) --- src/resources/beta/assistants/assistants.ts | 2 +- src/resources/moderations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/beta/assistants/assistants.ts b/src/resources/beta/assistants/assistants.ts index 3b13a84bb..08abb2c91 100644 --- a/src/resources/beta/assistants/assistants.ts +++ b/src/resources/beta/assistants/assistants.ts @@ -268,7 +268,7 @@ export interface AssistantUpdateParams { * A list of [File](https://platform.openai.com/docs/api-reference/files) IDs * attached to this assistant. There can be a maximum of 20 files attached to the * assistant. Files are ordered by their creation date in ascending order. If a - * file was previosuly attached to the list but does not show up in the list, it + * file was previously attached to the list but does not show up in the list, it * will be deleted from the assistant. */ file_ids?: Array; diff --git a/src/resources/moderations.ts b/src/resources/moderations.ts index 5beda53ac..8bde6ecca 100644 --- a/src/resources/moderations.ts +++ b/src/resources/moderations.ts @@ -55,7 +55,7 @@ export namespace Moderation { * Content that expresses, incites, or promotes hate based on race, gender, * ethnicity, religion, nationality, sexual orientation, disability status, or * caste. Hateful content aimed at non-protected groups (e.g., chess players) is - * harrassment. + * harassment. */ hate: boolean; From 045ee74fd3ffba9e6d1301fe1ffd8bd3c63720a2 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:35:28 -0500 Subject: [PATCH 3/7] chore(internal): bump license (#605) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7b1b36a64..621a6becf 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 OpenAI + Copyright 2024 OpenAI Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4ea159f0aa9a1f4c365c74ee726714fe692ddf9f Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:05:47 -0500 Subject: [PATCH 4/7] fix(headers): always send lowercase headers and strip undefined (BREAKING in rare cases) (#608) BREAKING: If you previously sent `My-Header: foo` and `my-header: bar`, both would get sent. Now, only one will. If you previously sent `My-Header: undefined`, it would send as such. Now, the header will not be sent. --- src/core.ts | 63 +++++++++++++++++++++++++++++++++++---------- tests/index.test.ts | 29 ++++++++++++++++----- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/core.ts b/src/core.ts index 70b8e679c..eafddc517 100644 --- a/src/core.ts +++ b/src/core.ts @@ -301,18 +301,7 @@ export abstract class APIClient { headers[this.idempotencyHeader] = options.idempotencyKey; } - const reqHeaders: Record = { - ...(contentLength && { 'Content-Length': contentLength }), - ...this.defaultHeaders(options), - ...headers, - }; - // let builtin fetch set the Content-Type for multipart bodies - if (isMultipartBody(options.body) && shimsKind !== 'node') { - delete reqHeaders['Content-Type']; - } - - // Strip any headers being explicitly omitted with null - Object.keys(reqHeaders).forEach((key) => reqHeaders[key] === null && delete reqHeaders[key]); + const reqHeaders = this.buildHeaders({ options, headers, contentLength }); const req: RequestInit = { method, @@ -324,9 +313,35 @@ export abstract class APIClient { signal: options.signal ?? null, }; + return { req, url, timeout }; + } + + private buildHeaders({ + options, + headers, + contentLength, + }: { + options: FinalRequestOptions; + headers: Record; + contentLength: string | null | undefined; + }): Record { + const reqHeaders: Record = {}; + if (contentLength) { + reqHeaders['content-length'] = contentLength; + } + + const defaultHeaders = this.defaultHeaders(options); + applyHeadersMut(reqHeaders, defaultHeaders); + applyHeadersMut(reqHeaders, headers); + + // let builtin fetch set the Content-Type for multipart bodies + if (isMultipartBody(options.body) && shimsKind !== 'node') { + delete reqHeaders['content-type']; + } + this.validateHeaders(reqHeaders, headers); - return { req, url, timeout }; + return reqHeaders; } /** @@ -1013,6 +1028,28 @@ export function hasOwn(obj: Object, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } +/** + * Copies headers from "newHeaders" onto "targetHeaders", + * using lower-case for all properties, + * ignoring any keys with undefined values, + * and deleting any keys with null values. + */ +function applyHeadersMut(targetHeaders: Headers, newHeaders: Headers): void { + for (const k in newHeaders) { + if (!hasOwn(newHeaders, k)) continue; + const lowerKey = k.toLowerCase(); + if (!lowerKey) continue; + + const val = newHeaders[k]; + + if (val === null) { + delete targetHeaders[lowerKey]; + } else if (val !== undefined) { + targetHeaders[lowerKey] = val; + } + } +} + export function debug(action: string, ...args: any[]) { if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') { console.log(`OpenAI:DEBUG:${action}`, ...args); diff --git a/tests/index.test.ts b/tests/index.test.ts index 538f7dfc9..e056d3b85 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -28,25 +28,25 @@ describe('instantiate client', () => { 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'); + expect((req.headers as Headers)['x-my-default-header']).toEqual('2'); }); - test('can be overriden with `undefined`', () => { + test('can ignore `undefined` and leave the default', () => { const { req } = client.buildRequest({ path: '/foo', method: 'post', headers: { 'X-My-Default-Header': undefined }, }); - expect((req.headers as Headers)['X-My-Default-Header']).toBeUndefined(); + expect((req.headers as Headers)['x-my-default-header']).toEqual('2'); }); - test('can be overriden with `null`', () => { + test('can be removed 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(); + expect(req.headers as Headers).not.toHaveProperty('x-my-default-header'); }); }); @@ -179,12 +179,27 @@ describe('request building', () => { describe('Content-Length', () => { test('handles multi-byte characters', () => { const { req } = client.buildRequest({ path: '/foo', method: 'post', body: { value: '—' } }); - expect((req.headers as Record)['Content-Length']).toEqual('20'); + expect((req.headers as Record)['content-length']).toEqual('20'); }); test('handles standard characters', () => { const { req } = client.buildRequest({ path: '/foo', method: 'post', body: { value: 'hello' } }); - expect((req.headers as Record)['Content-Length']).toEqual('22'); + expect((req.headers as Record)['content-length']).toEqual('22'); + }); + }); + + describe('custom headers', () => { + test('handles undefined', () => { + const { req } = client.buildRequest({ + path: '/foo', + method: 'post', + body: { value: 'hello' }, + headers: { 'X-Foo': 'baz', 'x-foo': 'bar', 'x-Foo': undefined, 'x-baz': 'bam', 'X-Baz': null }, + }); + expect((req.headers as Record)['x-foo']).toEqual('bar'); + expect((req.headers as Record)['x-Foo']).toEqual(undefined); + expect((req.headers as Record)['X-Foo']).toEqual(undefined); + expect((req.headers as Record)['x-baz']).toEqual(undefined); }); }); }); From e1ccc82e4991262a631dcffa4d09bdc553e50fbb Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:22:39 -0500 Subject: [PATCH 5/7] chore(internal): improve type signatures (#609) --- src/_shims/index-deno.ts | 2 +- src/_shims/index.d.ts | 2 +- src/_shims/node-runtime.ts | 2 +- src/_shims/registry.ts | 2 +- src/_shims/web-runtime.ts | 2 +- src/core.ts | 32 +++++++++++++++----------------- src/uploads.ts | 4 ++-- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/_shims/index-deno.ts b/src/_shims/index-deno.ts index b838e5f22..d9eabb5a9 100644 --- a/src/_shims/index-deno.ts +++ b/src/_shims/index-deno.ts @@ -64,7 +64,7 @@ const _Blob = Blob; type _Blob = Blob; export { _Blob as Blob }; -export async function getMultipartRequestOptions>( +export async function getMultipartRequestOptions>( form: FormData, opts: RequestOptions, ): Promise> { diff --git a/src/_shims/index.d.ts b/src/_shims/index.d.ts index 4e52b952e..d867b293b 100644 --- a/src/_shims/index.d.ts +++ b/src/_shims/index.d.ts @@ -65,7 +65,7 @@ export type ReadableStream = SelectType; -export function getMultipartRequestOptions>( +export function getMultipartRequestOptions>( form: FormData, opts: RequestOptions, ): Promise>; diff --git a/src/_shims/node-runtime.ts b/src/_shims/node-runtime.ts index 7d24b7077..a9c42ebeb 100644 --- a/src/_shims/node-runtime.ts +++ b/src/_shims/node-runtime.ts @@ -43,7 +43,7 @@ async function fileFromPath(path: string, ...args: any[]): Promise { const defaultHttpAgent: Agent = new KeepAliveAgent({ keepAlive: true, timeout: 5 * 60 * 1000 }); const defaultHttpsAgent: Agent = new KeepAliveAgent.HttpsAgent({ keepAlive: true, timeout: 5 * 60 * 1000 }); -async function getMultipartRequestOptions>( +async function getMultipartRequestOptions>( form: fd.FormData, opts: RequestOptions, ): Promise> { diff --git a/src/_shims/registry.ts b/src/_shims/registry.ts index 65b570d0c..1fa39642e 100644 --- a/src/_shims/registry.ts +++ b/src/_shims/registry.ts @@ -13,7 +13,7 @@ export interface Shims { Blob: any; File: any; ReadableStream: any; - getMultipartRequestOptions: >( + getMultipartRequestOptions: >( form: Shims['FormData'], opts: RequestOptions, ) => Promise>; diff --git a/src/_shims/web-runtime.ts b/src/_shims/web-runtime.ts index 92dadeb89..a5e90bcf8 100644 --- a/src/_shims/web-runtime.ts +++ b/src/_shims/web-runtime.ts @@ -84,7 +84,7 @@ export function getRuntime({ manuallyImported }: { manuallyImported?: boolean } } } ), - getMultipartRequestOptions: async >( + getMultipartRequestOptions: async >( // @ts-ignore form: FormData, opts: RequestOptions, diff --git a/src/core.ts b/src/core.ts index eafddc517..9e3cb2b43 100644 --- a/src/core.ts +++ b/src/core.ts @@ -217,27 +217,27 @@ export abstract class APIClient { return `stainless-node-retry-${uuid4()}`; } - get(path: string, opts?: PromiseOrValue>): APIPromise { + get(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('get', path, opts); } - post(path: string, opts?: PromiseOrValue>): APIPromise { + post(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('post', path, opts); } - patch(path: string, opts?: PromiseOrValue>): APIPromise { + patch(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('patch', path, opts); } - put(path: string, opts?: PromiseOrValue>): APIPromise { + put(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('put', path, opts); } - delete(path: string, opts?: PromiseOrValue>): APIPromise { + delete(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('delete', path, opts); } - private methodRequest( + private methodRequest( method: HTTPMethod, path: string, opts?: PromiseOrValue>, @@ -269,9 +269,7 @@ export abstract class APIClient { return null; } - buildRequest( - options: FinalRequestOptions, - ): { req: RequestInit; url: string; timeout: number } { + buildRequest(options: FinalRequestOptions): { req: RequestInit; url: string; timeout: number } { const { method, path, query, headers: headers = {} } = options; const body = @@ -373,15 +371,15 @@ export abstract class APIClient { return APIError.generate(status, error, message, headers); } - request( + request( options: PromiseOrValue>, remainingRetries: number | null = null, ): APIPromise { return new APIPromise(this.makeRequest(options, remainingRetries)); } - private async makeRequest( - optionsInput: PromiseOrValue, + private async makeRequest( + optionsInput: PromiseOrValue>, retriesRemaining: number | null, ): Promise { const options = await optionsInput; @@ -443,7 +441,7 @@ export abstract class APIClient { return new PagePromise(this, request, Page); } - buildURL>(path: string, query: Req | null | undefined): string { + buildURL(path: string, query: Req | null | undefined): string { const url = isAbsoluteURL(path) ? new URL(path) @@ -617,7 +615,7 @@ export abstract class AbstractPage implements AsyncIterable { ); } const nextOptions = { ...this.options }; - if ('params' in nextInfo) { + if ('params' in nextInfo && typeof nextOptions.query === 'object') { nextOptions.query = { ...nextOptions.query, ...nextInfo.params }; } else if ('url' in nextInfo) { const params = [...Object.entries(nextOptions.query || {}), ...nextInfo.url.searchParams.entries()]; @@ -715,7 +713,7 @@ export type Headers = Record; export type DefaultQuery = Record; export type KeysEnum = { [P in keyof Required]: true }; -export type RequestOptions | Readable> = { +export type RequestOptions | Readable> = { method?: HTTPMethod; path?: string; query?: Req | undefined; @@ -752,7 +750,7 @@ const requestOptionsKeys: KeysEnum = { __binaryResponse: true, }; -export const isRequestOptions = (obj: unknown): obj is RequestOptions | Readable> => { +export const isRequestOptions = (obj: unknown): obj is RequestOptions => { return ( typeof obj === 'object' && obj !== null && @@ -761,7 +759,7 @@ export const isRequestOptions = (obj: unknown): obj is RequestOptions | Readable> = RequestOptions & { +export type FinalRequestOptions | Readable> = RequestOptions & { method: HTTPMethod; path: string; }; diff --git a/src/uploads.ts b/src/uploads.ts index bc2afefa2..2398baf35 100644 --- a/src/uploads.ts +++ b/src/uploads.ts @@ -184,7 +184,7 @@ export const isMultipartBody = (body: any): body is MultipartBody => * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. * Otherwise returns the request as is. */ -export const maybeMultipartFormRequestOptions = async >( +export const maybeMultipartFormRequestOptions = async >( opts: RequestOptions, ): Promise> => { if (!hasUploadableValue(opts.body)) return opts; @@ -193,7 +193,7 @@ export const maybeMultipartFormRequestOptions = async >( +export const multipartFormRequestOptions = async >( opts: RequestOptions, ): Promise> => { const form = await createForm(opts.body); From 5e0f733d3cd3c8e6d41659141168cd0708e017a3 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:27:29 -0500 Subject: [PATCH 6/7] chore: add .keep files for examples and custom code directories (#612) --- examples/.keep | 4 ++++ src/lib/.keep | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 examples/.keep create mode 100644 src/lib/.keep diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 000000000..0651c89c0 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. diff --git a/src/lib/.keep b/src/lib/.keep new file mode 100644 index 000000000..7554f8b20 --- /dev/null +++ b/src/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. From dbf7695667b43f5074a503d49f3f18dab603f9be Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:27:54 -0500 Subject: [PATCH 7/7] release: 4.24.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 2 +- build-deno | 2 +- package.json | 2 +- src/version.ts | 2 +- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f33e27aba..52d941c80 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.24.1" + ".": "4.24.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 73926f94c..76b83a93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 4.24.2 (2024-01-08) + +Full Changelog: [v4.24.1...v4.24.2](https://github.com/openai/openai-node/compare/v4.24.1...v4.24.2) + +### Bug Fixes + +* **headers:** always send lowercase headers and strip undefined (BREAKING in rare cases) ([#608](https://github.com/openai/openai-node/issues/608)) ([4ea159f](https://github.com/openai/openai-node/commit/4ea159f0aa9a1f4c365c74ee726714fe692ddf9f)) + + +### Chores + +* add .keep files for examples and custom code directories ([#612](https://github.com/openai/openai-node/issues/612)) ([5e0f733](https://github.com/openai/openai-node/commit/5e0f733d3cd3c8e6d41659141168cd0708e017a3)) +* **internal:** bump license ([#605](https://github.com/openai/openai-node/issues/605)) ([045ee74](https://github.com/openai/openai-node/commit/045ee74fd3ffba9e6d1301fe1ffd8bd3c63720a2)) +* **internal:** improve type signatures ([#609](https://github.com/openai/openai-node/issues/609)) ([e1ccc82](https://github.com/openai/openai-node/commit/e1ccc82e4991262a631dcffa4d09bdc553e50fbb)) + + +### Documentation + +* fix docstring typos ([#600](https://github.com/openai/openai-node/issues/600)) ([1934fa1](https://github.com/openai/openai-node/commit/1934fa15f654ea89e226457f76febe6015616f6c)) +* improve audio example to show how to stream to a file ([#598](https://github.com/openai/openai-node/issues/598)) ([e950ad9](https://github.com/openai/openai-node/commit/e950ad969e845d608ed71bd3e3095cd6c941d93d)) + ## 4.24.1 (2023-12-22) Full Changelog: [v4.24.0...v4.24.1](https://github.com/openai/openai-node/compare/v4.24.0...v4.24.1) diff --git a/README.md b/README.md index bd32cd053..c538aa457 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can import in Deno via: ```ts -import OpenAI from 'https://deno.land/x/openai@v4.24.1/mod.ts'; +import OpenAI from 'https://deno.land/x/openai@v4.24.2/mod.ts'; ``` diff --git a/build-deno b/build-deno index 37d62c7da..60212c0ff 100755 --- a/build-deno +++ b/build-deno @@ -14,7 +14,7 @@ This is a build produced from https://github.com/openai/openai-node – please g Usage: \`\`\`ts -import OpenAI from "https://deno.land/x/openai@v4.24.1/mod.ts"; +import OpenAI from "https://deno.land/x/openai@v4.24.2/mod.ts"; const client = new OpenAI(); \`\`\` diff --git a/package.json b/package.json index 7a88ec1cf..13038f6da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openai", - "version": "4.24.1", + "version": "4.24.2", "description": "The official TypeScript library for the OpenAI API", "author": "OpenAI ", "types": "dist/index.d.ts", diff --git a/src/version.ts b/src/version.ts index 0cd107b3c..d2fe00e18 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '4.24.1'; // x-release-please-version +export const VERSION = '4.24.2'; // x-release-please-version