ai.matey.utils 0.2.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/conversation-history.js +139 -0
  3. package/dist/cjs/conversation-history.js.map +1 -0
  4. package/dist/cjs/index.js +42 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/model-cache.js +163 -0
  7. package/dist/cjs/model-cache.js.map +1 -0
  8. package/dist/cjs/parameter-normalizer.js +451 -0
  9. package/dist/cjs/parameter-normalizer.js.map +1 -0
  10. package/dist/cjs/streaming-modes.js +277 -0
  11. package/dist/cjs/streaming-modes.js.map +1 -0
  12. package/dist/cjs/streaming.js +892 -0
  13. package/dist/cjs/streaming.js.map +1 -0
  14. package/dist/cjs/structured-output.js +398 -0
  15. package/dist/cjs/structured-output.js.map +1 -0
  16. package/dist/cjs/system-message.js +222 -0
  17. package/dist/cjs/system-message.js.map +1 -0
  18. package/dist/cjs/validation.js +534 -0
  19. package/dist/cjs/validation.js.map +1 -0
  20. package/dist/cjs/warnings.js +301 -0
  21. package/dist/cjs/warnings.js.map +1 -0
  22. package/dist/esm/conversation-history.js +134 -0
  23. package/dist/esm/conversation-history.js.map +1 -0
  24. package/dist/esm/index.js +26 -0
  25. package/dist/esm/index.js.map +1 -0
  26. package/dist/esm/model-cache.js +158 -0
  27. package/dist/esm/model-cache.js.map +1 -0
  28. package/dist/esm/parameter-normalizer.js +434 -0
  29. package/dist/esm/parameter-normalizer.js.map +1 -0
  30. package/dist/esm/streaming-modes.js +265 -0
  31. package/dist/esm/streaming-modes.js.map +1 -0
  32. package/dist/esm/streaming.js +860 -0
  33. package/dist/esm/streaming.js.map +1 -0
  34. package/dist/esm/structured-output.js +387 -0
  35. package/dist/esm/structured-output.js.map +1 -0
  36. package/dist/esm/system-message.js +213 -0
  37. package/dist/esm/system-message.js.map +1 -0
  38. package/dist/esm/validation.js +523 -0
  39. package/dist/esm/validation.js.map +1 -0
  40. package/dist/esm/warnings.js +284 -0
  41. package/dist/esm/warnings.js.map +1 -0
  42. package/dist/types/conversation-history.d.ts +70 -0
  43. package/dist/types/conversation-history.d.ts.map +1 -0
  44. package/dist/types/index.d.ts +17 -0
  45. package/dist/types/index.d.ts.map +1 -0
  46. package/dist/types/model-cache.d.ts +88 -0
  47. package/dist/types/model-cache.d.ts.map +1 -0
  48. package/dist/types/parameter-normalizer.d.ts +154 -0
  49. package/dist/types/parameter-normalizer.d.ts.map +1 -0
  50. package/dist/types/streaming-modes.d.ts +139 -0
  51. package/dist/types/streaming-modes.d.ts.map +1 -0
  52. package/dist/types/streaming.d.ts +384 -0
  53. package/dist/types/streaming.d.ts.map +1 -0
  54. package/dist/types/structured-output.d.ts +157 -0
  55. package/dist/types/structured-output.d.ts.map +1 -0
  56. package/dist/types/system-message.d.ts +78 -0
  57. package/dist/types/system-message.d.ts.map +1 -0
  58. package/dist/types/validation.d.ts +46 -0
  59. package/dist/types/validation.d.ts.map +1 -0
  60. package/dist/types/warnings.d.ts +149 -0
  61. package/dist/types/warnings.d.ts.map +1 -0
  62. package/package.json +75 -0
  63. package/readme.md +280 -0
