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,395 @@
1
+ import { a as noopLogger, n as deepMerge, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BQigr5mB.mjs";
2
+ import { t as resolveApiKey } from "./env-DUwUWTsg.mjs";
3
+ import { n as OutboundTranscoder, t as InboundTranscoder } from "./transcode-CsAp97G9.mjs";
4
+ //#region src/providers/gemini/GeminiLiveProvider.ts
5
+ /**
6
+ * Gemini Live provider via the official `@google/genai` SDK (optional peer).
7
+ *
8
+ * Unlike the OpenAI-compatible path, Gemini speaks PCM (16 kHz in, 24 kHz
9
+ * out), so this provider owns a stateful transcoding pair: μ-law↔PCM with
10
+ * inter-chunk resampler state — the engine stays μ-law-only.
11
+ *
12
+ * Gemini specifics handled here:
13
+ * - turn ids are synthesized (`gturn_N`) — the wire has no response ids
14
+ * - `serverContent.interrupted` is the barge-in signal (maps to
15
+ * userSpeechStarted + responseDone; the server already cancelled itself)
16
+ * - session resumption handles are captured continuously and replayed on the
17
+ * next connect, so reconnects restore context server-side
18
+ * - `goAway` (imminent disconnect, e.g. the session time limit) is surfaced
19
+ * so the session can reconnect proactively
20
+ * - tool results go through the proper `toolResponse` message
21
+ */
22
+ const GEMINI_INPUT_MIME = "audio/pcm;rate=16000";
23
+ var GeminiLiveProvider = class extends BaseRealtimeProvider {
24
+ name = "gemini";
25
+ capabilities = {
26
+ truncate: false,
27
+ sessionUpdate: false,
28
+ voiceChangeMidSession: false,
29
+ transcodeRequired: true,
30
+ resumption: true,
31
+ agentTranscriptDeltas: true,
32
+ vadInterruptControl: false
33
+ };
34
+ config;
35
+ logger;
36
+ session = null;
37
+ sessionInit = null;
38
+ ready = false;
39
+ intentionalClose = false;
40
+ inbound = new InboundTranscoder(16e3);
41
+ outbound = new OutboundTranscoder(24e3);
42
+ turnCounter = 0;
43
+ currentTurnId = null;
44
+ userTranscriptBuffer = "";
45
+ agentTranscriptBuffer = "";
46
+ resumptionHandle = null;
47
+ resumed = false;
48
+ constructor(config, logger = noopLogger) {
49
+ super();
50
+ this.config = config;
51
+ this.logger = logger;
52
+ }
53
+ get isConnected() {
54
+ return this.ready && this.session !== null;
55
+ }
56
+ /** True when the last connect restored server-side context via a handle. */
57
+ get didResume() {
58
+ return this.resumed;
59
+ }
60
+ async connect(init) {
61
+ if (this.session) await this.close();
62
+ this.sessionInit = init;
63
+ this.intentionalClose = false;
64
+ this.ready = false;
65
+ this.resumed = false;
66
+ this.inbound.reset();
67
+ this.outbound.reset();
68
+ const connector = this.config.connector ?? await this.defaultConnector();
69
+ const handle = init.freshSession ? void 0 : init.resumptionHandle ?? this.resumptionHandle ?? void 0;
70
+ if (init.freshSession) this.resumptionHandle = null;
71
+ let setupResolve;
72
+ let rejectSetup;
73
+ const setupDone = new Promise((resolve, reject) => {
74
+ setupResolve = resolve;
75
+ rejectSetup = reject;
76
+ });
77
+ let setupError = null;
78
+ setupDone.catch(() => {});
79
+ const setupReject = (error) => {
80
+ setupError ??= error;
81
+ rejectSetup(error);
82
+ };
83
+ const timeoutMs = this.config.connectTimeoutMs ?? 1e4;
84
+ const timer = setTimeout(() => setupReject(/* @__PURE__ */ new Error(`gemini setup not complete within ${timeoutMs}ms`)), timeoutMs);
85
+ timer.unref?.();
86
+ try {
87
+ try {
88
+ this.session = await connector({
89
+ model: this.config.model,
90
+ config: this.buildConfig(init, handle),
91
+ callbacks: {
92
+ onopen: () => this.emit("open"),
93
+ onmessage: (message) => {
94
+ if (message?.setupComplete !== void 0) {
95
+ this.ready = true;
96
+ if (handle) this.resumed = true;
97
+ setupResolve();
98
+ return;
99
+ }
100
+ this.handleMessage(message);
101
+ },
102
+ onerror: (error) => {
103
+ const err = error instanceof Error ? error : new Error(String(error?.message ?? error));
104
+ if (!this.ready) setupReject(err);
105
+ else this.emit("error", err);
106
+ },
107
+ onclose: (event) => {
108
+ const wasReady = this.ready;
109
+ this.ready = false;
110
+ this.session = null;
111
+ if (!wasReady) {
112
+ setupReject(/* @__PURE__ */ new Error(`gemini closed during setup (${event?.code} ${event?.reason ?? ""})`));
113
+ return;
114
+ }
115
+ this.emit("close", {
116
+ code: event?.code,
117
+ reason: event?.reason,
118
+ retriable: !this.intentionalClose && !isNonRetriableClose(event?.code)
119
+ });
120
+ }
121
+ }
122
+ });
123
+ } catch (error) {
124
+ throw setupError ?? (error instanceof Error ? error : new Error(String(error)));
125
+ }
126
+ await setupDone;
127
+ } finally {
128
+ clearTimeout(timer);
129
+ }
130
+ }
131
+ async defaultConnector() {
132
+ let genai;
133
+ try {
134
+ genai = await import("@google/genai");
135
+ } catch (error) {
136
+ throw new Error("the '@google/genai' package is required for the Gemini provider — npm install @google/genai", { cause: error });
137
+ }
138
+ const { GoogleGenAI } = genai;
139
+ const ai = this.config.vertex ? new GoogleGenAI({
140
+ vertexai: true,
141
+ project: this.config.vertex.project,
142
+ location: this.config.vertex.location
143
+ }) : new GoogleGenAI({ apiKey: this.config.apiKey });
144
+ return (params) => ai.live.connect({
145
+ model: params.model,
146
+ config: params.config,
147
+ callbacks: params.callbacks
148
+ });
149
+ }
150
+ buildConfig(init, resumptionHandle) {
151
+ const transcription = this.config.transcription;
152
+ const vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
153
+ const config = {
154
+ responseModalities: ["AUDIO"],
155
+ systemInstruction: init.instructions,
156
+ speechConfig: {
157
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: init.voice ?? this.config.voice ?? "Aoede" } },
158
+ ...this.config.languageCode ? { languageCode: this.config.languageCode } : {}
159
+ },
160
+ ...init.tools?.length ? { tools: [{ functionDeclarations: init.tools.map((tool) => ({
161
+ name: tool.name,
162
+ description: tool.description ?? "",
163
+ parameters: tool.parameters
164
+ })) }] } : {},
165
+ realtimeInputConfig: { automaticActivityDetection: buildGeminiVad(vad) },
166
+ ...transcription === false ? {} : {
167
+ ...transcription?.input !== false ? { inputAudioTranscription: {} } : {},
168
+ ...transcription?.output !== false ? { outputAudioTranscription: {} } : {}
169
+ },
170
+ ...this.config.resumption !== false ? { sessionResumption: { ...resumptionHandle ? { handle: resumptionHandle } : {} } } : {},
171
+ ...this.config.contextWindowCompression ? { contextWindowCompression: { slidingWindow: {} } } : {}
172
+ };
173
+ return deepMerge(deepMerge(config, init.providerOptions), this.config.extraConfig);
174
+ }
175
+ async close() {
176
+ this.intentionalClose = true;
177
+ this.ready = false;
178
+ const session = this.session;
179
+ this.session = null;
180
+ if (!session) return;
181
+ try {
182
+ session.close();
183
+ } catch {}
184
+ }
185
+ sendAudio(base64Mulaw) {
186
+ if (!this.isConnected || !this.session) return;
187
+ const pcm = this.inbound.process(base64Mulaw);
188
+ if (pcm.length === 0) return;
189
+ this.session.sendRealtimeInput({ audio: {
190
+ data: pcm,
191
+ mimeType: GEMINI_INPUT_MIME
192
+ } });
193
+ }
194
+ sendText(text, options = {}) {
195
+ if (!this.session) return;
196
+ const role = options.role === "assistant" ? "model" : "user";
197
+ const content = options.role === "system" ? `(system note) ${text}` : text;
198
+ this.session.sendClientContent({
199
+ turns: [{
200
+ role,
201
+ parts: [{ text: content }]
202
+ }],
203
+ turnComplete: options.triggerResponse !== false
204
+ });
205
+ }
206
+ sendToolResult(callId, output, options = {}) {
207
+ if (!this.session) return;
208
+ const response = output !== null && typeof output === "object" && !Array.isArray(output) ? output : { result: output };
209
+ this.session.sendToolResponse({ functionResponses: [{
210
+ id: callId,
211
+ name: this.toolNameForCall(callId),
212
+ response
213
+ }] });
214
+ }
215
+ createResponse(options = {}) {
216
+ if (!this.session) return;
217
+ const text = options.instructions ? `(instruction) ${options.instructions}` : "(instruction) Continue the conversation now — speak to the caller.";
218
+ this.session.sendClientContent({
219
+ turns: [{
220
+ role: "user",
221
+ parts: [{ text }]
222
+ }],
223
+ turnComplete: true
224
+ });
225
+ }
226
+ async updateSession(_patch) {
227
+ this.logger.warn("gemini: updateSession ignored (no mid-session config on Live API)");
228
+ }
229
+ handleMessage(message) {
230
+ const serverContent = message?.serverContent;
231
+ if (message?.toolCall?.functionCalls) {
232
+ for (const call of message.toolCall.functionCalls) {
233
+ const id = call.id ?? `gcall_${++this.turnCounter}`;
234
+ this.callNames.set(id, call.name);
235
+ this.emit("toolCall", {
236
+ id,
237
+ name: call.name,
238
+ argumentsJson: JSON.stringify(call.args ?? {}),
239
+ responseId: this.currentTurnId ?? void 0
240
+ });
241
+ }
242
+ return;
243
+ }
244
+ if (message?.sessionResumptionUpdate) {
245
+ const update = message.sessionResumptionUpdate;
246
+ if (update.resumable && update.newHandle) {
247
+ this.resumptionHandle = update.newHandle;
248
+ this.emit("resumptionUpdate", update.newHandle);
249
+ }
250
+ return;
251
+ }
252
+ if (message?.goAway) {
253
+ const timeLeftMs = parseGoAwayTime(message.goAway.timeLeft);
254
+ this.logger.info("gemini goAway received", { timeLeftMs });
255
+ this.emit("goAway", { timeLeftMs });
256
+ return;
257
+ }
258
+ if (message?.usageMetadata) {
259
+ const usage = message.usageMetadata;
260
+ this.emit("usage", {
261
+ inputTokens: usage.promptTokenCount ?? 0,
262
+ outputTokens: usage.responseTokenCount ?? usage.candidatesTokenCount ?? 0,
263
+ totalTokens: usage.totalTokenCount ?? 0,
264
+ raw: usage
265
+ });
266
+ }
267
+ if (!serverContent) return;
268
+ if (serverContent.interrupted) {
269
+ const turnId = this.currentTurnId;
270
+ this.emit("userSpeechStarted");
271
+ if (turnId) {
272
+ this.flushAgentTranscript(turnId);
273
+ this.currentTurnId = null;
274
+ this.emit("responseDone", { responseId: turnId });
275
+ }
276
+ return;
277
+ }
278
+ if (serverContent.inputTranscription?.text) this.userTranscriptBuffer += serverContent.inputTranscription.text;
279
+ if (serverContent.outputTranscription?.text) {
280
+ const turnId = this.ensureTurn();
281
+ this.agentTranscriptBuffer += serverContent.outputTranscription.text;
282
+ this.emit("agentTranscriptDelta", {
283
+ responseId: turnId,
284
+ delta: serverContent.outputTranscription.text
285
+ });
286
+ }
287
+ const parts = serverContent.modelTurn?.parts ?? [];
288
+ for (const part of parts) if (part.inlineData?.data) {
289
+ const turnId = this.ensureTurn();
290
+ const mulaw = this.outbound.process(part.inlineData.data);
291
+ if (mulaw.length > 0) this.emit("audio", {
292
+ base64Mulaw: mulaw,
293
+ responseId: turnId
294
+ });
295
+ }
296
+ if (serverContent.turnComplete) {
297
+ this.flushUserTranscript();
298
+ const turnId = this.currentTurnId;
299
+ if (turnId) {
300
+ this.flushAgentTranscript(turnId);
301
+ this.currentTurnId = null;
302
+ this.emit("responseDone", { responseId: turnId });
303
+ }
304
+ }
305
+ }
306
+ callNames = /* @__PURE__ */ new Map();
307
+ toolNameForCall(callId) {
308
+ const name = this.callNames.get(callId) ?? callId;
309
+ this.callNames.delete(callId);
310
+ return name;
311
+ }
312
+ ensureTurn() {
313
+ if (!this.currentTurnId) {
314
+ this.currentTurnId = `gturn_${++this.turnCounter}`;
315
+ this.flushUserTranscript();
316
+ this.emit("responseStarted", { responseId: this.currentTurnId });
317
+ }
318
+ return this.currentTurnId;
319
+ }
320
+ flushUserTranscript() {
321
+ const text = this.userTranscriptBuffer.trim();
322
+ this.userTranscriptBuffer = "";
323
+ if (text) this.emit("userTranscript", { text });
324
+ }
325
+ flushAgentTranscript(turnId) {
326
+ const text = this.agentTranscriptBuffer.trim();
327
+ this.agentTranscriptBuffer = "";
328
+ if (text) this.emit("agentTranscript", {
329
+ responseId: turnId,
330
+ text
331
+ });
332
+ }
333
+ };
334
+ /** Protocol/auth/policy closes where retrying cannot help. */
335
+ function isNonRetriableClose(code) {
336
+ if (code === void 0) return false;
337
+ return code === 1002 || code === 1003 || code === 1007 || code === 1008 || code >= 4400 && code < 4500;
338
+ }
339
+ function buildGeminiVad(vad) {
340
+ if (vad === null) return { disabled: true };
341
+ return {
342
+ disabled: false,
343
+ ...vad?.startSensitivity ? { startOfSpeechSensitivity: `START_SENSITIVITY_${vad.startSensitivity.toUpperCase()}` } : {},
344
+ ...vad?.endSensitivity ? { endOfSpeechSensitivity: `END_SENSITIVITY_${vad.endSensitivity.toUpperCase()}` } : {},
345
+ ...vad?.prefixPaddingMs !== void 0 ? { prefixPaddingMs: vad.prefixPaddingMs } : {},
346
+ ...vad?.silenceDurationMs !== void 0 ? { silenceDurationMs: vad.silenceDurationMs } : {}
347
+ };
348
+ }
349
+ function parseGoAwayTime(timeLeft) {
350
+ if (typeof timeLeft === "number") return timeLeft;
351
+ if (typeof timeLeft === "string") {
352
+ const seconds = Number.parseFloat(timeLeft.replace(/s$/, ""));
353
+ if (!Number.isNaN(seconds)) return Math.round(seconds * 1e3);
354
+ }
355
+ }
356
+ //#endregion
357
+ //#region src/gemini.ts
358
+ /** Google Gemini Live provider — `realtime-voice-agents/gemini`. */
359
+ /** Env vars checked (in order) when no explicit apiKey is passed. */
360
+ const GEMINI_KEY_ENV_VARS = [
361
+ "GOOGLE_API_KEY",
362
+ "GEMINI_API_KEY",
363
+ "GOOGLE_GENAI_API_KEY"
364
+ ];
365
+ const GEMINI_DEFAULT_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025";
366
+ const GEMINI_DEFAULT_VOICE = "Aoede";
367
+ const GEMINI_VOICES = [
368
+ "Aoede",
369
+ "Charon",
370
+ "Fenrir",
371
+ "Kore",
372
+ "Puck"
373
+ ];
374
+ /** Create a Gemini Live provider factory for the bridge. */
375
+ function geminiLive(options = {}) {
376
+ const apiKey = resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
377
+ if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
378
+ const config = {
379
+ apiKey,
380
+ vertex: options.vertex,
381
+ model: options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025",
382
+ voice: options.voice ?? "Aoede",
383
+ languageCode: options.languageCode,
384
+ defaultVad: options.vad,
385
+ transcription: options.transcription,
386
+ resumption: options.resumption,
387
+ contextWindowCompression: options.contextWindowCompression,
388
+ extraConfig: options.config,
389
+ connectTimeoutMs: options.connectTimeoutMs,
390
+ connector: options.connector
391
+ };
392
+ return ({ logger }) => new GeminiLiveProvider(config, logger);
393
+ }
394
+ //#endregion
395
+ export { GEMINI_DEFAULT_MODEL, GEMINI_DEFAULT_VOICE, GEMINI_KEY_ENV_VARS, GEMINI_VOICES, GeminiLiveProvider, geminiLive };