@librechat/agents 3.3.13 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cjs/graphs/Graph.cjs +10 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +35 -206
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/index.cjs +121 -241
  6. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  7. package/dist/cjs/llm/google/index.cjs +19 -12
  8. package/dist/cjs/llm/google/index.cjs.map +1 -1
  9. package/dist/cjs/llm/mistral/index.cjs +26 -0
  10. package/dist/cjs/llm/mistral/index.cjs.map +1 -0
  11. package/dist/cjs/llm/openai/index.cjs +82 -80
  12. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  13. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  14. package/dist/cjs/llm/providers.cjs +19 -19
  15. package/dist/cjs/llm/providers.cjs.map +1 -1
  16. package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
  17. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
  18. package/dist/cjs/llm/stream/smoother.cjs +369 -0
  19. package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
  20. package/dist/cjs/llm/vertexai/index.cjs +13 -1
  21. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  22. package/dist/cjs/main.cjs +18 -10
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +34 -2
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/utils/tokens.cjs +76 -29
  26. package/dist/cjs/utils/tokens.cjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +10 -0
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/llm/anthropic/index.mjs +34 -205
  30. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  31. package/dist/esm/llm/bedrock/index.mjs +120 -240
  32. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  33. package/dist/esm/llm/google/index.mjs +19 -12
  34. package/dist/esm/llm/google/index.mjs.map +1 -1
  35. package/dist/esm/llm/mistral/index.mjs +26 -0
  36. package/dist/esm/llm/mistral/index.mjs.map +1 -0
  37. package/dist/esm/llm/openai/index.mjs +82 -80
  38. package/dist/esm/llm/openai/index.mjs.map +1 -1
  39. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  40. package/dist/esm/llm/providers.mjs +3 -3
  41. package/dist/esm/llm/providers.mjs.map +1 -1
  42. package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
  43. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
  44. package/dist/esm/llm/stream/smoother.mjs +365 -0
  45. package/dist/esm/llm/stream/smoother.mjs.map +1 -0
  46. package/dist/esm/llm/vertexai/index.mjs +13 -1
  47. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  48. package/dist/esm/main.mjs +4 -2
  49. package/dist/esm/tools/subagent/SubagentExecutor.mjs +34 -2
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  51. package/dist/esm/utils/tokens.mjs +76 -30
  52. package/dist/esm/utils/tokens.mjs.map +1 -1
  53. package/dist/types/graphs/Graph.d.ts +3 -0
  54. package/dist/types/index.d.ts +3 -0
  55. package/dist/types/llm/google/index.d.ts +2 -0
  56. package/dist/types/llm/mistral/index.d.ts +11 -0
  57. package/dist/types/llm/openai/index.d.ts +20 -4
  58. package/dist/types/llm/openrouter/index.d.ts +4 -1
  59. package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
  60. package/dist/types/llm/stream/smoother.d.ts +95 -0
  61. package/dist/types/llm/vertexai/index.d.ts +2 -0
  62. package/dist/types/tools/subagent/SubagentExecutor.d.ts +3 -0
  63. package/dist/types/types/llm.d.ts +21 -14
  64. package/dist/types/utils/tokens.d.ts +10 -0
  65. package/package.json +2 -2
  66. package/src/graphs/Graph.ts +11 -0
  67. package/src/index.ts +8 -0
  68. package/src/llm/anthropic/index.ts +85 -354
  69. package/src/llm/bedrock/index.ts +174 -390
  70. package/src/llm/bedrock/llm.spec.ts +2 -0
  71. package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
  72. package/src/llm/custom-chat-models.smoke.test.ts +16 -1
  73. package/src/llm/google/index.ts +17 -18
  74. package/src/llm/google/streamSmoothing.test.ts +121 -0
  75. package/src/llm/mistral/index.ts +33 -0
  76. package/src/llm/mistral/streamSmoothing.test.ts +97 -0
  77. package/src/llm/openai/deepseek.test.ts +56 -0
  78. package/src/llm/openai/index.ts +119 -126
  79. package/src/llm/openrouter/index.ts +4 -1
  80. package/src/llm/providers.ts +3 -3
  81. package/src/llm/stream/chunkAdapters.test.ts +202 -0
  82. package/src/llm/stream/chunkAdapters.ts +317 -0
  83. package/src/llm/stream/reassembly.test.ts +241 -0
  84. package/src/llm/stream/smoother.bench.test.ts +155 -0
  85. package/src/llm/stream/smoother.test.ts +519 -0
  86. package/src/llm/stream/smoother.ts +574 -0
  87. package/src/llm/vertexai/index.ts +17 -1
  88. package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
  89. package/src/specs/subagent.test.ts +44 -0
  90. package/src/specs/tokens.test.ts +159 -14
  91. package/src/tools/__tests__/SubagentExecutor.test.ts +48 -1
  92. package/src/tools/subagent/SubagentExecutor.ts +69 -5
  93. package/src/types/llm.ts +53 -36
  94. package/src/utils/tokens.ts +115 -30
