@memberjunction/ai-openai 6.1.0-edge.6 → 6.1.0
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/models/openAI.d.ts +71 -1
- package/dist/models/openAI.d.ts.map +1 -1
- package/dist/models/openAI.js +125 -11
- package/dist/models/openAI.js.map +1 -1
- package/dist/models/openAILiveRealtime.d.ts +297 -0
- package/dist/models/openAILiveRealtime.d.ts.map +1 -0
- package/dist/models/openAILiveRealtime.js +886 -0
- package/dist/models/openAILiveRealtime.js.map +1 -0
- package/dist/models/openAIRealtime.d.ts +6 -0
- package/dist/models/openAIRealtime.d.ts.map +1 -1
- package/dist/models/openAIRealtime.js +11 -1
- package/dist/models/openAIRealtime.js.map +1 -1
- package/package.json +4 -3
- package/readme.md +8 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var OpenAILiveRealtime_1;
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
13
|
+
import { BaseRealtimeModel, RealtimeDiagLog, RealtimeProxyRegistry, REALTIME_SDP_EXCHANGE_PATH, RealtimeToolBatchBarrier, } from '@memberjunction/ai';
|
|
14
|
+
import { MapUsageModalityDetail } from './openAIRealtime.js';
|
|
15
|
+
/**
|
|
16
|
+
* Live {@link IRealtimeSession} handle speaking the OpenAI Live wire protocol
|
|
17
|
+
* (`wss://api.openai.com/v1/live/sessions`).
|
|
18
|
+
*/
|
|
19
|
+
export class OpenAILiveSession {
|
|
20
|
+
constructor(socket, params, options = {}) {
|
|
21
|
+
this._active = false;
|
|
22
|
+
this._closed = false;
|
|
23
|
+
this._closeRequested = false;
|
|
24
|
+
this._lastReportedSeconds = 0;
|
|
25
|
+
this._countedResponseIds = new Set();
|
|
26
|
+
this._currentTaskRevision = 0;
|
|
27
|
+
this._toolBatchBarrier = new RealtimeToolBatchBarrier();
|
|
28
|
+
this._outputHandlers = [];
|
|
29
|
+
this._transcriptHandlers = [];
|
|
30
|
+
this._toolCallHandlers = [];
|
|
31
|
+
this._errorHandlers = [];
|
|
32
|
+
this._usageHandlers = [];
|
|
33
|
+
this._interruptionHandlers = [];
|
|
34
|
+
this._closeHandlers = [];
|
|
35
|
+
this._socket = socket;
|
|
36
|
+
this._params = params;
|
|
37
|
+
this._options = options;
|
|
38
|
+
const codec = options.audioCodec ?? 'pcm16';
|
|
39
|
+
const rate = options.sampleRate ?? (codec === 'g711_ulaw' || codec === 'g711_alaw' ? 8000 : 24000);
|
|
40
|
+
this.InputSampleRate = rate;
|
|
41
|
+
this.OutputSampleRate = rate;
|
|
42
|
+
this.AudioFormat = { Codec: codec, SampleRate: rate };
|
|
43
|
+
this.Capabilities = {
|
|
44
|
+
CanReconfigureTurnMode: false,
|
|
45
|
+
SupportedReasoningPlanes: ['local', 'remote'],
|
|
46
|
+
CanReconfigureDelegationMode: false,
|
|
47
|
+
EmitsUserInterruptionSignal: false,
|
|
48
|
+
EmitsProviderCutoffSignal: true,
|
|
49
|
+
EmitsResponseComplete: false,
|
|
50
|
+
UsageBases: ['seconds', 'tokens'],
|
|
51
|
+
ProvidesInputTranscription: true,
|
|
52
|
+
ProvidesOutputTranscription: true,
|
|
53
|
+
SupportsParallelToolCalls: true,
|
|
54
|
+
SupportsDynamicToolSet: OpenAILiveRealtime.SupportsDynamicToolSet,
|
|
55
|
+
};
|
|
56
|
+
this._startPromise = new Promise((resolve, reject) => {
|
|
57
|
+
this._resolveStart = resolve;
|
|
58
|
+
this._rejectStart = reject;
|
|
59
|
+
});
|
|
60
|
+
this.wireSocket();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Current task revision counter for cancellation and supersede-and-discard.
|
|
64
|
+
*/
|
|
65
|
+
get CurrentTaskRevision() {
|
|
66
|
+
return this._currentTaskRevision;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Advances the task revision counter, causing in-flight tool results from prior revisions to be discarded.
|
|
70
|
+
*/
|
|
71
|
+
BumpTaskRevision() {
|
|
72
|
+
this._toolBatchBarrier.Clear();
|
|
73
|
+
return ++this._currentTaskRevision;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Waits until the initial `session.started` confirmation frame arrives from the server.
|
|
77
|
+
*/
|
|
78
|
+
async WaitForStarted() {
|
|
79
|
+
return this._startPromise;
|
|
80
|
+
}
|
|
81
|
+
wireSocket() {
|
|
82
|
+
this._socket.addEventListener('open', () => {
|
|
83
|
+
RealtimeDiagLog('[OpenAILiveSession][diag] Socket opened, sending session.start');
|
|
84
|
+
this.sendSessionStart();
|
|
85
|
+
});
|
|
86
|
+
this._socket.addEventListener('message', (event) => {
|
|
87
|
+
if (typeof event.data === 'string') {
|
|
88
|
+
this.handleMessage(event.data);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
this._socket.addEventListener('error', (event) => {
|
|
92
|
+
const msg = event.message ?? 'OpenAI Live socket error';
|
|
93
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Socket error: ${msg}`);
|
|
94
|
+
if (!this._active) {
|
|
95
|
+
this._rejectStart(new Error(msg));
|
|
96
|
+
}
|
|
97
|
+
this.dispatchError({
|
|
98
|
+
Message: msg,
|
|
99
|
+
Fatal: !this._active,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
this._socket.addEventListener('close', (event) => {
|
|
103
|
+
const reason = event.reason || 'Socket closed';
|
|
104
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Socket closed: code=${event.code}, reason=${reason}`);
|
|
105
|
+
this._closed = true;
|
|
106
|
+
if (!this._active) {
|
|
107
|
+
this._rejectStart(new Error(`Socket closed before session.started: ${reason}`));
|
|
108
|
+
}
|
|
109
|
+
if (this._closeRequested) {
|
|
110
|
+
this._resolveClose?.();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const h of this._closeHandlers) {
|
|
114
|
+
h();
|
|
115
|
+
}
|
|
116
|
+
this.dispatchError({
|
|
117
|
+
Message: `OpenAI Live connection closed unexpectedly: ${reason}`,
|
|
118
|
+
Fatal: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
sendSessionStart() {
|
|
124
|
+
const compiledInstructions = this.buildInitialInstructions();
|
|
125
|
+
const wireAudioFormat = this.resolveWireAudioFormat();
|
|
126
|
+
const plane = this._options.reasoningPlane ?? 'local';
|
|
127
|
+
const mappedTools = this._params.Tools?.map((t) => ({
|
|
128
|
+
type: 'function',
|
|
129
|
+
name: t.Name,
|
|
130
|
+
description: t.Description,
|
|
131
|
+
parameters: t.ParametersSchema,
|
|
132
|
+
}));
|
|
133
|
+
const hasTools = !!(mappedTools && mappedTools.length > 0);
|
|
134
|
+
const rawParallelToolCalls = this._params.Config?.['parallelToolCalls'] ?? this._params.Config?.['parallel_tool_calls'];
|
|
135
|
+
const parallelToolCalls = typeof rawParallelToolCalls === 'boolean' ? rawParallelToolCalls : undefined;
|
|
136
|
+
const startFrame = {
|
|
137
|
+
event_id: randomUUID(),
|
|
138
|
+
type: 'session.start',
|
|
139
|
+
session: {
|
|
140
|
+
model: this._params.Model || 'gpt-live-1',
|
|
141
|
+
instructions: compiledInstructions,
|
|
142
|
+
audio: {
|
|
143
|
+
format: wireAudioFormat,
|
|
144
|
+
output: {
|
|
145
|
+
voice: this._options.voice ?? 'alloy',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
...(typeof parallelToolCalls === 'boolean' ? { parallel_tool_calls: parallelToolCalls } : {}),
|
|
149
|
+
delegation: {
|
|
150
|
+
type: plane === 'remote' || hasTools ? 'responses' : 'client',
|
|
151
|
+
...(plane === 'remote' || hasTools
|
|
152
|
+
? {
|
|
153
|
+
responses: {
|
|
154
|
+
model: this._options.remoteSettings?.Ref ?? 'gpt-4o',
|
|
155
|
+
...(this._options.remoteSettings?.Effort
|
|
156
|
+
? {
|
|
157
|
+
reasoning: {
|
|
158
|
+
effort: this._options.remoteSettings.Effort,
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
...(typeof this._options.remoteSettings?.MaxOutputTokens === 'number'
|
|
163
|
+
? {
|
|
164
|
+
max_output_tokens: this._options.remoteSettings.MaxOutputTokens,
|
|
165
|
+
}
|
|
166
|
+
: {}),
|
|
167
|
+
...(mappedTools && mappedTools.length > 0 ? { tools: mappedTools } : {}),
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
this.sendFrame(startFrame);
|
|
175
|
+
}
|
|
176
|
+
resolveWireAudioFormat() {
|
|
177
|
+
const codec = this.AudioFormat && typeof this.AudioFormat === 'object' ? this.AudioFormat.Codec : 'pcm16';
|
|
178
|
+
if (codec === 'g711_ulaw') {
|
|
179
|
+
return { type: 'audio/pcmu' };
|
|
180
|
+
}
|
|
181
|
+
if (codec === 'g711_alaw') {
|
|
182
|
+
return { type: 'audio/pcma' };
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
type: 'audio/pcm',
|
|
186
|
+
rate: this.InputSampleRate,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
buildInitialInstructions() {
|
|
190
|
+
const sections = [];
|
|
191
|
+
if (this._params.SystemPrompt) {
|
|
192
|
+
sections.push(this._params.SystemPrompt.trim());
|
|
193
|
+
}
|
|
194
|
+
if (this._params.InitialContext) {
|
|
195
|
+
sections.push(this._params.InitialContext.trim());
|
|
196
|
+
}
|
|
197
|
+
if (this._params.Tools && this._params.Tools.length > 0) {
|
|
198
|
+
sections.push(this.compileDelegationPolicy(this._params.Tools));
|
|
199
|
+
}
|
|
200
|
+
const interruptionPolicy = this.resolveInterruptionPolicy();
|
|
201
|
+
if (interruptionPolicy) {
|
|
202
|
+
sections.push(`Interruption policy:\n${interruptionPolicy}`);
|
|
203
|
+
}
|
|
204
|
+
const backchannelPolicy = this.resolveBackchannelPolicy();
|
|
205
|
+
if (backchannelPolicy) {
|
|
206
|
+
sections.push(`Backchannel policy:\n${backchannelPolicy}`);
|
|
207
|
+
}
|
|
208
|
+
return sections.join('\n\n');
|
|
209
|
+
}
|
|
210
|
+
extractPolicyFromConfig(policyKey) {
|
|
211
|
+
const direct = this._options[policyKey];
|
|
212
|
+
if (typeof direct === 'string' && direct.trim().length > 0) {
|
|
213
|
+
return direct.trim();
|
|
214
|
+
}
|
|
215
|
+
const fromStyle = this._options.styleDescriptors?.[policyKey];
|
|
216
|
+
if (typeof fromStyle === 'string' && fromStyle.trim().length > 0) {
|
|
217
|
+
return fromStyle.trim();
|
|
218
|
+
}
|
|
219
|
+
// options.persona has an inline structural shape, so it may receive a plain deserialized
|
|
220
|
+
// object where StyleDescriptors is already parsed as an object (e.g. in test fixtures or
|
|
221
|
+
// JSON configs) rather than the entity class instance where StyleDescriptors is a raw
|
|
222
|
+
// JSON string and StyleDescriptorsObject is the parsed accessor.
|
|
223
|
+
const personaStyleObj = this._options.persona?.StyleDescriptorsObject ??
|
|
224
|
+
(typeof this._options.persona?.['StyleDescriptors'] === 'object' && this._options.persona?.['StyleDescriptors'] !== null
|
|
225
|
+
? this._options.persona['StyleDescriptors']
|
|
226
|
+
: undefined);
|
|
227
|
+
const fromPersona = personaStyleObj?.[policyKey];
|
|
228
|
+
if (typeof fromPersona === 'string' && fromPersona.trim().length > 0) {
|
|
229
|
+
return fromPersona.trim();
|
|
230
|
+
}
|
|
231
|
+
const config = this._params.Config;
|
|
232
|
+
if (config) {
|
|
233
|
+
const configDirect = config[policyKey];
|
|
234
|
+
if (typeof configDirect === 'string' && configDirect.trim().length > 0) {
|
|
235
|
+
return configDirect.trim();
|
|
236
|
+
}
|
|
237
|
+
const configStyle = config['styleDescriptors'];
|
|
238
|
+
if (configStyle && typeof configStyle === 'object' && policyKey in configStyle) {
|
|
239
|
+
const val = configStyle[policyKey];
|
|
240
|
+
if (typeof val === 'string' && val.trim().length > 0) {
|
|
241
|
+
return val.trim();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const configPersona = config['persona'];
|
|
245
|
+
if (configPersona && typeof configPersona === 'object') {
|
|
246
|
+
const p = configPersona;
|
|
247
|
+
const sdo = p['StyleDescriptorsObject'] ?? (typeof p['StyleDescriptors'] === 'object' ? p['StyleDescriptors'] : undefined);
|
|
248
|
+
if (sdo && typeof sdo === 'object' && sdo !== null && policyKey in sdo) {
|
|
249
|
+
const val = sdo[policyKey];
|
|
250
|
+
if (typeof val === 'string' && val.trim().length > 0) {
|
|
251
|
+
return val.trim();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
resolveInterruptionPolicy() {
|
|
259
|
+
return this.extractPolicyFromConfig('interruptionPolicy');
|
|
260
|
+
}
|
|
261
|
+
resolveBackchannelPolicy() {
|
|
262
|
+
return this.extractPolicyFromConfig('backchannelPolicy');
|
|
263
|
+
}
|
|
264
|
+
compileDelegationPolicy(tools) {
|
|
265
|
+
return OpenAILiveRealtime.CompileDelegationPolicy(tools);
|
|
266
|
+
}
|
|
267
|
+
handleMessage(raw) {
|
|
268
|
+
let event;
|
|
269
|
+
try {
|
|
270
|
+
event = JSON.parse(raw);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Received non-JSON frame: ${raw.slice(0, 100)}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
switch (event.type) {
|
|
277
|
+
case 'session.started': {
|
|
278
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] session.started (id: ${event.session_id})`);
|
|
279
|
+
this._active = true;
|
|
280
|
+
this._resolveStart();
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'session.output_audio.delta': {
|
|
284
|
+
if (event.delta) {
|
|
285
|
+
const buf = Buffer.from(event.delta, 'base64');
|
|
286
|
+
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
287
|
+
for (const handler of this._outputHandlers) {
|
|
288
|
+
handler(arrayBuffer);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'session.input_transcript.delta': {
|
|
294
|
+
if (event.delta) {
|
|
295
|
+
const t = {
|
|
296
|
+
Text: event.delta,
|
|
297
|
+
IsFinal: false,
|
|
298
|
+
Role: 'user',
|
|
299
|
+
};
|
|
300
|
+
for (const handler of this._transcriptHandlers) {
|
|
301
|
+
handler(t);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case 'session.output_transcript.delta': {
|
|
307
|
+
if (event.delta) {
|
|
308
|
+
const t = {
|
|
309
|
+
Text: event.delta,
|
|
310
|
+
IsFinal: false,
|
|
311
|
+
Role: 'assistant',
|
|
312
|
+
};
|
|
313
|
+
for (const handler of this._transcriptHandlers) {
|
|
314
|
+
handler(t);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'session.usage.updated': {
|
|
320
|
+
if (typeof event.usage?.seconds === 'number') {
|
|
321
|
+
this._lastReportedSeconds = event.usage.seconds;
|
|
322
|
+
const u = {
|
|
323
|
+
InputTokens: 0,
|
|
324
|
+
OutputTokens: 0,
|
|
325
|
+
DurationSeconds: this._lastReportedSeconds,
|
|
326
|
+
};
|
|
327
|
+
for (const handler of this._usageHandlers) {
|
|
328
|
+
handler(u);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case 'session.delegation.created': {
|
|
334
|
+
// In remote reasoning plane, session.delegation.created is remote lifecycle notice only.
|
|
335
|
+
// Tool calls are dispatched via response.event (response.output_item.done).
|
|
336
|
+
if (this._options.reasoningPlane !== 'remote' && event.delegation_id) {
|
|
337
|
+
const call = {
|
|
338
|
+
CallID: event.delegation_id,
|
|
339
|
+
ToolName: 'backend_delegation',
|
|
340
|
+
Arguments: JSON.stringify({ delegation_id: event.delegation_id }),
|
|
341
|
+
TaskRevision: this._currentTaskRevision,
|
|
342
|
+
};
|
|
343
|
+
for (const handler of this._toolCallHandlers) {
|
|
344
|
+
handler(call);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'response.event': {
|
|
350
|
+
if (!event.event) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
// Trap 1: Read completed tool calls from nested response.output_item.done
|
|
354
|
+
if (event.event.type === 'response.output_item.done') {
|
|
355
|
+
const item = event.event.item;
|
|
356
|
+
if (item?.type === 'function_call' && item.call_id && item.name) {
|
|
357
|
+
this._toolBatchBarrier.TrackPendingCall(item.call_id, () => {
|
|
358
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Tool batch timed out with pending calls; flushing response.create`);
|
|
359
|
+
this.sendFrame({
|
|
360
|
+
event_id: randomUUID(),
|
|
361
|
+
type: 'response.create',
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
const call = {
|
|
365
|
+
CallID: item.call_id,
|
|
366
|
+
ToolName: item.name,
|
|
367
|
+
Arguments: item.arguments ?? '{}',
|
|
368
|
+
TaskRevision: this._currentTaskRevision,
|
|
369
|
+
};
|
|
370
|
+
for (const handler of this._toolCallHandlers) {
|
|
371
|
+
handler(call);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (event.event.type === 'response.completed') {
|
|
376
|
+
// Trap 2 & Trap 4: response.output is empty even with pending calls;
|
|
377
|
+
// Deduplicate token usage by response.id:
|
|
378
|
+
const resp = event.event.response;
|
|
379
|
+
if (resp?.id) {
|
|
380
|
+
if (this._countedResponseIds.has(resp.id)) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
this._countedResponseIds.add(resp.id);
|
|
384
|
+
}
|
|
385
|
+
if (resp?.usage) {
|
|
386
|
+
const u = {
|
|
387
|
+
InputTokens: resp.usage.input_tokens ?? 0,
|
|
388
|
+
OutputTokens: resp.usage.output_tokens ?? 0,
|
|
389
|
+
DurationSeconds: this._lastReportedSeconds,
|
|
390
|
+
InputTokenDetails: MapUsageModalityDetail(resp.usage.input_token_details),
|
|
391
|
+
OutputTokenDetails: MapUsageModalityDetail(resp.usage.output_token_details),
|
|
392
|
+
};
|
|
393
|
+
for (const handler of this._usageHandlers) {
|
|
394
|
+
handler(u);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case 'error': {
|
|
401
|
+
const err = event.error;
|
|
402
|
+
const message = err?.message || 'Unknown OpenAI Live error';
|
|
403
|
+
const isFatal = !this._active || err?.code === 'session_expired' || err?.code === 'unauthorized';
|
|
404
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Provider error: code=${err?.code}, msg=${message}`);
|
|
405
|
+
if (err?.code === 'moderation_cutoff' || err?.code === 'provider_cutoff') {
|
|
406
|
+
this.BumpTaskRevision();
|
|
407
|
+
for (const handler of this._interruptionHandlers) {
|
|
408
|
+
handler();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.dispatchError({
|
|
412
|
+
Message: message,
|
|
413
|
+
Code: err?.code,
|
|
414
|
+
Fatal: isFatal,
|
|
415
|
+
});
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'session.closed': {
|
|
419
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] session.closed: reason=${event.reason}`);
|
|
420
|
+
this._closed = true;
|
|
421
|
+
if (this._closeRequested) {
|
|
422
|
+
this._resolveClose?.();
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
for (const h of this._closeHandlers) {
|
|
426
|
+
h();
|
|
427
|
+
}
|
|
428
|
+
this.dispatchError({
|
|
429
|
+
Message: `Session closed by server: ${event.reason ?? 'unknown'}`,
|
|
430
|
+
Fatal: true,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
this._socket.close(1000, 'Closed');
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Ignore socket close failure on terminal frame
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
default:
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
dispatchError(error) {
|
|
446
|
+
for (const handler of this._errorHandlers) {
|
|
447
|
+
handler(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
sendFrame(event) {
|
|
451
|
+
if (this._closed) {
|
|
452
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Attempted to send frame on closed session: ${event.type}`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
this._socket.send(JSON.stringify(event));
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Failed to send frame: ${err}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// ─── IRealtimeSession Implementation ──────────────────────────────────────────
|
|
463
|
+
SendInput(chunk, kind) {
|
|
464
|
+
if (kind === 'video') {
|
|
465
|
+
return; // Audio-only driver ignores video frames
|
|
466
|
+
}
|
|
467
|
+
if (this._closed) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const isG711 = this.AudioFormat &&
|
|
471
|
+
typeof this.AudioFormat === 'object' &&
|
|
472
|
+
(this.AudioFormat.Codec === 'g711_ulaw' || this.AudioFormat.Codec === 'g711_alaw');
|
|
473
|
+
let bytes = new Uint8Array(chunk);
|
|
474
|
+
if (!isG711) {
|
|
475
|
+
if (this._trailingPcmBytes && this._trailingPcmBytes.length > 0) {
|
|
476
|
+
const merged = new Uint8Array(this._trailingPcmBytes.length + bytes.length);
|
|
477
|
+
merged.set(this._trailingPcmBytes, 0);
|
|
478
|
+
merged.set(bytes, this._trailingPcmBytes.length);
|
|
479
|
+
bytes = merged;
|
|
480
|
+
this._trailingPcmBytes = undefined;
|
|
481
|
+
}
|
|
482
|
+
if (bytes.length % 2 !== 0) {
|
|
483
|
+
this._trailingPcmBytes = bytes.slice(bytes.length - 1);
|
|
484
|
+
bytes = bytes.slice(0, bytes.length - 1);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (bytes.length === 0) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const b64 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64');
|
|
491
|
+
this.sendFrame({
|
|
492
|
+
event_id: randomUUID(),
|
|
493
|
+
type: 'session.input_audio.append',
|
|
494
|
+
audio: b64,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async RegisterTools(tools) {
|
|
498
|
+
const policy = this.compileDelegationPolicy(tools);
|
|
499
|
+
if (this._active) {
|
|
500
|
+
await this.SendInstructions(policy);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
OnOutput(handler) {
|
|
504
|
+
this._outputHandlers.push(handler);
|
|
505
|
+
}
|
|
506
|
+
OnTranscript(handler) {
|
|
507
|
+
this._transcriptHandlers.push(handler);
|
|
508
|
+
}
|
|
509
|
+
OnToolCall(handler) {
|
|
510
|
+
this._toolCallHandlers.push(handler);
|
|
511
|
+
}
|
|
512
|
+
async SendToolResult(callID, output, taskRevision) {
|
|
513
|
+
if (taskRevision !== undefined && taskRevision !== this._currentTaskRevision) {
|
|
514
|
+
RealtimeDiagLog(`[OpenAILiveSession][diag] Discarding stale tool result for ${callID}: revision ${taskRevision} vs current ${this._currentTaskRevision}`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (this._options.reasoningPlane === 'remote') {
|
|
518
|
+
// Trap 3: Appending a function result does not automatically continue the response:
|
|
519
|
+
// response.create is a required separate step, and response.item.create has no success ack.
|
|
520
|
+
// Requirement (§4.2): Exactly ONE response.create per batch, not per result.
|
|
521
|
+
this.sendFrame({
|
|
522
|
+
event_id: randomUUID(),
|
|
523
|
+
type: 'response.item.create',
|
|
524
|
+
item: {
|
|
525
|
+
type: 'function_call_output',
|
|
526
|
+
call_id: callID,
|
|
527
|
+
output,
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
const isBatchComplete = this._toolBatchBarrier.RecordResult(callID);
|
|
531
|
+
if (isBatchComplete) {
|
|
532
|
+
this.sendFrame({
|
|
533
|
+
event_id: randomUUID(),
|
|
534
|
+
type: 'response.create',
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
this.sendFrame({
|
|
540
|
+
event_id: randomUUID(),
|
|
541
|
+
type: 'session.commentary.append',
|
|
542
|
+
content: output,
|
|
543
|
+
delegation_id: callID,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
SendContextNote(text) {
|
|
548
|
+
this.sendFrame({
|
|
549
|
+
event_id: randomUUID(),
|
|
550
|
+
type: 'session.thinking.append',
|
|
551
|
+
content: text,
|
|
552
|
+
delegation_id: null,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
RequestSpokenUpdate(instructions) {
|
|
556
|
+
this.sendFrame({
|
|
557
|
+
event_id: randomUUID(),
|
|
558
|
+
type: 'session.commentary.append',
|
|
559
|
+
content: instructions,
|
|
560
|
+
delegation_id: null,
|
|
561
|
+
});
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
async SendInstructions(instructions) {
|
|
565
|
+
this.sendFrame({
|
|
566
|
+
event_id: randomUUID(),
|
|
567
|
+
type: 'session.instructions.append',
|
|
568
|
+
instructions,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
OnInterruption(handler) {
|
|
572
|
+
this._interruptionHandlers.push(handler);
|
|
573
|
+
}
|
|
574
|
+
OnError(handler) {
|
|
575
|
+
this._errorHandlers.push(handler);
|
|
576
|
+
}
|
|
577
|
+
OnClose(handler) {
|
|
578
|
+
this._closeHandlers.push(handler);
|
|
579
|
+
}
|
|
580
|
+
OnUsage(handler) {
|
|
581
|
+
this._usageHandlers.push(handler);
|
|
582
|
+
}
|
|
583
|
+
async Close() {
|
|
584
|
+
this._toolBatchBarrier.Clear();
|
|
585
|
+
if (this._closed) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this._closeRequested = true;
|
|
589
|
+
this._closePromise = new Promise((resolve) => {
|
|
590
|
+
this._resolveClose = resolve;
|
|
591
|
+
});
|
|
592
|
+
this.sendFrame({
|
|
593
|
+
event_id: randomUUID(),
|
|
594
|
+
type: 'session.close',
|
|
595
|
+
});
|
|
596
|
+
// Race graceful server close against safety timer (5s)
|
|
597
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
598
|
+
setTimeout(() => {
|
|
599
|
+
try {
|
|
600
|
+
this._socket.close(1000, 'Close timeout');
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Ignore error on close
|
|
604
|
+
}
|
|
605
|
+
resolve();
|
|
606
|
+
}, 5000);
|
|
607
|
+
});
|
|
608
|
+
await Promise.race([this._closePromise, timeoutPromise]);
|
|
609
|
+
this._closed = true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Driver class for OpenAI GPT-Live 1 full-duplex realtime models.
|
|
614
|
+
*/
|
|
615
|
+
let OpenAILiveRealtime = class OpenAILiveRealtime extends BaseRealtimeModel {
|
|
616
|
+
static { OpenAILiveRealtime_1 = this; }
|
|
617
|
+
static { this.SupportsDynamicToolSet = true; }
|
|
618
|
+
constructor(apiKey, endpoint) {
|
|
619
|
+
super(apiKey);
|
|
620
|
+
this._endpoint = endpoint ?? 'wss://api.openai.com/v1/live/sessions';
|
|
621
|
+
}
|
|
622
|
+
get SupportedVoices() {
|
|
623
|
+
return [
|
|
624
|
+
{ ID: 'alloy', Name: 'Alloy' },
|
|
625
|
+
{ ID: 'ash', Name: 'Ash' },
|
|
626
|
+
{ ID: 'ballad', Name: 'Ballad' },
|
|
627
|
+
{ ID: 'cedar', Name: 'Cedar' },
|
|
628
|
+
{ ID: 'coral', Name: 'Coral' },
|
|
629
|
+
{ ID: 'echo', Name: 'Echo' },
|
|
630
|
+
{ ID: 'marin', Name: 'Marin' },
|
|
631
|
+
{ ID: 'sage', Name: 'Sage' },
|
|
632
|
+
{ ID: 'shimmer', Name: 'Shimmer' },
|
|
633
|
+
{ ID: 'verse', Name: 'Verse' },
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Creation seam for the live WebSocket connection. Unit tests override this to return an in-memory mock.
|
|
638
|
+
*/
|
|
639
|
+
createSocket(url, apiKey) {
|
|
640
|
+
const WS = globalThis.WebSocket;
|
|
641
|
+
if (!WS) {
|
|
642
|
+
throw new Error('OpenAILiveRealtime requires Node 22+ or an environment with global WebSocket support.');
|
|
643
|
+
}
|
|
644
|
+
return new WS(url, undefined, {
|
|
645
|
+
headers: {
|
|
646
|
+
Authorization: `Bearer ${apiKey}`,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Opens a duplex OpenAI Live session and awaits initialization.
|
|
652
|
+
*/
|
|
653
|
+
async StartSession(params) {
|
|
654
|
+
const socket = this.createSocket(this._endpoint, this.apiKey);
|
|
655
|
+
const config = params.Config;
|
|
656
|
+
const reasoningConfig = config?.Reasoning;
|
|
657
|
+
let audioCodec = 'pcm16';
|
|
658
|
+
let sampleRate = 24000;
|
|
659
|
+
if (config?.AudioFormat && typeof config.AudioFormat === 'object') {
|
|
660
|
+
const af = config.AudioFormat;
|
|
661
|
+
if (af.Codec === 'g711_ulaw' || af.Codec === 'audio/pcmu') {
|
|
662
|
+
audioCodec = 'g711_ulaw';
|
|
663
|
+
sampleRate = af.SampleRate ?? 8000;
|
|
664
|
+
}
|
|
665
|
+
else if (af.Codec === 'g711_alaw' || af.Codec === 'audio/pcma') {
|
|
666
|
+
audioCodec = 'g711_alaw';
|
|
667
|
+
sampleRate = af.SampleRate ?? 8000;
|
|
668
|
+
}
|
|
669
|
+
else if (af.SampleRate) {
|
|
670
|
+
sampleRate = af.SampleRate;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else if (config?.AudioCodec === 'g711_ulaw' ||
|
|
674
|
+
config?.AudioCodec === 'audio/pcmu' ||
|
|
675
|
+
config?.audioCodec === 'g711_ulaw' ||
|
|
676
|
+
config?.audioCodec === 'audio/pcmu') {
|
|
677
|
+
audioCodec = 'g711_ulaw';
|
|
678
|
+
sampleRate = 8000;
|
|
679
|
+
}
|
|
680
|
+
else if (config?.AudioCodec === 'g711_alaw' ||
|
|
681
|
+
config?.AudioCodec === 'audio/pcma' ||
|
|
682
|
+
config?.audioCodec === 'g711_alaw' ||
|
|
683
|
+
config?.audioCodec === 'audio/pcma') {
|
|
684
|
+
audioCodec = 'g711_alaw';
|
|
685
|
+
sampleRate = 8000;
|
|
686
|
+
}
|
|
687
|
+
const options = {
|
|
688
|
+
reasoningPlane: reasoningConfig?.Plane ?? 'local',
|
|
689
|
+
remoteSettings: reasoningConfig?.Remote,
|
|
690
|
+
voice: OpenAILiveRealtime_1.resolveVoice(config),
|
|
691
|
+
audioCodec,
|
|
692
|
+
sampleRate,
|
|
693
|
+
persona: config?.['persona'],
|
|
694
|
+
styleDescriptors: config?.['styleDescriptors'],
|
|
695
|
+
interruptionPolicy: typeof config?.['interruptionPolicy'] === 'string' ? config['interruptionPolicy'] : undefined,
|
|
696
|
+
backchannelPolicy: typeof config?.['backchannelPolicy'] === 'string' ? config['backchannelPolicy'] : undefined,
|
|
697
|
+
};
|
|
698
|
+
const session = new OpenAILiveSession(socket, params, options);
|
|
699
|
+
await session.WaitForStarted();
|
|
700
|
+
return session;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Whether this driver can mint an ephemeral, server-scoped client credential for a
|
|
704
|
+
* **client-direct** realtime session (the browser opens its own provider socket).
|
|
705
|
+
*/
|
|
706
|
+
get SupportsClientDirect() {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Compiles the delegation policy block informing GPT-Live about available backend tools
|
|
711
|
+
* and when to delegate to the reasoning model.
|
|
712
|
+
*/
|
|
713
|
+
static CompileDelegationPolicy(tools) {
|
|
714
|
+
const toolBullets = tools
|
|
715
|
+
.map((t) => `- ${t.Name}: ${t.Description || 'Execute backend capability'}`)
|
|
716
|
+
.join('\n');
|
|
717
|
+
return ('Delegation policy:\n' +
|
|
718
|
+
'Backend tools:\n' +
|
|
719
|
+
toolBullets +
|
|
720
|
+
'\n\n' +
|
|
721
|
+
'Delegate to the backend when:\n' +
|
|
722
|
+
'- The user asks for assistance or changes requiring backend capabilities.\n\n' +
|
|
723
|
+
'Do not delegate to the backend when:\n' +
|
|
724
|
+
'- The user greets you or asks you to repeat a result already provided.\n\n' +
|
|
725
|
+
'Delegate before giving an answer that depends on backend work.\n' +
|
|
726
|
+
'Do not guess the result while waiting, but brief spoken holding phrases while backend work proceeds are permitted.');
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Mints a client session config for the browser-direct WebRTC topology.
|
|
730
|
+
*
|
|
731
|
+
* In the OpenAI Live WebRTC topology, the browser exchanges its offer SDP with the
|
|
732
|
+
* trusted server broker (via {@link ExchangeWebRtcSdp}); the project API key stays server-side.
|
|
733
|
+
* WebRTC omits audio.format entirely (negotiated via SDP).
|
|
734
|
+
*/
|
|
735
|
+
async CreateClientSession(params) {
|
|
736
|
+
const config = params.Config;
|
|
737
|
+
const reasoningConfig = config?.Reasoning;
|
|
738
|
+
const plane = reasoningConfig?.Plane ?? 'local';
|
|
739
|
+
const mappedTools = params.Tools?.map((t) => ({
|
|
740
|
+
type: 'function',
|
|
741
|
+
name: t.Name,
|
|
742
|
+
description: t.Description,
|
|
743
|
+
parameters: t.ParametersSchema,
|
|
744
|
+
}));
|
|
745
|
+
const hasTools = !!(mappedTools && mappedTools.length > 0);
|
|
746
|
+
const delegationType = plane === 'remote' || hasTools ? 'responses' : 'client';
|
|
747
|
+
const rawParallelToolCalls = config?.['parallelToolCalls'] ?? config?.['parallel_tool_calls'];
|
|
748
|
+
const parallelToolCalls = typeof rawParallelToolCalls === 'boolean' ? rawParallelToolCalls : undefined;
|
|
749
|
+
const sections = [];
|
|
750
|
+
if (params.SystemPrompt) {
|
|
751
|
+
sections.push(params.SystemPrompt.trim());
|
|
752
|
+
}
|
|
753
|
+
if (params.InitialContext) {
|
|
754
|
+
sections.push(params.InitialContext.trim());
|
|
755
|
+
}
|
|
756
|
+
if (hasTools && params.Tools && params.Tools.length > 0) {
|
|
757
|
+
sections.push(OpenAILiveRealtime_1.CompileDelegationPolicy(params.Tools));
|
|
758
|
+
}
|
|
759
|
+
const compiledInstructions = sections.join('\n\n');
|
|
760
|
+
// WebRTC session config: omit audio.format entirely (negotiated via SDP)
|
|
761
|
+
const sessionPayload = {
|
|
762
|
+
model: params.Model || 'gpt-live-1',
|
|
763
|
+
instructions: compiledInstructions,
|
|
764
|
+
audio: {
|
|
765
|
+
output: {
|
|
766
|
+
voice: OpenAILiveRealtime_1.resolveVoice(config),
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
...(typeof parallelToolCalls === 'boolean' ? { parallel_tool_calls: parallelToolCalls } : {}),
|
|
770
|
+
delegation: {
|
|
771
|
+
type: delegationType,
|
|
772
|
+
...(delegationType === 'responses'
|
|
773
|
+
? {
|
|
774
|
+
responses: {
|
|
775
|
+
model: reasoningConfig?.Remote?.Ref ?? 'gpt-4o',
|
|
776
|
+
...(reasoningConfig?.Remote?.Effort
|
|
777
|
+
? {
|
|
778
|
+
reasoning: {
|
|
779
|
+
effort: reasoningConfig.Remote.Effort,
|
|
780
|
+
},
|
|
781
|
+
}
|
|
782
|
+
: {}),
|
|
783
|
+
...(typeof reasoningConfig?.Remote?.MaxOutputTokens === 'number'
|
|
784
|
+
? {
|
|
785
|
+
max_output_tokens: reasoningConfig.Remote.MaxOutputTokens,
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
788
|
+
...(hasTools ? { tools: mappedTools } : {}),
|
|
789
|
+
},
|
|
790
|
+
}
|
|
791
|
+
: {}),
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
const ticket = RealtimeProxyRegistry.Instance.Issue({
|
|
795
|
+
UpstreamUrl: this._endpoint,
|
|
796
|
+
UpstreamAuthHeader: this.apiKey,
|
|
797
|
+
DriverClass: 'OpenAILiveRealtime',
|
|
798
|
+
UserID: params.UserID,
|
|
799
|
+
SessionConfig: sessionPayload,
|
|
800
|
+
TTLSeconds: 60,
|
|
801
|
+
});
|
|
802
|
+
const brokerUrl = this.resolveBrokerUrl(params, ticket.ID);
|
|
803
|
+
return {
|
|
804
|
+
Provider: 'openai-live',
|
|
805
|
+
Model: params.Model || 'gpt-live-1',
|
|
806
|
+
EphemeralToken: brokerUrl,
|
|
807
|
+
ExpiresAt: ticket.ExpiresAt,
|
|
808
|
+
SessionConfig: sessionPayload,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Resolves the configured voice name from `params.Config`, accepting either lowercase `voice`
|
|
813
|
+
* (the driver-neutral standard) or uppercase `Voice`, defaulting to `'alloy'`.
|
|
814
|
+
*/
|
|
815
|
+
static resolveVoice(config) {
|
|
816
|
+
const raw = config?.['voice'] ?? config?.['Voice'];
|
|
817
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : 'alloy';
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Resolves the browser-facing HTTP(S) broker URL for the OpenAI Live WebRTC SDP exchange.
|
|
821
|
+
*/
|
|
822
|
+
resolveBrokerUrl(params, ticketId) {
|
|
823
|
+
const override = params.Config?.['proxyBaseUrl'] ?? params.Config?.['brokerBaseUrl'];
|
|
824
|
+
const source = (typeof override === 'string' && override.trim().length > 0 ? override.trim() : '') ||
|
|
825
|
+
process.env['MJAPI_PUBLIC_URL'] ||
|
|
826
|
+
`${process.env['GRAPHQL_BASE_URL'] ?? 'http://localhost'}:${process.env['GRAPHQL_PORT'] ?? '4103'}`;
|
|
827
|
+
let end = source.length;
|
|
828
|
+
while (end > 0 && source.charCodeAt(end - 1) === 47 /* '/' */) {
|
|
829
|
+
end--;
|
|
830
|
+
}
|
|
831
|
+
const baseUrl = source.slice(0, end);
|
|
832
|
+
return `${baseUrl}${REALTIME_SDP_EXCHANGE_PATH}?ticket=${encodeURIComponent(ticketId)}`;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Exchanges a client WebRTC offer SDP for an answer SDP via OpenAI's `POST /v1/live/sessions`,
|
|
836
|
+
* using the server's project API key.
|
|
837
|
+
*
|
|
838
|
+
* In the OpenAI Live WebRTC topology, the browser never contacts api.openai.com directly;
|
|
839
|
+
* the trusted application server exchanges the offer for an answer and starts the session.
|
|
840
|
+
* Note: creating a WebRTC session pre-bills 15 seconds of voice duration.
|
|
841
|
+
*
|
|
842
|
+
* @param offerSdp The local SDP offer generated by the browser.
|
|
843
|
+
* @param sessionConfig The session configuration to initialize the session with.
|
|
844
|
+
* @returns The answer SDP, provider session ID, and the 15-second pre-bill accounting.
|
|
845
|
+
*/
|
|
846
|
+
async ExchangeWebRtcSdp(offerSdp, sessionConfig) {
|
|
847
|
+
const payload = {
|
|
848
|
+
session: sessionConfig ?? { model: 'gpt-live-1' },
|
|
849
|
+
transport: {
|
|
850
|
+
type: 'webrtc',
|
|
851
|
+
sdp: offerSdp,
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
const res = await this.postLiveSessions(payload);
|
|
855
|
+
return {
|
|
856
|
+
answerSdp: res.transport.sdp,
|
|
857
|
+
sessionId: res.id,
|
|
858
|
+
prebillSeconds: 15,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* HTTP POST seam for `/v1/live/sessions` (unit tests override this to mock the HTTP call).
|
|
863
|
+
*/
|
|
864
|
+
async postLiveSessions(payload) {
|
|
865
|
+
const res = await fetch('https://api.openai.com/v1/live/sessions', {
|
|
866
|
+
method: 'POST',
|
|
867
|
+
headers: {
|
|
868
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
869
|
+
'Content-Type': 'application/json',
|
|
870
|
+
},
|
|
871
|
+
body: JSON.stringify(payload),
|
|
872
|
+
});
|
|
873
|
+
if (!res.ok) {
|
|
874
|
+
const text = await res.text().catch(() => '');
|
|
875
|
+
throw new Error(`OpenAI Live WebRTC session exchange failed (${res.status}): ${text}`);
|
|
876
|
+
}
|
|
877
|
+
const data = (await res.json());
|
|
878
|
+
return data;
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
OpenAILiveRealtime = OpenAILiveRealtime_1 = __decorate([
|
|
882
|
+
RegisterClass(BaseRealtimeModel, 'OpenAILiveRealtime'),
|
|
883
|
+
__metadata("design:paramtypes", [String, String])
|
|
884
|
+
], OpenAILiveRealtime);
|
|
885
|
+
export { OpenAILiveRealtime };
|
|
886
|
+
//# sourceMappingURL=openAILiveRealtime.js.map
|