ai-stream-utils 1.5.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 +202 -392
- package/dist/convert-ui-message-stream-to-sse-stream-CcQRXKju.mjs +165 -0
- package/dist/index.d.mts +559 -108
- package/dist/index.mjs +439 -257
- 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,172 +1,88 @@
|
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
static hasMarker(error, marker15) {
|
|
32
|
-
const markerSymbol = Symbol.for(marker15);
|
|
33
|
-
return error != null && typeof error === "object" && markerSymbol in error && typeof error[markerSymbol] === "boolean" && error[markerSymbol] === true;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var name$1 = "AI_APICallError";
|
|
37
|
-
var marker2 = `vercel.ai.error.${name$1}`;
|
|
38
|
-
var symbol2 = Symbol.for(marker2);
|
|
39
|
-
var name2 = "AI_EmptyResponseBodyError";
|
|
40
|
-
var marker3 = `vercel.ai.error.${name2}`;
|
|
41
|
-
var symbol3 = Symbol.for(marker3);
|
|
42
|
-
var name3 = "AI_InvalidArgumentError";
|
|
43
|
-
var marker4 = `vercel.ai.error.${name3}`;
|
|
44
|
-
var symbol4 = Symbol.for(marker4);
|
|
45
|
-
var _a4, _b4;
|
|
46
|
-
var InvalidArgumentError = class extends (_b4 = AISDKError, _a4 = symbol4, _b4) {
|
|
47
|
-
constructor({ message, cause, argument }) {
|
|
48
|
-
super({
|
|
49
|
-
name: name3,
|
|
50
|
-
message,
|
|
51
|
-
cause
|
|
52
|
-
});
|
|
53
|
-
this[_a4] = true;
|
|
54
|
-
this.argument = argument;
|
|
55
|
-
}
|
|
56
|
-
static isInstance(error) {
|
|
57
|
-
return AISDKError.hasMarker(error, marker4);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
var name4 = "AI_InvalidPromptError";
|
|
61
|
-
var marker5 = `vercel.ai.error.${name4}`;
|
|
62
|
-
var symbol5 = Symbol.for(marker5);
|
|
63
|
-
var name5 = "AI_InvalidResponseDataError";
|
|
64
|
-
var marker6 = `vercel.ai.error.${name5}`;
|
|
65
|
-
var symbol6 = Symbol.for(marker6);
|
|
66
|
-
var name6 = "AI_JSONParseError";
|
|
67
|
-
var marker7 = `vercel.ai.error.${name6}`;
|
|
68
|
-
var symbol7 = Symbol.for(marker7);
|
|
69
|
-
var name7 = "AI_LoadAPIKeyError";
|
|
70
|
-
var marker8 = `vercel.ai.error.${name7}`;
|
|
71
|
-
var symbol8 = Symbol.for(marker8);
|
|
72
|
-
var name8 = "AI_LoadSettingError";
|
|
73
|
-
var marker9 = `vercel.ai.error.${name8}`;
|
|
74
|
-
var symbol9 = Symbol.for(marker9);
|
|
75
|
-
var name9 = "AI_NoContentGeneratedError";
|
|
76
|
-
var marker10 = `vercel.ai.error.${name9}`;
|
|
77
|
-
var symbol10 = Symbol.for(marker10);
|
|
78
|
-
var name10 = "AI_NoSuchModelError";
|
|
79
|
-
var marker11 = `vercel.ai.error.${name10}`;
|
|
80
|
-
var symbol11 = Symbol.for(marker11);
|
|
81
|
-
var name11 = "AI_TooManyEmbeddingValuesForCallError";
|
|
82
|
-
var marker12 = `vercel.ai.error.${name11}`;
|
|
83
|
-
var symbol12 = Symbol.for(marker12);
|
|
84
|
-
var name12 = "AI_TypeValidationError";
|
|
85
|
-
var marker13 = `vercel.ai.error.${name12}`;
|
|
86
|
-
var symbol13 = Symbol.for(marker13);
|
|
87
|
-
var name13 = "AI_UnsupportedFunctionalityError";
|
|
88
|
-
var marker14 = `vercel.ai.error.${name13}`;
|
|
89
|
-
var symbol14 = Symbol.for(marker14);
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
//#region node_modules/.pnpm/@ai-sdk+provider-utils@4.0.8_zod@4.3.5/node_modules/@ai-sdk/provider-utils/dist/index.mjs
|
|
93
|
-
function convertAsyncIteratorToReadableStream(iterator) {
|
|
94
|
-
let cancelled = false;
|
|
95
|
-
return new ReadableStream({
|
|
96
|
-
async pull(controller) {
|
|
97
|
-
if (cancelled) return;
|
|
98
|
-
try {
|
|
99
|
-
const { value, done } = await iterator.next();
|
|
100
|
-
if (done) controller.close();
|
|
101
|
-
else controller.enqueue(value);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
controller.error(error);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
async cancel(reason) {
|
|
107
|
-
cancelled = true;
|
|
108
|
-
if (iterator.return) try {
|
|
109
|
-
await iterator.return(reason);
|
|
110
|
-
} catch (e) {}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
5
|
+
//#region src/consume/consume-ui-message-stream.ts
|
|
6
|
+
/**
|
|
7
|
+
* Consumes a UIMessageStream by fully reading it and returning the final UI message.
|
|
8
|
+
*
|
|
9
|
+
* This function uses `readUIMessageStream` to process all chunks from the stream
|
|
10
|
+
* and returns the last assembled message, which contains the complete content
|
|
11
|
+
* with all parts fully resolved.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const message = await consumeUIMessageStream(stream);
|
|
16
|
+
* console.log(message.parts); // All parts fully assembled
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const stream = result.toUIMessageStream();
|
|
22
|
+
* const filteredStream = filterUIMessageStream(stream, includeParts(['text']));
|
|
23
|
+
* const message = await consumeUIMessageStream(filteredStream);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
async function consumeUIMessageStream(stream) {
|
|
27
|
+
let lastMessage;
|
|
28
|
+
for await (const message of readUIMessageStream({ stream })) lastMessage = message;
|
|
29
|
+
if (!lastMessage) throw new Error("Unexpected: stream ended without producing any messages");
|
|
30
|
+
return lastMessage;
|
|
113
31
|
}
|
|
114
|
-
var { btoa, atob } = globalThis;
|
|
115
|
-
var name = "AI_DownloadError";
|
|
116
|
-
var marker = `vercel.ai.error.${name}`;
|
|
117
|
-
var symbol = Symbol.for(marker);
|
|
118
|
-
var createIdGenerator = ({ prefix, size = 16, alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", separator = "-" } = {}) => {
|
|
119
|
-
const generator = () => {
|
|
120
|
-
const alphabetLength = alphabet.length;
|
|
121
|
-
const chars = new Array(size);
|
|
122
|
-
for (let i = 0; i < size; i++) chars[i] = alphabet[Math.random() * alphabetLength | 0];
|
|
123
|
-
return chars.join("");
|
|
124
|
-
};
|
|
125
|
-
if (prefix == null) return generator;
|
|
126
|
-
if (alphabet.includes(separator)) throw new InvalidArgumentError({
|
|
127
|
-
argument: "separator",
|
|
128
|
-
message: `The separator "${separator}" must not be part of the alphabet "${alphabet}".`
|
|
129
|
-
});
|
|
130
|
-
return () => `${prefix}${separator}${generator()}`;
|
|
131
|
-
};
|
|
132
|
-
var generateId = createIdGenerator();
|
|
133
|
-
var schemaSymbol = Symbol.for("vercel.ai.schema");
|
|
134
32
|
|
|
135
33
|
//#endregion
|
|
136
|
-
//#region src/
|
|
34
|
+
//#region src/internal/get-part-type-from-chunk.ts
|
|
137
35
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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
|
|
140
43
|
*/
|
|
141
|
-
function getPartTypeFromChunk(chunk,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
case `tool-input-available`:
|
|
145
|
-
case `tool-input-error`: {
|
|
146
|
-
const toolChunk = chunk;
|
|
147
|
-
return toolChunk.dynamic ? `dynamic-tool` : `tool-${toolChunk.toolName}`;
|
|
148
|
-
}
|
|
149
|
-
case `tool-input-delta`:
|
|
150
|
-
case `tool-output-available`:
|
|
151
|
-
case `tool-output-error`: {
|
|
152
|
-
const toolChunk = chunk;
|
|
153
|
-
if (toolChunk.dynamic) return `dynamic-tool`;
|
|
154
|
-
const toolState = toolCallStates?.get(toolChunk.toolCallId);
|
|
155
|
-
if (toolState) return toolState.dynamic ? `dynamic-tool` : `tool-${toolState.toolName}`;
|
|
156
|
-
return `dynamic-tool`;
|
|
157
|
-
}
|
|
44
|
+
function getPartTypeFromChunk(chunk, toolCallIdMap) {
|
|
45
|
+
const chunkType = chunk.type;
|
|
46
|
+
switch (chunkType) {
|
|
158
47
|
case `text-start`:
|
|
159
48
|
case `text-delta`:
|
|
160
49
|
case `text-end`: return `text`;
|
|
161
50
|
case `reasoning-start`:
|
|
162
51
|
case `reasoning-delta`:
|
|
163
52
|
case `reasoning-end`: return `reasoning`;
|
|
164
|
-
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
|
+
}
|
|
165
69
|
case `source-url`: return `source-url`;
|
|
166
70
|
case `source-document`: return `source-document`;
|
|
167
|
-
|
|
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;
|
|
168
80
|
}
|
|
81
|
+
throw new Error(`Unable to derive part type from chunk type: ${chunkType}`);
|
|
169
82
|
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/internal/utils.ts
|
|
170
86
|
/**
|
|
171
87
|
* Checks if a chunk is a control/meta chunk that should always pass through.
|
|
172
88
|
*/
|
|
@@ -201,7 +117,7 @@ function asArray(value) {
|
|
|
201
117
|
}
|
|
202
118
|
|
|
203
119
|
//#endregion
|
|
204
|
-
//#region src/
|
|
120
|
+
//#region src/internal/create-ui-message-stream-reader.ts
|
|
205
121
|
/**
|
|
206
122
|
* Creates an async generator that wraps a UIMessageStream with readUIMessageStream.
|
|
207
123
|
*
|
|
@@ -226,7 +142,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
226
142
|
const iterator = readUIMessageStream({ stream: new ReadableStream({ start(c) {
|
|
227
143
|
controller = c;
|
|
228
144
|
} }) })[Symbol.asyncIterator]();
|
|
229
|
-
const
|
|
145
|
+
const toolCallIdMap = /* @__PURE__ */ new Map();
|
|
230
146
|
try {
|
|
231
147
|
while (true) {
|
|
232
148
|
const { done, value: chunk } = await inputReader.read();
|
|
@@ -234,13 +150,6 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
234
150
|
controller.close();
|
|
235
151
|
break;
|
|
236
152
|
}
|
|
237
|
-
if (chunk.type === `tool-input-start`) {
|
|
238
|
-
const toolChunk = chunk;
|
|
239
|
-
toolCallStates.set(toolChunk.toolCallId, {
|
|
240
|
-
toolName: toolChunk.toolName,
|
|
241
|
-
dynamic: toolChunk.dynamic
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
153
|
if (isMetaChunk(chunk) || isStepStartChunk(chunk) || isStepEndChunk(chunk)) {
|
|
245
154
|
controller.enqueue(chunk);
|
|
246
155
|
yield {
|
|
@@ -262,7 +171,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
262
171
|
const result = await iterator.next();
|
|
263
172
|
const message = result.done ? void 0 : result.value;
|
|
264
173
|
const messagePart = message?.parts[message.parts.length - 1];
|
|
265
|
-
const expectedPartType = getPartTypeFromChunk(chunk,
|
|
174
|
+
const expectedPartType = getPartTypeFromChunk(chunk, toolCallIdMap);
|
|
266
175
|
yield {
|
|
267
176
|
chunk,
|
|
268
177
|
message,
|
|
@@ -279,7 +188,7 @@ async function* createUIMessageStreamReader(inputStream) {
|
|
|
279
188
|
}
|
|
280
189
|
|
|
281
190
|
//#endregion
|
|
282
|
-
//#region src/map-ui-message-stream.ts
|
|
191
|
+
//#region src/map/map-ui-message-stream.ts
|
|
283
192
|
/**
|
|
284
193
|
* Maps/filters a UIMessageStream at the chunk level using readUIMessageStream.
|
|
285
194
|
*
|
|
@@ -393,39 +302,11 @@ function mapUIMessageStream(stream, mapFn) {
|
|
|
393
302
|
if (chunks.length > 0) yield* emitChunks(chunks);
|
|
394
303
|
}
|
|
395
304
|
}
|
|
396
|
-
return createAsyncIterableStream(
|
|
305
|
+
return createAsyncIterableStream(convertAsyncIterableToStream(processChunks()));
|
|
397
306
|
}
|
|
398
307
|
|
|
399
308
|
//#endregion
|
|
400
|
-
//#region src/filter-ui-message-stream.ts
|
|
401
|
-
/**
|
|
402
|
-
* Creates a filter predicate that includes only the specified part types.
|
|
403
|
-
*
|
|
404
|
-
* @example
|
|
405
|
-
* ```typescript
|
|
406
|
-
* filterUIMessageStream(stream, includeParts(['text', 'tool-weather']));
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
function includeParts(includePartTypes) {
|
|
410
|
-
return ({ part }) => {
|
|
411
|
-
const partType = part.type;
|
|
412
|
-
return includePartTypes.includes(partType);
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Creates a filter predicate that excludes the specified part types.
|
|
417
|
-
*
|
|
418
|
-
* @example
|
|
419
|
-
* ```typescript
|
|
420
|
-
* filterUIMessageStream(stream, excludeParts(['reasoning', 'tool-calculator']));
|
|
421
|
-
* ```
|
|
422
|
-
*/
|
|
423
|
-
function excludeParts(excludePartTypes) {
|
|
424
|
-
return ({ part }) => {
|
|
425
|
-
const partType = part.type;
|
|
426
|
-
return !excludePartTypes.includes(partType);
|
|
427
|
-
};
|
|
428
|
-
}
|
|
309
|
+
//#region src/filter/filter-ui-message-stream.ts
|
|
429
310
|
/**
|
|
430
311
|
* Filters a UIMessageStream to include or exclude specific chunks.
|
|
431
312
|
*
|
|
@@ -468,7 +349,11 @@ function filterUIMessageStream(stream, predicate) {
|
|
|
468
349
|
}
|
|
469
350
|
|
|
470
351
|
//#endregion
|
|
471
|
-
//#region src/
|
|
352
|
+
//#region src/internal/serialize-part-to-chunks.ts
|
|
353
|
+
const generateId = createIdGenerator({
|
|
354
|
+
prefix: `aitxt`,
|
|
355
|
+
size: 24
|
|
356
|
+
});
|
|
472
357
|
/**
|
|
473
358
|
* Type guard to check if a message part is a source-url part.
|
|
474
359
|
*/
|
|
@@ -488,27 +373,16 @@ function isStepStartUIPart(part) {
|
|
|
488
373
|
return part.type === "step-start";
|
|
489
374
|
}
|
|
490
375
|
/**
|
|
491
|
-
* Extracts the part ID from a list of chunks belonging to the same part.
|
|
492
|
-
*/
|
|
493
|
-
function getPartId(chunks) {
|
|
494
|
-
for (const chunk of chunks) {
|
|
495
|
-
if ("id" in chunk && chunk.id) return chunk.id;
|
|
496
|
-
if ("toolCallId" in chunk && chunk.toolCallId) return chunk.toolCallId;
|
|
497
|
-
}
|
|
498
|
-
return "unknown";
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
376
|
* Serializes a UIMessagePart back to chunks (without step boundaries).
|
|
502
377
|
*
|
|
503
378
|
* This function converts a complete part (e.g., TextUIPart, ToolUIPart)
|
|
504
379
|
* back into the chunk format used by UIMessageStream.
|
|
505
380
|
*
|
|
506
381
|
* @param part - The part to serialize
|
|
507
|
-
* @param originalChunks - Original chunks for extracting IDs (for text/reasoning parts)
|
|
508
382
|
* @returns Array of chunks representing the part
|
|
509
383
|
*/
|
|
510
|
-
function serializePartToChunks(part
|
|
511
|
-
if (isStepStartUIPart(part)) return
|
|
384
|
+
function serializePartToChunks(part) {
|
|
385
|
+
if (isStepStartUIPart(part)) return [{ type: "start-step" }];
|
|
512
386
|
if (isFileUIPart(part)) return [{
|
|
513
387
|
type: "file",
|
|
514
388
|
mediaType: part.mediaType,
|
|
@@ -534,41 +408,46 @@ function serializePartToChunks(part, originalChunks) {
|
|
|
534
408
|
type: part.type,
|
|
535
409
|
data: part.data
|
|
536
410
|
}];
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
+
}
|
|
572
451
|
if (isToolOrDynamicToolUIPart(part)) {
|
|
573
452
|
const dynamic = part.type === "dynamic-tool";
|
|
574
453
|
const chunks = [{
|
|
@@ -604,11 +483,11 @@ function serializePartToChunks(part, originalChunks) {
|
|
|
604
483
|
});
|
|
605
484
|
return chunks;
|
|
606
485
|
}
|
|
607
|
-
|
|
486
|
+
throw new Error(`Cannot serialize unknown part type: ${part.type}`);
|
|
608
487
|
}
|
|
609
488
|
|
|
610
489
|
//#endregion
|
|
611
|
-
//#region src/flat-map-ui-message-stream.ts
|
|
490
|
+
//#region src/flat-map/flat-map-ui-message-stream.ts
|
|
612
491
|
/**
|
|
613
492
|
* Creates a predicate that matches parts by their type.
|
|
614
493
|
*/
|
|
@@ -663,7 +542,7 @@ function flatMapUIMessageStream(...args) {
|
|
|
663
542
|
index: allParts.length - 1,
|
|
664
543
|
parts: allParts
|
|
665
544
|
}));
|
|
666
|
-
for (const part of parts) yield* emitChunks(serializePartToChunks(part
|
|
545
|
+
for (const part of parts) yield* emitChunks(serializePartToChunks(part));
|
|
667
546
|
bufferedChunks = [];
|
|
668
547
|
lastBufferedPart = void 0;
|
|
669
548
|
}
|
|
@@ -681,7 +560,7 @@ function flatMapUIMessageStream(...args) {
|
|
|
681
560
|
continue;
|
|
682
561
|
}
|
|
683
562
|
if (isStepEndChunk(chunk)) {
|
|
684
|
-
if (currentMode ===
|
|
563
|
+
if (currentMode === `buffering` && lastBufferedPart) yield* flushBufferedPart(lastBufferedPart);
|
|
685
564
|
if (stepStartEmitted) {
|
|
686
565
|
yield chunk;
|
|
687
566
|
stepStartEmitted = false;
|
|
@@ -697,35 +576,338 @@ function flatMapUIMessageStream(...args) {
|
|
|
697
576
|
index: allParts.length - 1,
|
|
698
577
|
parts: allParts
|
|
699
578
|
}));
|
|
700
|
-
for (const part
|
|
579
|
+
for (const part of parts) yield* emitChunks([part]);
|
|
701
580
|
continue;
|
|
702
581
|
}
|
|
703
|
-
if (!message) throw new Error(
|
|
582
|
+
if (!message) throw new Error(`Unexpected: received content chunk but message is undefined`);
|
|
704
583
|
const currentPart = part;
|
|
705
584
|
if (!currentPart) throw new Error(`Unexpected: received content chunk but part is undefined`);
|
|
706
585
|
if (message.parts.length > lastPartCount) {
|
|
707
|
-
if (currentMode ===
|
|
708
|
-
if (currentMode ===
|
|
586
|
+
if (currentMode === `buffering` && lastBufferedPart) yield* flushBufferedPart(lastBufferedPart);
|
|
587
|
+
if (currentMode === `streaming` && lastPartCount > 0) {
|
|
709
588
|
const previousPart = message.parts[lastPartCount - 1];
|
|
710
589
|
if (previousPart) allParts.push(previousPart);
|
|
711
590
|
}
|
|
712
591
|
if (!predicate || predicate(currentPart)) {
|
|
713
|
-
currentMode =
|
|
592
|
+
currentMode = `buffering`;
|
|
714
593
|
bufferedChunks = [chunk];
|
|
715
594
|
lastBufferedPart = currentPart;
|
|
716
595
|
} else {
|
|
717
|
-
currentMode =
|
|
596
|
+
currentMode = `streaming`;
|
|
718
597
|
yield* emitChunks([chunk]);
|
|
719
598
|
}
|
|
720
599
|
lastPartCount = message.parts.length;
|
|
721
|
-
} else if (currentMode ===
|
|
600
|
+
} else if (currentMode === `buffering`) {
|
|
722
601
|
bufferedChunks.push(chunk);
|
|
723
602
|
lastBufferedPart = currentPart;
|
|
724
|
-
} 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;
|
|
725
727
|
}
|
|
728
|
+
return createAsyncIterableStream(convertAsyncIterableToStream(generator()));
|
|
729
|
+
}
|
|
730
|
+
[Symbol.asyncIterator]() {
|
|
731
|
+
return this.toStream()[Symbol.asyncIterator]();
|
|
726
732
|
}
|
|
727
|
-
|
|
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;
|
|
728
910
|
}
|
|
729
911
|
|
|
730
912
|
//#endregion
|
|
731
|
-
export { 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 };
|