@@ -252,4 +252,80 @@ describe('Converse stream seal dispatch', () => {
252
252
  setTimeoutSpy.mockRestore();
253
253
  }
254
254
  });
255
+
256
+ test('emits toolUse seal chunks without pacing delay while text is paced', async () => {
257
+ const model = new CustomChatBedrockConverse({
258
+ model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
259
+ region: 'us-east-1',
260
+ credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
261
+ _lc_stream_delay: 60,
262
+ });
263
+
264
+ (model as unknown as { client: { send: unknown } }).client.send = jest.fn(
265
+ async () => ({
266
+ stream: (async function* () {
267
+ yield {
268
+ contentBlockDelta: {
269
+ contentBlockIndex: 0,
270
+ delta: { text: 'alpha beta' },
271
+ },
272
+ };
273
+ yield {
274
+ contentBlockStart: {
275
+ contentBlockIndex: 1,
276
+ start: { toolUse: { toolUseId: 'call_1', name: 'weather' } },
277
+ },
278
+ };
279
+ yield {
280
+ contentBlockDelta: {
281
+ contentBlockIndex: 1,
282
+ delta: { toolUse: { input: '{"city":"NYC"}' } },
283
+ },
284
+ };
285
+ yield { contentBlockStop: { contentBlockIndex: 1 } };
286
+ })(),
287
+ })
288
+ );
289
+
290
+ const arrivals: { isSeal: boolean; hasText: boolean; at: number }[] = [];
291
+ for await (const chunk of model._streamResponseChunks(
292
+ [new HumanMessage('hi')],
293
+ {} as Parameters<CustomChatBedrockConverse['_streamResponseChunks']>[1],
294
+ undefined
295
+ )) {
296
+ const message = chunk.message as AIMessageChunk;
297
+ arrivals.push({
298
+ isSeal:
299
+ (message.response_metadata as Record<string, unknown>)[
300
+ STREAMED_TOOL_CALL_SEAL_METADATA_KEY
301
+ ] != null,
302
+ hasText: chunk.text !== '',
303
+ at: Date.now(),
304
+ });
305
+ }
306
+
307
+ const lastText = [...arrivals].reverse().find((entry) => entry.hasText);
308
+ const seal = arrivals.find((entry) => entry.isSeal);
309
+ expect(lastText).toBeDefined();
310
+ expect(seal).toBeDefined();
311
+ expect((seal as { at: number }).at).toBeGreaterThanOrEqual(
312
+ (lastText as { at: number }).at
313
+ );
314
+ expect(
315
+ (seal as { at: number }).at - (lastText as { at: number }).at
316
+ ).toBeLessThan(45);
317
+ });
318
+
319
+ test('defaults to 25ms adaptive smoothing with 0 disabling', () => {
320
+ const base = {
321
+ model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
322
+ region: 'us-east-1',
323
+ credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
324
+ };
325
+ expect(new CustomChatBedrockConverse(base)._lc_stream_delay).toBe(25);
326
+ expect(
327
+ new CustomChatBedrockConverse({ ...base, _lc_stream_delay: 0 })
328
+ ._lc_stream_delay
329
+ ).toBe(0);
330
+ });
255
331
  });
