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 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
- ({ chunk, part }) => {
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
- ({ chunk, part }) => {
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
- The `toolCall()` guard matches tool chunks representing state transitions (not streaming events):
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.toolName, chunk.input);
226
+ console.log("Tool input:", chunk.input);
200
227
  })
201
228
  .on(chunkType("tool-output-available"), ({ chunk }) => {
202
- console.log("Tool output:", chunk.toolName, chunk.output);
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
- > [!NOTE]
218
- > The `tool` option filters by part type (`tool-{name}`). Dynamic tools have part type `dynamic-tool`, so `toolCall({ tool: "myTool" })` will not match dynamic tools. Use `toolCall()` without the `tool` option to observe all tools including dynamic ones.
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.2.0",
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",