diff --git a/README.md b/README.md index 4cdebfe0a..e280c2354 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,15 @@ Error codes are as followed: | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | +### Azure OpenAI + +An example of using this library with Azure OpenAI can be found [here](https://github.com/openai/openai-node/blob/master/examples/azure.ts). + +Please note there are subtle differences in API shape & behavior between the Azure OpenAI API and the OpenAI API, +so using this library with Azure OpenAI may result in incorrect types, which can lead to bugs. + +See [`@azure/openai`](https://www.npmjs.com/package/@azure/openai) for an Azure-specific SDK provided by Microsoft. + ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. @@ -206,6 +215,37 @@ On timeout, an `APIConnectionTimeoutError` is thrown. Note that requests which time out will be [retried twice by default](#retries). +## Auto-pagination + +List methods in the OpenAI API are paginated. +You can use `for await … of` syntax to iterate through items across all pages: + +```ts +async function fetchAllFineTuningJobs(params) { + const allFineTuningJobs = []; + // Automatically fetches more pages as needed. + for await (const job of openai.fineTuning.jobs.list({ limit: 20 })) { + allFineTuningJobs.push(job); + } + return allFineTuningJobs; +} +``` + +Alternatively, you can make request a single page at a time: + +```ts +let page = await openai.fineTuning.jobs.list({ limit: 20 }); +for (const job of page.data) { + console.log(job); +} + +// Convenience methods are provided for manually paginating: +while (page.hasNextPage()) { + page = page.getNextPage(); + // ... +} +``` + ## Advanced Usage ### Accessing raw Response data (e.g., headers) diff --git a/bin/cli b/bin/cli index c2d110f19..00275ace4 100755 --- a/bin/cli +++ b/bin/cli @@ -1,21 +1,49 @@ -#!/usr/bin/env bash -set -eou pipefail - -if [ $# -eq 0 ]; then - echo "Usage: $0 " - echo - echo "Subcommands:" - echo " migrate Run migrations to update from openai v3 to v4" - echo - exit 1 -fi - -if [ "$1" = "migrate" ]; then - echo "This automatic code migration is provided by grit.io" - echo "Visit https://app.grit.io/studio?preset=openai_v4 for more details." - shift - npx -y @getgrit/launcher apply openai_v4 "$@" -else - echo "Unknown subcommand $1; Expected 'migrate'" >&2 - exit 1 -fi +#!/usr/bin/env node + +const { spawnSync } = require('child_process'); + +const commands = { + migrate: { + description: 'Run migrations to update from openai v3 to v4', + fn: () => { + console.log('This automatic code migration is provided by grit.io'); + console.log('Visit https://app.grit.io/studio?preset=openai_v4 for more details.') + + const result = spawnSync( + 'npx', + ['-y', '@getgrit/launcher', 'apply', 'openai_v4', ...process.argv.slice(3)], + { stdio: 'inherit' }, + ); + if (result.status !== 0) { + process.exit(result.status); + } + } + } +} + +function exitWithHelp() { + console.log("Usage: $0 "); + console.log(); + console.log('Subcommands:'); + + for (const [name, info] of Object.entries(commands)) { + console.log(` ${name} ${info.description}`); + } + + console.log(); + process.exit(1); +} + +if (process.argv.length < 3) { + exitWithHelp(); +} + +const commandName = process.argv[2]; + +const command = commands[commandName]; +if (!command) { + console.log(`Unknown subcommand ${commandName}.`); + exitWithHelp(); +} + +command.fn(); diff --git a/examples/azure.ts b/examples/azure.ts index 058143885..a903cfd6e 100755 --- a/examples/azure.ts +++ b/examples/azure.ts @@ -10,6 +10,9 @@ const resource = ''; // Navigate to the Azure OpenAI Studio to deploy a model. const model = ''; +// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning +const apiVersion = '2023-06-01-preview'; + const apiKey = process.env['AZURE_OPENAI_API_KEY']; if (!apiKey) { throw new Error('The AZURE_OPENAI_API_KEY environment variable is missing or empty.'); @@ -19,7 +22,7 @@ if (!apiKey) { const openai = new OpenAI({ apiKey, baseURL: `https://${resource}.openai.azure.com/openai/deployments/${model}`, - defaultQuery: { 'api-version': '2023-06-01-preview' }, + defaultQuery: { 'api-version': apiVersion }, defaultHeaders: { 'api-key': apiKey }, }); diff --git a/src/core.ts b/src/core.ts index 814370617..db7cb7269 100644 --- a/src/core.ts +++ b/src/core.ts @@ -9,6 +9,7 @@ import { type RequestInfo, type RequestInit, type Response, + type HeadersInit, } from 'openai/_shims/fetch'; export { type Response }; import { isMultipartBody } from './uploads'; @@ -153,7 +154,7 @@ export abstract class APIClient { this.fetch = overridenFetch ?? fetch; } - protected authHeaders(): Headers { + protected authHeaders(opts: FinalRequestOptions): Headers { return {}; } @@ -165,13 +166,13 @@ export abstract class APIClient { * Authorization: 'Bearer 123', * } */ - protected defaultHeaders(): Headers { + protected defaultHeaders(opts: FinalRequestOptions): Headers { return { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': this.getUserAgent(), ...getPlatformHeaders(), - ...this.authHeaders(), + ...this.authHeaders(opts), }; } @@ -272,7 +273,7 @@ export abstract class APIClient { const reqHeaders: Record = { ...(contentLength && { 'Content-Length': contentLength }), - ...this.defaultHeaders(), + ...this.defaultHeaders(options), ...headers, }; // let builtin fetch set the Content-Type for multipart bodies @@ -304,7 +305,18 @@ export abstract class APIClient { * This is useful for cases where you want to add certain headers based off of * the request properties, e.g. `method` or `url`. */ - protected async prepareRequest(request: RequestInit, { url }: { url: string }): Promise {} + protected async prepareRequest( + request: RequestInit, + { url, options }: { url: string; options: FinalRequestOptions }, + ): Promise {} + + protected parseHeaders(headers: HeadersInit | null | undefined): Record { + return ( + !headers ? {} + : Symbol.iterator in headers ? Object.fromEntries(Array.from(headers).map((header) => [...header])) + : { ...headers } + ); + } protected makeStatusError( status: number | undefined, @@ -333,7 +345,7 @@ export abstract class APIClient { const { req, url, timeout } = this.buildRequest(options); - await this.prepareRequest(req, { url }); + await this.prepareRequest(req, { url, options }); debug('request', url, options, req.headers); diff --git a/src/index.ts b/src/index.ts index 6add24d69..e8013a515 100644 --- a/src/index.ts +++ b/src/index.ts @@ -150,15 +150,15 @@ export class OpenAI extends Core.APIClient { return this._options.defaultQuery; } - protected override defaultHeaders(): Core.Headers { + protected override defaultHeaders(opts: Core.FinalRequestOptions): Core.Headers { return { - ...super.defaultHeaders(), + ...super.defaultHeaders(opts), 'OpenAI-Organization': this.organization, ...this._options.defaultHeaders, }; } - protected override authHeaders(): Core.Headers { + protected override authHeaders(opts: Core.FinalRequestOptions): Core.Headers { return { Authorization: `Bearer ${this.apiKey}` }; } @@ -206,6 +206,10 @@ export namespace OpenAI { export import Page = Pagination.Page; export import PageResponse = Pagination.PageResponse; + export import CursorPage = Pagination.CursorPage; + export import CursorPageParams = Pagination.CursorPageParams; + export import CursorPageResponse = Pagination.CursorPageResponse; + export import Completions = API.Completions; export import Completion = API.Completion; export import CompletionChoice = API.CompletionChoice; diff --git a/src/pagination.ts b/src/pagination.ts index cc6c804ca..a9ffb1f47 100644 --- a/src/pagination.ts +++ b/src/pagination.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. -import { AbstractPage, Response, APIClient, FinalRequestOptions } from './core'; +import { AbstractPage, Response, APIClient, FinalRequestOptions, PageInfo } from './core'; export interface PageResponse { data: Array; @@ -40,3 +40,61 @@ export class Page extends AbstractPage implements PageResponse return null; } } + +export interface CursorPageResponse { + data: Array; +} + +export interface CursorPageParams { + /** + * Identifier for the last job from the previous pagination request. + */ + after?: string; + + /** + * Number of fine-tuning jobs to retrieve. + */ + limit?: number; +} + +export class CursorPage + extends AbstractPage + implements CursorPageResponse +{ + data: Array; + + constructor( + client: APIClient, + response: Response, + body: CursorPageResponse, + options: FinalRequestOptions, + ) { + super(client, response, body, options); + + this.data = body.data; + } + + getPaginatedItems(): Item[] { + return this.data; + } + + // @deprecated Please use `nextPageInfo()` instead + nextPageParams(): Partial | null { + const info = this.nextPageInfo(); + if (!info) return null; + if ('params' in info) return info.params; + const params = Object.fromEntries(info.url.searchParams); + if (!Object.keys(params).length) return null; + return params; + } + + nextPageInfo(): PageInfo | null { + if (!this.data?.length) { + return null; + } + + const next = this.data[this.data.length - 1]?.id; + if (!next) return null; + return { params: { after: next } }; + } +} diff --git a/src/resources/fine-tuning/jobs.ts b/src/resources/fine-tuning/jobs.ts index 4fa64c85d..684da758f 100644 --- a/src/resources/fine-tuning/jobs.ts +++ b/src/resources/fine-tuning/jobs.ts @@ -5,7 +5,7 @@ import { APIResource } from 'openai/resource'; import { isRequestOptions } from 'openai/core'; import * as Files from 'openai/resources/files'; import * as API from './index'; -import { Page } from 'openai/pagination'; +import { CursorPage, CursorPageParams } from 'openai/pagination'; export class Jobs extends APIResource { /** @@ -81,17 +81,11 @@ export class Jobs extends APIResource { } } -/** - * Note: no pagination actually occurs yet, this is for forwards-compatibility. - */ -export class FineTuningJobsPage extends Page {} +export class FineTuningJobsPage extends CursorPage {} // alias so we can export it in the namespace type _FineTuningJobsPage = FineTuningJobsPage; -/** - * Note: no pagination actually occurs yet, this is for forwards-compatibility. - */ -export class FineTuningJobEventsPage extends Page {} +export class FineTuningJobEventsPage extends CursorPage {} // alias so we can export it in the namespace type _FineTuningJobEventsPage = FineTuningJobEventsPage; @@ -258,29 +252,9 @@ export namespace JobCreateParams { } } -export interface JobListParams { - /** - * Identifier for the last job from the previous pagination request. - */ - after?: string; - - /** - * Number of fine-tuning jobs to retrieve. - */ - limit?: number; -} - -export interface JobListEventsParams { - /** - * Identifier for the last event from the previous pagination request. - */ - after?: string; +export interface JobListParams extends CursorPageParams {} - /** - * Number of events to retrieve. - */ - limit?: number; -} +export interface JobListEventsParams extends CursorPageParams {} export namespace Jobs { export import FineTuningJob = API.FineTuningJob; diff --git a/src/streaming.ts b/src/streaming.ts index c654f3b64..230234546 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -9,9 +9,14 @@ type ServerSentEvent = { }; export class Stream implements AsyncIterable { + controller: AbortController; + + private response: Response; private decoder: SSEDecoder; - constructor(private response: Response, private controller: AbortController) { + constructor(response: Response, controller: AbortController) { + this.response = response; + this.controller = controller; this.decoder = new SSEDecoder(); }