realtime-voice-agents 2.0.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -0
  3. package/assets/elevator-jazz.ulaw +1 -0
  4. package/assets/keyboard-typing.ulaw +1 -0
  5. package/assets/lofi.ulaw +1 -0
  6. package/assets/ringing.ulaw +1 -0
  7. package/assets/thinking-hum.ulaw +1 -0
  8. package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
  9. package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
  10. package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
  11. package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
  12. package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
  13. package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
  14. package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
  15. package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
  16. package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
  17. package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
  18. package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
  19. package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
  20. package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
  21. package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
  22. package/dist/audio.cjs +21 -0
  23. package/dist/audio.d.cts +165 -0
  24. package/dist/audio.d.mts +165 -0
  25. package/dist/audio.mjs +4 -0
  26. package/dist/env-DSnGaERV.cjs +19 -0
  27. package/dist/env-DUwUWTsg.mjs +14 -0
  28. package/dist/events-BUMYdETO.d.cts +24 -0
  29. package/dist/events-BUMYdETO.d.mts +24 -0
  30. package/dist/events-BxDTIKKq.cjs +48 -0
  31. package/dist/events-BylBSBW-.mjs +43 -0
  32. package/dist/gemini.cjs +401 -0
  33. package/dist/gemini.d.cts +41 -0
  34. package/dist/gemini.d.mts +41 -0
  35. package/dist/gemini.mjs +395 -0
  36. package/dist/index.cjs +2142 -0
  37. package/dist/index.d.cts +920 -0
  38. package/dist/index.d.mts +920 -0
  39. package/dist/index.mjs +2113 -0
  40. package/dist/mulaw--cwU2c9L.mjs +64 -0
  41. package/dist/mulaw-DLUObjdP.cjs +117 -0
  42. package/dist/openai.cjs +61 -0
  43. package/dist/openai.d.cts +121 -0
  44. package/dist/openai.d.mts +121 -0
  45. package/dist/openai.mjs +53 -0
  46. package/dist/presets-Bf75YXs5.d.cts +24 -0
  47. package/dist/presets-Bf75YXs5.d.mts +24 -0
  48. package/dist/rest-BYqiVOhe.mjs +265 -0
  49. package/dist/rest-BvUKut_k.cjs +300 -0
  50. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  51. package/dist/session-config-BVLl7-ha.mjs +74 -0
  52. package/dist/session-config-c8sOw1XL.cjs +85 -0
  53. package/dist/store.cjs +3 -0
  54. package/dist/store.d.cts +2 -0
  55. package/dist/store.d.mts +2 -0
  56. package/dist/store.mjs +2 -0
  57. package/dist/testing.cjs +544 -0
  58. package/dist/testing.d.cts +191 -0
  59. package/dist/testing.d.mts +191 -0
  60. package/dist/testing.mjs +538 -0
  61. package/dist/transcode-C9aJG7_W.cjs +215 -0
  62. package/dist/transcode-CsAp97G9.mjs +198 -0
  63. package/dist/transport-B_PJFIVd.d.cts +155 -0
  64. package/dist/transport-CEaLFV4E.d.mts +155 -0
  65. package/dist/twilio.cjs +8 -0
  66. package/dist/twilio.d.cts +3 -0
  67. package/dist/twilio.d.mts +3 -0
  68. package/dist/twilio.mjs +2 -0
  69. package/dist/twiml-z9LjoF4_.d.cts +67 -0
  70. package/dist/twiml-z9LjoF4_.d.mts +67 -0
  71. package/dist/xai.cjs +92 -0
  72. package/dist/xai.d.cts +35 -0
  73. package/dist/xai.d.mts +35 -0
  74. package/dist/xai.mjs +86 -0
  75. package/package.json +183 -0
