Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: 1
generation_id: 9d640a0f-6abd-44f8-aaa0-df24b003656d
generation_id: 2e672b14-22c2-4e3e-b20d-2cdd0d719583
openapi_spec_hash: dd725fb7d43ceec7fb2de6f8713d14b6
openapi_transformed_spec_hash: 10930179c5f116288e24e0c6fda46559
config_hash: 28f37c225c55c75547d4feb84ee71044
codegen_sha: cfe310550a1bec7192f8673528a957c35703f034
config_hash: 85382dd94c503b5d225adc7636a77c9f
codegen_sha: 2ee3a8abe63e13ed0fc033198d9b99b51d168ad3
13 changes: 12 additions & 1 deletion src/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import { isObj, readEnv } from './internal/utils';
import { path } from './internal/utils/path';
import { OpenAI } from './client';
import type { ClientOptions } from './client';
import { assertNoDataResidency } from './internal/data-residency';

/** API Client for interfacing with the Azure OpenAI API. */
export interface AzureClientOptions extends Omit<ClientOptions, 'provider'> {
export interface AzureClientOptions extends Omit<ClientOptions, 'provider' | 'dataResidency'> {
/** AzureOpenAI does not support third-party provider configuration. */
provider?: never;

/** OpenAI data residency cannot be combined with Azure routing. */
dataResidency?: never;
Comment thread
apcha-oai marked this conversation as resolved.
Resolved

/**
* Defaults to process.env['OPENAI_API_VERSION'].
*/
Expand Down Expand Up @@ -74,8 +78,10 @@ export class AzureOpenAI extends OpenAI {
deployment,
azureADTokenProvider,
dangerouslyAllowBrowser,
dataResidency,
...opts
}: AzureClientOptions = {}) {
assertNoDataResidency(dataResidency, 'AzureOpenAI');
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' }).",
Expand Down Expand Up @@ -131,6 +137,11 @@ export class AzureOpenAI extends OpenAI {
this.deploymentName = deployment;
}

/** Clones this client with Azure options; OpenAI data residency remains unsupported. */
override withOptions(options: Partial<AzureClientOptions>): this {
return super.withOptions(options);
}

/** Builds an Azure request and inserts its deployment into model-scoped endpoint paths. */
override async buildRequest(
options: FinalRequestOptions,
Expand Down
8 changes: 7 additions & 1 deletion src/bedrock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Errors from './error';
import { OpenAI } from './client';
import type { ApiKeySetter, ClientOptions } from './client';
import { assertNoDataResidency } from './internal/data-residency';
import { assertBedrockRequestOrigin, brand_privateBedrockClient } from './internal/bedrock';
import type { RequestInit } from './internal/builtin-types';
import type { NullableHeaders } from './internal/headers';
Expand All @@ -15,7 +16,7 @@ import type * as ResponsesAPI from './resources/responses/responses';
/** Configures Amazon Bedrock's OpenAI-compatible endpoint and bearer-token authentication. */
export interface BedrockClientOptions extends Omit<
ClientOptions,
'apiKey' | 'adminAPIKey' | 'baseURL' | 'workloadIdentity'
'apiKey' | 'adminAPIKey' | 'baseURL' | 'workloadIdentity' | 'dataResidency'
> {
/**
* Bedrock bearer token used for authentication.
Expand All @@ -38,6 +39,9 @@ export interface BedrockClientOptions extends Omit<
*/
adminAPIKey?: never;

/** OpenAI data residency cannot be combined with Bedrock routing. */
dataResidency?: never;

/**
* BedrockOpenAI only supports Bedrock bearer token authentication.
*/
Expand Down Expand Up @@ -129,8 +133,10 @@ export class BedrockOpenAI extends OpenAI {
bedrockTokenProvider,
adminAPIKey,
workloadIdentity,
dataResidency,
...opts
}: BedrockClientOptions = {}) {
assertNoDataResidency(dataResidency, 'BedrockOpenAI');
if (adminAPIKey || workloadIdentity) {
throw new Errors.OpenAIError('BedrockOpenAI only supports Bedrock bearer token authentication.');
}
Expand Down
52 changes: 42 additions & 10 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types';
import type { HTTPMethod, PromiseOrValue, MergedRequestInit, FinalizedRequestInit } from './internal/types';
import { uuid4 } from './internal/utils/uuid';
import { validatePositiveInteger, isAbsoluteURL, safeJSON } from './internal/utils/values';
import { validatePositiveInteger, isAbsoluteURL, safeJSON, hasOwn } from './internal/utils/values';
import { sleep } from './internal/utils/sleep';
export type { Logger, LogLevel } from './internal/utils/log';
import { castToError, isAbortError } from './internal/errors';
Expand All @@ -13,6 +13,8 @@ import * as Shims from './internal/shims';
import * as Opts from './internal/request-options';
import { stringifyQuery } from './internal/utils/query';
import { VERSION } from './version';
import { resolveDataResidency, type DataResidency } from './internal/data-residency';
export type { DataResidency } from './internal/data-residency';
import * as Errors from './core/error';
import * as Pagination from './core/pagination';
import type { WorkloadIdentity } from './auth/types';
Expand Down Expand Up @@ -269,6 +271,8 @@ function isRunningInBrowserOrBrowserWorker(): boolean {
}

const WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER = 'workload-identity-auth';
const inheritedDataResidencySelection = Symbol('inheritedDataResidencySelection');
type InternalClientOptions = ClientOptions & { [inheritedDataResidencySelection]?: boolean };

export type ApiKeySetter = () => Promise<string>;

Expand Down Expand Up @@ -314,6 +318,14 @@ export interface ClientOptions {
*/
baseURL?: string | null | undefined;

/**
* Select an OpenAI regional endpoint. This overrides an inherited or environment
* base URL and is mutually exclusive with an explicit `baseURL` or `provider`.
* Availability depends on your project and model; no fallback is performed.
* `null` and `undefined` leave ordinary base URL resolution unchanged.
*/
dataResidency?: DataResidency | 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.
Expand Down Expand Up @@ -431,6 +443,8 @@ export class OpenAI {

private fetch: Fetch;
#encoder: Opts.RequestEncoder;
// Preserve an explicit global selection without storing a second routing URL.
#explicitDataResidency = false;
#responseAttempts = new WeakMap<AbortController, { timeout: number; retriesRemaining: number }>();
protected idempotencyHeader?: string;
protected _options: ClientOptions;
Expand All @@ -456,11 +470,12 @@ export class OpenAI {
* @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(clientOptions: ClientOptions = {}) {
const residencyBaseURL = resolveDataResidency(clientOptions);
const provider = clientOptions.provider;
if (provider) {
const conflictingOptions = (['apiKey', 'adminAPIKey', 'workloadIdentity', 'baseURL'] as const).filter(
(key) => clientOptions[key] != null,
);
const conflictingOptions = (
['apiKey', 'adminAPIKey', 'workloadIdentity', 'baseURL', 'dataResidency'] as const
).filter((key) => clientOptions[key] != null);
if (conflictingOptions.length) {
throw new Errors.OpenAIError(
`The \`provider\` option cannot be used with ${conflictingOptions
Expand All @@ -472,14 +487,16 @@ export class OpenAI {

const {
baseURL = provider ? null : readEnv('OPENAI_BASE_URL'),
dataResidency: _dataResidency,
[inheritedDataResidencySelection]: inheritedResidencySelection = false,
apiKey = provider ? null : (readEnv('OPENAI_API_KEY') ?? null),
adminAPIKey = provider ? null : (readEnv('OPENAI_ADMIN_KEY') ?? null),
organization = provider ? null : (readEnv('OPENAI_ORG_ID') ?? null),
project = provider ? null : (readEnv('OPENAI_PROJECT_ID') ?? null),
webhookSecret = readEnv('OPENAI_WEBHOOK_SECRET') ?? null,
workloadIdentity,
...opts
} = clientOptions;
} = clientOptions as InternalClientOptions;
const providerRuntime = provider ? configureProvider(provider) : undefined;
const options: ClientOptions = {
apiKey,
Expand All @@ -490,7 +507,7 @@ export class OpenAI {
workloadIdentity,
provider,
...opts,
baseURL: providerRuntime?.baseURL ?? (baseURL || `https://api.openai.com/v1`),
baseURL: providerRuntime?.baseURL ?? residencyBaseURL ?? (baseURL || `https://api.openai.com/v1`),
};

if (apiKey && workloadIdentity) {
Expand All @@ -510,6 +527,7 @@ export class OpenAI {
}

this.baseURL = options.baseURL!;
this.#explicitDataResidency = residencyBaseURL !== undefined || inheritedResidencySelection;
this.timeout = options.timeout ?? OpenAI.DEFAULT_TIMEOUT; /* 10 minutes */
this.logger = options.logger ?? console;
const defaultLogLevel = 'warn';
Expand Down Expand Up @@ -554,6 +572,7 @@ export class OpenAI {
* Create a new client instance re-using the same options given to the current client with optional overriding.
*/
withOptions(options: Partial<ClientOptions>): this {
const residencyBaseURL = resolveDataResidency(options);
const inheritedProvider = this._options.provider;
const provider = options.provider ?? inheritedProvider;
const inheritedOptions: ClientOptions = {
Expand All @@ -572,6 +591,9 @@ export class OpenAI {
project: this.project,
webhookSecret: this.webhookSecret,
};
if (residencyBaseURL !== undefined) {
delete inheritedOptions.baseURL;
}
if (provider) {
delete inheritedOptions.apiKey;
delete inheritedOptions.adminAPIKey;
Expand All @@ -584,19 +606,28 @@ export class OpenAI {
}
}

const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)({
const clientOptions: InternalClientOptions = {
...inheritedOptions,
...options,
provider,
});
return client;
[inheritedDataResidencySelection]:
this.#explicitDataResidency &&
residencyBaseURL === undefined &&
!hasOwn(options, 'baseURL') &&
!provider,
};
return new (this.constructor as any as new (props: ClientOptions) => typeof this)(clientOptions);
}

/**
* Check whether the base URL is set to its default.
*/
#baseURLOverridden(): boolean {
return this._provider !== undefined || this.baseURL !== 'https://api.openai.com/v1';
return (
this.#explicitDataResidency ||
this._provider !== undefined ||
this.baseURL !== 'https://api.openai.com/v1'
);
}

protected defaultQuery(): Record<string, string | undefined> | undefined {
Expand Down Expand Up @@ -1619,6 +1650,7 @@ function isUndiciDispatcherVersionMismatchError(error: unknown): boolean {
}

export declare namespace OpenAI {
export { type DataResidency as DataResidency };
export type RequestOptions = Opts.RequestOptions;

export import Page = Pagination.Page;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { OpenAI as default } from './client';

export { type Uploadable, toFile, toStreamingFile } from './core/uploads';
export { APIPromise } from './core/api-promise';
export { OpenAI, type ClientOptions } from './client';
export { OpenAI, type ClientOptions, type DataResidency } from './client';
export { PagePromise } from './core/pagination';
export {
OpenAIError,
Expand Down
44 changes: 44 additions & 0 deletions src/internal/data-residency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.

import { OpenAIError } from '../core/error';
import { hasOwn } from './utils/values';

/** OpenAI endpoint regions selectable without specifying a full base URL. */
export type DataResidency = 'global' | 'us' | 'eu' | 'ae';

const endpoints = new Map<DataResidency, string>([
['global', 'https://api.openai.com/v1'],
['us', 'https://us.api.openai.com/v1'],
['eu', 'https://eu.api.openai.com/v1'],
['ae', 'https://ae.api.openai.com/v1'],
]);

/** Resolves an explicit residency selection before client options are inherited. */
export function resolveDataResidency(options: {
/** Residency shorthand; null and undefined preserve ordinary URL resolution. */
dataResidency?: DataResidency | null | undefined;
/** Explicit API root, mutually exclusive with a residency selection. */
baseURL?: string | null | undefined;
}): string | undefined {
if (options.dataResidency === null || options.dataResidency === undefined) {
return undefined;
}
if (hasOwn(options, 'baseURL')) {
throw new OpenAIError('The `dataResidency` and `baseURL` options are mutually exclusive.');
}
const endpoint = endpoints.get(options.dataResidency);
if (endpoint === undefined) {
throw new OpenAIError('Invalid `dataResidency`; expected one of: global, us, eu, ae.');
}
return endpoint;
}

/** Prevents legacy provider clients from selecting an OpenAI endpoint. */
export function assertNoDataResidency(
dataResidency: DataResidency | null | undefined,
clientName: string,
): void {
if (dataResidency !== null && dataResidency !== undefined) {
throw new OpenAIError(`${clientName} does not support \`dataResidency\`.`);
}
}
Loading
Loading