@librechat/agents 3.3.12 → 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 (109) 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/instrumentation.cjs +1 -0
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuseSpanRegistry.cjs +6 -3
  6. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/index.cjs +35 -206
  8. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +121 -241
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/index.cjs +19 -12
  12. package/dist/cjs/llm/google/index.cjs.map +1 -1
  13. package/dist/cjs/llm/mistral/index.cjs +26 -0
  14. package/dist/cjs/llm/mistral/index.cjs.map +1 -0
  15. package/dist/cjs/llm/openai/index.cjs +82 -80
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  18. package/dist/cjs/llm/providers.cjs +19 -19
  19. package/dist/cjs/llm/providers.cjs.map +1 -1
  20. package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
  21. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
  22. package/dist/cjs/llm/stream/smoother.cjs +369 -0
  23. package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
  24. package/dist/cjs/llm/vertexai/index.cjs +13 -1
  25. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  26. package/dist/cjs/main.cjs +18 -10
  27. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +34 -2
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  29. package/dist/cjs/utils/tokens.cjs +76 -29
  30. package/dist/cjs/utils/tokens.cjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +10 -0
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/instrumentation.mjs +1 -0
  34. package/dist/esm/instrumentation.mjs.map +1 -1
  35. package/dist/esm/langfuseSpanRegistry.mjs +6 -3
  36. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  37. package/dist/esm/llm/anthropic/index.mjs +34 -205
  38. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  39. package/dist/esm/llm/bedrock/index.mjs +120 -240
  40. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  41. package/dist/esm/llm/google/index.mjs +19 -12
  42. package/dist/esm/llm/google/index.mjs.map +1 -1
  43. package/dist/esm/llm/mistral/index.mjs +26 -0
  44. package/dist/esm/llm/mistral/index.mjs.map +1 -0
  45. package/dist/esm/llm/openai/index.mjs +82 -80
  46. package/dist/esm/llm/openai/index.mjs.map +1 -1
  47. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  48. package/dist/esm/llm/providers.mjs +3 -3
  49. package/dist/esm/llm/providers.mjs.map +1 -1
  50. package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
  51. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
  52. package/dist/esm/llm/stream/smoother.mjs +365 -0
  53. package/dist/esm/llm/stream/smoother.mjs.map +1 -0
  54. package/dist/esm/llm/vertexai/index.mjs +13 -1
  55. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  56. package/dist/esm/main.mjs +4 -2
  57. package/dist/esm/tools/subagent/SubagentExecutor.mjs +34 -2
  58. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  59. package/dist/esm/utils/tokens.mjs +76 -30
  60. package/dist/esm/utils/tokens.mjs.map +1 -1
  61. package/dist/types/graphs/Graph.d.ts +3 -0
  62. package/dist/types/index.d.ts +3 -0
  63. package/dist/types/llm/google/index.d.ts +2 -0
  64. package/dist/types/llm/mistral/index.d.ts +11 -0
  65. package/dist/types/llm/openai/index.d.ts +20 -4
  66. package/dist/types/llm/openrouter/index.d.ts +4 -1
  67. package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
  68. package/dist/types/llm/stream/smoother.d.ts +95 -0
  69. package/dist/types/llm/vertexai/index.d.ts +2 -0
  70. package/dist/types/tools/subagent/SubagentExecutor.d.ts +3 -0
  71. package/dist/types/types/graph.d.ts +5 -0
  72. package/dist/types/types/llm.d.ts +21 -14
  73. package/dist/types/utils/tokens.d.ts +10 -0
  74. package/package.json +2 -2
  75. package/src/graphs/Graph.ts +11 -0
  76. package/src/index.ts +8 -0
  77. package/src/instrumentation.ts +1 -0
  78. package/src/langfuseSpanRegistry.ts +9 -0
  79. package/src/llm/anthropic/index.ts +85 -354
  80. package/src/llm/bedrock/index.ts +174 -390
  81. package/src/llm/bedrock/llm.spec.ts +2 -0
  82. package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
  83. package/src/llm/custom-chat-models.smoke.test.ts +16 -1
  84. package/src/llm/google/index.ts +17 -18
  85. package/src/llm/google/streamSmoothing.test.ts +121 -0
  86. package/src/llm/mistral/index.ts +33 -0
  87. package/src/llm/mistral/streamSmoothing.test.ts +97 -0
  88. package/src/llm/openai/deepseek.test.ts +56 -0
  89. package/src/llm/openai/index.ts +119 -126
  90. package/src/llm/openrouter/index.ts +4 -1
  91. package/src/llm/providers.ts +3 -3
  92. package/src/llm/stream/chunkAdapters.test.ts +202 -0
  93. package/src/llm/stream/chunkAdapters.ts +317 -0
  94. package/src/llm/stream/reassembly.test.ts +241 -0
  95. package/src/llm/stream/smoother.bench.test.ts +155 -0
  96. package/src/llm/stream/smoother.test.ts +519 -0
  97. package/src/llm/stream/smoother.ts +574 -0
  98. package/src/llm/vertexai/index.ts +17 -1
  99. package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
  100. package/src/specs/langfuse-instrumentation.test.ts +26 -0
  101. package/src/specs/langfuse-span-registry.test.ts +17 -0
  102. package/src/specs/subagent.test.ts +44 -0
  103. package/src/specs/summarization.test.ts +5 -2
  104. package/src/specs/tokens.test.ts +159 -14
  105. package/src/tools/__tests__/SubagentExecutor.test.ts +48 -1
  106. package/src/tools/subagent/SubagentExecutor.ts +69 -5
  107. package/src/types/graph.ts +5 -0
  108. package/src/types/llm.ts +53 -36
  109. package/src/utils/tokens.ts +115 -30
