@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.
- package/dist/cli.cjs.map +1 -1
- package/dist/inference/api_protos.d.cts +4 -4
- package/dist/inference/api_protos.d.ts +4 -4
- package/dist/inference/llm.cjs +30 -5
- package/dist/inference/llm.cjs.map +1 -1
- package/dist/inference/llm.d.cts +3 -1
- package/dist/inference/llm.d.ts +3 -1
- package/dist/inference/llm.d.ts.map +1 -1
- package/dist/inference/llm.js +30 -5
- package/dist/inference/llm.js.map +1 -1
- package/dist/ipc/inference_proc_executor.cjs.map +1 -1
- package/dist/ipc/job_proc_executor.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +1 -1
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/llm/chat_context.cjs +20 -2
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.cts +9 -0
- package/dist/llm/chat_context.d.ts +9 -0
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +20 -2
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/fallback_adapter.cjs +278 -0
- package/dist/llm/fallback_adapter.cjs.map +1 -0
- package/dist/llm/fallback_adapter.d.cts +73 -0
- package/dist/llm/fallback_adapter.d.ts +73 -0
- package/dist/llm/fallback_adapter.d.ts.map +1 -0
- package/dist/llm/fallback_adapter.js +254 -0
- package/dist/llm/fallback_adapter.js.map +1 -0
- package/dist/llm/fallback_adapter.test.cjs +176 -0
- package/dist/llm/fallback_adapter.test.cjs.map +1 -0
- package/dist/llm/fallback_adapter.test.js +175 -0
- package/dist/llm/fallback_adapter.test.js.map +1 -0
- package/dist/llm/index.cjs +3 -0
- package/dist/llm/index.cjs.map +1 -1
- package/dist/llm/index.d.cts +1 -0
- package/dist/llm/index.d.ts +1 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +4 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/llm.cjs +1 -1
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.d.cts +1 -0
- package/dist/llm/llm.d.ts +1 -0
- package/dist/llm/llm.d.ts.map +1 -1
- package/dist/llm/llm.js +1 -1
- package/dist/llm/llm.js.map +1 -1
- package/dist/llm/provider_format/openai.cjs +43 -20
- package/dist/llm/provider_format/openai.cjs.map +1 -1
- package/dist/llm/provider_format/openai.d.ts.map +1 -1
- package/dist/llm/provider_format/openai.js +43 -20
- package/dist/llm/provider_format/openai.js.map +1 -1
- package/dist/llm/provider_format/openai.test.cjs +35 -0
- package/dist/llm/provider_format/openai.test.cjs.map +1 -1
- package/dist/llm/provider_format/openai.test.js +35 -0
- package/dist/llm/provider_format/openai.test.js.map +1 -1
- package/dist/llm/provider_format/utils.cjs +1 -1
- package/dist/llm/provider_format/utils.cjs.map +1 -1
- package/dist/llm/provider_format/utils.d.ts.map +1 -1
- package/dist/llm/provider_format/utils.js +1 -1
- package/dist/llm/provider_format/utils.js.map +1 -1
- package/dist/stt/stt.cjs +1 -1
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.js +1 -1
- package/dist/stt/stt.js.map +1 -1
- package/dist/tts/tts.cjs +2 -2
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.js +2 -2
- package/dist/tts/tts.js.map +1 -1
- package/dist/voice/background_audio.cjs.map +1 -1
- package/dist/voice/generation.cjs +2 -1
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +2 -1
- package/dist/voice/generation.js.map +1 -1
- package/package.json +1 -1
- package/src/inference/llm.ts +42 -5
- package/src/ipc/job_proc_lazy_main.ts +1 -1
- package/src/llm/chat_context.ts +32 -2
- package/src/llm/fallback_adapter.test.ts +238 -0
- package/src/llm/fallback_adapter.ts +391 -0
- package/src/llm/index.ts +6 -0
- package/src/llm/llm.ts +2 -1
- package/src/llm/provider_format/openai.test.ts +40 -0
- package/src/llm/provider_format/openai.ts +46 -19
- package/src/llm/provider_format/utils.ts +5 -1
- package/src/stt/stt.ts +1 -1
- package/src/tts/tts.ts +2 -2
- package/src/voice/generation.ts +1 -0
package/src/inference/llm.ts
CHANGED
|
@@ -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 =
|
|
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'
|
|
139
|
+
logger.error({ error }, 'error while shutting down the job'),
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
process.send!({ case: 'done' });
|
package/src/llm/chat_context.ts
CHANGED
|
@@ -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.
|
|
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
|
+
});
|