Skip to content

Commit fe090c0

Browse files
authored
feat(azure): Realtime API support (#1287)
1 parent fb61fc2 commit fe090c0

File tree

10 files changed

+251
-19
lines changed

10 files changed

+251
-19
lines changed

README.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ const credential = new DefaultAzureCredential();
499499
const scope = 'https://cognitiveservices.azure.com/.default';
500500
const azureADTokenProvider = getBearerTokenProvider(credential, scope);
501501

502-
const openai = new AzureOpenAI({ azureADTokenProvider });
502+
const openai = new AzureOpenAI({ azureADTokenProvider, apiVersion: "<The API version, e.g. 2024-10-01-preview>" });
503503

504504
const result = await openai.chat.completions.create({
505505
model: 'gpt-4o',
@@ -509,6 +509,26 @@ const result = await openai.chat.completions.create({
509509
console.log(result.choices[0]!.message?.content);
510510
```
511511

512+
### Realtime API
513+
This SDK provides real-time streaming capabilities for Azure OpenAI through the `OpenAIRealtimeWS` and `OpenAIRealtimeWebSocket` clients described previously.
514+
515+
To utilize the real-time features, begin by creating a fully configured `AzureOpenAI` client and passing it into either `OpenAIRealtimeWS.azure` or `OpenAIRealtimeWebSocket.azure`. For example:
516+
517+
```ts
518+
const cred = new DefaultAzureCredential();
519+
const scope = 'https://cognitiveservices.azure.com/.default';
520+
const deploymentName = 'gpt-4o-realtime-preview-1001';
521+
const azureADTokenProvider = getBearerTokenProvider(cred, scope);
522+
const client = new AzureOpenAI({
523+
azureADTokenProvider,
524+
apiVersion: '2024-10-01-preview',
525+
deployment: deploymentName,
526+
});
527+
const rt = await OpenAIRealtimeWS.azure(client);
528+
```
529+
530+
Once the instance has been created, you can then begin sending requests and receiving streaming responses in real time.
531+
512532
### Retries
513533

514534
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.

examples/azure.ts renamed to examples/azure/chat.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { AzureOpenAI } from 'openai';
44
import { getBearerTokenProvider, DefaultAzureCredential } from '@azure/identity';
5+
import 'dotenv/config';
56

67
// Corresponds to your Model deployment within your OpenAI resource, e.g. gpt-4-1106-preview
78
// Navigate to the Azure OpenAI Studio to deploy a model.
@@ -13,7 +14,7 @@ const azureADTokenProvider = getBearerTokenProvider(credential, scope);
1314

1415
// Make sure to set AZURE_OPENAI_ENDPOINT with the endpoint of your Azure resource.
1516
// You can find it in the Azure Portal.
16-
const openai = new AzureOpenAI({ azureADTokenProvider });
17+
const openai = new AzureOpenAI({ azureADTokenProvider, apiVersion: '2024-10-01-preview' });
1718

1819
async function main() {
1920
console.log('Non-streaming:');

examples/azure/realtime/websocket.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket';
2+
import { AzureOpenAI } from 'openai';
3+
import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity';
4+
import 'dotenv/config';
5+
6+
async function main() {
7+
const cred = new DefaultAzureCredential();
8+
const scope = 'https://cognitiveservices.azure.com/.default';
9+
const deploymentName = 'gpt-4o-realtime-preview-1001';
10+
const azureADTokenProvider = getBearerTokenProvider(cred, scope);
11+
const client = new AzureOpenAI({
12+
azureADTokenProvider,
13+
apiVersion: '2024-10-01-preview',
14+
deployment: deploymentName,
15+
});
16+
const rt = await OpenAIRealtimeWebSocket.azure(client);
17+
18+
// access the underlying `ws.WebSocket` instance
19+
rt.socket.addEventListener('open', () => {
20+
console.log('Connection opened!');
21+
rt.send({
22+
type: 'session.update',
23+
session: {
24+
modalities: ['text'],
25+
model: 'gpt-4o-realtime-preview',
26+
},
27+
});
28+
29+
rt.send({
30+
type: 'conversation.item.create',
31+
item: {
32+
type: 'message',
33+
role: 'user',
34+
content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }],
35+
},
36+
});
37+
38+
rt.send({ type: 'response.create' });
39+
});
40+
41+
rt.on('error', (err) => {
42+
// in a real world scenario this should be logged somewhere as you
43+
// likely want to continue procesing events regardless of any errors
44+
throw err;
45+
});
46+
47+
rt.on('session.created', (event) => {
48+
console.log('session created!', event.session);
49+
console.log();
50+
});
51+
52+
rt.on('response.text.delta', (event) => process.stdout.write(event.delta));
53+
rt.on('response.text.done', () => console.log());
54+
55+
rt.on('response.done', () => rt.close());
56+
57+
rt.socket.addEventListener('close', () => console.log('\nConnection closed!'));
58+
}
59+
60+
main();

examples/azure/realtime/ws.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity';
2+
import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws';
3+
import { AzureOpenAI } from 'openai';
4+
import 'dotenv/config';
5+
6+
async function main() {
7+
const cred = new DefaultAzureCredential();
8+
const scope = 'https://cognitiveservices.azure.com/.default';
9+
const deploymentName = 'gpt-4o-realtime-preview-1001';
10+
const azureADTokenProvider = getBearerTokenProvider(cred, scope);
11+
const client = new AzureOpenAI({
12+
azureADTokenProvider,
13+
apiVersion: '2024-10-01-preview',
14+
deployment: deploymentName,
15+
});
16+
const rt = await OpenAIRealtimeWS.azure(client);
17+
18+
// access the underlying `ws.WebSocket` instance
19+
rt.socket.on('open', () => {
20+
console.log('Connection opened!');
21+
rt.send({
22+
type: 'session.update',
23+
session: {
24+
modalities: ['text'],
25+
model: 'gpt-4o-realtime-preview',
26+
},
27+
});
28+
rt.send({
29+
type: 'session.update',
30+
session: {
31+
modalities: ['text'],
32+
model: 'gpt-4o-realtime-preview',
33+
},
34+
});
35+
36+
rt.send({
37+
type: 'conversation.item.create',
38+
item: {
39+
type: 'message',
40+
role: 'user',
41+
content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }],
42+
},
43+
});
44+
45+
rt.send({ type: 'response.create' });
46+
});
47+
48+
rt.on('error', (err) => {
49+
// in a real world scenario this should be logged somewhere as you
50+
// likely want to continue procesing events regardless of any errors
51+
throw err;
52+
});
53+
54+
rt.on('session.created', (event) => {
55+
console.log('session created!', event.session);
56+
console.log();
57+
});
58+
59+
rt.on('response.text.delta', (event) => process.stdout.write(event.delta));
60+
rt.on('response.text.done', () => console.log());
61+
62+
rt.on('response.done', () => rt.close());
63+
64+
rt.socket.on('close', () => console.log('\nConnection closed!'));
65+
}
66+
67+
main();

examples/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"private": true,
88
"dependencies": {
99
"@azure/identity": "^4.2.0",
10+
"dotenv": "^16.4.7",
1011
"express": "^4.18.2",
1112
"next": "^14.1.1",
1213
"openai": "file:..",

examples/realtime/ws.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ async function main() {
99
rt.send({
1010
type: 'session.update',
1111
session: {
12-
modalities: ['foo'] as any,
12+
modalities: ['text'],
1313
model: 'gpt-4o-realtime-preview',
1414
},
1515
});

src/beta/realtime/internal-base.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RealtimeClientEvent, RealtimeServerEvent, ErrorEvent } from '../../resources/beta/realtime/realtime';
22
import { EventEmitter } from '../../lib/EventEmitter';
33
import { OpenAIError } from '../../error';
4+
import OpenAI, { AzureOpenAI } from '../../index';
45

56
export class OpenAIRealtimeError extends OpenAIError {
67
/**
@@ -73,11 +74,20 @@ export abstract class OpenAIRealtimeEmitter extends EventEmitter<RealtimeEvents>
7374
}
7475
}
7576

76-
export function buildRealtimeURL(props: { baseURL: string; model: string }): URL {
77-
const path = '/realtime';
77+
export function isAzure(client: Pick<OpenAI, 'apiKey' | 'baseURL'>): client is AzureOpenAI {
78+
return client instanceof AzureOpenAI;
79+
}
7880

79-
const url = new URL(props.baseURL + (props.baseURL.endsWith('/') ? path.slice(1) : path));
81+
export function buildRealtimeURL(client: Pick<OpenAI, 'apiKey' | 'baseURL'>, model: string): URL {
82+
const path = '/realtime';
83+
const baseURL = client.baseURL;
84+
const url = new URL(baseURL + (baseURL.endsWith('/') ? path.slice(1) : path));
8085
url.protocol = 'wss';
81-
url.searchParams.set('model', props.model);
86+
if (isAzure(client)) {
87+
url.searchParams.set('api-version', client.apiVersion);
88+
url.searchParams.set('deployment', model);
89+
} else {
90+
url.searchParams.set('model', model);
91+
}
8292
return url;
8393
}

src/beta/realtime/websocket.ts

+50-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { OpenAI } from '../../index';
1+
import { AzureOpenAI, OpenAI } from '../../index';
22
import { OpenAIError } from '../../error';
33
import * as Core from '../../core';
44
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
5-
import { OpenAIRealtimeEmitter, buildRealtimeURL } from './internal-base';
5+
import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base';
66

77
interface MessageEvent {
88
data: string;
@@ -26,6 +26,11 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
2626
props: {
2727
model: string;
2828
dangerouslyAllowBrowser?: boolean;
29+
/**
30+
* Callback to mutate the URL, needed for Azure.
31+
* @internal
32+
*/
33+
onURL?: (url: URL) => void;
2934
},
3035
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
3136
) {
@@ -44,11 +49,13 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
4449

4550
client ??= new OpenAI({ dangerouslyAllowBrowser });
4651

47-
this.url = buildRealtimeURL({ baseURL: client.baseURL, model: props.model });
52+
this.url = buildRealtimeURL(client, props.model);
53+
props.onURL?.(this.url);
54+
4855
// @ts-ignore
4956
this.socket = new WebSocket(this.url, [
5057
'realtime',
51-
`openai-insecure-api-key.${client.apiKey}`,
58+
...(isAzure(client) ? [] : [`openai-insecure-api-key.${client.apiKey}`]),
5259
'openai-beta.realtime-v1',
5360
]);
5461

@@ -77,6 +84,45 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
7784
this.socket.addEventListener('error', (event: any) => {
7885
this._onError(null, event.message, null);
7986
});
87+
88+
if (isAzure(client)) {
89+
if (this.url.searchParams.get('Authorization') !== null) {
90+
this.url.searchParams.set('Authorization', '<REDACTED>');
91+
} else {
92+
this.url.searchParams.set('api-key', '<REDACTED>');
93+
}
94+
}
95+
}
96+
97+
static async azure(
98+
client: AzureOpenAI,
99+
options: { deploymentName?: string; dangerouslyAllowBrowser?: boolean } = {},
100+
): Promise<OpenAIRealtimeWebSocket> {
101+
const token = await client._getAzureADToken();
102+
function onURL(url: URL) {
103+
if (client.apiKey !== '<Missing Key>') {
104+
url.searchParams.set('api-key', client.apiKey);
105+
} else {
106+
if (token) {
107+
url.searchParams.set('Authorization', `Bearer ${token}`);
108+
} else {
109+
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
110+
}
111+
}
112+
}
113+
const deploymentName = options.deploymentName ?? client.deploymentName;
114+
if (!deploymentName) {
115+
throw new Error('No deployment name provided');
116+
}
117+
const { dangerouslyAllowBrowser } = options;
118+
return new OpenAIRealtimeWebSocket(
119+
{
120+
model: deploymentName,
121+
onURL,
122+
...(dangerouslyAllowBrowser ? { dangerouslyAllowBrowser } : {}),
123+
},
124+
client,
125+
);
80126
}
81127

82128
send(event: RealtimeClientEvent) {

src/beta/realtime/ws.ts

+31-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as WS from 'ws';
2-
import { OpenAI } from '../../index';
2+
import { AzureOpenAI, OpenAI } from '../../index';
33
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
4-
import { OpenAIRealtimeEmitter, buildRealtimeURL } from './internal-base';
4+
import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base';
55

66
export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
77
url: URL;
@@ -14,12 +14,12 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
1414
super();
1515
client ??= new OpenAI();
1616

17-
this.url = buildRealtimeURL({ baseURL: client.baseURL, model: props.model });
17+
this.url = buildRealtimeURL(client, props.model);
1818
this.socket = new WS.WebSocket(this.url, {
1919
...props.options,
2020
headers: {
2121
...props.options?.headers,
22-
Authorization: `Bearer ${client.apiKey}`,
22+
...(isAzure(client) ? {} : { Authorization: `Bearer ${client.apiKey}` }),
2323
'OpenAI-Beta': 'realtime=v1',
2424
},
2525
});
@@ -51,6 +51,20 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
5151
});
5252
}
5353

54+
static async azure(
55+
client: AzureOpenAI,
56+
options: { deploymentName?: string; options?: WS.ClientOptions | undefined } = {},
57+
): Promise<OpenAIRealtimeWS> {
58+
const deploymentName = options.deploymentName ?? client.deploymentName;
59+
if (!deploymentName) {
60+
throw new Error('No deployment name provided');
61+
}
62+
return new OpenAIRealtimeWS(
63+
{ model: deploymentName, options: { headers: await getAzureHeaders(client) } },
64+
client,
65+
);
66+
}
67+
5468
send(event: RealtimeClientEvent) {
5569
try {
5670
this.socket.send(JSON.stringify(event));
@@ -67,3 +81,16 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
6781
}
6882
}
6983
}
84+
85+
async function getAzureHeaders(client: AzureOpenAI) {
86+
if (client.apiKey !== '<Missing Key>') {
87+
return { 'api-key': client.apiKey };
88+
} else {
89+
const token = await client._getAzureADToken();
90+
if (token) {
91+
return { Authorization: `Bearer ${token}` };
92+
} else {
93+
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)