V1 with working cmdline interface for easy of using GIT

This commit is contained in:
s41r4j
2025-08-08 16:27:43 +05:30
commit 71a398820a
2845 changed files with 396687 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export class MultipartBody {
constructor(public body: any) {}
get [Symbol.toStringTag](): string {
return 'MultipartBody';
}
}
+46
View File
@@ -0,0 +1,46 @@
# 👋 Wondering what everything in here does?
`openai` supports a wide variety of runtime environments like Node.js, Deno, Bun, browsers, and various
edge runtimes, as well as both CommonJS (CJS) and EcmaScript Modules (ESM).
To do this, `openai` provides shims for either using `node-fetch` when in Node (because `fetch` is still experimental there) or the global `fetch` API built into the environment when not in Node.
It uses [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to
automatically select the correct shims for each environment. However, conditional exports are a fairly new
feature and not supported everywhere. For instance, the TypeScript `"moduleResolution": "node"`
setting doesn't consult the `exports` map, compared to `"moduleResolution": "nodeNext"`, which does.
Unfortunately that's still the default setting, and it can result in errors like
getting the wrong raw `Response` type from `.asResponse()`, for example.
The user can work around these issues by manually importing one of:
- `import 'openai/shims/node'`
- `import 'openai/shims/web'`
All of the code here in `_shims` handles selecting the automatic default shims or manual overrides.
### How it works - Runtime
Runtime shims get installed by calling `setShims` exported by `openai/_shims/registry`.
Manually importing `openai/shims/node` or `openai/shims/web`, calls `setShims` with the respective runtime shims.
All client code imports shims from `openai/_shims/index`, which:
- checks if shims have been set manually
- if not, calls `setShims` with the shims from `openai/_shims/auto/runtime`
- re-exports the installed shims from `openai/_shims/registry`.
`openai/_shims/auto/runtime` exports web runtime shims.
If the `node` export condition is set, the export map replaces it with `openai/_shims/auto/runtime-node`.
### How it works - Type time
All client code imports shim types from `openai/_shims/index`, which selects the manual types from `openai/_shims/manual-types` if they have been declared, otherwise it exports the auto types from `openai/_shims/auto/types`.
`openai/_shims/manual-types` exports an empty namespace.
Manually importing `openai/shims/node` or `openai/shims/web` merges declarations into this empty namespace, so they get picked up by `openai/_shims/index`.
`openai/_shims/auto/types` exports web type definitions.
If the `node` export condition is set, the export map replaces it with `openai/_shims/auto/types-node`, though TS only picks this up if `"moduleResolution": "nodenext"` or `"moduleResolution": "bundler"`.
+4
View File
@@ -0,0 +1,4 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export * from '../bun-runtime';
+4
View File
@@ -0,0 +1,4 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export * from '../node-runtime';
+4
View File
@@ -0,0 +1,4 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export * from '../web-runtime';
+4
View File
@@ -0,0 +1,4 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export * from '../node-types';
+101
View File
@@ -0,0 +1,101 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export type Agent = any;
// @ts-ignore
declare const _fetch: typeof fetch;
export { _fetch as fetch };
// @ts-ignore
type _Request = Request;
export { _Request as Request };
// @ts-ignore
type _RequestInfo = RequestInfo;
export { type _RequestInfo as RequestInfo };
// @ts-ignore
type _RequestInit = RequestInit;
export { type _RequestInit as RequestInit };
// @ts-ignore
type _Response = Response;
export { _Response as Response };
// @ts-ignore
type _ResponseInit = ResponseInit;
export { type _ResponseInit as ResponseInit };
// @ts-ignore
type _ResponseType = ResponseType;
export { type _ResponseType as ResponseType };
// @ts-ignore
type _BodyInit = BodyInit;
export { type _BodyInit as BodyInit };
// @ts-ignore
type _Headers = Headers;
export { _Headers as Headers };
// @ts-ignore
type _HeadersInit = HeadersInit;
export { type _HeadersInit as HeadersInit };
type EndingType = 'native' | 'transparent';
export interface BlobPropertyBag {
endings?: EndingType;
type?: string;
}
export interface FilePropertyBag extends BlobPropertyBag {
lastModified?: number;
}
export type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
// @ts-ignore
type _FormData = FormData;
// @ts-ignore
declare const _FormData: typeof FormData;
export { _FormData as FormData };
// @ts-ignore
type _File = File;
// @ts-ignore
declare const _File: typeof File;
export { _File as File };
// @ts-ignore
type _Blob = Blob;
// @ts-ignore
declare const _Blob: typeof Blob;
export { _Blob as Blob };
export declare class Readable {
readable: boolean;
readonly readableEnded: boolean;
readonly readableFlowing: boolean | null;
readonly readableHighWaterMark: number;
readonly readableLength: number;
readonly readableObjectMode: boolean;
destroyed: boolean;
read(size?: number): any;
pause(): this;
resume(): this;
isPaused(): boolean;
destroy(error?: Error): this;
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
}
export declare class FsReadStream extends Readable {
path: {}; // node type is string | Buffer
}
// @ts-ignore
type _ReadableStream<R = any> = ReadableStream<R>;
// @ts-ignore
declare const _ReadableStream: typeof ReadableStream;
export { _ReadableStream as ReadableStream };
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+14
View File
@@ -0,0 +1,14 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import { type Shims } from './registry';
import { getRuntime as getWebRuntime } from './web-runtime';
import { ReadStream as FsReadStream } from 'node:fs';
export function getRuntime(): Shims {
const runtime = getWebRuntime();
function isFsReadStream(value: any): value is FsReadStream {
return value instanceof FsReadStream;
}
return { ...runtime, isFsReadStream };
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import { manual } from './manual-types';
import * as auto from "./auto/types";
import { type RequestOptions } from '../core';
type SelectType<Manual, Auto> = unknown extends Manual ? Auto : Manual;
export const kind: string;
// @ts-ignore
export type Agent = SelectType<manual.Agent, auto.Agent>;
// @ts-ignore
export const fetch: SelectType<typeof manual.fetch, typeof auto.fetch>;
// @ts-ignore
export type Request = SelectType<manual.Request, auto.Request>;
// @ts-ignore
export type RequestInfo = SelectType<manual.RequestInfo, auto.RequestInfo>;
// @ts-ignore
export type RequestInit = SelectType<manual.RequestInit, auto.RequestInit>;
// @ts-ignore
export type Response = SelectType<manual.Response, auto.Response>;
// @ts-ignore
export type ResponseInit = SelectType<manual.ResponseInit, auto.ResponseInit>;
// @ts-ignore
export type ResponseType = SelectType<manual.ResponseType, auto.ResponseType>;
// @ts-ignore
export type BodyInit = SelectType<manual.BodyInit, auto.BodyInit>;
// @ts-ignore
export type Headers = SelectType<manual.Headers, auto.Headers>;
// @ts-ignore
export const Headers: SelectType<typeof manual.Headers, typeof auto.Headers>;
// @ts-ignore
export type HeadersInit = SelectType<manual.HeadersInit, auto.HeadersInit>;
// @ts-ignore
export type BlobPropertyBag = SelectType<manual.BlobPropertyBag, auto.BlobPropertyBag>;
// @ts-ignore
export type FilePropertyBag = SelectType<manual.FilePropertyBag, auto.FilePropertyBag>;
// @ts-ignore
export type FileFromPathOptions = SelectType<manual.FileFromPathOptions, auto.FileFromPathOptions>;
// @ts-ignore
export type FormData = SelectType<manual.FormData, auto.FormData>;
// @ts-ignore
export const FormData: SelectType<typeof manual.FormData, typeof auto.FormData>;
// @ts-ignore
export type File = SelectType<manual.File, auto.File>;
// @ts-ignore
export const File: SelectType<typeof manual.File, typeof auto.File>;
// @ts-ignore
export type Blob = SelectType<manual.Blob, auto.Blob>;
// @ts-ignore
export const Blob: SelectType<typeof manual.Blob, typeof auto.Blob>;
// @ts-ignore
export type Readable = SelectType<manual.Readable, auto.Readable>;
// @ts-ignore
export type FsReadStream = SelectType<manual.FsReadStream, auto.FsReadStream>;
// @ts-ignore
export type ReadableStream = SelectType<manual.ReadableStream, auto.ReadableStream>;
// @ts-ignore
export const ReadableStream: SelectType<typeof manual.ReadableStream, typeof auto.ReadableStream>;
export function getMultipartRequestOptions<T = Record<string, unknown>>(
form: FormData,
opts: RequestOptions<T>,
): Promise<RequestOptions<T>>;
export function getDefaultAgent(url: string): any;
// @ts-ignore
export type FileFromPathOptions = SelectType<manual.FileFromPathOptions, auto.FileFromPathOptions>;
export function fileFromPath(path: string, options?: FileFromPathOptions): Promise<File>;
export function fileFromPath(path: string, filename?: string, options?: FileFromPathOptions): Promise<File>;
export function isFsReadStream(value: any): value is FsReadStream;
export const init: () => void;
+17
View File
@@ -0,0 +1,17 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
const shims = require('./registry');
const auto = require('openai/_shims/auto/runtime');
exports.init = () => {
if (!shims.kind) shims.setShims(auto.getRuntime(), { auto: true });
};
for (const property of Object.keys(shims)) {
Object.defineProperty(exports, property, {
get() {
return shims[property];
},
});
}
exports.init();
+11
View File
@@ -0,0 +1,11 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import * as shims from './registry.mjs';
import * as auto from "./auto/runtime";
export const init = () => {
if (!shims.kind) shims.setShims(auto.getRuntime(), { auto: true });
};
export * from './registry.mjs';
init();
+12
View File
@@ -0,0 +1,12 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
/**
* Types will get added to this namespace when you import one of the following:
*
* import 'openai/shims/node'
* import 'openai/shims/web'
*
* Importing more than one will cause type and runtime errors.
*/
export namespace manual {}
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+81
View File
@@ -0,0 +1,81 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import * as nf from 'node-fetch';
import * as fd from 'formdata-node';
import { type File, type FilePropertyBag } from 'formdata-node';
import KeepAliveAgent from 'agentkeepalive';
import { AbortController as AbortControllerPolyfill } from 'abort-controller';
import { ReadStream as FsReadStream } from 'node:fs';
import { type Agent } from 'node:http';
import { FormDataEncoder } from 'form-data-encoder';
import { Readable } from 'node:stream';
import { type RequestOptions } from '../core';
import { MultipartBody } from './MultipartBody';
import { type Shims } from './registry';
import { ReadableStream } from 'node:stream/web';
type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
let fileFromPathWarned = false;
/**
* @deprecated use fs.createReadStream('./my/file.txt') instead
*/
async function fileFromPath(path: string): Promise<File>;
async function fileFromPath(path: string, filename?: string): Promise<File>;
async function fileFromPath(path: string, options?: FileFromPathOptions): Promise<File>;
async function fileFromPath(path: string, filename?: string, options?: FileFromPathOptions): Promise<File>;
async function fileFromPath(path: string, ...args: any[]): Promise<File> {
// this import fails in environments that don't handle export maps correctly, like old versions of Jest
const { fileFromPath: _fileFromPath } = await import('formdata-node/file-from-path');
if (!fileFromPathWarned) {
console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path)}) instead`);
fileFromPathWarned = true;
}
// @ts-ignore
return await _fileFromPath(path, ...args);
}
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<T = Record<string, unknown>>(
form: fd.FormData,
opts: RequestOptions<T>,
): Promise<RequestOptions<T>> {
const encoder = new FormDataEncoder(form);
const readable = Readable.from(encoder);
const body = new MultipartBody(readable);
const headers = {
...opts.headers,
...encoder.headers,
'Content-Length': encoder.contentLength,
};
return { ...opts, body: body as any, headers };
}
export function getRuntime(): Shims {
// Polyfill global object if needed.
if (typeof AbortController === 'undefined') {
// @ts-expect-error (the types are subtly different, but compatible in practice)
globalThis.AbortController = AbortControllerPolyfill;
}
return {
kind: 'node',
fetch: nf.default,
Request: nf.Request,
Response: nf.Response,
Headers: nf.Headers,
FormData: fd.FormData,
Blob: fd.Blob,
File: fd.File,
ReadableStream,
getMultipartRequestOptions,
getDefaultAgent: (url: string): Agent => (url.startsWith('https') ? defaultHttpsAgent : defaultHttpAgent),
fileFromPath,
isFsReadStream: (value: any): value is FsReadStream => value instanceof FsReadStream,
};
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import * as nf from 'node-fetch';
import * as fd from 'formdata-node';
export { type Agent } from 'node:http';
export { type Readable } from 'node:stream';
export { type ReadStream as FsReadStream } from 'node:fs';
export { ReadableStream } from 'node:stream/web';
export const fetch: typeof nf.default;
export type Request = nf.Request;
export type RequestInfo = nf.RequestInfo;
export type RequestInit = nf.RequestInit;
export type Response = nf.Response;
export type ResponseInit = nf.ResponseInit;
export type ResponseType = nf.ResponseType;
export type BodyInit = nf.BodyInit;
export type Headers = nf.Headers;
export type HeadersInit = nf.HeadersInit;
type EndingType = 'native' | 'transparent';
export interface BlobPropertyBag {
endings?: EndingType;
type?: string;
}
export interface FilePropertyBag extends BlobPropertyBag {
lastModified?: number;
}
export type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
export type FormData = fd.FormData;
export const FormData: typeof fd.FormData;
export type File = fd.File;
export const File: typeof fd.File;
export type Blob = fd.Blob;
export const Blob: typeof fd.Blob;
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+65
View File
@@ -0,0 +1,65 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import { type RequestOptions } from '../core';
export interface Shims {
kind: string;
fetch: any;
Request: any;
Response: any;
Headers: any;
FormData: any;
Blob: any;
File: any;
ReadableStream: any;
getMultipartRequestOptions: <T = Record<string, unknown>>(
form: Shims['FormData'],
opts: RequestOptions<T>,
) => Promise<RequestOptions<T>>;
getDefaultAgent: (url: string) => any;
fileFromPath:
| ((path: string, filename?: string, options?: {}) => Promise<Shims['File']>)
| ((path: string, options?: {}) => Promise<Shims['File']>);
isFsReadStream: (value: any) => boolean;
}
export let auto = false;
export let kind: Shims['kind'] | undefined = undefined;
export let fetch: Shims['fetch'] | undefined = undefined;
export let Request: Shims['Request'] | undefined = undefined;
export let Response: Shims['Response'] | undefined = undefined;
export let Headers: Shims['Headers'] | undefined = undefined;
export let FormData: Shims['FormData'] | undefined = undefined;
export let Blob: Shims['Blob'] | undefined = undefined;
export let File: Shims['File'] | undefined = undefined;
export let ReadableStream: Shims['ReadableStream'] | undefined = undefined;
export let getMultipartRequestOptions: Shims['getMultipartRequestOptions'] | undefined = undefined;
export let getDefaultAgent: Shims['getDefaultAgent'] | undefined = undefined;
export let fileFromPath: Shims['fileFromPath'] | undefined = undefined;
export let isFsReadStream: Shims['isFsReadStream'] | undefined = undefined;
export function setShims(shims: Shims, options: { auto: boolean } = { auto: false }) {
if (auto) {
throw new Error(
`you must \`import 'openai/shims/${shims.kind}'\` before importing anything else from openai`,
);
}
if (kind) {
throw new Error(`can't \`import 'openai/shims/${shims.kind}'\` after \`import 'openai/shims/${kind}'\``);
}
auto = options.auto;
kind = shims.kind;
fetch = shims.fetch;
Request = shims.Request;
Response = shims.Response;
Headers = shims.Headers;
FormData = shims.FormData;
Blob = shims.Blob;
File = shims.File;
ReadableStream = shims.ReadableStream;
getMultipartRequestOptions = shims.getMultipartRequestOptions;
getDefaultAgent = shims.getDefaultAgent;
fileFromPath = shims.fileFromPath;
isFsReadStream = shims.isFsReadStream;
}
+103
View File
@@ -0,0 +1,103 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
import { MultipartBody } from './MultipartBody';
import { type RequestOptions } from '../core';
import { type Shims } from './registry';
export function getRuntime({ manuallyImported }: { manuallyImported?: boolean } = {}): Shims {
const recommendation =
manuallyImported ?
`You may need to use polyfills`
: `Add one of these imports before your first \`import … from 'openai'\`:
- \`import 'openai/shims/node'\` (if you're running on Node)
- \`import 'openai/shims/web'\` (otherwise)
`;
let _fetch, _Request, _Response, _Headers;
try {
// @ts-ignore
_fetch = fetch;
// @ts-ignore
_Request = Request;
// @ts-ignore
_Response = Response;
// @ts-ignore
_Headers = Headers;
} catch (error) {
throw new Error(
`this environment is missing the following Web Fetch API type: ${
(error as any).message
}. ${recommendation}`,
);
}
return {
kind: 'web',
fetch: _fetch,
Request: _Request,
Response: _Response,
Headers: _Headers,
FormData:
// @ts-ignore
typeof FormData !== 'undefined' ? FormData : (
class FormData {
// @ts-ignore
constructor() {
throw new Error(
`file uploads aren't supported in this environment yet as 'FormData' is undefined. ${recommendation}`,
);
}
}
),
Blob:
typeof Blob !== 'undefined' ? Blob : (
class Blob {
constructor() {
throw new Error(
`file uploads aren't supported in this environment yet as 'Blob' is undefined. ${recommendation}`,
);
}
}
),
File:
// @ts-ignore
typeof File !== 'undefined' ? File : (
class File {
// @ts-ignore
constructor() {
throw new Error(
`file uploads aren't supported in this environment yet as 'File' is undefined. ${recommendation}`,
);
}
}
),
ReadableStream:
// @ts-ignore
typeof ReadableStream !== 'undefined' ? ReadableStream : (
class ReadableStream {
// @ts-ignore
constructor() {
throw new Error(
`streaming isn't supported in this environment yet as 'ReadableStream' is undefined. ${recommendation}`,
);
}
}
),
getMultipartRequestOptions: async <T = Record<string, unknown>>(
// @ts-ignore
form: FormData,
opts: RequestOptions<T>,
): Promise<RequestOptions<T>> => ({
...opts,
body: new MultipartBody(form) as any,
}),
getDefaultAgent: (url: string) => undefined,
fileFromPath: () => {
throw new Error(
'The `fileFromPath` function is only supported in Node. See the README for more details: https://www.github.com/openai/openai-node#file-uploads',
);
},
isFsReadStream: (value: any) => false,
};
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
export type Agent = any;
declare const _fetch: typeof fetch;
export { _fetch as fetch };
type _Request = Request;
export { _Request as Request };
type _RequestInfo = RequestInfo;
export { type _RequestInfo as RequestInfo };
type _RequestInit = RequestInit;
export { type _RequestInit as RequestInit };
type _Response = Response;
export { _Response as Response };
type _ResponseInit = ResponseInit;
export { type _ResponseInit as ResponseInit };
type _ResponseType = ResponseType;
export { type _ResponseType as ResponseType };
type _BodyInit = BodyInit;
export { type _BodyInit as BodyInit };
type _Headers = Headers;
export { _Headers as Headers };
type _HeadersInit = HeadersInit;
export { type _HeadersInit as HeadersInit };
type EndingType = 'native' | 'transparent';
export interface BlobPropertyBag {
endings?: EndingType;
type?: string;
}
export interface FilePropertyBag extends BlobPropertyBag {
lastModified?: number;
}
export type FileFromPathOptions = Omit<FilePropertyBag, 'lastModified'>;
type _FormData = FormData;
declare const _FormData: typeof FormData;
export { _FormData as FormData };
type _File = File;
declare const _File: typeof File;
export { _File as File };
type _Blob = Blob;
declare const _Blob: typeof Blob;
export { _Blob as Blob };
export declare class Readable {
readable: boolean;
readonly readableEnded: boolean;
readonly readableFlowing: boolean | null;
readonly readableHighWaterMark: number;
readonly readableLength: number;
readonly readableObjectMode: boolean;
destroyed: boolean;
read(size?: number): any;
pause(): this;
resume(): this;
isPaused(): boolean;
destroy(error?: Error): this;
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
}
export declare class FsReadStream extends Readable {
path: {}; // node type is string | Buffer
}
type _ReadableStream<R = any> = ReadableStream<R>;
declare const _ReadableStream: typeof ReadableStream;
export { _ReadableStream as ReadableStream };
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+3
View File
@@ -0,0 +1,3 @@
/**
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
*/
+3
View File
@@ -0,0 +1,3 @@
# Partial JSON Parser
Vendored from https://www.npmjs.com/package/partial-json with some modifications
+247
View File
@@ -0,0 +1,247 @@
const STR = 0b000000001;
const NUM = 0b000000010;
const ARR = 0b000000100;
const OBJ = 0b000001000;
const NULL = 0b000010000;
const BOOL = 0b000100000;
const NAN = 0b001000000;
const INFINITY = 0b010000000;
const MINUS_INFINITY = 0b100000000;
const INF = INFINITY | MINUS_INFINITY;
const SPECIAL = NULL | BOOL | INF | NAN;
const ATOM = STR | NUM | SPECIAL;
const COLLECTION = ARR | OBJ;
const ALL = ATOM | COLLECTION;
const Allow = {
STR,
NUM,
ARR,
OBJ,
NULL,
BOOL,
NAN,
INFINITY,
MINUS_INFINITY,
INF,
SPECIAL,
ATOM,
COLLECTION,
ALL,
};
// The JSON string segment was unable to be parsed completely
class PartialJSON extends Error {}
class MalformedJSON extends Error {}
/**
* Parse incomplete JSON
* @param {string} jsonString Partial JSON to be parsed
* @param {number} allowPartial Specify what types are allowed to be partial, see {@link Allow} for details
* @returns The parsed JSON
* @throws {PartialJSON} If the JSON is incomplete (related to the `allow` parameter)
* @throws {MalformedJSON} If the JSON is malformed
*/
function parseJSON(jsonString: string, allowPartial: number = Allow.ALL): any {
if (typeof jsonString !== 'string') {
throw new TypeError(`expecting str, got ${typeof jsonString}`);
}
if (!jsonString.trim()) {
throw new Error(`${jsonString} is empty`);
}
return _parseJSON(jsonString.trim(), allowPartial);
}
const _parseJSON = (jsonString: string, allow: number) => {
const length = jsonString.length;
let index = 0;
const markPartialJSON = (msg: string) => {
throw new PartialJSON(`${msg} at position ${index}`);
};
const throwMalformedError = (msg: string) => {
throw new MalformedJSON(`${msg} at position ${index}`);
};
const parseAny: () => any = () => {
skipBlank();
if (index >= length) markPartialJSON('Unexpected end of input');
if (jsonString[index] === '"') return parseStr();
if (jsonString[index] === '{') return parseObj();
if (jsonString[index] === '[') return parseArr();
if (
jsonString.substring(index, index + 4) === 'null' ||
(Allow.NULL & allow && length - index < 4 && 'null'.startsWith(jsonString.substring(index)))
) {
index += 4;
return null;
}
if (
jsonString.substring(index, index + 4) === 'true' ||
(Allow.BOOL & allow && length - index < 4 && 'true'.startsWith(jsonString.substring(index)))
) {
index += 4;
return true;
}
if (
jsonString.substring(index, index + 5) === 'false' ||
(Allow.BOOL & allow && length - index < 5 && 'false'.startsWith(jsonString.substring(index)))
) {
index += 5;
return false;
}
if (
jsonString.substring(index, index + 8) === 'Infinity' ||
(Allow.INFINITY & allow && length - index < 8 && 'Infinity'.startsWith(jsonString.substring(index)))
) {
index += 8;
return Infinity;
}
if (
jsonString.substring(index, index + 9) === '-Infinity' ||
(Allow.MINUS_INFINITY & allow &&
1 < length - index &&
length - index < 9 &&
'-Infinity'.startsWith(jsonString.substring(index)))
) {
index += 9;
return -Infinity;
}
if (
jsonString.substring(index, index + 3) === 'NaN' ||
(Allow.NAN & allow && length - index < 3 && 'NaN'.startsWith(jsonString.substring(index)))
) {
index += 3;
return NaN;
}
return parseNum();
};
const parseStr: () => string = () => {
const start = index;
let escape = false;
index++; // skip initial quote
while (index < length && (jsonString[index] !== '"' || (escape && jsonString[index - 1] === '\\'))) {
escape = jsonString[index] === '\\' ? !escape : false;
index++;
}
if (jsonString.charAt(index) == '"') {
try {
return JSON.parse(jsonString.substring(start, ++index - Number(escape)));
} catch (e) {
throwMalformedError(String(e));
}
} else if (Allow.STR & allow) {
try {
return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"');
} catch (e) {
// SyntaxError: Invalid escape sequence
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf('\\')) + '"');
}
}
markPartialJSON('Unterminated string literal');
};
const parseObj = () => {
index++; // skip initial brace
skipBlank();
const obj: Record<string, any> = {};
try {
while (jsonString[index] !== '}') {
skipBlank();
if (index >= length && Allow.OBJ & allow) return obj;
const key = parseStr();
skipBlank();
index++; // skip colon
try {
const value = parseAny();
Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });
} catch (e) {
if (Allow.OBJ & allow) return obj;
else throw e;
}
skipBlank();
if (jsonString[index] === ',') index++; // skip comma
}
} catch (e) {
if (Allow.OBJ & allow) return obj;
else markPartialJSON("Expected '}' at end of object");
}
index++; // skip final brace
return obj;
};
const parseArr = () => {
index++; // skip initial bracket
const arr = [];
try {
while (jsonString[index] !== ']') {
arr.push(parseAny());
skipBlank();
if (jsonString[index] === ',') {
index++; // skip comma
}
}
} catch (e) {
if (Allow.ARR & allow) {
return arr;
}
markPartialJSON("Expected ']' at end of array");
}
index++; // skip final bracket
return arr;
};
const parseNum = () => {
if (index === 0) {
if (jsonString === '-' && Allow.NUM & allow) markPartialJSON("Not sure what '-' is");
try {
return JSON.parse(jsonString);
} catch (e) {
if (Allow.NUM & allow) {
try {
if ('.' === jsonString[jsonString.length - 1])
return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf('.')));
return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf('e')));
} catch (e) {}
}
throwMalformedError(String(e));
}
}
const start = index;
if (jsonString[index] === '-') index++;
while (jsonString[index] && !',]}'.includes(jsonString[index]!)) index++;
if (index == length && !(Allow.NUM & allow)) markPartialJSON('Unterminated number literal');
try {
return JSON.parse(jsonString.substring(start, index));
} catch (e) {
if (jsonString.substring(start, index) === '-' && Allow.NUM & allow)
markPartialJSON("Not sure what '-' is");
try {
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf('e')));
} catch (e) {
throwMalformedError(String(e));
}
}
};
const skipBlank = () => {
while (index < length && ' \n\r\t'.includes(jsonString[index]!)) {
index++;
}
};
return parseAny();
};
// using this function with malformed JSON is undefined behavior
const partialParse = (input: string) => parseJSON(input, Allow.ALL ^ Allow.NUM);
export { partialParse, PartialJSON, MalformedJSON };
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2020, Stefan Terdell
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+80
View File
@@ -0,0 +1,80 @@
import { ZodSchema, ZodTypeDef } from 'zod';
import { Refs, Seen } from './Refs';
import { JsonSchema7Type } from './parseDef';
export type Targets = 'jsonSchema7' | 'jsonSchema2019-09' | 'openApi3';
export type DateStrategy = 'format:date-time' | 'format:date' | 'string' | 'integer';
export const ignoreOverride = Symbol('Let zodToJsonSchema decide on which parser to use');
export type Options<Target extends Targets = 'jsonSchema7'> = {
name: string | undefined;
$refStrategy: 'root' | 'relative' | 'none' | 'seen' | 'extract-to-root';
basePath: string[];
effectStrategy: 'input' | 'any';
pipeStrategy: 'input' | 'output' | 'all';
dateStrategy: DateStrategy | DateStrategy[];
mapStrategy: 'entries' | 'record';
removeAdditionalStrategy: 'passthrough' | 'strict';
nullableStrategy: 'from-target' | 'property';
target: Target;
strictUnions: boolean;
definitionPath: string;
definitions: Record<string, ZodSchema | ZodTypeDef>;
errorMessages: boolean;
markdownDescription: boolean;
patternStrategy: 'escape' | 'preserve';
applyRegexFlags: boolean;
emailStrategy: 'format:email' | 'format:idn-email' | 'pattern:zod';
base64Strategy: 'format:binary' | 'contentEncoding:base64' | 'pattern:zod';
nameStrategy: 'ref' | 'duplicate-ref' | 'title';
override?: (
def: ZodTypeDef,
refs: Refs,
seen: Seen | undefined,
forceResolution?: boolean,
) => JsonSchema7Type | undefined | typeof ignoreOverride;
openaiStrictMode?: boolean;
};
const defaultOptions: Omit<Options, 'definitions' | 'basePath'> = {
name: undefined,
$refStrategy: 'root',
effectStrategy: 'input',
pipeStrategy: 'all',
dateStrategy: 'format:date-time',
mapStrategy: 'entries',
nullableStrategy: 'from-target',
removeAdditionalStrategy: 'passthrough',
definitionPath: 'definitions',
target: 'jsonSchema7',
strictUnions: false,
errorMessages: false,
markdownDescription: false,
patternStrategy: 'escape',
applyRegexFlags: false,
emailStrategy: 'format:email',
base64Strategy: 'contentEncoding:base64',
nameStrategy: 'ref',
};
export const getDefaultOptions = <Target extends Targets>(
options: Partial<Options<Target>> | string | undefined,
) => {
// We need to add `definitions` here as we may mutate it
return (
typeof options === 'string' ?
{
...defaultOptions,
basePath: ['#'],
definitions: {},
name: options,
}
: {
...defaultOptions,
basePath: ['#'],
definitions: {},
...options,
}) as Options<Target>;
};
+3
View File
@@ -0,0 +1,3 @@
# Zod to Json Schema
Vendored version of https://github.com/StefanTerdell/zod-to-json-schema that has been updated to generate JSON Schemas that are compatible with OpenAI's [strict mode](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas)
+47
View File
@@ -0,0 +1,47 @@
import type { ZodTypeDef } from 'zod';
import { getDefaultOptions, Options, Targets } from './Options';
import { JsonSchema7Type } from './parseDef';
import { zodDef } from './util';
export type Refs = {
seen: Map<ZodTypeDef, Seen>;
/**
* Set of all the `$ref`s we created, e.g. `Set(['#/$defs/ui'])`
* this notable does not include any `definitions` that were
* explicitly given as an option.
*/
seenRefs: Set<string>;
currentPath: string[];
propertyPath: string[] | undefined;
} & Options<Targets>;
export type Seen = {
def: ZodTypeDef;
path: string[];
jsonSchema: JsonSchema7Type | undefined;
};
export const getRefs = (options?: string | Partial<Options<Targets>>): Refs => {
const _options = getDefaultOptions(options);
const currentPath =
_options.name !== undefined ?
[..._options.basePath, _options.definitionPath, _options.name]
: _options.basePath;
return {
..._options,
currentPath: currentPath,
propertyPath: undefined,
seenRefs: new Set(),
seen: new Map(
Object.entries(_options.definitions).map(([name, def]) => [
zodDef(def),
{
def: zodDef(def),
path: [..._options.basePath, _options.definitionPath, name],
// Resolution of references will be forced even though seen, so it's ok that the schema is undefined here for now.
jsonSchema: undefined,
},
]),
),
};
};
+31
View File
@@ -0,0 +1,31 @@
import { JsonSchema7TypeUnion } from './parseDef';
import { Refs } from './Refs';
export type ErrorMessages<T extends JsonSchema7TypeUnion, OmitProperties extends string = ''> = Partial<
Omit<{ [key in keyof T]: string }, OmitProperties | 'type' | 'errorMessages'>
>;
export function addErrorMessage<T extends { errorMessage?: ErrorMessages<any> }>(
res: T,
key: keyof T,
errorMessage: string | undefined,
refs: Refs,
) {
if (!refs?.errorMessages) return;
if (errorMessage) {
res.errorMessage = {
...res.errorMessage,
[key]: errorMessage,
};
}
}
export function setResponseValueAndErrors<
Json7Type extends JsonSchema7TypeUnion & {
errorMessage?: ErrorMessages<Json7Type>;
},
Key extends keyof Omit<Json7Type, 'errorMessage'>,
>(res: Json7Type, key: Key, value: Json7Type[Key], errorMessage: string | undefined, refs: Refs) {
res[key] = value;
addErrorMessage(res, key, errorMessage, refs);
}
+37
View File
@@ -0,0 +1,37 @@
export * from './Options';
export * from './Refs';
export * from './errorMessages';
export * from './parseDef';
export * from './parsers/any';
export * from './parsers/array';
export * from './parsers/bigint';
export * from './parsers/boolean';
export * from './parsers/branded';
export * from './parsers/catch';
export * from './parsers/date';
export * from './parsers/default';
export * from './parsers/effects';
export * from './parsers/enum';
export * from './parsers/intersection';
export * from './parsers/literal';
export * from './parsers/map';
export * from './parsers/nativeEnum';
export * from './parsers/never';
export * from './parsers/null';
export * from './parsers/nullable';
export * from './parsers/number';
export * from './parsers/object';
export * from './parsers/optional';
export * from './parsers/pipeline';
export * from './parsers/promise';
export * from './parsers/readonly';
export * from './parsers/record';
export * from './parsers/set';
export * from './parsers/string';
export * from './parsers/tuple';
export * from './parsers/undefined';
export * from './parsers/union';
export * from './parsers/unknown';
export * from './zodToJsonSchema';
import { zodToJsonSchema } from './zodToJsonSchema';
export default zodToJsonSchema;
+258
View File
@@ -0,0 +1,258 @@
import { ZodFirstPartyTypeKind, ZodTypeDef } from 'zod';
import { JsonSchema7AnyType, parseAnyDef } from './parsers/any';
import { JsonSchema7ArrayType, parseArrayDef } from './parsers/array';
import { JsonSchema7BigintType, parseBigintDef } from './parsers/bigint';
import { JsonSchema7BooleanType, parseBooleanDef } from './parsers/boolean';
import { parseBrandedDef } from './parsers/branded';
import { parseCatchDef } from './parsers/catch';
import { JsonSchema7DateType, parseDateDef } from './parsers/date';
import { parseDefaultDef } from './parsers/default';
import { parseEffectsDef } from './parsers/effects';
import { JsonSchema7EnumType, parseEnumDef } from './parsers/enum';
import { JsonSchema7AllOfType, parseIntersectionDef } from './parsers/intersection';
import { JsonSchema7LiteralType, parseLiteralDef } from './parsers/literal';
import { JsonSchema7MapType, parseMapDef } from './parsers/map';
import { JsonSchema7NativeEnumType, parseNativeEnumDef } from './parsers/nativeEnum';
import { JsonSchema7NeverType, parseNeverDef } from './parsers/never';
import { JsonSchema7NullType, parseNullDef } from './parsers/null';
import { JsonSchema7NullableType, parseNullableDef } from './parsers/nullable';
import { JsonSchema7NumberType, parseNumberDef } from './parsers/number';
import { JsonSchema7ObjectType, parseObjectDef } from './parsers/object';
import { parseOptionalDef } from './parsers/optional';
import { parsePipelineDef } from './parsers/pipeline';
import { parsePromiseDef } from './parsers/promise';
import { JsonSchema7RecordType, parseRecordDef } from './parsers/record';
import { JsonSchema7SetType, parseSetDef } from './parsers/set';
import { JsonSchema7StringType, parseStringDef } from './parsers/string';
import { JsonSchema7TupleType, parseTupleDef } from './parsers/tuple';
import { JsonSchema7UndefinedType, parseUndefinedDef } from './parsers/undefined';
import { JsonSchema7UnionType, parseUnionDef } from './parsers/union';
import { JsonSchema7UnknownType, parseUnknownDef } from './parsers/unknown';
import { Refs, Seen } from './Refs';
import { parseReadonlyDef } from './parsers/readonly';
import { ignoreOverride } from './Options';
type JsonSchema7RefType = { $ref: string };
type JsonSchema7Meta = {
title?: string;
default?: any;
description?: string;
markdownDescription?: string;
};
export type JsonSchema7TypeUnion =
| JsonSchema7StringType
| JsonSchema7ArrayType
| JsonSchema7NumberType
| JsonSchema7BigintType
| JsonSchema7BooleanType
| JsonSchema7DateType
| JsonSchema7EnumType
| JsonSchema7LiteralType
| JsonSchema7NativeEnumType
| JsonSchema7NullType
| JsonSchema7NumberType
| JsonSchema7ObjectType
| JsonSchema7RecordType
| JsonSchema7TupleType
| JsonSchema7UnionType
| JsonSchema7UndefinedType
| JsonSchema7RefType
| JsonSchema7NeverType
| JsonSchema7MapType
| JsonSchema7AnyType
| JsonSchema7NullableType
| JsonSchema7AllOfType
| JsonSchema7UnknownType
| JsonSchema7SetType;
export type JsonSchema7Type = JsonSchema7TypeUnion & JsonSchema7Meta;
export function parseDef(
def: ZodTypeDef,
refs: Refs,
forceResolution = false, // Forces a new schema to be instantiated even though its def has been seen. Used for improving refs in definitions. See https://github.com/StefanTerdell/zod-to-json-schema/pull/61.
): JsonSchema7Type | undefined {
const seenItem = refs.seen.get(def);
if (refs.override) {
const overrideResult = refs.override?.(def, refs, seenItem, forceResolution);
if (overrideResult !== ignoreOverride) {
return overrideResult;
}
}
if (seenItem && !forceResolution) {
const seenSchema = get$ref(seenItem, refs);
if (seenSchema !== undefined) {
if ('$ref' in seenSchema) {
refs.seenRefs.add(seenSchema.$ref);
}
return seenSchema;
}
}
const newItem: Seen = { def, path: refs.currentPath, jsonSchema: undefined };
refs.seen.set(def, newItem);
const jsonSchema = selectParser(def, (def as any).typeName, refs, forceResolution);
if (jsonSchema) {
addMeta(def, refs, jsonSchema);
}
newItem.jsonSchema = jsonSchema;
return jsonSchema;
}
const get$ref = (
item: Seen,
refs: Refs,
):
| {
$ref: string;
}
| {}
| undefined => {
switch (refs.$refStrategy) {
case 'root':
return { $ref: item.path.join('/') };
// this case is needed as OpenAI strict mode doesn't support top-level `$ref`s, i.e.
// the top-level schema *must* be `{"type": "object", "properties": {...}}` but if we ever
// need to define a `$ref`, relative `$ref`s aren't supported, so we need to extract
// the schema to `#/definitions/` and reference that.
//
// e.g. if we need to reference a schema at
// `["#","definitions","contactPerson","properties","person1","properties","name"]`
// then we'll extract it out to `contactPerson_properties_person1_properties_name`
case 'extract-to-root':
const name = item.path.slice(refs.basePath.length + 1).join('_');
// we don't need to extract the root schema in this case, as it's already
// been added to the definitions
if (name !== refs.name && refs.nameStrategy === 'duplicate-ref') {
refs.definitions[name] = item.def;
}
return { $ref: [...refs.basePath, refs.definitionPath, name].join('/') };
case 'relative':
return { $ref: getRelativePath(refs.currentPath, item.path) };
case 'none':
case 'seen': {
if (
item.path.length < refs.currentPath.length &&
item.path.every((value, index) => refs.currentPath[index] === value)
) {
console.warn(`Recursive reference detected at ${refs.currentPath.join('/')}! Defaulting to any`);
return {};
}
return refs.$refStrategy === 'seen' ? {} : undefined;
}
}
};
const getRelativePath = (pathA: string[], pathB: string[]) => {
let i = 0;
for (; i < pathA.length && i < pathB.length; i++) {
if (pathA[i] !== pathB[i]) break;
}
return [(pathA.length - i).toString(), ...pathB.slice(i)].join('/');
};
const selectParser = (
def: any,
typeName: ZodFirstPartyTypeKind,
refs: Refs,
forceResolution: boolean,
): JsonSchema7Type | undefined => {
switch (typeName) {
case ZodFirstPartyTypeKind.ZodString:
return parseStringDef(def, refs);
case ZodFirstPartyTypeKind.ZodNumber:
return parseNumberDef(def, refs);
case ZodFirstPartyTypeKind.ZodObject:
return parseObjectDef(def, refs);
case ZodFirstPartyTypeKind.ZodBigInt:
return parseBigintDef(def, refs);
case ZodFirstPartyTypeKind.ZodBoolean:
return parseBooleanDef();
case ZodFirstPartyTypeKind.ZodDate:
return parseDateDef(def, refs);
case ZodFirstPartyTypeKind.ZodUndefined:
return parseUndefinedDef();
case ZodFirstPartyTypeKind.ZodNull:
return parseNullDef(refs);
case ZodFirstPartyTypeKind.ZodArray:
return parseArrayDef(def, refs);
case ZodFirstPartyTypeKind.ZodUnion:
case ZodFirstPartyTypeKind.ZodDiscriminatedUnion:
return parseUnionDef(def, refs);
case ZodFirstPartyTypeKind.ZodIntersection:
return parseIntersectionDef(def, refs);
case ZodFirstPartyTypeKind.ZodTuple:
return parseTupleDef(def, refs);
case ZodFirstPartyTypeKind.ZodRecord:
return parseRecordDef(def, refs);
case ZodFirstPartyTypeKind.ZodLiteral:
return parseLiteralDef(def, refs);
case ZodFirstPartyTypeKind.ZodEnum:
return parseEnumDef(def);
case ZodFirstPartyTypeKind.ZodNativeEnum:
return parseNativeEnumDef(def);
case ZodFirstPartyTypeKind.ZodNullable:
return parseNullableDef(def, refs);
case ZodFirstPartyTypeKind.ZodOptional:
return parseOptionalDef(def, refs);
case ZodFirstPartyTypeKind.ZodMap:
return parseMapDef(def, refs);
case ZodFirstPartyTypeKind.ZodSet:
return parseSetDef(def, refs);
case ZodFirstPartyTypeKind.ZodLazy:
return parseDef(def.getter()._def, refs);
case ZodFirstPartyTypeKind.ZodPromise:
return parsePromiseDef(def, refs);
case ZodFirstPartyTypeKind.ZodNaN:
case ZodFirstPartyTypeKind.ZodNever:
return parseNeverDef();
case ZodFirstPartyTypeKind.ZodEffects:
return parseEffectsDef(def, refs, forceResolution);
case ZodFirstPartyTypeKind.ZodAny:
return parseAnyDef();
case ZodFirstPartyTypeKind.ZodUnknown:
return parseUnknownDef();
case ZodFirstPartyTypeKind.ZodDefault:
return parseDefaultDef(def, refs);
case ZodFirstPartyTypeKind.ZodBranded:
return parseBrandedDef(def, refs);
case ZodFirstPartyTypeKind.ZodReadonly:
return parseReadonlyDef(def, refs);
case ZodFirstPartyTypeKind.ZodCatch:
return parseCatchDef(def, refs);
case ZodFirstPartyTypeKind.ZodPipeline:
return parsePipelineDef(def, refs);
case ZodFirstPartyTypeKind.ZodFunction:
case ZodFirstPartyTypeKind.ZodVoid:
case ZodFirstPartyTypeKind.ZodSymbol:
return undefined;
default:
return ((_: never) => undefined)(typeName);
}
};
const addMeta = (def: ZodTypeDef, refs: Refs, jsonSchema: JsonSchema7Type): JsonSchema7Type => {
if (def.description) {
jsonSchema.description = def.description;
if (refs.markdownDescription) {
jsonSchema.markdownDescription = def.description;
}
}
return jsonSchema;
};
+5
View File
@@ -0,0 +1,5 @@
export type JsonSchema7AnyType = {};
export function parseAnyDef(): JsonSchema7AnyType {
return {};
}
+36
View File
@@ -0,0 +1,36 @@
import { ZodArrayDef, ZodFirstPartyTypeKind } from 'zod';
import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export type JsonSchema7ArrayType = {
type: 'array';
items?: JsonSchema7Type | undefined;
minItems?: number;
maxItems?: number;
errorMessages?: ErrorMessages<JsonSchema7ArrayType, 'items'>;
};
export function parseArrayDef(def: ZodArrayDef, refs: Refs) {
const res: JsonSchema7ArrayType = {
type: 'array',
};
if (def.type?._def?.typeName !== ZodFirstPartyTypeKind.ZodAny) {
res.items = parseDef(def.type._def, {
...refs,
currentPath: [...refs.currentPath, 'items'],
});
}
if (def.minLength) {
setResponseValueAndErrors(res, 'minItems', def.minLength.value, def.minLength.message, refs);
}
if (def.maxLength) {
setResponseValueAndErrors(res, 'maxItems', def.maxLength.value, def.maxLength.message, refs);
}
if (def.exactLength) {
setResponseValueAndErrors(res, 'minItems', def.exactLength.value, def.exactLength.message, refs);
setResponseValueAndErrors(res, 'maxItems', def.exactLength.value, def.exactLength.message, refs);
}
return res;
}
+60
View File
@@ -0,0 +1,60 @@
import { ZodBigIntDef } from 'zod';
import { Refs } from '../Refs';
import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
export type JsonSchema7BigintType = {
type: 'integer';
format: 'int64';
minimum?: BigInt;
exclusiveMinimum?: BigInt;
maximum?: BigInt;
exclusiveMaximum?: BigInt;
multipleOf?: BigInt;
errorMessage?: ErrorMessages<JsonSchema7BigintType>;
};
export function parseBigintDef(def: ZodBigIntDef, refs: Refs): JsonSchema7BigintType {
const res: JsonSchema7BigintType = {
type: 'integer',
format: 'int64',
};
if (!def.checks) return res;
for (const check of def.checks) {
switch (check.kind) {
case 'min':
if (refs.target === 'jsonSchema7') {
if (check.inclusive) {
setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs);
} else {
setResponseValueAndErrors(res, 'exclusiveMinimum', check.value, check.message, refs);
}
} else {
if (!check.inclusive) {
res.exclusiveMinimum = true as any;
}
setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs);
}
break;
case 'max':
if (refs.target === 'jsonSchema7') {
if (check.inclusive) {
setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs);
} else {
setResponseValueAndErrors(res, 'exclusiveMaximum', check.value, check.message, refs);
}
} else {
if (!check.inclusive) {
res.exclusiveMaximum = true as any;
}
setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs);
}
break;
case 'multipleOf':
setResponseValueAndErrors(res, 'multipleOf', check.value, check.message, refs);
break;
}
}
return res;
}
@@ -0,0 +1,9 @@
export type JsonSchema7BooleanType = {
type: 'boolean';
};
export function parseBooleanDef(): JsonSchema7BooleanType {
return {
type: 'boolean',
};
}
@@ -0,0 +1,7 @@
import { ZodBrandedDef } from 'zod';
import { parseDef } from '../parseDef';
import { Refs } from '../Refs';
export function parseBrandedDef(_def: ZodBrandedDef<any>, refs: Refs) {
return parseDef(_def.type._def, refs);
}
+7
View File
@@ -0,0 +1,7 @@
import { ZodCatchDef } from 'zod';
import { parseDef } from '../parseDef';
import { Refs } from '../Refs';
export const parseCatchDef = (def: ZodCatchDef<any>, refs: Refs) => {
return parseDef(def.innerType._def, refs);
};
+83
View File
@@ -0,0 +1,83 @@
import { ZodDateDef } from 'zod';
import { Refs } from '../Refs';
import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
import { JsonSchema7NumberType } from './number';
import { DateStrategy } from '../Options';
export type JsonSchema7DateType =
| {
type: 'integer' | 'string';
format: 'unix-time' | 'date-time' | 'date';
minimum?: number;
maximum?: number;
errorMessage?: ErrorMessages<JsonSchema7NumberType>;
}
| {
anyOf: JsonSchema7DateType[];
};
export function parseDateDef(
def: ZodDateDef,
refs: Refs,
overrideDateStrategy?: DateStrategy,
): JsonSchema7DateType {
const strategy = overrideDateStrategy ?? refs.dateStrategy;
if (Array.isArray(strategy)) {
return {
anyOf: strategy.map((item, i) => parseDateDef(def, refs, item)),
};
}
switch (strategy) {
case 'string':
case 'format:date-time':
return {
type: 'string',
format: 'date-time',
};
case 'format:date':
return {
type: 'string',
format: 'date',
};
case 'integer':
return integerDateParser(def, refs);
}
}
const integerDateParser = (def: ZodDateDef, refs: Refs) => {
const res: JsonSchema7DateType = {
type: 'integer',
format: 'unix-time',
};
if (refs.target === 'openApi3') {
return res;
}
for (const check of def.checks) {
switch (check.kind) {
case 'min':
setResponseValueAndErrors(
res,
'minimum',
check.value, // This is in milliseconds
check.message,
refs,
);
break;
case 'max':
setResponseValueAndErrors(
res,
'maximum',
check.value, // This is in milliseconds
check.message,
refs,
);
break;
}
}
return res;
};
+10
View File
@@ -0,0 +1,10 @@
import { ZodDefaultDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export function parseDefaultDef(_def: ZodDefaultDef, refs: Refs): JsonSchema7Type & { default: any } {
return {
...parseDef(_def.innerType._def, refs),
default: _def.defaultValue(),
};
}
+11
View File
@@ -0,0 +1,11 @@
import { ZodEffectsDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export function parseEffectsDef(
_def: ZodEffectsDef,
refs: Refs,
forceResolution: boolean,
): JsonSchema7Type | undefined {
return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs, forceResolution) : {};
}
+13
View File
@@ -0,0 +1,13 @@
import { ZodEnumDef } from 'zod';
export type JsonSchema7EnumType = {
type: 'string';
enum: string[];
};
export function parseEnumDef(def: ZodEnumDef): JsonSchema7EnumType {
return {
type: 'string',
enum: [...def.values],
};
}
@@ -0,0 +1,64 @@
import { ZodIntersectionDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
import { JsonSchema7StringType } from './string';
export type JsonSchema7AllOfType = {
allOf: JsonSchema7Type[];
unevaluatedProperties?: boolean;
};
const isJsonSchema7AllOfType = (
type: JsonSchema7Type | JsonSchema7StringType,
): type is JsonSchema7AllOfType => {
if ('type' in type && type.type === 'string') return false;
return 'allOf' in type;
};
export function parseIntersectionDef(
def: ZodIntersectionDef,
refs: Refs,
): JsonSchema7AllOfType | JsonSchema7Type | undefined {
const allOf = [
parseDef(def.left._def, {
...refs,
currentPath: [...refs.currentPath, 'allOf', '0'],
}),
parseDef(def.right._def, {
...refs,
currentPath: [...refs.currentPath, 'allOf', '1'],
}),
].filter((x): x is JsonSchema7Type => !!x);
let unevaluatedProperties: Pick<JsonSchema7AllOfType, 'unevaluatedProperties'> | undefined =
refs.target === 'jsonSchema2019-09' ? { unevaluatedProperties: false } : undefined;
const mergedAllOf: JsonSchema7Type[] = [];
// If either of the schemas is an allOf, merge them into a single allOf
allOf.forEach((schema) => {
if (isJsonSchema7AllOfType(schema)) {
mergedAllOf.push(...schema.allOf);
if (schema.unevaluatedProperties === undefined) {
// If one of the schemas has no unevaluatedProperties set,
// the merged schema should also have no unevaluatedProperties set
unevaluatedProperties = undefined;
}
} else {
let nestedSchema: JsonSchema7Type = schema;
if ('additionalProperties' in schema && schema.additionalProperties === false) {
const { additionalProperties, ...rest } = schema;
nestedSchema = rest;
} else {
// As soon as one of the schemas has additionalProperties set not to false, we allow unevaluatedProperties
unevaluatedProperties = undefined;
}
mergedAllOf.push(nestedSchema);
}
});
return mergedAllOf.length ?
{
allOf: mergedAllOf,
...unevaluatedProperties,
}
: undefined;
}
+37
View File
@@ -0,0 +1,37 @@
import { ZodLiteralDef } from 'zod';
import { Refs } from '../Refs';
export type JsonSchema7LiteralType =
| {
type: 'string' | 'number' | 'integer' | 'boolean';
const: string | number | boolean;
}
| {
type: 'object' | 'array';
};
export function parseLiteralDef(def: ZodLiteralDef, refs: Refs): JsonSchema7LiteralType {
const parsedType = typeof def.value;
if (
parsedType !== 'bigint' &&
parsedType !== 'number' &&
parsedType !== 'boolean' &&
parsedType !== 'string'
) {
return {
type: Array.isArray(def.value) ? 'array' : 'object',
};
}
if (refs.target === 'openApi3') {
return {
type: parsedType === 'bigint' ? 'integer' : parsedType,
enum: [def.value],
} as any;
}
return {
type: parsedType === 'bigint' ? 'integer' : parsedType,
const: def.value,
};
}
+42
View File
@@ -0,0 +1,42 @@
import { ZodMapDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
import { JsonSchema7RecordType, parseRecordDef } from './record';
export type JsonSchema7MapType = {
type: 'array';
maxItems: 125;
items: {
type: 'array';
items: [JsonSchema7Type, JsonSchema7Type];
minItems: 2;
maxItems: 2;
};
};
export function parseMapDef(def: ZodMapDef, refs: Refs): JsonSchema7MapType | JsonSchema7RecordType {
if (refs.mapStrategy === 'record') {
return parseRecordDef(def, refs);
}
const keys =
parseDef(def.keyType._def, {
...refs,
currentPath: [...refs.currentPath, 'items', 'items', '0'],
}) || {};
const values =
parseDef(def.valueType._def, {
...refs,
currentPath: [...refs.currentPath, 'items', 'items', '1'],
}) || {};
return {
type: 'array',
maxItems: 125,
items: {
type: 'array',
items: [keys, values],
minItems: 2,
maxItems: 2,
},
};
}
@@ -0,0 +1,27 @@
import { ZodNativeEnumDef } from 'zod';
export type JsonSchema7NativeEnumType = {
type: 'string' | 'number' | ['string', 'number'];
enum: (string | number)[];
};
export function parseNativeEnumDef(def: ZodNativeEnumDef): JsonSchema7NativeEnumType {
const object = def.values;
const actualKeys = Object.keys(def.values).filter((key: string) => {
return typeof object[object[key]!] !== 'number';
});
const actualValues = actualKeys.map((key: string) => object[key]!);
const parsedTypes = Array.from(new Set(actualValues.map((values: string | number) => typeof values)));
return {
type:
parsedTypes.length === 1 ?
parsedTypes[0] === 'string' ?
'string'
: 'number'
: ['string', 'number'],
enum: actualValues,
};
}
+9
View File
@@ -0,0 +1,9 @@
export type JsonSchema7NeverType = {
not: {};
};
export function parseNeverDef(): JsonSchema7NeverType {
return {
not: {},
};
}
+16
View File
@@ -0,0 +1,16 @@
import { Refs } from '../Refs';
export type JsonSchema7NullType = {
type: 'null';
};
export function parseNullDef(refs: Refs): JsonSchema7NullType {
return refs.target === 'openApi3' ?
({
enum: ['null'],
nullable: true,
} as any)
: {
type: 'null',
};
}
+49
View File
@@ -0,0 +1,49 @@
import { ZodNullableDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
import { JsonSchema7NullType } from './null';
import { primitiveMappings } from './union';
export type JsonSchema7NullableType =
| {
anyOf: [JsonSchema7Type, JsonSchema7NullType];
}
| {
type: [string, 'null'];
};
export function parseNullableDef(def: ZodNullableDef, refs: Refs): JsonSchema7NullableType | undefined {
if (
['ZodString', 'ZodNumber', 'ZodBigInt', 'ZodBoolean', 'ZodNull'].includes(def.innerType._def.typeName) &&
(!def.innerType._def.checks || !def.innerType._def.checks.length)
) {
if (refs.target === 'openApi3' || refs.nullableStrategy === 'property') {
return {
type: primitiveMappings[def.innerType._def.typeName as keyof typeof primitiveMappings],
nullable: true,
} as any;
}
return {
type: [primitiveMappings[def.innerType._def.typeName as keyof typeof primitiveMappings], 'null'],
};
}
if (refs.target === 'openApi3') {
const base = parseDef(def.innerType._def, {
...refs,
currentPath: [...refs.currentPath],
});
if (base && '$ref' in base) return { allOf: [base], nullable: true } as any;
return base && ({ ...base, nullable: true } as any);
}
const base = parseDef(def.innerType._def, {
...refs,
currentPath: [...refs.currentPath, 'anyOf', '0'],
});
return base && { anyOf: [base, { type: 'null' }] };
}
+62
View File
@@ -0,0 +1,62 @@
import { ZodNumberDef } from 'zod';
import { addErrorMessage, ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
import { Refs } from '../Refs';
export type JsonSchema7NumberType = {
type: 'number' | 'integer';
minimum?: number;
exclusiveMinimum?: number;
maximum?: number;
exclusiveMaximum?: number;
multipleOf?: number;
errorMessage?: ErrorMessages<JsonSchema7NumberType>;
};
export function parseNumberDef(def: ZodNumberDef, refs: Refs): JsonSchema7NumberType {
const res: JsonSchema7NumberType = {
type: 'number',
};
if (!def.checks) return res;
for (const check of def.checks) {
switch (check.kind) {
case 'int':
res.type = 'integer';
addErrorMessage(res, 'type', check.message, refs);
break;
case 'min':
if (refs.target === 'jsonSchema7') {
if (check.inclusive) {
setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs);
} else {
setResponseValueAndErrors(res, 'exclusiveMinimum', check.value, check.message, refs);
}
} else {
if (!check.inclusive) {
res.exclusiveMinimum = true as any;
}
setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs);
}
break;
case 'max':
if (refs.target === 'jsonSchema7') {
if (check.inclusive) {
setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs);
} else {
setResponseValueAndErrors(res, 'exclusiveMaximum', check.value, check.message, refs);
}
} else {
if (!check.inclusive) {
res.exclusiveMaximum = true as any;
}
setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs);
}
break;
case 'multipleOf':
setResponseValueAndErrors(res, 'multipleOf', check.value, check.message, refs);
break;
}
}
return res;
}
+71
View File
@@ -0,0 +1,71 @@
import { ZodObjectDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
function decideAdditionalProperties(def: ZodObjectDef, refs: Refs) {
if (refs.removeAdditionalStrategy === 'strict') {
return def.catchall._def.typeName === 'ZodNever' ?
def.unknownKeys !== 'strict'
: parseDef(def.catchall._def, {
...refs,
currentPath: [...refs.currentPath, 'additionalProperties'],
}) ?? true;
} else {
return def.catchall._def.typeName === 'ZodNever' ?
def.unknownKeys === 'passthrough'
: parseDef(def.catchall._def, {
...refs,
currentPath: [...refs.currentPath, 'additionalProperties'],
}) ?? true;
}
}
export type JsonSchema7ObjectType = {
type: 'object';
properties: Record<string, JsonSchema7Type>;
additionalProperties: boolean | JsonSchema7Type;
required?: string[];
};
export function parseObjectDef(def: ZodObjectDef, refs: Refs) {
const result: JsonSchema7ObjectType = {
type: 'object',
...Object.entries(def.shape()).reduce(
(
acc: {
properties: Record<string, JsonSchema7Type>;
required: string[];
},
[propName, propDef],
) => {
if (propDef === undefined || propDef._def === undefined) return acc;
const propertyPath = [...refs.currentPath, 'properties', propName];
const parsedDef = parseDef(propDef._def, {
...refs,
currentPath: propertyPath,
propertyPath,
});
if (parsedDef === undefined) return acc;
if (refs.openaiStrictMode && propDef.isOptional() && !propDef.isNullable()) {
console.warn(
`Zod field at \`${propertyPath.join(
'/',
)}\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required\nThis will become an error in a future version of the SDK.`,
);
}
return {
properties: {
...acc.properties,
[propName]: parsedDef,
},
required:
propDef.isOptional() && !refs.openaiStrictMode ? acc.required : [...acc.required, propName],
};
},
{ properties: {}, required: [] },
),
additionalProperties: decideAdditionalProperties(def, refs),
};
if (!result.required!.length) delete result.required;
return result;
}
+25
View File
@@ -0,0 +1,25 @@
import { ZodOptionalDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export const parseOptionalDef = (def: ZodOptionalDef, refs: Refs): JsonSchema7Type | undefined => {
if (refs.currentPath.toString() === refs.propertyPath?.toString()) {
return parseDef(def.innerType._def, refs);
}
const innerSchema = parseDef(def.innerType._def, {
...refs,
currentPath: [...refs.currentPath, 'anyOf', '1'],
});
return innerSchema ?
{
anyOf: [
{
not: {},
},
innerSchema,
],
}
: {};
};
+28
View File
@@ -0,0 +1,28 @@
import { ZodPipelineDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
import { JsonSchema7AllOfType } from './intersection';
export const parsePipelineDef = (
def: ZodPipelineDef<any, any>,
refs: Refs,
): JsonSchema7AllOfType | JsonSchema7Type | undefined => {
if (refs.pipeStrategy === 'input') {
return parseDef(def.in._def, refs);
} else if (refs.pipeStrategy === 'output') {
return parseDef(def.out._def, refs);
}
const a = parseDef(def.in._def, {
...refs,
currentPath: [...refs.currentPath, 'allOf', '0'],
});
const b = parseDef(def.out._def, {
...refs,
currentPath: [...refs.currentPath, 'allOf', a ? '1' : '0'],
});
return {
allOf: [a, b].filter((x): x is JsonSchema7Type => x !== undefined),
};
};
@@ -0,0 +1,7 @@
import { ZodPromiseDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export function parsePromiseDef(def: ZodPromiseDef, refs: Refs): JsonSchema7Type | undefined {
return parseDef(def.type._def, refs);
}
@@ -0,0 +1,7 @@
import { ZodReadonlyDef } from 'zod';
import { parseDef } from '../parseDef';
import { Refs } from '../Refs';
export const parseReadonlyDef = (def: ZodReadonlyDef<any>, refs: Refs) => {
return parseDef(def.innerType._def, refs);
};
+73
View File
@@ -0,0 +1,73 @@
import { ZodFirstPartyTypeKind, ZodMapDef, ZodRecordDef, ZodTypeAny } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
import { JsonSchema7EnumType } from './enum';
import { JsonSchema7ObjectType } from './object';
import { JsonSchema7StringType, parseStringDef } from './string';
type JsonSchema7RecordPropertyNamesType =
| Omit<JsonSchema7StringType, 'type'>
| Omit<JsonSchema7EnumType, 'type'>;
export type JsonSchema7RecordType = {
type: 'object';
additionalProperties: JsonSchema7Type;
propertyNames?: JsonSchema7RecordPropertyNamesType;
};
export function parseRecordDef(
def: ZodRecordDef<ZodTypeAny, ZodTypeAny> | ZodMapDef,
refs: Refs,
): JsonSchema7RecordType {
if (refs.target === 'openApi3' && def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
return {
type: 'object',
required: def.keyType._def.values,
properties: def.keyType._def.values.reduce(
(acc: Record<string, JsonSchema7Type>, key: string) => ({
...acc,
[key]:
parseDef(def.valueType._def, {
...refs,
currentPath: [...refs.currentPath, 'properties', key],
}) ?? {},
}),
{},
),
additionalProperties: false,
} satisfies JsonSchema7ObjectType as any;
}
const schema: JsonSchema7RecordType = {
type: 'object',
additionalProperties:
parseDef(def.valueType._def, {
...refs,
currentPath: [...refs.currentPath, 'additionalProperties'],
}) ?? {},
};
if (refs.target === 'openApi3') {
return schema;
}
if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodString && def.keyType._def.checks?.length) {
const keyType: JsonSchema7RecordPropertyNamesType = Object.entries(
parseStringDef(def.keyType._def, refs),
).reduce((acc, [key, value]) => (key === 'type' ? acc : { ...acc, [key]: value }), {});
return {
...schema,
propertyNames: keyType,
};
} else if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
return {
...schema,
propertyNames: {
enum: def.keyType._def.values,
},
};
}
return schema;
}
+36
View File
@@ -0,0 +1,36 @@
import { ZodSetDef } from 'zod';
import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export type JsonSchema7SetType = {
type: 'array';
uniqueItems: true;
items?: JsonSchema7Type | undefined;
minItems?: number;
maxItems?: number;
errorMessage?: ErrorMessages<JsonSchema7SetType>;
};
export function parseSetDef(def: ZodSetDef, refs: Refs): JsonSchema7SetType {
const items = parseDef(def.valueType._def, {
...refs,
currentPath: [...refs.currentPath, 'items'],
});
const schema: JsonSchema7SetType = {
type: 'array',
uniqueItems: true,
items,
};
if (def.minSize) {
setResponseValueAndErrors(schema, 'minItems', def.minSize.value, def.minSize.message, refs);
}
if (def.maxSize) {
setResponseValueAndErrors(schema, 'maxItems', def.maxSize.value, def.maxSize.message, refs);
}
return schema;
}
+400
View File
@@ -0,0 +1,400 @@
// @ts-nocheck
import { ZodStringDef } from 'zod';
import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages';
import { Refs } from '../Refs';
let emojiRegex: RegExp | undefined;
/**
* Generated from the regular expressions found here as of 2024-05-22:
* https://github.com/colinhacks/zod/blob/master/src/types.ts.
*
* Expressions with /i flag have been changed accordingly.
*/
export const zodPatterns = {
/**
* `c` was changed to `[cC]` to replicate /i flag
*/
cuid: /^[cC][^\s-]{8,}$/,
cuid2: /^[0-9a-z]+$/,
ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
/**
* `a-z` was added to replicate /i flag
*/
email: /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/,
/**
* Constructed a valid Unicode RegExp
*
* Lazily instantiate since this type of regex isn't supported
* in all envs (e.g. React Native).
*
* See:
* https://github.com/colinhacks/zod/issues/2433
* Fix in Zod:
* https://github.com/colinhacks/zod/commit/9340fd51e48576a75adc919bff65dbc4a5d4c99b
*/
emoji: () => {
if (emojiRegex === undefined) {
emojiRegex = RegExp('^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$', 'u');
}
return emojiRegex;
},
/**
* Unused
*/
uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
/**
* Unused
*/
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,
/**
* Unused
*/
ipv6: /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,
base64: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,
nanoid: /^[a-zA-Z0-9_-]{21}$/,
} as const;
export type JsonSchema7StringType = {
type: 'string';
minLength?: number;
maxLength?: number;
format?:
| 'email'
| 'idn-email'
| 'uri'
| 'uuid'
| 'date-time'
| 'ipv4'
| 'ipv6'
| 'date'
| 'time'
| 'duration';
pattern?: string;
allOf?: {
pattern: string;
errorMessage?: ErrorMessages<{ pattern: string }>;
}[];
anyOf?: {
format: string;
errorMessage?: ErrorMessages<{ format: string }>;
}[];
errorMessage?: ErrorMessages<JsonSchema7StringType>;
contentEncoding?: string;
};
export function parseStringDef(def: ZodStringDef, refs: Refs): JsonSchema7StringType {
const res: JsonSchema7StringType = {
type: 'string',
};
function processPattern(value: string): string {
return refs.patternStrategy === 'escape' ? escapeNonAlphaNumeric(value) : value;
}
if (def.checks) {
for (const check of def.checks) {
switch (check.kind) {
case 'min':
setResponseValueAndErrors(
res,
'minLength',
typeof res.minLength === 'number' ? Math.max(res.minLength, check.value) : check.value,
check.message,
refs,
);
break;
case 'max':
setResponseValueAndErrors(
res,
'maxLength',
typeof res.maxLength === 'number' ? Math.min(res.maxLength, check.value) : check.value,
check.message,
refs,
);
break;
case 'email':
switch (refs.emailStrategy) {
case 'format:email':
addFormat(res, 'email', check.message, refs);
break;
case 'format:idn-email':
addFormat(res, 'idn-email', check.message, refs);
break;
case 'pattern:zod':
addPattern(res, zodPatterns.email, check.message, refs);
break;
}
break;
case 'url':
addFormat(res, 'uri', check.message, refs);
break;
case 'uuid':
addFormat(res, 'uuid', check.message, refs);
break;
case 'regex':
addPattern(res, check.regex, check.message, refs);
break;
case 'cuid':
addPattern(res, zodPatterns.cuid, check.message, refs);
break;
case 'cuid2':
addPattern(res, zodPatterns.cuid2, check.message, refs);
break;
case 'startsWith':
addPattern(res, RegExp(`^${processPattern(check.value)}`), check.message, refs);
break;
case 'endsWith':
addPattern(res, RegExp(`${processPattern(check.value)}$`), check.message, refs);
break;
case 'datetime':
addFormat(res, 'date-time', check.message, refs);
break;
case 'date':
addFormat(res, 'date', check.message, refs);
break;
case 'time':
addFormat(res, 'time', check.message, refs);
break;
case 'duration':
addFormat(res, 'duration', check.message, refs);
break;
case 'length':
setResponseValueAndErrors(
res,
'minLength',
typeof res.minLength === 'number' ? Math.max(res.minLength, check.value) : check.value,
check.message,
refs,
);
setResponseValueAndErrors(
res,
'maxLength',
typeof res.maxLength === 'number' ? Math.min(res.maxLength, check.value) : check.value,
check.message,
refs,
);
break;
case 'includes': {
addPattern(res, RegExp(processPattern(check.value)), check.message, refs);
break;
}
case 'ip': {
if (check.version !== 'v6') {
addFormat(res, 'ipv4', check.message, refs);
}
if (check.version !== 'v4') {
addFormat(res, 'ipv6', check.message, refs);
}
break;
}
case 'emoji':
addPattern(res, zodPatterns.emoji, check.message, refs);
break;
case 'ulid': {
addPattern(res, zodPatterns.ulid, check.message, refs);
break;
}
case 'base64': {
switch (refs.base64Strategy) {
case 'format:binary': {
addFormat(res, 'binary' as any, check.message, refs);
break;
}
case 'contentEncoding:base64': {
setResponseValueAndErrors(res, 'contentEncoding', 'base64', check.message, refs);
break;
}
case 'pattern:zod': {
addPattern(res, zodPatterns.base64, check.message, refs);
break;
}
}
break;
}
case 'nanoid': {
addPattern(res, zodPatterns.nanoid, check.message, refs);
}
case 'toLowerCase':
case 'toUpperCase':
case 'trim':
break;
default:
((_: never) => {})(check);
}
}
}
return res;
}
const escapeNonAlphaNumeric = (value: string) =>
Array.from(value)
.map((c) => (/[a-zA-Z0-9]/.test(c) ? c : `\\${c}`))
.join('');
const addFormat = (
schema: JsonSchema7StringType,
value: Required<JsonSchema7StringType>['format'],
message: string | undefined,
refs: Refs,
) => {
if (schema.format || schema.anyOf?.some((x) => x.format)) {
if (!schema.anyOf) {
schema.anyOf = [];
}
if (schema.format) {
schema.anyOf!.push({
format: schema.format,
...(schema.errorMessage &&
refs.errorMessages && {
errorMessage: { format: schema.errorMessage.format },
}),
});
delete schema.format;
if (schema.errorMessage) {
delete schema.errorMessage.format;
if (Object.keys(schema.errorMessage).length === 0) {
delete schema.errorMessage;
}
}
}
schema.anyOf!.push({
format: value,
...(message && refs.errorMessages && { errorMessage: { format: message } }),
});
} else {
setResponseValueAndErrors(schema, 'format', value, message, refs);
}
};
const addPattern = (
schema: JsonSchema7StringType,
regex: RegExp | (() => RegExp),
message: string | undefined,
refs: Refs,
) => {
if (schema.pattern || schema.allOf?.some((x) => x.pattern)) {
if (!schema.allOf) {
schema.allOf = [];
}
if (schema.pattern) {
schema.allOf!.push({
pattern: schema.pattern,
...(schema.errorMessage &&
refs.errorMessages && {
errorMessage: { pattern: schema.errorMessage.pattern },
}),
});
delete schema.pattern;
if (schema.errorMessage) {
delete schema.errorMessage.pattern;
if (Object.keys(schema.errorMessage).length === 0) {
delete schema.errorMessage;
}
}
}
schema.allOf!.push({
pattern: processRegExp(regex, refs),
...(message && refs.errorMessages && { errorMessage: { pattern: message } }),
});
} else {
setResponseValueAndErrors(schema, 'pattern', processRegExp(regex, refs), message, refs);
}
};
// Mutate z.string.regex() in a best attempt to accommodate for regex flags when applyRegexFlags is true
const processRegExp = (regexOrFunction: RegExp | (() => RegExp), refs: Refs): string => {
const regex = typeof regexOrFunction === 'function' ? regexOrFunction() : regexOrFunction;
if (!refs.applyRegexFlags || !regex.flags) return regex.source;
// Currently handled flags
const flags = {
i: regex.flags.includes('i'), // Case-insensitive
m: regex.flags.includes('m'), // `^` and `$` matches adjacent to newline characters
s: regex.flags.includes('s'), // `.` matches newlines
};
// The general principle here is to step through each character, one at a time, applying mutations as flags require. We keep track when the current character is escaped, and when it's inside a group /like [this]/ or (also) a range like /[a-z]/. The following is fairly brittle imperative code; edit at your peril!
const source = flags.i ? regex.source.toLowerCase() : regex.source;
let pattern = '';
let isEscaped = false;
let inCharGroup = false;
let inCharRange = false;
for (let i = 0; i < source.length; i++) {
if (isEscaped) {
pattern += source[i];
isEscaped = false;
continue;
}
if (flags.i) {
if (inCharGroup) {
if (source[i].match(/[a-z]/)) {
if (inCharRange) {
pattern += source[i];
pattern += `${source[i - 2]}-${source[i]}`.toUpperCase();
inCharRange = false;
} else if (source[i + 1] === '-' && source[i + 2]?.match(/[a-z]/)) {
pattern += source[i];
inCharRange = true;
} else {
pattern += `${source[i]}${source[i].toUpperCase()}`;
}
continue;
}
} else if (source[i].match(/[a-z]/)) {
pattern += `[${source[i]}${source[i].toUpperCase()}]`;
continue;
}
}
if (flags.m) {
if (source[i] === '^') {
pattern += `(^|(?<=[\r\n]))`;
continue;
} else if (source[i] === '$') {
pattern += `($|(?=[\r\n]))`;
continue;
}
}
if (flags.s && source[i] === '.') {
pattern += inCharGroup ? `${source[i]}\r\n` : `[${source[i]}\r\n]`;
continue;
}
pattern += source[i];
if (source[i] === '\\') {
isEscaped = true;
} else if (inCharGroup && source[i] === ']') {
inCharGroup = false;
} else if (!inCharGroup && source[i] === '[') {
inCharGroup = true;
}
}
try {
const regexTest = new RegExp(pattern);
} catch {
console.warn(
`Could not convert regex pattern at ${refs.currentPath.join(
'/',
)} to a flag-independent form! Falling back to the flag-ignorant source`,
);
return regex.source;
}
return pattern;
};
+54
View File
@@ -0,0 +1,54 @@
import { ZodTupleDef, ZodTupleItems, ZodTypeAny } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export type JsonSchema7TupleType = {
type: 'array';
minItems: number;
items: JsonSchema7Type[];
} & (
| {
maxItems: number;
}
| {
additionalItems?: JsonSchema7Type | undefined;
}
);
export function parseTupleDef(
def: ZodTupleDef<ZodTupleItems | [], ZodTypeAny | null>,
refs: Refs,
): JsonSchema7TupleType {
if (def.rest) {
return {
type: 'array',
minItems: def.items.length,
items: def.items
.map((x, i) =>
parseDef(x._def, {
...refs,
currentPath: [...refs.currentPath, 'items', `${i}`],
}),
)
.reduce((acc: JsonSchema7Type[], x) => (x === undefined ? acc : [...acc, x]), []),
additionalItems: parseDef(def.rest._def, {
...refs,
currentPath: [...refs.currentPath, 'additionalItems'],
}),
};
} else {
return {
type: 'array',
minItems: def.items.length,
maxItems: def.items.length,
items: def.items
.map((x, i) =>
parseDef(x._def, {
...refs,
currentPath: [...refs.currentPath, 'items', `${i}`],
}),
)
.reduce((acc: JsonSchema7Type[], x) => (x === undefined ? acc : [...acc, x]), []),
};
}
}
@@ -0,0 +1,9 @@
export type JsonSchema7UndefinedType = {
not: {};
};
export function parseUndefinedDef(): JsonSchema7UndefinedType {
return {
not: {},
};
}
+119
View File
@@ -0,0 +1,119 @@
import { ZodDiscriminatedUnionDef, ZodLiteralDef, ZodTypeAny, ZodUnionDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';
export const primitiveMappings = {
ZodString: 'string',
ZodNumber: 'number',
ZodBigInt: 'integer',
ZodBoolean: 'boolean',
ZodNull: 'null',
} as const;
type ZodPrimitive = keyof typeof primitiveMappings;
type JsonSchema7Primitive = (typeof primitiveMappings)[keyof typeof primitiveMappings];
export type JsonSchema7UnionType = JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType;
type JsonSchema7PrimitiveUnionType =
| {
type: JsonSchema7Primitive | JsonSchema7Primitive[];
}
| {
type: JsonSchema7Primitive | JsonSchema7Primitive[];
enum: (string | number | bigint | boolean | null)[];
};
type JsonSchema7AnyOfType = {
anyOf: JsonSchema7Type[];
};
export function parseUnionDef(
def: ZodUnionDef | ZodDiscriminatedUnionDef<any, any>,
refs: Refs,
): JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType | undefined {
if (refs.target === 'openApi3') return asAnyOf(def, refs);
const options: readonly ZodTypeAny[] =
def.options instanceof Map ? Array.from(def.options.values()) : def.options;
// This blocks tries to look ahead a bit to produce nicer looking schemas with type array instead of anyOf.
if (
options.every((x) => x._def.typeName in primitiveMappings && (!x._def.checks || !x._def.checks.length))
) {
// all types in union are primitive and lack checks, so might as well squash into {type: [...]}
const types = options.reduce((types: JsonSchema7Primitive[], x) => {
const type = primitiveMappings[x._def.typeName as ZodPrimitive]; //Can be safely casted due to row 43
return type && !types.includes(type) ? [...types, type] : types;
}, []);
return {
type: types.length > 1 ? types : types[0]!,
};
} else if (options.every((x) => x._def.typeName === 'ZodLiteral' && !x.description)) {
// all options literals
const types = options.reduce((acc: JsonSchema7Primitive[], x: { _def: ZodLiteralDef }) => {
const type = typeof x._def.value;
switch (type) {
case 'string':
case 'number':
case 'boolean':
return [...acc, type];
case 'bigint':
return [...acc, 'integer' as const];
case 'object':
if (x._def.value === null) return [...acc, 'null' as const];
case 'symbol':
case 'undefined':
case 'function':
default:
return acc;
}
}, []);
if (types.length === options.length) {
// all the literals are primitive, as far as null can be considered primitive
const uniqueTypes = types.filter((x, i, a) => a.indexOf(x) === i);
return {
type: uniqueTypes.length > 1 ? uniqueTypes : uniqueTypes[0]!,
enum: options.reduce(
(acc, x) => {
return acc.includes(x._def.value) ? acc : [...acc, x._def.value];
},
[] as (string | number | bigint | boolean | null)[],
),
};
}
} else if (options.every((x) => x._def.typeName === 'ZodEnum')) {
return {
type: 'string',
enum: options.reduce(
(acc: string[], x) => [...acc, ...x._def.values.filter((x: string) => !acc.includes(x))],
[],
),
};
}
return asAnyOf(def, refs);
}
const asAnyOf = (
def: ZodUnionDef | ZodDiscriminatedUnionDef<any, any>,
refs: Refs,
): JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType | undefined => {
const anyOf = ((def.options instanceof Map ? Array.from(def.options.values()) : def.options) as any[])
.map((x, i) =>
parseDef(x._def, {
...refs,
currentPath: [...refs.currentPath, 'anyOf', `${i}`],
}),
)
.filter(
(x): x is JsonSchema7Type =>
!!x && (!refs.strictUnions || (typeof x === 'object' && Object.keys(x).length > 0)),
);
return anyOf.length ? { anyOf } : undefined;
};
@@ -0,0 +1,5 @@
export type JsonSchema7UnknownType = {};
export function parseUnknownDef(): JsonSchema7UnknownType {
return {};
}
+11
View File
@@ -0,0 +1,11 @@
import type { ZodSchema, ZodTypeDef } from 'zod';
export const zodDef = (zodSchema: ZodSchema | ZodTypeDef): ZodTypeDef => {
return '_def' in zodSchema ? zodSchema._def : zodSchema;
};
export function isEmptyObj(obj: Object | null | undefined): boolean {
if (!obj) return true;
for (const _k in obj) return false;
return true;
}
+120
View File
@@ -0,0 +1,120 @@
import { ZodSchema } from 'zod';
import { Options, Targets } from './Options';
import { JsonSchema7Type, parseDef } from './parseDef';
import { getRefs } from './Refs';
import { zodDef, isEmptyObj } from './util';
const zodToJsonSchema = <Target extends Targets = 'jsonSchema7'>(
schema: ZodSchema<any>,
options?: Partial<Options<Target>> | string,
): (Target extends 'jsonSchema7' ? JsonSchema7Type : object) & {
$schema?: string;
definitions?: {
[key: string]: Target extends 'jsonSchema7' ? JsonSchema7Type
: Target extends 'jsonSchema2019-09' ? JsonSchema7Type
: object;
};
} => {
const refs = getRefs(options);
const name =
typeof options === 'string' ? options
: options?.nameStrategy === 'title' ? undefined
: options?.name;
const main =
parseDef(
schema._def,
name === undefined ? refs : (
{
...refs,
currentPath: [...refs.basePath, refs.definitionPath, name],
}
),
false,
) ?? {};
const title =
typeof options === 'object' && options.name !== undefined && options.nameStrategy === 'title' ?
options.name
: undefined;
if (title !== undefined) {
main.title = title;
}
const definitions = (() => {
if (isEmptyObj(refs.definitions)) {
return undefined;
}
const definitions: Record<string, any> = {};
const processedDefinitions = new Set();
// the call to `parseDef()` here might itself add more entries to `.definitions`
// so we need to continually evaluate definitions until we've resolved all of them
//
// we have a generous iteration limit here to avoid blowing up the stack if there
// are any bugs that would otherwise result in us iterating indefinitely
for (let i = 0; i < 500; i++) {
const newDefinitions = Object.entries(refs.definitions).filter(
([key]) => !processedDefinitions.has(key),
);
if (newDefinitions.length === 0) break;
for (const [key, schema] of newDefinitions) {
definitions[key] =
parseDef(
zodDef(schema),
{ ...refs, currentPath: [...refs.basePath, refs.definitionPath, key] },
true,
) ?? {};
processedDefinitions.add(key);
}
}
return definitions;
})();
const combined: ReturnType<typeof zodToJsonSchema<Target>> =
name === undefined ?
definitions ?
{
...main,
[refs.definitionPath]: definitions,
}
: main
: refs.nameStrategy === 'duplicate-ref' ?
{
...main,
...(definitions || refs.seenRefs.size ?
{
[refs.definitionPath]: {
...definitions,
// only actually duplicate the schema definition if it was ever referenced
// otherwise the duplication is completely pointless
...(refs.seenRefs.size ? { [name]: main } : undefined),
},
}
: undefined),
}
: {
$ref: [...(refs.$refStrategy === 'relative' ? [] : refs.basePath), refs.definitionPath, name].join(
'/',
),
[refs.definitionPath]: {
...definitions,
[name]: main,
},
};
if (refs.target === 'jsonSchema7') {
combined.$schema = 'http://json-schema.org/draft-07/schema#';
} else if (refs.target === 'jsonSchema2019-09') {
combined.$schema = 'https://json-schema.org/draft/2019-09/schema#';
}
return combined;
};
export { zodToJsonSchema };
+1
View File
@@ -0,0 +1 @@
export { OpenAIRealtimeError } from './internal-base';
+93
View File
@@ -0,0 +1,93 @@
import { RealtimeClientEvent, RealtimeServerEvent, ErrorEvent } from '../../resources/beta/realtime/realtime';
import { EventEmitter } from '../../lib/EventEmitter';
import { OpenAIError } from '../../error';
import OpenAI, { AzureOpenAI } from '../../index';
export class OpenAIRealtimeError extends OpenAIError {
/**
* The error data that the API sent back in an `error` event.
*/
error?: ErrorEvent.Error | undefined;
/**
* The unique ID of the server event.
*/
event_id?: string | undefined;
constructor(message: string, event: ErrorEvent | null) {
super(message);
this.error = event?.error;
this.event_id = event?.event_id;
}
}
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
type RealtimeEvents = Simplify<
{
event: (event: RealtimeServerEvent) => void;
error: (error: OpenAIRealtimeError) => void;
} & {
[EventType in Exclude<RealtimeServerEvent['type'], 'error'>]: (
event: Extract<RealtimeServerEvent, { type: EventType }>,
) => unknown;
}
>;
export abstract class OpenAIRealtimeEmitter extends EventEmitter<RealtimeEvents> {
/**
* Send an event to the API.
*/
abstract send(event: RealtimeClientEvent): void;
/**
* Close the websocket connection.
*/
abstract close(props?: { code: number; reason: string }): void;
protected _onError(event: null, message: string, cause: any): void;
protected _onError(event: ErrorEvent, message?: string | undefined): void;
protected _onError(event: ErrorEvent | null, message?: string | undefined, cause?: any): void {
message =
event?.error ?
`${event.error.message} code=${event.error.code} param=${event.error.param} type=${event.error.type} event_id=${event.error.event_id}`
: message ?? 'unknown error';
if (!this._hasListener('error')) {
const error = new OpenAIRealtimeError(
message +
`\n\nTo resolve these unhandled rejection errors you should bind an \`error\` callback, e.g. \`rt.on('error', (error) => ...)\` `,
event,
);
// @ts-ignore
error.cause = cause;
Promise.reject(error);
return;
}
const error = new OpenAIRealtimeError(message, event);
// @ts-ignore
error.cause = cause;
this._emit('error', error);
}
}
export function isAzure(client: Pick<OpenAI, 'apiKey' | 'baseURL'>): client is AzureOpenAI {
return client instanceof AzureOpenAI;
}
export function buildRealtimeURL(client: Pick<OpenAI, 'apiKey' | 'baseURL'>, model: string): URL {
const path = '/realtime';
const baseURL = client.baseURL;
const url = new URL(baseURL + (baseURL.endsWith('/') ? path.slice(1) : path));
url.protocol = 'wss';
if (isAzure(client)) {
url.searchParams.set('api-version', client.apiVersion);
url.searchParams.set('deployment', model);
} else {
url.searchParams.set('model', model);
}
return url;
}
+143
View File
@@ -0,0 +1,143 @@
import { AzureOpenAI, OpenAI } from '../../index';
import { OpenAIError } from '../../error';
import * as Core from '../../core';
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base';
interface MessageEvent {
data: string;
}
type _WebSocket =
typeof globalThis extends (
{
WebSocket: infer ws extends abstract new (...args: any) => any;
}
) ?
// @ts-ignore
InstanceType<ws>
: any;
export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
url: URL;
socket: _WebSocket;
constructor(
props: {
model: string;
dangerouslyAllowBrowser?: boolean;
/**
* Callback to mutate the URL, needed for Azure.
* @internal
*/
onURL?: (url: URL) => void;
},
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
) {
super();
const dangerouslyAllowBrowser =
props.dangerouslyAllowBrowser ??
(client as any)?._options?.dangerouslyAllowBrowser ??
(client?.apiKey.startsWith('ek_') ? true : null);
if (!dangerouslyAllowBrowser && Core.isRunningInBrowser()) {
throw new OpenAIError(
"It looks like you're running in a browser-like environment.\n\nThis is disabled by default, as it risks exposing your secret API credentials to attackers.\n\nYou can avoid this error by creating an ephemeral session token:\nhttps://platform.openai.com/docs/api-reference/realtime-sessions\n",
);
}
client ??= new OpenAI({ dangerouslyAllowBrowser });
this.url = buildRealtimeURL(client, props.model);
props.onURL?.(this.url);
// @ts-ignore
this.socket = new WebSocket(this.url.toString(), [
'realtime',
...(isAzure(client) ? [] : [`openai-insecure-api-key.${client.apiKey}`]),
'openai-beta.realtime-v1',
]);
this.socket.addEventListener('message', (websocketEvent: MessageEvent) => {
const event = (() => {
try {
return JSON.parse(websocketEvent.data.toString()) as RealtimeServerEvent;
} catch (err) {
this._onError(null, 'could not parse websocket event', err);
return null;
}
})();
if (event) {
this._emit('event', event);
if (event.type === 'error') {
this._onError(event);
} else {
// @ts-expect-error TS isn't smart enough to get the relationship right here
this._emit(event.type, event);
}
}
});
this.socket.addEventListener('error', (event: any) => {
this._onError(null, event.message, null);
});
if (isAzure(client)) {
if (this.url.searchParams.get('Authorization') !== null) {
this.url.searchParams.set('Authorization', '<REDACTED>');
} else {
this.url.searchParams.set('api-key', '<REDACTED>');
}
}
}
static async azure(
client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
options: { deploymentName?: string; dangerouslyAllowBrowser?: boolean } = {},
): Promise<OpenAIRealtimeWebSocket> {
const token = await client._getAzureADToken();
function onURL(url: URL) {
if (client.apiKey !== '<Missing Key>') {
url.searchParams.set('api-key', client.apiKey);
} else {
if (token) {
url.searchParams.set('Authorization', `Bearer ${token}`);
} else {
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
}
}
}
const deploymentName = options.deploymentName ?? client.deploymentName;
if (!deploymentName) {
throw new Error('No deployment name provided');
}
const { dangerouslyAllowBrowser } = options;
return new OpenAIRealtimeWebSocket(
{
model: deploymentName,
onURL,
...(dangerouslyAllowBrowser ? { dangerouslyAllowBrowser } : {}),
},
client,
);
}
send(event: RealtimeClientEvent) {
try {
this.socket.send(JSON.stringify(event));
} catch (err) {
this._onError(null, 'could not send data', err);
}
}
close(props?: { code: number; reason: string }) {
try {
this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
} catch (err) {
this._onError(null, 'could not close the connection', err);
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import * as WS from 'ws';
import { AzureOpenAI, OpenAI } from '../../index';
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base';
export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
url: URL;
socket: WS.WebSocket;
constructor(
props: { model: string; options?: WS.ClientOptions | undefined },
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
) {
super();
client ??= new OpenAI();
this.url = buildRealtimeURL(client, props.model);
this.socket = new WS.WebSocket(this.url, {
...props.options,
headers: {
...props.options?.headers,
...(isAzure(client) ? {} : { Authorization: `Bearer ${client.apiKey}` }),
'OpenAI-Beta': 'realtime=v1',
},
});
this.socket.on('message', (wsEvent) => {
const event = (() => {
try {
return JSON.parse(wsEvent.toString()) as RealtimeServerEvent;
} catch (err) {
this._onError(null, 'could not parse websocket event', err);
return null;
}
})();
if (event) {
this._emit('event', event);
if (event.type === 'error') {
this._onError(event);
} else {
// @ts-expect-error TS isn't smart enough to get the relationship right here
this._emit(event.type, event);
}
}
});
this.socket.on('error', (err) => {
this._onError(null, err.message, err);
});
}
static async azure(
client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
options: { deploymentName?: string; options?: WS.ClientOptions | undefined } = {},
): Promise<OpenAIRealtimeWS> {
const deploymentName = options.deploymentName ?? client.deploymentName;
if (!deploymentName) {
throw new Error('No deployment name provided');
}
return new OpenAIRealtimeWS(
{ model: deploymentName, options: { headers: await getAzureHeaders(client) } },
client,
);
}
send(event: RealtimeClientEvent) {
try {
this.socket.send(JSON.stringify(event));
} catch (err) {
this._onError(null, 'could not send data', err);
}
}
close(props?: { code: number; reason: string }) {
try {
this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
} catch (err) {
this._onError(null, 'could not close the connection', err);
}
}
}
async function getAzureHeaders(client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiKey'>) {
if (client.apiKey !== '<Missing Key>') {
return { 'api-key': client.apiKey };
} else {
const token = await client._getAzureADToken();
if (token) {
return { Authorization: `Bearer ${token}` };
} else {
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
}
}
}
+1330
View File
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { castToError, Headers } from './core';
export class OpenAIError extends Error {}
export class APIError<
TStatus extends number | undefined = number | undefined,
THeaders extends Headers | undefined = Headers | undefined,
TError extends Object | undefined = Object | undefined,
> extends OpenAIError {
/** HTTP status for the response that caused the error */
readonly status: TStatus;
/** HTTP headers for the response that caused the error */
readonly headers: THeaders;
/** JSON body of the response that caused the error */
readonly error: TError;
readonly code: string | null | undefined;
readonly param: string | null | undefined;
readonly type: string | undefined;
readonly request_id: string | null | undefined;
constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) {
super(`${APIError.makeMessage(status, error, message)}`);
this.status = status;
this.headers = headers;
this.request_id = headers?.['x-request-id'];
this.error = error;
const data = error as Record<string, any>;
this.code = data?.['code'];
this.param = data?.['param'];
this.type = data?.['type'];
}
private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
const msg =
error?.message ?
typeof error.message === 'string' ?
error.message
: JSON.stringify(error.message)
: error ? JSON.stringify(error)
: message;
if (status && msg) {
return `${status} ${msg}`;
}
if (status) {
return `${status} status code (no body)`;
}
if (msg) {
return msg;
}
return '(no status code or body)';
}
static generate(
status: number | undefined,
errorResponse: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
): APIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
const error = (errorResponse as Record<string, any>)?.['error'];
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
if (status === 401) {
return new AuthenticationError(status, error, message, headers);
}
if (status === 403) {
return new PermissionDeniedError(status, error, message, headers);
}
if (status === 404) {
return new NotFoundError(status, error, message, headers);
}
if (status === 409) {
return new ConflictError(status, error, message, headers);
}
if (status === 422) {
return new UnprocessableEntityError(status, error, message, headers);
}
if (status === 429) {
return new RateLimitError(status, error, message, headers);
}
if (status >= 500) {
return new InternalServerError(status, error, message, headers);
}
return new APIError(status, error, message, headers);
}
}
export class APIUserAbortError extends APIError<undefined, undefined, undefined> {
constructor({ message }: { message?: string } = {}) {
super(undefined, undefined, message || 'Request was aborted.', undefined);
}
}
export class APIConnectionError extends APIError<undefined, undefined, undefined> {
constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) {
super(undefined, undefined, message || 'Connection error.', undefined);
// in some environments the 'cause' property is already declared
// @ts-ignore
if (cause) this.cause = cause;
}
}
export class APIConnectionTimeoutError extends APIConnectionError {
constructor({ message }: { message?: string } = {}) {
super({ message: message ?? 'Request timed out.' });
}
}
export class BadRequestError extends APIError<400, Headers> {}
export class AuthenticationError extends APIError<401, Headers> {}
export class PermissionDeniedError extends APIError<403, Headers> {}
export class NotFoundError extends APIError<404, Headers> {}
export class ConflictError extends APIError<409, Headers> {}
export class UnprocessableEntityError extends APIError<422, Headers> {}
export class RateLimitError extends APIError<429, Headers> {}
export class InternalServerError extends APIError<number, Headers> {}
export class LengthFinishReasonError extends OpenAIError {
constructor() {
super(`Could not parse response content as the length limit was reached`);
}
}
export class ContentFilterFinishReasonError extends OpenAIError {
constructor() {
super(`Could not parse response content as the request was rejected by the content filter`);
}
}
+145
View File
@@ -0,0 +1,145 @@
import { File } from 'formdata-node';
import { spawn } from 'node:child_process';
import { Readable } from 'node:stream';
import { platform, versions } from 'node:process';
import { Response } from "../_shims";
const DEFAULT_SAMPLE_RATE = 24000;
const DEFAULT_CHANNELS = 1;
const isNode = Boolean(versions?.node);
const recordingProviders: Record<NodeJS.Platform, string> = {
win32: 'dshow',
darwin: 'avfoundation',
linux: 'alsa',
aix: 'alsa',
android: 'alsa',
freebsd: 'alsa',
haiku: 'alsa',
sunos: 'alsa',
netbsd: 'alsa',
openbsd: 'alsa',
cygwin: 'dshow',
};
function isResponse(stream: NodeJS.ReadableStream | Response | File): stream is Response {
return typeof (stream as any).body !== 'undefined';
}
function isFile(stream: NodeJS.ReadableStream | Response | File): stream is File {
return stream instanceof File;
}
async function nodejsPlayAudio(stream: NodeJS.ReadableStream | Response | File): Promise<void> {
return new Promise((resolve, reject) => {
try {
const ffplay = spawn('ffplay', ['-autoexit', '-nodisp', '-i', 'pipe:0']);
if (isResponse(stream)) {
stream.body.pipe(ffplay.stdin);
} else if (isFile(stream)) {
Readable.from(stream.stream()).pipe(ffplay.stdin);
} else {
stream.pipe(ffplay.stdin);
}
ffplay.on('close', (code: number) => {
if (code !== 0) {
reject(new Error(`ffplay process exited with code ${code}`));
}
resolve();
});
} catch (error) {
reject(error);
}
});
}
export async function playAudio(input: NodeJS.ReadableStream | Response | File): Promise<void> {
if (isNode) {
return nodejsPlayAudio(input);
}
throw new Error(
'Play audio is not supported in the browser yet. Check out https://npm.im/wavtools as an alternative.',
);
}
type RecordAudioOptions = {
signal?: AbortSignal;
device?: number;
timeout?: number;
};
function nodejsRecordAudio({ signal, device, timeout }: RecordAudioOptions = {}): Promise<File> {
return new Promise((resolve, reject) => {
const data: any[] = [];
const provider = recordingProviders[platform];
try {
const ffmpeg = spawn(
'ffmpeg',
[
'-f',
provider,
'-i',
`:${device ?? 0}`, // default audio input device; adjust as needed
'-ar',
DEFAULT_SAMPLE_RATE.toString(),
'-ac',
DEFAULT_CHANNELS.toString(),
'-f',
'wav',
'pipe:1',
],
{
stdio: ['ignore', 'pipe', 'pipe'],
},
);
ffmpeg.stdout.on('data', (chunk) => {
data.push(chunk);
});
ffmpeg.on('error', (error) => {
console.error(error);
reject(error);
});
ffmpeg.on('close', (code) => {
returnData();
});
function returnData() {
const audioBuffer = Buffer.concat(data);
const audioFile = new File([audioBuffer], 'audio.wav', { type: 'audio/wav' });
resolve(audioFile);
}
if (typeof timeout === 'number' && timeout > 0) {
const internalSignal = AbortSignal.timeout(timeout);
internalSignal.addEventListener('abort', () => {
ffmpeg.kill('SIGTERM');
});
}
if (signal) {
signal.addEventListener('abort', () => {
ffmpeg.kill('SIGTERM');
});
}
} catch (error) {
reject(error);
}
});
}
export async function recordAudio(options: RecordAudioOptions = {}) {
if (isNode) {
return nodejsRecordAudio(options);
}
throw new Error(
'Record audio is not supported in the browser. Check out https://npm.im/wavtools as an alternative.',
);
}
+154
View File
@@ -0,0 +1,154 @@
import { ResponseFormatJSONSchema } from '../resources/index';
import type { infer as zodInfer, ZodType } from 'zod';
import {
AutoParseableResponseFormat,
AutoParseableTextFormat,
AutoParseableTool,
makeParseableResponseFormat,
makeParseableTextFormat,
makeParseableTool,
} from '../lib/parser';
import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-schema';
import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser';
import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses';
function zodToJsonSchema(schema: ZodType, options: { name: string }): Record<string, unknown> {
return _zodToJsonSchema(schema, {
openaiStrictMode: true,
name: options.name,
nameStrategy: 'duplicate-ref',
$refStrategy: 'extract-to-root',
nullableStrategy: 'property',
});
}
/**
* Creates a chat completion `JSONSchema` response format object from
* the given Zod schema.
*
* If this is passed to the `.parse()`, `.stream()` or `.runTools()`
* chat completion methods then the response message will contain a
* `.parsed` property that is the result of parsing the content with
* the given Zod object.
*
* ```ts
* const completion = await client.beta.chat.completions.parse({
* model: 'gpt-4o-2024-08-06',
* messages: [
* { role: 'system', content: 'You are a helpful math tutor.' },
* { role: 'user', content: 'solve 8x + 31 = 2' },
* ],
* response_format: zodResponseFormat(
* z.object({
* steps: z.array(z.object({
* explanation: z.string(),
* answer: z.string(),
* })),
* final_answer: z.string(),
* }),
* 'math_answer',
* ),
* });
* const message = completion.choices[0]?.message;
* if (message?.parsed) {
* console.log(message.parsed);
* console.log(message.parsed.final_answer);
* }
* ```
*
* This can be passed directly to the `.create()` method but will not
* result in any automatic parsing, you'll have to parse the response yourself.
*/
export function zodResponseFormat<ZodInput extends ZodType>(
zodObject: ZodInput,
name: string,
props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'schema' | 'strict' | 'name'>,
): AutoParseableResponseFormat<zodInfer<ZodInput>> {
return makeParseableResponseFormat(
{
type: 'json_schema',
json_schema: {
...props,
name,
strict: true,
schema: zodToJsonSchema(zodObject, { name }),
},
},
(content) => zodObject.parse(JSON.parse(content)),
);
}
export function zodTextFormat<ZodInput extends ZodType>(
zodObject: ZodInput,
name: string,
props?: Omit<ResponseFormatTextJSONSchemaConfig, 'schema' | 'type' | 'strict' | 'name'>,
): AutoParseableTextFormat<zodInfer<ZodInput>> {
return makeParseableTextFormat(
{
type: 'json_schema',
...props,
name,
strict: true,
schema: zodToJsonSchema(zodObject, { name }),
},
(content) => zodObject.parse(JSON.parse(content)),
);
}
/**
* Creates a chat completion `function` tool that can be invoked
* automatically by the chat completion `.runTools()` method or automatically
* parsed by `.parse()` / `.stream()`.
*/
export function zodFunction<Parameters extends ZodType>(options: {
name: string;
parameters: Parameters;
function?: ((args: zodInfer<Parameters>) => unknown | Promise<unknown>) | undefined;
description?: string | undefined;
}): AutoParseableTool<{
arguments: Parameters;
name: string;
function: (args: zodInfer<Parameters>) => unknown;
}> {
// @ts-expect-error TODO
return makeParseableTool<any>(
{
type: 'function',
function: {
name: options.name,
parameters: zodToJsonSchema(options.parameters, { name: options.name }),
strict: true,
...(options.description ? { description: options.description } : undefined),
},
},
{
callback: options.function,
parser: (args) => options.parameters.parse(JSON.parse(args)),
},
);
}
export function zodResponsesFunction<Parameters extends ZodType>(options: {
name: string;
parameters: Parameters;
function?: ((args: zodInfer<Parameters>) => unknown | Promise<unknown>) | undefined;
description?: string | undefined;
}): AutoParseableResponseTool<{
arguments: Parameters;
name: string;
function: (args: zodInfer<Parameters>) => unknown;
}> {
return makeParseableResponseTool<any>(
{
type: 'function',
name: options.name,
parameters: zodToJsonSchema(options.parameters, { name: options.name }),
strict: true,
...(options.description ? { description: options.description } : undefined),
},
{
callback: options.function,
parser: (args) => options.parameters.parse(JSON.parse(args)),
},
);
}
+799
View File
@@ -0,0 +1,799 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { type Agent, type RequestInit } from './_shims/index';
import * as qs from './internal/qs';
import * as Core from './core';
import * as Errors from './error';
import * as Pagination from './pagination';
import { type CursorPageParams, CursorPageResponse, PageResponse } from './pagination';
import * as Uploads from './uploads';
import * as API from './resources/index';
import {
Batch,
BatchCreateParams,
BatchError,
BatchListParams,
BatchRequestCounts,
Batches,
BatchesPage,
} from './resources/batches';
import {
Completion,
CompletionChoice,
CompletionCreateParams,
CompletionCreateParamsNonStreaming,
CompletionCreateParamsStreaming,
CompletionUsage,
Completions,
} from './resources/completions';
import {
CreateEmbeddingResponse,
Embedding,
EmbeddingCreateParams,
EmbeddingModel,
Embeddings,
} from './resources/embeddings';
import {
FileContent,
FileCreateParams,
FileDeleted,
FileListParams,
FileObject,
FileObjectsPage,
FilePurpose,
Files,
} from './resources/files';
import {
Image,
ImageCreateVariationParams,
ImageEditParams,
ImageGenerateParams,
ImageModel,
Images,
ImagesResponse,
} from './resources/images';
import { Model, ModelDeleted, Models, ModelsPage } from './resources/models';
import {
Moderation,
ModerationCreateParams,
ModerationCreateResponse,
ModerationImageURLInput,
ModerationModel,
ModerationMultiModalInput,
ModerationTextInput,
Moderations,
} from './resources/moderations';
import { Audio, AudioModel, AudioResponseFormat } from './resources/audio/audio';
import { Beta } from './resources/beta/beta';
import { Chat } from './resources/chat/chat';
import {
ContainerCreateParams,
ContainerCreateResponse,
ContainerListParams,
ContainerListResponse,
ContainerListResponsesPage,
ContainerRetrieveResponse,
Containers,
} from './resources/containers/containers';
import {
EvalCreateParams,
EvalCreateResponse,
EvalCustomDataSourceConfig,
EvalDeleteResponse,
EvalListParams,
EvalListResponse,
EvalListResponsesPage,
EvalRetrieveResponse,
EvalStoredCompletionsDataSourceConfig,
EvalUpdateParams,
EvalUpdateResponse,
Evals,
} from './resources/evals/evals';
import { FineTuning } from './resources/fine-tuning/fine-tuning';
import { Graders } from './resources/graders/graders';
import { Responses } from './resources/responses/responses';
import {
Upload,
UploadCompleteParams,
UploadCreateParams,
Uploads as UploadsAPIUploads,
} from './resources/uploads/uploads';
import {
AutoFileChunkingStrategyParam,
FileChunkingStrategy,
FileChunkingStrategyParam,
OtherFileChunkingStrategyObject,
StaticFileChunkingStrategy,
StaticFileChunkingStrategyObject,
StaticFileChunkingStrategyObjectParam,
VectorStore,
VectorStoreCreateParams,
VectorStoreDeleted,
VectorStoreListParams,
VectorStoreSearchParams,
VectorStoreSearchResponse,
VectorStoreSearchResponsesPage,
VectorStoreUpdateParams,
VectorStores,
VectorStoresPage,
} from './resources/vector-stores/vector-stores';
import {
ChatCompletion,
ChatCompletionAssistantMessageParam,
ChatCompletionAudio,
ChatCompletionAudioParam,
ChatCompletionChunk,
ChatCompletionContentPart,
ChatCompletionContentPartImage,
ChatCompletionContentPartInputAudio,
ChatCompletionContentPartRefusal,
ChatCompletionContentPartText,
ChatCompletionCreateParams,
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming,
ChatCompletionDeleted,
ChatCompletionDeveloperMessageParam,
ChatCompletionFunctionCallOption,
ChatCompletionFunctionMessageParam,
ChatCompletionListParams,
ChatCompletionMessage,
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionModality,
ChatCompletionNamedToolChoice,
ChatCompletionPredictionContent,
ChatCompletionReasoningEffort,
ChatCompletionRole,
ChatCompletionStoreMessage,
ChatCompletionStreamOptions,
ChatCompletionSystemMessageParam,
ChatCompletionTokenLogprob,
ChatCompletionTool,
ChatCompletionToolChoiceOption,
ChatCompletionToolMessageParam,
ChatCompletionUpdateParams,
ChatCompletionUserMessageParam,
ChatCompletionsPage,
CreateChatCompletionRequestMessage,
} from './resources/chat/completions/completions';
export interface ClientOptions {
/**
* Defaults to process.env['OPENAI_API_KEY'].
*/
apiKey?: string | undefined;
/**
* Defaults to process.env['OPENAI_ORG_ID'].
*/
organization?: string | null | undefined;
/**
* Defaults to process.env['OPENAI_PROJECT_ID'].
*/
project?: string | null | undefined;
/**
* Override the default base URL for the API, e.g., "https://api.example.com/v2/"
*
* Defaults to process.env['OPENAI_BASE_URL'].
*/
baseURL?: string | null | undefined;
/**
* 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 | undefined;
/**
* 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 | undefined;
/**
* Specify a custom `fetch` function implementation.
*
* If not provided, we use `node-fetch` on Node.js and otherwise expect that `fetch` is
* defined globally.
*/
fetch?: Core.Fetch | undefined;
/**
* 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 | undefined;
/**
* 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 | undefined;
/**
* 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 | undefined;
/**
* By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
* Only set this option to `true` if you understand the risks and have appropriate mitigations in place.
*/
dangerouslyAllowBrowser?: boolean | undefined;
}
/**
* API Client for interfacing with the OpenAI API.
*/
export class OpenAI extends Core.APIClient {
apiKey: string;
organization: string | null;
project: string | null;
private _options: ClientOptions;
/**
* API Client for interfacing with the OpenAI API.
*
* @param {string | undefined} [opts.apiKey=process.env['OPENAI_API_KEY'] ?? undefined]
* @param {string | null | undefined} [opts.organization=process.env['OPENAI_ORG_ID'] ?? null]
* @param {string | null | undefined} [opts.project=process.env['OPENAI_PROJECT_ID'] ?? null]
* @param {string} [opts.baseURL=process.env['OPENAI_BASE_URL'] ?? https://api.openai.com/v1] - Override the default base URL for the API.
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
* @param {number} [opts.httpAgent] - An HTTP agent used to manage HTTP(s) connections.
* @param {Core.Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
* @param {Core.Headers} opts.defaultHeaders - Default headers to include with every request to the API.
* @param {Core.DefaultQuery} opts.defaultQuery - Default query parameters to include with every request to the API.
* @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
*/
constructor({
baseURL = Core.readEnv('OPENAI_BASE_URL'),
apiKey = Core.readEnv('OPENAI_API_KEY'),
organization = Core.readEnv('OPENAI_ORG_ID') ?? null,
project = Core.readEnv('OPENAI_PROJECT_ID') ?? null,
...opts
}: ClientOptions = {}) {
if (apiKey === undefined) {
throw new Errors.OpenAIError(
"The OPENAI_API_KEY environment variable is missing or empty; either provide it, or instantiate the OpenAI client with an apiKey option, like new OpenAI({ apiKey: 'My API Key' }).",
);
}
const options: ClientOptions = {
apiKey,
organization,
project,
...opts,
baseURL: baseURL || `https://api.openai.com/v1`,
};
if (!options.dangerouslyAllowBrowser && Core.isRunningInBrowser()) {
throw new Errors.OpenAIError(
"It looks like you're running in a browser-like environment.\n\nThis is disabled by default, as it risks exposing your secret API credentials to attackers.\nIf you understand the risks and have appropriate mitigations in place,\nyou can set the `dangerouslyAllowBrowser` option to `true`, e.g.,\n\nnew OpenAI({ apiKey, dangerouslyAllowBrowser: true });\n\nhttps://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety\n",
);
}
super({
baseURL: options.baseURL!,
timeout: options.timeout ?? 600000 /* 10 minutes */,
httpAgent: options.httpAgent,
maxRetries: options.maxRetries,
fetch: options.fetch,
});
this._options = options;
this.apiKey = apiKey;
this.organization = organization;
this.project = project;
}
completions: API.Completions = new API.Completions(this);
chat: API.Chat = new API.Chat(this);
embeddings: API.Embeddings = new API.Embeddings(this);
files: API.Files = new API.Files(this);
images: API.Images = new API.Images(this);
audio: API.Audio = new API.Audio(this);
moderations: API.Moderations = new API.Moderations(this);
models: API.Models = new API.Models(this);
fineTuning: API.FineTuning = new API.FineTuning(this);
graders: API.Graders = new API.Graders(this);
vectorStores: API.VectorStores = new API.VectorStores(this);
beta: API.Beta = new API.Beta(this);
batches: API.Batches = new API.Batches(this);
uploads: API.Uploads = new API.Uploads(this);
responses: API.Responses = new API.Responses(this);
evals: API.Evals = new API.Evals(this);
containers: API.Containers = new API.Containers(this);
protected override defaultQuery(): Core.DefaultQuery | undefined {
return this._options.defaultQuery;
}
protected override defaultHeaders(opts: Core.FinalRequestOptions): Core.Headers {
return {
...super.defaultHeaders(opts),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
...this._options.defaultHeaders,
};
}
protected override authHeaders(opts: Core.FinalRequestOptions): Core.Headers {
return { Authorization: `Bearer ${this.apiKey}` };
}
protected override stringifyQuery(query: Record<string, unknown>): string {
return qs.stringify(query, { arrayFormat: 'brackets' });
}
static OpenAI = this;
static DEFAULT_TIMEOUT = 600000; // 10 minutes
static OpenAIError = Errors.OpenAIError;
static APIError = Errors.APIError;
static APIConnectionError = Errors.APIConnectionError;
static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
static APIUserAbortError = Errors.APIUserAbortError;
static NotFoundError = Errors.NotFoundError;
static ConflictError = Errors.ConflictError;
static RateLimitError = Errors.RateLimitError;
static BadRequestError = Errors.BadRequestError;
static AuthenticationError = Errors.AuthenticationError;
static InternalServerError = Errors.InternalServerError;
static PermissionDeniedError = Errors.PermissionDeniedError;
static UnprocessableEntityError = Errors.UnprocessableEntityError;
static toFile = Uploads.toFile;
static fileFromPath = Uploads.fileFromPath;
}
OpenAI.Completions = Completions;
OpenAI.Chat = Chat;
OpenAI.ChatCompletionsPage = ChatCompletionsPage;
OpenAI.Embeddings = Embeddings;
OpenAI.Files = Files;
OpenAI.FileObjectsPage = FileObjectsPage;
OpenAI.Images = Images;
OpenAI.Audio = Audio;
OpenAI.Moderations = Moderations;
OpenAI.Models = Models;
OpenAI.ModelsPage = ModelsPage;
OpenAI.FineTuning = FineTuning;
OpenAI.Graders = Graders;
OpenAI.VectorStores = VectorStores;
OpenAI.VectorStoresPage = VectorStoresPage;
OpenAI.VectorStoreSearchResponsesPage = VectorStoreSearchResponsesPage;
OpenAI.Beta = Beta;
OpenAI.Batches = Batches;
OpenAI.BatchesPage = BatchesPage;
OpenAI.Uploads = UploadsAPIUploads;
OpenAI.Responses = Responses;
OpenAI.Evals = Evals;
OpenAI.EvalListResponsesPage = EvalListResponsesPage;
OpenAI.Containers = Containers;
OpenAI.ContainerListResponsesPage = ContainerListResponsesPage;
export declare namespace OpenAI {
export type RequestOptions = Core.RequestOptions;
export import Page = Pagination.Page;
export { type PageResponse as PageResponse };
export import CursorPage = Pagination.CursorPage;
export { type CursorPageParams as CursorPageParams, type CursorPageResponse as CursorPageResponse };
export {
Completions as Completions,
type Completion as Completion,
type CompletionChoice as CompletionChoice,
type CompletionUsage as CompletionUsage,
type CompletionCreateParams as CompletionCreateParams,
type CompletionCreateParamsNonStreaming as CompletionCreateParamsNonStreaming,
type CompletionCreateParamsStreaming as CompletionCreateParamsStreaming,
};
export {
Chat as Chat,
type ChatCompletion as ChatCompletion,
type ChatCompletionAssistantMessageParam as ChatCompletionAssistantMessageParam,
type ChatCompletionAudio as ChatCompletionAudio,
type ChatCompletionAudioParam as ChatCompletionAudioParam,
type ChatCompletionChunk as ChatCompletionChunk,
type ChatCompletionContentPart as ChatCompletionContentPart,
type ChatCompletionContentPartImage as ChatCompletionContentPartImage,
type ChatCompletionContentPartInputAudio as ChatCompletionContentPartInputAudio,
type ChatCompletionContentPartRefusal as ChatCompletionContentPartRefusal,
type ChatCompletionContentPartText as ChatCompletionContentPartText,
type ChatCompletionDeleted as ChatCompletionDeleted,
type ChatCompletionDeveloperMessageParam as ChatCompletionDeveloperMessageParam,
type ChatCompletionFunctionCallOption as ChatCompletionFunctionCallOption,
type ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam,
type ChatCompletionMessage as ChatCompletionMessage,
type ChatCompletionMessageParam as ChatCompletionMessageParam,
type ChatCompletionMessageToolCall as ChatCompletionMessageToolCall,
type ChatCompletionModality as ChatCompletionModality,
type ChatCompletionNamedToolChoice as ChatCompletionNamedToolChoice,
type ChatCompletionPredictionContent as ChatCompletionPredictionContent,
type ChatCompletionRole as ChatCompletionRole,
type ChatCompletionStoreMessage as ChatCompletionStoreMessage,
type ChatCompletionStreamOptions as ChatCompletionStreamOptions,
type ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam,
type ChatCompletionTokenLogprob as ChatCompletionTokenLogprob,
type ChatCompletionTool as ChatCompletionTool,
type ChatCompletionToolChoiceOption as ChatCompletionToolChoiceOption,
type ChatCompletionToolMessageParam as ChatCompletionToolMessageParam,
type ChatCompletionUserMessageParam as ChatCompletionUserMessageParam,
type CreateChatCompletionRequestMessage as CreateChatCompletionRequestMessage,
type ChatCompletionReasoningEffort as ChatCompletionReasoningEffort,
ChatCompletionsPage as ChatCompletionsPage,
type ChatCompletionCreateParams as ChatCompletionCreateParams,
type ChatCompletionCreateParamsNonStreaming as ChatCompletionCreateParamsNonStreaming,
type ChatCompletionCreateParamsStreaming as ChatCompletionCreateParamsStreaming,
type ChatCompletionUpdateParams as ChatCompletionUpdateParams,
type ChatCompletionListParams as ChatCompletionListParams,
};
export {
Embeddings as Embeddings,
type CreateEmbeddingResponse as CreateEmbeddingResponse,
type Embedding as Embedding,
type EmbeddingModel as EmbeddingModel,
type EmbeddingCreateParams as EmbeddingCreateParams,
};
export {
Files as Files,
type FileContent as FileContent,
type FileDeleted as FileDeleted,
type FileObject as FileObject,
type FilePurpose as FilePurpose,
FileObjectsPage as FileObjectsPage,
type FileCreateParams as FileCreateParams,
type FileListParams as FileListParams,
};
export {
Images as Images,
type Image as Image,
type ImageModel as ImageModel,
type ImagesResponse as ImagesResponse,
type ImageCreateVariationParams as ImageCreateVariationParams,
type ImageEditParams as ImageEditParams,
type ImageGenerateParams as ImageGenerateParams,
};
export { Audio as Audio, type AudioModel as AudioModel, type AudioResponseFormat as AudioResponseFormat };
export {
Moderations as Moderations,
type Moderation as Moderation,
type ModerationImageURLInput as ModerationImageURLInput,
type ModerationModel as ModerationModel,
type ModerationMultiModalInput as ModerationMultiModalInput,
type ModerationTextInput as ModerationTextInput,
type ModerationCreateResponse as ModerationCreateResponse,
type ModerationCreateParams as ModerationCreateParams,
};
export {
Models as Models,
type Model as Model,
type ModelDeleted as ModelDeleted,
ModelsPage as ModelsPage,
};
export { FineTuning as FineTuning };
export { Graders as Graders };
export {
VectorStores as VectorStores,
type AutoFileChunkingStrategyParam as AutoFileChunkingStrategyParam,
type FileChunkingStrategy as FileChunkingStrategy,
type FileChunkingStrategyParam as FileChunkingStrategyParam,
type OtherFileChunkingStrategyObject as OtherFileChunkingStrategyObject,
type StaticFileChunkingStrategy as StaticFileChunkingStrategy,
type StaticFileChunkingStrategyObject as StaticFileChunkingStrategyObject,
type StaticFileChunkingStrategyObjectParam as StaticFileChunkingStrategyObjectParam,
type VectorStore as VectorStore,
type VectorStoreDeleted as VectorStoreDeleted,
type VectorStoreSearchResponse as VectorStoreSearchResponse,
VectorStoresPage as VectorStoresPage,
VectorStoreSearchResponsesPage as VectorStoreSearchResponsesPage,
type VectorStoreCreateParams as VectorStoreCreateParams,
type VectorStoreUpdateParams as VectorStoreUpdateParams,
type VectorStoreListParams as VectorStoreListParams,
type VectorStoreSearchParams as VectorStoreSearchParams,
};
export { Beta as Beta };
export {
Batches as Batches,
type Batch as Batch,
type BatchError as BatchError,
type BatchRequestCounts as BatchRequestCounts,
BatchesPage as BatchesPage,
type BatchCreateParams as BatchCreateParams,
type BatchListParams as BatchListParams,
};
export {
UploadsAPIUploads as Uploads,
type Upload as Upload,
type UploadCreateParams as UploadCreateParams,
type UploadCompleteParams as UploadCompleteParams,
};
export { Responses as Responses };
export {
Evals as Evals,
type EvalCustomDataSourceConfig as EvalCustomDataSourceConfig,
type EvalStoredCompletionsDataSourceConfig as EvalStoredCompletionsDataSourceConfig,
type EvalCreateResponse as EvalCreateResponse,
type EvalRetrieveResponse as EvalRetrieveResponse,
type EvalUpdateResponse as EvalUpdateResponse,
type EvalListResponse as EvalListResponse,
type EvalDeleteResponse as EvalDeleteResponse,
EvalListResponsesPage as EvalListResponsesPage,
type EvalCreateParams as EvalCreateParams,
type EvalUpdateParams as EvalUpdateParams,
type EvalListParams as EvalListParams,
};
export {
Containers as Containers,
type ContainerCreateResponse as ContainerCreateResponse,
type ContainerRetrieveResponse as ContainerRetrieveResponse,
type ContainerListResponse as ContainerListResponse,
ContainerListResponsesPage as ContainerListResponsesPage,
type ContainerCreateParams as ContainerCreateParams,
type ContainerListParams as ContainerListParams,
};
export type AllModels = API.AllModels;
export type ChatModel = API.ChatModel;
export type ComparisonFilter = API.ComparisonFilter;
export type CompoundFilter = API.CompoundFilter;
export type ErrorObject = API.ErrorObject;
export type FunctionDefinition = API.FunctionDefinition;
export type FunctionParameters = API.FunctionParameters;
export type Metadata = API.Metadata;
export type Reasoning = API.Reasoning;
export type ReasoningEffort = API.ReasoningEffort;
export type ResponseFormatJSONObject = API.ResponseFormatJSONObject;
export type ResponseFormatJSONSchema = API.ResponseFormatJSONSchema;
export type ResponseFormatText = API.ResponseFormatText;
export type ResponsesModel = API.ResponsesModel;
}
// ---------------------- Azure ----------------------
/** API Client for interfacing with the Azure OpenAI API. */
export interface AzureClientOptions extends ClientOptions {
/**
* Defaults to process.env['OPENAI_API_VERSION'].
*/
apiVersion?: string | undefined;
/**
* Your Azure endpoint, including the resource, e.g. `https://example-resource.azure.openai.com/`
*/
endpoint?: string | undefined;
/**
* A model deployment, if given, sets the base client URL to include `/deployments/{deployment}`.
* Note: this means you won't be able to use non-deployment endpoints. Not supported with Assistants APIs.
*/
deployment?: string | undefined;
/**
* Defaults to process.env['AZURE_OPENAI_API_KEY'].
*/
apiKey?: string | undefined;
/**
* A function that returns an access token for Microsoft Entra (formerly known as Azure Active Directory),
* which will be invoked on every request.
*/
azureADTokenProvider?: (() => Promise<string>) | undefined;
}
/** API Client for interfacing with the Azure OpenAI API. */
export class AzureOpenAI extends OpenAI {
private _azureADTokenProvider: (() => Promise<string>) | undefined;
deploymentName: string | undefined;
apiVersion: string = '';
/**
* API Client for interfacing with the Azure OpenAI API.
*
* @param {string | undefined} [opts.apiVersion=process.env['OPENAI_API_VERSION'] ?? undefined]
* @param {string | undefined} [opts.endpoint=process.env['AZURE_OPENAI_ENDPOINT'] ?? undefined] - Your Azure endpoint, including the resource, e.g. `https://example-resource.azure.openai.com/`
* @param {string | undefined} [opts.apiKey=process.env['AZURE_OPENAI_API_KEY'] ?? undefined]
* @param {string | undefined} opts.deployment - A model deployment, if given, sets the base client URL to include `/deployments/{deployment}`.
* @param {string | null | undefined} [opts.organization=process.env['OPENAI_ORG_ID'] ?? null]
* @param {string} [opts.baseURL=process.env['OPENAI_BASE_URL']] - Sets the base URL for the API, e.g. `https://example-resource.azure.openai.com/openai/`.
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
* @param {number} [opts.httpAgent] - An HTTP agent used to manage HTTP(s) connections.
* @param {Core.Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
* @param {Core.Headers} opts.defaultHeaders - Default headers to include with every request to the API.
* @param {Core.DefaultQuery} opts.defaultQuery - Default query parameters to include with every request to the API.
* @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
*/
constructor({
baseURL = Core.readEnv('OPENAI_BASE_URL'),
apiKey = Core.readEnv('AZURE_OPENAI_API_KEY'),
apiVersion = Core.readEnv('OPENAI_API_VERSION'),
endpoint,
deployment,
azureADTokenProvider,
dangerouslyAllowBrowser,
...opts
}: AzureClientOptions = {}) {
if (!apiVersion) {
throw new Errors.OpenAIError(
"The OPENAI_API_VERSION environment variable is missing or empty; either provide it, or instantiate the AzureOpenAI client with an apiVersion option, like new AzureOpenAI({ apiVersion: 'My API Version' }).",
);
}
if (typeof azureADTokenProvider === 'function') {
dangerouslyAllowBrowser = true;
}
if (!azureADTokenProvider && !apiKey) {
throw new Errors.OpenAIError(
'Missing credentials. Please pass one of `apiKey` and `azureADTokenProvider`, or set the `AZURE_OPENAI_API_KEY` environment variable.',
);
}
if (azureADTokenProvider && apiKey) {
throw new Errors.OpenAIError(
'The `apiKey` and `azureADTokenProvider` arguments are mutually exclusive; only one can be passed at a time.',
);
}
// define a sentinel value to avoid any typing issues
apiKey ??= API_KEY_SENTINEL;
opts.defaultQuery = { ...opts.defaultQuery, 'api-version': apiVersion };
if (!baseURL) {
if (!endpoint) {
endpoint = process.env['AZURE_OPENAI_ENDPOINT'];
}
if (!endpoint) {
throw new Errors.OpenAIError(
'Must provide one of the `baseURL` or `endpoint` arguments, or the `AZURE_OPENAI_ENDPOINT` environment variable',
);
}
baseURL = `${endpoint}/openai`;
} else {
if (endpoint) {
throw new Errors.OpenAIError('baseURL and endpoint are mutually exclusive');
}
}
super({
apiKey,
baseURL,
...opts,
...(dangerouslyAllowBrowser !== undefined ? { dangerouslyAllowBrowser } : {}),
});
this._azureADTokenProvider = azureADTokenProvider;
this.apiVersion = apiVersion;
this.deploymentName = deployment;
}
override buildRequest(
options: Core.FinalRequestOptions<unknown>,
props: { retryCount?: number } = {},
): {
req: RequestInit;
url: string;
timeout: number;
} {
if (_deployments_endpoints.has(options.path) && options.method === 'post' && options.body !== undefined) {
if (!Core.isObj(options.body)) {
throw new Error('Expected request body to be an object');
}
const model = this.deploymentName || options.body['model'] || options.__metadata?.['model'];
if (model !== undefined && !this.baseURL.includes('/deployments')) {
options.path = `/deployments/${model}${options.path}`;
}
}
return super.buildRequest(options, props);
}
async _getAzureADToken(): Promise<string | undefined> {
if (typeof this._azureADTokenProvider === 'function') {
const token = await this._azureADTokenProvider();
if (!token || typeof token !== 'string') {
throw new Errors.OpenAIError(
`Expected 'azureADTokenProvider' argument to return a string but it returned ${token}`,
);
}
return token;
}
return undefined;
}
protected override authHeaders(opts: Core.FinalRequestOptions): Core.Headers {
return {};
}
protected override async prepareOptions(opts: Core.FinalRequestOptions<unknown>): Promise<void> {
/**
* The user should provide a bearer token provider if they want
* to use Azure AD authentication. The user shouldn't set the
* Authorization header manually because the header is overwritten
* with the Azure AD token if a bearer token provider is provided.
*/
if (opts.headers?.['api-key']) {
return super.prepareOptions(opts);
}
const token = await this._getAzureADToken();
opts.headers ??= {};
if (token) {
opts.headers['Authorization'] = `Bearer ${token}`;
} else if (this.apiKey !== API_KEY_SENTINEL) {
opts.headers['api-key'] = this.apiKey;
} else {
throw new Errors.OpenAIError('Unable to handle auth');
}
return super.prepareOptions(opts);
}
}
const _deployments_endpoints = new Set([
'/completions',
'/chat/completions',
'/embeddings',
'/audio/transcriptions',
'/audio/translations',
'/audio/speech',
'/images/generations',
'/images/edits',
]);
const API_KEY_SENTINEL = '<Missing Key>';
// ---------------------- End Azure ----------------------
export { toFile, fileFromPath } from './uploads';
export {
OpenAIError,
APIError,
APIConnectionError,
APIConnectionTimeoutError,
APIUserAbortError,
NotFoundError,
ConflictError,
RateLimitError,
BadRequestError,
AuthenticationError,
InternalServerError,
PermissionDeniedError,
UnprocessableEntityError,
} from './error';
export default OpenAI;
+176
View File
@@ -0,0 +1,176 @@
import { OpenAIError } from '../../error';
export type Bytes = string | ArrayBuffer | Uint8Array | Buffer | null | undefined;
/**
* A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
* reading lines from text.
*
* https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
*/
export class LineDecoder {
// prettier-ignore
static NEWLINE_CHARS = new Set(['\n', '\r']);
static NEWLINE_REGEXP = /\r\n|[\n\r]/g;
buffer: Uint8Array;
#carriageReturnIndex: number | null;
textDecoder: any; // TextDecoder found in browsers; not typed to avoid pulling in either "dom" or "node" types.
constructor() {
this.buffer = new Uint8Array();
this.#carriageReturnIndex = null;
}
decode(chunk: Bytes): string[] {
if (chunk == null) {
return [];
}
const binaryChunk =
chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
: typeof chunk === 'string' ? new TextEncoder().encode(chunk)
: chunk;
let newData = new Uint8Array(this.buffer.length + binaryChunk.length);
newData.set(this.buffer);
newData.set(binaryChunk, this.buffer.length);
this.buffer = newData;
const lines: string[] = [];
let patternIndex;
while ((patternIndex = findNewlineIndex(this.buffer, this.#carriageReturnIndex)) != null) {
if (patternIndex.carriage && this.#carriageReturnIndex == null) {
// skip until we either get a corresponding `\n`, a new `\r` or nothing
this.#carriageReturnIndex = patternIndex.index;
continue;
}
// we got double \r or \rtext\n
if (
this.#carriageReturnIndex != null &&
(patternIndex.index !== this.#carriageReturnIndex + 1 || patternIndex.carriage)
) {
lines.push(this.decodeText(this.buffer.slice(0, this.#carriageReturnIndex - 1)));
this.buffer = this.buffer.slice(this.#carriageReturnIndex);
this.#carriageReturnIndex = null;
continue;
}
const endIndex =
this.#carriageReturnIndex !== null ? patternIndex.preceding - 1 : patternIndex.preceding;
const line = this.decodeText(this.buffer.slice(0, endIndex));
lines.push(line);
this.buffer = this.buffer.slice(patternIndex.index);
this.#carriageReturnIndex = null;
}
return lines;
}
decodeText(bytes: Bytes): string {
if (bytes == null) return '';
if (typeof bytes === 'string') return bytes;
// Node:
if (typeof Buffer !== 'undefined') {
if (bytes instanceof Buffer) {
return bytes.toString();
}
if (bytes instanceof Uint8Array) {
return Buffer.from(bytes).toString();
}
throw new OpenAIError(
`Unexpected: received non-Uint8Array (${bytes.constructor.name}) stream chunk in an environment with a global "Buffer" defined, which this library assumes to be Node. Please report this error.`,
);
}
// Browser
if (typeof TextDecoder !== 'undefined') {
if (bytes instanceof Uint8Array || bytes instanceof ArrayBuffer) {
this.textDecoder ??= new TextDecoder('utf8');
return this.textDecoder.decode(bytes);
}
throw new OpenAIError(
`Unexpected: received non-Uint8Array/ArrayBuffer (${
(bytes as any).constructor.name
}) in a web platform. Please report this error.`,
);
}
throw new OpenAIError(
`Unexpected: neither Buffer nor TextDecoder are available as globals. Please report this error.`,
);
}
flush(): string[] {
if (!this.buffer.length) {
return [];
}
return this.decode('\n');
}
}
/**
* This function searches the buffer for the end patterns, (\r or \n)
* and returns an object with the index preceding the matched newline and the
* index after the newline char. `null` is returned if no new line is found.
*
* ```ts
* findNewLineIndex('abc\ndef') -> { preceding: 2, index: 3 }
* ```
*/
function findNewlineIndex(
buffer: Uint8Array,
startIndex: number | null,
): { preceding: number; index: number; carriage: boolean } | null {
const newline = 0x0a; // \n
const carriage = 0x0d; // \r
for (let i = startIndex ?? 0; i < buffer.length; i++) {
if (buffer[i] === newline) {
return { preceding: i, index: i + 1, carriage: false };
}
if (buffer[i] === carriage) {
return { preceding: i, index: i + 1, carriage: true };
}
}
return null;
}
export function findDoubleNewlineIndex(buffer: Uint8Array): number {
// This function searches the buffer for the end patterns (\r\r, \n\n, \r\n\r\n)
// and returns the index right after the first occurrence of any pattern,
// or -1 if none of the patterns are found.
const newline = 0x0a; // \n
const carriage = 0x0d; // \r
for (let i = 0; i < buffer.length - 1; i++) {
if (buffer[i] === newline && buffer[i + 1] === newline) {
// \n\n
return i + 2;
}
if (buffer[i] === carriage && buffer[i + 1] === carriage) {
// \r\r
return i + 2;
}
if (
buffer[i] === carriage &&
buffer[i + 1] === newline &&
i + 3 < buffer.length &&
buffer[i + 2] === carriage &&
buffer[i + 3] === newline
) {
// \r\n\r\n
return i + 4;
}
}
return -1;
}
+13
View File
@@ -0,0 +1,13 @@
BSD 3-Clause License
Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+3
View File
@@ -0,0 +1,3 @@
# qs
This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library.
+9
View File
@@ -0,0 +1,9 @@
import type { Format } from './types';
export const default_format: Format = 'RFC3986';
export const formatters: Record<Format, (str: PropertyKey) => string> = {
RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'),
RFC3986: (v: PropertyKey) => String(v),
};
export const RFC1738 = 'RFC1738';
export const RFC3986 = 'RFC3986';
+13
View File
@@ -0,0 +1,13 @@
import { default_format, formatters, RFC1738, RFC3986 } from './formats';
const formats = {
formatters,
RFC1738,
RFC3986,
default: default_format,
};
export { stringify } from './stringify';
export { formats };
export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types';
+388
View File
@@ -0,0 +1,388 @@
import { encode, is_buffer, maybe_map } from './utils';
import { default_format, formatters } from './formats';
import type { NonNullableProperties, StringifyOptions } from './types';
const has = Object.prototype.hasOwnProperty;
const array_prefix_generators = {
brackets(prefix: PropertyKey) {
return String(prefix) + '[]';
},
comma: 'comma',
indices(prefix: PropertyKey, key: string) {
return String(prefix) + '[' + key + ']';
},
repeat(prefix: PropertyKey) {
return String(prefix);
},
};
const is_array = Array.isArray;
const push = Array.prototype.push;
const push_to_array = function (arr: any[], value_or_array: any) {
push.apply(arr, is_array(value_or_array) ? value_or_array : [value_or_array]);
};
const to_ISO = Date.prototype.toISOString;
const defaults = {
addQueryPrefix: false,
allowDots: false,
allowEmptyArrays: false,
arrayFormat: 'indices',
charset: 'utf-8',
charsetSentinel: false,
delimiter: '&',
encode: true,
encodeDotInKeys: false,
encoder: encode,
encodeValuesOnly: false,
format: default_format,
formatter: formatters[default_format],
/** @deprecated */
indices: false,
serializeDate(date) {
return to_ISO.call(date);
},
skipNulls: false,
strictNullHandling: false,
} as NonNullableProperties<StringifyOptions & { formatter: (typeof formatters)['RFC1738'] }>;
function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint {
return (
typeof v === 'string' ||
typeof v === 'number' ||
typeof v === 'boolean' ||
typeof v === 'symbol' ||
typeof v === 'bigint'
);
}
const sentinel = {};
function inner_stringify(
object: any,
prefix: PropertyKey,
generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string),
commaRoundTrip: boolean,
allowEmptyArrays: boolean,
strictNullHandling: boolean,
skipNulls: boolean,
encodeDotInKeys: boolean,
encoder: StringifyOptions['encoder'],
filter: StringifyOptions['filter'],
sort: StringifyOptions['sort'],
allowDots: StringifyOptions['allowDots'],
serializeDate: StringifyOptions['serializeDate'],
format: StringifyOptions['format'],
formatter: StringifyOptions['formatter'],
encodeValuesOnly: boolean,
charset: StringifyOptions['charset'],
sideChannel: WeakMap<any, any>,
) {
let obj = object;
let tmp_sc = sideChannel;
let step = 0;
let find_flag = false;
while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) {
// Where object last appeared in the ref tree
const pos = tmp_sc.get(object);
step += 1;
if (typeof pos !== 'undefined') {
if (pos === step) {
throw new RangeError('Cyclic object value');
} else {
find_flag = true; // Break while
}
}
if (typeof tmp_sc.get(sentinel) === 'undefined') {
step = 0;
}
}
if (typeof filter === 'function') {
obj = filter(prefix, obj);
} else if (obj instanceof Date) {
obj = serializeDate?.(obj);
} else if (generateArrayPrefix === 'comma' && is_array(obj)) {
obj = maybe_map(obj, function (value) {
if (value instanceof Date) {
return serializeDate?.(value);
}
return value;
});
}
if (obj === null) {
if (strictNullHandling) {
return encoder && !encodeValuesOnly ?
// @ts-expect-error
encoder(prefix, defaults.encoder, charset, 'key', format)
: prefix;
}
obj = '';
}
if (is_non_nullish_primitive(obj) || is_buffer(obj)) {
if (encoder) {
const key_value =
encodeValuesOnly ? prefix
// @ts-expect-error
: encoder(prefix, defaults.encoder, charset, 'key', format);
return [
formatter?.(key_value) +
'=' +
// @ts-expect-error
formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)),
];
}
return [formatter?.(prefix) + '=' + formatter?.(String(obj))];
}
const values: string[] = [];
if (typeof obj === 'undefined') {
return values;
}
let obj_keys;
if (generateArrayPrefix === 'comma' && is_array(obj)) {
// we need to join elements in
if (encodeValuesOnly && encoder) {
// @ts-expect-error values only
obj = maybe_map(obj, encoder);
}
obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
} else if (is_array(filter)) {
obj_keys = filter;
} else {
const keys = Object.keys(obj);
obj_keys = sort ? keys.sort(sort) : keys;
}
const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix);
const adjusted_prefix =
commaRoundTrip && is_array(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix;
if (allowEmptyArrays && is_array(obj) && obj.length === 0) {
return adjusted_prefix + '[]';
}
for (let j = 0; j < obj_keys.length; ++j) {
const key = obj_keys[j];
const value =
// @ts-ignore
typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any];
if (skipNulls && value === null) {
continue;
}
// @ts-ignore
const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key;
const key_prefix =
is_array(obj) ?
typeof generateArrayPrefix === 'function' ?
generateArrayPrefix(adjusted_prefix, encoded_key)
: adjusted_prefix
: adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']');
sideChannel.set(object, step);
const valueSideChannel = new WeakMap();
valueSideChannel.set(sentinel, sideChannel);
push_to_array(
values,
inner_stringify(
value,
key_prefix,
generateArrayPrefix,
commaRoundTrip,
allowEmptyArrays,
strictNullHandling,
skipNulls,
encodeDotInKeys,
// @ts-ignore
generateArrayPrefix === 'comma' && encodeValuesOnly && is_array(obj) ? null : encoder,
filter,
sort,
allowDots,
serializeDate,
format,
formatter,
encodeValuesOnly,
charset,
valueSideChannel,
),
);
}
return values;
}
function normalize_stringify_options(
opts: StringifyOptions = defaults,
): NonNullableProperties<Omit<StringifyOptions, 'indices'>> & { indices?: boolean } {
if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
}
if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') {
throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided');
}
if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
throw new TypeError('Encoder has to be a function.');
}
const charset = opts.charset || defaults.charset;
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
}
let format = default_format;
if (typeof opts.format !== 'undefined') {
if (!has.call(formatters, opts.format)) {
throw new TypeError('Unknown format option provided.');
}
format = opts.format;
}
const formatter = formatters[format];
let filter = defaults.filter;
if (typeof opts.filter === 'function' || is_array(opts.filter)) {
filter = opts.filter;
}
let arrayFormat: StringifyOptions['arrayFormat'];
if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) {
arrayFormat = opts.arrayFormat;
} else if ('indices' in opts) {
arrayFormat = opts.indices ? 'indices' : 'repeat';
} else {
arrayFormat = defaults.arrayFormat;
}
if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
}
const allowDots =
typeof opts.allowDots === 'undefined' ?
!!opts.encodeDotInKeys === true ?
true
: defaults.allowDots
: !!opts.allowDots;
return {
addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
// @ts-ignore
allowDots: allowDots,
allowEmptyArrays:
typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
arrayFormat: arrayFormat,
charset: charset,
charsetSentinel:
typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
commaRoundTrip: !!opts.commaRoundTrip,
delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
encodeDotInKeys:
typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys,
encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
encodeValuesOnly:
typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
filter: filter,
format: format,
formatter: formatter,
serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
// @ts-ignore
sort: typeof opts.sort === 'function' ? opts.sort : null,
strictNullHandling:
typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
};
}
export function stringify(object: any, opts: StringifyOptions = {}) {
let obj = object;
const options = normalize_stringify_options(opts);
let obj_keys: PropertyKey[] | undefined;
let filter;
if (typeof options.filter === 'function') {
filter = options.filter;
obj = filter('', obj);
} else if (is_array(options.filter)) {
filter = options.filter;
obj_keys = filter;
}
const keys: string[] = [];
if (typeof obj !== 'object' || obj === null) {
return '';
}
const generateArrayPrefix = array_prefix_generators[options.arrayFormat];
const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip;
if (!obj_keys) {
obj_keys = Object.keys(obj);
}
if (options.sort) {
obj_keys.sort(options.sort);
}
const sideChannel = new WeakMap();
for (let i = 0; i < obj_keys.length; ++i) {
const key = obj_keys[i]!;
if (options.skipNulls && obj[key] === null) {
continue;
}
push_to_array(
keys,
inner_stringify(
obj[key],
key,
// @ts-expect-error
generateArrayPrefix,
commaRoundTrip,
options.allowEmptyArrays,
options.strictNullHandling,
options.skipNulls,
options.encodeDotInKeys,
options.encode ? options.encoder : null,
options.filter,
options.sort,
options.allowDots,
options.serializeDate,
options.format,
options.formatter,
options.encodeValuesOnly,
options.charset,
sideChannel,
),
);
}
const joined = keys.join(options.delimiter);
let prefix = options.addQueryPrefix === true ? '?' : '';
if (options.charsetSentinel) {
if (options.charset === 'iso-8859-1') {
// encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
prefix += 'utf8=%26%2310003%3B&';
} else {
// encodeURIComponent('✓')
prefix += 'utf8=%E2%9C%93&';
}
}
return joined.length > 0 ? prefix + joined : '';
}
+71
View File
@@ -0,0 +1,71 @@
export type Format = 'RFC1738' | 'RFC3986';
export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string;
export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string;
export type BooleanOptional = boolean | undefined;
export type StringifyBaseOptions = {
delimiter?: string;
allowDots?: boolean;
encodeDotInKeys?: boolean;
strictNullHandling?: boolean;
skipNulls?: boolean;
encode?: boolean;
encoder?: (
str: any,
defaultEncoder: DefaultEncoder,
charset: string,
type: 'key' | 'value',
format?: Format,
) => string;
filter?: Array<PropertyKey> | ((prefix: PropertyKey, value: any) => any);
arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma';
indices?: boolean;
sort?: ((a: PropertyKey, b: PropertyKey) => number) | null;
serializeDate?: (d: Date) => string;
format?: 'RFC1738' | 'RFC3986';
formatter?: (str: PropertyKey) => string;
encodeValuesOnly?: boolean;
addQueryPrefix?: boolean;
charset?: 'utf-8' | 'iso-8859-1';
charsetSentinel?: boolean;
allowEmptyArrays?: boolean;
commaRoundTrip?: boolean;
};
export type StringifyOptions = StringifyBaseOptions;
export type ParseBaseOptions = {
comma?: boolean;
delimiter?: string | RegExp;
depth?: number | false;
decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any;
arrayLimit?: number;
parseArrays?: boolean;
plainObjects?: boolean;
allowPrototypes?: boolean;
allowSparse?: boolean;
parameterLimit?: number;
strictDepth?: boolean;
strictNullHandling?: boolean;
ignoreQueryPrefix?: boolean;
charset?: 'utf-8' | 'iso-8859-1';
charsetSentinel?: boolean;
interpretNumericEntities?: boolean;
allowEmptyArrays?: boolean;
duplicates?: 'combine' | 'first' | 'last';
allowDots?: boolean;
decodeDotInKeys?: boolean;
};
export type ParseOptions = ParseBaseOptions;
export type ParsedQs = {
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
};
// Type to remove null or undefined union from each property
export type NonNullableProperties<T> = {
[K in keyof T]-?: Exclude<T[K], undefined | null>;
};
+265
View File
@@ -0,0 +1,265 @@
import { RFC1738 } from './formats';
import type { DefaultEncoder, Format } from './types';
const has = Object.prototype.hasOwnProperty;
const is_array = Array.isArray;
const hex_table = (() => {
const array = [];
for (let i = 0; i < 256; ++i) {
array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase());
}
return array;
})();
function compact_queue<T extends Record<string, any>>(queue: Array<{ obj: T; prop: string }>) {
while (queue.length > 1) {
const item = queue.pop();
if (!item) continue;
const obj = item.obj[item.prop];
if (is_array(obj)) {
const compacted: unknown[] = [];
for (let j = 0; j < obj.length; ++j) {
if (typeof obj[j] !== 'undefined') {
compacted.push(obj[j]);
}
}
// @ts-ignore
item.obj[item.prop] = compacted;
}
}
}
function array_to_object(source: any[], options: { plainObjects: boolean }) {
const obj = options && options.plainObjects ? Object.create(null) : {};
for (let i = 0; i < source.length; ++i) {
if (typeof source[i] !== 'undefined') {
obj[i] = source[i];
}
}
return obj;
}
export function merge(
target: any,
source: any,
options: { plainObjects?: boolean; allowPrototypes?: boolean } = {},
) {
if (!source) {
return target;
}
if (typeof source !== 'object') {
if (is_array(target)) {
target.push(source);
} else if (target && typeof target === 'object') {
if (
(options && (options.plainObjects || options.allowPrototypes)) ||
!has.call(Object.prototype, source)
) {
target[source] = true;
}
} else {
return [target, source];
}
return target;
}
if (!target || typeof target !== 'object') {
return [target].concat(source);
}
let mergeTarget = target;
if (is_array(target) && !is_array(source)) {
// @ts-ignore
mergeTarget = array_to_object(target, options);
}
if (is_array(target) && is_array(source)) {
source.forEach(function (item, i) {
if (has.call(target, i)) {
const targetItem = target[i];
if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
target[i] = merge(targetItem, item, options);
} else {
target.push(item);
}
} else {
target[i] = item;
}
});
return target;
}
return Object.keys(source).reduce(function (acc, key) {
const value = source[key];
if (has.call(acc, key)) {
acc[key] = merge(acc[key], value, options);
} else {
acc[key] = value;
}
return acc;
}, mergeTarget);
}
export function assign_single_source(target: any, source: any) {
return Object.keys(source).reduce(function (acc, key) {
acc[key] = source[key];
return acc;
}, target);
}
export function decode(str: string, _: any, charset: string) {
const strWithoutPlus = str.replace(/\+/g, ' ');
if (charset === 'iso-8859-1') {
// unescape never throws, no try...catch needed:
return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape);
}
// utf-8
try {
return decodeURIComponent(strWithoutPlus);
} catch (e) {
return strWithoutPlus;
}
}
const limit = 1024;
export const encode: (
str: any,
defaultEncoder: DefaultEncoder,
charset: string,
type: 'key' | 'value',
format: Format,
) => string = (str, _defaultEncoder, charset, _kind, format: Format) => {
// This code was originally written by Brian White for the io.js core querystring library.
// It has been adapted here for stricter adherence to RFC 3986
if (str.length === 0) {
return str;
}
let string = str;
if (typeof str === 'symbol') {
string = Symbol.prototype.toString.call(str);
} else if (typeof str !== 'string') {
string = String(str);
}
if (charset === 'iso-8859-1') {
return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) {
return '%26%23' + parseInt($0.slice(2), 16) + '%3B';
});
}
let out = '';
for (let j = 0; j < string.length; j += limit) {
const segment = string.length >= limit ? string.slice(j, j + limit) : string;
const arr = [];
for (let i = 0; i < segment.length; ++i) {
let c = segment.charCodeAt(i);
if (
c === 0x2d || // -
c === 0x2e || // .
c === 0x5f || // _
c === 0x7e || // ~
(c >= 0x30 && c <= 0x39) || // 0-9
(c >= 0x41 && c <= 0x5a) || // a-z
(c >= 0x61 && c <= 0x7a) || // A-Z
(format === RFC1738 && (c === 0x28 || c === 0x29)) // ( )
) {
arr[arr.length] = segment.charAt(i);
continue;
}
if (c < 0x80) {
arr[arr.length] = hex_table[c];
continue;
}
if (c < 0x800) {
arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)];
continue;
}
if (c < 0xd800 || c >= 0xe000) {
arr[arr.length] =
hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)];
continue;
}
i += 1;
c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff));
arr[arr.length] =
hex_table[0xf0 | (c >> 18)]! +
hex_table[0x80 | ((c >> 12) & 0x3f)] +
hex_table[0x80 | ((c >> 6) & 0x3f)] +
hex_table[0x80 | (c & 0x3f)];
}
out += arr.join('');
}
return out;
};
export function compact(value: any) {
const queue = [{ obj: { o: value }, prop: 'o' }];
const refs = [];
for (let i = 0; i < queue.length; ++i) {
const item = queue[i];
// @ts-ignore
const obj = item.obj[item.prop];
const keys = Object.keys(obj);
for (let j = 0; j < keys.length; ++j) {
const key = keys[j]!;
const val = obj[key];
if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
queue.push({ obj: obj, prop: key });
refs.push(val);
}
}
}
compact_queue(queue);
return value;
}
export function is_regexp(obj: any) {
return Object.prototype.toString.call(obj) === '[object RegExp]';
}
export function is_buffer(obj: any) {
if (!obj || typeof obj !== 'object') {
return false;
}
return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
}
export function combine(a: any, b: any) {
return [].concat(a, b);
}
export function maybe_map<T>(val: T[], fn: (v: T) => T) {
if (is_array(val)) {
const mapped = [];
for (let i = 0; i < val.length; i += 1) {
mapped.push(fn(val[i]!));
}
return mapped;
}
return fn(val);
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Most browsers don't yet have async iterable support for ReadableStream,
* and Node has a very different way of reading bytes from its "ReadableStream".
*
* This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
*/
export function ReadableStreamToAsyncIterable<T>(stream: any): AsyncIterableIterator<T> {
if (stream[Symbol.asyncIterator]) return stream;
const reader = stream.getReader();
return {
async next() {
try {
const result = await reader.read();
if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
return result;
} catch (e) {
reader.releaseLock(); // release lock when stream becomes errored
throw e;
}
},
async return() {
const cancelPromise = reader.cancel();
reader.releaseLock();
await cancelPromise;
return { done: true, value: undefined };
},
[Symbol.asyncIterator]() {
return this;
},
};
}
+4
View File
@@ -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.
+503
View File
@@ -0,0 +1,503 @@
import * as Core from '../core';
import { type CompletionUsage } from '../resources/completions';
import {
type ChatCompletion,
type ChatCompletionMessage,
type ChatCompletionMessageParam,
type ChatCompletionCreateParams,
type ChatCompletionTool,
} from '../resources/chat/completions';
import { OpenAIError } from '../error';
import {
type RunnableFunction,
isRunnableFunctionWithParse,
type BaseFunctionsArgs,
RunnableToolFunction,
} from './RunnableFunction';
import { ChatCompletionFunctionRunnerParams, ChatCompletionToolRunnerParams } from './ChatCompletionRunner';
import {
ChatCompletionStreamingFunctionRunnerParams,
ChatCompletionStreamingToolRunnerParams,
} from './ChatCompletionStreamingRunner';
import { isAssistantMessage, isFunctionMessage, isToolMessage } from './chatCompletionUtils';
import { BaseEvents, EventStream } from './EventStream';
import { ParsedChatCompletion } from '../resources/beta/chat/completions';
import OpenAI from '../index';
import { isAutoParsableTool, parseChatCompletion } from '../lib/parser';
const DEFAULT_MAX_CHAT_COMPLETIONS = 10;
export interface RunnerOptions extends Core.RequestOptions {
/** How many requests to make before canceling. Default 10. */
maxChatCompletions?: number;
}
export class AbstractChatCompletionRunner<
EventTypes extends AbstractChatCompletionRunnerEvents,
ParsedT,
> extends EventStream<EventTypes> {
protected _chatCompletions: ParsedChatCompletion<ParsedT>[] = [];
messages: ChatCompletionMessageParam[] = [];
protected _addChatCompletion(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
chatCompletion: ParsedChatCompletion<ParsedT>,
): ParsedChatCompletion<ParsedT> {
this._chatCompletions.push(chatCompletion);
this._emit('chatCompletion', chatCompletion);
const message = chatCompletion.choices[0]?.message;
if (message) this._addMessage(message as ChatCompletionMessageParam);
return chatCompletion;
}
protected _addMessage(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
message: ChatCompletionMessageParam,
emit = true,
) {
if (!('content' in message)) message.content = null;
this.messages.push(message);
if (emit) {
this._emit('message', message);
if ((isFunctionMessage(message) || isToolMessage(message)) && message.content) {
// Note, this assumes that {role: 'tool', content: …} is always the result of a call of tool of type=function.
this._emit('functionCallResult', message.content as string);
} else if (isAssistantMessage(message) && message.function_call) {
this._emit('functionCall', message.function_call);
} else if (isAssistantMessage(message) && message.tool_calls) {
for (const tool_call of message.tool_calls) {
if (tool_call.type === 'function') {
this._emit('functionCall', tool_call.function);
}
}
}
}
}
/**
* @returns a promise that resolves with the final ChatCompletion, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletion.
*/
async finalChatCompletion(): Promise<ParsedChatCompletion<ParsedT>> {
await this.done();
const completion = this._chatCompletions[this._chatCompletions.length - 1];
if (!completion) throw new OpenAIError('stream ended without producing a ChatCompletion');
return completion;
}
#getFinalContent(): string | null {
return this.#getFinalMessage().content ?? null;
}
/**
* @returns a promise that resolves with the content of the final ChatCompletionMessage, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalContent(): Promise<string | null> {
await this.done();
return this.#getFinalContent();
}
#getFinalMessage(): ChatCompletionMessage {
let i = this.messages.length;
while (i-- > 0) {
const message = this.messages[i];
if (isAssistantMessage(message)) {
const { function_call, ...rest } = message;
// TODO: support audio here
const ret: Omit<ChatCompletionMessage, 'audio'> = {
...rest,
content: (message as ChatCompletionMessage).content ?? null,
refusal: (message as ChatCompletionMessage).refusal ?? null,
};
if (function_call) {
ret.function_call = function_call;
}
return ret;
}
}
throw new OpenAIError('stream ended without producing a ChatCompletionMessage with role=assistant');
}
/**
* @returns a promise that resolves with the the final assistant ChatCompletionMessage response,
* or rejects if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalMessage(): Promise<ChatCompletionMessage> {
await this.done();
return this.#getFinalMessage();
}
#getFinalFunctionCall(): ChatCompletionMessage.FunctionCall | undefined {
for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[i];
if (isAssistantMessage(message) && message?.function_call) {
return message.function_call;
}
if (isAssistantMessage(message) && message?.tool_calls?.length) {
return message.tool_calls.at(-1)?.function;
}
}
return;
}
/**
* @returns a promise that resolves with the content of the final FunctionCall, or rejects
* if an error occurred or the stream ended prematurely without producing a ChatCompletionMessage.
*/
async finalFunctionCall(): Promise<ChatCompletionMessage.FunctionCall | undefined> {
await this.done();
return this.#getFinalFunctionCall();
}
#getFinalFunctionCallResult(): string | undefined {
for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[i];
if (isFunctionMessage(message) && message.content != null) {
return message.content;
}
if (
isToolMessage(message) &&
message.content != null &&
typeof message.content === 'string' &&
this.messages.some(
(x) =>
x.role === 'assistant' &&
x.tool_calls?.some((y) => y.type === 'function' && y.id === message.tool_call_id),
)
) {
return message.content;
}
}
return;
}
async finalFunctionCallResult(): Promise<string | undefined> {
await this.done();
return this.#getFinalFunctionCallResult();
}
#calculateTotalUsage(): CompletionUsage {
const total: CompletionUsage = {
completion_tokens: 0,
prompt_tokens: 0,
total_tokens: 0,
};
for (const { usage } of this._chatCompletions) {
if (usage) {
total.completion_tokens += usage.completion_tokens;
total.prompt_tokens += usage.prompt_tokens;
total.total_tokens += usage.total_tokens;
}
}
return total;
}
async totalUsage(): Promise<CompletionUsage> {
await this.done();
return this.#calculateTotalUsage();
}
allChatCompletions(): ChatCompletion[] {
return [...this._chatCompletions];
}
protected override _emitFinal(
this: AbstractChatCompletionRunner<AbstractChatCompletionRunnerEvents, ParsedT>,
) {
const completion = this._chatCompletions[this._chatCompletions.length - 1];
if (completion) this._emit('finalChatCompletion', completion);
const finalMessage = this.#getFinalMessage();
if (finalMessage) this._emit('finalMessage', finalMessage);
const finalContent = this.#getFinalContent();
if (finalContent) this._emit('finalContent', finalContent);
const finalFunctionCall = this.#getFinalFunctionCall();
if (finalFunctionCall) this._emit('finalFunctionCall', finalFunctionCall);
const finalFunctionCallResult = this.#getFinalFunctionCallResult();
if (finalFunctionCallResult != null) this._emit('finalFunctionCallResult', finalFunctionCallResult);
if (this._chatCompletions.some((c) => c.usage)) {
this._emit('totalUsage', this.#calculateTotalUsage());
}
}
#validateParams(params: ChatCompletionCreateParams): void {
if (params.n != null && params.n > 1) {
throw new OpenAIError(
'ChatCompletion convenience helpers only support n=1 at this time. To use n>1, please use chat.completions.create() directly.',
);
}
}
protected async _createChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: Core.RequestOptions,
): Promise<ParsedChatCompletion<ParsedT>> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#validateParams(params);
const chatCompletion = await client.chat.completions.create(
{ ...params, stream: false },
{ ...options, signal: this.controller.signal },
);
this._connected();
return this._addChatCompletion(parseChatCompletion(chatCompletion, params));
}
protected async _runChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: Core.RequestOptions,
): Promise<ChatCompletion> {
for (const message of params.messages) {
this._addMessage(message, false);
}
return await this._createChatCompletion(client, params, options);
}
protected async _runFunctions<FunctionsArgs extends BaseFunctionsArgs>(
client: OpenAI,
params:
| ChatCompletionFunctionRunnerParams<FunctionsArgs>
| ChatCompletionStreamingFunctionRunnerParams<FunctionsArgs>,
options?: RunnerOptions,
) {
const role = 'function' as const;
const { function_call = 'auto', stream, ...restParams } = params;
const singleFunctionToCall = typeof function_call !== 'string' && function_call?.name;
const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
const functionsByName: Record<string, RunnableFunction<any>> = {};
for (const f of params.functions) {
functionsByName[f.name || f.function.name] = f;
}
const functions: ChatCompletionCreateParams.Function[] = params.functions.map(
(f): ChatCompletionCreateParams.Function => ({
name: f.name || f.function.name,
parameters: f.parameters as Record<string, unknown>,
description: f.description,
}),
);
for (const message of params.messages) {
this._addMessage(message, false);
}
for (let i = 0; i < maxChatCompletions; ++i) {
const chatCompletion: ChatCompletion = await this._createChatCompletion(
client,
{
...restParams,
function_call,
functions,
messages: [...this.messages],
},
options,
);
const message = chatCompletion.choices[0]?.message;
if (!message) {
throw new OpenAIError(`missing message in ChatCompletion response`);
}
if (!message.function_call) return;
const { name, arguments: args } = message.function_call;
const fn = functionsByName[name];
if (!fn) {
const content = `Invalid function_call: ${JSON.stringify(name)}. Available options are: ${functions
.map((f) => JSON.stringify(f.name))
.join(', ')}. Please try again`;
this._addMessage({ role, name, content });
continue;
} else if (singleFunctionToCall && singleFunctionToCall !== name) {
const content = `Invalid function_call: ${JSON.stringify(name)}. ${JSON.stringify(
singleFunctionToCall,
)} requested. Please try again`;
this._addMessage({ role, name, content });
continue;
}
let parsed;
try {
parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args;
} catch (error) {
this._addMessage({
role,
name,
content: error instanceof Error ? error.message : String(error),
});
continue;
}
// @ts-expect-error it can't rule out `never` type.
const rawContent = await fn.function(parsed, this);
const content = this.#stringifyFunctionCallResult(rawContent);
this._addMessage({ role, name, content });
if (singleFunctionToCall) return;
}
}
protected async _runTools<FunctionsArgs extends BaseFunctionsArgs>(
client: OpenAI,
params:
| ChatCompletionToolRunnerParams<FunctionsArgs>
| ChatCompletionStreamingToolRunnerParams<FunctionsArgs>,
options?: RunnerOptions,
) {
const role = 'tool' as const;
const { tool_choice = 'auto', stream, ...restParams } = params;
const singleFunctionToCall = typeof tool_choice !== 'string' && tool_choice?.function?.name;
const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {};
// TODO(someday): clean this logic up
const inputTools = params.tools.map((tool): RunnableToolFunction<any> => {
if (isAutoParsableTool(tool)) {
if (!tool.$callback) {
throw new OpenAIError('Tool given to `.runTools()` that does not have an associated function');
}
return {
type: 'function',
function: {
function: tool.$callback,
name: tool.function.name,
description: tool.function.description || '',
parameters: tool.function.parameters as any,
parse: tool.$parseRaw,
strict: true,
},
};
}
return tool as any as RunnableToolFunction<any>;
});
const functionsByName: Record<string, RunnableFunction<any>> = {};
for (const f of inputTools) {
if (f.type === 'function') {
functionsByName[f.function.name || f.function.function.name] = f.function;
}
}
const tools: ChatCompletionTool[] =
'tools' in params ?
inputTools.map((t) =>
t.type === 'function' ?
{
type: 'function',
function: {
name: t.function.name || t.function.function.name,
parameters: t.function.parameters as Record<string, unknown>,
description: t.function.description,
strict: t.function.strict,
},
}
: (t as unknown as ChatCompletionTool),
)
: (undefined as any);
for (const message of params.messages) {
this._addMessage(message, false);
}
for (let i = 0; i < maxChatCompletions; ++i) {
const chatCompletion: ChatCompletion = await this._createChatCompletion(
client,
{
...restParams,
tool_choice,
tools,
messages: [...this.messages],
},
options,
);
const message = chatCompletion.choices[0]?.message;
if (!message) {
throw new OpenAIError(`missing message in ChatCompletion response`);
}
if (!message.tool_calls?.length) {
return;
}
for (const tool_call of message.tool_calls) {
if (tool_call.type !== 'function') continue;
const tool_call_id = tool_call.id;
const { name, arguments: args } = tool_call.function;
const fn = functionsByName[name];
if (!fn) {
const content = `Invalid tool_call: ${JSON.stringify(name)}. Available options are: ${Object.keys(
functionsByName,
)
.map((name) => JSON.stringify(name))
.join(', ')}. Please try again`;
this._addMessage({ role, tool_call_id, content });
continue;
} else if (singleFunctionToCall && singleFunctionToCall !== name) {
const content = `Invalid tool_call: ${JSON.stringify(name)}. ${JSON.stringify(
singleFunctionToCall,
)} requested. Please try again`;
this._addMessage({ role, tool_call_id, content });
continue;
}
let parsed;
try {
parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args;
} catch (error) {
const content = error instanceof Error ? error.message : String(error);
this._addMessage({ role, tool_call_id, content });
continue;
}
// @ts-expect-error it can't rule out `never` type.
const rawContent = await fn.function(parsed, this);
const content = this.#stringifyFunctionCallResult(rawContent);
this._addMessage({ role, tool_call_id, content });
if (singleFunctionToCall) {
return;
}
}
}
return;
}
#stringifyFunctionCallResult(rawContent: unknown): string {
return (
typeof rawContent === 'string' ? rawContent
: rawContent === undefined ? 'undefined'
: JSON.stringify(rawContent)
);
}
}
export interface AbstractChatCompletionRunnerEvents extends BaseEvents {
functionCall: (functionCall: ChatCompletionMessage.FunctionCall) => void;
message: (message: ChatCompletionMessageParam) => void;
chatCompletion: (completion: ChatCompletion) => void;
finalContent: (contentSnapshot: string) => void;
finalMessage: (message: ChatCompletionMessageParam) => void;
finalChatCompletion: (completion: ChatCompletion) => void;
finalFunctionCall: (functionCall: ChatCompletionMessage.FunctionCall) => void;
functionCallResult: (content: string) => void;
finalFunctionCallResult: (content: string) => void;
totalUsage: (usage: CompletionUsage) => void;
}
+779
View File
@@ -0,0 +1,779 @@
import {
TextContentBlock,
ImageFileContentBlock,
Message,
MessageContentDelta,
Text,
ImageFile,
TextDelta,
MessageDelta,
MessageContent,
} from '../resources/beta/threads/messages';
import * as Core from '../core';
import { RequestOptions } from '../core';
import {
Run,
RunCreateParamsBase,
RunCreateParamsStreaming,
Runs,
RunSubmitToolOutputsParamsBase,
RunSubmitToolOutputsParamsStreaming,
} from '../resources/beta/threads/runs/runs';
import { type ReadableStream } from '../_shims/index';
import { Stream } from '../streaming';
import { APIUserAbortError, OpenAIError } from '../error';
import {
AssistantStreamEvent,
MessageStreamEvent,
RunStepStreamEvent,
RunStreamEvent,
} from '../resources/beta/assistants';
import { RunStep, RunStepDelta, ToolCall, ToolCallDelta } from '../resources/beta/threads/runs/steps';
import { ThreadCreateAndRunParamsBase, Threads } from '../resources/beta/threads/threads';
import { BaseEvents, EventStream } from './EventStream';
export interface AssistantStreamEvents extends BaseEvents {
run: (run: Run) => void;
//New event structure
messageCreated: (message: Message) => void;
messageDelta: (message: MessageDelta, snapshot: Message) => void;
messageDone: (message: Message) => void;
runStepCreated: (runStep: RunStep) => void;
runStepDelta: (delta: RunStepDelta, snapshot: Runs.RunStep) => void;
runStepDone: (runStep: Runs.RunStep, snapshot: Runs.RunStep) => void;
toolCallCreated: (toolCall: ToolCall) => void;
toolCallDelta: (delta: ToolCallDelta, snapshot: ToolCall) => void;
toolCallDone: (toolCall: ToolCall) => void;
textCreated: (content: Text) => void;
textDelta: (delta: TextDelta, snapshot: Text) => void;
textDone: (content: Text, snapshot: Message) => void;
//No created or delta as this is not streamed
imageFileDone: (content: ImageFile, snapshot: Message) => void;
event: (event: AssistantStreamEvent) => void;
}
export type ThreadCreateAndRunParamsBaseStream = Omit<ThreadCreateAndRunParamsBase, 'stream'> & {
stream?: true;
};
export type RunCreateParamsBaseStream = Omit<RunCreateParamsBase, 'stream'> & {
stream?: true;
};
export type RunSubmitToolOutputsParamsStream = Omit<RunSubmitToolOutputsParamsBase, 'stream'> & {
stream?: true;
};
export class AssistantStream
extends EventStream<AssistantStreamEvents>
implements AsyncIterable<AssistantStreamEvent>
{
//Track all events in a single list for reference
#events: AssistantStreamEvent[] = [];
//Used to accumulate deltas
//We are accumulating many types so the value here is not strict
#runStepSnapshots: { [id: string]: Runs.RunStep } = {};
#messageSnapshots: { [id: string]: Message } = {};
#messageSnapshot: Message | undefined;
#finalRun: Run | undefined;
#currentContentIndex: number | undefined;
#currentContent: MessageContent | undefined;
#currentToolCallIndex: number | undefined;
#currentToolCall: ToolCall | undefined;
//For current snapshot methods
#currentEvent: AssistantStreamEvent | undefined;
#currentRunSnapshot: Run | undefined;
#currentRunStepSnapshot: Runs.RunStep | undefined;
[Symbol.asyncIterator](): AsyncIterator<AssistantStreamEvent> {
const pushQueue: AssistantStreamEvent[] = [];
const readQueue: {
resolve: (chunk: AssistantStreamEvent | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
//Catch all for passing along all events
this.on('event', (event) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(event);
} else {
pushQueue.push(event);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<AssistantStreamEvent>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<AssistantStreamEvent | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((chunk) => (chunk ? { value: chunk, done: false } : { value: undefined, done: true }));
}
const chunk = pushQueue.shift()!;
return { value: chunk, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
static fromReadableStream(stream: ReadableStream): AssistantStream {
const runner = new AssistantStream();
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
protected async _fromReadableStream(
readableStream: ReadableStream,
options?: Core.RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this._connected();
const stream = Stream.fromReadableStream<AssistantStreamEvent>(readableStream, this.controller);
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
toReadableStream(): ReadableStream {
const stream = new Stream(this[Symbol.asyncIterator].bind(this), this.controller);
return stream.toReadableStream();
}
static createToolAssistantStream(
threadId: string,
runId: string,
runs: Runs,
params: RunSubmitToolOutputsParamsStream,
options: RequestOptions | undefined,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._runToolAssistantStream(threadId, runId, runs, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
protected async _createToolAssistantStream(
run: Runs,
threadId: string,
runId: string,
params: RunSubmitToolOutputsParamsStream,
options?: Core.RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunSubmitToolOutputsParamsStreaming = { ...params, stream: true };
const stream = await run.submitToolOutputs(threadId, runId, body, {
...options,
signal: this.controller.signal,
});
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
static createThreadAssistantStream(
params: ThreadCreateAndRunParamsBaseStream,
thread: Threads,
options?: RequestOptions,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._threadAssistantStream(params, thread, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
static createAssistantStream(
threadId: string,
runs: Runs,
params: RunCreateParamsBaseStream,
options?: RequestOptions,
): AssistantStream {
const runner = new AssistantStream();
runner._run(() =>
runner._runAssistantStream(threadId, runs, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
currentEvent(): AssistantStreamEvent | undefined {
return this.#currentEvent;
}
currentRun(): Run | undefined {
return this.#currentRunSnapshot;
}
currentMessageSnapshot(): Message | undefined {
return this.#messageSnapshot;
}
currentRunStepSnapshot(): Runs.RunStep | undefined {
return this.#currentRunStepSnapshot;
}
async finalRunSteps(): Promise<Runs.RunStep[]> {
await this.done();
return Object.values(this.#runStepSnapshots);
}
async finalMessages(): Promise<Message[]> {
await this.done();
return Object.values(this.#messageSnapshots);
}
async finalRun(): Promise<Run> {
await this.done();
if (!this.#finalRun) throw Error('Final run was not received.');
return this.#finalRun;
}
protected async _createThreadAssistantStream(
thread: Threads,
params: ThreadCreateAndRunParamsBase,
options?: Core.RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunCreateParamsStreaming = { ...params, stream: true };
const stream = await thread.createAndRun(body, { ...options, signal: this.controller.signal });
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
protected async _createAssistantStream(
run: Runs,
threadId: string,
params: RunCreateParamsBase,
options?: Core.RequestOptions,
): Promise<Run> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
const body: RunCreateParamsStreaming = { ...params, stream: true };
const stream = await run.create(threadId, body, { ...options, signal: this.controller.signal });
this._connected();
for await (const event of stream) {
this.#addEvent(event);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addRun(this.#endRequest());
}
#addEvent(event: AssistantStreamEvent) {
if (this.ended) return;
this.#currentEvent = event;
this.#handleEvent(event);
switch (event.event) {
case 'thread.created':
//No action on this event.
break;
case 'thread.run.created':
case 'thread.run.queued':
case 'thread.run.in_progress':
case 'thread.run.requires_action':
case 'thread.run.completed':
case 'thread.run.incomplete':
case 'thread.run.failed':
case 'thread.run.cancelling':
case 'thread.run.cancelled':
case 'thread.run.expired':
this.#handleRun(event);
break;
case 'thread.run.step.created':
case 'thread.run.step.in_progress':
case 'thread.run.step.delta':
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
this.#handleRunStep(event);
break;
case 'thread.message.created':
case 'thread.message.in_progress':
case 'thread.message.delta':
case 'thread.message.completed':
case 'thread.message.incomplete':
this.#handleMessage(event);
break;
case 'error':
//This is included for completeness, but errors are processed in the SSE event processing so this should not occur
throw new Error(
'Encountered an error event in event processing - errors should be processed earlier',
);
default:
assertNever(event);
}
}
#endRequest(): Run {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
if (!this.#finalRun) throw Error('Final run has not been received');
return this.#finalRun;
}
#handleMessage(this: AssistantStream, event: MessageStreamEvent) {
const [accumulatedMessage, newContent] = this.#accumulateMessage(event, this.#messageSnapshot);
this.#messageSnapshot = accumulatedMessage;
this.#messageSnapshots[accumulatedMessage.id] = accumulatedMessage;
for (const content of newContent) {
const snapshotContent = accumulatedMessage.content[content.index];
if (snapshotContent?.type == 'text') {
this._emit('textCreated', snapshotContent.text);
}
}
switch (event.event) {
case 'thread.message.created':
this._emit('messageCreated', event.data);
break;
case 'thread.message.in_progress':
break;
case 'thread.message.delta':
this._emit('messageDelta', event.data.delta, accumulatedMessage);
if (event.data.delta.content) {
for (const content of event.data.delta.content) {
//If it is text delta, emit a text delta event
if (content.type == 'text' && content.text) {
let textDelta = content.text;
let snapshot = accumulatedMessage.content[content.index];
if (snapshot && snapshot.type == 'text') {
this._emit('textDelta', textDelta, snapshot.text);
} else {
throw Error('The snapshot associated with this text delta is not text or missing');
}
}
if (content.index != this.#currentContentIndex) {
//See if we have in progress content
if (this.#currentContent) {
switch (this.#currentContent.type) {
case 'text':
this._emit('textDone', this.#currentContent.text, this.#messageSnapshot);
break;
case 'image_file':
this._emit('imageFileDone', this.#currentContent.image_file, this.#messageSnapshot);
break;
}
}
this.#currentContentIndex = content.index;
}
this.#currentContent = accumulatedMessage.content[content.index];
}
}
break;
case 'thread.message.completed':
case 'thread.message.incomplete':
//We emit the latest content we were working on on completion (including incomplete)
if (this.#currentContentIndex !== undefined) {
const currentContent = event.data.content[this.#currentContentIndex];
if (currentContent) {
switch (currentContent.type) {
case 'image_file':
this._emit('imageFileDone', currentContent.image_file, this.#messageSnapshot);
break;
case 'text':
this._emit('textDone', currentContent.text, this.#messageSnapshot);
break;
}
}
}
if (this.#messageSnapshot) {
this._emit('messageDone', event.data);
}
this.#messageSnapshot = undefined;
}
}
#handleRunStep(this: AssistantStream, event: RunStepStreamEvent) {
const accumulatedRunStep = this.#accumulateRunStep(event);
this.#currentRunStepSnapshot = accumulatedRunStep;
switch (event.event) {
case 'thread.run.step.created':
this._emit('runStepCreated', event.data);
break;
case 'thread.run.step.delta':
const delta = event.data.delta;
if (
delta.step_details &&
delta.step_details.type == 'tool_calls' &&
delta.step_details.tool_calls &&
accumulatedRunStep.step_details.type == 'tool_calls'
) {
for (const toolCall of delta.step_details.tool_calls) {
if (toolCall.index == this.#currentToolCallIndex) {
this._emit(
'toolCallDelta',
toolCall,
accumulatedRunStep.step_details.tool_calls[toolCall.index] as ToolCall,
);
} else {
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall);
}
this.#currentToolCallIndex = toolCall.index;
this.#currentToolCall = accumulatedRunStep.step_details.tool_calls[toolCall.index];
if (this.#currentToolCall) this._emit('toolCallCreated', this.#currentToolCall);
}
}
}
this._emit('runStepDelta', event.data.delta, accumulatedRunStep);
break;
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
this.#currentRunStepSnapshot = undefined;
const details = event.data.step_details;
if (details.type == 'tool_calls') {
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall as ToolCall);
this.#currentToolCall = undefined;
}
}
this._emit('runStepDone', event.data, accumulatedRunStep);
break;
case 'thread.run.step.in_progress':
break;
}
}
#handleEvent(this: AssistantStream, event: AssistantStreamEvent) {
this.#events.push(event);
this._emit('event', event);
}
#accumulateRunStep(event: RunStepStreamEvent): Runs.RunStep {
switch (event.event) {
case 'thread.run.step.created':
this.#runStepSnapshots[event.data.id] = event.data;
return event.data;
case 'thread.run.step.delta':
let snapshot = this.#runStepSnapshots[event.data.id] as Runs.RunStep;
if (!snapshot) {
throw Error('Received a RunStepDelta before creation of a snapshot');
}
let data = event.data;
if (data.delta) {
const accumulated = AssistantStream.accumulateDelta(snapshot, data.delta) as Runs.RunStep;
this.#runStepSnapshots[event.data.id] = accumulated;
}
return this.#runStepSnapshots[event.data.id] as Runs.RunStep;
case 'thread.run.step.completed':
case 'thread.run.step.failed':
case 'thread.run.step.cancelled':
case 'thread.run.step.expired':
case 'thread.run.step.in_progress':
this.#runStepSnapshots[event.data.id] = event.data;
break;
}
if (this.#runStepSnapshots[event.data.id]) return this.#runStepSnapshots[event.data.id] as Runs.RunStep;
throw new Error('No snapshot available');
}
#accumulateMessage(
event: AssistantStreamEvent,
snapshot: Message | undefined,
): [Message, MessageContentDelta[]] {
let newContent: MessageContentDelta[] = [];
switch (event.event) {
case 'thread.message.created':
//On creation the snapshot is just the initial message
return [event.data, newContent];
case 'thread.message.delta':
if (!snapshot) {
throw Error(
'Received a delta with no existing snapshot (there should be one from message creation)',
);
}
let data = event.data;
//If this delta does not have content, nothing to process
if (data.delta.content) {
for (const contentElement of data.delta.content) {
if (contentElement.index in snapshot.content) {
let currentContent = snapshot.content[contentElement.index];
snapshot.content[contentElement.index] = this.#accumulateContent(
contentElement,
currentContent,
);
} else {
snapshot.content[contentElement.index] = contentElement as MessageContent;
// This is a new element
newContent.push(contentElement);
}
}
}
return [snapshot, newContent];
case 'thread.message.in_progress':
case 'thread.message.completed':
case 'thread.message.incomplete':
//No changes on other thread events
if (snapshot) {
return [snapshot, newContent];
} else {
throw Error('Received thread message event with no existing snapshot');
}
}
throw Error('Tried to accumulate a non-message event');
}
#accumulateContent(
contentElement: MessageContentDelta,
currentContent: MessageContent | undefined,
): TextContentBlock | ImageFileContentBlock {
return AssistantStream.accumulateDelta(currentContent as unknown as Record<any, any>, contentElement) as
| TextContentBlock
| ImageFileContentBlock;
}
static accumulateDelta(acc: Record<string, any>, delta: Record<string, any>): Record<string, any> {
for (const [key, deltaValue] of Object.entries(delta)) {
if (!acc.hasOwnProperty(key)) {
acc[key] = deltaValue;
continue;
}
let accValue = acc[key];
if (accValue === null || accValue === undefined) {
acc[key] = deltaValue;
continue;
}
// We don't accumulate these special properties
if (key === 'index' || key === 'type') {
acc[key] = deltaValue;
continue;
}
// Type-specific accumulation logic
if (typeof accValue === 'string' && typeof deltaValue === 'string') {
accValue += deltaValue;
} else if (typeof accValue === 'number' && typeof deltaValue === 'number') {
accValue += deltaValue;
} else if (Core.isObj(accValue) && Core.isObj(deltaValue)) {
accValue = this.accumulateDelta(accValue as Record<string, any>, deltaValue as Record<string, any>);
} else if (Array.isArray(accValue) && Array.isArray(deltaValue)) {
if (accValue.every((x) => typeof x === 'string' || typeof x === 'number')) {
accValue.push(...deltaValue); // Use spread syntax for efficient addition
continue;
}
for (const deltaEntry of deltaValue) {
if (!Core.isObj(deltaEntry)) {
throw new Error(`Expected array delta entry to be an object but got: ${deltaEntry}`);
}
const index = deltaEntry['index'];
if (index == null) {
console.error(deltaEntry);
throw new Error('Expected array delta entry to have an `index` property');
}
if (typeof index !== 'number') {
throw new Error(`Expected array delta entry \`index\` property to be a number but got ${index}`);
}
const accEntry = accValue[index];
if (accEntry == null) {
accValue.push(deltaEntry);
} else {
accValue[index] = this.accumulateDelta(accEntry, deltaEntry);
}
}
continue;
} else {
throw Error(`Unhandled record type: ${key}, deltaValue: ${deltaValue}, accValue: ${accValue}`);
}
acc[key] = accValue;
}
return acc;
}
#handleRun(this: AssistantStream, event: RunStreamEvent) {
this.#currentRunSnapshot = event.data;
switch (event.event) {
case 'thread.run.created':
break;
case 'thread.run.queued':
break;
case 'thread.run.in_progress':
break;
case 'thread.run.requires_action':
case 'thread.run.cancelled':
case 'thread.run.failed':
case 'thread.run.completed':
case 'thread.run.expired':
this.#finalRun = event.data;
if (this.#currentToolCall) {
this._emit('toolCallDone', this.#currentToolCall);
this.#currentToolCall = undefined;
}
break;
case 'thread.run.cancelling':
break;
}
}
protected _addRun(run: Run): Run {
return run;
}
protected async _threadAssistantStream(
params: ThreadCreateAndRunParamsBase,
thread: Threads,
options?: Core.RequestOptions,
): Promise<Run> {
return await this._createThreadAssistantStream(thread, params, options);
}
protected async _runAssistantStream(
threadId: string,
runs: Runs,
params: RunCreateParamsBase,
options?: Core.RequestOptions,
): Promise<Run> {
return await this._createAssistantStream(runs, threadId, params, options);
}
protected async _runToolAssistantStream(
threadId: string,
runId: string,
runs: Runs,
params: RunSubmitToolOutputsParamsStream,
options?: Core.RequestOptions,
): Promise<Run> {
return await this._createToolAssistantStream(runs, threadId, runId, params, options);
}
}
function assertNever(_x: never) {}
+76
View File
@@ -0,0 +1,76 @@
import {
type ChatCompletionMessageParam,
type ChatCompletionCreateParamsNonStreaming,
} from '../resources/chat/completions';
import { type RunnableFunctions, type BaseFunctionsArgs, RunnableTools } from './RunnableFunction';
import {
AbstractChatCompletionRunner,
AbstractChatCompletionRunnerEvents,
RunnerOptions,
} from './AbstractChatCompletionRunner';
import { isAssistantMessage } from './chatCompletionUtils';
import OpenAI from '../index';
import { AutoParseableTool } from '../lib/parser';
export interface ChatCompletionRunnerEvents extends AbstractChatCompletionRunnerEvents {
content: (content: string) => void;
}
export type ChatCompletionFunctionRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsNonStreaming,
'functions'
> & {
functions: RunnableFunctions<FunctionsArgs>;
};
export type ChatCompletionToolRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsNonStreaming,
'tools'
> & {
tools: RunnableTools<FunctionsArgs> | AutoParseableTool<any, true>[];
};
export class ChatCompletionRunner<ParsedT = null> extends AbstractChatCompletionRunner<
ChatCompletionRunnerEvents,
ParsedT
> {
/** @deprecated - please use `runTools` instead. */
static runFunctions(
client: OpenAI,
params: ChatCompletionFunctionRunnerParams<any[]>,
options?: RunnerOptions,
): ChatCompletionRunner<null> {
const runner = new ChatCompletionRunner();
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' },
};
runner._run(() => runner._runFunctions(client, params, opts));
return runner;
}
static runTools<ParsedT>(
client: OpenAI,
params: ChatCompletionToolRunnerParams<any[]>,
options?: RunnerOptions,
): ChatCompletionRunner<ParsedT> {
const runner = new ChatCompletionRunner<ParsedT>();
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runTools' },
};
runner._run(() => runner._runTools(client, params, opts));
return runner;
}
override _addMessage(
this: ChatCompletionRunner<ParsedT>,
message: ChatCompletionMessageParam,
emit: boolean = true,
) {
super._addMessage(message, emit);
if (isAssistantMessage(message) && message.content) {
this._emit('content', message.content as string);
}
}
}
+871
View File
@@ -0,0 +1,871 @@
import * as Core from '../core';
import {
OpenAIError,
APIUserAbortError,
LengthFinishReasonError,
ContentFilterFinishReasonError,
} from '../error';
import {
ChatCompletionTokenLogprob,
type ChatCompletion,
type ChatCompletionChunk,
type ChatCompletionCreateParams,
type ChatCompletionCreateParamsStreaming,
type ChatCompletionCreateParamsBase,
type ChatCompletionRole,
} from '../resources/chat/completions/completions';
import {
AbstractChatCompletionRunner,
type AbstractChatCompletionRunnerEvents,
} from './AbstractChatCompletionRunner';
import { type ReadableStream } from '../_shims/index';
import { Stream } from '../streaming';
import OpenAI from '../index';
import { ParsedChatCompletion } from '../resources/beta/chat/completions';
import {
AutoParseableResponseFormat,
hasAutoParseableInput,
isAutoParsableResponseFormat,
isAutoParsableTool,
maybeParseChatCompletion,
shouldParseToolCall,
} from '../lib/parser';
import { partialParse } from '../_vendor/partial-json-parser/parser';
export interface ContentDeltaEvent {
delta: string;
snapshot: string;
parsed: unknown | null;
}
export interface ContentDoneEvent<ParsedT = null> {
content: string;
parsed: ParsedT | null;
}
export interface RefusalDeltaEvent {
delta: string;
snapshot: string;
}
export interface RefusalDoneEvent {
refusal: string;
}
export interface FunctionToolCallArgumentsDeltaEvent {
name: string;
index: number;
arguments: string;
parsed_arguments: unknown;
arguments_delta: string;
}
export interface FunctionToolCallArgumentsDoneEvent {
name: string;
index: number;
arguments: string;
parsed_arguments: unknown;
}
export interface LogProbsContentDeltaEvent {
content: Array<ChatCompletionTokenLogprob>;
snapshot: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsContentDoneEvent {
content: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsRefusalDeltaEvent {
refusal: Array<ChatCompletionTokenLogprob>;
snapshot: Array<ChatCompletionTokenLogprob>;
}
export interface LogProbsRefusalDoneEvent {
refusal: Array<ChatCompletionTokenLogprob>;
}
export interface ChatCompletionStreamEvents<ParsedT = null> extends AbstractChatCompletionRunnerEvents {
content: (contentDelta: string, contentSnapshot: string) => void;
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
'content.delta': (props: ContentDeltaEvent) => void;
'content.done': (props: ContentDoneEvent<ParsedT>) => void;
'refusal.delta': (props: RefusalDeltaEvent) => void;
'refusal.done': (props: RefusalDoneEvent) => void;
'tool_calls.function.arguments.delta': (props: FunctionToolCallArgumentsDeltaEvent) => void;
'tool_calls.function.arguments.done': (props: FunctionToolCallArgumentsDoneEvent) => void;
'logprobs.content.delta': (props: LogProbsContentDeltaEvent) => void;
'logprobs.content.done': (props: LogProbsContentDoneEvent) => void;
'logprobs.refusal.delta': (props: LogProbsRefusalDeltaEvent) => void;
'logprobs.refusal.done': (props: LogProbsRefusalDoneEvent) => void;
}
export type ChatCompletionStreamParams = Omit<ChatCompletionCreateParamsBase, 'stream'> & {
stream?: true;
};
interface ChoiceEventState {
content_done: boolean;
refusal_done: boolean;
logprobs_content_done: boolean;
logprobs_refusal_done: boolean;
current_tool_call_index: number | null;
done_tool_calls: Set<number>;
}
export class ChatCompletionStream<ParsedT = null>
extends AbstractChatCompletionRunner<ChatCompletionStreamEvents<ParsedT>, ParsedT>
implements AsyncIterable<ChatCompletionChunk>
{
#params: ChatCompletionCreateParams | null;
#choiceEventStates: ChoiceEventState[];
#currentChatCompletionSnapshot: ChatCompletionSnapshot | undefined;
constructor(params: ChatCompletionCreateParams | null) {
super();
this.#params = params;
this.#choiceEventStates = [];
}
get currentChatCompletionSnapshot(): ChatCompletionSnapshot | undefined {
return this.#currentChatCompletionSnapshot;
}
/**
* Intended for use on the frontend, consuming a stream produced with
* `.toReadableStream()` on the backend.
*
* Note that messages sent to the model do not appear in `.on('message')`
* in this context.
*/
static fromReadableStream(stream: ReadableStream): ChatCompletionStream<null> {
const runner = new ChatCompletionStream(null);
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
static createChatCompletion<ParsedT>(
client: OpenAI,
params: ChatCompletionStreamParams,
options?: Core.RequestOptions,
): ChatCompletionStream<ParsedT> {
const runner = new ChatCompletionStream<ParsedT>(params as ChatCompletionCreateParamsStreaming);
runner._run(() =>
runner._runChatCompletion(
client,
{ ...params, stream: true },
{ ...options, headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' } },
),
);
return runner;
}
#beginRequest() {
if (this.ended) return;
this.#currentChatCompletionSnapshot = undefined;
}
#getChoiceEventState(choice: ChatCompletionSnapshot.Choice): ChoiceEventState {
let state = this.#choiceEventStates[choice.index];
if (state) {
return state;
}
state = {
content_done: false,
refusal_done: false,
logprobs_content_done: false,
logprobs_refusal_done: false,
done_tool_calls: new Set(),
current_tool_call_index: null,
};
this.#choiceEventStates[choice.index] = state;
return state;
}
#addChunk(this: ChatCompletionStream<ParsedT>, chunk: ChatCompletionChunk) {
if (this.ended) return;
const completion = this.#accumulateChatCompletion(chunk);
this._emit('chunk', chunk, completion);
for (const choice of chunk.choices) {
const choiceSnapshot = completion.choices[choice.index]!;
if (
choice.delta.content != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.content
) {
this._emit('content', choice.delta.content, choiceSnapshot.message.content);
this._emit('content.delta', {
delta: choice.delta.content,
snapshot: choiceSnapshot.message.content,
parsed: choiceSnapshot.message.parsed,
});
}
if (
choice.delta.refusal != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.refusal
) {
this._emit('refusal.delta', {
delta: choice.delta.refusal,
snapshot: choiceSnapshot.message.refusal,
});
}
if (choice.logprobs?.content != null && choiceSnapshot.message?.role === 'assistant') {
this._emit('logprobs.content.delta', {
content: choice.logprobs?.content,
snapshot: choiceSnapshot.logprobs?.content ?? [],
});
}
if (choice.logprobs?.refusal != null && choiceSnapshot.message?.role === 'assistant') {
this._emit('logprobs.refusal.delta', {
refusal: choice.logprobs?.refusal,
snapshot: choiceSnapshot.logprobs?.refusal ?? [],
});
}
const state = this.#getChoiceEventState(choiceSnapshot);
if (choiceSnapshot.finish_reason) {
this.#emitContentDoneEvents(choiceSnapshot);
if (state.current_tool_call_index != null) {
this.#emitToolCallDoneEvent(choiceSnapshot, state.current_tool_call_index);
}
}
for (const toolCall of choice.delta.tool_calls ?? []) {
if (state.current_tool_call_index !== toolCall.index) {
this.#emitContentDoneEvents(choiceSnapshot);
// new tool call started, the previous one is done
if (state.current_tool_call_index != null) {
this.#emitToolCallDoneEvent(choiceSnapshot, state.current_tool_call_index);
}
}
state.current_tool_call_index = toolCall.index;
}
for (const toolCallDelta of choice.delta.tool_calls ?? []) {
const toolCallSnapshot = choiceSnapshot.message.tool_calls?.[toolCallDelta.index];
if (!toolCallSnapshot?.type) {
continue;
}
if (toolCallSnapshot?.type === 'function') {
this._emit('tool_calls.function.arguments.delta', {
name: toolCallSnapshot.function?.name,
index: toolCallDelta.index,
arguments: toolCallSnapshot.function.arguments,
parsed_arguments: toolCallSnapshot.function.parsed_arguments,
arguments_delta: toolCallDelta.function?.arguments ?? '',
});
} else {
assertNever(toolCallSnapshot?.type);
}
}
}
}
#emitToolCallDoneEvent(choiceSnapshot: ChatCompletionSnapshot.Choice, toolCallIndex: number) {
const state = this.#getChoiceEventState(choiceSnapshot);
if (state.done_tool_calls.has(toolCallIndex)) {
// we've already fired the done event
return;
}
const toolCallSnapshot = choiceSnapshot.message.tool_calls?.[toolCallIndex];
if (!toolCallSnapshot) {
throw new Error('no tool call snapshot');
}
if (!toolCallSnapshot.type) {
throw new Error('tool call snapshot missing `type`');
}
if (toolCallSnapshot.type === 'function') {
const inputTool = this.#params?.tools?.find(
(tool) => tool.type === 'function' && tool.function.name === toolCallSnapshot.function.name,
);
this._emit('tool_calls.function.arguments.done', {
name: toolCallSnapshot.function.name,
index: toolCallIndex,
arguments: toolCallSnapshot.function.arguments,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCallSnapshot.function.arguments)
: inputTool?.function.strict ? JSON.parse(toolCallSnapshot.function.arguments)
: null,
});
} else {
assertNever(toolCallSnapshot.type);
}
}
#emitContentDoneEvents(choiceSnapshot: ChatCompletionSnapshot.Choice) {
const state = this.#getChoiceEventState(choiceSnapshot);
if (choiceSnapshot.message.content && !state.content_done) {
state.content_done = true;
const responseFormat = this.#getAutoParseableResponseFormat();
this._emit('content.done', {
content: choiceSnapshot.message.content,
parsed: responseFormat ? responseFormat.$parseRaw(choiceSnapshot.message.content) : (null as any),
});
}
if (choiceSnapshot.message.refusal && !state.refusal_done) {
state.refusal_done = true;
this._emit('refusal.done', { refusal: choiceSnapshot.message.refusal });
}
if (choiceSnapshot.logprobs?.content && !state.logprobs_content_done) {
state.logprobs_content_done = true;
this._emit('logprobs.content.done', { content: choiceSnapshot.logprobs.content });
}
if (choiceSnapshot.logprobs?.refusal && !state.logprobs_refusal_done) {
state.logprobs_refusal_done = true;
this._emit('logprobs.refusal.done', { refusal: choiceSnapshot.logprobs.refusal });
}
}
#endRequest(): ParsedChatCompletion<ParsedT> {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
const snapshot = this.#currentChatCompletionSnapshot;
if (!snapshot) {
throw new OpenAIError(`request ended without sending any chunks`);
}
this.#currentChatCompletionSnapshot = undefined;
this.#choiceEventStates = [];
return finalizeChatCompletion(snapshot, this.#params);
}
protected override async _createChatCompletion(
client: OpenAI,
params: ChatCompletionCreateParams,
options?: Core.RequestOptions,
): Promise<ParsedChatCompletion<ParsedT>> {
super._createChatCompletion;
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
const stream = await client.chat.completions.create(
{ ...params, stream: true },
{ ...options, signal: this.controller.signal },
);
this._connected();
for await (const chunk of stream) {
this.#addChunk(chunk);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addChatCompletion(this.#endRequest());
}
protected async _fromReadableStream(
readableStream: ReadableStream,
options?: Core.RequestOptions,
): Promise<ChatCompletion> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
this._connected();
const stream = Stream.fromReadableStream<ChatCompletionChunk>(readableStream, this.controller);
let chatId;
for await (const chunk of stream) {
if (chatId && chatId !== chunk.id) {
// A new request has been made.
this._addChatCompletion(this.#endRequest());
}
this.#addChunk(chunk);
chatId = chunk.id;
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this._addChatCompletion(this.#endRequest());
}
#getAutoParseableResponseFormat(): AutoParseableResponseFormat<ParsedT> | null {
const responseFormat = this.#params?.response_format;
if (isAutoParsableResponseFormat<ParsedT>(responseFormat)) {
return responseFormat;
}
return null;
}
#accumulateChatCompletion(chunk: ChatCompletionChunk): ChatCompletionSnapshot {
let snapshot = this.#currentChatCompletionSnapshot;
const { choices, ...rest } = chunk;
if (!snapshot) {
snapshot = this.#currentChatCompletionSnapshot = {
...rest,
choices: [],
};
} else {
Object.assign(snapshot, rest);
}
for (const { delta, finish_reason, index, logprobs = null, ...other } of chunk.choices) {
let choice = snapshot.choices[index];
if (!choice) {
choice = snapshot.choices[index] = { finish_reason, index, message: {}, logprobs, ...other };
}
if (logprobs) {
if (!choice.logprobs) {
choice.logprobs = Object.assign({}, logprobs);
} else {
const { content, refusal, ...rest } = logprobs;
assertIsEmpty(rest);
Object.assign(choice.logprobs, rest);
if (content) {
choice.logprobs.content ??= [];
choice.logprobs.content.push(...content);
}
if (refusal) {
choice.logprobs.refusal ??= [];
choice.logprobs.refusal.push(...refusal);
}
}
}
if (finish_reason) {
choice.finish_reason = finish_reason;
if (this.#params && hasAutoParseableInput(this.#params)) {
if (finish_reason === 'length') {
throw new LengthFinishReasonError();
}
if (finish_reason === 'content_filter') {
throw new ContentFilterFinishReasonError();
}
}
}
Object.assign(choice, other);
if (!delta) continue; // Shouldn't happen; just in case.
const { content, refusal, function_call, role, tool_calls, ...rest } = delta;
assertIsEmpty(rest);
Object.assign(choice.message, rest);
if (refusal) {
choice.message.refusal = (choice.message.refusal || '') + refusal;
}
if (role) choice.message.role = role;
if (function_call) {
if (!choice.message.function_call) {
choice.message.function_call = function_call;
} else {
if (function_call.name) choice.message.function_call.name = function_call.name;
if (function_call.arguments) {
choice.message.function_call.arguments ??= '';
choice.message.function_call.arguments += function_call.arguments;
}
}
}
if (content) {
choice.message.content = (choice.message.content || '') + content;
if (!choice.message.refusal && this.#getAutoParseableResponseFormat()) {
choice.message.parsed = partialParse(choice.message.content);
}
}
if (tool_calls) {
if (!choice.message.tool_calls) choice.message.tool_calls = [];
for (const { index, id, type, function: fn, ...rest } of tool_calls) {
const tool_call = (choice.message.tool_calls[index] ??=
{} as ChatCompletionSnapshot.Choice.Message.ToolCall);
Object.assign(tool_call, rest);
if (id) tool_call.id = id;
if (type) tool_call.type = type;
if (fn) tool_call.function ??= { name: fn.name ?? '', arguments: '' };
if (fn?.name) tool_call.function!.name = fn.name;
if (fn?.arguments) {
tool_call.function!.arguments += fn.arguments;
if (shouldParseToolCall(this.#params, tool_call)) {
tool_call.function!.parsed_arguments = partialParse(tool_call.function!.arguments);
}
}
}
}
}
return snapshot;
}
[Symbol.asyncIterator](this: ChatCompletionStream<ParsedT>): AsyncIterator<ChatCompletionChunk> {
const pushQueue: ChatCompletionChunk[] = [];
const readQueue: {
resolve: (chunk: ChatCompletionChunk | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
this.on('chunk', (chunk) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(chunk);
} else {
pushQueue.push(chunk);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<ChatCompletionChunk>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<ChatCompletionChunk | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((chunk) => (chunk ? { value: chunk, done: false } : { value: undefined, done: true }));
}
const chunk = pushQueue.shift()!;
return { value: chunk, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
toReadableStream(): ReadableStream {
const stream = new Stream(this[Symbol.asyncIterator].bind(this), this.controller);
return stream.toReadableStream();
}
}
function finalizeChatCompletion<ParsedT>(
snapshot: ChatCompletionSnapshot,
params: ChatCompletionCreateParams | null,
): ParsedChatCompletion<ParsedT> {
const { id, choices, created, model, system_fingerprint, ...rest } = snapshot;
const completion: ChatCompletion = {
...rest,
id,
choices: choices.map(
({ message, finish_reason, index, logprobs, ...choiceRest }): ChatCompletion.Choice => {
if (!finish_reason) {
throw new OpenAIError(`missing finish_reason for choice ${index}`);
}
const { content = null, function_call, tool_calls, ...messageRest } = message;
const role = message.role as 'assistant'; // this is what we expect; in theory it could be different which would make our types a slight lie but would be fine.
if (!role) {
throw new OpenAIError(`missing role for choice ${index}`);
}
if (function_call) {
const { arguments: args, name } = function_call;
if (args == null) {
throw new OpenAIError(`missing function_call.arguments for choice ${index}`);
}
if (!name) {
throw new OpenAIError(`missing function_call.name for choice ${index}`);
}
return {
...choiceRest,
message: {
content,
function_call: { arguments: args, name },
role,
refusal: message.refusal ?? null,
},
finish_reason,
index,
logprobs,
};
}
if (tool_calls) {
return {
...choiceRest,
index,
finish_reason,
logprobs,
message: {
...messageRest,
role,
content,
refusal: message.refusal ?? null,
tool_calls: tool_calls.map((tool_call, i) => {
const { function: fn, type, id, ...toolRest } = tool_call;
const { arguments: args, name, ...fnRest } = fn || {};
if (id == null) {
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].id\n${str(snapshot)}`);
}
if (type == null) {
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].type\n${str(snapshot)}`);
}
if (name == null) {
throw new OpenAIError(
`missing choices[${index}].tool_calls[${i}].function.name\n${str(snapshot)}`,
);
}
if (args == null) {
throw new OpenAIError(
`missing choices[${index}].tool_calls[${i}].function.arguments\n${str(snapshot)}`,
);
}
return { ...toolRest, id, type, function: { ...fnRest, name, arguments: args } };
}),
},
};
}
return {
...choiceRest,
message: { ...messageRest, content, role, refusal: message.refusal ?? null },
finish_reason,
index,
logprobs,
};
},
),
created,
model,
object: 'chat.completion',
...(system_fingerprint ? { system_fingerprint } : {}),
};
return maybeParseChatCompletion(completion, params);
}
function str(x: unknown) {
return JSON.stringify(x);
}
/**
* Represents a streamed chunk of a chat completion response returned by model,
* based on the provided input.
*/
export interface ChatCompletionSnapshot {
/**
* A unique identifier for the chat completion.
*/
id: string;
/**
* A list of chat completion choices. Can be more than one if `n` is greater
* than 1.
*/
choices: Array<ChatCompletionSnapshot.Choice>;
/**
* The Unix timestamp (in seconds) of when the chat completion was created.
*/
created: number;
/**
* The model to generate the completion.
*/
model: string;
// Note we do not include an "object" type on the snapshot,
// because the object is not a valid "chat.completion" until finalized.
// object: 'chat.completion';
/**
* This fingerprint represents the backend configuration that the model runs with.
*
* Can be used in conjunction with the `seed` request parameter to understand when
* backend changes have been made that might impact determinism.
*/
system_fingerprint?: string;
}
export namespace ChatCompletionSnapshot {
export interface Choice {
/**
* A chat completion delta generated by streamed model responses.
*/
message: Choice.Message;
/**
* The reason the model stopped generating tokens. This will be `stop` if the model
* hit a natural stop point or a provided stop sequence, `length` if the maximum
* number of tokens specified in the request was reached, `content_filter` if
* content was omitted due to a flag from our content filters, or `function_call`
* if the model called a function.
*/
finish_reason: ChatCompletion.Choice['finish_reason'] | null;
/**
* Log probability information for the choice.
*/
logprobs: ChatCompletion.Choice.Logprobs | null;
/**
* The index of the choice in the list of choices.
*/
index: number;
}
export namespace Choice {
/**
* A chat completion delta generated by streamed model responses.
*/
export interface Message {
/**
* The contents of the chunk message.
*/
content?: string | null;
refusal?: string | null;
parsed?: unknown | null;
/**
* The name and arguments of a function that should be called, as generated by the
* model.
*/
function_call?: Message.FunctionCall;
tool_calls?: Array<Message.ToolCall>;
/**
* The role of the author of this message.
*/
role?: ChatCompletionRole;
}
export namespace Message {
export interface ToolCall {
/**
* The ID of the tool call.
*/
id: string;
function: ToolCall.Function;
/**
* The type of the tool.
*/
type: 'function';
}
export namespace ToolCall {
export interface Function {
/**
* The arguments to call the function with, as generated by the model in JSON
* format. Note that the model does not always generate valid JSON, and may
* hallucinate parameters not defined by your function schema. Validate the
* arguments in your code before calling your function.
*/
arguments: string;
parsed_arguments?: unknown;
/**
* The name of the function to call.
*/
name: string;
}
}
/**
* The name and arguments of a function that should be called, as generated by the
* model.
*/
export interface FunctionCall {
/**
* The arguments to call the function with, as generated by the model in JSON
* format. Note that the model does not always generate valid JSON, and may
* hallucinate parameters not defined by your function schema. Validate the
* arguments in your code before calling your function.
*/
arguments?: string;
/**
* The name of the function to call.
*/
name?: string;
}
}
}
}
type AssertIsEmpty<T extends {}> = keyof T extends never ? T : never;
/**
* Ensures the given argument is an empty object, useful for
* asserting that all known properties on an object have been
* destructured.
*/
function assertIsEmpty<T extends {}>(obj: AssertIsEmpty<T>): asserts obj is AssertIsEmpty<T> {
return;
}
function assertNever(_x: never) {}
+72
View File
@@ -0,0 +1,72 @@
import {
type ChatCompletionChunk,
type ChatCompletionCreateParamsStreaming,
} from '../resources/chat/completions';
import { RunnerOptions, type AbstractChatCompletionRunnerEvents } from './AbstractChatCompletionRunner';
import { type ReadableStream } from '../_shims/index';
import { RunnableTools, type BaseFunctionsArgs, type RunnableFunctions } from './RunnableFunction';
import { ChatCompletionSnapshot, ChatCompletionStream } from './ChatCompletionStream';
import OpenAI from '../index';
import { AutoParseableTool } from '../lib/parser';
export interface ChatCompletionStreamEvents extends AbstractChatCompletionRunnerEvents {
content: (contentDelta: string, contentSnapshot: string) => void;
chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void;
}
export type ChatCompletionStreamingFunctionRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsStreaming,
'functions'
> & {
functions: RunnableFunctions<FunctionsArgs>;
};
export type ChatCompletionStreamingToolRunnerParams<FunctionsArgs extends BaseFunctionsArgs> = Omit<
ChatCompletionCreateParamsStreaming,
'tools'
> & {
tools: RunnableTools<FunctionsArgs> | AutoParseableTool<any, true>[];
};
export class ChatCompletionStreamingRunner<ParsedT = null>
extends ChatCompletionStream<ParsedT>
implements AsyncIterable<ChatCompletionChunk>
{
static override fromReadableStream(stream: ReadableStream): ChatCompletionStreamingRunner<null> {
const runner = new ChatCompletionStreamingRunner(null);
runner._run(() => runner._fromReadableStream(stream));
return runner;
}
/** @deprecated - please use `runTools` instead. */
static runFunctions<T extends (string | object)[]>(
client: OpenAI,
params: ChatCompletionStreamingFunctionRunnerParams<T>,
options?: RunnerOptions,
): ChatCompletionStreamingRunner<null> {
const runner = new ChatCompletionStreamingRunner(null);
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' },
};
runner._run(() => runner._runFunctions(client, params, opts));
return runner;
}
static runTools<T extends (string | object)[], ParsedT = null>(
client: OpenAI,
params: ChatCompletionStreamingToolRunnerParams<T>,
options?: RunnerOptions,
): ChatCompletionStreamingRunner<ParsedT> {
const runner = new ChatCompletionStreamingRunner<ParsedT>(
// @ts-expect-error TODO these types are incompatible
params,
);
const opts = {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runTools' },
};
runner._run(() => runner._runTools(client, params, opts));
return runner;
}
}
+98
View File
@@ -0,0 +1,98 @@
type EventListener<Events, EventType extends keyof Events> = Events[EventType];
type EventListeners<Events, EventType extends keyof Events> = Array<{
listener: EventListener<Events, EventType>;
once?: boolean;
}>;
export type EventParameters<Events, EventType extends keyof Events> = {
[Event in EventType]: EventListener<Events, EventType> extends (...args: infer P) => any ? P : never;
}[EventType];
export class EventEmitter<EventTypes extends Record<string, (...args: any) => any>> {
#listeners: {
[Event in keyof EventTypes]?: EventListeners<EventTypes, Event>;
} = {};
/**
* Adds the listener function to the end of the listeners array for the event.
* No checks are made to see if the listener has already been added. Multiple calls passing
* the same combination of event and listener will result in the listener being added, and
* called, multiple times.
* @returns this, so that calls can be chained
*/
on<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener });
return this;
}
/**
* Removes the specified listener from the listener array for the event.
* off() will remove, at most, one instance of a listener from the listener array. If any single
* listener has been added multiple times to the listener array for the specified event, then
* off() must be called multiple times to remove each instance.
* @returns this, so that calls can be chained
*/
off<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners = this.#listeners[event];
if (!listeners) return this;
const index = listeners.findIndex((l) => l.listener === listener);
if (index >= 0) listeners.splice(index, 1);
return this;
}
/**
* Adds a one-time listener function for the event. The next time the event is triggered,
* this listener is removed and then invoked.
* @returns this, so that calls can be chained
*/
once<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener, once: true });
return this;
}
/**
* This is similar to `.once()`, but returns a Promise that resolves the next time
* the event is triggered, instead of calling a listener callback.
* @returns a Promise that resolves the next time given event is triggered,
* or rejects if an error is emitted. (If you request the 'error' event,
* returns a promise that resolves with the error).
*
* Example:
*
* const message = await stream.emitted('message') // rejects if the stream errors
*/
emitted<Event extends keyof EventTypes>(
event: Event,
): Promise<
EventParameters<EventTypes, Event> extends [infer Param] ? Param
: EventParameters<EventTypes, Event> extends [] ? void
: EventParameters<EventTypes, Event>
> {
return new Promise((resolve, reject) => {
// TODO: handle errors
this.once(event, resolve as any);
});
}
protected _emit<Event extends keyof EventTypes>(
this: EventEmitter<EventTypes>,
event: Event,
...args: EventParameters<EventTypes, Event>
) {
const listeners: EventListeners<EventTypes, Event> | undefined = this.#listeners[event];
if (listeners) {
this.#listeners[event] = listeners.filter((l) => !l.once) as any;
listeners.forEach(({ listener }: any) => listener(...(args as any)));
}
}
protected _hasListener(event: keyof EventTypes): boolean {
const listeners = this.#listeners[event];
return listeners && listeners.length > 0;
}
}
+239
View File
@@ -0,0 +1,239 @@
import { APIUserAbortError, OpenAIError } from '../error';
export class EventStream<EventTypes extends BaseEvents> {
controller: AbortController = new AbortController();
#connectedPromise: Promise<void>;
#resolveConnectedPromise: () => void = () => {};
#rejectConnectedPromise: (error: OpenAIError) => void = () => {};
#endPromise: Promise<void>;
#resolveEndPromise: () => void = () => {};
#rejectEndPromise: (error: OpenAIError) => void = () => {};
#listeners: {
[Event in keyof EventTypes]?: EventListeners<EventTypes, Event>;
} = {};
#ended = false;
#errored = false;
#aborted = false;
#catchingPromiseCreated = false;
constructor() {
this.#connectedPromise = new Promise<void>((resolve, reject) => {
this.#resolveConnectedPromise = resolve;
this.#rejectConnectedPromise = reject;
});
this.#endPromise = new Promise<void>((resolve, reject) => {
this.#resolveEndPromise = resolve;
this.#rejectEndPromise = reject;
});
// Don't let these promises cause unhandled rejection errors.
// we will manually cause an unhandled rejection error later
// if the user hasn't registered any error listener or called
// any promise-returning method.
this.#connectedPromise.catch(() => {});
this.#endPromise.catch(() => {});
}
protected _run(this: EventStream<EventTypes>, executor: () => Promise<any>) {
// Unfortunately if we call `executor()` immediately we get runtime errors about
// references to `this` before the `super()` constructor call returns.
setTimeout(() => {
executor().then(() => {
this._emitFinal();
this._emit('end');
}, this.#handleError.bind(this));
}, 0);
}
protected _connected(this: EventStream<EventTypes>) {
if (this.ended) return;
this.#resolveConnectedPromise();
this._emit('connect');
}
get ended(): boolean {
return this.#ended;
}
get errored(): boolean {
return this.#errored;
}
get aborted(): boolean {
return this.#aborted;
}
abort() {
this.controller.abort();
}
/**
* Adds the listener function to the end of the listeners array for the event.
* No checks are made to see if the listener has already been added. Multiple calls passing
* the same combination of event and listener will result in the listener being added, and
* called, multiple times.
* @returns this ChatCompletionStream, so that calls can be chained
*/
on<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener });
return this;
}
/**
* Removes the specified listener from the listener array for the event.
* off() will remove, at most, one instance of a listener from the listener array. If any single
* listener has been added multiple times to the listener array for the specified event, then
* off() must be called multiple times to remove each instance.
* @returns this ChatCompletionStream, so that calls can be chained
*/
off<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners = this.#listeners[event];
if (!listeners) return this;
const index = listeners.findIndex((l) => l.listener === listener);
if (index >= 0) listeners.splice(index, 1);
return this;
}
/**
* Adds a one-time listener function for the event. The next time the event is triggered,
* this listener is removed and then invoked.
* @returns this ChatCompletionStream, so that calls can be chained
*/
once<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
const listeners: EventListeners<EventTypes, Event> =
this.#listeners[event] || (this.#listeners[event] = []);
listeners.push({ listener, once: true });
return this;
}
/**
* This is similar to `.once()`, but returns a Promise that resolves the next time
* the event is triggered, instead of calling a listener callback.
* @returns a Promise that resolves the next time given event is triggered,
* or rejects if an error is emitted. (If you request the 'error' event,
* returns a promise that resolves with the error).
*
* Example:
*
* const message = await stream.emitted('message') // rejects if the stream errors
*/
emitted<Event extends keyof EventTypes>(
event: Event,
): Promise<
EventParameters<EventTypes, Event> extends [infer Param] ? Param
: EventParameters<EventTypes, Event> extends [] ? void
: EventParameters<EventTypes, Event>
> {
return new Promise((resolve, reject) => {
this.#catchingPromiseCreated = true;
if (event !== 'error') this.once('error', reject);
this.once(event, resolve as any);
});
}
async done(): Promise<void> {
this.#catchingPromiseCreated = true;
await this.#endPromise;
}
#handleError(this: EventStream<EventTypes>, error: unknown) {
this.#errored = true;
if (error instanceof Error && error.name === 'AbortError') {
error = new APIUserAbortError();
}
if (error instanceof APIUserAbortError) {
this.#aborted = true;
return this._emit('abort', error);
}
if (error instanceof OpenAIError) {
return this._emit('error', error);
}
if (error instanceof Error) {
const openAIError: OpenAIError = new OpenAIError(error.message);
// @ts-ignore
openAIError.cause = error;
return this._emit('error', openAIError);
}
return this._emit('error', new OpenAIError(String(error)));
}
_emit<Event extends keyof BaseEvents>(event: Event, ...args: EventParameters<BaseEvents, Event>): void;
_emit<Event extends keyof EventTypes>(event: Event, ...args: EventParameters<EventTypes, Event>): void;
_emit<Event extends keyof EventTypes>(
this: EventStream<EventTypes>,
event: Event,
...args: EventParameters<EventTypes, Event>
) {
// make sure we don't emit any events after end
if (this.#ended) {
return;
}
if (event === 'end') {
this.#ended = true;
this.#resolveEndPromise();
}
const listeners: EventListeners<EventTypes, Event> | undefined = this.#listeners[event];
if (listeners) {
this.#listeners[event] = listeners.filter((l) => !l.once) as any;
listeners.forEach(({ listener }: any) => listener(...(args as any)));
}
if (event === 'abort') {
const error = args[0] as APIUserAbortError;
if (!this.#catchingPromiseCreated && !listeners?.length) {
Promise.reject(error);
}
this.#rejectConnectedPromise(error);
this.#rejectEndPromise(error);
this._emit('end');
return;
}
if (event === 'error') {
// NOTE: _emit('error', error) should only be called from #handleError().
const error = args[0] as OpenAIError;
if (!this.#catchingPromiseCreated && !listeners?.length) {
// Trigger an unhandled rejection if the user hasn't registered any error handlers.
// If you are seeing stack traces here, make sure to handle errors via either:
// - runner.on('error', () => ...)
// - await runner.done()
// - await runner.finalChatCompletion()
// - etc.
Promise.reject(error);
}
this.#rejectConnectedPromise(error);
this.#rejectEndPromise(error);
this._emit('end');
}
}
protected _emitFinal(): void {}
}
type EventListener<Events, EventType extends keyof Events> = Events[EventType];
type EventListeners<Events, EventType extends keyof Events> = Array<{
listener: EventListener<Events, EventType>;
once?: boolean;
}>;
export type EventParameters<Events, EventType extends keyof Events> = {
[Event in EventType]: EventListener<Events, EventType> extends (...args: infer P) => any ? P : never;
}[EventType];
export interface BaseEvents {
connect: () => void;
error: (error: OpenAIError) => void;
abort: (error: APIUserAbortError) => void;
end: () => void;
}
+265
View File
@@ -0,0 +1,265 @@
import { OpenAIError } from '../error';
import type { ChatCompletionTool } from '../resources/chat/completions';
import {
ResponseTextConfig,
type FunctionTool,
type ParsedContent,
type ParsedResponse,
type ParsedResponseFunctionToolCall,
type ParsedResponseOutputItem,
type Response,
type ResponseCreateParamsBase,
type ResponseCreateParamsNonStreaming,
type ResponseFunctionToolCall,
type Tool,
} from '../resources/responses/responses';
import { type AutoParseableTextFormat, isAutoParsableResponseFormat } from '../lib/parser';
export type ParseableToolsParams = Array<Tool> | ChatCompletionTool | null;
export type ResponseCreateParamsWithTools = ResponseCreateParamsBase & {
tools?: ParseableToolsParams;
};
type TextConfigParams = { text?: ResponseTextConfig };
export type ExtractParsedContentFromParams<Params extends TextConfigParams> =
NonNullable<Params['text']>['format'] extends AutoParseableTextFormat<infer P> ? P : null;
export function maybeParseResponse<
Params extends ResponseCreateParamsBase | null,
ParsedT = Params extends null ? null : ExtractParsedContentFromParams<NonNullable<Params>>,
>(response: Response, params: Params): ParsedResponse<ParsedT> {
if (!params || !hasAutoParseableInput(params)) {
return {
...response,
output_parsed: null,
output: response.output.map((item) => {
if (item.type === 'function_call') {
return {
...item,
parsed_arguments: null,
};
}
if (item.type === 'message') {
return {
...item,
content: item.content.map((content) => ({
...content,
parsed: null,
})),
};
} else {
return item;
}
}),
};
}
return parseResponse(response, params);
}
export function parseResponse<
Params extends ResponseCreateParamsBase,
ParsedT = ExtractParsedContentFromParams<Params>,
>(response: Response, params: Params): ParsedResponse<ParsedT> {
const output: Array<ParsedResponseOutputItem<ParsedT>> = response.output.map(
(item): ParsedResponseOutputItem<ParsedT> => {
if (item.type === 'function_call') {
return {
...item,
parsed_arguments: parseToolCall(params, item),
};
}
if (item.type === 'message') {
const content: Array<ParsedContent<ParsedT>> = item.content.map((content) => {
if (content.type === 'output_text') {
return {
...content,
parsed: parseTextFormat(params, content.text),
};
}
return content;
});
return {
...item,
content,
};
}
return item;
},
);
const parsed: Omit<ParsedResponse<ParsedT>, 'output_parsed'> = Object.assign({}, response, { output });
if (!Object.getOwnPropertyDescriptor(response, 'output_text')) {
addOutputText(parsed);
}
Object.defineProperty(parsed, 'output_parsed', {
enumerable: true,
get() {
for (const output of parsed.output) {
if (output.type !== 'message') {
continue;
}
for (const content of output.content) {
if (content.type === 'output_text' && content.parsed !== null) {
return content.parsed;
}
}
}
return null;
},
});
return parsed as ParsedResponse<ParsedT>;
}
function parseTextFormat<
Params extends ResponseCreateParamsBase,
ParsedT = ExtractParsedContentFromParams<Params>,
>(params: Params, content: string): ParsedT | null {
if (params.text?.format?.type !== 'json_schema') {
return null;
}
if ('$parseRaw' in params.text?.format) {
const text_format = params.text?.format as unknown as AutoParseableTextFormat<ParsedT>;
return text_format.$parseRaw(content);
}
return JSON.parse(content);
}
export function hasAutoParseableInput(params: ResponseCreateParamsWithTools): boolean {
if (isAutoParsableResponseFormat(params.text?.format)) {
return true;
}
return false;
}
type ToolOptions = {
name: string;
arguments: any;
function?: ((args: any) => any) | undefined;
};
export type AutoParseableResponseTool<
OptionsT extends ToolOptions,
HasFunction = OptionsT['function'] extends Function ? true : false,
> = FunctionTool & {
__arguments: OptionsT['arguments']; // type-level only
__name: OptionsT['name']; // type-level only
$brand: 'auto-parseable-tool';
$callback: ((args: OptionsT['arguments']) => any) | undefined;
$parseRaw(args: string): OptionsT['arguments'];
};
export function makeParseableResponseTool<OptionsT extends ToolOptions>(
tool: FunctionTool,
{
parser,
callback,
}: {
parser: (content: string) => OptionsT['arguments'];
callback: ((args: any) => any) | undefined;
},
): AutoParseableResponseTool<OptionsT['arguments']> {
const obj = { ...tool };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-tool',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
$callback: {
value: callback,
enumerable: false,
},
});
return obj as AutoParseableResponseTool<OptionsT['arguments']>;
}
export function isAutoParsableTool(tool: any): tool is AutoParseableResponseTool<any> {
return tool?.['$brand'] === 'auto-parseable-tool';
}
function getInputToolByName(input_tools: Array<Tool>, name: string): FunctionTool | undefined {
return input_tools.find((tool) => tool.type === 'function' && tool.name === name) as
| FunctionTool
| undefined;
}
function parseToolCall<Params extends ResponseCreateParamsBase>(
params: Params,
toolCall: ResponseFunctionToolCall,
): ParsedResponseFunctionToolCall {
const inputTool = getInputToolByName(params.tools ?? [], toolCall.name);
return {
...toolCall,
...toolCall,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.arguments)
: inputTool?.strict ? JSON.parse(toolCall.arguments)
: null,
};
}
export function shouldParseToolCall(
params: ResponseCreateParamsNonStreaming | null | undefined,
toolCall: ResponseFunctionToolCall,
): boolean {
if (!params) {
return false;
}
const inputTool = getInputToolByName(params.tools ?? [], toolCall.name);
return isAutoParsableTool(inputTool) || inputTool?.strict || false;
}
export function validateInputTools(tools: ChatCompletionTool[] | undefined) {
for (const tool of tools ?? []) {
if (tool.type !== 'function') {
throw new OpenAIError(
`Currently only \`function\` tool types support auto-parsing; Received \`${tool.type}\``,
);
}
if (tool.function.strict !== true) {
throw new OpenAIError(
`The \`${tool.function.name}\` tool is not marked with \`strict: true\`. Only strict function tools can be auto-parsed`,
);
}
}
}
export function addOutputText(rsp: Response): void {
const texts: string[] = [];
for (const output of rsp.output) {
if (output.type !== 'message') {
continue;
}
for (const content of output.content) {
if (content.type === 'output_text') {
texts.push(content.text);
}
}
}
rsp.output_text = texts.join('');
}
+136
View File
@@ -0,0 +1,136 @@
import { type ChatCompletionRunner } from './ChatCompletionRunner';
import { type ChatCompletionStreamingRunner } from './ChatCompletionStreamingRunner';
import { JSONSchema } from './jsonschema';
type PromiseOrValue<T> = T | Promise<T>;
export type RunnableFunctionWithParse<Args extends object> = {
/**
* @param args the return value from `parse`.
* @param runner the runner evaluating this callback.
* @returns a string to send back to OpenAI.
*/
function: (
args: Args,
runner: ChatCompletionRunner<unknown> | ChatCompletionStreamingRunner<unknown>,
) => PromiseOrValue<unknown>;
/**
* @param input the raw args from the OpenAI function call.
* @returns the parsed arguments to pass to `function`
*/
parse: (input: string) => PromiseOrValue<Args>;
/**
* The parameters the function accepts, describes as a JSON Schema object.
*/
parameters: JSONSchema;
/**
* A description of what the function does, used by the model to choose when and how to call the function.
*/
description: string;
/**
* The name of the function to be called. Will default to function.name if omitted.
*/
name?: string | undefined;
strict?: boolean | undefined;
};
export type RunnableFunctionWithoutParse = {
/**
* @param args the raw args from the OpenAI function call.
* @returns a string to send back to OpenAI
*/
function: (
args: string,
runner: ChatCompletionRunner<unknown> | ChatCompletionStreamingRunner<unknown>,
) => PromiseOrValue<unknown>;
/**
* The parameters the function accepts, describes as a JSON Schema object.
*/
parameters: JSONSchema;
/**
* A description of what the function does, used by the model to choose when and how to call the function.
*/
description: string;
/**
* The name of the function to be called. Will default to function.name if omitted.
*/
name?: string | undefined;
strict?: boolean | undefined;
};
export type RunnableFunction<Args extends object | string> =
Args extends string ? RunnableFunctionWithoutParse
: Args extends object ? RunnableFunctionWithParse<Args>
: never;
export type RunnableToolFunction<Args extends object | string> =
Args extends string ? RunnableToolFunctionWithoutParse
: Args extends object ? RunnableToolFunctionWithParse<Args>
: never;
export type RunnableToolFunctionWithoutParse = {
type: 'function';
function: RunnableFunctionWithoutParse;
};
export type RunnableToolFunctionWithParse<Args extends object> = {
type: 'function';
function: RunnableFunctionWithParse<Args>;
};
export function isRunnableFunctionWithParse<Args extends object>(
fn: any,
): fn is RunnableFunctionWithParse<Args> {
return typeof (fn as any).parse === 'function';
}
export type BaseFunctionsArgs = readonly (object | string)[];
export type RunnableFunctions<FunctionsArgs extends BaseFunctionsArgs> =
[any[]] extends [FunctionsArgs] ? readonly RunnableFunction<any>[]
: {
[Index in keyof FunctionsArgs]: Index extends number ? RunnableFunction<FunctionsArgs[Index]>
: FunctionsArgs[Index];
};
export type RunnableTools<FunctionsArgs extends BaseFunctionsArgs> =
[any[]] extends [FunctionsArgs] ? readonly RunnableToolFunction<any>[]
: {
[Index in keyof FunctionsArgs]: Index extends number ? RunnableToolFunction<FunctionsArgs[Index]>
: FunctionsArgs[Index];
};
/**
* This is helper class for passing a `function` and `parse` where the `function`
* argument type matches the `parse` return type.
*
* @deprecated - please use ParsingToolFunction instead.
*/
export class ParsingFunction<Args extends object> {
function: RunnableFunctionWithParse<Args>['function'];
parse: RunnableFunctionWithParse<Args>['parse'];
parameters: RunnableFunctionWithParse<Args>['parameters'];
description: RunnableFunctionWithParse<Args>['description'];
name?: RunnableFunctionWithParse<Args>['name'];
constructor(input: RunnableFunctionWithParse<Args>) {
this.function = input.function;
this.parse = input.parse;
this.parameters = input.parameters;
this.description = input.description;
this.name = input.name;
}
}
/**
* This is helper class for passing a `function` and `parse` where the `function`
* argument type matches the `parse` return type.
*/
export class ParsingToolFunction<Args extends object> {
type: 'function';
function: RunnableFunctionWithParse<Args>;
constructor(input: RunnableFunctionWithParse<Args>) {
this.type = 'function';
this.function = input;
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Like `Promise.allSettled()` but throws an error if any promises are rejected.
*/
export const allSettledWithThrow = async <R>(promises: Promise<R>[]): Promise<R[]> => {
const results = await Promise.allSettled(promises);
const rejected = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
if (rejected.length) {
for (const result of rejected) {
console.error(result.reason);
}
throw new Error(`${rejected.length} promise(s) failed - see the above errors`);
}
// Note: TS was complaining about using `.filter().map()` here for some reason
const values: R[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
values.push(result.value);
}
}
return values;
};
+28
View File
@@ -0,0 +1,28 @@
import {
type ChatCompletionAssistantMessageParam,
type ChatCompletionFunctionMessageParam,
type ChatCompletionMessageParam,
type ChatCompletionToolMessageParam,
} from '../resources';
export const isAssistantMessage = (
message: ChatCompletionMessageParam | null | undefined,
): message is ChatCompletionAssistantMessageParam => {
return message?.role === 'assistant';
};
export const isFunctionMessage = (
message: ChatCompletionMessageParam | null | undefined,
): message is ChatCompletionFunctionMessageParam => {
return message?.role === 'function';
};
export const isToolMessage = (
message: ChatCompletionMessageParam | null | undefined,
): message is ChatCompletionToolMessageParam => {
return message?.role === 'tool';
};
export function isPresent<T>(obj: T | null | undefined): obj is T {
return obj != null;
}
+148
View File
@@ -0,0 +1,148 @@
// File mostly copied from @types/json-schema, but stripped down a bit for brevity
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/817274f3280152ba2929a6067c93df8b34c4c9aa/types/json-schema/index.d.ts
//
// ==================================================================================================
// JSON Schema Draft 07
// ==================================================================================================
// https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
// --------------------------------------------------------------------------------------------------
/**
* Primitive type
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
*/
export type JSONSchemaTypeName =
| ({} & string)
| 'string'
| 'number'
| 'integer'
| 'boolean'
| 'object'
| 'array'
| 'null';
/**
* Primitive type
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
*/
export type JSONSchemaType =
| string //
| number
| boolean
| JSONSchemaObject
| JSONSchemaArray
| null;
// Workaround for infinite type recursion
export interface JSONSchemaObject {
[key: string]: JSONSchemaType;
}
// Workaround for infinite type recursion
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
export interface JSONSchemaArray extends Array<JSONSchemaType> {}
/**
* Meta schema
*
* Recommended values:
* - 'http://json-schema.org/schema#'
* - 'http://json-schema.org/hyper-schema#'
* - 'http://json-schema.org/draft-07/schema#'
* - 'http://json-schema.org/draft-07/hyper-schema#'
*
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5
*/
export type JSONSchemaVersion = string;
/**
* JSON Schema v7
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
*/
export type JSONSchemaDefinition = JSONSchema | boolean;
export interface JSONSchema {
$id?: string | undefined;
$comment?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1
*/
type?: JSONSchemaTypeName | JSONSchemaTypeName[] | undefined;
enum?: JSONSchemaType[] | undefined;
const?: JSONSchemaType | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2
*/
multipleOf?: number | undefined;
maximum?: number | undefined;
exclusiveMaximum?: number | undefined;
minimum?: number | undefined;
exclusiveMinimum?: number | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3
*/
maxLength?: number | undefined;
minLength?: number | undefined;
pattern?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4
*/
items?: JSONSchemaDefinition | JSONSchemaDefinition[] | undefined;
additionalItems?: JSONSchemaDefinition | undefined;
maxItems?: number | undefined;
minItems?: number | undefined;
uniqueItems?: boolean | undefined;
contains?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5
*/
maxProperties?: number | undefined;
minProperties?: number | undefined;
required?: string[] | undefined;
properties?:
| {
[key: string]: JSONSchemaDefinition;
}
| undefined;
patternProperties?:
| {
[key: string]: JSONSchemaDefinition;
}
| undefined;
additionalProperties?: JSONSchemaDefinition | undefined;
propertyNames?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6
*/
if?: JSONSchemaDefinition | undefined;
then?: JSONSchemaDefinition | undefined;
else?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7
*/
allOf?: JSONSchemaDefinition[] | undefined;
anyOf?: JSONSchemaDefinition[] | undefined;
oneOf?: JSONSchemaDefinition[] | undefined;
not?: JSONSchemaDefinition | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7
*/
format?: string | undefined;
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10
*/
title?: string | undefined;
description?: string | undefined;
default?: JSONSchemaType | undefined;
readOnly?: boolean | undefined;
writeOnly?: boolean | undefined;
examples?: JSONSchemaType | undefined;
}
+276
View File
@@ -0,0 +1,276 @@
import {
ChatCompletion,
ChatCompletionCreateParams,
ChatCompletionMessageToolCall,
ChatCompletionTool,
} from '../resources/chat/completions';
import {
ChatCompletionStreamingToolRunnerParams,
ChatCompletionStreamParams,
ChatCompletionToolRunnerParams,
ParsedChatCompletion,
ParsedChoice,
ParsedFunctionToolCall,
} from '../resources/beta/chat/completions';
import { ResponseFormatJSONSchema } from '../resources/shared';
import { ContentFilterFinishReasonError, LengthFinishReasonError, OpenAIError } from '../error';
import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses';
type AnyChatCompletionCreateParams =
| ChatCompletionCreateParams
| ChatCompletionToolRunnerParams<any>
| ChatCompletionStreamingToolRunnerParams<any>
| ChatCompletionStreamParams;
export type ExtractParsedContentFromParams<Params extends AnyChatCompletionCreateParams> =
Params['response_format'] extends AutoParseableResponseFormat<infer P> ? P : null;
export type AutoParseableResponseFormat<ParsedT> = ResponseFormatJSONSchema & {
__output: ParsedT; // type-level only
$brand: 'auto-parseable-response-format';
$parseRaw(content: string): ParsedT;
};
export function makeParseableResponseFormat<ParsedT>(
response_format: ResponseFormatJSONSchema,
parser: (content: string) => ParsedT,
): AutoParseableResponseFormat<ParsedT> {
const obj = { ...response_format };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-response-format',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
});
return obj as AutoParseableResponseFormat<ParsedT>;
}
export type AutoParseableTextFormat<ParsedT> = ResponseFormatTextJSONSchemaConfig & {
__output: ParsedT; // type-level only
$brand: 'auto-parseable-response-format';
$parseRaw(content: string): ParsedT;
};
export function makeParseableTextFormat<ParsedT>(
response_format: ResponseFormatTextJSONSchemaConfig,
parser: (content: string) => ParsedT,
): AutoParseableTextFormat<ParsedT> {
const obj = { ...response_format };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-response-format',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
});
return obj as AutoParseableTextFormat<ParsedT>;
}
export function isAutoParsableResponseFormat<ParsedT>(
response_format: any,
): response_format is AutoParseableResponseFormat<ParsedT> {
return response_format?.['$brand'] === 'auto-parseable-response-format';
}
type ToolOptions = {
name: string;
arguments: any;
function?: ((args: any) => any) | undefined;
};
export type AutoParseableTool<
OptionsT extends ToolOptions,
HasFunction = OptionsT['function'] extends Function ? true : false,
> = ChatCompletionTool & {
__arguments: OptionsT['arguments']; // type-level only
__name: OptionsT['name']; // type-level only
__hasFunction: HasFunction; // type-level only
$brand: 'auto-parseable-tool';
$callback: ((args: OptionsT['arguments']) => any) | undefined;
$parseRaw(args: string): OptionsT['arguments'];
};
export function makeParseableTool<OptionsT extends ToolOptions>(
tool: ChatCompletionTool,
{
parser,
callback,
}: {
parser: (content: string) => OptionsT['arguments'];
callback: ((args: any) => any) | undefined;
},
): AutoParseableTool<OptionsT['arguments']> {
const obj = { ...tool };
Object.defineProperties(obj, {
$brand: {
value: 'auto-parseable-tool',
enumerable: false,
},
$parseRaw: {
value: parser,
enumerable: false,
},
$callback: {
value: callback,
enumerable: false,
},
});
return obj as AutoParseableTool<OptionsT['arguments']>;
}
export function isAutoParsableTool(tool: any): tool is AutoParseableTool<any> {
return tool?.['$brand'] === 'auto-parseable-tool';
}
export function maybeParseChatCompletion<
Params extends ChatCompletionCreateParams | null,
ParsedT = Params extends null ? null : ExtractParsedContentFromParams<NonNullable<Params>>,
>(completion: ChatCompletion, params: Params): ParsedChatCompletion<ParsedT> {
if (!params || !hasAutoParseableInput(params)) {
return {
...completion,
choices: completion.choices.map((choice) => ({
...choice,
message: {
...choice.message,
parsed: null,
...(choice.message.tool_calls ?
{
tool_calls: choice.message.tool_calls,
}
: undefined),
},
})),
};
}
return parseChatCompletion(completion, params);
}
export function parseChatCompletion<
Params extends ChatCompletionCreateParams,
ParsedT = ExtractParsedContentFromParams<Params>,
>(completion: ChatCompletion, params: Params): ParsedChatCompletion<ParsedT> {
const choices: Array<ParsedChoice<ParsedT>> = completion.choices.map((choice): ParsedChoice<ParsedT> => {
if (choice.finish_reason === 'length') {
throw new LengthFinishReasonError();
}
if (choice.finish_reason === 'content_filter') {
throw new ContentFilterFinishReasonError();
}
return {
...choice,
message: {
...choice.message,
...(choice.message.tool_calls ?
{
tool_calls:
choice.message.tool_calls?.map((toolCall) => parseToolCall(params, toolCall)) ?? undefined,
}
: undefined),
parsed:
choice.message.content && !choice.message.refusal ?
parseResponseFormat(params, choice.message.content)
: null,
},
};
});
return { ...completion, choices };
}
function parseResponseFormat<
Params extends ChatCompletionCreateParams,
ParsedT = ExtractParsedContentFromParams<Params>,
>(params: Params, content: string): ParsedT | null {
if (params.response_format?.type !== 'json_schema') {
return null;
}
if (params.response_format?.type === 'json_schema') {
if ('$parseRaw' in params.response_format) {
const response_format = params.response_format as AutoParseableResponseFormat<ParsedT>;
return response_format.$parseRaw(content);
}
return JSON.parse(content);
}
return null;
}
function parseToolCall<Params extends ChatCompletionCreateParams>(
params: Params,
toolCall: ChatCompletionMessageToolCall,
): ParsedFunctionToolCall {
const inputTool = params.tools?.find((inputTool) => inputTool.function?.name === toolCall.function.name);
return {
...toolCall,
function: {
...toolCall.function,
parsed_arguments:
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.function.arguments)
: inputTool?.function.strict ? JSON.parse(toolCall.function.arguments)
: null,
},
};
}
export function shouldParseToolCall(
params: ChatCompletionCreateParams | null | undefined,
toolCall: ChatCompletionMessageToolCall,
): boolean {
if (!params) {
return false;
}
const inputTool = params.tools?.find((inputTool) => inputTool.function?.name === toolCall.function.name);
return isAutoParsableTool(inputTool) || inputTool?.function.strict || false;
}
export function hasAutoParseableInput(params: AnyChatCompletionCreateParams): boolean {
if (isAutoParsableResponseFormat(params.response_format)) {
return true;
}
return (
params.tools?.some(
(t) => isAutoParsableTool(t) || (t.type === 'function' && t.function.strict === true),
) ?? false
);
}
export function validateInputTools(tools: ChatCompletionTool[] | undefined) {
for (const tool of tools ?? []) {
if (tool.type !== 'function') {
throw new OpenAIError(
`Currently only \`function\` tool types support auto-parsing; Received \`${tool.type}\``,
);
}
if (tool.function.strict !== true) {
throw new OpenAIError(
`The \`${tool.function.name}\` tool is not marked with \`strict: true\`. Only strict function tools can be auto-parsed`,
);
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import {
ResponseAudioDeltaEvent,
ResponseAudioDoneEvent,
ResponseAudioTranscriptDeltaEvent,
ResponseAudioTranscriptDoneEvent,
ResponseCodeInterpreterCallCodeDeltaEvent,
ResponseCodeInterpreterCallCodeDoneEvent,
ResponseCodeInterpreterCallCompletedEvent,
ResponseCodeInterpreterCallInProgressEvent,
ResponseCodeInterpreterCallInterpretingEvent,
ResponseCompletedEvent,
ResponseContentPartAddedEvent,
ResponseContentPartDoneEvent,
ResponseCreatedEvent,
ResponseErrorEvent,
ResponseFailedEvent,
ResponseFileSearchCallCompletedEvent,
ResponseFileSearchCallInProgressEvent,
ResponseFileSearchCallSearchingEvent,
ResponseFunctionCallArgumentsDeltaEvent as RawResponseFunctionCallArgumentsDeltaEvent,
ResponseFunctionCallArgumentsDoneEvent,
ResponseInProgressEvent,
ResponseOutputItemAddedEvent,
ResponseOutputItemDoneEvent,
ResponseRefusalDeltaEvent,
ResponseRefusalDoneEvent,
ResponseTextDeltaEvent as RawResponseTextDeltaEvent,
ResponseTextDoneEvent,
ResponseIncompleteEvent,
ResponseWebSearchCallCompletedEvent,
ResponseWebSearchCallInProgressEvent,
ResponseWebSearchCallSearchingEvent,
} from '../../resources/responses/responses';
export type ResponseFunctionCallArgumentsDeltaEvent = RawResponseFunctionCallArgumentsDeltaEvent & {
snapshot: string;
};
export type ResponseTextDeltaEvent = RawResponseTextDeltaEvent & {
snapshot: string;
};
export type ParsedResponseStreamEvent =
| ResponseAudioDeltaEvent
| ResponseAudioDoneEvent
| ResponseAudioTranscriptDeltaEvent
| ResponseAudioTranscriptDoneEvent
| ResponseCodeInterpreterCallCodeDeltaEvent
| ResponseCodeInterpreterCallCodeDoneEvent
| ResponseCodeInterpreterCallCompletedEvent
| ResponseCodeInterpreterCallInProgressEvent
| ResponseCodeInterpreterCallInterpretingEvent
| ResponseCompletedEvent
| ResponseContentPartAddedEvent
| ResponseContentPartDoneEvent
| ResponseCreatedEvent
| ResponseErrorEvent
| ResponseFileSearchCallCompletedEvent
| ResponseFileSearchCallInProgressEvent
| ResponseFileSearchCallSearchingEvent
| ResponseFunctionCallArgumentsDeltaEvent
| ResponseFunctionCallArgumentsDoneEvent
| ResponseInProgressEvent
| ResponseFailedEvent
| ResponseIncompleteEvent
| ResponseOutputItemAddedEvent
| ResponseOutputItemDoneEvent
| ResponseRefusalDeltaEvent
| ResponseRefusalDoneEvent
| ResponseTextDeltaEvent
| ResponseTextDoneEvent
| ResponseWebSearchCallCompletedEvent
| ResponseWebSearchCallInProgressEvent
| ResponseWebSearchCallSearchingEvent;
+344
View File
@@ -0,0 +1,344 @@
import {
ResponseTextConfig,
type ParsedResponse,
type Response,
type ResponseCreateParamsBase,
type ResponseCreateParamsStreaming,
type ResponseStreamEvent,
} from '../../resources/responses/responses';
import * as Core from '../../core';
import { APIUserAbortError, OpenAIError } from '../../error';
import OpenAI from '../../index';
import { type BaseEvents, EventStream } from '../EventStream';
import { type ResponseFunctionCallArgumentsDeltaEvent, type ResponseTextDeltaEvent } from './EventTypes';
import { maybeParseResponse, ParseableToolsParams } from '../ResponsesParser';
import { Stream } from "../../streaming";
export type ResponseStreamParams = ResponseCreateAndStreamParams | ResponseStreamByIdParams;
export type ResponseCreateAndStreamParams = Omit<ResponseCreateParamsBase, 'stream'> & {
stream?: true;
};
export type ResponseStreamByIdParams = {
/**
* The ID of the response to stream.
*/
response_id: string;
/**
* If provided, the stream will start after the event with the given sequence number.
*/
starting_after?: number;
/**
* Configuration options for a text response from the model. Can be plain text or
* structured JSON data. Learn more:
*
* - [Text inputs and outputs](https://platform.openai.com/docs/guides/text)
* - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)
*/
text?: ResponseTextConfig;
/**
* An array of tools the model may call while generating a response. When continuing a stream, provide
* the same tools as the original request.
*/
tools?: ParseableToolsParams;
};
type ResponseEvents = BaseEvents &
Omit<
{
[K in ResponseStreamEvent['type']]: (event: Extract<ResponseStreamEvent, { type: K }>) => void;
},
'response.output_text.delta' | 'response.function_call_arguments.delta'
> & {
event: (event: ResponseStreamEvent) => void;
'response.output_text.delta': (event: ResponseTextDeltaEvent) => void;
'response.function_call_arguments.delta': (event: ResponseFunctionCallArgumentsDeltaEvent) => void;
};
export type ResponseStreamingParams = Omit<ResponseCreateParamsBase, 'stream'> & {
stream?: true;
};
export class ResponseStream<ParsedT = null>
extends EventStream<ResponseEvents>
implements AsyncIterable<ResponseStreamEvent>
{
#params: ResponseStreamingParams | null;
#currentResponseSnapshot: Response | undefined;
#finalResponse: ParsedResponse<ParsedT> | undefined;
constructor(params: ResponseStreamingParams | null) {
super();
this.#params = params;
}
static createResponse<ParsedT>(
client: OpenAI,
params: ResponseStreamParams,
options?: Core.RequestOptions,
): ResponseStream<ParsedT> {
const runner = new ResponseStream<ParsedT>(params as ResponseCreateParamsStreaming);
runner._run(() =>
runner._createOrRetrieveResponse(client, params, {
...options,
headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' },
}),
);
return runner;
}
#beginRequest() {
if (this.ended) return;
this.#currentResponseSnapshot = undefined;
}
#addEvent(this: ResponseStream<ParsedT>, event: ResponseStreamEvent, starting_after: number | null) {
if (this.ended) return;
const maybeEmit = (name: string, event: ResponseStreamEvent & { snapshot?: string }) => {
if (starting_after == null || event.sequence_number > starting_after) {
this._emit(name as any, event);
}
};
const response = this.#accumulateResponse(event);
maybeEmit('event', event);
switch (event.type) {
case 'response.output_text.delta': {
const output = response.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
const content = output.content[event.content_index];
if (!content) {
throw new OpenAIError(`missing content at index ${event.content_index}`);
}
if (content.type !== 'output_text') {
throw new OpenAIError(`expected content to be 'output_text', got ${content.type}`);
}
maybeEmit('response.output_text.delta', {
...event,
snapshot: content.text,
});
}
break;
}
case 'response.function_call_arguments.delta': {
const output = response.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'function_call') {
maybeEmit('response.function_call_arguments.delta', {
...event,
snapshot: output.arguments,
});
}
break;
}
default:
maybeEmit(event.type, event);
break;
}
}
#endRequest(): ParsedResponse<ParsedT> {
if (this.ended) {
throw new OpenAIError(`stream has ended, this shouldn't happen`);
}
const snapshot = this.#currentResponseSnapshot;
if (!snapshot) {
throw new OpenAIError(`request ended without sending any events`);
}
this.#currentResponseSnapshot = undefined;
const parsedResponse = finalizeResponse<ParsedT>(snapshot, this.#params);
this.#finalResponse = parsedResponse;
return parsedResponse;
}
protected async _createOrRetrieveResponse(
client: OpenAI,
params: ResponseStreamParams,
options?: Core.RequestOptions,
): Promise<ParsedResponse<ParsedT>> {
const signal = options?.signal;
if (signal) {
if (signal.aborted) this.controller.abort();
signal.addEventListener('abort', () => this.controller.abort());
}
this.#beginRequest();
let stream: Stream<ResponseStreamEvent> | undefined;
let starting_after: number | null = null;
if ('response_id' in params) {
stream = await client.responses.retrieve(
params.response_id,
{ stream: true },
{ ...options, signal: this.controller.signal, stream: true },
);
starting_after = params.starting_after ?? null;
} else {
stream = await client.responses.create(
{ ...params, stream: true },
{ ...options, signal: this.controller.signal },
);
}
this._connected();
for await (const event of stream) {
this.#addEvent(event, starting_after);
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
}
return this.#endRequest();
}
#accumulateResponse(event: ResponseStreamEvent): Response {
let snapshot = this.#currentResponseSnapshot;
if (!snapshot) {
if (event.type !== 'response.created') {
throw new OpenAIError(
`When snapshot hasn't been set yet, expected 'response.created' event, got ${event.type}`,
);
}
snapshot = this.#currentResponseSnapshot = event.response;
return snapshot;
}
switch (event.type) {
case 'response.output_item.added': {
snapshot.output.push(event.item);
break;
}
case 'response.content_part.added': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
output.content.push(event.part);
}
break;
}
case 'response.output_text.delta': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'message') {
const content = output.content[event.content_index];
if (!content) {
throw new OpenAIError(`missing content at index ${event.content_index}`);
}
if (content.type !== 'output_text') {
throw new OpenAIError(`expected content to be 'output_text', got ${content.type}`);
}
content.text += event.delta;
}
break;
}
case 'response.function_call_arguments.delta': {
const output = snapshot.output[event.output_index];
if (!output) {
throw new OpenAIError(`missing output at index ${event.output_index}`);
}
if (output.type === 'function_call') {
output.arguments += event.delta;
}
break;
}
case 'response.completed': {
this.#currentResponseSnapshot = event.response;
break;
}
}
return snapshot;
}
[Symbol.asyncIterator](this: ResponseStream<ParsedT>): AsyncIterator<ResponseStreamEvent> {
const pushQueue: ResponseStreamEvent[] = [];
const readQueue: {
resolve: (event: ResponseStreamEvent | undefined) => void;
reject: (err: unknown) => void;
}[] = [];
let done = false;
this.on('event', (event) => {
const reader = readQueue.shift();
if (reader) {
reader.resolve(event);
} else {
pushQueue.push(event);
}
});
this.on('end', () => {
done = true;
for (const reader of readQueue) {
reader.resolve(undefined);
}
readQueue.length = 0;
});
this.on('abort', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
this.on('error', (err) => {
done = true;
for (const reader of readQueue) {
reader.reject(err);
}
readQueue.length = 0;
});
return {
next: async (): Promise<IteratorResult<ResponseStreamEvent>> => {
if (!pushQueue.length) {
if (done) {
return { value: undefined, done: true };
}
return new Promise<ResponseStreamEvent | undefined>((resolve, reject) =>
readQueue.push({ resolve, reject }),
).then((event) => (event ? { value: event, done: false } : { value: undefined, done: true }));
}
const event = pushQueue.shift()!;
return { value: event, done: false };
},
return: async () => {
this.abort();
return { value: undefined, done: true };
},
};
}
/**
* @returns a promise that resolves with the final Response, or rejects
* if an error occurred or the stream ended prematurely without producing a REsponse.
*/
async finalResponse(): Promise<ParsedResponse<ParsedT>> {
await this.done();
const response = this.#finalResponse;
if (!response) throw new OpenAIError('stream ended without producing a ChatCompletion');
return response;
}
}
function finalizeResponse<ParsedT>(
snapshot: Response,
params: ResponseStreamingParams | null,
): ParsedResponse<ParsedT> {
return maybeParseResponse(snapshot, params);
}

Some files were not shown because too many files have changed in this diff Show More