@@ -1836,7 +1836,7 @@ describe('custom chat model class smoke tests', () => {
1836
1836
  );
1837
1837
  expect(model.applicationInferenceProfile).toBe(applicationInferenceProfile);
1838
1838
  expect(model._lc_stream_delay).toBe(12);
1839
- expect(defaultStreamDelayModel._lc_stream_delay).toBe(0);
1839
+ expect(defaultStreamDelayModel._lc_stream_delay).toBe(25);
1840
1840
  expect(model.invocationParams({}).serviceTier).toEqual({
1841
1841
  type: 'priority',
1842
1842
  });
@@ -1949,3 +1949,18 @@ describe('custom chat model class smoke tests', () => {
1949
1949
  expect(capturedFetch.getSignal()?.aborted).toBe(true);
1950
1950
  });
1951
1951
  });
1952
+
1953
+ describe('StreamSmoothingOptions type surface', () => {
1954
+ it('accepts _lc_stream_delay: 0 across the typed provider options', () => {
1955
+ const azure: import('@/types').AzureClientOptions = { _lc_stream_delay: 0 };
1956
+ const openrouter: ChatOpenRouterCallOptions &
1957
+ import('@/types').StreamSmoothingOptions = { _lc_stream_delay: 0 };
1958
+ const mistral: import('@/types').MistralAIClientOptions = {
1959
+ model: 'mistral-large-latest',
1960
+ _lc_stream_delay: 0,
1961
+ };
1962
+ expect(azure._lc_stream_delay).toBe(0);
1963
+ expect(openrouter._lc_stream_delay).toBe(0);
1964
+ expect(mistral._lc_stream_delay).toBe(0);
1965
+ });
1966
+ });
@@ -16,6 +16,8 @@ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager
16
16
  import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
17
17
  import type { GeminiApiUsageMetadata, InputTokenDetails } from './types';
18
18
  import type { GoogleClientOptions, GoogleThinkingConfig } from '@/types';
