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.
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/assets/elevator-jazz.ulaw +1 -0
- package/assets/keyboard-typing.ulaw +1 -0
- package/assets/lofi.ulaw +1 -0
- package/assets/ringing.ulaw +1 -0
- package/assets/thinking-hum.ulaw +1 -0
- package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
- package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
- package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
- package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
- package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
- package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
- package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
- package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
- package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
- package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
- package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
- package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
- package/dist/audio.cjs +21 -0
- package/dist/audio.d.cts +165 -0
- package/dist/audio.d.mts +165 -0
- package/dist/audio.mjs +4 -0
- package/dist/env-DSnGaERV.cjs +19 -0
- package/dist/env-DUwUWTsg.mjs +14 -0
- package/dist/events-BUMYdETO.d.cts +24 -0
- package/dist/events-BUMYdETO.d.mts +24 -0
- package/dist/events-BxDTIKKq.cjs +48 -0
- package/dist/events-BylBSBW-.mjs +43 -0
- package/dist/gemini.cjs +401 -0
- package/dist/gemini.d.cts +41 -0
- package/dist/gemini.d.mts +41 -0
- package/dist/gemini.mjs +395 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.d.cts +920 -0
- package/dist/index.d.mts +920 -0
- package/dist/index.mjs +2113 -0
- package/dist/mulaw--cwU2c9L.mjs +64 -0
- package/dist/mulaw-DLUObjdP.cjs +117 -0
- package/dist/openai.cjs +61 -0
- package/dist/openai.d.cts +121 -0
- package/dist/openai.d.mts +121 -0
- package/dist/openai.mjs +53 -0
- package/dist/presets-Bf75YXs5.d.cts +24 -0
- package/dist/presets-Bf75YXs5.d.mts +24 -0
- package/dist/rest-BYqiVOhe.mjs +265 -0
- package/dist/rest-BvUKut_k.cjs +300 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/session-config-BVLl7-ha.mjs +74 -0
- package/dist/session-config-c8sOw1XL.cjs +85 -0
- package/dist/store.cjs +3 -0
- package/dist/store.d.cts +2 -0
- package/dist/store.d.mts +2 -0
- package/dist/store.mjs +2 -0
- package/dist/testing.cjs +544 -0
- package/dist/testing.d.cts +191 -0
- package/dist/testing.d.mts +191 -0
- package/dist/testing.mjs +538 -0
- package/dist/transcode-C9aJG7_W.cjs +215 -0
- package/dist/transcode-CsAp97G9.mjs +198 -0
- package/dist/transport-B_PJFIVd.d.cts +155 -0
- package/dist/transport-CEaLFV4E.d.mts +155 -0
- package/dist/twilio.cjs +8 -0
- package/dist/twilio.d.cts +3 -0
- package/dist/twilio.d.mts +3 -0
- package/dist/twilio.mjs +2 -0
- package/dist/twiml-z9LjoF4_.d.cts +67 -0
- package/dist/twiml-z9LjoF4_.d.mts +67 -0
- package/dist/xai.cjs +92 -0
- package/dist/xai.d.cts +35 -0
- package/dist/xai.d.mts +35 -0
- package/dist/xai.mjs +86 -0
- package/package.json +183 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
+
const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-DI4pKtOb.cjs");
|
|
3
|
+
const require_session_config = require("./session-config-c8sOw1XL.cjs");
|
|
4
|
+
let ws = require("ws");
|
|
5
|
+
ws = require_rolldown_runtime.__toESM(ws, 1);
|
|
6
|
+
//#region src/providers/openai-compatible/OpenAICompatibleProvider.ts
|
|
7
|
+
/**
|
|
8
|
+
* Provider for the OpenAI Realtime API (GA protocol) and API-compatible
|
|
9
|
+
* services (xAI Grok Voice Agent). Speaks `audio/pcmu` in both directions, so
|
|
10
|
+
* Twilio payloads pass through verbatim — zero transcoding on this path.
|
|
11
|
+
*
|
|
12
|
+
* Connect handshake: open WS → receive `session.created` → send
|
|
13
|
+
* `session.update` (audio/pcmu, VAD, tools, instructions) → resolve on
|
|
14
|
+
* `session.updated`. A handshake that hangs rejects after `connectTimeoutMs`
|
|
15
|
+
* so a dead upstream can't leak half-open calls.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_BASE_URL = "wss://api.openai.com/v1/realtime";
|
|
18
|
+
/** WS close codes where retrying cannot help (protocol/auth/policy failures). */
|
|
19
|
+
const NON_RETRIABLE_CLOSE_CODES = /* @__PURE__ */ new Set([
|
|
20
|
+
1002,
|
|
21
|
+
1003,
|
|
22
|
+
1007,
|
|
23
|
+
1008
|
|
24
|
+
]);
|
|
25
|
+
var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRealtimeProvider {
|
|
26
|
+
name;
|
|
27
|
+
capabilities;
|
|
28
|
+
config;
|
|
29
|
+
logger;
|
|
30
|
+
ws = null;
|
|
31
|
+
sessionInit = null;
|
|
32
|
+
ready = false;
|
|
33
|
+
intentionalClose = false;
|
|
34
|
+
currentResponseId = null;
|
|
35
|
+
/**
|
|
36
|
+
* response.create serialization. The GA API rejects a `response.create`
|
|
37
|
+
* issued while another response is in flight
|
|
38
|
+
* (`conversation_already_has_active_response`), so creates requested
|
|
39
|
+
* mid-response wait in a single pending slot and fire on `response.done`.
|
|
40
|
+
* Later requests coalesce into that slot — one response reads the whole
|
|
41
|
+
* conversation state, so only the instructions payload is worth keeping.
|
|
42
|
+
*/
|
|
43
|
+
responseActive = false;
|
|
44
|
+
pendingCreate = null;
|
|
45
|
+
lastCreateSent = null;
|
|
46
|
+
constructor(config, logger = require_BaseRealtimeProvider.noopLogger) {
|
|
47
|
+
super();
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.logger = logger;
|
|
50
|
+
this.name = config.providerName ?? "openai";
|
|
51
|
+
this.capabilities = {
|
|
52
|
+
truncate: true,
|
|
53
|
+
sessionUpdate: true,
|
|
54
|
+
voiceChangeMidSession: false,
|
|
55
|
+
transcodeRequired: false,
|
|
56
|
+
resumption: false,
|
|
57
|
+
agentTranscriptDeltas: true,
|
|
58
|
+
vadInterruptControl: true,
|
|
59
|
+
...config.capabilityOverrides
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
get isConnected() {
|
|
63
|
+
return this.ready && this.ws?.readyState === ws.default.OPEN;
|
|
64
|
+
}
|
|
65
|
+
async connect(init) {
|
|
66
|
+
if (this.ws) await this.close();
|
|
67
|
+
let vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
|
|
68
|
+
if (init.bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && vad !== null) vad = {
|
|
69
|
+
interruptResponse: false,
|
|
70
|
+
...vad ?? { type: "server" }
|
|
71
|
+
};
|
|
72
|
+
this.sessionInit = {
|
|
73
|
+
...init,
|
|
74
|
+
vad,
|
|
75
|
+
transcription: init.transcription !== void 0 ? init.transcription : this.config.defaultTranscription
|
|
76
|
+
};
|
|
77
|
+
this.intentionalClose = false;
|
|
78
|
+
this.ready = false;
|
|
79
|
+
this.responseActive = false;
|
|
80
|
+
this.pendingCreate = null;
|
|
81
|
+
this.lastCreateSent = null;
|
|
82
|
+
this.currentResponseId = null;
|
|
83
|
+
const url = `${this.config.baseUrl ?? DEFAULT_BASE_URL}?model=${encodeURIComponent(this.config.model)}`;
|
|
84
|
+
const ws$1 = new ws.default(url, {
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
87
|
+
...this.config.headers
|
|
88
|
+
},
|
|
89
|
+
perMessageDeflate: false
|
|
90
|
+
});
|
|
91
|
+
this.ws = ws$1;
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const timeoutMs = this.config.connectTimeoutMs ?? 1e4;
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
fail(/* @__PURE__ */ new Error(`${this.name} session not ready within ${timeoutMs}ms`));
|
|
96
|
+
}, timeoutMs);
|
|
97
|
+
timer.unref?.();
|
|
98
|
+
let settled = false;
|
|
99
|
+
const fail = (error) => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
try {
|
|
104
|
+
ws$1.close();
|
|
105
|
+
} catch {}
|
|
106
|
+
reject(error);
|
|
107
|
+
};
|
|
108
|
+
ws$1.on("unexpected-response", (_request, response) => {
|
|
109
|
+
let body = "";
|
|
110
|
+
response.setEncoding("utf8");
|
|
111
|
+
response.on("data", (chunk) => {
|
|
112
|
+
if (body.length < 512) body += chunk;
|
|
113
|
+
});
|
|
114
|
+
response.on("end", () => {
|
|
115
|
+
const detail = body.trim().slice(0, 500);
|
|
116
|
+
this.logger.error("provider rejected the WebSocket upgrade", {
|
|
117
|
+
status: response.statusCode,
|
|
118
|
+
body: detail
|
|
119
|
+
});
|
|
120
|
+
fail(/* @__PURE__ */ new Error(`${this.name} rejected the WebSocket upgrade: HTTP ${response.statusCode}${detail ? ` — ${detail}` : ""}`));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
ws$1.on("open", () => this.emit("open"));
|
|
124
|
+
ws$1.on("message", (raw) => {
|
|
125
|
+
const event = this.parseEvent(raw);
|
|
126
|
+
if (!event) return;
|
|
127
|
+
if (!settled) {
|
|
128
|
+
if (event.type === "session.created") {
|
|
129
|
+
this.send(this.buildSessionPayload());
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "session.updated") {
|
|
133
|
+
settled = true;
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
this.ready = true;
|
|
136
|
+
resolve();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (event.type === "error") {
|
|
140
|
+
fail(/* @__PURE__ */ new Error(`${this.name} session setup error: ${JSON.stringify(event.error ?? event)}`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.handleEvent(event);
|
|
145
|
+
});
|
|
146
|
+
ws$1.on("error", (error) => {
|
|
147
|
+
if (!settled) fail(error instanceof Error ? error : new Error(String(error)));
|
|
148
|
+
else this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
149
|
+
});
|
|
150
|
+
ws$1.on("close", (code, reasonBuf) => {
|
|
151
|
+
const reason = reasonBuf?.toString();
|
|
152
|
+
this.ready = false;
|
|
153
|
+
if (!settled) {
|
|
154
|
+
fail(/* @__PURE__ */ new Error(`${this.name} socket closed during setup (${code} ${reason ?? ""})`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.emit("close", {
|
|
158
|
+
code,
|
|
159
|
+
reason,
|
|
160
|
+
retriable: !this.intentionalClose && !NON_RETRIABLE_CLOSE_CODES.has(code)
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async close() {
|
|
166
|
+
this.intentionalClose = true;
|
|
167
|
+
this.ready = false;
|
|
168
|
+
const ws$2 = this.ws;
|
|
169
|
+
this.ws = null;
|
|
170
|
+
if (!ws$2) return;
|
|
171
|
+
if (ws$2.readyState === ws.default.CLOSED) return;
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
ws$2.terminate();
|
|
175
|
+
resolve();
|
|
176
|
+
}, 1e3);
|
|
177
|
+
timer.unref?.();
|
|
178
|
+
ws$2.once("close", () => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve();
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
ws$2.close(1e3);
|
|
184
|
+
} catch {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
ws$2.terminate();
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
sendAudio(base64Mulaw) {
|
|
192
|
+
if (!this.isConnected) return;
|
|
193
|
+
this.send({
|
|
194
|
+
type: "input_audio_buffer.append",
|
|
195
|
+
audio: base64Mulaw
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
sendText(text, options = {}) {
|
|
199
|
+
const role = options.role ?? "user";
|
|
200
|
+
this.send({
|
|
201
|
+
type: "conversation.item.create",
|
|
202
|
+
item: {
|
|
203
|
+
type: "message",
|
|
204
|
+
role,
|
|
205
|
+
content: [{
|
|
206
|
+
type: role === "assistant" ? "output_text" : "input_text",
|
|
207
|
+
text
|
|
208
|
+
}]
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
if (options.triggerResponse !== false) this.createResponse();
|
|
212
|
+
}
|
|
213
|
+
sendToolResult(callId, output, options = {}) {
|
|
214
|
+
this.send({
|
|
215
|
+
type: "conversation.item.create",
|
|
216
|
+
item: {
|
|
217
|
+
type: "function_call_output",
|
|
218
|
+
call_id: callId,
|
|
219
|
+
output: typeof output === "string" ? output : JSON.stringify(output ?? null)
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
if (options.triggerResponse !== false) this.createResponse();
|
|
223
|
+
}
|
|
224
|
+
createResponse(options = {}) {
|
|
225
|
+
if (this.responseActive) {
|
|
226
|
+
this.pendingCreate = {
|
|
227
|
+
instructions: options.instructions ?? this.pendingCreate?.instructions,
|
|
228
|
+
attempts: 0
|
|
229
|
+
};
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.sendCreate({
|
|
233
|
+
instructions: options.instructions,
|
|
234
|
+
attempts: 0
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
sendCreate(create) {
|
|
238
|
+
this.lastCreateSent = create;
|
|
239
|
+
this.send({
|
|
240
|
+
type: "response.create",
|
|
241
|
+
...create.instructions ? { response: { instructions: create.instructions } } : {}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async updateSession(patch) {
|
|
245
|
+
if (!this.sessionInit) throw new Error("updateSession before connect");
|
|
246
|
+
this.sessionInit = {
|
|
247
|
+
...this.sessionInit,
|
|
248
|
+
...patch
|
|
249
|
+
};
|
|
250
|
+
this.send(this.buildSessionPayload());
|
|
251
|
+
}
|
|
252
|
+
buildSessionPayload() {
|
|
253
|
+
return (this.config.buildSession ?? require_session_config.buildSessionUpdate)(this.sessionInit, {
|
|
254
|
+
defaultVoice: this.config.voice,
|
|
255
|
+
extraSessionOptions: this.config.extraSessionOptions
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
cancelResponse() {
|
|
259
|
+
if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
|
|
260
|
+
this.send({ type: "response.cancel" });
|
|
261
|
+
}
|
|
262
|
+
truncatePlayback(itemId, audioEndMs) {
|
|
263
|
+
this.send({
|
|
264
|
+
type: "conversation.item.truncate",
|
|
265
|
+
item_id: itemId,
|
|
266
|
+
content_index: 0,
|
|
267
|
+
audio_end_ms: Math.max(0, Math.round(audioEndMs))
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
handleEvent(event) {
|
|
271
|
+
switch (event.type) {
|
|
272
|
+
case "response.created": {
|
|
273
|
+
const responseId = event.response?.id ?? `resp_${Date.now()}`;
|
|
274
|
+
this.currentResponseId = responseId;
|
|
275
|
+
this.responseActive = true;
|
|
276
|
+
this.emit("responseStarted", { responseId });
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "response.output_item.added": {
|
|
280
|
+
const itemId = event.item?.id;
|
|
281
|
+
if (itemId) this.emit("outputItemAdded", {
|
|
282
|
+
itemId,
|
|
283
|
+
responseId: event.response_id ?? this.currentResponseId ?? ""
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "response.output_audio.delta":
|
|
288
|
+
if (typeof event.delta === "string" && event.delta.length > 0) this.emit("audio", {
|
|
289
|
+
base64Mulaw: event.delta,
|
|
290
|
+
responseId: event.response_id ?? this.currentResponseId ?? "",
|
|
291
|
+
itemId: event.item_id
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
case "response.output_audio_transcript.delta":
|
|
295
|
+
if (typeof event.delta === "string") this.emit("agentTranscriptDelta", {
|
|
296
|
+
responseId: event.response_id ?? this.currentResponseId ?? "",
|
|
297
|
+
delta: event.delta
|
|
298
|
+
});
|
|
299
|
+
break;
|
|
300
|
+
case "response.output_audio_transcript.done":
|
|
301
|
+
if (typeof event.transcript === "string") this.emit("agentTranscript", {
|
|
302
|
+
responseId: event.response_id ?? this.currentResponseId ?? "",
|
|
303
|
+
text: event.transcript
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
306
|
+
case "conversation.item.input_audio_transcription.completed":
|
|
307
|
+
if (typeof event.transcript === "string" && event.transcript.trim().length > 0) this.emit("userTranscript", { text: event.transcript.trim() });
|
|
308
|
+
break;
|
|
309
|
+
case "input_audio_buffer.speech_started":
|
|
310
|
+
if (this.pendingCreate && !this.pendingCreate.instructions) this.pendingCreate = null;
|
|
311
|
+
this.emit("userSpeechStarted");
|
|
312
|
+
break;
|
|
313
|
+
case "input_audio_buffer.speech_stopped":
|
|
314
|
+
this.emit("userSpeechStopped");
|
|
315
|
+
break;
|
|
316
|
+
case "response.done": {
|
|
317
|
+
const responseId = event.response?.id ?? this.currentResponseId ?? "";
|
|
318
|
+
const usage = normalizeUsage(event.response?.usage);
|
|
319
|
+
this.responseActive = false;
|
|
320
|
+
const pending = this.pendingCreate;
|
|
321
|
+
if (pending) {
|
|
322
|
+
this.pendingCreate = null;
|
|
323
|
+
this.sendCreate(pending);
|
|
324
|
+
}
|
|
325
|
+
if (usage) this.emit("usage", usage);
|
|
326
|
+
this.emit("responseDone", {
|
|
327
|
+
responseId,
|
|
328
|
+
usage: usage ?? void 0
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "response.function_call_arguments.done":
|
|
333
|
+
this.emit("toolCall", {
|
|
334
|
+
id: event.call_id ?? event.item_id ?? `call_${Date.now()}`,
|
|
335
|
+
name: event.name,
|
|
336
|
+
argumentsJson: typeof event.arguments === "string" ? event.arguments : "{}",
|
|
337
|
+
responseId: event.response_id ?? this.currentResponseId ?? void 0,
|
|
338
|
+
itemId: event.item_id
|
|
339
|
+
});
|
|
340
|
+
break;
|
|
341
|
+
case "error": {
|
|
342
|
+
const code = event.error?.code;
|
|
343
|
+
if (code === "conversation_already_has_active_response") {
|
|
344
|
+
this.responseActive = true;
|
|
345
|
+
const last = this.lastCreateSent;
|
|
346
|
+
if (last && last.attempts < 2 && !this.pendingCreate) this.pendingCreate = {
|
|
347
|
+
...last,
|
|
348
|
+
attempts: last.attempts + 1
|
|
349
|
+
};
|
|
350
|
+
this.logger.warn("response.create raced an active response; deferred until it completes", { error: event.error });
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
{
|
|
354
|
+
const message = typeof event.error?.message === "string" ? event.error.message : "";
|
|
355
|
+
if (code === "response_cancel_not_active" || /cancellation failed: no active response/i.test(message)) {
|
|
356
|
+
this.logger.warn("response.cancel raced completion (ignored)", { error: event.error });
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.logger.warn("provider error event", { error: event.error });
|
|
361
|
+
this.emit("error", /* @__PURE__ */ new Error(`${this.name} error: ${JSON.stringify(event.error ?? event)}`));
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
parseEvent(raw) {
|
|
367
|
+
try {
|
|
368
|
+
return JSON.parse(raw.toString());
|
|
369
|
+
} catch {
|
|
370
|
+
this.logger.warn("unparseable provider frame");
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
send(payload) {
|
|
375
|
+
if (this.ws?.readyState !== ws.default.OPEN) return;
|
|
376
|
+
try {
|
|
377
|
+
this.ws.send(JSON.stringify(payload));
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function normalizeUsage(raw) {
|
|
384
|
+
if (!raw || typeof raw !== "object") return null;
|
|
385
|
+
const inputDetails = raw.input_token_details ?? raw.input_tokens_details;
|
|
386
|
+
const outputDetails = raw.output_token_details ?? raw.output_tokens_details;
|
|
387
|
+
return {
|
|
388
|
+
inputTokens: raw.input_tokens ?? 0,
|
|
389
|
+
outputTokens: raw.output_tokens ?? 0,
|
|
390
|
+
totalTokens: raw.total_tokens ?? (raw.input_tokens ?? 0) + (raw.output_tokens ?? 0),
|
|
391
|
+
...inputDetails ? { inputTokenDetails: {
|
|
392
|
+
textTokens: inputDetails.text_tokens,
|
|
393
|
+
audioTokens: inputDetails.audio_tokens,
|
|
394
|
+
cachedTokens: inputDetails.cached_tokens
|
|
395
|
+
} } : {},
|
|
396
|
+
...outputDetails ? { outputTokenDetails: {
|
|
397
|
+
textTokens: outputDetails.text_tokens,
|
|
398
|
+
audioTokens: outputDetails.audio_tokens
|
|
399
|
+
} } : {},
|
|
400
|
+
raw
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
Object.defineProperty(exports, "OpenAICompatibleProvider", {
|
|
405
|
+
enumerable: true,
|
|
406
|
+
get: function() {
|
|
407
|
+
return OpenAICompatibleProvider;
|
|
408
|
+
}
|
|
409
|
+
});
|
package/dist/audio.cjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_mulaw = require("./mulaw-DLUObjdP.cjs");
|
|
3
|
+
const require_BackgroundAudioPlayer = require("./BackgroundAudioPlayer-jfRULWKC.cjs");
|
|
4
|
+
const require_transcode = require("./transcode-C9aJG7_W.cjs");
|
|
5
|
+
exports.BackgroundAudioPlayer = require_BackgroundAudioPlayer.BackgroundAudioPlayer;
|
|
6
|
+
exports.InboundTranscoder = require_transcode.InboundTranscoder;
|
|
7
|
+
exports.MULAW_FRAME_BYTES_20MS = require_mulaw.MULAW_FRAME_BYTES_20MS;
|
|
8
|
+
exports.MULAW_SAMPLE_RATE = require_mulaw.MULAW_SAMPLE_RATE;
|
|
9
|
+
exports.MULAW_SILENCE_BYTE = require_mulaw.MULAW_SILENCE_BYTE;
|
|
10
|
+
exports.OutboundTranscoder = require_transcode.OutboundTranscoder;
|
|
11
|
+
exports.Resampler = require_transcode.Resampler;
|
|
12
|
+
exports.base64ByteLength = require_mulaw.base64ByteLength;
|
|
13
|
+
exports.fadeMulaw = require_BackgroundAudioPlayer.fadeMulaw;
|
|
14
|
+
exports.isPresetName = require_BackgroundAudioPlayer.isPresetName;
|
|
15
|
+
exports.loadBackgroundAudio = require_BackgroundAudioPlayer.loadBackgroundAudio;
|
|
16
|
+
exports.mulawBytesToMs = require_mulaw.mulawBytesToMs;
|
|
17
|
+
exports.mulawDecodeSample = require_mulaw.mulawDecodeSample;
|
|
18
|
+
exports.mulawEncodeSample = require_mulaw.mulawEncodeSample;
|
|
19
|
+
exports.mulawToPcm16 = require_mulaw.mulawToPcm16;
|
|
20
|
+
exports.pcm16ToMulaw = require_mulaw.pcm16ToMulaw;
|
|
21
|
+
exports.scaleMulaw = require_BackgroundAudioPlayer.scaleMulaw;
|
package/dist/audio.d.cts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { n as BackgroundAudioPreset, r as BackgroundAudioSpec, t as BackgroundAudioOptions } from "./presets-Bf75YXs5.cjs";
|
|
2
|
+
//#region src/audio/mulaw.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* G.711 μ-law codec (the format Twilio Media Streams speak: 8 kHz, 1 byte/sample).
|
|
5
|
+
*
|
|
6
|
+
* Decode uses a 256-entry lookup table; encode uses the canonical Sun g711.c
|
|
7
|
+
* algorithm (bias 0x84, clip 32635). μ-law silence is 0xFF.
|
|
8
|
+
*/
|
|
9
|
+
/** One byte of μ-law silence (encodes linear 0). */
|
|
10
|
+
declare const MULAW_SILENCE_BYTE = 255;
|
|
11
|
+
/** Samples per second on the Twilio media stream. μ-law is 1 byte per sample. */
|
|
12
|
+
declare const MULAW_SAMPLE_RATE = 8000;
|
|
13
|
+
/** Bytes per 20 ms Twilio media frame at 8 kHz μ-law. */
|
|
14
|
+
declare const MULAW_FRAME_BYTES_20MS = 160;
|
|
15
|
+
/** Decode a single μ-law byte to a linear PCM16 sample. */
|
|
16
|
+
declare function mulawDecodeSample(byte: number): number;
|
|
17
|
+
/** Encode a single linear PCM16 sample to a μ-law byte. */
|
|
18
|
+
declare function mulawEncodeSample(sample: number): number;
|
|
19
|
+
/** Decode a μ-law buffer to linear PCM16 samples. */
|
|
20
|
+
declare function mulawToPcm16(mulaw: Uint8Array): Int16Array;
|
|
21
|
+
/** Encode linear PCM16 samples to a μ-law buffer. */
|
|
22
|
+
declare function pcm16ToMulaw(pcm: Int16Array): Uint8Array;
|
|
23
|
+
/** Duration in milliseconds of a μ-law byte count at 8 kHz (1 byte = 125 µs). */
|
|
24
|
+
declare function mulawBytesToMs(bytes: number): number;
|
|
25
|
+
/** Byte length of a base64 string without decoding it. */
|
|
26
|
+
declare function base64ByteLength(base64: string): number;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/audio/resampler.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* Stateful polyphase FIR sample-rate converter.
|
|
31
|
+
*
|
|
32
|
+
* Built for streaming telephony audio: the instance carries the FIR history
|
|
33
|
+
* across process() calls, so chunk boundaries produce bit-identical output to
|
|
34
|
+
* single-pass processing — no per-chunk zero-padding, no boundary clicks.
|
|
35
|
+
* Coefficients are computed once per (from, to, quality) in a module cache.
|
|
36
|
+
*
|
|
37
|
+
* Rational L/M design: the prototype lowpass is a Kaiser-windowed sinc at the
|
|
38
|
+
* upsampled rate (from × L), decomposed into L phases of N/L taps. Each phase
|
|
39
|
+
* is normalized to unit DC gain so constant signals pass through exactly.
|
|
40
|
+
*/
|
|
41
|
+
interface ResamplerOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Prototype filter length multiplier. The prototype has
|
|
44
|
+
* `quality × max(L, M)` taps (rounded up to a multiple of L); higher is
|
|
45
|
+
* cleaner and slower. Default 48 (≈70–80 dB alias rejection for 24k→8k).
|
|
46
|
+
*/
|
|
47
|
+
quality?: number;
|
|
48
|
+
/** Kaiser window beta. Default 8. */
|
|
49
|
+
kaiserBeta?: number;
|
|
50
|
+
/** Passband edge as a fraction of the tighter Nyquist. Default 0.9. */
|
|
51
|
+
rolloff?: number;
|
|
52
|
+
}
|
|
53
|
+
declare class Resampler {
|
|
54
|
+
readonly fromRate: number;
|
|
55
|
+
readonly toRate: number;
|
|
56
|
+
private readonly L;
|
|
57
|
+
private readonly M;
|
|
58
|
+
private readonly bank;
|
|
59
|
+
/** Trailing input samples carried between chunks (tapsPerPhase − 1 long). */
|
|
60
|
+
private hist;
|
|
61
|
+
/** Phase index of the next output sample. */
|
|
62
|
+
private phase;
|
|
63
|
+
/**
|
|
64
|
+
* Input index (relative to the start of the next [hist + chunk] buffer) that
|
|
65
|
+
* the next output sample is anchored on. Always ≥ tapsPerPhase − 1.
|
|
66
|
+
*/
|
|
67
|
+
private kRel;
|
|
68
|
+
constructor(fromRate: number, toRate: number, options?: ResamplerOptions);
|
|
69
|
+
/** True when no rate conversion is needed (input is passed through). */
|
|
70
|
+
get isPassthrough(): boolean;
|
|
71
|
+
process(input: Int16Array): Int16Array;
|
|
72
|
+
/** Drain the filter tail by pushing one phase-length of silence. */
|
|
73
|
+
flush(): Int16Array;
|
|
74
|
+
/** Clear all carried state (history, phase) for reuse on a fresh stream. */
|
|
75
|
+
reset(): void;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/audio/transcode.d.ts
|
|
79
|
+
/** Twilio → provider: base64 μ-law 8 kHz in, base64 PCM16LE at `outRate` out. */
|
|
80
|
+
declare class InboundTranscoder {
|
|
81
|
+
private readonly resampler;
|
|
82
|
+
constructor(outRate?: number, options?: ResamplerOptions);
|
|
83
|
+
process(base64Mulaw: string): string;
|
|
84
|
+
reset(): void;
|
|
85
|
+
}
|
|
86
|
+
/** Provider → Twilio: base64 PCM16LE at `inRate` in, base64 μ-law 8 kHz out. */
|
|
87
|
+
declare class OutboundTranscoder {
|
|
88
|
+
private readonly resampler;
|
|
89
|
+
/** Dangling byte from a chunk that split a 16-bit sample. */
|
|
90
|
+
private carry;
|
|
91
|
+
constructor(inRate?: number, options?: ResamplerOptions);
|
|
92
|
+
process(base64Pcm16: string): string;
|
|
93
|
+
reset(): void;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/audio/gain.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* Gain application on μ-law frames, used for background-audio fades and volume.
|
|
99
|
+
* μ-law is logarithmic, so gain must ramp in the linear domain:
|
|
100
|
+
* decode → scale → re-encode.
|
|
101
|
+
*/
|
|
102
|
+
/** Scale an entire μ-law frame by a constant linear gain. */
|
|
103
|
+
declare function scaleMulaw(frame: Uint8Array, gain: number): Uint8Array;
|
|
104
|
+
/**
|
|
105
|
+
* Apply a linearly interpolated gain ramp across a μ-law frame
|
|
106
|
+
* (`startGain` at the first sample → `endGain` at the last).
|
|
107
|
+
*/
|
|
108
|
+
declare function fadeMulaw(frame: Uint8Array, startGain: number, endGain: number): Uint8Array;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/audio/background/BackgroundAudioPlayer.d.ts
|
|
111
|
+
interface BackgroundAudioPlayerDeps {
|
|
112
|
+
sendMedia: (base64Payload: string) => void;
|
|
113
|
+
onStarted?: (info: {
|
|
114
|
+
preset?: string;
|
|
115
|
+
}) => void;
|
|
116
|
+
onStopped?: (info: {
|
|
117
|
+
preset?: string;
|
|
118
|
+
}) => void;
|
|
119
|
+
now?: () => number;
|
|
120
|
+
}
|
|
121
|
+
declare class BackgroundAudioPlayer {
|
|
122
|
+
private readonly deps;
|
|
123
|
+
private readonly now;
|
|
124
|
+
private generation;
|
|
125
|
+
private active;
|
|
126
|
+
private startDelayTimer;
|
|
127
|
+
/** Tool-call ids currently holding the loop. */
|
|
128
|
+
private readonly holders;
|
|
129
|
+
private pendingSpec;
|
|
130
|
+
constructor(deps: BackgroundAudioPlayerDeps);
|
|
131
|
+
get isPlaying(): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Refcounted acquisition (one holder per tool call). The loop starts after
|
|
134
|
+
* `startDelayMs` unless every holder releases first.
|
|
135
|
+
*/
|
|
136
|
+
acquire(holderId: string, spec: BackgroundAudioSpec, options?: BackgroundAudioOptions): void;
|
|
137
|
+
/** Release one holder; the loop stops when the last holder releases. */
|
|
138
|
+
release(holderId: string, opts?: {
|
|
139
|
+
immediate?: boolean;
|
|
140
|
+
}): void;
|
|
141
|
+
/** Direct start (facade `playBackgroundAudio`). Bypasses refcounting. */
|
|
142
|
+
start(spec: BackgroundAudioSpec, options?: BackgroundAudioOptions): void;
|
|
143
|
+
/**
|
|
144
|
+
* Stop the loop. Fade-out is applied when `immediate` is false and a
|
|
145
|
+
* fadeOutMs was configured; agent-audio preemption should pass
|
|
146
|
+
* `immediate: true` (a fade would delay real speech behind it).
|
|
147
|
+
*/
|
|
148
|
+
stop(opts?: {
|
|
149
|
+
immediate?: boolean;
|
|
150
|
+
}): void;
|
|
151
|
+
/** Real agent audio arrived — kill the loop instantly, no fade, no clear. */
|
|
152
|
+
notifyAgentAudio(): void;
|
|
153
|
+
private cancelDelay;
|
|
154
|
+
private startNow;
|
|
155
|
+
private tick;
|
|
156
|
+
/** Wrap-around frame extraction for seamless looping. */
|
|
157
|
+
private extractFrame;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/audio/background/loader.d.ts
|
|
161
|
+
declare function isPresetName(spec: BackgroundAudioSpec): spec is BackgroundAudioPreset;
|
|
162
|
+
/** Resolve a spec to raw μ-law bytes (cached per preset/path). */
|
|
163
|
+
declare function loadBackgroundAudio(spec: BackgroundAudioSpec): Buffer;
|
|
164
|
+
//#endregion
|
|
165
|
+
export { type BackgroundAudioOptions, BackgroundAudioPlayer, type BackgroundAudioPlayerDeps, type BackgroundAudioPreset, type BackgroundAudioSpec, InboundTranscoder, MULAW_FRAME_BYTES_20MS, MULAW_SAMPLE_RATE, MULAW_SILENCE_BYTE, OutboundTranscoder, Resampler, type ResamplerOptions, base64ByteLength, fadeMulaw, isPresetName, loadBackgroundAudio, mulawBytesToMs, mulawDecodeSample, mulawEncodeSample, mulawToPcm16, pcm16ToMulaw, scaleMulaw };
|