@livekit/agents 1.0.33 → 1.0.35

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 (90) hide show
  1. package/dist/cli.cjs.map +1 -1
  2. package/dist/inference/api_protos.d.cts +4 -4
  3. package/dist/inference/api_protos.d.ts +4 -4
  4. package/dist/inference/llm.cjs +30 -5
  5. package/dist/inference/llm.cjs.map +1 -1
  6. package/dist/inference/llm.d.cts +3 -1
  7. package/dist/inference/llm.d.ts +3 -1
  8. package/dist/inference/llm.d.ts.map +1 -1
  9. package/dist/inference/llm.js +30 -5
  10. package/dist/inference/llm.js.map +1 -1
  11. package/dist/ipc/inference_proc_executor.cjs.map +1 -1
  12. package/dist/ipc/job_proc_executor.cjs.map +1 -1
  13. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  14. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  15. package/dist/ipc/job_proc_lazy_main.js +1 -1
  16. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  17. package/dist/llm/chat_context.cjs +20 -2
  18. package/dist/llm/chat_context.cjs.map +1 -1
  19. package/dist/llm/chat_context.d.cts +9 -0
  20. package/dist/llm/chat_context.d.ts +9 -0
  21. package/dist/llm/chat_context.d.ts.map +1 -1
  22. package/dist/llm/chat_context.js +20 -2
  23. package/dist/llm/chat_context.js.map +1 -1
  24. package/dist/llm/fallback_adapter.cjs +278 -0
  25. package/dist/llm/fallback_adapter.cjs.map +1 -0
  26. package/dist/llm/fallback_adapter.d.cts +73 -0
  27. package/dist/llm/fallback_adapter.d.ts +73 -0
  28. package/dist/llm/fallback_adapter.d.ts.map +1 -0
  29. package/dist/llm/fallback_adapter.js +254 -0
  30. package/dist/llm/fallback_adapter.js.map +1 -0
  31. package/dist/llm/fallback_adapter.test.cjs +176 -0
  32. package/dist/llm/fallback_adapter.test.cjs.map +1 -0
  33. package/dist/llm/fallback_adapter.test.js +175 -0
  34. package/dist/llm/fallback_adapter.test.js.map +1 -0
  35. package/dist/llm/index.cjs +3 -0
  36. package/dist/llm/index.cjs.map +1 -1
  37. package/dist/llm/index.d.cts +1 -0
  38. package/dist/llm/index.d.ts +1 -0
  39. package/dist/llm/index.d.ts.map +1 -1
  40. package/dist/llm/index.js +4 -0
  41. package/dist/llm/index.js.map +1 -1
  42. package/dist/llm/llm.cjs +1 -1
  43. package/dist/llm/llm.cjs.map +1 -1
  44. package/dist/llm/llm.d.cts +1 -0
  45. package/dist/llm/llm.d.ts +1 -0
  46. package/dist/llm/llm.d.ts.map +1 -1
  47. package/dist/llm/llm.js +1 -1
  48. package/dist/llm/llm.js.map +1 -1
  49. package/dist/llm/provider_format/openai.cjs +43 -20
  50. package/dist/llm/provider_format/openai.cjs.map +1 -1
  51. package/dist/llm/provider_format/openai.d.ts.map +1 -1
  52. package/dist/llm/provider_format/openai.js +43 -20
  53. package/dist/llm/provider_format/openai.js.map +1 -1
  54. package/dist/llm/provider_format/openai.test.cjs +35 -0
  55. package/dist/llm/provider_format/openai.test.cjs.map +1 -1
  56. package/dist/llm/provider_format/openai.test.js +35 -0
  57. package/dist/llm/provider_format/openai.test.js.map +1 -1
  58. package/dist/llm/provider_format/utils.cjs +1 -1
  59. package/dist/llm/provider_format/utils.cjs.map +1 -1
  60. package/dist/llm/provider_format/utils.d.ts.map +1 -1
  61. package/dist/llm/provider_format/utils.js +1 -1
  62. package/dist/llm/provider_format/utils.js.map +1 -1
  63. package/dist/stt/stt.cjs +1 -1
  64. package/dist/stt/stt.cjs.map +1 -1
  65. package/dist/stt/stt.js +1 -1
  66. package/dist/stt/stt.js.map +1 -1
  67. package/dist/tts/tts.cjs +2 -2
  68. package/dist/tts/tts.cjs.map +1 -1
  69. package/dist/tts/tts.js +2 -2
  70. package/dist/tts/tts.js.map +1 -1
  71. package/dist/voice/background_audio.cjs.map +1 -1
  72. package/dist/voice/generation.cjs +2 -1
  73. package/dist/voice/generation.cjs.map +1 -1
  74. package/dist/voice/generation.d.ts.map +1 -1
  75. package/dist/voice/generation.js +2 -1
  76. package/dist/voice/generation.js.map +1 -1
  77. package/package.json +1 -1
  78. package/src/inference/llm.ts +42 -5
  79. package/src/ipc/job_proc_lazy_main.ts +1 -1
  80. package/src/llm/chat_context.ts +32 -2
  81. package/src/llm/fallback_adapter.test.ts +238 -0
  82. package/src/llm/fallback_adapter.ts +391 -0
  83. package/src/llm/index.ts +6 -0
  84. package/src/llm/llm.ts +2 -1
  85. package/src/llm/provider_format/openai.test.ts +40 -0
  86. package/src/llm/provider_format/openai.ts +46 -19
  87. package/src/llm/provider_format/utils.ts +5 -1
  88. package/src/stt/stt.ts +1 -1
  89. package/src/tts/tts.ts +2 -2
  90. package/src/voice/generation.ts +1 -0
