ai-stream-utils 1.6.0 → 2.0.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/README.md +197 -416
- package/dist/convert-ui-message-stream-to-sse-stream-CcQRXKju.mjs +165 -0
- package/dist/index.d.mts +538 -110
- package/dist/index.mjs +414 -261
- package/dist/types-B4nePmEd.d.mts +53 -0
- package/dist/utils/index.d.mts +2 -46
- package/dist/utils/index.mjs +2 -81
- package/package.json +49 -31
- package/dist/create-async-iterable-stream-x_DKVIDi.mjs +0 -59
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { a as convertArrayToStream, c as convertAsyncIterableToStream, i as convertAsyncIterableToArray, n as convertStreamToArray, o as convertArrayToAsyncIterable, r as convertSSEToUIMessageStream, s as createAsyncIterableStream, t as convertUIMessageToSSEStream } from "./convert-ui-message-stream-to-sse-stream-CcQRXKju.mjs";
|
|
2
|
+
import "./utils/index.mjs";
|
|
3
|
+
import { createIdGenerator, getToolOrDynamicToolName, isDataUIPart, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolOrDynamicToolUIPart, readUIMessageStream } from "ai";
|
|
3
4
|
|
|
4
|
-
//#region src/consume-ui-message-stream.ts
|
|
5
|
+
//#region src/consume/consume-ui-message-stream.ts
|
|
5
6
|
/**
|
|
6
7
|
* Consumes a UIMessageStream by fully reading it and returning the final UI message.
|
|
7
8
|
*
|
|
@@ -30,172 +31,58 @@ async function consumeUIMessageStream(stream) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
//#endregion
|
|
33
|
-
//#region
|
|
34
|
-
var marker$1 = "vercel.ai.error";
|
|
35
|
-
var symbol$1 = Symbol.for(marker$1);
|
|
36
|
-
var _a, _b;
|
|
37
|
-
var AISDKError = class _AISDKError extends (_b = Error, _a = symbol$1, _b) {
|
|
38
|
-
/**
|
|
39
|
-
* Creates an AI SDK Error.
|
|
40
|
-
*
|
|
41
|
-
* @param {Object} params - The parameters for creating the error.
|
|
42
|
-
* @param {string} params.name - The name of the error.
|
|
43
|
-
* @param {string} params.message - The error message.
|
|
44
|
-
* @param {unknown} [params.cause] - The underlying cause of the error.
|
|
45
|
-
*/
|
|
46
|
-
constructor({ name: name14, message, cause }) {
|
|
47
|
-
super(message);
|
|
48
|
-
this[_a] = true;
|
|
49
|
-
this.name = name14;
|
|
50
|
-
this.cause = cause;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Checks if the given error is an AI SDK Error.
|
|
54
|
-
* @param {unknown} error - The error to check.
|
|
55
|
-
* @returns {boolean} True if the error is an AI SDK Error, false otherwise.
|
|
56
|
-
*/
|
|
57
|
-
static isInstance(error) {
|
|
58
|
-
return _AISDKError.hasMarker(error, marker$1);
|
|
59
|
-
}
|
|
60
|
-
static hasMarker(error, marker15) {
|
|
61
|
-
const markerSymbol = Symbol.for(marker15);
|
|
62
|
-
return error != null && typeof error === "object" && markerSymbol in error && typeof error[markerSymbol] === "boolean" && error[markerSymbol] === true;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
var name$1 = "AI_APICallError";
|
|
66
|
-
var marker2 = `vercel.ai.error.${name$1}`;
|
|
67
|
-
var symbol2 = Symbol.for(marker2);
|
|
68
|
-
var name2 = "AI_EmptyResponseBodyError";
|
|
69
|
-
var marker3 = `vercel.ai.error.${name2}`;
|
|
70
|
-
var symbol3 = Symbol.for(marker3);
|
|
71
|
-
var name3 = "AI_InvalidArgumentError";
|
|
72
|
-
var marker4 = `vercel.ai.error.${name3}`;
|
|
73
|
-
var symbol4 = Symbol.for(marker4);
|
|
74
|
-
var _a4, _b4;
|
|
75
|
-
var InvalidArgumentError = class extends (_b4 = AISDKError, _a4 = symbol4, _b4) {
|
|
76
|
-
constructor({ message, cause, argument }) {
|
|
77
|
-
super({
|
|
78
|
-
name: name3,
|
|
79
|
-
message,
|
|
80
|
-
cause
|
|
81
|
-
});
|
|
82
|
-
this[_a4] = true;
|
|
83
|
-
this.argument = argument;
|
|
84
|
-
}
|
|
85
|
-
static isInstance(error) {
|
|
86
|
-
return AISDKError.hasMarker(error, marker4);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var name4 = "AI_InvalidPromptError";
|
|
90
|
-
var marker5 = `vercel.ai.error.${name4}`;
|
|
91
|
-
var symbol5 = Symbol.for(marker5);
|
|
92
|
-
var name5 = "AI_InvalidResponseDataError";
|
|
93
|
-
var marker6 = `vercel.ai.error.${name5}`;
|
|
94
|
-
var symbol6 = Symbol.for(marker6);
|
|
95
|
-
var name6 = "AI_JSONParseError";
|
|
96
|
-
var marker7 = `vercel.ai.error.${name6}`;
|
|
97
|
-
var symbol7 = Symbol.for(marker7);
|
|
98
|
-
var name7 = "AI_LoadAPIKeyError";
|
|
99
|
-
var marker8 = `vercel.ai.error.${name7}`;
|
|
100
|
-
var symbol8 = Symbol.for(marker8);
|
|
101
|
-
var name8 = "AI_LoadSettingError";
|
|
102
|
-
var marker9 = `vercel.ai.error.${name8}`;
|
|
103
|
-
var symbol9 = Symbol.for(marker9);
|
|
104
|
-
var name9 = "AI_NoContentGeneratedError";
|
|
105
|
-
var marker10 = `vercel.ai.error.${name9}`;
|
|
106
|
-
var symbol10 = Symbol.for(marker10);
|
|
107
|
-
var name10 = "AI_NoSuchModelError";
|
|
108
|
-
var marker11 = `vercel.ai.error.${name10}`;
|
|
109
|
-
var symbol11 = Symbol.for(marker11);
|
|
110
|
-
var name11 = "AI_TooManyEmbeddingValuesForCallError";
|
|
111
|
-
var marker12 = `vercel.ai.error.${name11}`;
|
|
112
|
-
var symbol12 = Symbol.for(marker12);
|
|
113
|
-
var name12 = "AI_TypeValidationError";
|
|
114
|
-
var marker13 = `vercel.ai.error.${name12}`;
|
|
115
|
-
var symbol13 = Symbol.for(marker13);
|
|
116
|
-
var name13 = "AI_UnsupportedFunctionalityError";
|
|
117
|
-
var marker14 = `vercel.ai.error.${name13}`;
|
|
118
|
-
var symbol14 = Symbol.for(marker14);
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region node_modules/.pnpm/@ai-sdk+provider-utils@4.0.8_zod@4.3.5/node_modules/@ai-sdk/provider-utils/dist/index.mjs
|
|
122
|
-
function convertAsyncIteratorToReadableStream(iterator) {
|
|
123
|
-
let cancelled = false;
|
|
124
|
-
return new ReadableStream({
|
|
125
|
-
async pull(controller) {
|
|
126
|
-
if (cancelled) return;
|
|
127
|
-
try {
|
|
128
|
-
const { value, done } = await iterator.next();
|
|
129
|
-
if (done) controller.close();
|
|
130
|
-
else controller.enqueue(value);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
controller.error(error);
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
async cancel(reason) {
|
|
136
|
-
cancelled = true;
|
|
137
|
-
if (iterator.return) try {
|
|
138
|
-
await iterator.return(reason);
|
|
139
|
-
} catch (e) {}
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
var { btoa, atob } = globalThis;
|
|
144
|
-
var name = "AI_DownloadError";
|
|
145
|
-
var marker = `vercel.ai.error.${name}`;
|
|
146
|
-
var symbol = Symbol.for(marker);
|
|
147
|
-
var createIdGenerator = ({ prefix, size = 16, alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", separator = "-" } = {}) => {
|
|
148
|
-
const generator = () => {
|
|
149
|
-
const alphabetLength = alphabet.length;
|
|
150
|
-
const chars = new Array(size);
|
|
151
|
-
for (let i = 0; i < size; i++) chars[i] = alphabet[Math.random() * alphabetLength | 0];
|
|
152
|
-
return chars.join("");
|
|
153
|
-
};
|
|
154
|
-
if (prefix == null) return generator;
|
|
155
|
-
if (alphabet.includes(separator)) throw new InvalidArgumentError({
|
|
156
|
-
argument: "separator",
|
|
157
|
-
message: `The separator "${separator}" must not be part of the alphabet "${alphabet}".`
|
|
158
|
-
});
|
|
159
|
-
return () => `${prefix}${separator}${generator()}`;
|
|
160
|
-
};
|
|
161
|
-
var generateId = createIdGenerator();
|
|
162
|
-
var schemaSymbol = Symbol.for("vercel.ai.schema");
|
|
163
|
-
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region src/utils/internal/stream-utils.ts
|
|
34
|
+
//#region src/internal/get-part-type-from-chunk.ts
|
|
166
35
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
36
|
+
* Derives the part type directly from a chunk's type.
|
|
37
|
+
* This avoids relying on `message.parts[-1]` which can be incorrect
|
|
38
|
+
* when chunks from different part types are interleaved.
|
|
39
|
+
*
|
|
40
|
+
* @param chunk - The chunk to derive part type from
|
|
41
|
+
* @param toolCallIdMap - Map to track toolCallId → partType for tool chunks
|
|
42
|
+
* @returns The part type string, or undefined for meta chunks
|
|
169
43
|
*/
|
|
170
|
-
function getPartTypeFromChunk(chunk,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
case `tool-input-available`:
|
|
174
|
-
case `tool-input-error`: {
|
|
175
|
-
const toolChunk = chunk;
|
|
176
|
-
return toolChunk.dynamic ? `dynamic-tool` : `tool-${toolChunk.toolName}`;
|
|
177
|
-
}
|
|
178
|
-
case `tool-input-delta`:
|
|
179
|
-
case `tool-output-available`:
|
|
180
|
-
case `tool-output-error`: {
|
|
181
|
-
const toolChunk = chunk;
|
|
182
|
-
if (toolChunk.dynamic) return `dynamic-tool`;
|
|
183
|
-
const toolState = toolCallStates?.get(toolChunk.toolCallId);
|
|
184
|
-
if (toolState) return toolState.dynamic ? `dynamic-tool` : `tool-${toolState.toolName}`;
|
|
185
|
-
return `dynamic-tool`;
|
|
186
|
-
}
|
|
44
|
+
function getPartTypeFromChunk(chunk, toolCallIdMap) {
|
|
45
|
+
const chunkType = chunk.type;
|
|
46
|
+
switch (chunkType) {
|
|
187
47
|
case `text-start`:
|
|
188
48
|
case `text-delta`:
|
|
189
49
|
case `text-end`: return `text`;
|
|
190
50
|
case `reasoning-start`:
|
|
191
51
|
case `reasoning-delta`:
|
|
192
52
|
case `reasoning-end`: return `reasoning`;
|
|
193
|
-
case `
|
|
53
|
+
case `tool-input-start`: {
|
|
54
|
+
const c = chunk;
|
|
55
|
+
const partType = c.dynamic ? `dynamic-tool` : `tool-${c.toolName}`;
|
|
56
|
+
toolCallIdMap.set(c.toolCallId, partType);
|
|
57
|
+
return partType;
|
|
58
|
+
}
|
|
59
|
+
case `tool-input-delta`:
|
|
60
|
+
case `tool-input-available`:
|
|
61
|
+
case `tool-input-error`:
|
|
62
|
+
case `tool-output-available`:
|
|
63
|
+
case `tool-output-error`:
|
|
64
|
+
case `tool-output-denied`:
|
|
65
|
+
case `tool-approval-request`: {
|
|
66
|
+
const c = chunk;
|
|
67
|
+
return toolCallIdMap.get(c.toolCallId);
|
|
68
|
+
}
|
|
194
69
|
case `source-url`: return `source-url`;
|
|
195
70
|
case `source-document`: return `source-document`;
|
|
196
|
-
|
|
71
|
+
case `file`: return `file`;
|
|
72
|
+
case `start`:
|
|
73
|
+
case `finish`:
|
|
74
|
+
case `start-step`:
|
|
75
|
+
case `finish-step`:
|
|
76
|
+
case `error`:
|
|
77
|
+
case `abort`:
|
|
78
|
+
case `message-metadata`: return;
|
|
79
|
+
default: if (chunkType.startsWith(`data-`)) return chunkType;
|
|
197
80
|
}
|
|
81
|
+
throw new Error(`Unable to derive part type from chunk type: ${chunkType}`);
|
|
198
82
|
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/internal/utils.ts
|
|
199
86
|
/**
|
|
200
87
|
* Checks if a chunk is a control/meta chunk that should always pass through.
|
|
201
88
|
*/
|
|
@@ -230,7 +117,7 @@ function asArray(value) {
|
|
|
230
117
|
}
|
|
231
118
|
|
|
232
119
|
//#endregion
|
|
233
|
-
//#region src/
|
|
120
|
+
//#region src/internal/create-ui-message-stream-reader.ts
|
|
234
121
|
/**
|
|
235
122
|
* Creates an async generator that wraps a UIMessageStream with readUIMessageStream.
|
|
236
123
|
*
|
|
@@ -255,7 +142,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
255
142
|
const iterator = readUIMessageStream({ stream: new ReadableStream({ start(c) {
|
|
256
143
|
controller = c;
|
|
257
144
|
} }) })[Symbol.asyncIterator]();
|
|
258
|
-
const
|
|
145
|
+
const toolCallIdMap = /* @__PURE__ */ new Map();
|
|
259
146
|
try {
|
|
260
147
|
while (true) {
|
|
261
148
|
const { done, value: chunk } = await inputReader.read();
|
|
@@ -263,13 +150,6 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
263
150
|
controller.close();
|
|
264
151
|
break;
|
|
265
152
|
}
|
|
266
|
-
if (chunk.type === `tool-input-start`) {
|
|
267
|
-
const toolChunk = chunk;
|
|
268
|
-
toolCallStates.set(toolChunk.toolCallId, {
|
|
269
|
-
toolName: toolChunk.toolName,
|
|
270
|
-
dynamic: toolChunk.dynamic
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
153
|
if (isMetaChunk(chunk) || isStepStartChunk(chunk) || isStepEndChunk(chunk)) {
|
|
274
154
|
controller.enqueue(chunk);
|
|
275
155
|
yield {
|
|
@@ -291,7 +171,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
291
171
|
const result = await iterator.next();
|
|
292
172
|
const message = result.done ? void 0 : result.value;
|
|
293
173
|
const messagePart = message?.parts[message.parts.length - 1];
|
|
294
|
-
const expectedPartType = getPartTypeFromChunk(chunk,
|
|
174
|
+
const expectedPartType = getPartTypeFromChunk(chunk, toolCallIdMap);
|
|
295
175
|
yield {
|
|
296
176
|
chunk,
|
|
297
177
|
message,
|
|
@@ -308,7 +188,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
308
188
|
}
|
|
309
189
|
|
|
310
190
|
//#endregion
|
|
311
|
-
//#region src/map-ui-message-stream.ts
|
|
191
|
+
//#region src/map/map-ui-message-stream.ts
|
|
312
192
|
/**
|
|
313
193
|
* Maps/filters a UIMessageStream at the chunk level using readUIMessageStream.
|
|
314
194
|
*
|
|
@@ -422,39 +302,11 @@ function mapUIMessageStream(stream, mapFn) {
|
|
|
422
302
|
if (chunks.length > 0) yield* emitChunks(chunks);
|
|
423
303
|
}
|
|
424
304
|
}
|
|
425
|
-
return createAsyncIterableStream(
|
|
305
|
+
return createAsyncIterableStream(convertAsyncIterableToStream(processChunks()));
|
|
426
306
|
}
|
|
427
307
|
|
|
428
308
|
//#endregion
|
|
429
|
-
//#region src/filter-ui-message-stream.ts
|
|
430
|
-
/**
|
|
431
|
-
* Creates a filter predicate that includes only the specified part types.
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* ```typescript
|
|
435
|
-
* filterUIMessageStream(stream, includeParts(['text', 'tool-weather']));
|
|
436
|
-
* ```
|
|
437
|
-
*/
|
|
438
|
-
function includeParts(includePartTypes) {
|
|
439
|
-
return ({ part }) => {
|
|
440
|
-
const partType = part.type;
|
|
441
|
-
return includePartTypes.includes(partType);
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Creates a filter predicate that excludes the specified part types.
|
|
446
|
-
*
|
|
447
|
-
* @example
|
|
448
|
-
* ```typescript
|
|
449
|
-
* filterUIMessageStream(stream, excludeParts(['reasoning', 'tool-calculator']));
|
|
450
|
-
* ```
|
|
451
|
-
*/
|
|
452
|
-
function excludeParts(excludePartTypes) {
|
|
453
|
-
return ({ part }) => {
|
|
454
|
-
const partType = part.type;
|
|
455
|
-
return !excludePartTypes.includes(partType);
|
|
456
|
-
};
|
|
457
|
-
}
|
|
309
|
+
//#region src/filter/filter-ui-message-stream.ts
|
|
458
310
|
/**
|
|
459
311
|
* Filters a UIMessageStream to include or exclude specific chunks.
|
|
460
312
|
*
|
|
@@ -497,7 +349,11 @@ function filterUIMessageStream(stream, predicate) {
|
|
|
497
349
|
}
|
|
498
350
|
|
|
499
351
|
//#endregion
|
|
500
|
-
//#region src/
|
|
352
|
+
//#region src/internal/serialize-part-to-chunks.ts
|
|
353
|
+
const generateId = createIdGenerator({
|
|
354
|
+
prefix: `aitxt`,
|
|
355
|
+
size: 24
|
|
356
|
+
});
|
|
501
357
|
/**
|
|
502
358
|
* Type guard to check if a message part is a source-url part.
|
|
503
359
|
*/
|
|
@@ -517,27 +373,16 @@ function isStepStartUIPart(part) {
|
|
|
517
373
|
return part.type === "step-start";
|
|
518
374
|
}
|
|
519
375
|
/**
|
|
520
|
-
* Extracts the part ID from a list of chunks belonging to the same part.
|
|
521
|
-
*/
|
|
522
|
-
function getPartId(chunks) {
|
|
523
|
-
for (const chunk of chunks) {
|
|
524
|
-
if ("id" in chunk && chunk.id) return chunk.id;
|
|
525
|
-
if ("toolCallId" in chunk && chunk.toolCallId) return chunk.toolCallId;
|
|
526
|
-
}
|
|
527
|
-
return "unknown";
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
376
|
* Serializes a UIMessagePart back to chunks (without step boundaries).
|
|
531
377
|
*
|
|
532
378
|
* This function converts a complete part (e.g., TextUIPart, ToolUIPart)
|
|
533
379
|
* back into the chunk format used by UIMessageStream.
|
|
534
380
|
*
|
|
535
381
|
* @param part - The part to serialize
|
|
536
|
-
* @param originalChunks - Original chunks for extracting IDs (for text/reasoning parts)
|
|
537
382
|
* @returns Array of chunks representing the part
|
|
538
383
|
*/
|
|
539
|
-
function serializePartToChunks(part
|
|
540
|
-
if (isStepStartUIPart(part)) return
|
|
384
|
+
function serializePartToChunks(part) {
|
|
385
|
+
if (isStepStartUIPart(part)) return [{ type: "start-step" }];
|
|
541
386
|
if (isFileUIPart(part)) return [{
|
|
542
387
|
type: "file",
|
|
543
388
|
mediaType: part.mediaType,
|
|
@@ -563,41 +408,46 @@ function serializePartToChunks(part, originalChunks) {
|
|
|
563
408
|
type: part.type,
|
|
564
409
|
data: part.data
|
|
565
410
|
}];
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
411
|
+
if (isTextUIPart(part)) {
|
|
412
|
+
const id = generateId();
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
type: "text-start",
|
|
416
|
+
id,
|
|
417
|
+
providerMetadata: part.providerMetadata
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
type: "text-delta",
|
|
421
|
+
id,
|
|
422
|
+
delta: part.text
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
type: "text-end",
|
|
426
|
+
id,
|
|
427
|
+
providerMetadata: part.providerMetadata
|
|
428
|
+
}
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
if (isReasoningUIPart(part)) {
|
|
432
|
+
const id = generateId();
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
type: "reasoning-start",
|
|
436
|
+
id,
|
|
437
|
+
providerMetadata: part.providerMetadata
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
type: "reasoning-delta",
|
|
441
|
+
id,
|
|
442
|
+
delta: part.text
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: "reasoning-end",
|
|
446
|
+
id,
|
|
447
|
+
providerMetadata: part.providerMetadata
|
|
448
|
+
}
|
|
449
|
+
];
|
|
450
|
+
}
|
|
601
451
|
if (isToolOrDynamicToolUIPart(part)) {
|
|
602
452
|
const dynamic = part.type === "dynamic-tool";
|
|
603
453
|
const chunks = [{
|
|
@@ -633,11 +483,11 @@ function serializePartToChunks(part, originalChunks) {
|
|
|
633
483
|
});
|
|
634
484
|
return chunks;
|
|
635
485
|
}
|
|
636
|
-
|
|
486
|
+
throw new Error(`Cannot serialize unknown part type: ${part.type}`);
|
|
637
487
|
}
|
|
638
488
|
|
|
639
489
|
//#endregion
|
|
640
|
-
//#region src/flat-map-ui-message-stream.ts
|
|
490
|
+
//#region src/flat-map/flat-map-ui-message-stream.ts
|
|
641
491
|
/**
|
|
642
492
|
* Creates a predicate that matches parts by their type.
|
|
643
493
|
*/
|
|
@@ -692,7 +542,7 @@ function flatMapUIMessageStream(...args) {
|
|
|
692
542
|
index: allParts.length - 1,
|
|
693
543
|
parts: allParts
|
|
694
544
|
}));
|
|
695
|
-
for (const part of parts) yield* emitChunks(serializePartToChunks(part
|
|
545
|
+
for (const part of parts) yield* emitChunks(serializePartToChunks(part));
|
|
696
546
|
bufferedChunks = [];
|
|
697
547
|
lastBufferedPart = void 0;
|
|
698
548
|
}
|
|
@@ -710,7 +560,7 @@ function flatMapUIMessageStream(...args) {
|
|
|
710
560
|
continue;
|
|
711
561
|
}
|
|
712
562
|
if (isStepEndChunk(chunk)) {
|
|
713
|
-
if (currentMode ===
|
|
563
|
+
if (currentMode === `buffering` && lastBufferedPart) yield* flushBufferedPart(lastBufferedPart);
|
|
714
564
|
if (stepStartEmitted) {
|
|
715
565
|
yield chunk;
|
|
716
566
|
stepStartEmitted = false;
|
|
@@ -726,35 +576,338 @@ function flatMapUIMessageStream(...args) {
|
|
|
726
576
|
index: allParts.length - 1,
|
|
727
577
|
parts: allParts
|
|
728
578
|
}));
|
|
729
|
-
for (const part
|
|
579
|
+
for (const part of parts) yield* emitChunks([part]);
|
|
730
580
|
continue;
|
|
731
581
|
}
|
|
732
|
-
if (!message) throw new Error(
|
|
582
|
+
if (!message) throw new Error(`Unexpected: received content chunk but message is undefined`);
|
|
733
583
|
const currentPart = part;
|
|
734
584
|
if (!currentPart) throw new Error(`Unexpected: received content chunk but part is undefined`);
|
|
735
585
|
if (message.parts.length > lastPartCount) {
|
|
736
|
-
if (currentMode ===
|
|
737
|
-
if (currentMode ===
|
|
586
|
+
if (currentMode === `buffering` && lastBufferedPart) yield* flushBufferedPart(lastBufferedPart);
|
|
587
|
+
if (currentMode === `streaming` && lastPartCount > 0) {
|
|
738
588
|
const previousPart = message.parts[lastPartCount - 1];
|
|
739
589
|
if (previousPart) allParts.push(previousPart);
|
|
740
590
|
}
|
|
741
591
|
if (!predicate || predicate(currentPart)) {
|
|
742
|
-
currentMode =
|
|
592
|
+
currentMode = `buffering`;
|
|
743
593
|
bufferedChunks = [chunk];
|
|
744
594
|
lastBufferedPart = currentPart;
|
|
745
595
|
} else {
|
|
746
|
-
currentMode =
|
|
596
|
+
currentMode = `streaming`;
|
|
747
597
|
yield* emitChunks([chunk]);
|
|
748
598
|
}
|
|
749
599
|
lastPartCount = message.parts.length;
|
|
750
|
-
} else if (currentMode ===
|
|
600
|
+
} else if (currentMode === `buffering`) {
|
|
751
601
|
bufferedChunks.push(chunk);
|
|
752
602
|
lastBufferedPart = currentPart;
|
|
753
|
-
} else if (currentMode ===
|
|
603
|
+
} else if (currentMode === `streaming`) yield* emitChunks([chunk]);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return createAsyncIterableStream(convertAsyncIterableToStream(processChunks()));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/pipe/chunk-pipeline.ts
|
|
611
|
+
/**
|
|
612
|
+
* Pipeline for chunk-based operations.
|
|
613
|
+
* Operations receive individual chunks with their associated part type.
|
|
614
|
+
*/
|
|
615
|
+
var ChunkPipeline = class ChunkPipeline {
|
|
616
|
+
consumed = false;
|
|
617
|
+
sourceIterable;
|
|
618
|
+
prevBuilder;
|
|
619
|
+
constructor(sourceIterable, prevBuilder = (s) => s) {
|
|
620
|
+
this.sourceIterable = sourceIterable;
|
|
621
|
+
this.prevBuilder = prevBuilder;
|
|
622
|
+
}
|
|
623
|
+
assertNotConsumed() {
|
|
624
|
+
if (this.consumed) throw new Error(`Pipeline has already been consumed.`);
|
|
625
|
+
}
|
|
626
|
+
filter(predicate) {
|
|
627
|
+
/**
|
|
628
|
+
* The predicate is cast to a simple function type for runtime execution.
|
|
629
|
+
*/
|
|
630
|
+
const predicateFn = predicate;
|
|
631
|
+
const nextBuilder = (iterable) => {
|
|
632
|
+
const prevIterable = this.prevBuilder(iterable);
|
|
633
|
+
async function* generator() {
|
|
634
|
+
for await (const item of prevIterable) {
|
|
635
|
+
/**
|
|
636
|
+
* Meta chunks always pass through without filtering.
|
|
637
|
+
*/
|
|
638
|
+
if (item.partType === void 0) {
|
|
639
|
+
yield item;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (predicateFn({
|
|
643
|
+
chunk: item.chunk,
|
|
644
|
+
part: { type: item.partType }
|
|
645
|
+
})) yield item;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return generator();
|
|
649
|
+
};
|
|
650
|
+
return new ChunkPipeline(this.sourceIterable, nextBuilder);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Transforms chunks by applying a mapping function.
|
|
654
|
+
* The callback only receives content chunks because meta chunks pass through unchanged.
|
|
655
|
+
* Returning null filters out the chunk, while returning an array yields multiple chunks.
|
|
656
|
+
*/
|
|
657
|
+
map(fn) {
|
|
658
|
+
const nextBuilder = (iterable) => {
|
|
659
|
+
const prevIterable = this.prevBuilder(iterable);
|
|
660
|
+
async function* generator() {
|
|
661
|
+
for await (const item of prevIterable) {
|
|
662
|
+
/**
|
|
663
|
+
* Meta chunks always pass through unchanged without transformation.
|
|
664
|
+
*/
|
|
665
|
+
if (item.partType === void 0) {
|
|
666
|
+
yield item;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* The asArray utility normalizes the result: null becomes an empty array,
|
|
671
|
+
* a single chunk becomes an array with one element, and arrays pass through as-is.
|
|
672
|
+
* An empty array means the chunk is filtered out and not yielded.
|
|
673
|
+
*/
|
|
674
|
+
const chunks = asArray(fn({
|
|
675
|
+
chunk: item.chunk,
|
|
676
|
+
part: { type: item.partType }
|
|
677
|
+
}));
|
|
678
|
+
for (const chunk of chunks) yield {
|
|
679
|
+
chunk,
|
|
680
|
+
partType: item.partType
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return generator();
|
|
685
|
+
};
|
|
686
|
+
return new ChunkPipeline(this.sourceIterable, nextBuilder);
|
|
687
|
+
}
|
|
688
|
+
on(predicate, callback) {
|
|
689
|
+
/**
|
|
690
|
+
* The predicate is cast to a simple function type for runtime execution.
|
|
691
|
+
*/
|
|
692
|
+
const predicateFn = predicate;
|
|
693
|
+
const nextBuilder = (iterable) => {
|
|
694
|
+
const prevIterable = this.prevBuilder(iterable);
|
|
695
|
+
async function* generator() {
|
|
696
|
+
for await (const item of prevIterable) {
|
|
697
|
+
/**
|
|
698
|
+
* Build the input object for the predicate and callback.
|
|
699
|
+
* Content chunks get a part object with the type, while meta chunks get undefined.
|
|
700
|
+
*/
|
|
701
|
+
const input = {
|
|
702
|
+
chunk: item.chunk,
|
|
703
|
+
part: item.partType !== void 0 ? { type: item.partType } : void 0
|
|
704
|
+
};
|
|
705
|
+
if (predicateFn(input)) await callback(input);
|
|
706
|
+
yield item;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return generator();
|
|
710
|
+
};
|
|
711
|
+
return new ChunkPipeline(this.sourceIterable, nextBuilder);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Executes the pipeline and returns the resulting stream.
|
|
715
|
+
* All chunks pass through including step boundaries and meta chunks.
|
|
716
|
+
*/
|
|
717
|
+
toStream() {
|
|
718
|
+
this.assertNotConsumed();
|
|
719
|
+
this.consumed = true;
|
|
720
|
+
const processedIterable = this.prevBuilder(this.sourceIterable);
|
|
721
|
+
/**
|
|
722
|
+
* Unwrap internal chunks by extracting just the chunk property.
|
|
723
|
+
* This removes the internal partType metadata used for pipeline operations.
|
|
724
|
+
*/
|
|
725
|
+
async function* generator() {
|
|
726
|
+
for await (const item of processedIterable) yield item.chunk;
|
|
754
727
|
}
|
|
728
|
+
return createAsyncIterableStream(convertAsyncIterableToStream(generator()));
|
|
729
|
+
}
|
|
730
|
+
[Symbol.asyncIterator]() {
|
|
731
|
+
return this.toStream()[Symbol.asyncIterator]();
|
|
755
732
|
}
|
|
756
|
-
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/pipe/pipe.ts
|
|
737
|
+
/**
|
|
738
|
+
* Creates an iterable with part type information from a raw chunk stream.
|
|
739
|
+
*
|
|
740
|
+
* Part type is derived directly from the chunk's type rather than from message.parts[-1],
|
|
741
|
+
* which ensures correct association when chunks from different part types are interleaved.
|
|
742
|
+
*
|
|
743
|
+
* Step boundaries (start-step/finish-step) pass through as-is with undefined partType.
|
|
744
|
+
* The AI SDK's readUIMessageStream handles all step boundary scenarios gracefully.
|
|
745
|
+
*/
|
|
746
|
+
async function* createIterable(input) {
|
|
747
|
+
/**
|
|
748
|
+
* Tracks toolCallId → partType mapping for tool chunks
|
|
749
|
+
*/
|
|
750
|
+
const toolCallIdMap = /* @__PURE__ */ new Map();
|
|
751
|
+
/**
|
|
752
|
+
* Use Symbol.asyncIterator if available (AsyncIterable), otherwise convert ReadableStream
|
|
753
|
+
*/
|
|
754
|
+
const iterable = Symbol.asyncIterator in input ? input : createAsyncIterableStream(input);
|
|
755
|
+
for await (const chunk of iterable) yield {
|
|
756
|
+
chunk,
|
|
757
|
+
partType: getPartTypeFromChunk(chunk, toolCallIdMap)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Creates a type-safe pipeline for UIMessageStream operations.
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* ```typescript
|
|
765
|
+
* pipe<MyUIMessage>(stream)
|
|
766
|
+
* .filter(isPartType('text'))
|
|
767
|
+
* .map(({ chunk }) => chunk)
|
|
768
|
+
* .toStream();
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
function pipe(input) {
|
|
772
|
+
return new ChunkPipeline(createIterable(input));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/pipe/type-guards.ts
|
|
777
|
+
/**
|
|
778
|
+
* Creates a filter guard that includes specific content chunk types.
|
|
779
|
+
* Use with `.filter()` to narrow to specific chunks.
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript
|
|
783
|
+
* pipe<MyUIMessage>(stream)
|
|
784
|
+
* .filter(includeChunks('text-delta'))
|
|
785
|
+
* .map(({ chunk }) => chunk); // chunk is narrowed to text-delta chunk
|
|
786
|
+
*
|
|
787
|
+
* pipe<MyUIMessage>(stream)
|
|
788
|
+
* .filter(includeChunks(['text-delta', 'text-end']))
|
|
789
|
+
* .map(({ chunk }) => chunk); // chunk is narrowed to text-delta | text-end
|
|
790
|
+
* ```
|
|
791
|
+
*/
|
|
792
|
+
function includeChunks(types) {
|
|
793
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
794
|
+
const guard = (input) => typeArray.includes(input.chunk.type);
|
|
795
|
+
return guard;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Creates a filter guard that includes specific part types.
|
|
799
|
+
* Use with `.filter()` to narrow to chunks belonging to specific parts.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```typescript
|
|
803
|
+
* pipe<MyUIMessage>(stream)
|
|
804
|
+
* .filter(includeParts('text'))
|
|
805
|
+
* .map(({ chunk, part }) => chunk); // chunk is narrowed to text chunks
|
|
806
|
+
*
|
|
807
|
+
* pipe<MyUIMessage>(stream)
|
|
808
|
+
* .filter(includeParts(['text', 'reasoning']))
|
|
809
|
+
* .map(({ chunk }) => chunk); // chunk is text or reasoning chunks
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
function includeParts(types) {
|
|
813
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
814
|
+
const guard = (input) => typeArray.includes(input.part?.type);
|
|
815
|
+
return guard;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Creates a filter guard that excludes specific content chunk types.
|
|
819
|
+
* Use with `.filter()` to keep all chunks except the specified types.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* pipe<MyUIMessage>(stream)
|
|
824
|
+
* .filter(excludeChunks('text-delta'))
|
|
825
|
+
* .map(({ chunk }) => chunk); // chunk is all content chunks except text-delta
|
|
826
|
+
*
|
|
827
|
+
* pipe<MyUIMessage>(stream)
|
|
828
|
+
* .filter(excludeChunks(['text-start', 'text-end']))
|
|
829
|
+
* .map(({ chunk }) => chunk); // excludes text-start and text-end
|
|
830
|
+
* ```
|
|
831
|
+
*/
|
|
832
|
+
function excludeChunks(types) {
|
|
833
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
834
|
+
const guard = (input) => !typeArray.includes(input.chunk.type);
|
|
835
|
+
return guard;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Creates a filter guard that excludes specific part types.
|
|
839
|
+
* Use with `.filter()` to keep all chunks except those belonging to specified parts.
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* ```typescript
|
|
843
|
+
* pipe<MyUIMessage>(stream)
|
|
844
|
+
* .filter(excludeParts('text'))
|
|
845
|
+
* .map(({ chunk }) => chunk); // excludes all text chunks
|
|
846
|
+
*
|
|
847
|
+
* pipe<MyUIMessage>(stream)
|
|
848
|
+
* .filter(excludeParts(['text', 'reasoning']))
|
|
849
|
+
* .map(({ chunk }) => chunk); // excludes text and reasoning chunks
|
|
850
|
+
* ```
|
|
851
|
+
*/
|
|
852
|
+
function excludeParts(types) {
|
|
853
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
854
|
+
const guard = (input) => !typeArray.includes(input.part?.type);
|
|
855
|
+
return guard;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Creates an observe guard that matches specific chunk types, including meta chunks.
|
|
859
|
+
* Use with `.on()` to observe specific chunks without filtering.
|
|
860
|
+
*
|
|
861
|
+
* @example
|
|
862
|
+
* ```typescript
|
|
863
|
+
* // Observe content chunks
|
|
864
|
+
* pipe<MyUIMessage>(stream)
|
|
865
|
+
* .on(chunkType('text-delta'), ({ chunk, part }) => {
|
|
866
|
+
* // chunk is text-delta, part is { type: 'text' }
|
|
867
|
+
* });
|
|
868
|
+
*
|
|
869
|
+
* // Observe meta chunks
|
|
870
|
+
* pipe<MyUIMessage>(stream)
|
|
871
|
+
* .on(chunkType('start'), ({ chunk, part }) => {
|
|
872
|
+
* // chunk is start chunk, part is undefined
|
|
873
|
+
* });
|
|
874
|
+
*
|
|
875
|
+
* // Observe multiple chunk types
|
|
876
|
+
* pipe<MyUIMessage>(stream)
|
|
877
|
+
* .on(chunkType(['text-delta', 'start']), ({ chunk, part }) => {
|
|
878
|
+
* // chunk is text-delta | start, part is { type: 'text' } | undefined
|
|
879
|
+
* });
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
function chunkType(types) {
|
|
883
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
884
|
+
const guard = (input) => typeArray.includes(input.chunk.type);
|
|
885
|
+
return guard;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Creates an observe guard that matches specific part types.
|
|
889
|
+
* Use with `.on()` to observe chunks belonging to specific parts without filtering.
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* ```typescript
|
|
893
|
+
* // Observe text parts
|
|
894
|
+
* pipe<MyUIMessage>(stream)
|
|
895
|
+
* .on(partType('text'), ({ chunk, part }) => {
|
|
896
|
+
* // chunk is text chunk, part is { type: 'text' }
|
|
897
|
+
* });
|
|
898
|
+
*
|
|
899
|
+
* // Observe multiple part types
|
|
900
|
+
* pipe<MyUIMessage>(stream)
|
|
901
|
+
* .on(partType(['text', 'reasoning']), ({ chunk, part }) => {
|
|
902
|
+
* // chunk is text | reasoning chunk, part is { type: 'text' | 'reasoning' }
|
|
903
|
+
* });
|
|
904
|
+
* ```
|
|
905
|
+
*/
|
|
906
|
+
function partType(types) {
|
|
907
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
908
|
+
const guard = (input) => typeArray.includes(input.part?.type);
|
|
909
|
+
return guard;
|
|
757
910
|
}
|
|
758
911
|
|
|
759
912
|
//#endregion
|
|
760
|
-
export { consumeUIMessageStream, excludeParts, filterUIMessageStream, flatMapUIMessageStream, includeParts, mapUIMessageStream, partTypeIs };
|
|
913
|
+
export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, mapUIMessageStream, partType, partTypeIs, pipe };
|