ai-stream-utils 2.1.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
  },
@@ -170,10 +197,19 @@ const stream = pipe(result.toUIMessageStream())
170
197
 
171
198
  - `chunkType("text-delta")` or `chunkType(["start", "finish"])`: Observe specific chunk types
172
199
  - `partType("text")` or `partType(["text", "reasoning"])`: Observe chunks belonging to specific part types
200
+ - `toolCall()` or `toolCall({ tool: "weather" })` or `toolCall({ state: "output-available" })`: Observe tool state transitions
173
201
 
174
202
  > [!NOTE]
175
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`.
176
204
 
205
+ The `toolCall()` type guard matches tool chunks representing state transitions (not streaming events):
206
+
207
+ - `input-available`: Tool input fully parsed
208
+ - `approval-requested`: Tool awaiting user approval
209
+ - `output-available`: Tool execution completed
210
+ - `output-error`: Tool execution failed
211
+ - `output-denied`: User denied approval
212
+
177
213
  #### Examples
178
214
 
179
215
  Log stream lifecycle events.
@@ -187,10 +223,45 @@ const stream = pipe(result.toUIMessageStream())
187
223
  console.log("Stream finished:", chunk.finishReason);
188
224
  })
189
225
  .on(chunkType("tool-input-available"), ({ chunk }) => {
190
- console.log("Tool input:", chunk.toolName, chunk.input);
226
+ console.log("Tool input:", chunk.input);
191
227
  })
192
228
  .on(chunkType("tool-output-available"), ({ chunk }) => {
193
- console.log("Tool output:", chunk.toolName, chunk.output);
229
+ console.log("Tool output:", chunk.output);
230
+ })
231
+ .toStream();
232
+ ```
233
+
234
+ Observe tool state transitions for a specific tool.
235
+
236
+ ```typescript
237
+ const stream = pipe(result.toUIMessageStream())
238
+ .on(toolCall({ tool: "weather", state: "approval-requested" }), ({ chunk }) => {
239
+ console.log("Weather tool needs approval");
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
+ })
247
+ .toStream();
248
+ ```
249
+
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);
194
265
  })
195
266
  .toStream();