@@ -27,7 +27,14 @@ export type OpenAIModels =
27
27
  | 'openai/gpt-4o-mini'
28
28
  | 'openai/gpt-oss-120b';
29
29
 
30
- export type GoogleModels = 'google/gemini-2.0-flash-lite';
30
+ export type GoogleModels =
31
+ | 'google/gemini-3-pro-preview'
32
+ | 'google/gemini-3-flash-preview'
33
+ | 'google/gemini-2.5-pro'
34
+ | 'google/gemini-2.5-flash'
35
+ | 'google/gemini-2.5-flash-lite'
36
+ | 'google/gemini-2.0-flash'
37
+ | 'google/gemini-2.0-flash-lite';
31
38
 
32
39
  export type QwenModels = 'qwen/qwen3-235b-a22b-instruct';
33
40
 
@@ -235,6 +242,7 @@ export class LLMStream extends llm.LLMStream {
235
242
  private toolIndex?: number;
236
243
  private fncName?: string;
237
244
  private fncRawArguments?: string;
245
+ private toolExtra?: Record<string, unknown>;
238
246
 
239
247
  constructor(
240
248
  llm: LLM,
@@ -277,6 +285,7 @@ export class LLMStream extends llm.LLMStream {
277
285
  // (defined inside the run method to make sure the state is reset for each run/attempt)
278
286
  let retryable = true;
279
287
  this.toolCallId = this.fncName = this.fncRawArguments = this.toolIndex = undefined;
288
+ this.toolExtra = undefined;
280
289
 
281
290
  try {
282
291
  const messages = (await this.chatCtx.toProviderFormat(
@@ -386,8 +395,6 @@ export class LLMStream extends llm.LLMStream {
386
395
  options: { retryable },
387
396
  });
388
397
  }
389
- } finally {
390
- this.queue.close();
391
398
  }
392
399
  }
393
400
 
@@ -430,6 +437,7 @@ export class LLMStream extends llm.LLMStream {
430
437
  if (this.toolCallId && tool.id && tool.index !== this.toolIndex) {
431
438
  callChunk = this.createRunningToolCallChunk(id, delta);
432
439
  this.toolCallId = this.fncName = this.fncRawArguments = undefined;
440
+ this.toolExtra = undefined;
433
441
  }
434
442
 
435
443
  // Start or continue building the current tool call
@@ -438,6 +446,10 @@ export class LLMStream extends llm.LLMStream {
438
446
  this.toolCallId = tool.id;
439
447
  this.fncName = tool.function.name;
440
448
  this.fncRawArguments = tool.function.arguments || '';
449
+ // Extract extra from tool call (e.g., Google thought signatures)
450
+ this.toolExtra =
451
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
452
+ ((tool as any).extra_content as Record<string, unknown> | undefined) ?? undefined;
441
453
  } else if (tool.function.arguments) {
442
454
  this.fncRawArguments = (this.fncRawArguments || '') + tool.function.arguments;
443
455
  }
@@ -456,11 +468,17 @@ export class LLMStream extends llm.LLMStream {
456
468
  ) {
457
469
  const callChunk = this.createRunningToolCallChunk(id, delta);
458
470
  this.toolCallId = this.fncName = this.fncRawArguments = undefined;
471
+ this.toolExtra = undefined;
459
472
  return callChunk;
460
473
  }
461
474
 
475
+ // Extract extra from delta (e.g., Google thought signatures on text parts)
476
+ const deltaExtra =
477
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
478
+ ((delta as any).extra_content as Record<string, unknown> | undefined) ?? undefined;
479
+
462
480
  // Regular content message
463
- if (!delta.content) {
481
+ if (!delta.content && !deltaExtra) {
464
482
  return undefined;
465
483
  }
466
484
 
@@ -468,7 +486,8 @@ export class LLMStream extends llm.LLMStream {
468
486
  id,
469
487
  delta: {
470
488
  role: 'assistant',
471
- content: delta.content,
489
+ content: delta.content || undefined,
490
+ extra: deltaExtra,
472
491
  },
473
492
  };
474
493
  }
@@ -477,19 +496,37 @@ export class LLMStream extends llm.LLMStream {
477
496
  id: string,
478
497
  delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta,
479
498
  ): llm.ChatChunk {
499
+ const toolExtra = this.toolExtra ? { ...this.toolExtra } : {};
500
+ const thoughtSignature = this.extractThoughtSignature(toolExtra);
501
+ const deltaExtra =
502
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
503
+ ((delta as any).extra_content as Record<string, unknown> | undefined) ?? undefined;
504
+
480
505
  return {
481
506
  id,
482
507
  delta: {
483
508
  role: 'assistant',
484
509
  content: delta.content || undefined,
510
+ extra: deltaExtra,
485
511
  toolCalls: [
486
512
  llm.FunctionCall.create({
487
513
  callId: this.toolCallId || '',
488
514
  name: this.fncName || '',
489
515
  args: this.fncRawArguments || '',
516
+ extra: toolExtra,
517
+ thoughtSignature,
490
518
  }),
491
519
  ],
492
520
  },
493
521
  };
494
522
  }
523
+
524
+ private extractThoughtSignature(extra?: Record<string, unknown>): string | undefined {
525
+ const googleExtra = extra?.google;
526
+ if (googleExtra && typeof googleExtra === 'object') {
527
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
528
+ return (googleExtra as any).thoughtSignature || (googleExtra as any).thought_signature;
529
+ }
530
+ return undefined;
531
+ }
495
532
  }
@@ -136,7 +136,7 @@ const startJob = (
136
136
  shutdownTasks.push(callback());
137
137
  }
138
138
  await Promise.all(shutdownTasks).catch((error) =>
139
- logger.error('error while shutting down the job', error),
139
+ logger.error({ error }, 'error while shutting down the job'),
140
140
  );
141
141
 
142
142
  process.send!({ case: 'done' });
@@ -189,6 +189,12 @@ export class FunctionCall {
189
189
 
190
190
  createdAt: number;
191
191
 
192
+ extra: Record<string, unknown>;
193
+ /**
194
+ * Optional grouping identifier for parallel tool calls.
195
+ */
196
+ groupId?: string;
197
+
192
198
  /**
193
199
  * Opaque signature for Gemini thinking mode.
194
200
  * When using Gemini 3+ models with thinking enabled, this signature must be
@@ -202,6 +208,8 @@ export class FunctionCall {
202
208
  args: string;
203
209
  id?: string;
204
210
  createdAt?: number;
211
+ extra?: Record<string, unknown>;
212
+ groupId?: string;
205
213
  thoughtSignature?: string;
206
214
  }) {
207
215
  const {
@@ -210,6 +218,8 @@ export class FunctionCall {
210
218
  args,
211
219
  id = shortuuid('item_'),
212
220
  createdAt = Date.now(),
221
+ extra = {},
222
+ groupId,
213
223
  thoughtSignature,
214
224
  } = params;
215
225
  this.id = id;
@@ -217,7 +227,15 @@ export class FunctionCall {
217
227
  this.args = args;
218
228
  this.name = name;
219
229
  this.createdAt = createdAt;
220
- this.thoughtSignature = thoughtSignature;
230
+ this.extra = { ...extra };
231
+ this.groupId = groupId;
232
+ this.thoughtSignature =
233
+ thoughtSignature ??
234
+ (typeof this.extra.google === 'object' && this.extra.google !== null
235
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ (this.extra.google as any).thoughtSignature ||
237
+ (this.extra.google as any).thought_signature
238
+ : undefined);
221
239
  }
222
240
 
223
241
  static create(params: {
@@ -226,6 +244,8 @@ export class FunctionCall {
226
244
  args: string;
227
245
  id?: string;
228
246
  createdAt?: number;
247
+ extra?: Record<string, unknown>;
248
+ groupId?: string;
229
249
  thoughtSignature?: string;
230
250
  }) {
231
251
  return new FunctionCall(params);
@@ -241,6 +261,14 @@ export class FunctionCall {
241
261
  args: this.args,
242
262
  };
243
263
 
264
+ if (Object.keys(this.extra).length > 0) {
265
+ result.extra = this.extra as JSONValue;
266
+ }
267
+
268
+ if (this.groupId) {
269
+ result.groupId = this.groupId;
270
+ }
271
+
244
272
  if (this.thoughtSignature) {
245
273
  result.thoughtSignature = this.thoughtSignature;
246
274
  }
@@ -627,7 +655,9 @@ export class ChatContext {
627
655
  a.name !== b.name ||
628
656
  a.callId !== b.callId ||
629
657
  a.args !== b.args ||
630
- a.thoughtSignature !== b.thoughtSignature
658
+ a.thoughtSignature !== b.thoughtSignature ||
659
+ a.groupId !== b.groupId ||
660
+ JSON.stringify(a.extra) !== JSON.stringify(b.extra)
631
661
  ) {
632
662
  return false;
633
663
  }
@@ -0,0 +1,238 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { beforeAll, describe, expect, it, vi } from 'vitest';
5
+ import { APIConnectionError, APIError } from '../_exceptions.js';
6
+ import { initializeLogger } from '../log.js';
7
+ import type { APIConnectOptions } from '../types.js';
8
+ import { delay } from '../utils.js';
9
+ import type { ChatContext } from './chat_context.js';
10
+ import { FallbackAdapter } from './fallback_adapter.js';
11
+ import { type ChatChunk, LLM, LLMStream } from './llm.js';
12
+ import type { ToolChoice, ToolContext } from './tool_context.js';
13
+
14
+ class MockLLMStream extends LLMStream {
15
+ public myLLM: LLM;
16
+
17
+ constructor(
18
+ llm: LLM,
19
+ opts: {
20
+ chatCtx: ChatContext;
21
+ toolCtx?: ToolContext;
22
+ connOptions: APIConnectOptions;
23
+ },
24
+ private shouldFail: boolean = false,
25
+ private failAfterChunks: number = 0,
26
+ ) {
27
+ super(llm, opts);
28
+ this.myLLM = llm;
29
+ }
30
+
31
+ protected async run(): Promise<void> {
32
+ if (this.shouldFail && this.failAfterChunks === 0) {
33
+ throw new APIError('Mock LLM failed immediately');
34
+ }
35
+
36
+ const chunk: ChatChunk = {
37
+ id: 'test-id',
38
+ delta: { role: 'assistant', content: 'chunk' },
39
+ };
40
+
41
+ for (let i = 0; i < 3; i++) {
42
+ if (this.shouldFail && i === this.failAfterChunks) {
43
+ throw new APIError('Mock LLM failed after chunks');
44
+ }
45
+ this.queue.put(chunk);
46
+ await delay(10);
47
+ }
48
+ }
49
+ }
50
+
51
+ class MockLLM extends LLM {
52
+ shouldFail: boolean = false;
53
+ failAfterChunks: number = 0;
54
+ private _label: string;
55
+
56
+ constructor(label: string) {
57
+ super();
58
+ this._label = label;
59
+ }
60
+
61
+ label(): string {
62
+ return this._label;
63
+ }
64
+
65
+ chat(opts: {
66
+ chatCtx: ChatContext;
67
+ toolCtx?: ToolContext;
68
+ connOptions?: APIConnectOptions;
69
+ parallelToolCalls?: boolean;
70
+ toolChoice?: ToolChoice;
71
+ extraKwargs?: Record<string, unknown>;
72
+ }): LLMStream {
73
+ return new MockLLMStream(
74
+ this,
75
+ {
76
+ chatCtx: opts.chatCtx,
77
+ toolCtx: opts.toolCtx,
78
+ connOptions: opts.connOptions!,
79
+ },
80
+ this.shouldFail,
81
+ this.failAfterChunks,
82
+ );
83
+ }
84
+ }
85
+
86
+ describe('FallbackAdapter', () => {
87
+ beforeAll(() => {
88
+ initializeLogger({ pretty: false });
89
+ // Suppress unhandled rejections from LLMStream background tasks
90
+ process.on('unhandledRejection', () => {});
91
+ });
92
+
93
+ it('should initialize correctly', () => {
94
+ const llm1 = new MockLLM('llm1');
95
+ const adapter = new FallbackAdapter({ llms: [llm1] });
96
+ expect(adapter.llms).toHaveLength(1);
97
+ expect(adapter.llms[0]).toBe(llm1);
98
+ });
99
+
100
+ it('should throw if no LLMs provided', () => {
101
+ expect(() => new FallbackAdapter({ llms: [] })).toThrow();
102
+ });
103
+
104
+ it('should use primary LLM if successful', async () => {
105
+ const llm1 = new MockLLM('llm1');
106
+ const llm2 = new MockLLM('llm2');
107
+ const adapter = new FallbackAdapter({ llms: [llm1, llm2] });
108
+
109
+ const stream = adapter.chat({
110
+ chatCtx: {} as ChatContext,
111
+ });
112
+
113
+ const chunks: ChatChunk[] = [];
114
+ for await (const chunk of stream) {
115
+ chunks.push(chunk);
116
+ }
117
+
118
+ expect(chunks).toHaveLength(3);
119
+ // Should verify it used llm1 (we can check logs or spy, but simple success is good first step)
120
+ });
121
+
122
+ it('should fallback to second LLM if first fails immediately', async () => {
123
+ const llm1 = new MockLLM('llm1');
124
+ llm1.shouldFail = true;
125
+ const llm2 = new MockLLM('llm2');
126
+ const adapter = new FallbackAdapter({ llms: [llm1, llm2] });
127
+
128
+ const stream = adapter.chat({
129
+ chatCtx: {} as ChatContext,
130
+ });
131
+
132
+ const chunks: ChatChunk[] = [];
133
+ for await (const chunk of stream) {
134
+ chunks.push(chunk);
135
+ }
136
+
137
+ expect(chunks).toHaveLength(3);
138
+ expect(adapter._status[0]!.available).toBe(false);
139
+ expect(adapter._status[1]!.available).toBe(true);
140
+ });
141
+
142
+ it('should fail if all LLMs fail', async () => {
143
+ const llm1 = new MockLLM('llm1');
144
+ llm1.shouldFail = true;
145
+ const llm2 = new MockLLM('llm2');
146
+ llm2.shouldFail = true;
147
+ const adapter = new FallbackAdapter({ llms: [llm1, llm2] });
148
+
149
+ const stream = adapter.chat({
150
+ chatCtx: {} as ChatContext,
151
+ });
152
+
153
+ const errorPromise = new Promise<Error>((resolve) => {
154
+ adapter.on('error', (e) => resolve(e.error));
155
+ });
156
+
157
+ for await (const _ of stream) {
158
+ // consume
159
+ }
160
+
161
+ const error = await errorPromise;
162
+ expect(error).toBeInstanceOf(APIConnectionError);
163
+ });
164
+
165
+ it('should fail if chunks sent and retryOnChunkSent is false', async () => {
166
+ const llm1 = new MockLLM('llm1');
167
+ llm1.shouldFail = true;
168
+ llm1.failAfterChunks = 1; // Fail after 1 chunk
169
+ const llm2 = new MockLLM('llm2');
170
+ const adapter = new FallbackAdapter({
171
+ llms: [llm1, llm2],
172
+ retryOnChunkSent: false,
173
+ });
174
+
175
+ const stream = adapter.chat({
176
+ chatCtx: {} as ChatContext,
177
+ });
178
+
179
+ const errorPromise = new Promise<Error>((resolve) => {
180
+ adapter.on('error', (e) => resolve(e.error));
181
+ });
182
+
183
+ for await (const _ of stream) {
184
+ // consume
185
+ }
186
+
187
+ const error = await errorPromise;
188
+ expect(error).toBeInstanceOf(APIError);
189
+ });
190
+
191
+ it('should fallback if chunks sent and retryOnChunkSent is true', async () => {
192
+ const llm1 = new MockLLM('llm1');
193
+ llm1.shouldFail = true;
194
+ llm1.failAfterChunks = 1;
195
+ const llm2 = new MockLLM('llm2');
196
+ const adapter = new FallbackAdapter({
197
+ llms: [llm1, llm2],
198
+ retryOnChunkSent: true,
199
+ });
200
+
201
+ const stream = adapter.chat({
202
+ chatCtx: {} as ChatContext,
203
+ });
204
+
205
+ const chunks: ChatChunk[] = [];
206
+ for await (const chunk of stream) {
207
+ chunks.push(chunk);
208
+ }
209
+
210
+ // 1 chunk from failed llm1 + 3 chunks from llm2
211
+ expect(chunks).toHaveLength(4);
212
+ });
213
+
214
+ it('should emit availability changed events', async () => {
215
+ const llm1 = new MockLLM('llm1');
216
+ llm1.shouldFail = true;
217
+ const llm2 = new MockLLM('llm2');
218
+ const adapter = new FallbackAdapter({ llms: [llm1, llm2] });
219
+
220
+ const eventSpy = vi.fn();
221
+ (adapter as any).on('llm_availability_changed', eventSpy);
222
+
223
+ const stream = adapter.chat({
224
+ chatCtx: {} as ChatContext,
225
+ });
226
+
227
+ for await (const _ of stream) {
228
+ // consume
229
+ }
230
+
231
+ expect(eventSpy).toHaveBeenCalledWith(
232
+ expect.objectContaining({
233
+ llm: llm1,
234
+ available: false,
235
+ }),
236
+ );
237
+ });
238
+ });