Skip to content

Commit

Permalink
feat(client): add Azure client (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot committed May 5, 2024
1 parent 6c9cc82 commit 92f9049
Show file tree
Hide file tree
Showing 5 changed files with 604 additions and 30 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,25 @@ Error codes are as followed:
| >=500 | `InternalServerError` |
| N/A | `APIConnectionError` |

### Azure OpenAI
## Microsoft Azure OpenAI

An example of using this library with Azure OpenAI can be found [here](https://github.com/openai/openai-node/blob/master/examples/azure.ts).
To use this library with [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview), use the `AzureOpenAI`
class instead of the `OpenAI` class.

Please note there are subtle differences in API shape & behavior between the Azure OpenAI API and the OpenAI API,
so using this library with Azure OpenAI may result in incorrect types, which can lead to bugs.
> [!IMPORTANT]
> The Azure API shape differs from the core API shape which means that the static types for responses / params
> won't always be correct.
```ts
const openai = new AzureOpenAI();

See [`@azure/openai`](https://www.npmjs.com/package/@azure/openai) for an Azure-specific SDK provided by Microsoft.
const result = await openai.chat.completions.create({
model: 'gpt-4-1106-preview',
messages: [{ role: 'user', content: 'Say hello!' }],
});

console.log(result.choices[0]!.message?.content);
```

### Retries

Expand Down
32 changes: 8 additions & 24 deletions examples/azure.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,27 @@
#!/usr/bin/env -S npm run tsn -T

import OpenAI from 'openai';
import { AzureOpenAI } from 'openai';

// The name of your Azure OpenAI Resource.
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
const resource = '<your resource name>';

// Corresponds to your Model deployment within your OpenAI resource, e.g. my-gpt35-16k-deployment
// Corresponds to your Model deployment within your OpenAI resource, e.g. gpt-4-1106-preview
// Navigate to the Azure OpenAI Studio to deploy a model.
const model = '<your model>';

// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning
const apiVersion = '2023-06-01-preview';
const deployment = 'gpt-4-1106-preview';

const apiKey = process.env['AZURE_OPENAI_API_KEY'];
if (!apiKey) {
throw new Error('The AZURE_OPENAI_API_KEY environment variable is missing or empty.');
}

// Azure OpenAI requires a custom baseURL, api-version query param, and api-key header.
const openai = new OpenAI({
apiKey,
baseURL: `https://${resource}.openai.azure.com/openai/deployments/${model}`,
defaultQuery: { 'api-version': apiVersion },
defaultHeaders: { 'api-key': apiKey },
});
// Make sure to set both AZURE_OPENAI_ENDPOINT with the endpoint of your Azure resource and AZURE_OPENAI_API_KEY with the API key.
// You can find both information in the Azure Portal.
const openai = new AzureOpenAI();

async function main() {
console.log('Non-streaming:');
const result = await openai.chat.completions.create({
model,
model: deployment,
messages: [{ role: 'user', content: 'Say hello!' }],
});
console.log(result.choices[0]!.message?.content);

console.log();
console.log('Streaming:');
const stream = await openai.chat.completions.create({
model,
model: deployment,
messages: [{ role: 'user', content: 'Say hello!' }],
stream: true,
});
Expand Down
185 changes: 184 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import * as Core from './core';
import * as Errors from './error';
import { type Agent } from './_shims/index';
import { type Agent, type RequestInit } from './_shims/index';
import * as Uploads from './uploads';
import * as Pagination from 'openai/pagination';
import * as API from 'openai/resources/index';
Expand Down Expand Up @@ -310,4 +310,187 @@ export namespace OpenAI {
export import FunctionParameters = API.FunctionParameters;
}

// ---------------------- 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?: (() => string) | undefined;
}

/** API Client for interfacing with the Azure OpenAI API. */
export class AzureOpenAI extends OpenAI {
private _azureADTokenProvider: (() => 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.
* @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',
);
}

if (deployment) {
baseURL = `${endpoint}/openai/deployments/${deployment}`;
} else {
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;
}

override buildRequest(options: Core.FinalRequestOptions<unknown>): {
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 = options.body['model'];
delete options.body['model'];
if (model !== undefined && !this.baseURL.includes('/deployments')) {
options.path = `/deployments/${model}${options.path}`;
}
}
return super.buildRequest(options);
}

private _getAzureADToken(): string | undefined {
if (typeof this._azureADTokenProvider === 'function') {
const token = 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 {
if (opts.headers?.['Authorization'] || opts.headers?.['api-key']) {
return {};
}
const token = this._getAzureADToken();
if (token) {
return { Authorization: `Bearer ${token}` };
}
if (this.apiKey !== API_KEY_SENTINEL) {
return { 'api-key': this.apiKey };
}
throw new Errors.OpenAIError('Unable to handle auth');
}
}

const _deployments_endpoints = new Set([
'/completions',
'/chat/completions',
'/embeddings',
'/audio/transcriptions',
'/audio/translations',
'/audio/speech',
'/images/generations',
]);

const API_KEY_SENTINEL = '<Missing Key>';

// ---------------------- End Azure ----------------------

export default OpenAI;
1 change: 1 addition & 0 deletions src/resources/beta/vector-stores/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class Files extends APIResource {

/**
* Upload a file to the `files` API and then attach it to the given vector store.
*
* Note the file will be asynchronously processed (you can use the alternative
* polling helper method to wait for processing to complete).
*/
Expand Down

0 comments on commit 92f9049

Please sign in to comment.