196
267
  ```
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
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-B4nePmEd.mjs";
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
  /**
@@ -345,6 +345,21 @@ type ToolChunkTypes<UI_MESSAGE extends UIMessage> = Extract<InferUIMessageChunkT
345
345
  * Content chunk types remaining after excluding all tool chunks.
346
346
  */
347
347
  type ExcludeToolChunkTypes<UI_MESSAGE extends UIMessage> = Exclude<ContentChunkType<UI_MESSAGE>, ToolChunkTypes<UI_MESSAGE>>;
348
+ /**
349
+ * Tool states that have corresponding stream chunks.
350
+ * These are the "final" states from UIToolInvocation, not streaming events.
351
+ */
352
+ type ToolCallState = "input-available" | "approval-requested" | "output-available" | "output-error" | "output-denied";
353
+ /**
354
+ * Map tool states to their corresponding chunk types.
355
+ */
356
+ type ToolStateToChunkType = {
357
+ "input-available": "tool-input-available";
358
+ "approval-requested": "tool-approval-request";
359
+ "output-available": "tool-output-available";
360
+ "output-error": "tool-output-error";
361
+ "output-denied": "tool-output-denied";
362
+ };
348
363
  //#endregion
349
364
  //#region src/flat-map/flat-map-ui-message-stream.d.ts
350
365
  /**
@@ -538,6 +553,48 @@ type MapUIMessageStreamFn<UI_MESSAGE extends UIMessage> = (input: MapInput<UI_ME
538
553
  */
539
554
  declare function mapUIMessageStream<UI_MESSAGE extends UIMessage>(stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>, mapFn: MapUIMessageStreamFn<UI_MESSAGE>): AsyncIterableStream$1<InferUIMessageChunk<UI_MESSAGE>>;
540
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
541
598
  //#region src/pipe/chunk-pipeline.d.ts
542
599
  /**
543
600
  * Pipeline for chunk-based operations.
@@ -576,8 +633,8 @@ declare class ChunkPipeline<UI_MESSAGE extends UIMessage, CHUNK extends InferUIM
576
633
  * Content chunks include a part object with the type, while meta chunks have undefined part.
577
634
  * All chunks pass through regardless of whether the callback is invoked.
578
635
  */
579
- on<NARROWED_CHUNK extends CHUNK, NARROWED_PART extends {
580
- type: PART[`type`];
636
+ on<NARROWED_CHUNK extends InferUIMessageChunk<UI_MESSAGE>, NARROWED_PART extends {
637
+ type: string;
581
638
  } | undefined>(guard: ObserveGuard<UI_MESSAGE, NARROWED_CHUNK, NARROWED_PART>, callback: ChunkObserveFn<NARROWED_CHUNK, NARROWED_PART>): ChunkPipeline<UI_MESSAGE, CHUNK, PART>;
582
639
  /**
583
640
  * Observes chunks matching a predicate without filtering them.
@@ -790,5 +847,58 @@ declare function excludeTools<UI_MESSAGE extends UIMessage>(): FilterGuard<UI_ME
790
847
  declare function excludeTools<UI_MESSAGE extends UIMessage, TOOL_NAME extends InferToolName<UI_MESSAGE>>(toolNames: TOOL_NAME | Array<TOOL_NAME>): FilterGuard<UI_MESSAGE, InferUIMessageChunk<UI_MESSAGE>, {
791
848
  type: Exclude<InferUIMessagePartType<UI_MESSAGE>, `tool-${TOOL_NAME}`>;
792
849
  }>;
850
+ /**
851
+ * Creates an observe guard that matches tool call chunks by tool name and/or state.
852
+ * Use with `.on()` to observe tool call state transitions without filtering.
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * // Match all tool state transitions (any tool, any state)
857
+ * pipe<MyUIMessage>(stream)
858
+ * .on(toolCall(), ({ chunk, part }) => {
859
+ * // Observes: tool-input-available, tool-approval-request,
860
+ * // tool-output-available, tool-output-error, tool-output-denied
861
+ * });
862
+ *
863
+ * // Match specific tool (any state)
864
+ * pipe<MyUIMessage>(stream)
865
+ * .on(toolCall({ tool: "weather" }), ({ chunk, part }) => {
866
+ * // part.type is 'tool-weather'
867
+ * });
868
+ *
869
+ * // Match specific state (all tools)
870
+ * pipe<MyUIMessage>(stream)
871
+ * .on(toolCall({ state: "output-available" }), ({ chunk, part }) => {
872
+ * // chunk.type is 'tool-output-available'
873
+ * });
874
+ *
875
+ * // Match specific tool AND state
876
+ * pipe<MyUIMessage>(stream)
877
+ * .on(toolCall({ tool: "weather", state: "output-available" }), ({ chunk, part }) => {
878
+ * // chunk.type is 'tool-output-available', part.type is 'tool-weather'
879
+ * });
880
+ * ```
881
+ */
882
+ declare function toolCall<UI_MESSAGE extends UIMessage>(): ObserveGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, ToolStateToChunkType[ToolCallState]>, {
883
+ type: `tool-${InferToolName<UI_MESSAGE>}` | "dynamic-tool";
884
+ }>;
885
+ declare function toolCall<UI_MESSAGE extends UIMessage, TOOL_NAME extends InferToolName<UI_MESSAGE>, STATE extends ToolCallState>(options: {
886
+ tool: TOOL_NAME;
887
+ state: STATE;
888
+ }): ObserveGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, ToolStateToChunkType[STATE]>, {
889
+ type: `tool-${TOOL_NAME}`;
890
+ }>;
891
+ declare function toolCall<UI_MESSAGE extends UIMessage, TOOL_NAME extends InferToolName<UI_MESSAGE>>(options: {
892
+ tool: TOOL_NAME;
893
+ state?: undefined;
894
+ }): ObserveGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, ToolStateToChunkType[ToolCallState]>, {
895
+ type: `tool-${TOOL_NAME}`;
896
+ }>;
897
+ declare function toolCall<UI_MESSAGE extends UIMessage, STATE extends ToolCallState>(options: {
898
+ tool?: undefined;
899
+ state: STATE;
900
+ }): ObserveGuard<UI_MESSAGE, ExtractChunk<UI_MESSAGE, ToolStateToChunkType[STATE]>, {
901
+ type: `tool-${InferToolName<UI_MESSAGE>}` | "dynamic-tool";
902
+ }>;
793
903
  //#endregion
