dvgateway-adapters 1.9.7 → 1.9.12

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 (53) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/llm/anthropic.d.ts +32 -15
  6. package/dist/llm/anthropic.d.ts.map +1 -1
  7. package/dist/llm/anthropic.js +56 -14
  8. package/dist/llm/anthropic.js.map +1 -1
  9. package/dist/llm/index.d.ts +1 -1
  10. package/dist/llm/index.d.ts.map +1 -1
  11. package/dist/llm/index.js +1 -1
  12. package/dist/llm/index.js.map +1 -1
  13. package/dist/llm/openai-llm.d.ts +2 -3
  14. package/dist/llm/openai-llm.d.ts.map +1 -1
  15. package/dist/llm/openai-llm.js +2 -3
  16. package/dist/llm/openai-llm.js.map +1 -1
  17. package/dist/llm/sampling.test.d.ts +2 -0
  18. package/dist/llm/sampling.test.d.ts.map +1 -0
  19. package/dist/llm/sampling.test.js +106 -0
  20. package/dist/llm/sampling.test.js.map +1 -0
  21. package/dist/realtime/gemini-live.d.ts +33 -2
  22. package/dist/realtime/gemini-live.d.ts.map +1 -1
  23. package/dist/realtime/gemini-live.js +42 -1
  24. package/dist/realtime/gemini-live.js.map +1 -1
  25. package/dist/realtime/gemini-live.test.js +33 -1
  26. package/dist/realtime/gemini-live.test.js.map +1 -1
  27. package/dist/realtime/gpt-live.d.ts +250 -0
  28. package/dist/realtime/gpt-live.d.ts.map +1 -0
  29. package/dist/realtime/gpt-live.js +628 -0
  30. package/dist/realtime/gpt-live.js.map +1 -0
  31. package/dist/realtime/gpt-live.test.d.ts +2 -0
  32. package/dist/realtime/gpt-live.test.d.ts.map +1 -0
  33. package/dist/realtime/gpt-live.test.js +292 -0
  34. package/dist/realtime/gpt-live.test.js.map +1 -0
  35. package/dist/realtime/index.d.ts +4 -0
  36. package/dist/realtime/index.d.ts.map +1 -1
  37. package/dist/realtime/index.js +2 -0
  38. package/dist/realtime/index.js.map +1 -1
  39. package/dist/realtime/openai-realtime.d.ts +2 -2
  40. package/dist/realtime/openai-realtime.js +2 -2
  41. package/dist/realtime/speech-segments.d.ts +43 -0
  42. package/dist/realtime/speech-segments.d.ts.map +1 -0
  43. package/dist/realtime/speech-segments.js +90 -0
  44. package/dist/realtime/speech-segments.js.map +1 -0
  45. package/dist/realtime/speech-segments.test.d.ts +2 -0
  46. package/dist/realtime/speech-segments.test.d.ts.map +1 -0
  47. package/dist/realtime/speech-segments.test.js +71 -0
  48. package/dist/realtime/speech-segments.test.js.map +1 -0
  49. package/dist/tts/elevenlabs.d.ts +3 -3
  50. package/dist/tts/elevenlabs.js +2 -2
  51. package/dist/tts/openai-tts.d.ts +3 -3
  52. package/dist/tts/openai-tts.js +1 -1
  53. package/package.json +3 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpt-live.d.ts","sourceRoot":"","sources":["../../src/realtime/gpt-live.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,628 @@