@@ -0,0 +1,574 @@
1
+ export const DEFAULT_STREAM_DELAY = 25;
2
+ export const SMOOTH_TARGET_LATENCY_MS = 250;
3
+ export const MAX_STREAM_QUEUE_CHUNKS = 256;
4
+ export const MAX_STREAM_QUEUE_TEXT_CHARS = 8192;
5
+ export const MAX_SMOOTH_ITEM_SEGMENT_CHARS = 4096;
6
+ export const STREAM_CHUNK_MIN_SIZE = 4;
7
+ export const STREAM_BOUNDARIES: ReadonlySet<string> = new Set([
8
+ ' ',
9
+ '.',
10
+ ',',
11
+ '!',
12
+ '?',
13
+ ';',
14
+ ':',
15
+ ]);
16
+
17
+ export const STREAM_ABORT_MESSAGE = 'AbortError: User aborted the request.';
18
+ export const STREAM_PRODUCER_FAILURE = 'Stream producer failed.';
19
+
20
+ /**
21
+ * How long generator teardown waits for the background producer to observe a
22
+ * consumer close before abandoning it. Well-behaved streams settle in
23
+ * microseconds (the next enqueue throws); a stalled provider that ignores
24
+ * aborts otherwise blocks teardown — and abort propagation — indefinitely.
25
+ * An abandoned producer still self-terminates on its next enqueue attempt.
26
+ */
27
+ export const PRODUCER_CLOSE_GRACE_MS = 1000;
28
+
29
+ /**
30
+ * Resolves a configured stream delay to its effective value (default 25ms;
31
+ * 0 disables smoothing). Non-finite inputs (NaN from a malformed config
32
+ * value, ±Infinity) normalize to the default rather than poisoning piece
33
+ * arithmetic downstream.
34
+ */
35
+ export function resolveStreamDelay(delay?: number): number {
36
+ if (delay == null || !Number.isFinite(delay)) {
37
+ return DEFAULT_STREAM_DELAY;
38
+ }
39
+ return Math.max(0, delay);
40
+ }
41
+
42
+ export function isSignalAborted(signal?: AbortSignal): boolean {
43
+ return signal?.aborted === true;
44
+ }
45
+
46
+ /**
47
+ * How far past the target size the word-boundary search may extend before
48
+ * hard-cutting. Natural language hits a boundary within a few characters;
49
+ * boundary-free runs (base64, minified data, long identifiers) must not
50
+ * stretch a piece — or an admission segment — arbitrarily far past its
51
+ * budget.
52
+ */
53
+ export const STREAM_BOUNDARY_LOOKAHEAD_CHARS = 64;
54
+
55
+ export function findStreamChunkBoundary(
56
+ text: string,
57
+ minSize: number
58
+ ): number {
59
+ if (minSize >= text.length) {
60
+ return text.length;
61
+ }
62
+
63
+ const scanEnd = Math.min(
64
+ text.length,
65
+ minSize + STREAM_BOUNDARY_LOOKAHEAD_CHARS
66
+ );
67
+ for (let position = minSize; position < scanEnd; position++) {
68
+ if (STREAM_BOUNDARIES.has(text[position])) {
69
+ return position + 1;
70
+ }
71
+ }
72
+
73
+ return scanEnd;
74
+ }
75
+
76
+ /**
77
+ * Backlog-proportional piece sizing: emit enough per tick that the current
78
+ * backlog drains in ~`targetLatencyMs`, so render lag stays pinned near the
79
+ * target regardless of how fast the provider streams. Token-sized arrivals
80
+ * never exceed the minimum piece, matching the legacy fixed-size splitter.
81
+ */
82
+ export function computeAdaptivePieceSize(
83
+ bufferedTextLength: number,
84
+ tickMs: number,
85
+ targetLatencyMs: number = SMOOTH_TARGET_LATENCY_MS
86
+ ): number {
87
+ if (bufferedTextLength <= 0) {
88
+ return STREAM_CHUNK_MIN_SIZE;
89
+ }
90
+ if (tickMs <= 0 || targetLatencyMs <= 0) {
91
+ return bufferedTextLength;
92
+ }
93
+ return Math.max(
94
+ STREAM_CHUNK_MIN_SIZE,
95
+ Math.ceil((bufferedTextLength * tickMs) / targetLatencyMs)
96
+ );
97
+ }
98
+
99
+ /**
100
+ * A cadence, not an additive sleep: time the consumer already spent since the
101
+ * last visible emission counts against the target delay, so slow downstream
102
+ * handlers never compound latency.
103
+ */
104
+ export function getCadencedStreamDelay({
105
+ targetDelay,
106
+ lastVisibleTextAt,
107
+ now,
108
+ }: {
109
+ targetDelay: number;
110
+ lastVisibleTextAt?: number;
111
+ now: number;
112
+ }): number {
113
+ if (targetDelay <= 0 || lastVisibleTextAt == null) {
114
+ return 0;
115
+ }
116
+ return Math.max(0, targetDelay - (now - lastVisibleTextAt));
117
+ }
118
+
119
+ /** Abort-aware sleep that resolves (never rejects) on abort; callers re-check the signal. */
120
+ export async function waitForStreamDelay(
121
+ delay: number,
122
+ signal?: AbortSignal
123
+ ): Promise<void> {
124
+ if (delay <= 0 || isSignalAborted(signal)) {
125
+ return;
126
+ }
127
+ await new Promise<void>((resolve) => {
128
+ const timeoutRef: { current?: ReturnType<typeof setTimeout> } = {};
129
+ const onAbort = (): void => {
130
+ if (timeoutRef.current) {
131
+ clearTimeout(timeoutRef.current);
132
+ }
133
+ signal?.removeEventListener('abort', onAbort);
134
+ resolve();
135
+ };
136
+ timeoutRef.current = setTimeout(() => {
137
+ signal?.removeEventListener('abort', onAbort);
138
+ resolve();
139
+ }, delay);
140
+ signal?.addEventListener('abort', onAbort, { once: true });
141
+ if (isSignalAborted(signal)) {
142
+ onAbort();
143
+ }
144
+ });
145
+ }
146
+
147
+ export type SmoothPiece = {
148
+ text: string;
149
+ isFirst: boolean;
150
+ isLast: boolean;
151
+ };
152
+
153
+ /**
154
+ * One classified unit of provider stream output.
155
+ *
156
+ * - `smooth: true` — visible text, paced at the configured cadence and (unless
157
+ * `atomic`) sliced adaptively at dequeue time.
158
+ * - `atomic: true` — paced as a single piece, never split (text-bearing chunks
159
+ * whose metadata cannot survive slicing, e.g. logprobs / finish_reason).
160
+ * - `smooth: false` — passthrough: tool-call deltas, usage-only, id-only and
161
+ * seal chunks. Zero delay, strict FIFO with the text around them.
162
+ *
163
+ * `emit` builds the provider-specific output for one piece; `isFirst` lets
164
+ * providers keep usage_metadata on only the first piece of a split.
165
+ */
166
+ export type SmoothItem<TEmit> = {
167
+ text: string;
168
+ smooth: boolean;
169
+ atomic?: boolean;
170
+ emit: (piece: SmoothPiece) => TEmit;
171
+ };
172
+
173
+ type ProducerState = {
174
+ done: boolean;
175
+ failed: boolean;
176
+ error?: unknown;
177
+ };
178
+
179
+ type QueuedSmoothItem<TEmit> = {
180
+ item: SmoothItem<TEmit>;
181
+ textLength: number;
182
+ };
183
+
184
+ /**
185
+ * Bounded producer/consumer smoothing engine.
186
+ *
187
+ * The producer drains `source` eagerly into a bounded queue (the buffer is the
188
+ * backlog measurement adaptive sizing needs); at capacity it parks, applying
189
+ * backpressure to the underlying stream. The consumer emits paced pieces,
190
+ * decrementing the text budget and waking the producer *before* each cadenced
191
+ * sleep so the provider stream keeps being read during pacing.
192
+ *
193
+ * `delayMs <= 0` disables smoothing entirely: every item passes through FIFO,
194
+ * unsplit and undelayed.
195
+ */
196
+ export async function* smoothStream<TEmit>({
197
+ source,
198
+ delayMs,
199
+ signal,
200
+ abortUpstream,
201
+ }: {
202
+ source: AsyncIterable<SmoothItem<TEmit>>;
203
+ delayMs: number;
204
+ signal?: AbortSignal;
205
+ abortUpstream?: () => void;
206
+ }): AsyncGenerator<TEmit> {
207
+ if (!(delayMs > 0)) {
208
+ /** Disabled smoothing preserves fully lazy streaming: no background
209
+ * producer, no read-ahead — each provider chunk is pulled only when the
210
+ * consumer asks, exactly like the pre-engine pass-through paths. */
211
+ for await (const item of source) {
212
+ if (isSignalAborted(signal)) {
213
+ abortUpstream?.();
214
+ throw new Error(STREAM_ABORT_MESSAGE);
215
+ }
216
+ yield item.emit({ text: item.text, isFirst: true, isLast: true });
217
+ }
218
+ return;
219
+ }
220
+
221
+ const queuedItems: QueuedSmoothItem<TEmit>[] = [];
222
+ const producerState: ProducerState = { done: false, failed: false };
223
+ let queuedItemIndex = 0;
224
+ let bufferedTextLength = 0;
225
+ let consumerClosed = false;
226
+ let notifyConsumer: (() => void) | undefined;
227
+ let notifyProducer: (() => void) | undefined;
228
+
229
+ const notifyConsumerForItem = (): void => {
230
+ notifyConsumer?.();
231
+ notifyConsumer = undefined;
232
+ };
233
+
234
+ const notifyProducerForSpace = (): void => {
235
+ notifyProducer?.();
236
+ notifyProducer = undefined;
237
+ };
238
+
239
+ const hasQueuedItems = (): boolean => queuedItemIndex < queuedItems.length;
240
+
241
+ const getQueuedItemCount = (): number =>
242
+ queuedItems.length - queuedItemIndex;
243
+
244
+ const isQueueAtCapacity = (): boolean =>
245
+ getQueuedItemCount() >= MAX_STREAM_QUEUE_CHUNKS ||
246
+ bufferedTextLength >= MAX_STREAM_QUEUE_TEXT_CHARS;
247
+
248
+ /** Abort-aware: a consumer parked on an empty queue must wake when the
249
+ * signal fires even if the provider stream never honors the abort — the
250
+ * loop's top-of-iteration check then throws the canonical error. */
251
+ const waitForNextItem = async (): Promise<void> => {
252
+ if (
253
+ hasQueuedItems() ||
254
+ producerState.done ||
255
+ producerState.failed ||
256
+ isSignalAborted(signal)
257
+ ) {
258
+ return;
259
+ }
260
+ await new Promise<void>((resolve) => {
261
+ const onAbort = (): void => {
262
+ signal?.removeEventListener('abort', onAbort);
263
+ resolve();
264
+ };
265
+ notifyConsumer = (): void => {
266
+ signal?.removeEventListener('abort', onAbort);
267
+ resolve();
268
+ };
269
+ signal?.addEventListener('abort', onAbort, { once: true });
270
+ if (isSignalAborted(signal)) {
271
+ onAbort();
272
+ }
273
+ });
274
+ };
275
+
276
+ const waitForQueueSpace = async (): Promise<void> => {
277
+ while (
278
+ isQueueAtCapacity() &&
279
+ !consumerClosed &&
280
+ !isSignalAborted(signal)
281
+ ) {
282
+ await new Promise<void>((resolve) => {
283
+ const onAbort = (): void => {
284
+ signal?.removeEventListener('abort', onAbort);
285
+ resolve();
286
+ };
287
+ const onSpace = (): void => {
288
+ signal?.removeEventListener('abort', onAbort);
289
+ resolve();
290
+ };
291
+ notifyProducer = onSpace;
292
+ signal?.addEventListener('abort', onAbort, { once: true });
293
+ if (isSignalAborted(signal)) {
294
+ onAbort();
295
+ }
296
+ });
297
+ }
298
+ };
299
+
300
+ const dequeue = (): QueuedSmoothItem<TEmit> | undefined => {
301
+ if (!hasQueuedItems()) {
302
+ return undefined;
303
+ }
304
+ const queuedItem = queuedItems[queuedItemIndex];
305
+ queuedItemIndex++;
306
+ if (queuedItemIndex > 128 && queuedItemIndex * 2 >= queuedItems.length) {
307
+ queuedItems.splice(0, queuedItemIndex);
308
+ queuedItemIndex = 0;
309
+ }
310
+ return queuedItem;
311
+ };
312
+
313
+ const throwAborted = (): never => {
314
+ abortUpstream?.();
315
+ throw new Error(STREAM_ABORT_MESSAGE);
316
+ };
317
+
318
+ const enqueue = async (item: SmoothItem<TEmit>): Promise<void> => {
319
+ await waitForQueueSpace();
320
+ if (consumerClosed || isSignalAborted(signal)) {
321
+ throwAborted();
322
+ }
323
+ const textLength = item.smooth ? item.text.length : 0;
324
+ queuedItems.push({ item, textLength });
325
+ bufferedTextLength += textLength;
326
+ notifyConsumerForItem();
327
+ };
328
+
329
+ /**
330
+ * Oversized splittable items are segmented at admission so a single giant
331
+ * provider chunk cannot blow past the text budget: each segment re-checks
332
+ * capacity, so the producer parks mid-chunk once the buffer fills — the
333
+ * same bound the legacy split-before-enqueue queues enforced. The wrapped
334
+ * emit maps segment-local pieces back to chunk-global isFirst/isLast so
335
+ * provider clone contracts are unaffected.
336
+ */
337
+ const enqueueSegmented = async (item: SmoothItem<TEmit>): Promise<void> => {
338
+ if (
339
+ !item.smooth ||
340
+ item.atomic === true ||
341
+ item.text.length <= MAX_SMOOTH_ITEM_SEGMENT_CHARS
342
+ ) {
343
+ await enqueue(item);
344
+ return;
345
+ }
346
+
347
+ const segments: { start: number; end: number }[] = [];
348
+ let offset = 0;
349
+ while (offset < item.text.length) {
350
+ const end =
351
+ offset +
352
+ findStreamChunkBoundary(
353
+ item.text.slice(offset),
354
+ MAX_SMOOTH_ITEM_SEGMENT_CHARS
355
+ );
356
+ segments.push({ start: offset, end });
357
+ offset = end;
358
+ }
359
+
360
+ for (let i = 0; i < segments.length; i++) {
361
+ const isFirstSegment = i === 0;
362
+ const isLastSegment = i === segments.length - 1;
363
+ await enqueue({
364
+ text: item.text.slice(segments[i].start, segments[i].end),
365
+ smooth: true,
366
+ emit: (piece) =>
367
+ item.emit({
368
+ text: piece.text,
369
+ isFirst: isFirstSegment && piece.isFirst,
370
+ isLast: isLastSegment && piece.isLast,
371
+ }),
372
+ });
373
+ }
374
+ };
375
+
376
+ const producer = (async (): Promise<void> => {
377
+ try {
378
+ for await (const item of source) {
379
+ if (isSignalAborted(signal)) {
380
+ throwAborted();
381
+ }
382
+ await enqueueSegmented(item);
383
+ }
384
+ } catch (error) {
385
+ producerState.failed = true;
386
+ producerState.error = error;
387
+ } finally {
388
+ producerState.done = true;
389
+ notifyConsumerForItem();
390
+ }
391
+ })();
392
+
393
+ let hasEmittedText = false;
394
+ let lastVisibleTextAt: number | undefined;
395
+ let drainTicksRemaining: number | undefined;
396
+ let current: QueuedSmoothItem<TEmit> | undefined;
397
+ let headOffset = 0;
398
+ let keepStreaming = true;
399
+ try {
400
+ while (keepStreaming) {
401
+ if (isSignalAborted(signal)) {
402
+ throwAborted();
403
+ }
404
+
405
+ if (current == null) {
406
+ await waitForNextItem();
407
+ current = dequeue();
408
+ headOffset = 0;
409
+ }
410
+
411
+ if (current == null) {
412
+ if (producerState.failed) {
413
+ throw producerState.error ?? new Error(STREAM_PRODUCER_FAILURE);
414
+ }
415
+ if (producerState.done) {
416
+ keepStreaming = false;
417
+ }
418
+ continue;
419
+ }
420
+
421
+ const { item } = current;
422
+
423
+ if (!item.smooth) {
424
+ notifyProducerForSpace();
425
+ current = undefined;
426
+ yield item.emit({ text: item.text, isFirst: true, isLast: true });
427
+ continue;
428
+ }
429
+
430
+ if (item.text === '') {
431
+ bufferedTextLength = Math.max(
432
+ 0,
433
+ bufferedTextLength - current.textLength
434
+ );
435
+ notifyProducerForSpace();
436
+ current = undefined;
437
+ continue;
438
+ }
439
+
440
+ /** Once the producer is done the backlog is final: drain it linearly
441
+ * across the remaining target window instead of letting the
442
+ * proportional formula decay geometrically and stretch the tail. */
443
+ if (producerState.done && drainTicksRemaining == null) {
444
+ drainTicksRemaining = Math.max(
445
+ 1,
446
+ Math.floor(SMOOTH_TARGET_LATENCY_MS / delayMs)
447
+ );
448
+ }
449
+ const tickBudget =
450
+ drainTicksRemaining != null
451
+ ? Math.max(
452
+ STREAM_CHUNK_MIN_SIZE,
453
+ Math.ceil(bufferedTextLength / drainTicksRemaining)
454
+ )
455
+ : computeAdaptivePieceSize(bufferedTextLength, delayMs);
456
+ if (drainTicksRemaining != null && drainTicksRemaining > 1) {
457
+ drainTicksRemaining -= 1;
458
+ }
459
+
460
+ await waitForStreamDelay(
461
+ getCadencedStreamDelay({
462
+ targetDelay: hasEmittedText ? delayMs : 0,
463
+ lastVisibleTextAt,
464
+ now: Date.now(),
465
+ }),
466
+ signal
467
+ );
468
+ if (isSignalAborted(signal)) {
469
+ throwAborted();
470
+ }
471
+ hasEmittedText = true;
472
+ lastVisibleTextAt = Date.now();
473
+
474
+ if (item.atomic === true) {
475
+ bufferedTextLength = Math.max(
476
+ 0,
477
+ bufferedTextLength - current.textLength
478
+ );
479
+ notifyProducerForSpace();
480
+ current = undefined;
481
+ yield item.emit({ text: item.text, isFirst: true, isLast: true });
482
+ continue;
483
+ }
484
+
485
+ /** One cadence tick drains up to the adaptive budget ACROSS queued
486
+ * items, so token-sized provider deltas coalesce instead of costing a
487
+ * full tick each; passthrough items flush free mid-batch (FIFO), and
488
+ * atomic items end the batch to take their own tick. */
489
+ let consumed = 0;
490
+ while (consumed < tickBudget) {
491
+ if (isSignalAborted(signal)) {
492
+ throwAborted();
493
+ }
494
+ if (current == null) {
495
+ if (!hasQueuedItems()) {
496
+ break;
497
+ }
498
+ current = dequeue();
499
+ headOffset = 0;
500
+ if (current == null) {
501
+ break;
502
+ }
503
+ }
504
+
505
+ const batchItem = current.item;
506
+ if (!batchItem.smooth) {
507
+ notifyProducerForSpace();
508
+ current = undefined;
509
+ yield batchItem.emit({
510
+ text: batchItem.text,
511
+ isFirst: true,
512
+ isLast: true,
513
+ });
514
+ continue;
515
+ }
516
+ if (batchItem.text === '') {
517
+ bufferedTextLength = Math.max(
518
+ 0,
519
+ bufferedTextLength - current.textLength
520
+ );
521
+ notifyProducerForSpace();
522
+ current = undefined;
523
+ continue;
524
+ }
525
+ if (batchItem.atomic === true) {
526
+ break;
527
+ }
528
+
529
+ const remainingText = batchItem.text.slice(headOffset);
530
+ const pieceLength = findStreamChunkBoundary(
531
+ remainingText,
532
+ tickBudget - consumed
533
+ );
534
+ const pieceEnd = headOffset + pieceLength;
535
+ const piece = batchItem.text.slice(headOffset, pieceEnd);
536
+ const isFirst = headOffset === 0;
537
+ const isLast = pieceEnd === batchItem.text.length;
538
+
539
+ bufferedTextLength = Math.max(0, bufferedTextLength - piece.length);
540
+ notifyProducerForSpace();
541
+ if (isLast) {
542
+ current = undefined;
543
+ } else {
544
+ headOffset = pieceEnd;
545
+ }
546
+ consumed += piece.length;
547
+ yield batchItem.emit({ text: piece, isFirst, isLast });
548
+ }
549
+ }
550
+ } finally {
551
+ consumerClosed = true;
552
+ if (producerState.done) {
553
+ await producer;
554
+ } else {
555
+ abortUpstream?.();
556
+ notifyProducerForSpace();
557
+ const iterator = source as Partial<AsyncGenerator<SmoothItem<TEmit>>>;
558
+ const closing = iterator.return?.call(source, undefined as never);
559
+ if (closing != null) {
560
+ void closing.then(
561
+ () => undefined,
562
+ () => undefined
563
+ );
564
+ }
565
+ await Promise.race([
566
+ producer,
567
+ new Promise<void>((resolve) => {
568
+ const timeout = setTimeout(resolve, PRODUCER_CLOSE_GRACE_MS);
569
+ timeout.unref();
570
+ }),
571
+ ]);
572
+ }
573
+ }
574
+ }
@@ -16,6 +16,8 @@ import {
16
16
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
17
17
  GOOGLE_STREAMED_TOOL_CALL_ADAPTER,
18
18
  } from '@/tools/streamedToolCallSeals';
