import { VERSION } from './version'; import { Stream } from './streaming'; import { OpenAIError, APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, } from './error'; import { kind as shimsKind, type Readable, getDefaultAgent, type Agent, fetch, type RequestInfo, type RequestInit, type Response, type HeadersInit, } from './_shims/index'; export { type Response }; import { isMultipartBody } from './uploads'; export { maybeMultipartFormRequestOptions, multipartFormRequestOptions, createForm, type Uploadable, } from './uploads'; export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise; type PromiseOrValue = T | Promise; type APIResponseProps = { response: Response; options: FinalRequestOptions; controller: AbortController; }; async function defaultParseResponse(props: APIResponseProps): Promise { const { response } = props; if (props.options.stream) { debug('response', response.status, response.url, response.headers, response.body); // Note: there is an invariant here that isn't represented in the type system // that if you set `stream: true` the response type must also be `Stream` if (props.options.__streamClass) { return props.options.__streamClass.fromSSEResponse(response, props.controller) as any; } return Stream.fromSSEResponse(response, props.controller) as any; } // fetch refuses to read the body when the status code is 204. if (response.status === 204) { return null as T; } if (props.options.__binaryResponse) { return response as unknown as T; } const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { const json = await response.json(); debug('response', response.status, response.url, response.headers, json); return json as T; } const text = await response.text(); debug('response', response.status, response.url, response.headers, text); // TODO handle blob, arraybuffer, other content types, etc. return text as unknown as T; } /** * A subclass of `Promise` providing additional helper methods * for interacting with the SDK. */ export class APIPromise extends Promise { private parsedPromise: Promise | undefined; constructor( private responsePromise: Promise, private parseResponse: (props: APIResponseProps) => PromiseOrValue = defaultParseResponse, ) { super((resolve) => { // this is maybe a bit weird but this has to be a no-op to not implicitly // parse the response body; instead .then, .catch, .finally are overridden // to parse the response resolve(null as any); }); } _thenUnwrap(transform: (data: T) => U): APIPromise { return new APIPromise(this.responsePromise, async (props) => transform(await this.parseResponse(props))); } /** * Gets the raw `Response` instance instead of parsing the response * data. * * If you want to parse the response body but still get the `Response` * instance, you can use {@link withResponse()}. * * 👋 Getting the wrong TypeScript type for `Response`? * Try setting `"moduleResolution": "NodeNext"` if you can, * or 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) */ asResponse(): Promise { return this.responsePromise.then((p) => p.response); } /** * Gets the parsed response data and the raw `Response` instance. * * If you just want to get the raw `Response` instance without parsing it, * you can use {@link asResponse()}. * * * 👋 Getting the wrong TypeScript type for `Response`? * Try setting `"moduleResolution": "NodeNext"` if you can, * or 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) */ async withResponse(): Promise<{ data: T; response: Response }> { const [data, response] = await Promise.all([this.parse(), this.asResponse()]); return { data, response }; } private parse(): Promise { if (!this.parsedPromise) { this.parsedPromise = this.responsePromise.then(this.parseResponse); } return this.parsedPromise; } override then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null, ): Promise { return this.parse().then(onfulfilled, onrejected); } override catch( onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null, ): Promise { return this.parse().catch(onrejected); } override finally(onfinally?: (() => void) | undefined | null): Promise { return this.parse().finally(onfinally); } } export abstract class APIClient { baseURL: string; maxRetries: number; timeout: number; httpAgent: Agent | undefined; private fetch: Fetch; protected idempotencyHeader?: string; constructor({ baseURL, maxRetries = 2, timeout = 600000, // 10 minutes httpAgent, fetch: overridenFetch, }: { baseURL: string; maxRetries?: number | undefined; timeout: number | undefined; httpAgent: Agent | undefined; fetch: Fetch | undefined; }) { this.baseURL = baseURL; this.maxRetries = validatePositiveInteger('maxRetries', maxRetries); this.timeout = validatePositiveInteger('timeout', timeout); this.httpAgent = httpAgent; this.fetch = overridenFetch ?? fetch; } protected authHeaders(opts: FinalRequestOptions): Headers { return {}; } /** * Override this to add your own default headers, for example: * * { * ...super.defaultHeaders(), * Authorization: 'Bearer 123', * } */ protected defaultHeaders(opts: FinalRequestOptions): Headers { return { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': this.getUserAgent(), ...getPlatformHeaders(), ...this.authHeaders(opts), }; } protected abstract defaultQuery(): DefaultQuery | undefined; /** * Override this to add your own headers validation: */ protected validateHeaders(headers: Headers, customHeaders: Headers) {} protected defaultIdempotencyKey(): string { return `stainless-node-retry-${uuid4()}`; } get(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('get', path, opts); } post(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('post', path, opts); } patch(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('patch', path, opts); } put(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('put', path, opts); } delete(path: string, opts?: PromiseOrValue>): APIPromise { return this.methodRequest('delete', path, opts); } private methodRequest( method: HTTPMethod, path: string, opts?: PromiseOrValue>, ): APIPromise { return this.request(Promise.resolve(opts).then((opts) => ({ method, path, ...opts }))); } getAPIList = AbstractPage>( path: string, Page: new (...args: any[]) => PageClass, opts?: RequestOptions, ): PagePromise { return this.requestAPIList(Page, { method: 'get', path, ...opts }); } private calculateContentLength(body: unknown): string | null { if (typeof body === 'string') { if (typeof Buffer !== 'undefined') { return Buffer.byteLength(body, 'utf8').toString(); } if (typeof TextEncoder !== 'undefined') { const encoder = new TextEncoder(); const encoded = encoder.encode(body); return encoded.length.toString(); } } return null; } buildRequest(options: FinalRequestOptions): { req: RequestInit; url: string; timeout: number } { const { method, path, query, headers: headers = {} } = options; const body = isMultipartBody(options.body) ? options.body.body : options.body ? JSON.stringify(options.body, null, 2) : null; const contentLength = this.calculateContentLength(body); const url = this.buildURL(path!, query); if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); const timeout = options.timeout ?? this.timeout; const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url); const minAgentTimeout = timeout + 1000; if ( typeof (httpAgent as any)?.options?.timeout === 'number' && minAgentTimeout > ((httpAgent as any).options.timeout ?? 0) ) { // Allow any given request to bump our agent active socket timeout. // This may seem strange, but leaking active sockets should be rare and not particularly problematic, // and without mutating agent we would need to create more of them. // This tradeoff optimizes for performance. (httpAgent as any).options.timeout = minAgentTimeout; } if (this.idempotencyHeader && method !== 'get') { if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey(); headers[this.idempotencyHeader] = options.idempotencyKey; } const reqHeaders = this.buildHeaders({ options, headers, contentLength }); const req: RequestInit = { method, ...(body && { body: body as any }), headers: reqHeaders, ...(httpAgent && { agent: httpAgent }), // @ts-ignore node-fetch uses a custom AbortSignal type that is // not compatible with standard web types signal: options.signal ?? null, }; return { req, url, timeout }; } private buildHeaders({ options, headers, contentLength, }: { options: FinalRequestOptions; headers: Record; contentLength: string | null | undefined; }): Record { const reqHeaders: Record = {}; if (contentLength) { reqHeaders['content-length'] = contentLength; } const defaultHeaders = this.defaultHeaders(options); applyHeadersMut(reqHeaders, defaultHeaders); applyHeadersMut(reqHeaders, headers); // let builtin fetch set the Content-Type for multipart bodies if (isMultipartBody(options.body) && shimsKind !== 'node') { delete reqHeaders['content-type']; } this.validateHeaders(reqHeaders, headers); return reqHeaders; } /** * Used as a callback for mutating the given `FinalRequestOptions` object. */ protected async prepareOptions(options: FinalRequestOptions): Promise {} /** * Used as a callback for mutating the given `RequestInit` object. * * This is useful for cases where you want to add certain headers based off of * the request properties, e.g. `method` or `url`. */ protected async prepareRequest( request: RequestInit, { url, options }: { url: string; options: FinalRequestOptions }, ): Promise {} protected parseHeaders(headers: HeadersInit | null | undefined): Record { return ( !headers ? {} : Symbol.iterator in headers ? Object.fromEntries(Array.from(headers as Iterable).map((header) => [...header])) : { ...headers } ); } protected makeStatusError( status: number | undefined, error: Object | undefined, message: string | undefined, headers: Headers | undefined, ) { return APIError.generate(status, error, message, headers); } request( options: PromiseOrValue>, remainingRetries: number | null = null, ): APIPromise { return new APIPromise(this.makeRequest(options, remainingRetries)); } private async makeRequest( optionsInput: PromiseOrValue>, retriesRemaining: number | null, ): Promise { const options = await optionsInput; if (retriesRemaining == null) { retriesRemaining = options.maxRetries ?? this.maxRetries; } await this.prepareOptions(options); const { req, url, timeout } = this.buildRequest(options); await this.prepareRequest(req, { url, options }); debug('request', url, options, req.headers); if (options.signal?.aborted) { throw new APIUserAbortError(); } const controller = new AbortController(); const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError); if (response instanceof Error) { if (options.signal?.aborted) { throw new APIUserAbortError(); } if (retriesRemaining) { return this.retryRequest(options, retriesRemaining); } if (response.name === 'AbortError') { throw new APIConnectionTimeoutError(); } throw new APIConnectionError({ cause: response }); } const responseHeaders = createResponseHeaders(response.headers); if (!response.ok) { if (retriesRemaining && this.shouldRetry(response)) { const retryMessage = `retrying, ${retriesRemaining} attempts remaining`; debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders); return this.retryRequest(options, retriesRemaining, responseHeaders); } const errText = await response.text().catch((e) => castToError(e).message); const errJSON = safeJSON(errText); const errMessage = errJSON ? undefined : errText; const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`; debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders, errMessage); const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders); throw err; } return { response, options, controller }; } requestAPIList = AbstractPage>( Page: new (...args: ConstructorParameters) => PageClass, options: FinalRequestOptions, ): PagePromise { const request = this.makeRequest(options, null); return new PagePromise(this, request, Page); } buildURL(path: string, query: Req | null | undefined): string { const url = isAbsoluteURL(path) ? new URL(path) : new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); const defaultQuery = this.defaultQuery(); if (!isEmptyObj(defaultQuery)) { query = { ...defaultQuery, ...query } as Req; } if (typeof query === 'object' && query && !Array.isArray(query)) { url.search = this.stringifyQuery(query as Record); } return url.toString(); } protected stringifyQuery(query: Record): string { return Object.entries(query) .filter(([_, value]) => typeof value !== 'undefined') .map(([key, value]) => { if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; } if (value === null) { return `${encodeURIComponent(key)}=`; } throw new OpenAIError( `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, ); }) .join('&'); } async fetchWithTimeout( url: RequestInfo, init: RequestInit | undefined, ms: number, controller: AbortController, ): Promise { const { signal, ...options } = init || {}; if (signal) signal.addEventListener('abort', () => controller.abort()); const timeout = setTimeout(() => controller.abort(), ms); return ( this.getRequestClient() // use undefined this binding; fetch errors if bound to something else in browser/cloudflare .fetch.call(undefined, url, { signal: controller.signal as any, ...options }) .finally(() => { clearTimeout(timeout); }) ); } protected getRequestClient(): RequestClient { return { fetch: this.fetch }; } private shouldRetry(response: Response): boolean { // Note this is not a standard header. const shouldRetryHeader = response.headers.get('x-should-retry'); // If the server explicitly says whether or not to retry, obey. if (shouldRetryHeader === 'true') return true; if (shouldRetryHeader === 'false') return false; // Retry on request timeouts. if (response.status === 408) return true; // Retry on lock timeouts. if (response.status === 409) return true; // Retry on rate limits. if (response.status === 429) return true; // Retry internal errors. if (response.status >= 500) return true; return false; } private async retryRequest( options: FinalRequestOptions, retriesRemaining: number, responseHeaders?: Headers | undefined, ): Promise { let timeoutMillis: number | undefined; // Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it. const retryAfterMillisHeader = responseHeaders?.['retry-after-ms']; if (retryAfterMillisHeader) { const timeoutMs = parseFloat(retryAfterMillisHeader); if (!Number.isNaN(timeoutMs)) { timeoutMillis = timeoutMs; } } // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After const retryAfterHeader = responseHeaders?.['retry-after']; if (retryAfterHeader && !timeoutMillis) { const timeoutSeconds = parseFloat(retryAfterHeader); if (!Number.isNaN(timeoutSeconds)) { timeoutMillis = timeoutSeconds * 1000; } else { timeoutMillis = Date.parse(retryAfterHeader) - Date.now(); } } // If the API asks us to wait a certain amount of time (and it's a reasonable amount), // just do what it says, but otherwise calculate a default if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) { const maxRetries = options.maxRetries ?? this.maxRetries; timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); } await sleep(timeoutMillis); return this.makeRequest(options, retriesRemaining - 1); } private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number { const initialRetryDelay = 0.5; const maxRetryDelay = 8.0; const numRetries = maxRetries - retriesRemaining; // Apply exponential backoff, but not more than the max. const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay); // Apply some jitter, take up to at most 25 percent of the retry time. const jitter = 1 - Math.random() * 0.25; return sleepSeconds * jitter * 1000; } private getUserAgent(): string { return `${this.constructor.name}/JS ${VERSION}`; } } export type PageInfo = { url: URL } | { params: Record | null }; export abstract class AbstractPage implements AsyncIterable { #client: APIClient; protected options: FinalRequestOptions; protected response: Response; protected body: unknown; constructor(client: APIClient, response: Response, body: unknown, options: FinalRequestOptions) { this.#client = client; this.options = options; this.response = response; this.body = body; } /** * @deprecated Use nextPageInfo instead */ abstract nextPageParams(): Partial> | null; abstract nextPageInfo(): PageInfo | null; abstract getPaginatedItems(): Item[]; hasNextPage(): boolean { const items = this.getPaginatedItems(); if (!items.length) return false; return this.nextPageInfo() != null; } async getNextPage(): Promise { const nextInfo = this.nextPageInfo(); if (!nextInfo) { throw new OpenAIError( 'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.', ); } const nextOptions = { ...this.options }; if ('params' in nextInfo && typeof nextOptions.query === 'object') { nextOptions.query = { ...nextOptions.query, ...nextInfo.params }; } else if ('url' in nextInfo) { const params = [...Object.entries(nextOptions.query || {}), ...nextInfo.url.searchParams.entries()]; for (const [key, value] of params) { nextInfo.url.searchParams.set(key, value as any); } nextOptions.query = undefined; nextOptions.path = nextInfo.url.toString(); } return await this.#client.requestAPIList(this.constructor as any, nextOptions); } async *iterPages() { // eslint-disable-next-line @typescript-eslint/no-this-alias let page: AbstractPage = this; yield page; while (page.hasNextPage()) { page = await page.getNextPage(); yield page; } } async *[Symbol.asyncIterator]() { for await (const page of this.iterPages()) { for (const item of page.getPaginatedItems()) { yield item; } } } } /** * This subclass of Promise will resolve to an instantiated Page once the request completes. * * It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg: * * for await (const item of client.items.list()) { * console.log(item) * } */ export class PagePromise< PageClass extends AbstractPage, Item = ReturnType[number], > extends APIPromise implements AsyncIterable { constructor( client: APIClient, request: Promise, Page: new (...args: ConstructorParameters) => PageClass, ) { super( request, async (props) => new Page(client, props.response, await defaultParseResponse(props), props.options), ); } /** * Allow auto-paginating iteration on an unawaited list call, eg: * * for await (const item of client.items.list()) { * console.log(item) * } */ async *[Symbol.asyncIterator]() { const page = await this; for await (const item of page) { yield item; } } } export const createResponseHeaders = ( headers: Awaited>['headers'], ): Record => { return new Proxy( Object.fromEntries( // @ts-ignore headers.entries(), ), { get(target, name) { const key = name.toString(); return target[key.toLowerCase()] || target[key]; }, }, ); }; type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; export type RequestClient = { fetch: Fetch }; export type Headers = Record; export type DefaultQuery = Record; export type KeysEnum = { [P in keyof Required]: true }; export type RequestOptions | Readable> = { method?: HTTPMethod; path?: string; query?: Req | undefined; body?: Req | null | undefined; headers?: Headers | undefined; maxRetries?: number; stream?: boolean | undefined; timeout?: number; httpAgent?: Agent; signal?: AbortSignal | undefined | null; idempotencyKey?: string; __binaryResponse?: boolean | undefined; __streamClass?: typeof Stream; }; // This is required so that we can determine if a given object matches the RequestOptions // type at runtime. While this requires duplication, it is enforced by the TypeScript // compiler such that any missing / extraneous keys will cause an error. const requestOptionsKeys: KeysEnum = { method: true, path: true, query: true, body: true, headers: true, maxRetries: true, stream: true, timeout: true, httpAgent: true, signal: true, idempotencyKey: true, __binaryResponse: true, __streamClass: true, }; export const isRequestOptions = (obj: unknown): obj is RequestOptions => { return ( typeof obj === 'object' && obj !== null && !isEmptyObj(obj) && Object.keys(obj).every((k) => hasOwn(requestOptionsKeys, k)) ); }; export type FinalRequestOptions | Readable> = RequestOptions & { method: HTTPMethod; path: string; }; declare const Deno: any; declare const EdgeRuntime: any; type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown'; type PlatformName = | 'MacOS' | 'Linux' | 'Windows' | 'FreeBSD' | 'OpenBSD' | 'iOS' | 'Android' | `Other:${string}` | 'Unknown'; type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari'; type PlatformProperties = { 'X-Stainless-Lang': 'js'; 'X-Stainless-Package-Version': string; 'X-Stainless-OS': PlatformName; 'X-Stainless-Arch': Arch; 'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown'; 'X-Stainless-Runtime-Version': string; }; const getPlatformProperties = (): PlatformProperties => { if (typeof Deno !== 'undefined' && Deno.build != null) { return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, 'X-Stainless-OS': normalizePlatform(Deno.build.os), 'X-Stainless-Arch': normalizeArch(Deno.build.arch), 'X-Stainless-Runtime': 'deno', 'X-Stainless-Runtime-Version': Deno.version, }; } if (typeof EdgeRuntime !== 'undefined') { return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, 'X-Stainless-OS': 'Unknown', 'X-Stainless-Arch': `other:${EdgeRuntime}`, 'X-Stainless-Runtime': 'edge', 'X-Stainless-Runtime-Version': process.version, }; } // Check if Node.js if (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]') { return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, 'X-Stainless-OS': normalizePlatform(process.platform), 'X-Stainless-Arch': normalizeArch(process.arch), 'X-Stainless-Runtime': 'node', 'X-Stainless-Runtime-Version': process.version, }; } const browserInfo = getBrowserInfo(); if (browserInfo) { return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, 'X-Stainless-OS': 'Unknown', 'X-Stainless-Arch': 'unknown', 'X-Stainless-Runtime': `browser:${browserInfo.browser}`, 'X-Stainless-Runtime-Version': browserInfo.version, }; } // TODO add support for Cloudflare workers, etc. return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, 'X-Stainless-OS': 'Unknown', 'X-Stainless-Arch': 'unknown', 'X-Stainless-Runtime': 'unknown', 'X-Stainless-Runtime-Version': 'unknown', }; }; type BrowserInfo = { browser: Browser; version: string; }; declare const navigator: { userAgent: string } | undefined; // Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts function getBrowserInfo(): BrowserInfo | null { if (typeof navigator === 'undefined' || !navigator) { return null; } // NOTE: The order matters here! const browserPatterns = [ { key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, { key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, { key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ }, { key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, { key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, { key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ }, ]; // Find the FIRST matching browser for (const { key, pattern } of browserPatterns) { const match = pattern.exec(navigator.userAgent); if (match) { const major = match[1] || 0; const minor = match[2] || 0; const patch = match[3] || 0; return { browser: key, version: `${major}.${minor}.${patch}` }; } } return null; } const normalizeArch = (arch: string): Arch => { // Node docs: // - https://nodejs.org/api/process.html#processarch // Deno docs: // - https://doc.deno.land/deno/stable/~/Deno.build if (arch === 'x32') return 'x32'; if (arch === 'x86_64' || arch === 'x64') return 'x64'; if (arch === 'arm') return 'arm'; if (arch === 'aarch64' || arch === 'arm64') return 'arm64'; if (arch) return `other:${arch}`; return 'unknown'; }; const normalizePlatform = (platform: string): PlatformName => { // Node platforms: // - https://nodejs.org/api/process.html#processplatform // Deno platforms: // - https://doc.deno.land/deno/stable/~/Deno.build // - https://github.com/denoland/deno/issues/14799 platform = platform.toLowerCase(); // NOTE: this iOS check is untested and may not work // Node does not work natively on IOS, there is a fork at // https://github.com/nodejs-mobile/nodejs-mobile // however it is unknown at the time of writing how to detect if it is running if (platform.includes('ios')) return 'iOS'; if (platform === 'android') return 'Android'; if (platform === 'darwin') return 'MacOS'; if (platform === 'win32') return 'Windows'; if (platform === 'freebsd') return 'FreeBSD'; if (platform === 'openbsd') return 'OpenBSD'; if (platform === 'linux') return 'Linux'; if (platform) return `Other:${platform}`; return 'Unknown'; }; let _platformHeaders: PlatformProperties; const getPlatformHeaders = () => { return (_platformHeaders ??= getPlatformProperties()); }; export const safeJSON = (text: string) => { try { return JSON.parse(text); } catch (err) { return undefined; } }; // https://stackoverflow.com/a/19709846 const startsWithSchemeRegexp = new RegExp('^(?:[a-z]+:)?//', 'i'); const isAbsoluteURL = (url: string): boolean => { return startsWithSchemeRegexp.test(url); }; export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { throw new OpenAIError(`${name} must be an integer`); } if (n < 0) { throw new OpenAIError(`${name} must be a positive integer`); } return n; }; export const castToError = (err: any): Error => { if (err instanceof Error) return err; return new Error(err); }; export const ensurePresent = (value: T | null | undefined): T => { if (value == null) throw new OpenAIError(`Expected a value to be given but received ${value} instead.`); return value; }; /** * Read an environment variable. * * Trims beginning and trailing whitespace. * * Will return undefined if the environment variable doesn't exist or cannot be accessed. */ export const readEnv = (env: string): string | undefined => { if (typeof process !== 'undefined') { return process.env?.[env]?.trim() ?? undefined; } if (typeof Deno !== 'undefined') { return Deno.env?.get?.(env)?.trim(); } return undefined; }; export const coerceInteger = (value: unknown): number => { if (typeof value === 'number') return Math.round(value); if (typeof value === 'string') return parseInt(value, 10); throw new OpenAIError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceFloat = (value: unknown): number => { if (typeof value === 'number') return value; if (typeof value === 'string') return parseFloat(value); throw new OpenAIError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceBoolean = (value: unknown): boolean => { if (typeof value === 'boolean') return value; if (typeof value === 'string') return value === 'true'; return Boolean(value); }; export const maybeCoerceInteger = (value: unknown): number | undefined => { if (value === undefined) { return undefined; } return coerceInteger(value); }; export const maybeCoerceFloat = (value: unknown): number | undefined => { if (value === undefined) { return undefined; } return coerceFloat(value); }; export const maybeCoerceBoolean = (value: unknown): boolean | undefined => { if (value === undefined) { return undefined; } return coerceBoolean(value); }; // https://stackoverflow.com/a/34491287 export function isEmptyObj(obj: Object | null | undefined): boolean { if (!obj) return true; for (const _k in obj) return false; return true; } // https://eslint.org/docs/latest/rules/no-prototype-builtins export function hasOwn(obj: Object, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } /** * Copies headers from "newHeaders" onto "targetHeaders", * using lower-case for all properties, * ignoring any keys with undefined values, * and deleting any keys with null values. */ function applyHeadersMut(targetHeaders: Headers, newHeaders: Headers): void { for (const k in newHeaders) { if (!hasOwn(newHeaders, k)) continue; const lowerKey = k.toLowerCase(); if (!lowerKey) continue; const val = newHeaders[k]; if (val === null) { delete targetHeaders[lowerKey]; } else if (val !== undefined) { targetHeaders[lowerKey] = val; } } } export function debug(action: string, ...args: any[]) { if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') { console.log(`OpenAI:DEBUG:${action}`, ...args); } } /** * https://stackoverflow.com/a/2117523 */ const uuid4 = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }; export const isRunningInBrowser = () => { return ( // @ts-ignore typeof window !== 'undefined' && // @ts-ignore typeof window.document !== 'undefined' && // @ts-ignore typeof navigator !== 'undefined' ); }; export interface HeadersProtocol { get: (header: string) => string | null | undefined; } export type HeadersLike = Record | HeadersProtocol; export const isHeadersProtocol = (headers: any): headers is HeadersProtocol => { return typeof headers?.get === 'function'; }; export const getRequiredHeader = (headers: HeadersLike, header: string): string => { const lowerCasedHeader = header.toLowerCase(); if (isHeadersProtocol(headers)) { // to deal with the case where the header looks like Stainless-Event-Id const intercapsHeader = header[0]?.toUpperCase() + header.substring(1).replace(/([^\w])(\w)/g, (_m, g1, g2) => g1 + g2.toUpperCase()); for (const key of [header, lowerCasedHeader, header.toUpperCase(), intercapsHeader]) { const value = headers.get(key); if (value) { return value; } } } for (const [key, value] of Object.entries(headers)) { if (key.toLowerCase() === lowerCasedHeader) { if (Array.isArray(value)) { if (value.length <= 1) return value[0]; console.warn(`Received ${value.length} entries for the ${header} header, using the first entry.`); return value[0]; } return value; } } throw new Error(`Could not find ${header} header`); }; /** * Encodes a string to Base64 format. */ export const toBase64 = (str: string | null | undefined): string => { if (!str) return ''; if (typeof Buffer !== 'undefined') { return Buffer.from(str).toString('base64'); } if (typeof btoa !== 'undefined') { return btoa(str); } throw new OpenAIError('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined'); }; export function isObj(obj: unknown): obj is Record { return obj != null && typeof obj === 'object' && !Array.isArray(obj); }