@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.
@@ -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