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.
- package/LICENSE +21 -0
- package/dist/cjs/conversation-history.js +139 -0
- package/dist/cjs/conversation-history.js.map +1 -0
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/model-cache.js +163 -0
- package/dist/cjs/model-cache.js.map +1 -0
- package/dist/cjs/parameter-normalizer.js +451 -0
- package/dist/cjs/parameter-normalizer.js.map +1 -0
- package/dist/cjs/streaming-modes.js +277 -0
- package/dist/cjs/streaming-modes.js.map +1 -0
- package/dist/cjs/streaming.js +892 -0
- package/dist/cjs/streaming.js.map +1 -0
- package/dist/cjs/structured-output.js +398 -0
- package/dist/cjs/structured-output.js.map +1 -0
- package/dist/cjs/system-message.js +222 -0
- package/dist/cjs/system-message.js.map +1 -0
- package/dist/cjs/validation.js +534 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/warnings.js +301 -0
- package/dist/cjs/warnings.js.map +1 -0
- package/dist/esm/conversation-history.js +134 -0
- package/dist/esm/conversation-history.js.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/model-cache.js +158 -0
- package/dist/esm/model-cache.js.map +1 -0
- package/dist/esm/parameter-normalizer.js +434 -0
- package/dist/esm/parameter-normalizer.js.map +1 -0
- package/dist/esm/streaming-modes.js +265 -0
- package/dist/esm/streaming-modes.js.map +1 -0
- package/dist/esm/streaming.js +860 -0
- package/dist/esm/streaming.js.map +1 -0
- package/dist/esm/structured-output.js +387 -0
- package/dist/esm/structured-output.js.map +1 -0
- package/dist/esm/system-message.js +213 -0
- package/dist/esm/system-message.js.map +1 -0
- package/dist/esm/validation.js +523 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/warnings.js +284 -0
- package/dist/esm/warnings.js.map +1 -0
- package/dist/types/conversation-history.d.ts +70 -0
- package/dist/types/conversation-history.d.ts.map +1 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/model-cache.d.ts +88 -0
- package/dist/types/model-cache.d.ts.map +1 -0
- package/dist/types/parameter-normalizer.d.ts +154 -0
- package/dist/types/parameter-normalizer.d.ts.map +1 -0
- package/dist/types/streaming-modes.d.ts +139 -0
- package/dist/types/streaming-modes.d.ts.map +1 -0
- package/dist/types/streaming.d.ts +384 -0
- package/dist/types/streaming.d.ts.map +1 -0
- package/dist/types/structured-output.d.ts +157 -0
- package/dist/types/structured-output.d.ts.map +1 -0
- package/dist/types/system-message.d.ts +78 -0
- package/dist/types/system-message.d.ts.map +1 -0
- package/dist/types/validation.d.ts +46 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/warnings.d.ts +149 -0
- package/dist/types/warnings.d.ts.map +1 -0
- package/package.json +75 -0
- 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
|