@@ -0,0 +1,538 @@
1
+ import { i as base64ByteLength } from "./mulaw--cwU2c9L.mjs";
2
+ import { WebSocketServer } from "ws";
3
+ import { setTimeout } from "node:timers/promises";
4
+ //#region src/testing/FakeTwilioMediaStream.ts
5
+ /**
6
+ * FakeTwilioMediaStream — a deterministic stand-in for Twilio's side of a
7
+ * Media Stream WebSocket.
8
+ *
9
+ * It exposes the ws-shaped surface the bridge consumes (send/close/on), and a
10
+ * test-driver surface for the "caller": connect(), sendMedia(), stop(), plus a
11
+ * manual playout clock (`advancePlayback`) that emulates Twilio's buffer-and-
12
+ * play-in-order behavior:
13
+ *
14
+ * - outbound `media` frames queue with duration = μ-law bytes / 8 per ms
15
+ * - outbound `mark` frames queue in order and echo back once playout reaches
16
+ * them
17
+ * - outbound `clear` drops everything buffered and immediately echoes every
18
+ * pending mark (exactly what real Twilio does with interrupted marks)
19
+ *
20
+ * No wall-clock timers: tests advance playback explicitly, so scenarios are
21
+ * fully deterministic.
22
+ */
23
+ /** Base64 μ-law silence of the given duration (whole milliseconds at 8 kHz). */
24
+ function mulawSilenceBase64(ms) {
25
+ return Buffer.alloc(Math.round(ms * 8), 255).toString("base64");
26
+ }
27
+ var FakeTwilioMediaStream = class {
28
+ readyState = 1;
29
+ callSid;
30
+ streamSid;
31
+ /** Everything the bridge sent, parsed, in order. */
32
+ outbound = [];
33
+ /** Base64 payloads of outbound media frames, in order. */
34
+ sentMediaPayloads = [];
35
+ /** Names of marks echoed back to the bridge (played or discarded). */
36
+ echoedMarks = [];
37
+ /** Number of `clear` frames received from the bridge. */
38
+ clearCount = 0;
39
+ messageListeners = [];
40
+ closeListeners = [];
41
+ errorListeners = [];
42
+ playoutQueue = [];
43
+ playedMsTotal = 0;
44
+ callerClockMs = 0;
45
+ mediaSequence = 0;
46
+ closedByBridge = false;
47
+ constructor(options = {}) {
48
+ this.callSid = options.callSid ?? "CA" + "f".repeat(32);
49
+ this.streamSid = options.streamSid ?? "MZ" + "f".repeat(32);
50
+ }
51
+ send(data) {
52
+ let frame;
53
+ try {
54
+ frame = JSON.parse(data);
55
+ } catch {
56
+ return;
57
+ }
58
+ this.outbound.push(frame);
59
+ switch (frame.event) {
60
+ case "media": {
61
+ const payload = frame.media?.payload ?? "";
62
+ this.sentMediaPayloads.push(payload);
63
+ const ms = base64ByteLength(payload) / 8;
64
+ this.playoutQueue.push({
65
+ kind: "media",
66
+ remainingMs: ms,
67
+ totalMs: ms
68
+ });
69
+ break;
70
+ }
71
+ case "mark":
72
+ this.playoutQueue.push({
73
+ kind: "mark",
74
+ name: frame.mark?.name ?? ""
75
+ });
76
+ break;
77
+ case "clear": {
78
+ this.clearCount++;
79
+ const pendingMarks = this.playoutQueue.filter((i) => i.kind === "mark");
80
+ this.playoutQueue.length = 0;
81
+ for (const mark of pendingMarks) this.echoMark(mark.name);
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ close(code, reason) {
87
+ if (this.readyState === 3) return;
88
+ this.readyState = 3;
89
+ this.closedByBridge = true;
90
+ for (const listener of this.closeListeners) listener(code ?? 1e3, reason);
91
+ }
92
+ on(event, listener) {
93
+ if (event === "message") this.messageListeners.push(listener);
94
+ else if (event === "close") this.closeListeners.push(listener);
95
+ else if (event === "error") this.errorListeners.push(listener);
96
+ return this;
97
+ }
98
+ /** Send the `connected` preamble and `start` frame. */
99
+ connect(options = {}) {
100
+ if (options.sendConnectedFrame !== false) this.deliver({
101
+ event: "connected",
102
+ protocol: "Call",
103
+ version: "1.0.0"
104
+ });
105
+ this.deliver({
106
+ event: "start",
107
+ sequenceNumber: "1",
108
+ streamSid: options.streamSid ?? this.streamSid,
109
+ start: {
110
+ streamSid: options.streamSid ?? this.streamSid,
111
+ accountSid: options.accountSid ?? "AC" + "f".repeat(32),
112
+ callSid: options.callSid ?? this.callSid,
113
+ tracks: ["inbound"],
114
+ mediaFormat: {
115
+ encoding: "audio/x-mulaw",
116
+ sampleRate: 8e3,
117
+ channels: 1
118
+ },
119
+ customParameters: options.customParameters ?? {}
120
+ }
121
+ });
122
+ }
123
+ /** Deliver one caller media frame (base64 μ-law) to the bridge. */
124
+ sendMedia(payload) {
125
+ this.mediaSequence++;
126
+ const ms = base64ByteLength(payload) / 8;
127
+ this.deliver({
128
+ event: "media",
129
+ sequenceNumber: String(this.mediaSequence + 1),
130
+ streamSid: this.streamSid,
131
+ media: {
132
+ track: "inbound",
133
+ chunk: String(this.mediaSequence),
134
+ timestamp: String(Math.round(this.callerClockMs)),
135
+ payload
136
+ }
137
+ });
138
+ this.callerClockMs += ms;
139
+ }
140
+ /** Deliver `ms` of caller silence as 20 ms μ-law frames. */
141
+ sendSilence(ms) {
142
+ let remaining = ms;
143
+ while (remaining > 0) {
144
+ const frameMs = Math.min(20, remaining);
145
+ this.sendMedia(mulawSilenceBase64(frameMs));
146
+ remaining -= frameMs;
147
+ }
148
+ }
149
+ sendDtmf(digit) {
150
+ this.deliver({
151
+ event: "dtmf",
152
+ streamSid: this.streamSid,
153
+ dtmf: {
154
+ track: "inbound_track",
155
+ digit
156
+ }
157
+ });
158
+ }
159
+ /** Deliver the `stop` frame (Twilio ended the stream). */
160
+ stop() {
161
+ this.deliver({
162
+ event: "stop",
163
+ streamSid: this.streamSid,
164
+ stop: { callSid: this.callSid }
165
+ });
166
+ }
167
+ /** Simulate the socket dropping from Twilio's side. */
168
+ disconnect(code = 1006, reason = "connection lost") {
169
+ if (this.readyState === 3) return;
170
+ this.readyState = 3;
171
+ for (const listener of this.closeListeners) listener(code, reason);
172
+ }
173
+ /** Total milliseconds of agent audio actually "played" to the caller. */
174
+ get playedMs() {
175
+ return this.playedMsTotal;
176
+ }
177
+ /** Milliseconds of agent audio still buffered. */
178
+ get queuedMs() {
179
+ return this.playoutQueue.reduce((sum, item) => sum + (item.kind === "media" ? item.remainingMs : 0), 0);
180
+ }
181
+ get wasClosedByBridge() {
182
+ return this.closedByBridge;
183
+ }
184
+ /**
185
+ * Advance the playout clock: consume buffered media in order and echo each
186
+ * mark as playout reaches it.
187
+ */
188
+ advancePlayback(ms) {
189
+ let budget = ms;
190
+ while (this.playoutQueue.length > 0) {
191
+ const head = this.playoutQueue[0];
192
+ if (head.kind === "mark") {
193
+ this.playoutQueue.shift();
194
+ this.echoMark(head.name);
195
+ continue;
196
+ }
197
+ if (budget <= 0) break;
198
+ const consumed = Math.min(budget, head.remainingMs);
199
+ head.remainingMs -= consumed;
200
+ budget -= consumed;
201
+ this.playedMsTotal += consumed;
202
+ if (head.remainingMs <= 1e-9) this.playoutQueue.shift();
203
+ }
204
+ while (this.playoutQueue[0]?.kind === "mark") {
205
+ const mark = this.playoutQueue.shift();
206
+ this.echoMark(mark.name);
207
+ }
208
+ }
209
+ /** Play out everything currently buffered. */
210
+ playAll() {
211
+ this.advancePlayback(Number.MAX_SAFE_INTEGER);
212
+ }
213
+ echoMark(name) {
214
+ this.echoedMarks.push(name);
215
+ this.deliver({
216
+ event: "mark",
217
+ streamSid: this.streamSid,
218
+ mark: { name }
219
+ });
220
+ }
221
+ deliver(frame) {
222
+ if (this.readyState === 3) return;
223
+ const data = JSON.stringify(frame);
224
+ for (const listener of this.messageListeners) listener(data);
225
+ }
226
+ };
227
+ //#endregion
228
+ //#region src/testing/FakeOpenAIServer.ts
229
+ let responseCounter = 0;
230
+ var FakeOpenAIConnection = class {
231
+ socket;
232
+ /** Every parsed client → server frame, in order. */
233
+ received = [];
234
+ /** HTTP headers of the client's upgrade request (auth, extensions…). */
235
+ upgradeHeaders;
236
+ server;
237
+ constructor(server, socket, upgradeHeaders = {}) {
238
+ this.server = server;
239
+ this.socket = socket;
240
+ this.upgradeHeaders = upgradeHeaders;
241
+ }
242
+ /** Base64 payloads from input_audio_buffer.append frames. */
243
+ get appendedAudio() {
244
+ return this.received.filter((f) => f.type === "input_audio_buffer.append").map((f) => f.audio);
245
+ }
246
+ eventsOfType(type) {
247
+ return this.received.filter((f) => f.type === type);
248
+ }
249
+ async waitForEvent(predicate, timeoutMs = 2e3) {
250
+ const matches = typeof predicate === "string" ? (frame) => frame.type === predicate : predicate;
251
+ const deadline = Date.now() + timeoutMs;
252
+ let seen = 0;
253
+ while (Date.now() < deadline) {
254
+ for (; seen < this.received.length; seen++) {
255
+ const frame = this.received[seen];
256
+ if (matches(frame)) return frame;
257
+ }
258
+ await setTimeout(5);
259
+ }
260
+ throw new Error(`timed out waiting for client event; got: ${this.received.map((f) => f.type).join(", ")}`);
261
+ }
262
+ send(frame) {
263
+ this.socket.send(JSON.stringify(frame));
264
+ }
265
+ /** Script a complete assistant audio response (created → deltas → done). */
266
+ sendAudioResponse(options) {
267
+ const responseId = options.responseId ?? `resp_${++responseCounter}`;
268
+ const itemId = options.itemId ?? `item_${responseCounter}`;
269
+ this.send({
270
+ type: "response.created",
271
+ response: { id: responseId }
272
+ });
273
+ this.send({
274
+ type: "response.output_item.added",
275
+ response_id: responseId,
276
+ item: {
277
+ id: itemId,
278
+ type: "message"
279
+ }
280
+ });
281
+ for (const chunk of options.chunks) this.send({
282
+ type: "response.output_audio.delta",
283
+ response_id: responseId,
284
+ item_id: itemId,
285
+ delta: chunk
286
+ });
287
+ if (options.transcript) {
288
+ this.send({
289
+ type: "response.output_audio_transcript.delta",
290
+ response_id: responseId,
291
+ delta: options.transcript
292
+ });
293
+ this.send({
294
+ type: "response.output_audio_transcript.done",
295
+ response_id: responseId,
296
+ transcript: options.transcript
297
+ });
298
+ }
299
+ if (options.complete !== false) this.send({
300
+ type: "response.done",
301
+ response: {
302
+ id: responseId,
303
+ status: "completed",
304
+ usage: options.usage
305
+ }
306
+ });
307
+ return {
308
+ responseId,
309
+ itemId
310
+ };
311
+ }
312
+ sendToolCall(options) {
313
+ const callId = options.callId ?? `call_${++responseCounter}`;
314
+ const responseId = options.responseId ?? `resp_${responseCounter}`;
315
+ this.send({
316
+ type: "response.created",
317
+ response: { id: responseId }
318
+ });
319
+ this.send({
320
+ type: "response.function_call_arguments.done",
321
+ response_id: responseId,
322
+ item_id: `item_${responseCounter}`,
323
+ call_id: callId,
324
+ name: options.name,
325
+ arguments: options.argumentsJson ?? "{}"
326
+ });
327
+ this.send({
328
+ type: "response.done",
329
+ response: {
330
+ id: responseId,
331
+ status: "completed"
332
+ }
333
+ });
334
+ return callId;
335
+ }
336
+ sendSpeechStarted() {
337
+ this.send({ type: "input_audio_buffer.speech_started" });
338
+ }
339
+ sendSpeechStopped() {
340
+ this.send({ type: "input_audio_buffer.speech_stopped" });
341
+ }
342
+ sendUserTranscript(text) {
343
+ this.send({
344
+ type: "conversation.item.input_audio_transcription.completed",
345
+ transcript: text
346
+ });
347
+ }
348
+ sendError(error) {
349
+ this.send({
350
+ type: "error",
351
+ error
352
+ });
353
+ }
354
+ drop(code = 1011, reason = "fake server drop") {
355
+ this.socket.close(code, reason);
356
+ }
357
+ };
358
+ var FakeOpenAIServer = class FakeOpenAIServer {
359
+ connections = [];
360
+ wss;
361
+ options;
362
+ url;
363
+ constructor(wss, url, options) {
364
+ this.wss = wss;
365
+ this.url = url;
366
+ this.options = options;
367
+ wss.on("connection", (socket, request) => {
368
+ const connection = new FakeOpenAIConnection(this, socket, request.headers);
369
+ this.connections.push(connection);
370
+ socket.on("message", (raw) => {
371
+ let frame;
372
+ try {
373
+ frame = JSON.parse(raw.toString());
374
+ } catch {
375
+ return;
376
+ }
377
+ connection.received.push(frame);
378
+ if (frame.type === "session.update" && this.options.autoAckSessionUpdate !== false) connection.send({
379
+ type: "session.updated",
380
+ session: frame.session
381
+ });
382
+ });
383
+ socket.send(JSON.stringify({
384
+ type: "session.created",
385
+ session: {}
386
+ }));
387
+ });
388
+ }
389
+ static async start(options = {}) {
390
+ const wss = new WebSocketServer({
391
+ port: 0,
392
+ host: "127.0.0.1"
393
+ });
394
+ await new Promise((resolve, reject) => {
395
+ wss.once("listening", resolve);
396
+ wss.once("error", reject);
397
+ });
398
+ const address = wss.address();
399
+ if (typeof address === "string" || address === null) throw new Error("no server address");
400
+ return new FakeOpenAIServer(wss, `ws://127.0.0.1:${address.port}`, options);
401
+ }
402
+ get latest() {
403
+ const connection = this.connections[this.connections.length - 1];
404
+ if (!connection) throw new Error("no connections yet");
405
+ return connection;
406
+ }
407
+ async waitForConnection(timeoutMs = 2e3) {
408
+ const count = this.connections.length;
409
+ const deadline = Date.now() + timeoutMs;
410
+ while (Date.now() < deadline) {
411
+ if (this.connections.length > count) return this.connections[this.connections.length - 1];
412
+ await setTimeout(5);
413
+ }
414
+ throw new Error("timed out waiting for a provider connection");
415
+ }
416
+ async close() {
417
+ for (const connection of this.connections) try {
418
+ connection.socket.terminate();
419
+ } catch {}
420
+ await new Promise((resolve) => this.wss.close(() => resolve()));
421
+ }
422
+ };
423
+ //#endregion
424
+ //#region src/testing/FakeGeminiLive.ts
425
+ /**
426
+ * FakeGeminiLive — a scripted stand-in for @google/genai's live connection.
427
+ *
428
+ * Instead of a WebSocket server, this fakes the SDK seam: pass
429
+ * `fake.connector` as the Gemini provider's `connector`. Each connect yields a
430
+ * FakeGeminiSession that records client messages and lets tests push server
431
+ * messages (audio parts, tool calls, transcriptions, resumption updates,
432
+ * goAway) through the provider's callbacks.
433
+ */
434
+ var FakeGeminiSession = class {
435
+ params;
436
+ realtimeInputs = [];
437
+ clientContents = [];
438
+ toolResponses = [];
439
+ closed = false;
440
+ constructor(params) {
441
+ this.params = params;
442
+ }
443
+ sendRealtimeInput(input) {
444
+ this.realtimeInputs.push(input.audio);
445
+ }
446
+ sendClientContent(content) {
447
+ this.clientContents.push(content);
448
+ }
449
+ sendToolResponse(response) {
450
+ this.toolResponses.push(response);
451
+ }
452
+ close() {
453
+ if (this.closed) return;
454
+ this.closed = true;
455
+ this.params.callbacks.onclose?.({
456
+ code: 1e3,
457
+ reason: "client close"
458
+ });
459
+ }
460
+ serverMessage(message) {
461
+ this.params.callbacks.onmessage(message);
462
+ }
463
+ completeSetup() {
464
+ this.serverMessage({ setupComplete: {} });
465
+ }
466
+ /** Stream a model turn: PCM24k audio parts (+ transcript), then turnComplete. */
467
+ sendAudioTurn(options) {
468
+ for (const chunk of options.pcm24kBase64Chunks) this.serverMessage({ serverContent: { modelTurn: { parts: [{ inlineData: {
469
+ data: chunk,
470
+ mimeType: "audio/pcm;rate=24000"
471
+ } }] } } });
472
+ if (options.transcript) this.serverMessage({ serverContent: { outputTranscription: { text: options.transcript } } });
473
+ this.serverMessage({ serverContent: { turnComplete: true } });
474
+ }
475
+ sendToolCall(options) {
476
+ const id = options.id ?? `fn_${Math.random().toString(36).slice(2, 8)}`;
477
+ this.serverMessage({ toolCall: { functionCalls: [{
478
+ id,
479
+ name: options.name,
480
+ args: options.args ?? {}
481
+ }] } });
482
+ return id;
483
+ }
484
+ sendInterrupted() {
485
+ this.serverMessage({ serverContent: { interrupted: true } });
486
+ }
487
+ sendInputTranscription(text) {
488
+ this.serverMessage({ serverContent: { inputTranscription: { text } } });
489
+ }
490
+ sendResumptionUpdate(handle) {
491
+ this.serverMessage({ sessionResumptionUpdate: {
492
+ resumable: true,
493
+ newHandle: handle
494
+ } });
495
+ }
496
+ sendGoAway(timeLeft) {
497
+ this.serverMessage({ goAway: { timeLeft } });
498
+ }
499
+ sendUsage(usage) {
500
+ this.serverMessage({ usageMetadata: usage });
501
+ }
502
+ /** Drop the connection from the server side. */
503
+ drop(code = 1011, reason = "fake drop") {
504
+ if (this.closed) return;
505
+ this.closed = true;
506
+ this.params.callbacks.onclose?.({
507
+ code,
508
+ reason
509
+ });
510
+ }
511
+ };
512
+ var FakeGeminiLive = class {
513
+ sessions = [];
514
+ /** Auto-send setupComplete on connect. Default true. */
515
+ autoCompleteSetup = true;
516
+ connector = async (params) => {
517
+ const session = new FakeGeminiSession(params);
518
+ this.sessions.push(session);
519
+ params.callbacks.onopen?.();
520
+ if (this.autoCompleteSetup) queueMicrotask(() => session.completeSetup());
521
+ return session;
522
+ };
523
+ get latest() {
524
+ const session = this.sessions[this.sessions.length - 1];
525
+ if (!session) throw new Error("no gemini sessions yet");
526
+ return session;
527
+ }
528
+ async waitForSessions(count, timeoutMs = 2e3) {
529
+ const deadline = Date.now() + timeoutMs;
530
+ while (Date.now() < deadline) {
531
+ if (this.sessions.length >= count) return this.sessions[count - 1];
532
+ await setTimeout(5);
533
+ }
534
+ throw new Error(`timed out waiting for ${count} gemini sessions (have ${this.sessions.length})`);
535
+ }
536
+ };
537
+ //#endregion
538
+ export { FakeGeminiLive, FakeGeminiSession, FakeOpenAIConnection, FakeOpenAIServer, FakeTwilioMediaStream, mulawSilenceBase64 };