Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: unreleased changes #245

Merged
merged 5 commits into from
Aug 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
70 changes: 49 additions & 21 deletions bin/cli
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
#!/usr/bin/env bash
set -eou pipefail

if [ $# -eq 0 ]; then
echo "Usage: $0 <subcommand>"
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 <subcommand>");
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();
5 changes: 4 additions & 1 deletion examples/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const resource = '<your resource name>';
// Navigate to the Azure OpenAI Studio to deploy a model.
const model = '<your 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.');
Expand All @@ -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 },
});

Expand Down
24 changes: 18 additions & 6 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type RequestInfo,
type RequestInit,
type Response,
type HeadersInit,
} from 'openai/_shims/fetch';
export { type Response };
import { isMultipartBody } from './uploads';
Expand Down Expand Up @@ -153,7 +154,7 @@ export abstract class APIClient {
this.fetch = overridenFetch ?? fetch;
}

protected authHeaders(): Headers {
protected authHeaders(opts: FinalRequestOptions): Headers {
return {};
}

Expand All @@ -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),
};
}

Expand Down Expand Up @@ -272,7 +273,7 @@ export abstract class APIClient {

const reqHeaders: Record<string, string> = {
...(contentLength && { 'Content-Length': contentLength }),
...this.defaultHeaders(),
...this.defaultHeaders(options),
...headers,
};
// let builtin fetch set the Content-Type for multipart bodies
Expand Down Expand Up @@ -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<void> {}
protected async prepareRequest(
request: RequestInit,
{ url, options }: { url: string; options: FinalRequestOptions },
): Promise<void> {}

protected parseHeaders(headers: HeadersInit | null | undefined): Record<string, string> {
return (
!headers ? {}
: Symbol.iterator in headers ? Object.fromEntries(Array.from(headers).map((header) => [...header]))
: { ...headers }
);
}

protected makeStatusError(
status: number | undefined,
Expand Down Expand Up @@ -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);

Expand Down
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` };
}

Expand Down Expand Up @@ -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;
Expand Down
60 changes: 59 additions & 1 deletion src/pagination.ts
Original file line number Diff line number Diff line change
@@ -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<Item> {
data: Array<Item>;
Expand Down Expand Up @@ -40,3 +40,61 @@ export class Page<Item> extends AbstractPage<Item> implements PageResponse<Item>
return null;
}
}

export interface CursorPageResponse<Item> {
data: Array<Item>;
}

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<Item extends { id: string }>
extends AbstractPage<Item>
implements CursorPageResponse<Item>
{
data: Array<Item>;

constructor(
client: APIClient,
response: Response,
body: CursorPageResponse<Item>,
options: FinalRequestOptions,
) {
super(client, response, body, options);

this.data = body.data;
}

getPaginatedItems(): Item[] {
return this.data;
}

// @deprecated Please use `nextPageInfo()` instead
nextPageParams(): Partial<CursorPageParams> | 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 } };
}
}
36 changes: 5 additions & 31 deletions src/resources/fine-tuning/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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<FineTuningJob> {}
export class FineTuningJobsPage extends CursorPage<FineTuningJob> {}
// 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<FineTuningJobEvent> {}
export class FineTuningJobEventsPage extends CursorPage<FineTuningJobEvent> {}
// alias so we can export it in the namespace
type _FineTuningJobEventsPage = FineTuningJobEventsPage;

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ type ServerSentEvent = {
};

export class Stream<Item> implements AsyncIterable<Item> {
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();
}

Expand Down