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,402 @@
1
+ import { a as noopLogger, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BQigr5mB.mjs";
2
+ import { t as buildSessionUpdate } from "./session-config-BVLl7-ha.mjs";
3
+ import WebSocket$1 from "ws";
4
+ //#region src/providers/openai-compatible/OpenAICompatibleProvider.ts
5
+ /**
6
+ * Provider for the OpenAI Realtime API (GA protocol) and API-compatible
7
+ * services (xAI Grok Voice Agent). Speaks `audio/pcmu` in both directions, so
8
+ * Twilio payloads pass through verbatim — zero transcoding on this path.
9
+ *
10
+ * Connect handshake: open WS → receive `session.created` → send
11
+ * `session.update` (audio/pcmu, VAD, tools, instructions) → resolve on
12
+ * `session.updated`. A handshake that hangs rejects after `connectTimeoutMs`
13
+ * so a dead upstream can't leak half-open calls.
14
+ */
15
+ const DEFAULT_BASE_URL = "wss://api.openai.com/v1/realtime";
16
+ /** WS close codes where retrying cannot help (protocol/auth/policy failures). */
17
+ const NON_RETRIABLE_CLOSE_CODES = /* @__PURE__ */ new Set([
18
+ 1002,
19
+ 1003,
20
+ 1007,
21
+ 1008
22
+ ]);
23
+ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
24
+ name;
25
+ capabilities;
26
+ config;
27
+ logger;
28
+ ws = null;
29
+ sessionInit = null;
30
+ ready = false;
31
+ intentionalClose = false;
32
+ currentResponseId = null;
33
+ /**
34
+ * response.create serialization. The GA API rejects a `response.create`
35
+ * issued while another response is in flight
36
+ * (`conversation_already_has_active_response`), so creates requested
37
+ * mid-response wait in a single pending slot and fire on `response.done`.
38
+ * Later requests coalesce into that slot — one response reads the whole
39
+ * conversation state, so only the instructions payload is worth keeping.
40
+ */
41
+ responseActive = false;
42
+ pendingCreate = null;
43
+ lastCreateSent = null;
44
+ constructor(config, logger = noopLogger) {
45
+ super();
46
+ this.config = config;
47
+ this.logger = logger;
48
+ this.name = config.providerName ?? "openai";
49
+ this.capabilities = {
50
+ truncate: true,
51
+ sessionUpdate: true,
52
+ voiceChangeMidSession: false,
53
+ transcodeRequired: false,
54
+ resumption: false,
55
+ agentTranscriptDeltas: true,
56
+ vadInterruptControl: true,
57
+ ...config.capabilityOverrides
58
+ };
59
+ }
60
+ get isConnected() {
61
+ return this.ready && this.ws?.readyState === WebSocket$1.OPEN;
62
+ }
63
+ async connect(init) {
64
+ if (this.ws) await this.close();
65
+ let vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
66
+ if (init.bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && vad !== null) vad = {
67
+ interruptResponse: false,
68
+ ...vad ?? { type: "server" }
69
+ };
70
+ this.sessionInit = {
71
+ ...init,
72
+ vad,
73
+ transcription: init.transcription !== void 0 ? init.transcription : this.config.defaultTranscription
74
+ };
75
+ this.intentionalClose = false;
76
+ this.ready = false;
77
+ this.responseActive = false;
78
+ this.pendingCreate = null;
79
+ this.lastCreateSent = null;
80
+ this.currentResponseId = null;
81
+ const url = `${this.config.baseUrl ?? DEFAULT_BASE_URL}?model=${encodeURIComponent(this.config.model)}`;
82
+ const ws = new WebSocket$1(url, {
83
+ headers: {
84
+ Authorization: `Bearer ${this.config.apiKey}`,
85
+ ...this.config.headers
86
+ },
87
+ perMessageDeflate: false
88
+ });
89
+ this.ws = ws;
90
+ await new Promise((resolve, reject) => {
91
+ const timeoutMs = this.config.connectTimeoutMs ?? 1e4;
92
+ const timer = setTimeout(() => {
93
+ fail(/* @__PURE__ */ new Error(`${this.name} session not ready within ${timeoutMs}ms`));
94
+ }, timeoutMs);
95
+ timer.unref?.();
96
+ let settled = false;
97
+ const fail = (error) => {
98
+ if (settled) return;
99
+ settled = true;
100
+ clearTimeout(timer);
101
+ try {
102
+ ws.close();
103
+ } catch {}
104
+ reject(error);
105
+ };
106
+ ws.on("unexpected-response", (_request, response) => {
107
+ let body = "";
108
+ response.setEncoding("utf8");
109
+ response.on("data", (chunk) => {
110
+ if (body.length < 512) body += chunk;
111
+ });
112
+ response.on("end", () => {
113
+ const detail = body.trim().slice(0, 500);
114
+ this.logger.error("provider rejected the WebSocket upgrade", {
115
+ status: response.statusCode,
116
+ body: detail
117
+ });
118
+ fail(/* @__PURE__ */ new Error(`${this.name} rejected the WebSocket upgrade: HTTP ${response.statusCode}${detail ? ` — ${detail}` : ""}`));
119
+ });
120
+ });
121
+ ws.on("open", () => this.emit("open"));
122
+ ws.on("message", (raw) => {
123
+ const event = this.parseEvent(raw);
124
+ if (!event) return;
125
+ if (!settled) {
126
+ if (event.type === "session.created") {
127
+ this.send(this.buildSessionPayload());
128
+ return;
129
+ }
130
+ if (event.type === "session.updated") {
131
+ settled = true;
132
+ clearTimeout(timer);
133
+ this.ready = true;
134
+ resolve();
135
+ return;
136
+ }
137
+ if (event.type === "error") {
138
+ fail(/* @__PURE__ */ new Error(`${this.name} session setup error: ${JSON.stringify(event.error ?? event)}`));
139
+ return;
140
+ }
141
+ }
142
+ this.handleEvent(event);
143
+ });
144
+ ws.on("error", (error) => {
145
+ if (!settled) fail(error instanceof Error ? error : new Error(String(error)));
146
+ else this.emit("error", error instanceof Error ? error : new Error(String(error)));
147
+ });
148
+ ws.on("close", (code, reasonBuf) => {
149
+ const reason = reasonBuf?.toString();
150
+ this.ready = false;
151
+ if (!settled) {
152
+ fail(/* @__PURE__ */ new Error(`${this.name} socket closed during setup (${code} ${reason ?? ""})`));
153
+ return;
154
+ }
155
+ this.emit("close", {
156
+ code,
157
+ reason,
158
+ retriable: !this.intentionalClose && !NON_RETRIABLE_CLOSE_CODES.has(code)
159
+ });
160
+ });
161
+ });
162
+ }
163
+ async close() {
164
+ this.intentionalClose = true;
165
+ this.ready = false;
166
+ const ws = this.ws;
167
+ this.ws = null;
168
+ if (!ws) return;
169
+ if (ws.readyState === WebSocket$1.CLOSED) return;
170
+ await new Promise((resolve) => {
171
+ const timer = setTimeout(() => {
172
+ ws.terminate();
173
+ resolve();
174
+ }, 1e3);
175
+ timer.unref?.();
176
+ ws.once("close", () => {
177
+ clearTimeout(timer);
178
+ resolve();
179
+ });
180
+ try {
181
+ ws.close(1e3);
182
+ } catch {
183
+ clearTimeout(timer);
184
+ ws.terminate();
185
+ resolve();
186
+ }
187
+ });
188
+ }
189
+ sendAudio(base64Mulaw) {
190
+ if (!this.isConnected) return;
191
+ this.send({
192
+ type: "input_audio_buffer.append",
193
+ audio: base64Mulaw
194
+ });
195
+ }
196
+ sendText(text, options = {}) {
197
+ const role = options.role ?? "user";
198
+ this.send({
199
+ type: "conversation.item.create",
200
+ item: {
201
+ type: "message",
202
+ role,
203
+ content: [{
204
+ type: role === "assistant" ? "output_text" : "input_text",
205
+ text
206
+ }]
207
+ }
208
+ });
209
+ if (options.triggerResponse !== false) this.createResponse();
210
+ }
211
+ sendToolResult(callId, output, options = {}) {
212
+ this.send({
213
+ type: "conversation.item.create",
214
+ item: {
215
+ type: "function_call_output",
216
+ call_id: callId,
217
+ output: typeof output === "string" ? output : JSON.stringify(output ?? null)
218
+ }
219
+ });
220
+ if (options.triggerResponse !== false) this.createResponse();
221
+ }
222
+ createResponse(options = {}) {
223
+ if (this.responseActive) {
224
+ this.pendingCreate = {
225
+ instructions: options.instructions ?? this.pendingCreate?.instructions,
226
+ attempts: 0
227
+ };
228
+ return;
229
+ }
230
+ this.sendCreate({
231
+ instructions: options.instructions,
232
+ attempts: 0
233
+ });
234
+ }
235
+ sendCreate(create) {
236
+ this.lastCreateSent = create;
237
+ this.send({
238
+ type: "response.create",
239
+ ...create.instructions ? { response: { instructions: create.instructions } } : {}
240
+ });
241
+ }
242
+ async updateSession(patch) {
243
+ if (!this.sessionInit) throw new Error("updateSession before connect");
244
+ this.sessionInit = {
245
+ ...this.sessionInit,
246
+ ...patch
247
+ };
248
+ this.send(this.buildSessionPayload());
249
+ }
250
+ buildSessionPayload() {
251
+ return (this.config.buildSession ?? buildSessionUpdate)(this.sessionInit, {
252
+ defaultVoice: this.config.voice,
253
+ extraSessionOptions: this.config.extraSessionOptions
254
+ });
255
+ }
256
+ cancelResponse() {
257
+ if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
258
+ this.send({ type: "response.cancel" });
259
+ }
260
+ truncatePlayback(itemId, audioEndMs) {
261
+ this.send({
262
+ type: "conversation.item.truncate",
263
+ item_id: itemId,
264
+ content_index: 0,
265
+ audio_end_ms: Math.max(0, Math.round(audioEndMs))
266
+ });
267
+ }
268
+ handleEvent(event) {
269
+ switch (event.type) {
270
+ case "response.created": {
271
+ const responseId = event.response?.id ?? `resp_${Date.now()}`;
272
+ this.currentResponseId = responseId;
273
+ this.responseActive = true;
274
+ this.emit("responseStarted", { responseId });
275
+ break;
276
+ }
277
+ case "response.output_item.added": {
278
+ const itemId = event.item?.id;
279
+ if (itemId) this.emit("outputItemAdded", {
280
+ itemId,
281
+ responseId: event.response_id ?? this.currentResponseId ?? ""
282
+ });
283
+ break;
284
+ }
285
+ case "response.output_audio.delta":
286
+ if (typeof event.delta === "string" && event.delta.length > 0) this.emit("audio", {
287
+ base64Mulaw: event.delta,
288
+ responseId: event.response_id ?? this.currentResponseId ?? "",
289
+ itemId: event.item_id
290
+ });
291
+ break;
292
+ case "response.output_audio_transcript.delta":
293
+ if (typeof event.delta === "string") this.emit("agentTranscriptDelta", {
294
+ responseId: event.response_id ?? this.currentResponseId ?? "",
295
+ delta: event.delta
296
+ });
297
+ break;
298
+ case "response.output_audio_transcript.done":
299
+ if (typeof event.transcript === "string") this.emit("agentTranscript", {
300
+ responseId: event.response_id ?? this.currentResponseId ?? "",
301
+ text: event.transcript
302
+ });
303
+ break;
304
+ case "conversation.item.input_audio_transcription.completed":
305
+ if (typeof event.transcript === "string" && event.transcript.trim().length > 0) this.emit("userTranscript", { text: event.transcript.trim() });
306
+ break;
307
+ case "input_audio_buffer.speech_started":
308
+ if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
309
+ this.emit("userSpeechStarted");
310
+ break;
311
+ case "input_audio_buffer.speech_stopped":
312
+ this.emit("userSpeechStopped");
313
+ break;
314
+ case "response.done": {
315
+ const responseId = event.response?.id ?? this.currentResponseId ?? "";
316
+ const usage = normalizeUsage(event.response?.usage);
317
+ this.responseActive = false;
318
+ const pending = this.pendingCreate;
319
+ if (pending) {
320
+ this.pendingCreate = null;
321
+ this.sendCreate(pending);
322
+ }
323
+ if (usage) this.emit("usage", usage);
324
+ this.emit("responseDone", {
325
+ responseId,
326
+ usage: usage ?? void 0
327
+ });
328
+ break;
329
+ }
330
+ case "response.function_call_arguments.done":
331
+ this.emit("toolCall", {
332
+ id: event.call_id ?? event.item_id ?? `call_${Date.now()}`,
333
+ name: event.name,
334
+ argumentsJson: typeof event.arguments === "string" ? event.arguments : "{}",
335
+ responseId: event.response_id ?? this.currentResponseId ?? void 0,
336
+ itemId: event.item_id
337
+ });
338
+ break;
339
+ case "error": {
340
+ const code = event.error?.code;
341
+ if (code === "conversation_already_has_active_response") {
342
+ this.responseActive = true;
343
+ const last = this.lastCreateSent;
344
+ if (last && last.attempts < 2 && !this.pendingCreate) this.pendingCreate = {
345
+ ...last,
346
+ attempts: last.attempts + 1
347
+ };
348
+ this.logger.warn("response.create raced an active response; deferred until it completes", { error: event.error });
349
+ break;
350
+ }
351
+ {
352
+ const message = typeof event.error?.message === "string" ? event.error.message : "";
353
+ if (code === "response_cancel_not_active" || /cancellation failed: no active response/i.test(message)) {
354
+ this.logger.warn("response.cancel raced completion (ignored)", { error: event.error });
355
+ break;
356
+ }
357
+ }
358
+ this.logger.warn("provider error event", { error: event.error });
359
+ this.emit("error", /* @__PURE__ */ new Error(`${this.name} error: ${JSON.stringify(event.error ?? event)}`));
360
+ break;
361
+ }
362
+ }
363
+ }
364
+ parseEvent(raw) {
365
+ try {
366
+ return JSON.parse(raw.toString());
367
+ } catch {
368
+ this.logger.warn("unparseable provider frame");
369
+ return null;
370
+ }
371
+ }
372
+ send(payload) {
373
+ if (this.ws?.readyState !== WebSocket$1.OPEN) return;
374
+ try {
375
+ this.ws.send(JSON.stringify(payload));
376
+ } catch (error) {
377
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
378
+ }
379
+ }
380
+ };
381
+ function normalizeUsage(raw) {
382
+ if (!raw || typeof raw !== "object") return null;
383
+ const inputDetails = raw.input_token_details ?? raw.input_tokens_details;
384
+ const outputDetails = raw.output_token_details ?? raw.output_tokens_details;
385
+ return {
386
+ inputTokens: raw.input_tokens ?? 0,
387
+ outputTokens: raw.output_tokens ?? 0,
388
+ totalTokens: raw.total_tokens ?? (raw.input_tokens ?? 0) + (raw.output_tokens ?? 0),
389
+ ...inputDetails ? { inputTokenDetails: {
390
+ textTokens: inputDetails.text_tokens,
391
+ audioTokens: inputDetails.audio_tokens,
392
+ cachedTokens: inputDetails.cached_tokens
393
+ } } : {},
394
+ ...outputDetails ? { outputTokenDetails: {
395
+ textTokens: outputDetails.text_tokens,
396
+ audioTokens: outputDetails.audio_tokens
397
+ } } : {},
398
+ raw
399
+ };
400
+ }
401
+ //#endregion
402
+ export { OpenAICompatibleProvider as t };