ai-stream-utils 2.2.0 → 2.3.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 +60 -11
- package/dist/index.d.mts +44 -2
- package/dist/index.mjs +57 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -145,6 +145,31 @@ const stream = pipe(result.toUIMessageStream())
|
|
|
145
145
|
.toStream();
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
#### Helpers
|
|
149
|
+
|
|
150
|
+
`transformProviderMetadata()` changes `providerMetadata` on the chunks that carry it (`text-*`, `reasoning-*`, `tool-input-*`, `source-*`, `file`). Every other chunk passes through untouched, so you don't have to narrow by chunk type yourself.
|
|
151
|
+
|
|
152
|
+
The callback receives the current `metadata` (may be `undefined`) and returns one of three things:
|
|
153
|
+
|
|
154
|
+
- an **object** to set/replace the metadata (merge by spreading `metadata`)
|
|
155
|
+
- **`undefined`** to leave the chunk unchanged
|
|
156
|
+
- **`null`** to remove the `providerMetadata` field entirely (the chunk still passes through)
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const stream = pipe(result.toUIMessageStream())
|
|
160
|
+
.map(
|
|
161
|
+
transformProviderMetadata(({ chunk, part, metadata }) => {
|
|
162
|
+
if (chunk.type === "tool-input-available")
|
|
163
|
+
return { ...metadata, app: { toolCallId: chunk.toolCallId } }; // add to tool input chunks
|
|
164
|
+
|
|
165
|
+
if (part.type === "text") return null; // delete for text parts
|
|
166
|
+
|
|
167
|
+
return undefined; // leave everything else unchanged
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
.toStream();
|
|
171
|
+
```
|
|
172
|
+
|
|
148
173
|
### `.on()`
|
|
149
174
|
|
|
150
175
|
Observe chunks without modifying the stream. The callback is invoked for matching chunks.
|
|
@@ -152,11 +177,13 @@ Observe chunks without modifying the stream. The callback is invoked for matchin
|
|
|
152
177
|
```typescript
|
|
153
178
|
const stream = pipe(result.toUIMessageStream())
|
|
154
179
|
.on(
|
|
155
|
-
(
|
|
180
|
+
(predicate) => {
|
|
181
|
+
const { chunk, part } = predicate;
|
|
156
182
|
// return true to invoke callback, false to skip
|
|
157
183
|
return chunk.type === "text-delta";
|
|
158
184
|
},
|
|
159
|
-
(
|
|
185
|
+
(callback) => {
|
|
186
|
+
const { chunk, part } = callback;
|
|
160
187
|
// callback invoked for matching chunks
|
|
161
188
|
console.log(chunk, part);
|
|
162
189
|
},
|
|
@@ -172,7 +199,10 @@ const stream = pipe(result.toUIMessageStream())
|
|
|
172
199
|
- `partType("text")` or `partType(["text", "reasoning"])`: Observe chunks belonging to specific part types
|
|
173
200
|
- `toolCall()` or `toolCall({ tool: "weather" })` or `toolCall({ state: "output-available" })`: Observe tool state transitions
|
|
174
201
|
|
|
175
|
-
|
|
202
|
+
> [!NOTE]
|
|
203
|
+
> The `partType` type guard still operates on chunks. That means `partType("text")` will match any text chunks such as `text-start`, `text-delta`, and `text-end`.
|
|
204
|
+
|
|
205
|
+
The `toolCall()` type guard matches tool chunks representing state transitions (not streaming events):
|
|
176
206
|
|
|
177
207
|
- `input-available`: Tool input fully parsed
|
|
178
208
|
- `approval-requested`: Tool awaiting user approval
|
|
@@ -180,9 +210,6 @@ The `toolCall()` guard matches tool chunks representing state transitions (not s
|
|
|
180
210
|
- `output-error`: Tool execution failed
|
|
181
211
|
- `output-denied`: User denied approval
|
|
182
212
|
|
|
183
|
-
> [!NOTE]
|
|
184
|
-
> The `partType` type guard still operates on chunks. That means `partType("text")` will match any text chunks such as `text-start`, `text-delta`, and `text-end`.
|
|
185
|
-
|
|
186
213
|
#### Examples
|
|
187
214
|
|
|
188
215
|
Log stream lifecycle events.
|
|
@@ -196,26 +223,48 @@ const stream = pipe(result.toUIMessageStream())
|
|
|
196
223
|
console.log("Stream finished:", chunk.finishReason);
|
|
197
224
|
})
|
|
198
225
|
.on(chunkType("tool-input-available"), ({ chunk }) => {
|
|
199
|
-
console.log("Tool input:", chunk.
|
|
226
|
+
console.log("Tool input:", chunk.input);
|
|
200
227
|
})
|
|
201
228
|
.on(chunkType("tool-output-available"), ({ chunk }) => {
|
|
202
|
-
console.log("Tool output:", chunk.
|
|
229
|
+
console.log("Tool output:", chunk.output);
|
|
203
230
|
})
|
|
204
231
|
.toStream();
|
|
205
232
|
```
|
|
206
233
|
|
|
207
|
-
Observe tool state transitions.
|
|
234
|
+
Observe tool state transitions for a specific tool.
|
|
208
235
|
|
|
209
236
|
```typescript
|
|
210
237
|
const stream = pipe(result.toUIMessageStream())
|
|
211
238
|
.on(toolCall({ tool: "weather", state: "approval-requested" }), ({ chunk }) => {
|
|
212
239
|
console.log("Weather tool needs approval");
|
|
213
240
|
})
|
|
241
|
+
.on(toolCall({ tool: "weather", state: "output-available" }), ({ chunk }) => {
|
|
242
|
+
console.log("Weather output:", chunk.output);
|
|
243
|
+
})
|
|
244
|
+
.on(toolCall({ tool: "weather", state: "input-available" }), ({ chunk }) => {
|
|
245
|
+
console.log("Weather input:", chunk.input);
|
|
246
|
+
})
|
|
214
247
|
.toStream();
|
|
215
248
|
```
|
|
216
249
|
|
|
217
|
-
|
|
218
|
-
|
|
250
|
+
Observe all tool calls.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const stream = pipe(result.toUIMessageStream())
|
|
254
|
+
// on
|
|
255
|
+
.on(toolCall({ state: `input-available` }), ({ chunk, part }) => {
|
|
256
|
+
console.log(`Tool call ${part.type} (${chunk.toolCallId}) input=`, chunk.input);
|
|
257
|
+
})
|
|
258
|
+
/** onResult */
|
|
259
|
+
.on(toolCall({ state: `output-available` }), ({ chunk, part }) => {
|
|
260
|
+
console.log(`Tool result ${part.type} (${chunk.toolCallId}) output=`, chunk.output);
|
|
261
|
+
})
|
|
262
|
+
/** onError */
|
|
263
|
+
.on(toolCall({ state: `output-error` }), ({ chunk, part }) => {
|
|
264
|
+
console.log(`Tool error ${part.type} (${chunk.toolCallId}) error=`, chunk.errorText);
|
|
265
|
+
})
|
|
266
|
+
.toStream();
|
|
267
|
+
```
|
|
219
268
|
|
|
220
269
|
### `.toStream()`
|
|
221
270
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as convertSSEToUIMessageStream, c as convertArrayToStream, i as convertStreamToArray, l as convertArrayToAsyncIterable, n as createAsyncIterableStream, o as convertAsyncIterableToStream, r as convertUIMessageToSSEStream, s as convertAsyncIterableToArray, t as AsyncIterableStream } from "./types-CNApJ39t.mjs";
|
|
2
2
|
import "./utils/index.mjs";
|
|
3
|
-
import { AsyncIterableStream as AsyncIterableStream$1, InferUIMessageChunk, UIDataTypes, UIMessage, UITools } from "ai";
|
|
3
|
+
import { AsyncIterableStream as AsyncIterableStream$1, InferUIMessageChunk, ProviderMetadata, UIDataTypes, UIMessage, UITools } from "ai";
|
|
4
4
|
|
|
5
5
|
//#region src/consume/consume-ui-message-stream.d.ts
|
|
6
6
|
/**
|
|
@@ -553,6 +553,48 @@ type MapUIMessageStreamFn<UI_MESSAGE extends UIMessage> = (input: MapInput<UI_ME
|
|
|
553
553
|
*/
|
|
554
554
|
declare function mapUIMessageStream<UI_MESSAGE extends UIMessage>(stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>, mapFn: MapUIMessageStreamFn<UI_MESSAGE>): AsyncIterableStream$1<InferUIMessageChunk<UI_MESSAGE>>;
|
|
555
555
|
//#endregion
|
|
556
|
+
//#region src/pipe/transform-provider-metadata.d.ts
|
|
557
|
+
/**
|
|
558
|
+
* A composable helper that plugs into the `.map()` operator to change provider
|
|
559
|
+
* metadata on the chunks that carry it:
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
563
|
+
* pipe<MyUIMessage>(stream)
|
|
564
|
+
* .map(transformProviderMetadata(({ metadata }) => ({ ...metadata, app: { id } })))
|
|
565
|
+
* .toStream();
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
/**
|
|
569
|
+
* Chunk variants in UI_MESSAGE that actually carry a `providerMetadata` field.
|
|
570
|
+
* Optional keys are still present in `keyof`, so this discriminates correctly:
|
|
571
|
+
* variants without the field (error, tool-output-*, start, finish, ...) drop to never.
|
|
572
|
+
*/
|
|
573
|
+
type ProviderMetadataChunk<UI_MESSAGE extends UIMessage> = InferUIMessageChunk<UI_MESSAGE> extends infer CHUNK ? CHUNK extends unknown ? "providerMetadata" extends keyof CHUNK ? CHUNK : never : never : never;
|
|
574
|
+
/**
|
|
575
|
+
* Mapper invoked for every metadata-bearing chunk. Receives the chunk, its part
|
|
576
|
+
* type, and the current `providerMetadata` (may be undefined). Return value:
|
|
577
|
+
* - an object to set/replace `providerMetadata` (merge by spreading `metadata`)
|
|
578
|
+
* - `undefined` to leave the chunk unchanged
|
|
579
|
+
* - `null` to remove the `providerMetadata` field entirely
|
|
580
|
+
*
|
|
581
|
+
* `null` removes the field, not the chunk: the chunk always passes through.
|
|
582
|
+
*/
|
|
583
|
+
type ProviderMetadataTransformFn<UI_MESSAGE extends UIMessage> = (input: {
|
|
584
|
+
chunk: ProviderMetadataChunk<UI_MESSAGE>;
|
|
585
|
+
part: {
|
|
586
|
+
type: string;
|
|
587
|
+
};
|
|
588
|
+
metadata: ProviderMetadata | undefined;
|
|
589
|
+
}) => ProviderMetadata | null | undefined;
|
|
590
|
+
/**
|
|
591
|
+
* Creates a `.map()` callback that rewrites `providerMetadata` on metadata-bearing
|
|
592
|
+
* chunks and passes every other chunk through unchanged.
|
|
593
|
+
*/
|
|
594
|
+
declare function transformProviderMetadata<UI_MESSAGE extends UIMessage>(fn: ProviderMetadataTransformFn<UI_MESSAGE>): ChunkMapFn<UI_MESSAGE, InferUIMessageChunk<UI_MESSAGE>, {
|
|
595
|
+
type: string;
|
|
596
|
+
}>;
|
|
597
|
+
//#endregion
|
|
556
598
|
//#region src/pipe/chunk-pipeline.d.ts
|
|
557
599
|
/**
|
|
558
600
|
* Pipeline for chunk-based operations.
|
|
@@ -859,4 +901,4 @@ declare function toolCall<UI_MESSAGE extends UIMessage, STATE extends ToolCallSt
|
|
|
859
901
|
type: `tool-${InferToolName<UI_MESSAGE>}` | "dynamic-tool";
|
|
860
902
|
}>;
|
|
861
903
|
//#endregion
|
|
862
|
-
export { AsyncIterableStream, type FlatMapContext, type FlatMapInput, type FlatMapUIMessageStreamFn, type FlatMapUIMessageStreamPredicate, type MapInput, type MapUIMessageStreamFn, chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe, toolCall };
|
|
904
|
+
export { AsyncIterableStream, type FlatMapContext, type FlatMapInput, type FlatMapUIMessageStreamFn, type FlatMapUIMessageStreamPredicate, type MapInput, type MapUIMessageStreamFn, chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe, toolCall, transformProviderMetadata };
|
package/dist/index.mjs
CHANGED
|
@@ -606,6 +606,62 @@ function flatMapUIMessageStream(...args) {
|
|
|
606
606
|
return createAsyncIterableStream(convertAsyncIterableToStream(processChunks()));
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/pipe/transform-provider-metadata.ts
|
|
611
|
+
/**
|
|
612
|
+
* Runtime allow-list whose keys are pinned to `ProviderMetadataChunkType` by the
|
|
613
|
+
* compiler. The AI SDK does not expose its zod chunk union for runtime
|
|
614
|
+
* introspection (`uiMessageChunkSchema` is an async, non-introspectable `Schema`
|
|
615
|
+
* wrapper), so completeness is enforced at the type level instead: if the SDK
|
|
616
|
+
* adds or removes a metadata-bearing chunk, `ProviderMetadataChunkType` changes
|
|
617
|
+
* and this literal stops compiling (missing key, or unknown key) until updated.
|
|
618
|
+
*/
|
|
619
|
+
const PROVIDER_METADATA_CHUNK_TYPES = {
|
|
620
|
+
"text-start": true,
|
|
621
|
+
"text-delta": true,
|
|
622
|
+
"text-end": true,
|
|
623
|
+
"reasoning-start": true,
|
|
624
|
+
"reasoning-delta": true,
|
|
625
|
+
"reasoning-end": true,
|
|
626
|
+
"tool-input-start": true,
|
|
627
|
+
"tool-input-available": true,
|
|
628
|
+
"tool-input-error": true,
|
|
629
|
+
"source-url": true,
|
|
630
|
+
"source-document": true,
|
|
631
|
+
file: true
|
|
632
|
+
};
|
|
633
|
+
/** Narrows any chunk to the metadata-bearing union via the pinned allow-list. */
|
|
634
|
+
function isProviderMetadataChunk(chunk) {
|
|
635
|
+
return Object.hasOwn(PROVIDER_METADATA_CHUNK_TYPES, chunk.type);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Creates a `.map()` callback that rewrites `providerMetadata` on metadata-bearing
|
|
639
|
+
* chunks and passes every other chunk through unchanged.
|
|
640
|
+
*/
|
|
641
|
+
function transformProviderMetadata(fn) {
|
|
642
|
+
return ({ chunk, part }) => {
|
|
643
|
+
if (!isProviderMetadataChunk(chunk)) return chunk;
|
|
644
|
+
const metadata = chunk.providerMetadata;
|
|
645
|
+
const next = fn({
|
|
646
|
+
chunk,
|
|
647
|
+
part,
|
|
648
|
+
metadata
|
|
649
|
+
});
|
|
650
|
+
/** undefined leaves the chunk unchanged. */
|
|
651
|
+
if (next === void 0) return chunk;
|
|
652
|
+
/** null removes the field while keeping the chunk. */
|
|
653
|
+
if (next === null) {
|
|
654
|
+
const { providerMetadata: _omit, ...rest } = chunk;
|
|
655
|
+
return rest;
|
|
656
|
+
}
|
|
657
|
+
/** an object sets/replaces the field. */
|
|
658
|
+
return {
|
|
659
|
+
...chunk,
|
|
660
|
+
providerMetadata: next
|
|
661
|
+
};
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
609
665
|
//#endregion
|
|
610
666
|
//#region src/pipe/chunk-pipeline.ts
|
|
611
667
|
/**
|
|
@@ -968,4 +1024,4 @@ function toolCall(options) {
|
|
|
968
1024
|
}
|
|
969
1025
|
|
|
970
1026
|
//#endregion
|
|
971
|
-
export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe, toolCall };
|
|
1027
|
+
export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe, toolCall, transformProviderMetadata };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-stream-utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI SDK: Filter and transform UI messages while streaming to the client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
47
47
|
"@types/node": "^25.2.3",
|
|
48
48
|
"ai": "^6.0.86",
|
|
49
|
+
"dotenv": "^17.4.2",
|
|
49
50
|
"husky": "^9.1.7",
|
|
50
51
|
"lint-staged": "^16.2.7",
|
|
51
52
|
"msw": "^2.12.10",
|