Skip to content

Commit cd95e16

Browse files
RobertCraigiestainless-app[bot]
authored andcommitted
fix: update non beta realtime websockets helpers
1 parent 51c8c02 commit cd95e16

File tree

2 files changed

+67
-32
lines changed

2 files changed

+67
-32
lines changed

src/realtime/websocket.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,17 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
3131
* @internal
3232
*/
3333
onURL?: (url: URL) => void;
34+
/** Indicates the token was resolved by the factory just before connecting. @internal */
35+
__resolvedApiKey?: boolean;
3436
},
3537
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
3638
) {
3739
super();
38-
40+
const hasProvider = typeof (client as any)?._options?.apiKey === 'function';
3941
const dangerouslyAllowBrowser =
4042
props.dangerouslyAllowBrowser ??
4143
(client as any)?._options?.dangerouslyAllowBrowser ??
42-
(client?.apiKey.startsWith('ek_') ? true : null);
43-
44+
(client?.apiKey?.startsWith('ek_') ? true : null);
4445
if (!dangerouslyAllowBrowser && isRunningInBrowser()) {
4546
throw new OpenAIError(
4647
"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",
@@ -49,13 +50,24 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
4950

5051
client ??= new OpenAI({ dangerouslyAllowBrowser });
5152

53+
if (hasProvider && !props?.__resolvedApiKey) {
54+
throw new Error(
55+
[
56+
'Cannot open Realtime WebSocket with a function-based apiKey.',
57+
'Use the .create() method so that the key is resolved before connecting:',
58+
'await OpenAIRealtimeWebSocket.create(client, { model })',
59+
].join('\n'),
60+
);
61+
}
62+
5263
this.url = buildRealtimeURL(client, props.model);
5364
props.onURL?.(this.url);
5465

5566
// @ts-ignore
5667
this.socket = new WebSocket(this.url.toString(), [
5768
'realtime',
5869
...(isAzure(client) ? [] : [`openai-insecure-api-key.${client.apiKey}`]),
70+
'openai-beta.realtime-v1',
5971
]);
6072

6173
this.socket.addEventListener('message', (websocketEvent: MessageEvent) => {
@@ -93,20 +105,23 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
93105
}
94106
}
95107

108+
static async create(
109+
client: Pick<OpenAI, 'apiKey' | 'baseURL' | '_callApiKey'>,
110+
props: { model: string; dangerouslyAllowBrowser?: boolean },
111+
): Promise<OpenAIRealtimeWebSocket> {
112+
return new OpenAIRealtimeWebSocket({ ...props, __resolvedApiKey: await client._callApiKey() }, client);
113+
}
114+
96115
static async azure(
97-
client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
116+
client: Pick<AzureOpenAI, '_callApiKey' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
98117
options: { deploymentName?: string; dangerouslyAllowBrowser?: boolean } = {},
99118
): Promise<OpenAIRealtimeWebSocket> {
100-
const token = await client._getAzureADToken();
119+
const isApiKeyProvider = await client._callApiKey();
101120
function onURL(url: URL) {
102-
if (client.apiKey !== '<Missing Key>') {
103-
url.searchParams.set('api-key', client.apiKey);
121+
if (isApiKeyProvider) {
122+
url.searchParams.set('Authorization', `Bearer ${client.apiKey}`);
104123
} else {
105-
if (token) {
106-
url.searchParams.set('Authorization', `Bearer ${token}`);
107-
} else {
108-
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
109-
}
124+
url.searchParams.set('api-key', client.apiKey);
110125
}
111126
}
112127
const deploymentName = options.deploymentName ?? client.deploymentName;
@@ -119,6 +134,7 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
119134
model: deploymentName,
120135
onURL,
121136
...(dangerouslyAllowBrowser ? { dangerouslyAllowBrowser } : {}),
137+
__resolvedApiKey: isApiKeyProvider,
122138
},
123139
client,
124140
);

src/realtime/ws.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,32 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
88
socket: WS.WebSocket;
99

1010
constructor(
11-
props: { model: string; options?: WS.ClientOptions | undefined },
11+
props: {
12+
model: string;
13+
options?: WS.ClientOptions | undefined;
14+
/** @internal */ __resolvedApiKey?: boolean;
15+
},
1216
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
1317
) {
1418
super();
1519
client ??= new OpenAI();
16-
20+
const hasProvider = typeof (client as any)?._options?.apiKey === 'function';
21+
if (hasProvider && !props.__resolvedApiKey) {
22+
throw new Error(
23+
[
24+
'Cannot open Realtime WebSocket with a function-based apiKey.',
25+
'Use the .create() method so that the key is resolved before connecting:',
26+
'await OpenAIRealtimeWS.create(client, { model })',
27+
].join('\n'),
28+
);
29+
}
1730
this.url = buildRealtimeURL(client, props.model);
1831
this.socket = new WS.WebSocket(this.url, {
1932
...props.options,
2033
headers: {
2134
...props.options?.headers,
22-
...(isAzure(client) ? {} : { Authorization: `Bearer ${client.apiKey}` }),
35+
...(isAzure(client) && !props.__resolvedApiKey ? {} : { Authorization: `Bearer ${client.apiKey}` }),
36+
'OpenAI-Beta': 'realtime=v1',
2337
},
2438
});
2539

@@ -50,16 +64,34 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
5064
});
5165
}
5266

67+
static async create(
68+
client: Pick<OpenAI, 'apiKey' | 'baseURL' | '_callApiKey'>,
69+
props: { model: string; options?: WS.ClientOptions | undefined },
70+
): Promise<OpenAIRealtimeWS> {
71+
return new OpenAIRealtimeWS({ ...props, __resolvedApiKey: await client._callApiKey() }, client);
72+
}
73+
5374
static async azure(
54-
client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
55-
options: { deploymentName?: string; options?: WS.ClientOptions | undefined } = {},
75+
client: Pick<AzureOpenAI, '_callApiKey' | 'apiVersion' | 'apiKey' | 'baseURL' | 'deploymentName'>,
76+
props: { deploymentName?: string; options?: WS.ClientOptions | undefined } = {},
5677
): Promise<OpenAIRealtimeWS> {
57-
const deploymentName = options.deploymentName ?? client.deploymentName;
78+
const isApiKeyProvider = await client._callApiKey();
79+
const deploymentName = props.deploymentName ?? client.deploymentName;
5880
if (!deploymentName) {
5981
throw new Error('No deployment name provided');
6082
}
6183
return new OpenAIRealtimeWS(
62-
{ model: deploymentName, options: { headers: await getAzureHeaders(client) } },
84+
{
85+
model: deploymentName,
86+
options: {
87+
...props.options,
88+
headers: {
89+
...props.options?.headers,
90+
...(isApiKeyProvider ? {} : { 'api-key': client.apiKey }),
91+
},
92+
},
93+
__resolvedApiKey: isApiKeyProvider,
94+
},
6395
client,
6496
);
6597
}
@@ -80,16 +112,3 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
80112
}
81113
}
82114
}
83-
84-
async function getAzureHeaders(client: Pick<AzureOpenAI, '_getAzureADToken' | 'apiKey'>) {
85-
if (client.apiKey !== '<Missing Key>') {
86-
return { 'api-key': client.apiKey };
87-
} else {
88-
const token = await client._getAzureADToken();
89-
if (token) {
90-
return { Authorization: `Bearer ${token}` };
91-
} else {
92-
throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.');
93-
}
94-
}
95-
}

0 commit comments

Comments
 (0)