19
+ import { smoothGenerationChunks } from '@/llm/stream/chunkAdapters';
20
+ import { resolveStreamDelay } from '@/llm/stream/smoother';
19
21
  import {
20
22
  convertResponseContentToChatGenerationChunk,
21
23
  convertBaseMessagesToContent,
@@ -36,6 +38,7 @@ type GoogleToolConfigWithServerSideInvocations = ToolConfig & {
36
38
  };
37
39
 
38
40
  export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
41
+ _lc_stream_delay: number;
39
42
  thinkingConfig?: GoogleThinkingConfig;
40
43
  includeServerSideToolInvocations?: boolean;
41
44
 
@@ -55,6 +58,7 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
55
58
  constructor(fields: GoogleClientOptions) {
56
59
  super(fields);
57
60
 
61
+ this._lc_stream_delay = resolveStreamDelay(fields._lc_stream_delay);
58
62
  this.model = fields.model.replace(/^models\//, '');
59
63
 
60
64
  this.maxOutputTokens = fields.maxOutputTokens ?? this.maxOutputTokens;
@@ -296,6 +300,18 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
296
300
  messages: BaseMessage[],
297
301
  options: this['ParsedCallOptions'],
298
302
  runManager?: CallbackManagerForLLMRun
303
+ ): AsyncGenerator<ChatGenerationChunk> {
304
+ yield* smoothGenerationChunks({
305
+ chunks: this._streamProviderChunks(messages, options),
306
+ delayMs: this._lc_stream_delay,
307
+ signal: options.signal,
308
+ runManager,
309
+ });
310
+ }
311
+
312
+ private async *_streamProviderChunks(
313
+ messages: BaseMessage[],
314
+ options: this['ParsedCallOptions']
299
315
  ): AsyncGenerator<ChatGenerationChunk> {
300
316
  const prompt = convertBaseMessagesToContent(
301
317
  messages,
@@ -349,33 +365,16 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
349
365
  }
350
366
 
351
367
  yield chunk;
352
- await runManager?.handleLLMNewToken(
353
- chunk.text || '',
354
- undefined,
355
- undefined,
356
- undefined,
357
- undefined,
358
- { chunk }
359
- );
360
368
  }
361
369
 
362
370
  if (lastUsageMetadata) {
363
- const finalChunk = new ChatGenerationChunk({
371
+ yield new ChatGenerationChunk({
364
372
  text: '',
365
373
  message: new AIMessageChunk({
366
374
  content: '',
367
375
  usage_metadata: lastUsageMetadata,
368
376
  }),
369
377
  });
370
- yield finalChunk;
371
- await runManager?.handleLLMNewToken(
372
- finalChunk.text || '',
373
- undefined,
374
- undefined,
375
- undefined,
376
- undefined,
377
- { chunk: finalChunk }
378
- );
379
378
  }
380
379
  }
381
380
  }
@@ -0,0 +1,121 @@
1
+ import { expect, test, describe, jest } from '@jest/globals';
2
+ import { HumanMessage, AIMessageChunk } from '@langchain/core/messages';
3
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
4
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
5
+ import { CustomChatGoogleGenerativeAI } from './index';
6
+
7
+ describe('Google stream smoothing', () => {
8
+ function textResponse(text: string): Record<string, unknown> {
9
+ return {
10
+ candidates: [
11
+ {
12
+ content: { role: 'model', parts: [{ text }] },
13
+ index: 0,
14
+ },
15
+ ],
16
+ };
17
+ }
18
+
19
+ async function runStream(
20
+ responses: Record<string, unknown>[],
21
+ modelFields: Record<string, unknown> = {}
22
+ ): Promise<{
23
+ yielded: ChatGenerationChunk[];
24
+ dispatchedTokens: string[];
25
+ }> {
26
+ const model = new CustomChatGoogleGenerativeAI({
27
+ model: 'gemini-2.5-flash',
28
+ apiKey: 'test-key',
29
+ ...modelFields,
30
+ });
31
+
32
+ (
33
+ model as unknown as {
34
+ client: { generateContentStream: unknown };
35
+ }
36
+ ).client.generateContentStream = jest.fn(async () => ({
37
+ stream: (async function* () {
38
+ yield* responses;
39
+ })(),
40
+ }));
41
+
42
+ const dispatchedTokens: string[] = [];
43
+ const runManager = {
44
+ handleLLMNewToken: jest.fn(async (token: string) => {
45
+ dispatchedTokens.push(token);
46
+ }),
47
+ } as unknown as CallbackManagerForLLMRun;
48
+
49
+ const yielded: ChatGenerationChunk[] = [];
50
+ for await (const chunk of model._streamResponseChunks(
51
+ [new HumanMessage('hi')],
52
+ {} as Parameters<
53
+ CustomChatGoogleGenerativeAI['_streamResponseChunks']
54
+ >[1],
55
+ runManager
56
+ )) {
57
+ yielded.push(chunk);
58
+ }
59
+ return { yielded, dispatchedTokens };
60
+ }
61
+
62
+ test('splits large text responses at stream boundaries with pacing', async () => {
63
+ const { yielded, dispatchedTokens } = await runStream(
64
+ [textResponse('alpha beta gamma')],
65
+ { _lc_stream_delay: 1 }
66
+ );
67
+
68
+ const texts = yielded.map((chunk) => chunk.text).filter(Boolean);
69
+ expect(texts).toEqual(['alpha ', 'beta ', 'gamma']);
70
+ expect(dispatchedTokens.filter(Boolean)).toEqual([
71
+ 'alpha ',
72
+ 'beta ',
73
+ 'gamma',
74
+ ]);
75
+ });
76
+
77
+ test('passes chunks through unsplit when smoothing is disabled', async () => {
78
+ const { yielded } = await runStream([textResponse('alpha beta gamma')], {
79
+ _lc_stream_delay: 0,
80
+ });
81
+
82
+ expect(yielded.map((chunk) => chunk.text).filter(Boolean)).toEqual([
83
+ 'alpha beta gamma',
84
+ ]);
85
+ });
86
+
87
+ test('emits the final usage chunk without pacing delay', async () => {
88
+ const start = Date.now();
89
+ const { yielded } = await runStream(
90
+ [
91
+ {
92
+ ...textResponse('short text here'),
93
+ usageMetadata: {
94
+ promptTokenCount: 3,
95
+ candidatesTokenCount: 4,
96
+ totalTokenCount: 7,
97
+ },
98
+ },
99
+ ],
100
+ { _lc_stream_delay: 30 }
101
+ );
102
+ const elapsed = Date.now() - start;
103
+
104
+ const usageChunk = yielded.find(
105
+ (chunk) =>
106
+ chunk.text === '' &&
107
+ (chunk.message as AIMessageChunk).usage_metadata != null
108
+ );
109
+ expect(usageChunk).toBeDefined();
110
+ expect(elapsed).toBeLessThan(1000);
111
+ });
112
+
113
+ test('defaults to 25ms adaptive smoothing with 0 disabling', () => {
114
+ const base = { model: 'gemini-2.5-flash', apiKey: 'test-key' };
115
+ expect(new CustomChatGoogleGenerativeAI(base)._lc_stream_delay).toBe(25);
116
+ expect(
117
+ new CustomChatGoogleGenerativeAI({ ...base, _lc_stream_delay: 0 })
118
+ ._lc_stream_delay
119
+ ).toBe(0);
120
+ });
121
+ });
@@ -0,0 +1,33 @@
1
+ import { ChatMistralAI } from '@langchain/mistralai';
2
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
3
+ import type { ChatGenerationChunk } from '@langchain/core/outputs';
4
+ import type { BaseMessage } from '@langchain/core/messages';
5
+ import type { MistralAIClientOptions } from '@/types';
6
+ import { smoothGenerationChunks } from '@/llm/stream/chunkAdapters';
7
+ import { resolveStreamDelay } from '@/llm/stream/smoother';
8
+
9
+ export class CustomChatMistralAI extends ChatMistralAI {
10
+ _lc_stream_delay: number;
11
+
12
+ static lc_name(): 'LibreChatMistralAI' {
13
+ return 'LibreChatMistralAI';
14
+ }
15
+
16
+ constructor(fields?: MistralAIClientOptions) {
17
+ super(fields);
18
+ this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
19
+ }
20
+
21
+ async *_streamResponseChunks(
22
+ messages: BaseMessage[],
23
+ options: this['ParsedCallOptions'],
24
+ runManager?: CallbackManagerForLLMRun
25
+ ): AsyncGenerator<ChatGenerationChunk> {
26
+ yield* smoothGenerationChunks({
27
+ chunks: super._streamResponseChunks(messages, options, undefined),
28
+ delayMs: this._lc_stream_delay,
29
+ signal: options.signal,
30
+ runManager,
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,97 @@
1
+ import { expect, test, describe, jest, afterEach } from '@jest/globals';
2
+ import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
3
+ import { ChatGenerationChunk } from '@langchain/core/outputs';
4
+ import { ChatMistralAI } from '@langchain/mistralai';
5
+ import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
6
+ import { CustomChatMistralAI } from './index';
7
+
8
+ describe('Mistral stream smoothing', () => {
9
+ afterEach(() => {
10
+ jest.restoreAllMocks();
11
+ });
12
+
13
+ function stubParentStream(texts: string[]): void {
14
+ jest
15
+ .spyOn(
16
+ ChatMistralAI.prototype as unknown as {
17
+ _streamResponseChunks: (
18
+ ...args: unknown[]
19
+ ) => AsyncGenerator<ChatGenerationChunk>;
20
+ },
21
+ '_streamResponseChunks'
22
+ )
23
+ .mockImplementation(async function* () {
24
+ for (const text of texts) {
25
+ yield new ChatGenerationChunk({
26
+ text,
27
+ message: new AIMessageChunk({ content: text }),
28
+ });
29
+ }
30
+ });
31
+ }
32
+
33
+ async function collect(
34
+ model: CustomChatMistralAI,
35
+ runManager?: CallbackManagerForLLMRun
36
+ ): Promise<string[]> {
37
+ const texts: string[] = [];
38
+ for await (const chunk of model._streamResponseChunks(
39
+ [new HumanMessage('hi')],
40
+ {} as Parameters<CustomChatMistralAI['_streamResponseChunks']>[1],
41
+ runManager
42
+ )) {
43
+ if (chunk.text) {
44
+ texts.push(chunk.text);
45
+ }
46
+ }
47
+ return texts;
48
+ }
49
+
50
+ test('splits large text chunks at stream boundaries with pacing', async () => {
51
+ stubParentStream(['alpha beta gamma']);
52
+ const model = new CustomChatMistralAI({
53
+ model: 'mistral-large-latest',
54
+ apiKey: 'test-key',
55
+ _lc_stream_delay: 1,
56
+ });
57
+
58
+ const dispatchedTokens: string[] = [];
59
+ const runManager = {
60
+ handleLLMNewToken: jest.fn(async (token: string) => {
61
+ dispatchedTokens.push(token);
62
+ }),
63
+ } as unknown as CallbackManagerForLLMRun;
64
+
65
+ expect(await collect(model, runManager)).toEqual([
66
+ 'alpha ',
67
+ 'beta ',
68
+ 'gamma',
69
+ ]);
70
+ expect(dispatchedTokens.filter(Boolean)).toEqual([
71
+ 'alpha ',
72
+ 'beta ',
73
+ 'gamma',
74
+ ]);
75
+ });
76
+
77
+ test('passes chunks through unsplit when smoothing is disabled', async () => {
78
+ stubParentStream(['alpha beta gamma']);
79
+ const model = new CustomChatMistralAI({
80
+ model: 'mistral-large-latest',
81
+ apiKey: 'test-key',
82
+ _lc_stream_delay: 0,
83
+ });
84
+
85
+ expect(await collect(model)).toEqual(['alpha beta gamma']);
86
+ });
87
+
88
+ test('defaults to 25ms adaptive smoothing with 0 disabling', () => {
89
+ const base = { model: 'mistral-large-latest', apiKey: 'test-key' };
90
+ expect(new CustomChatMistralAI(base)._lc_stream_delay).toBe(25);
91
+ expect(
92
+ new CustomChatMistralAI({ ...base, _lc_stream_delay: 0 })
93
+ ._lc_stream_delay
94
+ ).toBe(0);
95
+ expect(CustomChatMistralAI.lc_name()).toBe('LibreChatMistralAI');
96
+ });
97
+ });
@@ -557,6 +557,62 @@ describe('ChatDeepSeek', () => {
557
557
  expect(textChunks).toEqual(['alpha ', 'beta ', 'gamma']);
558
558
  });
559
559
 
560
+ it('does not delay tool-call and finish chunks between paced text', async () => {
561
+ const toolCallChunk: OpenAIChatCompletionChunk = {
562
+ id: 'chatcmpl-deepseek-test',
563
+ object: 'chat.completion.chunk',
564
+ created: 0,
565
+ model: 'deepseek-v4-pro',
566
+ choices: [
567
+ {
568
+ index: 0,
569
+ delta: {
570
+ role: 'assistant',
571
+ tool_calls: [
572
+ {
573
+ index: 0,
574
+ id: 'call_1',
575
+ type: 'function',
576
+ function: { name: 'lookup', arguments: '{}' },
577
+ },
578
+ ],
579
+ },
580
+ finish_reason: null,
581
+ logprobs: null,
582
+ },
583
+ ],
584
+ };
585
+ const model = new CapturingChatDeepSeek(
586
+ {
587
+ apiKey: 'test-key',
588
+ model: 'deepseek-v4-pro',
589
+ streaming: true,
590
+ _lc_stream_delay: 150,
591
+ },
592
+ [createContentChunk('alpha beta'), toolCallChunk]
593
+ );
594
+ const arrivals: { text: string; hasToolCall: boolean; at: number }[] = [];
595
+
596
+ for await (const chunk of model.streamChunksWithSignal(
597
+ new AbortController().signal
598
+ )) {
599
+ const message = chunk.message as Partial<AIMessage>;
600
+ arrivals.push({
601
+ text: chunk.text,
602
+ hasToolCall: (message.tool_calls?.length ?? 0) > 0,
603
+ at: Date.now(),
604
+ });
605
+ }
606
+
607
+ const toolArrival = arrivals.find((entry) => entry.hasToolCall);
608
+ const lastText = [...arrivals].reverse().find((entry) => entry.text !== '');
609
+ expect(toolArrival).toBeDefined();
610
+ expect(lastText).toBeDefined();
611
+ expect(
612
+ (toolArrival as { at: number }).at - (lastText as { at: number }).at
613
+ ).toBeLessThan(100);
614
+ });
615
+
560
616
  it('keeps delayed DeepSeek logprob chunks intact', async () => {
561
617
  const logprobs = { content: [], refusal: null } as NonNullable<
562
618
  OpenAIChatCompletionChunk['choices'][number]['logprobs']