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