1
+ /**
2
+ * OpenAI GPT-Live Speech-to-Speech Adapter (@since SDK 1.9.10)
3
+ *
4
+ * GPT-Live (`gpt-live-1`) is a **full-duplex** voice model: it listens while it
5
+ * speaks and decides on its own when to start and stop talking. Reasoning and
6
+ * tool use are **delegated** to a backend — either an OpenAI Responses model
7
+ * (`delegation: { type: 'responses' }`) or your own code (`{ type: 'client' }`).
8
+ *
9
+ * Wiring is the same as the other S2S adapters (`OpenAIRealtimeAdapter`,
10
+ * `GeminiLiveAdapter`): feed `gw.streamAudio(..., { pipelineType: 's2s' })` into
11
+ * `startSession()` and inject `onAudioOutput` chunks with `gw.injectTts()`.
12
+ *
13
+ * What this adapter does for you (each item was measured on the real API,
14
+ * 2026-10-11 — see docs/gpt-live-integration-plan.md §9):
15
+ *
16
+ * - **16 kHz PCM end to end.** GPT-Live accepts `audio/pcm` at 16 kHz, the same
17
+ * format as DVGateway, so there is no resampling.
18
+ * - **Speech gate.** GPT-Live streams output audio continuously, silence included
19
+ * (the output length equals the session length). Injecting it raw would keep
20
+ * the gateway's TTS player "speaking" forever and `tts:complete` would never
21
+ * fire. The adapter forwards only speech segments (RMS gate + hang time).
22
+ * - **No silent defaults.** `delegation` is required (OpenAI treats a missing
23
+ * delegation as client mode) and `store` is sent as `false` explicitly
24
+ * (`store: true` keeps a stereo recording at OpenAI for 30 days).
25
+ * - **Delegation policy prompt.** Without one, the model said "yes, that time is
26
+ * available — I'll book it" without checking in 2 of 5 runs; with the policy
27
+ * prompt it delegated 15 of 15 times and never claimed completion.
28
+ * `buildGptLiveInstructions()` produces that prompt shape.
29
+ * - **Unspoken-result detection.** In 1 of 15 runs the model received the backend
30
+ * result and never said it. `onUnspokenResult` fires when no new speech starts
31
+ * within `unspokenResultMs` after a result is sent, so you can re-send it.
32
+ *
33
+ * What it does NOT do (by design — see the plan doc):
34
+ * - No automatic reconnect (a new connection is a new billed session and a
35
+ * pending action could run twice). Errors go to `onError`; you decide.
36
+ * - No exact wording. GPT-Live paraphrases. Play legally required notices
37
+ * (e.g. "AI 상담원입니다") with `gw.say()` **before** `startSession()`.
38
+ *
39
+ * API reference: https://developers.openai.com/api/docs/guides/live
40
+ * WebSocket: wss://api.openai.com/v1/live/sessions
41
+ */
42
+ import WebSocket from 'ws';
43
+ import { float32ToSlin16 } from 'dvgateway-sdk';
44
+ // ─── Constants ───────────────────────────────────────────────────────────────
45
+ export const GPT_LIVE_URL = 'wss://api.openai.com/v1/live/sessions';
46
+ export const GPT_LIVE_MODEL = 'gpt-live-1';
47
+ const SAMPLE_RATE = 16000;
48
+ const FRAME_SAMPLES = SAMPLE_RATE / 50; // 20 ms
49
+ /** OpenAI error codes that retrying cannot fix (same set LiveKit treats as fatal). */
50
+ const FATAL_ERROR_CODES = new Set([
51
+ 'insufficient_quota',
52
+ 'invalid_api_key',
53
+ 'account_deactivated',
54
+ 'billing_hard_limit_reached',
55
+ ]);
56
+ /** Error with the OpenAI code attached when there is one. */
57
+ export class GptLiveError extends Error {
58
+ code;
59
+ fatal;
60
+ status;
61
+ constructor(message, code, fatal = false, status) {
62
+ super(message);
63
+ this.code = code;
64
+ this.fatal = fatal;
65
+ this.status = status;
66
+ this.name = 'GptLiveError';
67
+ }
68
+ }
69
+ /**
70
+ * Build conversation instructions in the shape OpenAI recommends for GPT-Live
71
+ * (Backchannel / Interruption / Delegation policy — the headings are kept in
72
+ * English as OpenAI advises) plus DVGateway's phone rules.
73
+ *
74
+ * The delegation policy is what made the model stop claiming unchecked results
75
+ * in our measurement (2/5 → 0/15).
76
+ */
77
+ export function buildGptLiveInstructions(o = {}) {
78
+ const persona = o.persona ?? '당신은 한국어 전화 상담원입니다. 차분하고 친절하게, 서두르지 않고 한국어로 말합니다.';
79
+ const caps = o.capabilities?.length ? o.capabilities : ['정보 조회: 상대가 묻는 내용에 필요한 정보를 확인합니다.'];
80
+ const when = o.delegateWhen?.length ? o.delegateWhen : ['답이 백엔드 정보에 달려 있을 때.', '이미 맡긴 작업을 상대가 고칠 때.'];
81
+ const notWhen = o.dontDelegateWhen?.length ? o.dontDelegateWhen
82
+ : ['인사이거나, 이미 받은 결과를 다시 말해 달라고 할 때.', '무엇을 원하는지 짧게 되물어야 알 수 있을 때.'];
83
+ const rules = [
84
+ '확인되지 않은 일을 가능하다거나 완료됐다고 말하지 않습니다. 결과를 받은 뒤에만 말합니다.',
85
+ '결과를 기다리는 동안에는 확인 중이라고만 말합니다.',
86
+ '묻는 말은 문장 끝에 한 번만 합니다.',
87
+ '이름·날짜·숫자가 불분명하면 그 부분만 다시 묻습니다.',
88
+ '잘 못 들었으면 "제가 잘 못 들었습니다" 라고 말합니다. 상대에게 또박또박 말하라고 하지 않습니다.',
89
+ ...(o.rules ?? []),
90
+ ];
91
+ return [
92
+ persona,
93
+ '상대가 답답해하면 짧게 공감하고 다음에 할 일로 넘어갑니다.',
94
+ '',
95
+ 'Backchannel policy: 맞장구는 가끔만 합니다. 상대의 말을 덮지 않습니다.',
96
+ '',
97
+ 'Interruption policy: 상대가 말하면 바로 멈추고 듣습니다.',
98
+ '',
99
+ 'Delegation policy:',
100
+ 'Backend tools:',
101
+ ...caps.map((c) => `- ${c}`),
102
+ '',
103
+ 'Delegate to the backend when:',
104
+ ...when.map((c) => `- ${c}`),
105
+ '',
106
+ 'Do not delegate to the backend when:',
107
+ ...notWhen.map((c) => `- ${c}`),
108
+ '',
109
+ 'Delegate before giving an answer that depends on backend work.',
110
+ 'Do not guess the result while waiting.',
111
+ '',
112
+ ...rules.map((r) => `- ${r}`),
113
+ ].join('\n');
114
+ }
115
+ // ─── Speech gate (exported for tests) ────────────────────────────────────────
116
+ /**
117
+ * Forwards only speech from a continuous output stream.
118
+ * Opens at the first frame with RMS ≥ threshold; stays open until `hangMs` of
119
+ * quieter frames have passed (so pauses inside a sentence survive).
120
+ */
121
+ export class SpeechGate {
122
+ threshold;
123
+ hangMs;
124
+ open = false;
125
+ quietMs = 0;
126
+ constructor(threshold = 0.001, hangMs = 600) {
127
+ this.threshold = threshold;
128
+ this.hangMs = hangMs;
129
+ }
130
+ get isOpen() { return this.open; }
131
+ /**
132
+ * Feed one chunk of 16 kHz PCM16 (little-endian). Returns what to forward and
133
+ * whether the gate opened/closed during this chunk.
134
+ */
135
+ push(pcm) {
136
+ const frameBytes = FRAME_SAMPLES * 2;
137
+ const keep = [];
138
+ let opened = false;
139
+ let closed = false;
140
+ for (let off = 0; off < pcm.length; off += frameBytes) {
141
+ const end = Math.min(off + frameBytes, pcm.length);
142
+ const frame = pcm.subarray(off, end - ((end - off) % 2));
143
+ if (frame.length < 2)
144
+ continue;
145
+ const loud = frameRms(frame) >= this.threshold;
146
+ const ms = (frame.length / 2) * 1000 / SAMPLE_RATE;
147
+ if (loud) {
148
+ if (!this.open) {
149
+ this.open = true;
150
+ opened = true;
151
+ }
152
+ this.quietMs = 0;
153
+ keep.push(frame);
154
+ }
155
+ else if (this.open) {
156
+ this.quietMs += ms;
157
+ if (this.quietMs >= this.hangMs) {
158
+ this.open = false;
159
+ this.quietMs = 0;
160
+ closed = true;
161
+ }
162
+ else {
163
+ keep.push(frame);
164
+ }
165
+ }
166
+ }
167
+ return { out: Buffer.concat(keep), opened, closed };
168
+ }
169
+ }
170
+ export function frameRms(pcm) {
171
+ const n = pcm.length >> 1;
172
+ if (n === 0)
173
+ return 0;
174
+ let sum = 0;
175
+ for (let i = 0; i < n; i++) {
176
+ const v = pcm.readInt16LE(i * 2) / 32768;
177
+ sum += v * v;
178
+ }
179
+ return Math.sqrt(sum / n);
180
+ }
181
+ /** Build the `session.start` event (exported for tests). */
182
+ export function buildGptLiveSessionStart(o) {
183
+ const session = {
184
+ model: GPT_LIVE_MODEL,
185
+ instructions: o.instructions ?? buildGptLiveInstructions({}),
186
+ audio: { format: { type: 'audio/pcm', rate: SAMPLE_RATE }, output: { voice: o.voice ?? 'marin' } },
187
+ store: o.store ?? false, // explicit — never rely on the server default
188
+ };
189
+ if (o.history?.length) {
190
+ session.input = o.history.slice(-128).map((m) => ({
191
+ type: 'message',
192
+ role: m.role,
193
+ content: [{ type: m.role === 'assistant' ? 'output_text' : 'input_text', text: m.text }],
194
+ }));
195
+ }
196
+ if (o.delegation.type === 'client') {
197
+ session.delegation = { type: 'client' };
198
+ }
199
+ else {
200
+ const r = o.delegation.responses;
201
+ const tools = (r.tools ?? []).map((t) => ({
202
+ type: 'function',
203
+ name: t.name,
204
+ ...(t.description && { description: t.description }),
205
+ ...(t.parameters && { parameters: t.parameters }),
206
+ }));
207
+ if (r.webSearch)
208
+ tools.push({ type: 'web_search' });
209
+ const responses = { model: r.model, parallel_tool_calls: r.parallelToolCalls ?? false };
210
+ if (r.instructions)
211
+ responses.instructions = r.instructions;
212
+ if (tools.length)
213
+ responses.tools = tools;
214
+ if (r.toolChoice)
215
+ responses.tool_choice = r.toolChoice;
216
+ if (r.reasoning)
217
+ responses.reasoning = r.reasoning;
218
+ if (r.serviceTier)
219
+ responses.service_tier = r.serviceTier;
220
+ if (r.maxOutputTokens)
221
+ responses.max_output_tokens = Math.max(16, r.maxOutputTokens);
222
+ session.delegation = { type: 'responses', responses };
223
+ }
224
+ return { type: 'session.start', event_id: 'dvg_session_start', session };
225
+ }
226
+ /** OpenAI echoes part of a wrong key in error bodies — never pass it on. */
227
+ export function redactKey(s) {
228
+ return s.replace(/sk-[A-Za-z0-9_*-]+/g, 'sk-***');
229
+ }
230
+ class Session {
231
+ ws = null;
232
+ started = false;
233
+ closing = false;
234
+ finalized = false;
235
+ usageSeconds = 0;
236
+ closeReason;
237
+ gate;
238
+ finished = [];
239
+ open = {};
240
+ pendingCalls = new Set();
241
+ delegations = new Map();
242
+ eventSeq = 0;
243
+ constructor(gate) { this.gate = gate; }
244
+ }
245
+ // ─── Adapter ─────────────────────────────────────────────────────────────────
246
+ export class GptLiveAdapter {
247
+ o;
248
+ sessions = new Map();
249
+ audioOutputHandler = null;
250
+ transcriptHandler = null;
251
+ errorHandler = null;
252
+ toolCallHandler = null;
253
+ speechActivityHandler = null;
254
+ delegationHandler = null;
255
+ usageHandler = null;
256
+ unspokenHandler = null;
257
+ constructor(opts) {
258
+ if (!opts?.apiKey)
259
+ throw new Error('GptLiveAdapter: apiKey is required');
260
+ if (!opts.delegation || (opts.delegation.type !== 'client' && opts.delegation.type !== 'responses')) {
261
+ throw new Error('GptLiveAdapter: delegation is required ({ type: "client" } or { type: "responses", responses: { model } })');
262
+ }
263
+ if (opts.delegation.type === 'responses' && !opts.delegation.responses?.model) {
264
+ throw new Error('GptLiveAdapter: delegation.responses.model is required');
265
+ }
266
+ this.o = opts;
267
+ }
268
+ onAudioOutput(h) { this.audioOutputHandler = h; }
269
+ onTranscript(h) { this.transcriptHandler = h; }
270
+ onError(h) { this.errorHandler = h; }
271
+ /** Responses delegation: a backend function call to run. Answer with submitToolResult(). */
272
+ onToolCall(h) { this.toolCallHandler = h; }
273
+ /** caller: from input transcript timing · callee: from the output speech gate (both approximate). */
274
+ onSpeechActivity(h) { this.speechActivityHandler = h; }
275
+ /** Client delegation: return `{ speak }` (said aloud, paraphrased) or `{ note }` (silent context). */
276
+ onDelegation(h) { this.delegationHandler = h; }
277
+ onUsage(h) { this.usageHandler = h; }
278
+ /** A result was sent but the model did not start speaking within `unspokenResultMs`. */
279
+ onUnspokenResult(h) { this.unspokenHandler = h; }
280
+ async startSession(linkedId, audioIn) {
281
+ if (this.sessions.has(linkedId))
282
+ throw new Error(`GPT-Live session already running for ${linkedId}`);
283
+ const g = this.o.speechGate ?? {};
284
+ const s = new Session(new SpeechGate(g.rmsThreshold ?? 0.001, g.hangMs ?? 600));
285
+ this.sessions.set(linkedId, s);
286
+ try {
287
+ const ws = await this.connect(linkedId, s);
288
+ s.ws = ws;
289
+ this.send(s, buildGptLiveSessionStart(this.o));
290
+ await this.waitStarted(linkedId, s);
291
+ void this.pipeAudio(linkedId, s, audioIn);
292
+ await new Promise((resolve) => {
293
+ if (ws.readyState === WebSocket.CLOSED)
294
+ resolve();
295
+ else
296
+ ws.once('close', () => resolve());
297
+ });
298
+ }
299
+ finally {
300
+ this.finish(linkedId, s);
301
+ }
302
+ }
303
+ /** Close gracefully: session.close → wait for session.closed (final usage) → close the socket. */
304
+ async stop(linkedId) {
305
+ const ids = linkedId ? [linkedId] : [...this.sessions.keys()];
306
+ await Promise.all(ids.map(async (id) => {
307
+ const s = this.sessions.get(id);
308
+ if (!s?.ws)
309
+ return;
310
+ if (s.ws.readyState === WebSocket.OPEN && !s.closing) {
311
+ s.closing = true;
312
+ this.send(s, { type: 'session.close', event_id: this.eid(s, 'close') });
313
+ await new Promise((resolve) => {
314
+ const t = setTimeout(resolve, this.o.closeTimeoutMs ?? 15000);
315
+ const iv = setInterval(() => {
316
+ if (s.finalized || s.ws?.readyState !== WebSocket.OPEN) {
317
+ clearTimeout(t);
318
+ clearInterval(iv);
319
+ resolve();
320
+ }
321
+ }, 20);
322
+ t.unref?.();
323
+ });
324
+ }
325
+ try {
326
+ s.ws.close(1000, 'session ended');
327
+ }
328
+ catch { /* already closed */ }
329
+ }));
330
+ }
331
+ /** Responses delegation: return a function result. Continues the backend once every pending call is answered. */
332
+ submitToolResult(linkedId, callId, result) {
333
+ const s = this.sessions.get(linkedId);
334
+ if (!s)
335
+ return;
336
+ this.send(s, {
337
+ type: 'response.item.create',
338
+ event_id: this.eid(s, 'tool'),
339
+ item: { type: 'function_call_output', call_id: callId, output: typeof result === 'string' ? result : JSON.stringify(result) },
340
+ });
341
+ s.pendingCalls.delete(callId);
342
+ if (s.pendingCalls.size === 0)
343
+ this.send(s, { type: 'response.create', event_id: this.eid(s, 'continue') });
344
+ }
345
+ /** Something to say now (paraphrased). Pass the client delegation id for a task result, else null. */
346
+ appendCommentary(linkedId, delegationId, text) {
347
+ this.append(linkedId, 'session.commentary.append', delegationId, text);
348
+ }
349
+ /** Silent context the model may use later. */
350
+ appendThinking(linkedId, delegationId, text) {
351
+ this.append(linkedId, 'session.thinking.append', delegationId, text);
352
+ }
353
+ /** Trusted instructions (can interrupt current speech). */
354
+ appendInstructions(linkedId, text) {
355
+ this.append(linkedId, 'session.instructions.append', null, text);
356
+ }
357
+ muteInput(linkedId) { const s = this.sessions.get(linkedId); if (s)
358
+ this.send(s, { type: 'session.input_audio.mute', event_id: this.eid(s, 'mute') }); }
359
+ unmuteInput(linkedId) { const s = this.sessions.get(linkedId); if (s)
360
+ this.send(s, { type: 'session.input_audio.unmute', event_id: this.eid(s, 'unmute') }); }
361
+ // ── internals ────────────────────────────────────────────────────────────
362
+ eid(s, tag) { s.eventSeq += 1; return `dvg_${tag}_${s.eventSeq}`; }
363
+ send(s, ev) {
364
+ if (s.ws && s.ws.readyState === WebSocket.OPEN)
365
+ s.ws.send(JSON.stringify(ev));
366
+ }
367
+ append(linkedId, type, delegationId, text) {
368
+ const s = this.sessions.get(linkedId);
369
+ if (!s)
370
+ return;
371
+ const eventId = this.eid(s, 'append');
372
+ this.send(s, { type, event_id: eventId, delegation_id: delegationId, content: text });
373
+ if (type === 'session.commentary.append' && delegationId)
374
+ this.watchResult(linkedId, s, delegationId, eventId);
375
+ }
376
+ connect(linkedId, s) {
377
+ return new Promise((resolve, reject) => {
378
+ const ws = new WebSocket(this.o.url ?? GPT_LIVE_URL, { headers: { Authorization: `Bearer ${this.o.apiKey}` } });
379
+ const timer = setTimeout(() => { ws.terminate(); reject(new GptLiveError('GPT-Live: connect timeout')); }, this.o.connectTimeoutMs ?? 15000);
380
+ ws.once('open', () => { clearTimeout(timer); resolve(ws); });
381
+ ws.once('unexpected-response', (_req, res) => {
382
+ clearTimeout(timer);
383
+ let body = '';
384
+ res.on('data', (d) => { if (body.length < 2000)
385
+ body += d.toString(); });
386
+ res.on('end', () => {
387
+ let code;
388
+ try {
389
+ code = JSON.parse(body)?.error?.code;
390
+ }
391
+ catch { /* not JSON */ }
392
+ const err = new GptLiveError(`GPT-Live HTTP ${res.statusCode}: ${redactKey(body).slice(0, 500)}`, code, res.statusCode === 401 || res.statusCode === 403 || (code !== undefined && FATAL_ERROR_CODES.has(code)), res.statusCode);
393
+ reject(err);
394
+ });
395
+ });
396
+ ws.once('error', (e) => { clearTimeout(timer); reject(new GptLiveError(`GPT-Live: ${redactKey(String(e.message))}`)); });
397
+ ws.on('message', (data) => {
398
+ let ev;
399
+ try {
400
+ ev = JSON.parse(data.toString());
401
+ }
402
+ catch {
403
+ return;
404
+ }
405
+ try {
406
+ this.handle(linkedId, s, ev);
407
+ }
408
+ catch (e) {
409
+ this.errorHandler?.(e, linkedId);
410
+ }
411
+ });
412
+ }).then((ws) => {
413
+ ws.on('error', (e) => this.errorHandler?.(new GptLiveError(`GPT-Live: ${redactKey(String(e.message))}`), linkedId));
414
+ return ws;
415
+ }).catch((e) => {
416
+ this.errorHandler?.(e, linkedId);
417
+ throw e;
418
+ });
419
+ }
420
+ waitStarted(linkedId, s) {
421
+ return new Promise((resolve, reject) => {
422
+ const timer = setTimeout(() => {
423
+ const e = new GptLiveError('GPT-Live: session.started not received');
424
+ this.errorHandler?.(e, linkedId);
425
+ s.ws?.close();
426
+ reject(e);
427
+ }, this.o.connectTimeoutMs ?? 15000);
428
+ const iv = setInterval(() => {
429
+ if (s.started) {
430
+ clearTimeout(timer);
431
+ clearInterval(iv);
432
+ resolve();
433
+ }
434
+ else if (!s.ws || s.ws.readyState !== WebSocket.OPEN) {
435
+ clearTimeout(timer);
436
+ clearInterval(iv);
437
+ reject(new GptLiveError('GPT-Live: closed before session.started'));
438
+ }
439
+ }, 10);
440
+ });
441
+ }
442
+ async pipeAudio(linkedId, s, audioIn) {
443
+ let checked = false;
444
+ try {
445
+ for await (const chunk of audioIn) {
446
+ if (!s.ws || s.ws.readyState !== WebSocket.OPEN || s.closing)
447
+ break;
448
+ if (!checked) {
449
+ checked = true;
450
+ // 🔴 full duplex: a mixed stream feeds the model its own voice. Warn once, keep going
451
+ // (blocking the call would be worse than a degraded one).
452
+ if (chunk.dir !== 'in' && !this.o.allowMixedInput) {
453
+ this.errorHandler?.(new GptLiveError(`GPT-Live: input audio is not caller-only (dir=${chunk.dir}) — the model will hear its own voice. ` +
454
+ "Use Stasis mode=customer and gw.streamAudio(id, { dir: 'in' }).", 'input_not_caller_only'), linkedId);
455
+ }
456
+ }
457
+ const pcm = float32ToSlin16(chunk.samples);
458
+ this.send(s, { type: 'session.input_audio.append', audio: Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).toString('base64') });
459
+ }
460
+ }
461
+ catch (e) {
462
+ this.errorHandler?.(e, linkedId);
463
+ }
464
+ }
465
+ handle(linkedId, s, ev) {
466
+ switch (ev.type) {
467
+ case 'session.started':
468
+ s.started = true;
469
+ break;
470
+ case 'session.output_audio.delta': {
471
+ if (!ev.delta)
472
+ break;
473
+ const { out, opened, closed } = s.gate.push(Buffer.from(ev.delta, 'base64'));
474
+ if (opened) {
475
+ this.speechActivityHandler?.({ linkedId, side: 'callee', speaking: true, timestampMs: Date.now() });
476
+ // new speech after a result → the result was (very likely) said
477
+ for (const d of s.delegations.values())
478
+ if (d.answeredAt !== undefined && !d.spoken)
479
+ this.markSpoken(d);
480
+ }
481
+ if (out.length)
482
+ this.audioOutputHandler?.(out, linkedId);
483
+ if (closed)
484
+ this.speechActivityHandler?.({ linkedId, side: 'callee', speaking: false, timestampMs: Date.now() });
485
+ break;
486
+ }
487
+ case 'session.input_transcript.delta':
488
+ this.transcriptDelta(linkedId, s, 'customer', ev);
489
+ break;
490
+ case 'session.output_transcript.delta': {
491
+ this.transcriptDelta(linkedId, s, 'agent', ev);
492
+ for (const d of s.delegations.values()) {
493
+ if (d.ackStartMs !== undefined && typeof ev.start_ms === 'number' && ev.start_ms >= d.ackStartMs && !d.spoken) {
494
+ d.textAfterAck += ev.delta ?? '';
495
+ // a tail of the sentence that was already going («…만요.») is short — require more than that
496
+ if (d.textAfterAck.replace(/\s/g, '').length >= 10)
497
+ this.markSpoken(d);
498
+ }
499
+ }
500
+ break;
501
+ }
502
+ case 'session.commentary.appended': {
503
+ const d = s.delegations.get(ev.client_event_id);
504
+ if (d && typeof ev.start_ms === 'number')
505
+ d.ackStartMs = ev.start_ms;
506
+ break;
507
+ }
508
+ case 'session.delegation.created': {
509
+ const id = ev.delegation?.id;
510
+ if (!id || ev.delegation?.target !== 'client' || !this.delegationHandler)
511
+ break;
512
+ const req = {
513
+ linkedId, delegationId: id,
514
+ transcript: [...s.finished],
515
+ pending: s.open.customer?.text ?? '',
516
+ };
517
+ Promise.resolve(this.delegationHandler(req)).then((res) => {
518
+ if (res === undefined || res === null)
519
+ return;
520
+ const r = typeof res === 'string' ? { speak: res } : res;
521
+ if (r.note)
522
+ this.appendThinking(linkedId, id, r.note);
523
+ if (r.speak)
524
+ this.appendCommentary(linkedId, id, r.speak);
525
+ }).catch((e) => this.errorHandler?.(e, linkedId));
526
+ break;
527
+ }
528
+ case 'response.event': {
529
+ const inner = ev.event ?? {};
530
+ if (inner.type === 'response.output_item.done' && inner.item?.type === 'function_call' && inner.item.call_id) {
531
+ // the completed item carries name + call_id (arguments.done alone does not — OpenAI delegation guide)
532
+ s.pendingCalls.add(inner.item.call_id);
533
+ let args = {};
534
+ try {
535
+ args = JSON.parse(inner.item.arguments ?? '{}');
536
+ }
537
+ catch { /* keep empty */ }
538
+ this.toolCallHandler?.({ callId: inner.item.call_id, name: inner.item.name ?? '', args, linkedId });
539
+ }
540
+ break;
541
+ }
542
+ case 'session.usage.updated':
543
+ if (typeof ev.usage?.seconds === 'number')
544
+ s.usageSeconds = ev.usage.seconds; // cumulative — replace, never add
545
+ break;
546
+ case 'session.closed':
547
+ s.finalized = true;
548
+ s.closeReason = ev.reason;
549
+ if (typeof ev.usage?.seconds === 'number')
550
+ s.usageSeconds = ev.usage.seconds;
551
+ break;
552
+ case 'error': {
553
+ const code = ev.error?.code ?? undefined;
554
+ const msg = redactKey(String(ev.error?.message ?? 'unknown error'));
555
+ this.errorHandler?.(new GptLiveError(`GPT-Live${code ? ` [${code}]` : ''}: ${msg}`, code, code !== undefined && FATAL_ERROR_CODES.has(code)), linkedId);
556
+ break;
557
+ }
558
+ default:
559
+ break;
560
+ }
561
+ }
562
+ transcriptDelta(linkedId, s, speaker, ev) {
563
+ const text = ev.delta ?? '';
564
+ if (!text)
565
+ return;
566
+ const pause = this.o.transcriptPauseMs ?? 800;
567
+ const start = typeof ev.start_ms === 'number' ? ev.start_ms : undefined;
568
+ const cur = s.open[speaker];
569
+ if (cur && start !== undefined && start - cur.lastEndMs > pause)
570
+ this.closeUtterance(linkedId, s, speaker);
571
+ const u = s.open[speaker];
572
+ if (!u) {
573
+ s.open[speaker] = { speaker, text, lastEndMs: ev.end_ms ?? start ?? 0 };
574
+ if (speaker === 'customer')
575
+ this.speechActivityHandler?.({ linkedId, side: 'caller', speaking: true, timestampMs: Date.now() });
576
+ }
577
+ else {
578
+ u.text += text;
579
+ if (typeof ev.end_ms === 'number')
580
+ u.lastEndMs = Math.max(u.lastEndMs, ev.end_ms);
581
+ }
582
+ }
583
+ closeUtterance(linkedId, s, speaker) {
584
+ const u = s.open[speaker];
585
+ if (!u)
586
+ return;
587
+ delete s.open[speaker];
588
+ const text = u.text.trim();
589
+ if (text) {
590
+ s.finished.push({ speaker, text });
591
+ this.transcriptHandler?.({ linkedId, text, isFinal: true, speaker, timestampMs: Date.now() });
592
+ }
593
+ if (speaker === 'customer')
594
+ this.speechActivityHandler?.({ linkedId, side: 'caller', speaking: false, timestampMs: Date.now() });
595
+ }
596
+ watchResult(linkedId, s, delegationId, eventId) {
597
+ const ms = this.o.unspokenResultMs ?? 5000;
598
+ const d = { eventId, answeredAt: Date.now(), textAfterAck: '', spoken: false };
599
+ s.delegations.set(eventId, d);
600
+ if (ms <= 0)
601
+ return;
602
+ d.timer = setTimeout(() => {
603
+ if (!d.spoken && this.sessions.get(linkedId) === s) {
604
+ this.unspokenHandler?.({ linkedId, delegationId, waitedMs: ms });
605
+ }
606
+ }, ms);
607
+ d.timer.unref?.();
608
+ }
609
+ markSpoken(d) {
610
+ d.spoken = true;
611
+ if (d.timer)
612
+ clearTimeout(d.timer);
613
+ }
614
+ finish(linkedId, s) {
615
+ if (this.sessions.get(linkedId) !== s)
616
+ return;
617
+ this.closeUtterance(linkedId, s, 'customer');
618
+ this.closeUtterance(linkedId, s, 'agent');
619
+ for (const d of s.delegations.values())
620
+ if (d.timer)
621
+ clearTimeout(d.timer);
622
+ this.sessions.delete(linkedId);
623
+ if (s.started) {
624
+ this.usageHandler?.({ linkedId, seconds: s.usageSeconds, confirmed: s.finalized, ...(s.closeReason !== undefined && { closeReason: s.closeReason }) });
625
+ }
626
+ }
627
+ }
628
+ //# sourceMappingURL=gpt-live.js.map