@@ -0,0 +1,892 @@
1
+ "use strict";
2
+ /**
3
+ * General Stream Utilities
4
+ *
5
+ * This module provides general-purpose stream transformation utilities.
6
+ * Use these for mapping, filtering, collecting, and error handling in streams.
7
+ *
8
+ * For delta/accumulated mode conversion, see ./streaming-modes.ts
9
+ *
10
+ * @example Accumulating stream content
11
+ * ```typescript
12
+ * import { createStreamAccumulator, accumulateChunk } from 'ai.matey.utils';
13
+ *
14
+ * const accumulator = createStreamAccumulator();
15
+ * for await (const chunk of stream) {
16
+ * const updated = accumulateChunk(accumulator, chunk);
17
+ * console.log('Accumulated so far:', updated.content);
18
+ * }
19
+ * ```
20
+ *
21
+ * @module streaming
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.createStreamAccumulator = createStreamAccumulator;
25
+ exports.accumulateChunk = accumulateChunk;
26
+ exports.accumulatorToMessage = accumulatorToMessage;
27
+ exports.accumulatorToResponse = accumulatorToResponse;
28
+ exports.transformStream = transformStream;
29
+ exports.filterStream = filterStream;
30
+ exports.mapStream = mapStream;
31
+ exports.tapStream = tapStream;
32
+ exports.collectStream = collectStream;
33
+ exports.streamToResponse = streamToResponse;
34
+ exports.streamToText = streamToText;
35
+ exports.splitStream = splitStream;
36
+ exports.catchStreamErrors = catchStreamErrors;
37
+ exports.streamWithTimeout = streamWithTimeout;
38
+ exports.isContentChunk = isContentChunk;
39
+ exports.isDoneChunk = isDoneChunk;
40
+ exports.isErrorChunk = isErrorChunk;
41
+ exports.getContentDeltas = getContentDeltas;
42
+ exports.streamToTextIterator = streamToTextIterator;
43
+ exports.validateChunkSequence = validateChunkSequence;
44
+ exports.validateStream = validateStream;
45
+ exports.assembleStreamChunks = assembleStreamChunks;
46
+ exports.createStreamError = createStreamError;
47
+ exports.streamWithBackpressure = streamWithBackpressure;
48
+ exports.rateLimitStream = rateLimitStream;
49
+ exports.collectStreamFull = collectStreamFull;
50
+ exports.processStream = processStream;
51
+ exports.streamToLines = streamToLines;
52
+ exports.throttleStream = throttleStream;
53
+ exports.teeStream = teeStream;
54
+ /**
55
+ * Create a new stream accumulator.
56
+ *
57
+ * @returns Empty accumulator
58
+ */
59
+ function createStreamAccumulator() {
60
+ return {
61
+ content: '',
62
+ role: 'assistant',
63
+ sequence: 0,
64
+ };
65
+ }
66
+ /**
67
+ * Accumulate a stream chunk.
68
+ *
69
+ * @param accumulator Current accumulator state
70
+ * @param chunk Stream chunk to accumulate
71
+ * @returns Updated accumulator
72
+ */
73
+ function accumulateChunk(accumulator, chunk) {
74
+ const updated = { ...accumulator, sequence: chunk.sequence };
75
+ switch (chunk.type) {
76
+ case 'content':
77
+ updated.content += chunk.delta;
78
+ break;
79
+ case 'metadata':
80
+ updated.metadata = {
81
+ ...updated.metadata,
82
+ ...chunk.metadata,
83
+ };
84
+ break;
85
+ default:
86
+ // Other chunk types don't affect accumulation
87
+ break;
88
+ }
89
+ return updated;
90
+ }
91
+ /**
92
+ * Convert accumulated state to IR message.
93
+ *
94
+ * @param accumulator Stream accumulator
95
+ * @returns IR message
96
+ */
97
+ function accumulatorToMessage(accumulator) {
98
+ return {
99
+ role: accumulator.role,
100
+ content: accumulator.content,
101
+ };
102
+ }
103
+ /**
104
+ * Convert accumulated state and done chunk to IR response.
105
+ *
106
+ * @param accumulator Stream accumulator
107
+ * @param doneChunk Done chunk with finish reason and usage
108
+ * @param requestMetadata Original request metadata
109
+ * @returns IR chat response
110
+ */
111
+ function accumulatorToResponse(accumulator, doneChunk, requestMetadata) {
112
+ return {
113
+ message: accumulatorToMessage(accumulator),
114
+ finishReason: doneChunk.finishReason,
115
+ usage: doneChunk.usage,
116
+ metadata: {
117
+ ...requestMetadata,
118
+ ...accumulator.metadata,
119
+ },
120
+ };
121
+ }
122
+ // ============================================================================
123
+ // Stream Transformation
124
+ // ============================================================================
125
+ /**
126
+ * Transform stream chunks.
127
+ *
128
+ * @param stream Original stream
129
+ * @param transformer Transform function
130
+ * @returns Transformed stream
131
+ */
132
+ async function* transformStream(stream, transformer) {
133
+ for await (const chunk of stream) {
134
+ const transformed = transformer(chunk);
135
+ if (transformed !== null) {
136
+ yield transformed;
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Filter stream chunks.
142
+ *
143
+ * @param stream Original stream
144
+ * @param predicate Filter predicate
145
+ * @returns Filtered stream
146
+ */
147
+ async function* filterStream(stream, predicate) {
148
+ for await (const chunk of stream) {
149
+ if (predicate(chunk)) {
150
+ yield chunk;
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Map stream chunks.
156
+ *
157
+ * @param stream Original stream
158
+ * @param mapper Mapping function
159
+ * @returns Mapped stream
160
+ */
161
+ async function* mapStream(stream, mapper) {
162
+ for await (const chunk of stream) {
163
+ yield mapper(chunk);
164
+ }
165
+ }
166
+ /**
167
+ * Tap into stream without modifying it.
168
+ *
169
+ * @param stream Original stream
170
+ * @param callback Function to call for each chunk
171
+ * @returns Same stream
172
+ */
173
+ async function* tapStream(stream, callback) {
174
+ for await (const chunk of stream) {
175
+ await callback(chunk);
176
+ yield chunk;
177
+ }
178
+ }
179
+ // ============================================================================
180
+ // Stream Collection
181
+ // ============================================================================
182
+ /**
183
+ * Collect all chunks from a stream.
184
+ *
185
+ * @param stream Stream to collect
186
+ * @returns Array of all chunks
187
+ */
188
+ async function collectStream(stream) {
189
+ const chunks = [];
190
+ for await (const chunk of stream) {
191
+ chunks.push(chunk);
192
+ }
193
+ return chunks;
194
+ }
195
+ /**
196
+ * Collect stream into a complete response.
197
+ *
198
+ * @param stream Stream to collect
199
+ * @param requestMetadata Original request metadata
200
+ * @returns Complete IR response
201
+ */
202
+ async function streamToResponse(stream, requestMetadata) {
203
+ let accumulator = createStreamAccumulator();
204
+ let doneChunk;
205
+ for await (const chunk of stream) {
206
+ if (chunk.type === 'done') {
207
+ doneChunk = chunk;
208
+ }
209
+ else {
210
+ accumulator = accumulateChunk(accumulator, chunk);
211
+ }
212
+ }
213
+ if (!doneChunk) {
214
+ // Stream ended without done chunk - create default
215
+ doneChunk = {
216
+ type: 'done',
217
+ sequence: accumulator.sequence + 1,
218
+ finishReason: 'stop',
219
+ };
220
+ }
221
+ return accumulatorToResponse(accumulator, doneChunk, requestMetadata);
222
+ }
223
+ /**
224
+ * Collect just the content text from a stream.
225
+ *
226
+ * @param stream Stream to collect
227
+ * @returns Complete text content
228
+ */
229
+ async function streamToText(stream) {
230
+ let text = '';
231
+ for await (const chunk of stream) {
232
+ if (chunk.type === 'content') {
233
+ text += chunk.delta;
234
+ }
235
+ }
236
+ return text;
237
+ }
238
+ // ============================================================================
239
+ // Stream Splitting/Merging
240
+ // ============================================================================
241
+ /**
242
+ * Split a stream into multiple consumers.
243
+ *
244
+ * @param stream Original stream
245
+ * @param consumerCount Number of consumers
246
+ * @returns Array of streams, one per consumer
247
+ */
248
+ function splitStream(stream, consumerCount) {
249
+ const chunks = [];
250
+ const consumers = Array.from({ length: consumerCount }, () => ({
251
+ resolve: () => { },
252
+ queue: [],
253
+ }));
254
+ let streamDone = false;
255
+ const activeConsumers = consumerCount;
256
+ // Start consuming the source stream
257
+ void (async () => {
258
+ try {
259
+ for await (const chunk of stream) {
260
+ chunks.push(chunk);
261
+ // Distribute to all consumers
262
+ for (const consumer of consumers) {
263
+ consumer.queue.push(chunk);
264
+ if (consumer.resolve) {
265
+ const resolver = consumer.resolve;
266
+ consumer.resolve = () => { };
267
+ resolver({ value: consumer.queue.shift(), done: false });
268
+ }
269
+ }
270
+ }
271
+ }
272
+ finally {
273
+ streamDone = true;
274
+ // Signal completion to all consumers
275
+ for (const consumer of consumers) {
276
+ if (consumer.resolve) {
277
+ consumer.resolve({ value: undefined, done: true });
278
+ }
279
+ }
280
+ }
281
+ })();
282
+ // Create consumer streams
283
+ return consumers.map((consumer) => {
284
+ return (async function* () {
285
+ while (activeConsumers > 0) {
286
+ if (consumer.queue.length > 0) {
287
+ yield consumer.queue.shift();
288
+ }
289
+ else if (streamDone) {
290
+ break;
291
+ }
292
+ else {
293
+ // Wait for next chunk
294
+ await new Promise((resolve) => {
295
+ consumer.resolve = (result) => {
296
+ if (!result.done && result.value) {
297
+ consumer.queue.push(result.value);
298
+ }
299
+ resolve();
300
+ };
301
+ });
302
+ }
303
+ }
304
+ })();
305
+ });
306
+ }
307
+ // ============================================================================
308
+ // Stream Error Handling
309
+ // ============================================================================
310
+ /**
311
+ * Wrap stream with error handling.
312
+ *
313
+ * @param stream Original stream
314
+ * @param onError Error handler
315
+ * @returns Wrapped stream
316
+ */
317
+ async function* catchStreamErrors(stream, onError) {
318
+ try {
319
+ for await (const chunk of stream) {
320
+ yield chunk;
321
+ }
322
+ }
323
+ catch (error) {
324
+ const errorChunk = onError(error);
325
+ if (errorChunk) {
326
+ yield errorChunk;
327
+ }
328
+ }
329
+ }
330
+ /**
331
+ * Add timeout to stream.
332
+ *
333
+ * @param stream Original stream
334
+ * @param timeoutMs Timeout in milliseconds
335
+ * @param onTimeout Callback when timeout occurs
336
+ * @returns Stream with timeout
337
+ */
338
+ async function* streamWithTimeout(stream, timeoutMs, onTimeout) {
339
+ const iterator = stream[Symbol.asyncIterator]();
340
+ let timeoutId;
341
+ try {
342
+ while (true) {
343
+ const timeoutPromise = new Promise((_, reject) => {
344
+ timeoutId = setTimeout(() => reject(new Error('Stream timeout')), timeoutMs);
345
+ });
346
+ const result = await Promise.race([iterator.next(), timeoutPromise]);
347
+ if (timeoutId !== undefined) {
348
+ clearTimeout(timeoutId);
349
+ }
350
+ if (result.done) {
351
+ break;
352
+ }
353
+ yield result.value;
354
+ }
355
+ }
356
+ catch (error) {
357
+ if (error.message === 'Stream timeout') {
358
+ yield onTimeout();
359
+ }
360
+ else {
361
+ throw error;
362
+ }
363
+ }
364
+ }
365
+ // ============================================================================
366
+ // Stream Utilities
367
+ // ============================================================================
368
+ /**
369
+ * Check if a chunk is a content chunk.
370
+ */
371
+ function isContentChunk(chunk) {
372
+ return chunk.type === 'content';
373
+ }
374
+ /**
375
+ * Check if a chunk is a done chunk.
376
+ */
377
+ function isDoneChunk(chunk) {
378
+ return chunk.type === 'done';
379
+ }
380
+ /**
381
+ * Check if a chunk is an error chunk.
382
+ */
383
+ function isErrorChunk(chunk) {
384
+ return chunk.type === 'error';
385
+ }
386
+ /**
387
+ * Get content deltas from stream.
388
+ *
389
+ * @param stream Stream to process
390
+ * @returns Stream of just content deltas
391
+ */
392
+ async function* getContentDeltas(stream) {
393
+ for await (const chunk of stream) {
394
+ if (chunk.type === 'content') {
395
+ yield chunk.delta;
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * Convert an IR stream to a simple text iterator.
401
+ * Yields content deltas as strings and throws on errors.
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * for await (const text of streamToTextIterator(stream)) {
406
+ * process.stdout.write(text);
407
+ * }
408
+ * ```
409
+ */
410
+ async function* streamToTextIterator(stream) {
411
+ for await (const chunk of stream) {
412
+ if (chunk.type === 'content') {
413
+ yield chunk.delta;
414
+ }
415
+ else if (chunk.type === 'error') {
416
+ throw new Error(chunk.error.message);
417
+ }
418
+ }
419
+ }
420
+ function validateChunkSequence(chunks) {
421
+ const sequences = chunks.map((c) => c.sequence).filter((s) => s !== undefined);
422
+ const gaps = [];
423
+ const duplicates = [];
424
+ let outOfOrder = false;
425
+ const seen = new Set();
426
+ let expectedSequence = 0;
427
+ for (let i = 0; i < sequences.length; i++) {
428
+ const seq = sequences[i];
429
+ if (seq === undefined) {
430
+ continue;
431
+ }
432
+ // Check for duplicates
433
+ if (seen.has(seq)) {
434
+ duplicates.push(seq);
435
+ }
436
+ seen.add(seq);
437
+ // Check for out-of-order
438
+ if (seq < expectedSequence) {
439
+ outOfOrder = true;
440
+ }
441
+ // Check for gaps
442
+ if (seq > expectedSequence) {
443
+ for (let missing = expectedSequence; missing < seq; missing++) {
444
+ if (!seen.has(missing)) {
445
+ gaps.push(missing);
446
+ }
447
+ }
448
+ }
449
+ expectedSequence = Math.max(expectedSequence, seq + 1);
450
+ }
451
+ return {
452
+ valid: gaps.length === 0 && duplicates.length === 0 && !outOfOrder,
453
+ gaps,
454
+ duplicates,
455
+ outOfOrder,
456
+ expectedNext: expectedSequence,
457
+ };
458
+ }
459
+ async function* validateStream(stream, options = {}) {
460
+ const { strictSequence = true, rejectDuplicates = true, maxGap = 0, onWarning } = options;
461
+ let expectedSequence = 0;
462
+ const seen = new Set();
463
+ for await (const chunk of stream) {
464
+ const seq = chunk.sequence;
465
+ if (seq === undefined) {
466
+ yield chunk;
467
+ continue;
468
+ }
469
+ // Check for duplicates
470
+ if (seen.has(seq)) {
471
+ const message = `Duplicate sequence number: ${seq}`;
472
+ if (rejectDuplicates) {
473
+ throw new Error(message);
474
+ }
475
+ else if (onWarning) {
476
+ onWarning(message);
477
+ }
478
+ }
479
+ seen.add(seq);
480
+ // Check for gaps
481
+ const gap = seq - expectedSequence;
482
+ if (gap > maxGap) {
483
+ const message = `Sequence gap detected: expected ${expectedSequence}, got ${seq} (gap: ${gap})`;
484
+ if (strictSequence) {
485
+ throw new Error(message);
486
+ }
487
+ else if (onWarning) {
488
+ onWarning(message);
489
+ }
490
+ }
491
+ // Check for out-of-order
492
+ if (seq < expectedSequence) {
493
+ const message = `Out-of-order chunk: sequence ${seq} after ${expectedSequence}`;
494
+ if (strictSequence) {
495
+ throw new Error(message);
496
+ }
497
+ else if (onWarning) {
498
+ onWarning(message);
499
+ }
500
+ }
501
+ expectedSequence = Math.max(expectedSequence, seq + 1);
502
+ yield chunk;
503
+ }
504
+ }
505
+ /**
506
+ * Assemble stream chunks into a complete message.
507
+ *
508
+ * Alternative to streamToResponse that returns just the assembled message.
509
+ *
510
+ * @param stream Stream to assemble
511
+ * @returns Assembled message text
512
+ */
513
+ async function assembleStreamChunks(stream) {
514
+ return streamToText(stream);
515
+ }
516
+ /**
517
+ * Create an error chunk.
518
+ *
519
+ * Helper for creating properly formatted error chunks.
520
+ *
521
+ * @param code Error code
522
+ * @param message Error message
523
+ * @param sequence Sequence number
524
+ * @param details Optional error details
525
+ * @returns Error chunk
526
+ */
527
+ function createStreamError(code, message, sequence, details) {
528
+ return {
529
+ type: 'error',
530
+ sequence,
531
+ error: {
532
+ code,
533
+ message,
534
+ details,
535
+ },
536
+ };
537
+ }
538
+ // ============================================================================
539
+ // Backpressure Handling
540
+ // ============================================================================
541
+ /**
542
+ * Add backpressure control to stream.
543
+ *
544
+ * Buffers chunks and allows consumer to control flow.
545
+ *
546
+ * @param stream Original stream
547
+ * @param bufferSize Maximum buffer size before pausing
548
+ * @returns Stream with backpressure control
549
+ */
550
+ async function* streamWithBackpressure(stream, bufferSize = 10) {
551
+ const buffer = [];
552
+ let sourceComplete = false;
553
+ let sourceError = null;
554
+ // Start consuming source stream in background
555
+ void (async () => {
556
+ try {
557
+ for await (const chunk of stream) {
558
+ // Wait if buffer is full
559
+ while (buffer.length >= bufferSize) {
560
+ await new Promise((resolve) => setTimeout(resolve, 10));
561
+ }
562
+ buffer.push(chunk);
563
+ }
564
+ }
565
+ catch (error) {
566
+ sourceError = error;
567
+ }
568
+ finally {
569
+ sourceComplete = true;
570
+ }
571
+ })();
572
+ // Yield chunks with backpressure
573
+ while (!sourceComplete || buffer.length > 0 || sourceError) {
574
+ if (sourceError) {
575
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- sourceError is already typed as Error | null, narrowed to Error here
576
+ throw sourceError;
577
+ }
578
+ if (buffer.length > 0) {
579
+ yield buffer.shift();
580
+ }
581
+ else if (!sourceComplete) {
582
+ // Wait for more chunks
583
+ await new Promise((resolve) => setTimeout(resolve, 10));
584
+ }
585
+ else {
586
+ break;
587
+ }
588
+ }
589
+ }
590
+ /**
591
+ * Rate limit stream chunks.
592
+ *
593
+ * Ensures chunks are yielded at a maximum rate.
594
+ *
595
+ * @param stream Original stream
596
+ * @param chunksPerSecond Maximum chunks per second
597
+ * @returns Rate-limited stream
598
+ */
599
+ async function* rateLimitStream(stream, chunksPerSecond) {
600
+ const delayMs = 1000 / chunksPerSecond;
601
+ let lastYieldTime = 0;
602
+ for await (const chunk of stream) {
603
+ const now = Date.now();
604
+ const timeSinceLastYield = now - lastYieldTime;
605
+ if (timeSinceLastYield < delayMs) {
606
+ await new Promise((resolve) => setTimeout(resolve, delayMs - timeSinceLastYield));
607
+ }
608
+ lastYieldTime = Date.now();
609
+ yield chunk;
610
+ }
611
+ }
612
+ /**
613
+ * Collect a stream into a rich result with content, message, and metadata.
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * const stream = backend.executeStream(request);
618
+ * const result = await collectStreamFull(stream);
619
+ * console.log(result.content);
620
+ * console.log('Tokens:', result.usage?.totalTokens);
621
+ * ```
622
+ */
623
+ async function collectStreamFull(stream) {
624
+ const chunks = [];
625
+ let content = '';
626
+ let requestId;
627
+ let usage;
628
+ let finishReason = 'stop';
629
+ let message;
630
+ for await (const chunk of stream) {
631
+ chunks.push(chunk);
632
+ switch (chunk.type) {
633
+ case 'start':
634
+ requestId = chunk.metadata?.requestId;
635
+ break;
636
+ case 'content':
637
+ content += chunk.delta;
638
+ break;
639
+ case 'done':
640
+ finishReason = chunk.finishReason;
641
+ usage = chunk.usage;
642
+ message = chunk.message;
643
+ break;
644
+ case 'error':
645
+ throw new Error(chunk.error.message);
646
+ }
647
+ }
648
+ return {
649
+ content,
650
+ message: message ?? { role: 'assistant', content },
651
+ chunks,
652
+ finishReason,
653
+ usage,
654
+ requestId,
655
+ };
656
+ }
657
+ /**
658
+ * Process a stream with callbacks while also returning the collected result.
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * const result = await processStream(stream, {
663
+ * onContent: (delta) => process.stdout.write(delta),
664
+ * onDone: () => console.log('\nDone!'),
665
+ * });
666
+ * ```
667
+ */
668
+ async function processStream(stream, options = {}) {
669
+ const chunks = [];
670
+ let content = '';
671
+ let requestId;
672
+ let usage;
673
+ let finishReason = 'stop';
674
+ let message;
675
+ try {
676
+ for await (const chunk of stream) {
677
+ chunks.push(chunk);
678
+ switch (chunk.type) {
679
+ case 'start':
680
+ requestId = chunk.metadata?.requestId;
681
+ options.onStart?.(requestId ?? '');
682
+ break;
683
+ case 'content':
684
+ content += chunk.delta;
685
+ options.onContent?.(chunk.delta, content);
686
+ break;
687
+ case 'done':
688
+ finishReason = chunk.finishReason;
689
+ usage = chunk.usage;
690
+ message = chunk.message;
691
+ break;
692
+ case 'error': {
693
+ const error = new Error(chunk.error.message);
694
+ options.onError?.(error);
695
+ throw error;
696
+ }
697
+ }
698
+ }
699
+ const result = {
700
+ content,
701
+ message: message ?? { role: 'assistant', content },
702
+ chunks,
703
+ finishReason,
704
+ usage,
705
+ requestId,
706
+ };
707
+ options.onDone?.(result);
708
+ return result;
709
+ }
710
+ catch (error) {
711
+ const err = error instanceof Error ? error : new Error(String(error));
712
+ options.onError?.(err);
713
+ throw err;
714
+ }
715
+ }
716
+ /**
717
+ * Convert an IR stream to a line-by-line iterator.
718
+ * Buffers content and yields complete lines.
719
+ *
720
+ * @example
721
+ * ```typescript
722
+ * for await (const line of streamToLines(stream)) {
723
+ * console.log('Line:', line);
724
+ * }
725
+ * ```
726
+ */
727
+ async function* streamToLines(stream) {
728
+ let buffer = '';
729
+ for await (const chunk of stream) {
730
+ if (chunk.type === 'content') {
731
+ buffer += chunk.delta;
732
+ // Yield complete lines
733
+ const lines = buffer.split('\n');
734
+ buffer = lines.pop() ?? '';
735
+ for (const line of lines) {
736
+ yield line;
737
+ }
738
+ }
739
+ else if (chunk.type === 'error') {
740
+ throw new Error(chunk.error.message);
741
+ }
742
+ }
743
+ // Yield remaining buffer if not empty
744
+ if (buffer) {
745
+ yield buffer;
746
+ }
747
+ }
748
+ /**
749
+ * Create a throttled version of a stream that limits how often chunks are yielded.
750
+ * Batches content chunks that arrive faster than the interval.
751
+ * Unlike rateLimitStream, this merges content chunks rather than delaying them.
752
+ *
753
+ * @param stream - The source stream
754
+ * @param intervalMs - Minimum milliseconds between yields
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * // Update UI at most every 50ms
759
+ * for await (const chunk of throttleStream(stream, 50)) {
760
+ * updateUI(chunk);
761
+ * }
762
+ * ```
763
+ */
764
+ async function* throttleStream(stream, intervalMs) {
765
+ let lastYield = 0;
766
+ let pendingChunk = null;
767
+ for await (const chunk of stream) {
768
+ const now = Date.now();
769
+ // Always yield start, done, and error chunks immediately
770
+ if (chunk.type !== 'content') {
771
+ if (pendingChunk) {
772
+ yield pendingChunk;
773
+ pendingChunk = null;
774
+ }
775
+ yield chunk;
776
+ lastYield = now;
777
+ continue;
778
+ }
779
+ // For content chunks, throttle
780
+ if (now - lastYield >= intervalMs) {
781
+ if (pendingChunk?.type === 'content') {
782
+ // Merge pending with current
783
+ const merged = {
784
+ type: 'content',
785
+ sequence: chunk.sequence,
786
+ delta: pendingChunk.delta + chunk.delta,
787
+ accumulated: chunk.accumulated,
788
+ };
789
+ yield merged;
790
+ pendingChunk = null;
791
+ }
792
+ else {
793
+ yield chunk;
794
+ }
795
+ lastYield = now;
796
+ }
797
+ else {
798
+ // Accumulate into pending
799
+ if (pendingChunk?.type === 'content') {
800
+ pendingChunk = {
801
+ type: 'content',
802
+ sequence: chunk.sequence,
803
+ delta: pendingChunk.delta + chunk.delta,
804
+ accumulated: chunk.accumulated,
805
+ };
806
+ }
807
+ else {
808
+ pendingChunk = chunk;
809
+ }
810
+ }
811
+ }
812
+ // Yield any remaining pending chunk
813
+ if (pendingChunk) {
814
+ yield pendingChunk;
815
+ }
816
+ }
817
+ /**
818
+ * Split a stream into multiple streams that can be consumed independently.
819
+ * Each returned stream receives all chunks from the source.
820
+ * Alias for splitStream with a simpler interface.
821
+ *
822
+ * @param stream - The source stream
823
+ * @param count - Number of streams to create
824
+ *
825
+ * @example
826
+ * ```typescript
827
+ * const [stream1, stream2] = teeStream(originalStream, 2);
828
+ *
829
+ * // Consume independently
830
+ * const [result1, result2] = await Promise.all([
831
+ * collectStreamFull(stream1),
832
+ * processStream(stream2, { onContent: console.log }),
833
+ * ]);
834
+ * ```
835
+ */
836
+ function teeStream(stream, count = 2) {
837
+ const queues = Array.from({ length: count }, () => []);
838
+ const resolvers = Array(count).fill(null);
839
+ let done = false;
840
+ let error = null;
841
+ // Start consuming the source stream
842
+ void (async () => {
843
+ try {
844
+ for await (const chunk of stream) {
845
+ for (let i = 0; i < count; i++) {
846
+ const queue = queues[i];
847
+ if (queue) {
848
+ queue.push(chunk);
849
+ }
850
+ resolvers[i]?.();
851
+ }
852
+ }
853
+ }
854
+ catch (e) {
855
+ error = e instanceof Error ? e : new Error(String(e));
856
+ }
857
+ finally {
858
+ done = true;
859
+ for (const resolve of resolvers) {
860
+ resolve?.();
861
+ }
862
+ }
863
+ })();
864
+ // Create output generators
865
+ return queues.map((queue, index) => {
866
+ return (async function* () {
867
+ let cursor = 0;
868
+ while (true) {
869
+ // Yield any queued chunks
870
+ while (cursor < queue.length) {
871
+ const chunk = queue[cursor++];
872
+ if (chunk) {
873
+ yield chunk;
874
+ }
875
+ }
876
+ // Check if done
877
+ if (done) {
878
+ if (error) {
879
+ throw error;
880
+ }
881
+ return;
882
+ }
883
+ // Wait for more chunks
884
+ await new Promise((resolve) => {
885
+ resolvers[index] = resolve;
886
+ });
887
+ resolvers[index] = null;
888
+ }
889
+ })();
890
+ });
891
+ }
892
+ //# sourceMappingURL=streaming.js.map