19
+ import { smoothGenerationChunks } from '@/llm/stream/chunkAdapters';
20
+ import { resolveStreamDelay } from '@/llm/stream/smoother';
19
21
 
20
22
  /**
21
23
  * `@langchain/google-common`'s `_streamResponseChunks` emits usage on TWO
@@ -474,6 +476,7 @@ class CustomChatConnection extends ChatConnection<VertexAIClientOptions> {
474
476
  */
475
477
  export class ChatVertexAI extends ChatGoogle {
476
478
  lc_namespace = ['langchain', 'chat_models', 'vertexai'];
479
+ _lc_stream_delay: number;
477
480
  dynamicThinkingBudget = false;
478
481
  thinkingConfig?: GoogleThinkingConfig;
479
482
 
@@ -498,6 +501,7 @@ export class ChatVertexAI extends ChatGoogle {
498
501
  });
499
502
  this.dynamicThinkingBudget = dynamicThinkingBudget;
500
503
  this.thinkingConfig = fields?.thinkingConfig;
504
+ this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
501
505
  }
502
506
  invocationParams(
503
507
  options?: this['ParsedCallOptions'] | undefined
@@ -512,12 +516,24 @@ export class ChatVertexAI extends ChatGoogle {
512
516
  messages: BaseMessage[],
513
517
  options: this['ParsedCallOptions'],
514
518
  runManager?: CallbackManagerForLLMRun
519
+ ): AsyncGenerator<ChatGenerationChunk> {
520
+ yield* smoothGenerationChunks({
521
+ chunks: this._streamRepairedChunks(messages, options),
522
+ delayMs: this._lc_stream_delay,
523
+ signal: options.signal,
524
+ runManager,
525
+ });
526
+ }
527
+
528
+ private async *_streamRepairedChunks(
529
+ messages: BaseMessage[],
530
+ options: this['ParsedCallOptions']
515
531
  ): AsyncGenerator<ChatGenerationChunk> {
516
532
  let lastGoodUsage: UsageMetadata | undefined;
517
533
  for await (const chunk of super._streamResponseChunks(
518
534
  messages,
519
535
  options,
520
- runManager
536
+ undefined
521
537
  )) {
522
538
  const genUsage = (
523
539
  chunk.generationInfo as { usage_metadata?: UsageMetadata } | undefined