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,265 @@
1
+ import { t as TypedEmitter } from "./events-BylBSBW-.mjs";
2
+ //#region src/twilio/messages.ts
3
+ const INBOUND_EVENTS = /* @__PURE__ */ new Set([
4
+ "connected",
5
+ "start",
6
+ "media",
7
+ "stop",
8
+ "mark",
9
+ "dtmf"
10
+ ]);
11
+ /**
12
+ * Parse a raw WebSocket frame into a Twilio inbound message.
13
+ * Returns null for non-JSON frames or unknown event types (Twilio may add
14
+ * events; unknown ones must not crash a live call).
15
+ */
16
+ function parseTwilioMessage(raw) {
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ if (typeof parsed !== "object" || parsed === null) return null;
24
+ const event = parsed.event;
25
+ if (typeof event !== "string" || !INBOUND_EVENTS.has(event)) return null;
26
+ return parsed;
27
+ }
28
+ //#endregion
29
+ //#region src/twilio/transport.ts
30
+ /**
31
+ * TwilioMediaTransport — framing layer over one Twilio Media Stream WebSocket.
32
+ *
33
+ * Wraps any ws-shaped socket (`ws`, @fastify/websocket, express-ws all hand
34
+ * you one), parses inbound frames into typed events, and provides outbound
35
+ * send helpers that carry the streamSid captured from `start`.
36
+ *
37
+ * The handshake (`awaitStart`) tolerates Twilio's `connected` preamble without
38
+ * hardcoding it, bounds the number of pre-start frames, and times out — a
39
+ * socket that never sends `start` must not leak a session.
40
+ */
41
+ /** ws.OPEN — sockets without readyState are assumed open. */
42
+ const OPEN = 1;
43
+ var TwilioMediaTransport = class extends TypedEmitter {
44
+ ws;
45
+ streamSidValue = null;
46
+ startEvent = null;
47
+ closed = false;
48
+ constructor(ws) {
49
+ super();
50
+ this.ws = ws;
51
+ ws.on("message", (data) => this.handleMessage(data));
52
+ ws.on("close", (code, reason) => {
53
+ this.closed = true;
54
+ this.emit("close", {
55
+ code,
56
+ reason: reason?.toString()
57
+ });
58
+ });
59
+ ws.on("error", (error) => this.emit("error", error));
60
+ }
61
+ get streamSid() {
62
+ return this.streamSidValue;
63
+ }
64
+ get start() {
65
+ return this.startEvent;
66
+ }
67
+ get isOpen() {
68
+ return !this.closed && (this.ws.readyState === void 0 || this.ws.readyState === OPEN);
69
+ }
70
+ /**
71
+ * Resolve with the `start` frame, or reject on timeout / too many pre-start
72
+ * frames / socket close. Media frames arriving before `start` are counted
73
+ * but not delivered (there is no session to deliver them to yet).
74
+ */
75
+ awaitStart(options = {}) {
76
+ if (this.startEvent) return Promise.resolve(this.startEvent);
77
+ const timeoutMs = options.timeoutMs ?? 5e3;
78
+ const maxPreStart = options.maxPreStartMessages ?? 8;
79
+ return new Promise((resolve, reject) => {
80
+ let preStartSeen = 0;
81
+ const timer = setTimeout(() => {
82
+ cleanup();
83
+ reject(/* @__PURE__ */ new Error(`Twilio start frame not received within ${timeoutMs}ms`));
84
+ }, timeoutMs);
85
+ timer.unref?.();
86
+ const onStart = (event) => {
87
+ cleanup();
88
+ resolve(event);
89
+ };
90
+ const onPreStart = () => {
91
+ preStartSeen++;
92
+ if (preStartSeen > maxPreStart) {
93
+ cleanup();
94
+ reject(/* @__PURE__ */ new Error(`no start frame within the first ${maxPreStart} messages`));
95
+ }
96
+ };
97
+ const onClose = () => {
98
+ cleanup();
99
+ reject(/* @__PURE__ */ new Error("socket closed before start frame"));
100
+ };
101
+ const cleanup = () => {
102
+ clearTimeout(timer);
103
+ this.off("start", onStart);
104
+ this.off("close", onClose);
105
+ this.off("media", onPreStart);
106
+ };
107
+ this.on("start", onStart);
108
+ this.on("media", onPreStart);
109
+ this.on("close", onClose);
110
+ });
111
+ }
112
+ /** Forward one media payload (base64 μ-law) to the caller. */
113
+ sendMedia(payload) {
114
+ const streamSid = this.requireStreamSid();
115
+ this.sendRaw(JSON.stringify({
116
+ event: "media",
117
+ streamSid,
118
+ media: { payload }
119
+ }));
120
+ }
121
+ /** Interleave a named mark; Twilio echoes it once playout reaches it. */
122
+ sendMark(name) {
123
+ const streamSid = this.requireStreamSid();
124
+ this.sendRaw(JSON.stringify({
125
+ event: "mark",
126
+ streamSid,
127
+ mark: { name }
128
+ }));
129
+ }
130
+ /** Flush Twilio's playout buffer (barge-in). Pending marks echo back as discarded. */
131
+ sendClear() {
132
+ const streamSid = this.requireStreamSid();
133
+ this.sendRaw(JSON.stringify({
134
+ event: "clear",
135
+ streamSid
136
+ }));
137
+ }
138
+ close(code, reason) {
139
+ if (this.closed) return;
140
+ this.closed = true;
141
+ try {
142
+ this.ws.close(code, reason);
143
+ } catch {}
144
+ }
145
+ handleMessage(data) {
146
+ const message = parseTwilioMessage(data);
147
+ if (!message) return;
148
+ switch (message.event) {
149
+ case "start":
150
+ this.startEvent = message;
151
+ this.streamSidValue = message.start.streamSid ?? message.streamSid;
152
+ this.emit("start", message);
153
+ break;
154
+ case "media":
155
+ this.emit("media", message);
156
+ break;
157
+ case "stop":
158
+ this.emit("stop", message);
159
+ break;
160
+ case "mark":
161
+ this.emit("mark", message);
162
+ break;
163
+ case "dtmf": this.emit("dtmf", message);
164
+ }
165
+ }
166
+ requireStreamSid() {
167
+ if (!this.streamSidValue) throw new Error("cannot send to Twilio before the start frame (no streamSid yet)");
168
+ return this.streamSidValue;
169
+ }
170
+ sendRaw(json) {
171
+ if (!this.isOpen) return;
172
+ try {
173
+ this.ws.send(json);
174
+ } catch (error) {
175
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
176
+ }
177
+ }
178
+ };
179
+ //#endregion
180
+ //#region src/twilio/twiml.ts
181
+ /** TwiML helpers for wiring a call into the media-stream bridge. */
182
+ function escapeXml(value) {
183
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
184
+ }
185
+ /**
186
+ * Build the TwiML that connects a call to the bridge.
187
+ *
188
+ * `<Connect><Stream>` is the bidirectional form. `<Start><Stream>` is
189
+ * send-only — the caller would never hear the agent.
190
+ */
191
+ function connectStreamTwiml(options) {
192
+ const params = Object.entries(options.parameters ?? {}).map(([name, value]) => `\n <Parameter name="${escapeXml(name)}" value="${escapeXml(value)}" />`).join("");
193
+ return `<?xml version="1.0" encoding="UTF-8"?>
194
+ <Response>
195
+ <Connect>
196
+ ${params ? `<Stream url="${escapeXml(options.wsUrl)}">${params}\n </Stream>` : `<Stream url="${escapeXml(options.wsUrl)}" />`}
197
+ </Connect>
198
+ </Response>`;
199
+ }
200
+ //#endregion
201
+ //#region src/twilio/rest.ts
202
+ /**
203
+ * Twilio REST control plane: hangup, transfer, SMS/WhatsApp.
204
+ *
205
+ * Media and control are different planes — the WebSocket carries audio, but
206
+ * ending or redirecting the PSTN leg goes through the REST API. The `twilio`
207
+ * SDK is an optional peer dependency, imported lazily so bridges that never
208
+ * use REST features don't need it installed.
209
+ */
210
+ /** Loose E.164 check — validated BEFORE interpolation into TwiML. */
211
+ const PHONE_PATTERN = /^\+?[0-9]{5,20}$/;
212
+ function assertValidPhoneNumber(phoneNumber) {
213
+ if (!PHONE_PATTERN.test(phoneNumber)) throw new Error(`invalid phone number "${phoneNumber}" (expected E.164-like digits, e.g. +15551234567)`);
214
+ }
215
+ var TwilioRestClient = class {
216
+ config;
217
+ clientPromise = null;
218
+ constructor(config) {
219
+ this.config = config;
220
+ }
221
+ client() {
222
+ if (!this.clientPromise) this.clientPromise = import("twilio").then((mod) => {
223
+ return (mod.default ?? mod)(this.config.accountSid, this.config.authToken);
224
+ }).catch((error) => {
225
+ this.clientPromise = null;
226
+ throw new Error(`the 'twilio' package is required for REST call control — npm install twilio (${String(error)})`);
227
+ });
228
+ return this.clientPromise;
229
+ }
230
+ /** End the PSTN leg cleanly (better than just dropping the stream). */
231
+ async completeCall(callSid) {
232
+ await (await this.client()).calls(callSid).update({ status: "completed" });
233
+ }
234
+ /**
235
+ * Blind-transfer the call: replace its TwiML with a `<Dial>`. The number
236
+ * is validated and XML-escaped — never interpolate LLM output raw into
237
+ * markup.
238
+ */
239
+ async transferCall(callSid, phoneNumber, options = {}) {
240
+ assertValidPhoneNumber(phoneNumber);
241
+ const callerId = options.callerId ?? this.config.callerId;
242
+ if (callerId) assertValidPhoneNumber(callerId);
243
+ const twiml = `<Response><Dial${callerId ? ` callerId="${escapeXml(callerId)}"` : ""}><Number>${escapeXml(phoneNumber)}</Number></Dial></Response>`;
244
+ await (await this.client()).calls(callSid).update({ twiml });
245
+ }
246
+ async sendSms(options) {
247
+ assertValidPhoneNumber(options.to);
248
+ return { sid: (await (await this.client()).messages.create({
249
+ to: options.to,
250
+ from: options.from,
251
+ body: options.body
252
+ })).sid };
253
+ }
254
+ async sendWhatsapp(options) {
255
+ const to = options.to.startsWith("whatsapp:") ? options.to : `whatsapp:${options.to}`;
256
+ const from = options.from.startsWith("whatsapp:") ? options.from : `whatsapp:${options.from}`;
257
+ return { sid: (await (await this.client()).messages.create({
258
+ to,
259
+ from,
260
+ body: options.body
261
+ })).sid };
262
+ }
263
+ };
264
+ //#endregion
265
+ export { TwilioMediaTransport as a, escapeXml as i, assertValidPhoneNumber as n, parseTwilioMessage as o, connectStreamTwiml as r, TwilioRestClient as t };
@@ -0,0 +1,300 @@
1
+ const require_events = require("./events-BxDTIKKq.cjs");
2
+ //#region src/twilio/messages.ts
3
+ const INBOUND_EVENTS = /* @__PURE__ */ new Set([
4
+ "connected",
5
+ "start",
6
+ "media",
7
+ "stop",
8
+ "mark",
9
+ "dtmf"
10
+ ]);
11
+ /**
12
+ * Parse a raw WebSocket frame into a Twilio inbound message.
13
+ * Returns null for non-JSON frames or unknown event types (Twilio may add
14
+ * events; unknown ones must not crash a live call).
15
+ */
16
+ function parseTwilioMessage(raw) {
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ if (typeof parsed !== "object" || parsed === null) return null;
24
+ const event = parsed.event;
25
+ if (typeof event !== "string" || !INBOUND_EVENTS.has(event)) return null;
26
+ return parsed;
27
+ }
28
+ //#endregion
29
+ //#region src/twilio/transport.ts
30
+ /**
31
+ * TwilioMediaTransport — framing layer over one Twilio Media Stream WebSocket.
32
+ *
33
+ * Wraps any ws-shaped socket (`ws`, @fastify/websocket, express-ws all hand
34
+ * you one), parses inbound frames into typed events, and provides outbound
35
+ * send helpers that carry the streamSid captured from `start`.
36
+ *
37
+ * The handshake (`awaitStart`) tolerates Twilio's `connected` preamble without
38
+ * hardcoding it, bounds the number of pre-start frames, and times out — a
39
+ * socket that never sends `start` must not leak a session.
40
+ */
41
+ /** ws.OPEN — sockets without readyState are assumed open. */
42
+ const OPEN = 1;
43
+ var TwilioMediaTransport = class extends require_events.TypedEmitter {
44
+ ws;
45
+ streamSidValue = null;
46
+ startEvent = null;
47
+ closed = false;
48
+ constructor(ws) {
49
+ super();
50
+ this.ws = ws;
51
+ ws.on("message", (data) => this.handleMessage(data));
52
+ ws.on("close", (code, reason) => {
53
+ this.closed = true;
54
+ this.emit("close", {
55
+ code,
56
+ reason: reason?.toString()
57
+ });
58
+ });
59
+ ws.on("error", (error) => this.emit("error", error));
60
+ }
61
+ get streamSid() {
62
+ return this.streamSidValue;
63
+ }
64
+ get start() {
65
+ return this.startEvent;
66
+ }
67
+ get isOpen() {
68
+ return !this.closed && (this.ws.readyState === void 0 || this.ws.readyState === OPEN);
69
+ }
70
+ /**
71
+ * Resolve with the `start` frame, or reject on timeout / too many pre-start
72
+ * frames / socket close. Media frames arriving before `start` are counted
73
+ * but not delivered (there is no session to deliver them to yet).
74
+ */
75
+ awaitStart(options = {}) {
76
+ if (this.startEvent) return Promise.resolve(this.startEvent);
77
+ const timeoutMs = options.timeoutMs ?? 5e3;
78
+ const maxPreStart = options.maxPreStartMessages ?? 8;
79
+ return new Promise((resolve, reject) => {
80
+ let preStartSeen = 0;
81
+ const timer = setTimeout(() => {
82
+ cleanup();
83
+ reject(/* @__PURE__ */ new Error(`Twilio start frame not received within ${timeoutMs}ms`));
84
+ }, timeoutMs);
85
+ timer.unref?.();
86
+ const onStart = (event) => {
87
+ cleanup();
88
+ resolve(event);
89
+ };
90
+ const onPreStart = () => {
91
+ preStartSeen++;
92
+ if (preStartSeen > maxPreStart) {
93
+ cleanup();
94
+ reject(/* @__PURE__ */ new Error(`no start frame within the first ${maxPreStart} messages`));
95
+ }
96
+ };
97
+ const onClose = () => {
98
+ cleanup();
99
+ reject(/* @__PURE__ */ new Error("socket closed before start frame"));
100
+ };
101
+ const cleanup = () => {
102
+ clearTimeout(timer);
103
+ this.off("start", onStart);
104
+ this.off("close", onClose);
105
+ this.off("media", onPreStart);
106
+ };
107
+ this.on("start", onStart);
108
+ this.on("media", onPreStart);
109
+ this.on("close", onClose);
110
+ });
111
+ }
112
+ /** Forward one media payload (base64 μ-law) to the caller. */
113
+ sendMedia(payload) {
114
+ const streamSid = this.requireStreamSid();
115
+ this.sendRaw(JSON.stringify({
116
+ event: "media",
117
+ streamSid,
118
+ media: { payload }
119
+ }));
120
+ }
121
+ /** Interleave a named mark; Twilio echoes it once playout reaches it. */
122
+ sendMark(name) {
123
+ const streamSid = this.requireStreamSid();
124
+ this.sendRaw(JSON.stringify({
125
+ event: "mark",
126
+ streamSid,
127
+ mark: { name }
128
+ }));
129
+ }
130
+ /** Flush Twilio's playout buffer (barge-in). Pending marks echo back as discarded. */
131
+ sendClear() {
132
+ const streamSid = this.requireStreamSid();
133
+ this.sendRaw(JSON.stringify({
134
+ event: "clear",
135
+ streamSid
136
+ }));
137
+ }
138
+ close(code, reason) {
139
+ if (this.closed) return;
140
+ this.closed = true;
141
+ try {
142
+ this.ws.close(code, reason);
143
+ } catch {}
144
+ }
145
+ handleMessage(data) {
146
+ const message = parseTwilioMessage(data);
147
+ if (!message) return;
148
+ switch (message.event) {
149
+ case "start":
150
+ this.startEvent = message;
151
+ this.streamSidValue = message.start.streamSid ?? message.streamSid;
152
+ this.emit("start", message);
153
+ break;
154
+ case "media":
155
+ this.emit("media", message);
156
+ break;
157
+ case "stop":
158
+ this.emit("stop", message);
159
+ break;
160
+ case "mark":
161
+ this.emit("mark", message);
162
+ break;
163
+ case "dtmf": this.emit("dtmf", message);
164
+ }
165
+ }
166
+ requireStreamSid() {
167
+ if (!this.streamSidValue) throw new Error("cannot send to Twilio before the start frame (no streamSid yet)");
168
+ return this.streamSidValue;
169
+ }
170
+ sendRaw(json) {
171
+ if (!this.isOpen) return;
172
+ try {
173
+ this.ws.send(json);
174
+ } catch (error) {
175
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
176
+ }
177
+ }
178
+ };
179
+ //#endregion
180
+ //#region src/twilio/twiml.ts
181
+ /** TwiML helpers for wiring a call into the media-stream bridge. */
182
+ function escapeXml(value) {
183
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
184
+ }
185
+ /**
186
+ * Build the TwiML that connects a call to the bridge.
187
+ *
188
+ * `<Connect><Stream>` is the bidirectional form. `<Start><Stream>` is
189
+ * send-only — the caller would never hear the agent.
190
+ */
191
+ function connectStreamTwiml(options) {
192
+ const params = Object.entries(options.parameters ?? {}).map(([name, value]) => `\n <Parameter name="${escapeXml(name)}" value="${escapeXml(value)}" />`).join("");
193
+ return `<?xml version="1.0" encoding="UTF-8"?>
194
+ <Response>
195
+ <Connect>
196
+ ${params ? `<Stream url="${escapeXml(options.wsUrl)}">${params}\n </Stream>` : `<Stream url="${escapeXml(options.wsUrl)}" />`}
197
+ </Connect>
198
+ </Response>`;
199
+ }
200
+ //#endregion
201
+ //#region src/twilio/rest.ts
202
+ /**
203
+ * Twilio REST control plane: hangup, transfer, SMS/WhatsApp.
204
+ *
205
+ * Media and control are different planes — the WebSocket carries audio, but
206
+ * ending or redirecting the PSTN leg goes through the REST API. The `twilio`
207
+ * SDK is an optional peer dependency, imported lazily so bridges that never
208
+ * use REST features don't need it installed.
209
+ */
210
+ /** Loose E.164 check — validated BEFORE interpolation into TwiML. */
211
+ const PHONE_PATTERN = /^\+?[0-9]{5,20}$/;
212
+ function assertValidPhoneNumber(phoneNumber) {
213
+ if (!PHONE_PATTERN.test(phoneNumber)) throw new Error(`invalid phone number "${phoneNumber}" (expected E.164-like digits, e.g. +15551234567)`);
214
+ }
215
+ var TwilioRestClient = class {
216
+ config;
217
+ clientPromise = null;
218
+ constructor(config) {
219
+ this.config = config;
220
+ }
221
+ client() {
222
+ if (!this.clientPromise) this.clientPromise = import("twilio").then((mod) => {
223
+ return (mod.default ?? mod)(this.config.accountSid, this.config.authToken);
224
+ }).catch((error) => {
225
+ this.clientPromise = null;
226
+ throw new Error(`the 'twilio' package is required for REST call control — npm install twilio (${String(error)})`);
227
+ });
228
+ return this.clientPromise;
229
+ }
230
+ /** End the PSTN leg cleanly (better than just dropping the stream). */
231
+ async completeCall(callSid) {
232
+ await (await this.client()).calls(callSid).update({ status: "completed" });
233
+ }
234
+ /**
235
+ * Blind-transfer the call: replace its TwiML with a `<Dial>`. The number
236
+ * is validated and XML-escaped — never interpolate LLM output raw into
237
+ * markup.
238
+ */
239
+ async transferCall(callSid, phoneNumber, options = {}) {
240
+ assertValidPhoneNumber(phoneNumber);
241
+ const callerId = options.callerId ?? this.config.callerId;
242
+ if (callerId) assertValidPhoneNumber(callerId);
243
+ const twiml = `<Response><Dial${callerId ? ` callerId="${escapeXml(callerId)}"` : ""}><Number>${escapeXml(phoneNumber)}</Number></Dial></Response>`;
244
+ await (await this.client()).calls(callSid).update({ twiml });
245
+ }
246
+ async sendSms(options) {
247
+ assertValidPhoneNumber(options.to);
248
+ return { sid: (await (await this.client()).messages.create({
249
+ to: options.to,
250
+ from: options.from,
251
+ body: options.body
252
+ })).sid };
253
+ }
254
+ async sendWhatsapp(options) {
255
+ const to = options.to.startsWith("whatsapp:") ? options.to : `whatsapp:${options.to}`;
256
+ const from = options.from.startsWith("whatsapp:") ? options.from : `whatsapp:${options.from}`;
257
+ return { sid: (await (await this.client()).messages.create({
258
+ to,
259
+ from,
260
+ body: options.body
261
+ })).sid };
262
+ }
263
+ };
264
+ //#endregion
265
+ Object.defineProperty(exports, "TwilioMediaTransport", {
266
+ enumerable: true,
267
+ get: function() {
268
+ return TwilioMediaTransport;
269
+ }
270
+ });
271
+ Object.defineProperty(exports, "TwilioRestClient", {
272
+ enumerable: true,
273
+ get: function() {
274
+ return TwilioRestClient;
275
+ }
276
+ });
277
+ Object.defineProperty(exports, "assertValidPhoneNumber", {
278
+ enumerable: true,
279
+ get: function() {
280
+ return assertValidPhoneNumber;
281
+ }
282
+ });
283
+ Object.defineProperty(exports, "connectStreamTwiml", {
284
+ enumerable: true,
285
+ get: function() {
286
+ return connectStreamTwiml;
287
+ }
288
+ });
289
+ Object.defineProperty(exports, "escapeXml", {
290
+ enumerable: true,
291
+ get: function() {
292
+ return escapeXml;
293
+ }
294
+ });
295
+ Object.defineProperty(exports, "parseTwilioMessage", {
296
+ enumerable: true,
297
+ get: function() {
298
+ return parseTwilioMessage;
299
+ }
300
+ });
@@ -0,0 +1,28 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ Object.defineProperty(exports, "__toESM", {
24
+ enumerable: true,
25
+ get: function() {
26
+ return __toESM;
27
+ }
28
+ });
@@ -0,0 +1,74 @@
1
+ import { n as deepMerge } from "./BaseRealtimeProvider-BQigr5mB.mjs";
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 = deepMerge(session, init.providerOptions);
67
+ session = deepMerge(session, config.extraSessionOptions);
68
+ return {
69
+ type: "session.update",
70
+ session
71
+ };
72
+ }
73
+ //#endregion
74
+ export { buildTurnDetection as n, buildSessionUpdate as t };