@zhivex-ai/openai 0.13.6 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +29 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -878
- package/dist/index.js.map +1 -1
- package/dist/realtime.d.ts +27 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +631 -0
- package/dist/realtime.js.map +1 -0
- package/dist/resource-models.d.ts +57 -0
- package/dist/resource-models.d.ts.map +1 -0
- package/dist/resource-models.js +172 -0
- package/dist/resource-models.js.map +1 -0
- package/package.json +2 -2
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import { toJSONSchema } from "zod";
|
|
2
|
+
import { CallbackRealtimeSession, ConfigurationError, UnsupportedFeatureError, assertTrustedEndpoint, decodeBase64WithLimit, encodeAudioFrame, encodeMediaFrame, isCallableToolDefinition, openWebSocketConnection, toToolSet, toolResultPayload, withRetry, withTimeoutSignal } from "@zhivex-ai/core/provider";
|
|
3
|
+
import { inferOpenAIRealtimeMode, openAIRealtimeSupportsImageInput, realtimeCapabilities } from "./capabilities.js";
|
|
4
|
+
import { jsonHeaders, parseJson, RESERVED_REQUEST_HEADERS } from "./http.js";
|
|
5
|
+
const OPENAI_REALTIME_AUDIO_MAX_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
export const openAIRealtimeMcpMetadataKey = "openai.realtime_mcp";
|
|
7
|
+
export const resolveOpenAIRealtimeHeaders = (apiKey, providerOptions, includeContentType = false) => {
|
|
8
|
+
const rawHeaders = providerOptions?.headers;
|
|
9
|
+
const customHeaders = rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)
|
|
10
|
+
? { ...rawHeaders }
|
|
11
|
+
: {};
|
|
12
|
+
const safetyIdentifier = providerOptions?.safety_identifier;
|
|
13
|
+
for (const key of Object.keys(customHeaders)) {
|
|
14
|
+
const normalizedKey = key.toLowerCase();
|
|
15
|
+
if (RESERVED_REQUEST_HEADERS.has(normalizedKey) ||
|
|
16
|
+
(typeof safetyIdentifier === "string" && safetyIdentifier && normalizedKey === "openai-safety-identifier")) {
|
|
17
|
+
delete customHeaders[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...(includeContentType ? jsonHeaders(apiKey) : { authorization: `Bearer ${apiKey}` }),
|
|
22
|
+
...customHeaders,
|
|
23
|
+
...(typeof safetyIdentifier === "string" && safetyIdentifier
|
|
24
|
+
? { "OpenAI-Safety-Identifier": safetyIdentifier }
|
|
25
|
+
: {})
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const mapRealtimeTools = (input) => input
|
|
29
|
+
? Object.values(input).map((tool) => {
|
|
30
|
+
if (isCallableToolDefinition(tool)) {
|
|
31
|
+
return {
|
|
32
|
+
type: "function",
|
|
33
|
+
name: tool.name,
|
|
34
|
+
description: tool.description,
|
|
35
|
+
parameters: toJSONSchema(tool.schema)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (tool.provider && tool.provider !== "openai") {
|
|
39
|
+
throw new UnsupportedFeatureError(`Provider "openai" does not support hosted tools declared for provider "${tool.provider}".`);
|
|
40
|
+
}
|
|
41
|
+
if (tool.type !== "mcp") {
|
|
42
|
+
throw new UnsupportedFeatureError(`Provider "openai" Realtime does not support the hosted tool type "${tool.type}". Use function or MCP tools.`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: tool.type,
|
|
46
|
+
...(tool.config && typeof tool.config === "object" ? tool.config : {})
|
|
47
|
+
};
|
|
48
|
+
})
|
|
49
|
+
: undefined;
|
|
50
|
+
const mapRealtimeToolChoice = (toolChoice) => {
|
|
51
|
+
if (!toolChoice || typeof toolChoice === "string") {
|
|
52
|
+
return toolChoice;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
type: "function",
|
|
56
|
+
name: toolChoice.toolName
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const mapRealtimeProviderOptions = (providerOptions) => {
|
|
60
|
+
if (!providerOptions) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
return Object.fromEntries(Object.entries(providerOptions).filter(([key]) => !["headers", "safety_identifier", "realtime_url", "realtime_query", "expires_after"].includes(key)));
|
|
64
|
+
};
|
|
65
|
+
const mapRealtimeAudioFormat = (mediaType, sampleRateHz) => mediaType || sampleRateHz
|
|
66
|
+
? {
|
|
67
|
+
...(mediaType ? { type: mediaType } : {}),
|
|
68
|
+
...(sampleRateHz ? { rate: sampleRateHz } : {})
|
|
69
|
+
}
|
|
70
|
+
: undefined;
|
|
71
|
+
const mapRealtimeSessionConfig = (config, modelId) => {
|
|
72
|
+
const mode = inferOpenAIRealtimeMode(modelId ?? "", config.mode);
|
|
73
|
+
const tools = mapRealtimeTools(toToolSet(config.tools));
|
|
74
|
+
const audio = {
|
|
75
|
+
input: {
|
|
76
|
+
format: mapRealtimeAudioFormat(config.inputAudioMediaType, config.inputSampleRateHz),
|
|
77
|
+
transcription: mode === "transcription" || config.inputTranscription
|
|
78
|
+
? {
|
|
79
|
+
model: config.inputTranscription?.model ?? (mode === "transcription" ? modelId : undefined),
|
|
80
|
+
language: config.inputTranscription?.language,
|
|
81
|
+
prompt: config.inputTranscription?.prompt,
|
|
82
|
+
delay: config.inputTranscription?.delay
|
|
83
|
+
}
|
|
84
|
+
: undefined,
|
|
85
|
+
noise_reduction: config.noiseReduction ?? undefined,
|
|
86
|
+
turn_detection: config.turnDetection ?? undefined
|
|
87
|
+
},
|
|
88
|
+
...(mode === "transcription"
|
|
89
|
+
? {}
|
|
90
|
+
: {
|
|
91
|
+
output: {
|
|
92
|
+
format: mapRealtimeAudioFormat(config.outputAudioMediaType, config.outputSampleRateHz),
|
|
93
|
+
voice: config.voice
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
type: mode === "transcription" ? "transcription" : "realtime",
|
|
99
|
+
model: modelId,
|
|
100
|
+
instructions: config.translation?.instructions ?? config.instructions,
|
|
101
|
+
output_modalities: mode === "transcription" ? undefined : config.outputAudioMediaType || config.voice || mode === "translation" ? ["audio"] : ["text"],
|
|
102
|
+
tools: mode === "conversation" ? tools : undefined,
|
|
103
|
+
tool_choice: mode === "conversation" && config.toolChoice ? mapRealtimeToolChoice(config.toolChoice) : undefined,
|
|
104
|
+
reasoning: mode === "conversation" && config.reasoning?.effort
|
|
105
|
+
? {
|
|
106
|
+
effort: config.reasoning.effort
|
|
107
|
+
}
|
|
108
|
+
: undefined,
|
|
109
|
+
include: config.inputTranscription?.includeLogprobs && mode === "transcription" ? ["item.input_audio_transcription.logprobs"] : undefined,
|
|
110
|
+
translation: mode === "translation"
|
|
111
|
+
? {
|
|
112
|
+
target_language: config.translation?.targetLanguage,
|
|
113
|
+
source_language: config.translation?.sourceLanguage
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
116
|
+
audio,
|
|
117
|
+
...mapRealtimeProviderOptions(config.providerOptions)
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const openAIRealtimeURL = (baseURL, modelId, mode, providerOptions) => {
|
|
121
|
+
const override = providerOptions?.realtime_url;
|
|
122
|
+
if (typeof override === "string" && override) {
|
|
123
|
+
return override;
|
|
124
|
+
}
|
|
125
|
+
const url = new URL(baseURL);
|
|
126
|
+
url.protocol = url.protocol === "https:" ? "wss:" : url.protocol === "http:" ? "ws:" : url.protocol;
|
|
127
|
+
const endpoint = mode === "translation" ? "realtime/translations" : mode === "transcription" ? "realtime/transcription_sessions" : "realtime";
|
|
128
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${endpoint}`;
|
|
129
|
+
url.searchParams.set("model", modelId);
|
|
130
|
+
const extraQuery = providerOptions?.realtime_query;
|
|
131
|
+
if (extraQuery && typeof extraQuery === "object" && !Array.isArray(extraQuery)) {
|
|
132
|
+
for (const [key, value] of Object.entries(extraQuery)) {
|
|
133
|
+
if (value != null) {
|
|
134
|
+
url.searchParams.set(key, String(value));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return url.toString();
|
|
139
|
+
};
|
|
140
|
+
const parseRealtimeProviderMetadata = (payload) => payload;
|
|
141
|
+
const parseRealtimeJsonValue = (value) => {
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(value);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return value == null ? null : value;
|
|
151
|
+
};
|
|
152
|
+
const openAIRealtimeMcpProviderDataEvent = (payload, data) => [
|
|
153
|
+
{
|
|
154
|
+
type: "realtime-provider-data",
|
|
155
|
+
provider: "openai",
|
|
156
|
+
data: {
|
|
157
|
+
...data,
|
|
158
|
+
raw_event: payload
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
const openAIRealtimeMcpCommonData = (payload, item) => {
|
|
163
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof item?.id === "string" ? item.id : undefined;
|
|
164
|
+
const serverLabel = typeof payload.server_label === "string"
|
|
165
|
+
? payload.server_label
|
|
166
|
+
: typeof item?.server_label === "string"
|
|
167
|
+
? item.server_label
|
|
168
|
+
: undefined;
|
|
169
|
+
const name = typeof payload.name === "string" ? payload.name : typeof item?.name === "string" ? item.name : undefined;
|
|
170
|
+
return {
|
|
171
|
+
...(itemId ? { item_id: itemId } : {}),
|
|
172
|
+
...(serverLabel ? { server_label: serverLabel } : {}),
|
|
173
|
+
...(name ? { name } : {})
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const parseOpenAIRealtimeEvent = (payload) => {
|
|
177
|
+
const type = String(payload.type ?? "");
|
|
178
|
+
if (type === "session.created" || type === "session.updated") {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
if (type === "mcp_list_tools.in_progress" || type === "mcp_list_tools.completed" || type === "mcp_list_tools.failed") {
|
|
182
|
+
const status = type.slice("mcp_list_tools.".length);
|
|
183
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
184
|
+
type: "mcp_list_tools",
|
|
185
|
+
status,
|
|
186
|
+
...openAIRealtimeMcpCommonData(payload),
|
|
187
|
+
...(payload.error !== undefined ? { error: parseRealtimeJsonValue(payload.error) } : {})
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (type === "conversation.item.done") {
|
|
191
|
+
const item = payload.item && typeof payload.item === "object" ? payload.item : undefined;
|
|
192
|
+
if (item?.type === "mcp_list_tools") {
|
|
193
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
194
|
+
type: "mcp_list_tools",
|
|
195
|
+
status: "completed",
|
|
196
|
+
...openAIRealtimeMcpCommonData(payload, item),
|
|
197
|
+
...(item.tools !== undefined ? { tools: parseRealtimeJsonValue(item.tools) } : {})
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (item?.type === "mcp_approval_request") {
|
|
201
|
+
const approvalRequestId = typeof item.id === "string" ? item.id : undefined;
|
|
202
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
203
|
+
type: "mcp_approval_request",
|
|
204
|
+
status: "approval_required",
|
|
205
|
+
...openAIRealtimeMcpCommonData(payload, item),
|
|
206
|
+
...(approvalRequestId ? { approval_request_id: approvalRequestId } : {}),
|
|
207
|
+
...(item.arguments !== undefined ? { arguments: parseRealtimeJsonValue(item.arguments) } : {})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (item?.type === "mcp_approval_response") {
|
|
211
|
+
const approve = item.approve === true;
|
|
212
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
213
|
+
type: "mcp_approval_response",
|
|
214
|
+
status: approve ? "approved" : "rejected",
|
|
215
|
+
approve,
|
|
216
|
+
...openAIRealtimeMcpCommonData(payload, item),
|
|
217
|
+
...(typeof item.approval_request_id === "string" ? { approval_request_id: item.approval_request_id } : {}),
|
|
218
|
+
...(typeof item.reason === "string" ? { reason: item.reason } : {})
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (type === "response.mcp_call_arguments.delta" || type === "response.mcp_call_arguments.done") {
|
|
223
|
+
const isDone = type.endsWith(".done");
|
|
224
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
225
|
+
type: "mcp_call",
|
|
226
|
+
status: isDone ? "arguments_done" : "arguments_delta",
|
|
227
|
+
...openAIRealtimeMcpCommonData(payload),
|
|
228
|
+
...(isDone && payload.arguments !== undefined
|
|
229
|
+
? { arguments: parseRealtimeJsonValue(payload.arguments) }
|
|
230
|
+
: payload.delta !== undefined
|
|
231
|
+
? { delta: parseRealtimeJsonValue(payload.delta) }
|
|
232
|
+
: {})
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (type === "response.mcp_call.in_progress" || type === "response.mcp_call.completed" || type === "response.mcp_call.failed") {
|
|
236
|
+
const status = type.slice("response.mcp_call.".length);
|
|
237
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
238
|
+
type: "mcp_call",
|
|
239
|
+
status,
|
|
240
|
+
...openAIRealtimeMcpCommonData(payload),
|
|
241
|
+
...(payload.output !== undefined ? { output: parseRealtimeJsonValue(payload.output) } : {}),
|
|
242
|
+
...(payload.error !== undefined ? { error: parseRealtimeJsonValue(payload.error) } : {})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (type === "response.output_item.done") {
|
|
246
|
+
const item = payload.item && typeof payload.item === "object" ? payload.item : undefined;
|
|
247
|
+
if (item?.type === "mcp_call") {
|
|
248
|
+
return openAIRealtimeMcpProviderDataEvent(payload, {
|
|
249
|
+
type: "mcp_call",
|
|
250
|
+
status: item.status === "failed" ? "failed" : "completed",
|
|
251
|
+
...openAIRealtimeMcpCommonData(payload, item),
|
|
252
|
+
...(item.arguments !== undefined ? { arguments: parseRealtimeJsonValue(item.arguments) } : {}),
|
|
253
|
+
...(item.output !== undefined ? { output: parseRealtimeJsonValue(item.output) } : {}),
|
|
254
|
+
...(item.error !== undefined ? { error: parseRealtimeJsonValue(item.error) } : {}),
|
|
255
|
+
...(typeof item.approval_request_id === "string" ? { approval_request_id: item.approval_request_id } : {})
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (type === "response.text.delta" || type === "response.output_text.delta") {
|
|
260
|
+
return [
|
|
261
|
+
{
|
|
262
|
+
type: "realtime-text-delta",
|
|
263
|
+
textDelta: String(payload.delta ?? ""),
|
|
264
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
265
|
+
responseId: typeof payload.response_id === "string" ? payload.response_id : undefined,
|
|
266
|
+
role: "assistant",
|
|
267
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
if (type === "response.audio.delta" || type === "response.output_audio.delta") {
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
type: "realtime-audio-output",
|
|
275
|
+
audio: decodeBase64WithLimit(String(payload.delta ?? ""), {
|
|
276
|
+
maxBytes: OPENAI_REALTIME_AUDIO_MAX_BYTES,
|
|
277
|
+
provider: "openai",
|
|
278
|
+
endpoint: "realtime"
|
|
279
|
+
}),
|
|
280
|
+
mediaType: typeof payload.media_type === "string" ? payload.media_type : "audio/pcm",
|
|
281
|
+
sampleRateHz: typeof payload.sample_rate_hz === "number" ? payload.sample_rate_hz : undefined,
|
|
282
|
+
channels: typeof payload.channels === "number" ? payload.channels : undefined,
|
|
283
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
284
|
+
responseId: typeof payload.response_id === "string" ? payload.response_id : undefined,
|
|
285
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
286
|
+
}
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
if (type === "conversation.item.input_audio_transcription.delta" || type === "input_audio_buffer.transcription.delta") {
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
type: "realtime-transcript",
|
|
293
|
+
text: String(payload.delta ?? ""),
|
|
294
|
+
role: "user",
|
|
295
|
+
isFinal: false,
|
|
296
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
297
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
if (type === "conversation.item.input_audio_transcription.completed" || type === "input_audio_buffer.transcription.completed") {
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
type: "realtime-transcript",
|
|
305
|
+
text: String(payload.transcript ?? ""),
|
|
306
|
+
role: "user",
|
|
307
|
+
isFinal: true,
|
|
308
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
309
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
310
|
+
}
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
if (type === "response.audio_transcript.delta" || type === "response.audio_transcription.delta" || type === "response.output_audio_transcript.delta") {
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
type: "realtime-transcript",
|
|
317
|
+
text: String(payload.delta ?? ""),
|
|
318
|
+
role: "assistant",
|
|
319
|
+
isFinal: false,
|
|
320
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
321
|
+
responseId: typeof payload.response_id === "string" ? payload.response_id : undefined,
|
|
322
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
323
|
+
}
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
if (type === "response.audio_transcript.done" || type === "response.audio_transcription.done" || type === "response.output_audio_transcript.done") {
|
|
327
|
+
return [
|
|
328
|
+
{
|
|
329
|
+
type: "realtime-transcript",
|
|
330
|
+
text: String(payload.transcript ?? ""),
|
|
331
|
+
role: "assistant",
|
|
332
|
+
isFinal: true,
|
|
333
|
+
itemId: typeof payload.item_id === "string" ? payload.item_id : undefined,
|
|
334
|
+
responseId: typeof payload.response_id === "string" ? payload.response_id : undefined,
|
|
335
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
if (type === "response.function_call_arguments.done" || type === "response.output_item.done") {
|
|
340
|
+
const name = typeof payload.name === "string"
|
|
341
|
+
? payload.name
|
|
342
|
+
: payload.item && typeof payload.item === "object" && typeof payload.item.name === "string"
|
|
343
|
+
? String(payload.item.name)
|
|
344
|
+
: undefined;
|
|
345
|
+
const callId = typeof payload.call_id === "string"
|
|
346
|
+
? payload.call_id
|
|
347
|
+
: payload.item && typeof payload.item === "object" && typeof payload.item.call_id === "string"
|
|
348
|
+
? String(payload.item.call_id)
|
|
349
|
+
: undefined;
|
|
350
|
+
const rawArgs = typeof payload.arguments === "string"
|
|
351
|
+
? payload.arguments
|
|
352
|
+
: payload.item && typeof payload.item === "object" && typeof payload.item.arguments === "string"
|
|
353
|
+
? String(payload.item.arguments)
|
|
354
|
+
: "{}";
|
|
355
|
+
if (!name || !callId) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
return [
|
|
359
|
+
{
|
|
360
|
+
type: "realtime-tool-call",
|
|
361
|
+
toolCall: {
|
|
362
|
+
id: callId,
|
|
363
|
+
name,
|
|
364
|
+
input: JSON.parse(rawArgs || "{}")
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
if (type === "response.done") {
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
type: "realtime-response-complete",
|
|
373
|
+
reason: typeof payload.status === "string" ? payload.status : undefined,
|
|
374
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
if (type === "error") {
|
|
379
|
+
const error = payload.error && typeof payload.error === "object" ? payload.error : undefined;
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
type: "realtime-error",
|
|
383
|
+
message: typeof payload.message === "string"
|
|
384
|
+
? payload.message
|
|
385
|
+
: typeof error?.message === "string"
|
|
386
|
+
? error.message
|
|
387
|
+
: "Realtime API error.",
|
|
388
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
389
|
+
}
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
if (type === "session.end") {
|
|
393
|
+
return [
|
|
394
|
+
{
|
|
395
|
+
type: "realtime-end",
|
|
396
|
+
reason: "session-end",
|
|
397
|
+
providerMetadata: parseRealtimeProviderMetadata(payload)
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
};
|
|
403
|
+
export class OpenAIRealtimeModel {
|
|
404
|
+
modelId;
|
|
405
|
+
apiKey;
|
|
406
|
+
baseURL;
|
|
407
|
+
fetcher;
|
|
408
|
+
connectionFactory;
|
|
409
|
+
realtimeURL;
|
|
410
|
+
browserTokenURL;
|
|
411
|
+
allowUnsafeEndpoints;
|
|
412
|
+
provider = "openai";
|
|
413
|
+
capabilities;
|
|
414
|
+
constructor(modelId, apiKey, baseURL, fetcher, connectionFactory, realtimeURL, browserTokenURL, allowUnsafeEndpoints = false) {
|
|
415
|
+
this.modelId = modelId;
|
|
416
|
+
this.apiKey = apiKey;
|
|
417
|
+
this.baseURL = baseURL;
|
|
418
|
+
this.fetcher = fetcher;
|
|
419
|
+
this.connectionFactory = connectionFactory;
|
|
420
|
+
this.realtimeURL = realtimeURL;
|
|
421
|
+
this.browserTokenURL = browserTokenURL;
|
|
422
|
+
this.allowUnsafeEndpoints = allowUnsafeEndpoints;
|
|
423
|
+
this.capabilities = realtimeCapabilities(modelId);
|
|
424
|
+
}
|
|
425
|
+
resolveConfig(config) {
|
|
426
|
+
if (config.delegation)
|
|
427
|
+
throw new UnsupportedFeatureError("Client delegation requires a GPT-Live model.");
|
|
428
|
+
const mode = inferOpenAIRealtimeMode(this.modelId, config.mode);
|
|
429
|
+
if (mode !== "conversation" && (config.tools || config.toolChoice)) {
|
|
430
|
+
throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime tools in ${mode} mode.`);
|
|
431
|
+
}
|
|
432
|
+
if (mode === "translation" && !config.translation?.targetLanguage) {
|
|
433
|
+
throw new ConfigurationError('OpenAI realtime translation sessions require "translation.targetLanguage".');
|
|
434
|
+
}
|
|
435
|
+
if (mode === "transcription" && config.voice) {
|
|
436
|
+
throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime audio output in transcription mode.`);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
mode,
|
|
440
|
+
autoResponse: mode === "conversation",
|
|
441
|
+
...config
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async connect(config = {}, options) {
|
|
445
|
+
const initialConfig = this.resolveConfig(config);
|
|
446
|
+
const providerOptions = initialConfig.providerOptions;
|
|
447
|
+
const headers = resolveOpenAIRealtimeHeaders(this.apiKey, providerOptions);
|
|
448
|
+
const trustedRealtimeHost = new URL(this.realtimeURL ?? this.baseURL).hostname;
|
|
449
|
+
const realtimeEndpoint = assertTrustedEndpoint(this.realtimeURL ??
|
|
450
|
+
openAIRealtimeURL(this.baseURL, this.modelId, inferOpenAIRealtimeMode(this.modelId, initialConfig.mode), providerOptions), {
|
|
451
|
+
label: "OpenAI realtime endpoint",
|
|
452
|
+
protocols: ["wss"],
|
|
453
|
+
allowedHosts: [trustedRealtimeHost],
|
|
454
|
+
allowUnsafe: this.allowUnsafeEndpoints
|
|
455
|
+
}).toString();
|
|
456
|
+
const connection = await (this.connectionFactory ?? openWebSocketConnection)(realtimeEndpoint, headers, options);
|
|
457
|
+
const session = new CallbackRealtimeSession({
|
|
458
|
+
provider: this.provider,
|
|
459
|
+
modelId: this.modelId,
|
|
460
|
+
capabilities: this.capabilities,
|
|
461
|
+
config: initialConfig,
|
|
462
|
+
connection,
|
|
463
|
+
callbacks: {
|
|
464
|
+
parseEvent: parseOpenAIRealtimeEvent,
|
|
465
|
+
buildAudioPayloads: (frame, sessionConfig) => {
|
|
466
|
+
const mode = inferOpenAIRealtimeMode(this.modelId, sessionConfig.mode);
|
|
467
|
+
const payloads = [
|
|
468
|
+
{
|
|
469
|
+
type: "input_audio_buffer.append",
|
|
470
|
+
audio: encodeAudioFrame(frame)
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
if (frame.isFinal) {
|
|
474
|
+
payloads.push({ type: "input_audio_buffer.commit" });
|
|
475
|
+
if (mode === "conversation" && (sessionConfig.autoResponse ?? true)) {
|
|
476
|
+
payloads.push({ type: "response.create" });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return payloads;
|
|
480
|
+
},
|
|
481
|
+
buildMediaPayloads: (frame) => {
|
|
482
|
+
if (!openAIRealtimeSupportsImageInput(this.modelId)) {
|
|
483
|
+
throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime image input.`);
|
|
484
|
+
}
|
|
485
|
+
if (!frame.mediaType.startsWith("image/")) {
|
|
486
|
+
throw new UnsupportedFeatureError(`Provider "openai" only supports realtime image media input, but received "${frame.mediaType}".`);
|
|
487
|
+
}
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
type: "conversation.item.create",
|
|
491
|
+
item: {
|
|
492
|
+
type: "message",
|
|
493
|
+
role: "user",
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "input_image",
|
|
497
|
+
image_url: `data:${frame.mediaType};base64,${encodeMediaFrame(frame)}`
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
},
|
|
504
|
+
buildTextPayloads: (text, sessionConfig) => {
|
|
505
|
+
const mode = inferOpenAIRealtimeMode(this.modelId, sessionConfig.mode);
|
|
506
|
+
if (mode !== "conversation") {
|
|
507
|
+
throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime text input in ${mode} mode.`);
|
|
508
|
+
}
|
|
509
|
+
const payloads = [
|
|
510
|
+
{
|
|
511
|
+
type: "conversation.item.create",
|
|
512
|
+
item: {
|
|
513
|
+
type: "message",
|
|
514
|
+
role: "user",
|
|
515
|
+
content: [{ type: "input_text", text }]
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
if (sessionConfig.autoResponse ?? true) {
|
|
520
|
+
payloads.push({ type: "response.create" });
|
|
521
|
+
}
|
|
522
|
+
return payloads;
|
|
523
|
+
},
|
|
524
|
+
buildToolResultPayloads: (result, sessionConfig) => {
|
|
525
|
+
const mode = inferOpenAIRealtimeMode(this.modelId, sessionConfig.mode);
|
|
526
|
+
if (mode !== "conversation") {
|
|
527
|
+
throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime tools in ${mode} mode.`);
|
|
528
|
+
}
|
|
529
|
+
const mcpMetadata = result.providerMetadata?.[openAIRealtimeMcpMetadataKey];
|
|
530
|
+
if (mcpMetadata && typeof mcpMetadata === "object" && !Array.isArray(mcpMetadata)) {
|
|
531
|
+
const approval = mcpMetadata;
|
|
532
|
+
if (approval.type === "mcp_approval_response" &&
|
|
533
|
+
typeof approval.approval_request_id === "string" &&
|
|
534
|
+
typeof approval.approve === "boolean") {
|
|
535
|
+
return [
|
|
536
|
+
{
|
|
537
|
+
type: "conversation.item.create",
|
|
538
|
+
item: {
|
|
539
|
+
...(typeof approval.id === "string" ? { id: approval.id } : {}),
|
|
540
|
+
type: "mcp_approval_response",
|
|
541
|
+
approval_request_id: approval.approval_request_id,
|
|
542
|
+
approve: approval.approve,
|
|
543
|
+
...(typeof approval.reason === "string" ? { reason: approval.reason } : {})
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const payloads = [
|
|
550
|
+
{
|
|
551
|
+
type: "conversation.item.create",
|
|
552
|
+
item: {
|
|
553
|
+
type: "function_call_output",
|
|
554
|
+
call_id: result.toolCallId,
|
|
555
|
+
output: JSON.stringify(toolResultPayload(result))
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
];
|
|
559
|
+
if (sessionConfig.autoResponse ?? true) {
|
|
560
|
+
payloads.push({ type: "response.create" });
|
|
561
|
+
}
|
|
562
|
+
return payloads;
|
|
563
|
+
},
|
|
564
|
+
buildUpdatePayloads: (sessionConfig) => [
|
|
565
|
+
{
|
|
566
|
+
type: "session.update",
|
|
567
|
+
session: mapRealtimeSessionConfig(this.resolveConfig(sessionConfig), this.modelId)
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
buildInitialPayloads: (sessionConfig) => [
|
|
571
|
+
{
|
|
572
|
+
type: "session.update",
|
|
573
|
+
session: mapRealtimeSessionConfig(sessionConfig, this.modelId)
|
|
574
|
+
}
|
|
575
|
+
]
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
await session.initialize();
|
|
579
|
+
return session;
|
|
580
|
+
}
|
|
581
|
+
async createBrowserToken(config = {}, options) {
|
|
582
|
+
const resolvedConfig = this.resolveConfig(config);
|
|
583
|
+
const providerOptions = { ...(resolvedConfig.providerOptions ?? {}) };
|
|
584
|
+
const expiresAfter = providerOptions.expires_after;
|
|
585
|
+
delete providerOptions.expires_after;
|
|
586
|
+
const { signal, cleanup } = withTimeoutSignal({
|
|
587
|
+
abortSignal: options?.signal,
|
|
588
|
+
timeoutMs: options?.timeoutMs
|
|
589
|
+
});
|
|
590
|
+
try {
|
|
591
|
+
const response = await withRetry(() => this.fetcher(this.browserTokenURL ?? `${this.baseURL}/realtime/client_secrets`, {
|
|
592
|
+
method: "POST",
|
|
593
|
+
redirect: "error",
|
|
594
|
+
headers: resolveOpenAIRealtimeHeaders(this.apiKey, providerOptions, true),
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
...(expiresAfter ? { expires_after: expiresAfter } : {}),
|
|
597
|
+
session: mapRealtimeSessionConfig({
|
|
598
|
+
...resolvedConfig,
|
|
599
|
+
providerOptions
|
|
600
|
+
}, this.modelId)
|
|
601
|
+
}),
|
|
602
|
+
signal
|
|
603
|
+
}));
|
|
604
|
+
const payload = await parseJson(response);
|
|
605
|
+
const secret = payload.client_secret;
|
|
606
|
+
const value = secret && typeof secret === "object" && typeof secret.value === "string"
|
|
607
|
+
? secret.value
|
|
608
|
+
: typeof payload.token === "string"
|
|
609
|
+
? payload.token
|
|
610
|
+
: typeof payload.value === "string"
|
|
611
|
+
? payload.value
|
|
612
|
+
: "";
|
|
613
|
+
const expiresAtMs = secret && typeof secret === "object" && typeof secret.expires_at_ms === "number"
|
|
614
|
+
? secret.expires_at_ms
|
|
615
|
+
: typeof payload.expires_at_ms === "number"
|
|
616
|
+
? payload.expires_at_ms
|
|
617
|
+
: typeof payload.expires_at === "number"
|
|
618
|
+
? payload.expires_at * 1000
|
|
619
|
+
: undefined;
|
|
620
|
+
return {
|
|
621
|
+
value,
|
|
622
|
+
expiresAtMs,
|
|
623
|
+
rawResponse: payload
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
cleanup();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
//# sourceMappingURL=realtime.js.map
|