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