794
- 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 };
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
  /**
@@ -937,6 +993,35 @@ function excludeTools(toolNames) {
937
993
  };
938
994
  return guard;
939
995
  }
996
+ /**
997
+ * Reverse mapping from chunk type to state.
998
+ */
999
+ const chunkTypeToState = Object.fromEntries(Object.entries({
1000
+ "input-available": "tool-input-available",
1001
+ "approval-requested": "tool-approval-request",
1002
+ "output-available": "tool-output-available",
1003
+ "output-error": "tool-output-error",
1004
+ "output-denied": "tool-output-denied"
1005
+ }).map(([state, chunkType]) => [chunkType, state]));
1006
+ function toolCall(options) {
1007
+ const toolName = options?.tool;
1008
+ const state = options?.state;
1009
+ const guard = (input) => {
1010
+ const chunkType = input.chunk.type;
1011
+ const partType = input.part?.type;
1012
+ /** Must be a tool chunk type that maps to a state */
1013
+ const matchingState = chunkTypeToState[chunkType];
1014
+ if (!matchingState) return false;
1015
+ /** Check state match */
1016
+ if (state && matchingState !== state) return false;
1017
+ /** Check tool name match */
1018
+ if (toolName && partType) {
1019
+ if (partType !== `tool-${toolName}`) return false;
1020
+ }
1021
+ return true;
1022
+ };
1023
+ return guard;
1024
+ }
940
1025
 
941
1026
  //#endregion
942
- export { chunkType, consumeUIMessageStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream, excludeChunks, excludeParts, excludeTools, filterUIMessageStream, flatMapUIMessageStream, includeChunks, includeParts, includeTools, mapUIMessageStream, partType, partTypeIs, pipe };
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 };
@@ -1,4 +1,4 @@
1
- import { AsyncIterableStream, AsyncIterableStream as AsyncIterableStream$1, UIMessageChunk } from "ai";
1
+ import { AsyncIterableStream, AsyncIterableStream as AsyncIterableStream$1, InferUIMessageChunk, UIMessage, UIMessageChunk } from "ai";
2
2
 
3
3
  //#region src/utils/convert-array-to-async-iterable.d.ts
4
4
  /**
@@ -29,7 +29,7 @@ declare function convertAsyncIterableToStream<T>(iterable: AsyncIterable<T>): Re
29
29
  /**
30
30
  * Converts an SSE stream to a UI message stream.
31
31
  */
32
- declare function convertSSEToUIMessageStream(stream: ReadableStream<string>): ReadableStream<UIMessageChunk>;
32
+ declare function convertSSEToUIMessageStream<UI_MESSAGE extends UIMessage = UIMessage>(stream: ReadableStream<string>): ReadableStream<InferUIMessageChunk<UI_MESSAGE>>;
33
33
  //#endregion
34
34
  //#region src/utils/convert-stream-to-array.d.ts
35
35
  /**
@@ -1,2 +1,2 @@
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-B4nePmEd.mjs";
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
  export { type AsyncIterableStream, convertArrayToAsyncIterable, convertArrayToStream, convertAsyncIterableToArray, convertAsyncIterableToStream, convertSSEToUIMessageStream, convertStreamToArray, convertUIMessageToSSEStream, createAsyncIterableStream };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-stream-utils",
3
- "version": "2.1.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",
@@ -41,10 +41,12 @@
41
41
  "@ai-sdk/openai": "^3.0.29",
42
42
  "@ai-sdk/provider": "^3.0.8",
43
43
  "@ai-sdk/provider-utils": "^4.0.15",
44
+ "@ai-sdk/react": "^3.0.110",
44
45
  "@arethetypeswrong/cli": "^0.18.2",
45
46
  "@total-typescript/tsconfig": "^1.0.4",
46
47
  "@types/node": "^25.2.3",
47
48
  "ai": "^6.0.86",
49
+ "dotenv": "^17.4.2",
48
50
  "husky": "^9.1.7",
49
51
  "lint-staged": "^16.2.7",
50
52
  "msw": "^2.12.10",