ai 6.0.296 → 6.0.298

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/index.d.mts +36 -5
  3. package/dist/index.d.ts +36 -5
  4. package/dist/index.js +187 -32
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +187 -32
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +1 -1
  9. package/dist/internal/index.mjs +1 -1
  10. package/docs/04-ai-sdk-ui/02-chatbot.mdx +10 -1
  11. package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +8 -0
  12. package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +10 -3
  13. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +10 -1
  14. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
  15. package/docs/07-reference/02-ai-sdk-ui/41-create-ui-message-stream-response.mdx +7 -0
  16. package/docs/07-reference/02-ai-sdk-ui/42-pipe-ui-message-stream-to-response.mdx +7 -0
  17. package/package.json +4 -4
  18. package/src/agent/create-agent-ui-stream-response.ts +3 -0
  19. package/src/agent/pipe-agent-ui-stream-to-response.ts +3 -0
  20. package/src/embed/embed-many.ts +5 -3
  21. package/src/generate-image/generate-image.ts +7 -2
  22. package/src/generate-text/stream-text-result.ts +5 -0
  23. package/src/generate-text/stream-text.ts +4 -1
  24. package/src/generate-video/generate-video.ts +6 -4
  25. package/src/registry/provider-registry.ts +1 -1
  26. package/src/ui/chat.ts +13 -4
  27. package/src/ui/http-chat-transport.ts +27 -1
  28. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +15 -3
  29. package/src/ui/process-ui-message-stream.ts +51 -14
  30. package/src/ui/ui-messages.ts +14 -0
  31. package/src/ui/validate-ui-messages.ts +2 -0
  32. package/src/ui-message-stream/create-sse-stream-with-keep-alive.ts +89 -0
  33. package/src/ui-message-stream/create-ui-message-stream-response.ts +7 -1
  34. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +7 -1
  35. package/src/ui-message-stream/ui-message-stream-response-init.ts +7 -0
  36. package/src/util/get-own.ts +18 -0
  37. package/src/util/set-own.ts +16 -0
@@ -164,7 +164,7 @@ function detectMediaType({
164
164
  var import_provider_utils2 = require("@ai-sdk/provider-utils");
165
165
 
166
166
  // src/version.ts
167
- var VERSION = true ? "6.0.296" : "0.0.0-test";
167
+ var VERSION = true ? "6.0.298" : "0.0.0-test";
168
168
 
169
169
  // src/util/download/download.ts
170
170
  var download = async ({
@@ -144,7 +144,7 @@ import {
144
144
  } from "@ai-sdk/provider-utils";
145
145
 
146
146
  // src/version.ts
147
- var VERSION = true ? "6.0.296" : "0.0.0-test";
147
+ var VERSION = true ? "6.0.298" : "0.0.0-test";
148
148
 
149
149
  // src/util/download/download.ts
150
150
  var download = async ({
@@ -728,7 +728,16 @@ With `streamText`, you can control how error messages and usage information are
728
728
 
729
729
  By default, the error message is masked for security reasons.
730
730
  The default error message is "An error occurred."
731
- You can forward error messages or send your own error message by providing a `getErrorMessage` function:
731
+
732
+ Tool execution errors marked `providerExecuted: true` are an exception. They
733
+ bypass the UI stream's `onError` callback: strings are forwarded unchanged, and
734
+ other error values are serialized with `JSON.stringify`. This preserves provider
735
+ error codes when chat history is converted back into model messages. Stream
736
+ errors and invalid tool calls still use `onError`, even for provider-executed
737
+ tools. Do not rely on this callback to redact provider-executed tool execution
738
+ errors before they reach the client.
739
+
740
+ You can forward error messages or send your own error message by providing an `onError` function:
732
741
 
733
742
  ```ts filename="app/api/chat/route.ts" highlight="13-27"
734
743
  import { convertToModelMessages, streamText, UIMessage } from 'ai';
@@ -92,6 +92,14 @@ export async function loadChat(id: string): Promise<UIMessage[]> {
92
92
 
93
93
  When processing messages on the server that contain tool calls, custom metadata, or data parts, you should validate them using `validateUIMessages` before sending them to the model.
94
94
 
95
+ ### Persisting streaming tool input
96
+
97
+ An `input-streaming` tool part includes `rawInput` while tool arguments are
98
+ still arriving. Persist the complete UI message, including this field, so a
99
+ resumed stream can append later tool-input deltas to the exact text received
100
+ before the interruption. AI SDK updates `rawInput` as each delta arrives and
101
+ removes it when the tool input becomes available.
102
+
95
103
  ### Validation with tools
96
104
 
97
105
  When your messages include tool calls, validate them against your tool definitions:
@@ -90,6 +90,10 @@ export function Chat({
90
90
 
91
91
  When you enable `resume`, the `useChat` hook makes a `GET` request to `/api/chat/[id]/stream` on mount to check for and resume any active streams.
92
92
 
93
+ The default transport URL-encodes the chat ID as a single path segment. IDs equal
94
+ to `.` or `..` are rejected with an `InvalidArgumentError` because URL parsers
95
+ interpret them as path traversal segments.
96
+
93
97
  Let's start by creating the POST handler to create the resumable stream.
94
98
 
95
99
  ### 2. Create the POST handler
@@ -215,6 +219,9 @@ The diagram above shows the complete lifecycle of a resumable stream:
215
219
 
216
220
  By default, the `useChat` hook makes a GET request to `/api/chat/[id]/stream` when resuming. Customize this endpoint, credentials, and headers, using the `prepareReconnectToStreamRequest` option in `DefaultChatTransport`:
217
221
 
222
+ The callback receives the original, unencoded ID. Its returned `api` URL is used
223
+ verbatim, so encode and validate any ID you include in a custom URL yourself.
224
+
218
225
  ```tsx filename="app/chat/[chatId]/chat.tsx"
219
226
  import { useChat } from '@ai-sdk/react';
220
227
  import { DefaultChatTransport } from 'ai';
@@ -228,10 +235,10 @@ export function Chat({ chatData, resume }) {
228
235
  // Customize reconnect settings (optional)
229
236
  prepareReconnectToStreamRequest: ({ id }) => {
230
237
  return {
231
- api: `/api/chat/${id}/stream`, // Default pattern
238
+ api: `/api/chat/${encodeURIComponent(id)}/stream`,
232
239
  // Or use a different pattern:
233
- // api: `/api/streams/${id}/resume`,
234
- // api: `/api/resume-chat?id=${id}`,
240
+ // api: `/api/streams/${encodeURIComponent(id)}/resume`,
241
+ // api: `/api/resume-chat?id=${encodeURIComponent(id)}`,
235
242
  credentials: 'include', // Include cookies/auth
236
243
  headers: {
237
244
  Authorization: 'Bearer token',
@@ -682,7 +682,16 @@ export async function POST(req: Request) {
682
682
  Language models can make errors when calling tools.
683
683
  By default, these errors are masked for security reasons, and show up as "An error occurred" in the UI.
684
684
 
685
- To surface the errors, you can use the `onError` function when calling `toUIMessageResponse`.
685
+ Tool execution errors marked `providerExecuted: true` bypass this masking and the
686
+ UI stream's `onError` callback. String errors pass through unchanged; other error
687
+ values are serialized with `JSON.stringify`. The original error data is needed
688
+ when converting persisted UI messages back into model messages, for example to
689
+ preserve Anthropic tool error codes. The callback still handles invalid tool
690
+ calls and stream errors, regardless of whether a tool is provider-executed.
691
+
692
+ To customize masked errors, pass `onError` to `result.toUIMessageStream()` or
693
+ `result.toUIMessageStreamResponse()`. This callback does not redact
694
+ provider-executed tool execution errors sent to the client.
686
695
 
687
696
  ```tsx
688
697
  export function errorHandler(error: unknown) {
@@ -3559,7 +3559,7 @@ To see `streamText` in action, check out [these examples](#examples).
3559
3559
  type: '(error: unknown) => string',
3560
3560
  isOptional: true,
3561
3561
  description:
3562
- 'Process an error, e.g. to log it. Returns error message to include in the data stream. Defaults to () => "An error occurred."',
3562
+ 'Process an error, e.g. to log it. Returns the error message to include in the data stream. Defaults to () => "An error occurred." Tool execution errors marked providerExecuted: true bypass this callback: strings pass through unchanged and other values are JSON-stringified to preserve provider error data for model-message round trips. Stream errors and invalid tool calls still use this callback.',
3563
3563
  },
3564
3564
  {
3565
3565
  name: 'consumeSseStream',
@@ -102,6 +102,13 @@ const response = createUIMessageStreamResponse({
102
102
  isOptional: true,
103
103
  description: 'Additional headers for the response.',
104
104
  },
105
+ {
106
+ name: 'keepAliveMs',
107
+ type: 'number',
108
+ isOptional: true,
109
+ description:
110
+ 'Interval in milliseconds for sending SSE keep-alive comments. When set, an opening comment is sent immediately and additional comments are sent after the stream has been idle for the configured interval.',
111
+ },
105
112
  {
106
113
  name: 'consumeSseStream',
107
114
  type: '(options: { stream: ReadableStream<string> }) => PromiseLike<void> | void',
@@ -66,6 +66,13 @@ await pipeUIMessageStreamToResponse({
66
66
  isOptional: true,
67
67
  description: 'Additional headers for the response.',
68
68
  },
69
+ {
70
+ name: 'keepAliveMs',
71
+ type: 'number',
72
+ isOptional: true,
73
+ description:
74
+ 'Interval in milliseconds for sending SSE keep-alive comments. When set, an opening comment is sent immediately and additional comments are sent after the stream has been idle for the configured interval.',
75
+ },
69
76
  {
70
77
  name: 'consumeSseStream',
71
78
  type: '({ stream }: { stream: ReadableStream<string> }) => PromiseLike<void> | void',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.296",
3
+ "version": "6.0.298",
4
4
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@opentelemetry/api": "^1.9.0",
48
- "@ai-sdk/gateway": "3.0.205",
48
+ "@ai-sdk/gateway": "3.0.207",
49
49
  "@ai-sdk/provider": "3.0.18",
50
- "@ai-sdk/provider-utils": "4.0.56"
50
+ "@ai-sdk/provider-utils": "4.0.57"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@edge-runtime/vm": "^5.0.0",
@@ -58,7 +58,7 @@
58
58
  "tsx": "^4.19.2",
59
59
  "typescript": "5.8.3",
60
60
  "zod": "3.25.76",
61
- "@ai-sdk/google": "3.0.129",
61
+ "@ai-sdk/google": "3.0.130",
62
62
  "@ai-sdk/test-server": "1.0.6",
63
63
  "@vercel/ai-tsconfig": "0.0.0"
64
64
  },
@@ -25,6 +25,7 @@ import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settin
25
25
  * @param headers - Additional headers for the response. Optional.
26
26
  * @param status - The status code for the response. Optional.
27
27
  * @param statusText - The status text for the response. Optional.
28
+ * @param keepAliveMs - Optional interval for sending SSE keep-alive comments.
28
29
  * @param consumeSseStream - Whether to consume the SSE stream. Optional.
29
30
  *
30
31
  * @returns The response object.
@@ -38,6 +39,7 @@ export async function createAgentUIStreamResponse<
38
39
  headers,
39
40
  status,
40
41
  statusText,
42
+ keepAliveMs,
41
43
  consumeSseStream,
42
44
  ...options
43
45
  }: {
@@ -58,6 +60,7 @@ export async function createAgentUIStreamResponse<
58
60
  headers,
59
61
  status,
60
62
  statusText,
63
+ keepAliveMs,
61
64
  consumeSseStream,
62
65
  stream: await createAgentUIStream(options),
63
66
  });
@@ -27,6 +27,7 @@ import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settin
27
27
  * @param headers - Additional headers for the response. Optional.
28
28
  * @param status - The status code for the response. Optional.
29
29
  * @param statusText - The status text for the response. Optional.
30
+ * @param keepAliveMs - Optional interval for sending SSE keep-alive comments.
30
31
  * @param consumeSseStream - Whether to consume the SSE stream. Optional.
31
32
  */
32
33
  export async function pipeAgentUIStreamToResponse<
@@ -39,6 +40,7 @@ export async function pipeAgentUIStreamToResponse<
39
40
  headers,
40
41
  status,
41
42
  statusText,
43
+ keepAliveMs,
42
44
  consumeSseStream,
43
45
  ...options
44
46
  }: {
@@ -61,6 +63,7 @@ export async function pipeAgentUIStreamToResponse<
61
63
  headers,
62
64
  status,
63
65
  statusText,
66
+ keepAliveMs,
64
67
  consumeSseStream,
65
68
  stream: await createAgentUIStream(options),
66
69
  });
@@ -1,3 +1,5 @@
1
+ import { getOwn } from '../util/get-own';
2
+ import { setOwn } from '../util/set-own';
1
3
  import {
2
4
  withUserAgentSuffix,
3
5
  type ProviderOptions,
@@ -358,10 +360,10 @@ export async function embedMany({
358
360
  for (const [providerName, metadata] of Object.entries(
359
361
  result.providerMetadata,
360
362
  )) {
361
- providerMetadata[providerName] = {
362
- ...providerMetadata[providerName],
363
+ setOwn(providerMetadata, providerName, {
364
+ ...getOwn(providerMetadata, providerName),
363
365
  ...metadata,
364
- };
366
+ });
365
367
  }
366
368
  }
367
369
  }
@@ -1,3 +1,5 @@
1
+ import { getOwn } from '../util/get-own';
2
+ import { setOwn } from '../util/set-own';
1
3
  import type {
2
4
  ImageModelV3,
3
5
  ImageModelV3CallOptions,
@@ -284,10 +286,13 @@ export async function generateImage({
284
286
  .images;
285
287
  }
286
288
  } else {
287
- providerMetadata[providerName] ??= { images: [] };
288
- providerMetadata[providerName].images.push(
289
+ const currentEntry = getOwn(providerMetadata, providerName) ?? {
290
+ images: [],
291
+ };
292
+ currentEntry.images.push(
289
293
  ...result.providerMetadata[providerName].images,
290
294
  );
295
+ setOwn(providerMetadata, providerName, currentEntry);
291
296
  }
292
297
  }
293
298
  }
@@ -99,6 +99,11 @@ export type UIMessageStreamOptions<UI_MESSAGE extends UIMessage> = {
99
99
  /**
100
100
  * Process an error, e.g. to log it. Default to `() => 'An error occurred.'`.
101
101
  *
102
+ * Tool execution errors marked `providerExecuted: true` bypass this callback.
103
+ * Their strings pass through unchanged; other values are JSON-stringified to
104
+ * preserve provider error data for model-message round trips. Stream errors
105
+ * and invalid tool calls still use it.
106
+ *
102
107
  * @returns error message to include in the data stream.
103
108
  */
104
109
  onError?: (error: unknown) => string;
@@ -2698,7 +2698,7 @@ class DefaultStreamTextResult<
2698
2698
  sendSources = false,
2699
2699
  sendStart = true,
2700
2700
  sendFinish = true,
2701
- onError = () => 'An error occurred.', // prevent leaking server error details to the client by default
2701
+ onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
2702
2702
  }: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream<
2703
2703
  InferUIMessageChunk<UI_MESSAGE>
2704
2704
  > {
@@ -3032,6 +3032,9 @@ class DefaultStreamTextResult<
3032
3032
  case 'tool-error': {
3033
3033
  const dynamic = isDynamic(part);
3034
3034
 
3035
+ // Preserve provider error codes for model-message round trips.
3036
+ // These execution errors intentionally bypass onError; invalid
3037
+ // tool calls and stream errors still go through it.
3035
3038
  controller.enqueue({
3036
3039
  type: 'tool-output-error',
3037
3040
  toolCallId: part.toolCallId,
@@ -1,3 +1,5 @@
1
+ import { getOwn } from '../util/get-own';
2
+ import { setOwn } from '../util/set-own';
1
3
  import type {
2
4
  Experimental_VideoModelV3,
3
5
  Experimental_VideoModelV3CallOptions,
@@ -385,12 +387,12 @@ export async function experimental_generateVideo({
385
387
  for (const [providerName, metadata] of Object.entries(
386
388
  result.providerMetadata,
387
389
  )) {
388
- const existingMetadata = providerMetadata[providerName];
390
+ const existingMetadata = getOwn(providerMetadata, providerName);
389
391
  if (existingMetadata != null && typeof existingMetadata === 'object') {
390
- providerMetadata[providerName] = {
392
+ setOwn(providerMetadata, providerName, {
391
393
  ...existingMetadata,
392
394
  ...metadata,
393
- };
395
+ });
394
396
 
395
397
  // Merge videos arrays if both exist
396
398
  if (
@@ -405,7 +407,7 @@ export async function experimental_generateVideo({
405
407
  ];
406
408
  }
407
409
  } else {
408
- providerMetadata[providerName] = metadata;
410
+ setOwn(providerMetadata, providerName, metadata);
409
411
  }
410
412
  }
411
413
  }
@@ -133,7 +133,7 @@ class DefaultProviderRegistry<
133
133
  PROVIDERS extends Record<string, ProviderV3>,
134
134
  SEPARATOR extends string,
135
135
  > implements ProviderRegistryProvider<PROVIDERS, SEPARATOR> {
136
- private providers: PROVIDERS = {} as PROVIDERS;
136
+ private providers: PROVIDERS = Object.create(null);
137
137
  private separator: SEPARATOR;
138
138
  private languageModelMiddleware?:
139
139
  | LanguageModelMiddleware
package/src/ui/chat.ts CHANGED
@@ -792,6 +792,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
792
792
  responseMessageIndex === -1
793
793
  ? lastMessage
794
794
  : this.state.messages[responseMessageIndex];
795
+ const resumableResponseMessage =
796
+ trigger === 'resume-stream' &&
797
+ responseMessage?.role === 'assistant' &&
798
+ responseMessage.parts.some(
799
+ part => isToolUIPart(part) && part.state === 'input-streaming',
800
+ )
801
+ ? this.state.snapshot(responseMessage)
802
+ : undefined;
795
803
  const usesEarlierAssistantMessage =
796
804
  responseMessageIndex !== -1 &&
797
805
  responseMessageIndex < this.state.messages.length - 1 &&
@@ -809,10 +817,11 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
809
817
  ? this.resumableStreamState
810
818
  : createStreamingUIMessageState({
811
819
  lastMessage:
812
- trigger === 'resume-stream' ||
813
- trigger === 'regenerate-message'
814
- ? undefined
815
- : this.state.snapshot(responseMessage),
820
+ trigger === 'resume-stream'
821
+ ? resumableResponseMessage
822
+ : trigger === 'regenerate-message'
823
+ ? undefined
824
+ : this.state.snapshot(responseMessage),
816
825
  messageId: this.generateId(),
817
826
  }),
818
827
  abortController,
@@ -4,10 +4,19 @@ import {
4
4
  type FetchFunction,
5
5
  type Resolvable,
6
6
  } from '@ai-sdk/provider-utils';
7
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
7
8
  import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
8
9
  import type { ChatTransport } from './chat-transport';
9
10
  import type { UIMessage } from './ui-messages';
10
11
 
12
+ function appendPathToUrl(url: string, path: string): string {
13
+ const queryOrFragmentStart = url.search(/[?#]/);
14
+
15
+ return queryOrFragmentStart === -1
16
+ ? `${url}${path}`
17
+ : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
18
+ }
19
+
11
20
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
12
21
  options: {
13
22
  id: string;
@@ -233,7 +242,24 @@ export abstract class HttpChatTransport<
233
242
  requestMetadata: options.metadata,
234
243
  });
235
244
 
236
- const api = preparedRequest?.api ?? `${this.api}/${options.chatId}/stream`;
245
+ let api = preparedRequest?.api;
246
+ if (api == null) {
247
+ // encodeURIComponent leaves dot segments unchanged, and URL parsers
248
+ // normalize them even when their dots are percent-encoded.
249
+ if (options.chatId === '.' || options.chatId === '..') {
250
+ throw new InvalidArgumentError({
251
+ parameter: 'chatId',
252
+ value: options.chatId,
253
+ message:
254
+ 'Chat IDs must not be "." or ".." when using the default reconnect URL.',
255
+ });
256
+ }
257
+
258
+ api = appendPathToUrl(
259
+ this.api,
260
+ `/${encodeURIComponent(options.chatId)}/stream`,
261
+ );
262
+ }
237
263
  const headers =
238
264
  preparedRequest?.headers !== undefined
239
265
  ? normalizeHeaders(preparedRequest.headers)
@@ -3,7 +3,9 @@ import { isToolUIPart, type UIMessage } from './ui-messages';
3
3
  /**
4
4
  * Check if the last message is an assistant message with completed tool calls.
5
5
  * The last step of the message must have at least one tool invocation and
6
- * all tool invocations must have a result.
6
+ * all tool invocations must have a result. Completed model text may follow
7
+ * the tool invocation because client-side tool results update parts in place,
8
+ * but trailing text without a completed stream state prevents resubmission.
7
9
  */
8
10
  export function lastAssistantMessageIsCompleteWithToolCalls({
9
11
  messages,
@@ -24,13 +26,23 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
24
26
  return part.type === 'step-start' ? index : lastIndex;
25
27
  }, -1);
26
28
 
27
- const lastStepToolInvocations = message.parts
28
- .slice(lastStepStartIndex + 1)
29
+ const lastStepParts = message.parts.slice(lastStepStartIndex + 1);
30
+
31
+ const lastStepToolInvocations = lastStepParts
29
32
  .filter(isToolUIPart)
30
33
  .filter(part => !part.providerExecuted);
31
34
 
35
+ const lastToolInvocationIndex = lastStepParts.reduce(
36
+ (lastIndex, part, index) =>
37
+ isToolUIPart(part) && !part.providerExecuted ? index : lastIndex,
38
+ -1,
39
+ );
40
+
32
41
  return (
33
42
  lastStepToolInvocations.length > 0 &&
43
+ !lastStepParts
44
+ .slice(lastToolInvocationIndex + 1)
45
+ .some(part => part.type === 'text' && part.state !== 'done') &&
34
46
  lastStepToolInvocations.every(
35
47
  part =>
36
48
  (part.state === 'output-available' && part.preliminary !== true) ||
@@ -60,22 +60,55 @@ export function createStreamingUIMessageState<UI_MESSAGE extends UIMessage>({
60
60
  lastMessage: UI_MESSAGE | undefined;
61
61
  messageId: string;
62
62
  }): StreamingUIMessageState<UI_MESSAGE> {
63
+ const message =
64
+ lastMessage?.role === 'assistant'
65
+ ? lastMessage
66
+ : ({
67
+ id: messageId,
68
+ metadata: undefined,
69
+ role: 'assistant',
70
+ parts: [] as UIMessagePart<
71
+ InferUIMessageData<UI_MESSAGE>,
72
+ InferUIMessageTools<UI_MESSAGE>
73
+ >[],
74
+ } as UI_MESSAGE);
75
+ const partialToolCalls: StreamingUIMessageState<UI_MESSAGE>['partialToolCalls'] =
76
+ createIdMap();
77
+ const lastStepStartIndex = message.parts.findLastIndex(
78
+ part => part.type === 'step-start',
79
+ );
80
+ let staticToolIndex = 0;
81
+
82
+ for (const part of message.parts.slice(lastStepStartIndex + 1)) {
83
+ if (!isToolUIPart(part)) {
84
+ continue;
85
+ }
86
+
87
+ const index = staticToolIndex;
88
+ if (isStaticToolUIPart(part)) {
89
+ staticToolIndex++;
90
+ }
91
+
92
+ if (part.state !== 'input-streaming') {
93
+ continue;
94
+ }
95
+
96
+ partialToolCalls[part.toolCallId] = {
97
+ text: part.rawInput ?? '',
98
+ index,
99
+ toolName:
100
+ part.type === 'dynamic-tool' ? part.toolName : getStaticToolName(part),
101
+ dynamic: part.type === 'dynamic-tool',
102
+ title: part.title,
103
+ toolMetadata: part.toolMetadata,
104
+ };
105
+ }
106
+
63
107
  return {
64
- message:
65
- lastMessage?.role === 'assistant'
66
- ? lastMessage
67
- : ({
68
- id: messageId,
69
- metadata: undefined,
70
- role: 'assistant',
71
- parts: [] as UIMessagePart<
72
- InferUIMessageData<UI_MESSAGE>,
73
- InferUIMessageTools<UI_MESSAGE>
74
- >[],
75
- } as UI_MESSAGE),
108
+ message,
76
109
  activeTextParts: createIdMap(),
77
110
  activeReasoningParts: createIdMap(),
78
- partialToolCalls: createIdMap(),
111
+ partialToolCalls,
79
112
  };
80
113
  }
81
114
 
@@ -167,6 +200,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
167
200
  | {
168
201
  state: 'input-streaming';
169
202
  input: unknown;
203
+ rawInput?: string;
170
204
  providerExecuted?: boolean;
171
205
  providerMetadata?: ProviderMetadata;
172
206
  }
@@ -282,6 +316,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
282
316
  | {
283
317
  state: 'input-streaming';
284
318
  input: unknown;
319
+ rawInput?: string;
285
320
  providerMetadata?: ProviderMetadata;
286
321
  }
287
322
  | {
@@ -322,7 +357,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
322
357
  anyPart.input = anyOptions.input;
323
358
  anyPart.output = anyOptions.output;
324
359
  anyPart.errorText = anyOptions.errorText;
325
- anyPart.rawInput = anyOptions.rawInput ?? anyPart.rawInput;
360
+ anyPart.rawInput = anyOptions.rawInput;
326
361
  anyPart.preliminary = anyOptions.preliminary;
327
362
  if (options.title !== undefined) {
328
363
  anyPart.title = options.title;
@@ -615,6 +650,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
615
650
  toolName: partialToolCall.toolName,
616
651
  state: 'input-streaming',
617
652
  input: partialArgs,
653
+ rawInput: partialToolCall.text,
618
654
  title: partialToolCall.title,
619
655
  toolMetadata: partialToolCall.toolMetadata,
620
656
  });
@@ -624,6 +660,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
624
660
  toolName: partialToolCall.toolName,
625
661
  state: 'input-streaming',
626
662
  input: partialArgs,
663
+ rawInput: partialToolCall.text,
627
664
  title: partialToolCall.title,
628
665
  toolMetadata: partialToolCall.toolMetadata,
629
666
  });
@@ -238,6 +238,13 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
238
238
  | {
239
239
  state: 'input-streaming';
240
240
  input?: DeepPartial<asUITool<TOOL>['input']> | undefined;
241
+ /**
242
+ * The raw tool input text received so far.
243
+ *
244
+ * This is used to continue input streaming when a message is persisted
245
+ * and later resumed.
246
+ */
247
+ rawInput?: string;
241
248
  output?: never;
242
249
  errorText?: never;
243
250
  callProviderMetadata?: ProviderMetadata;
@@ -361,6 +368,13 @@ export type DynamicToolUIPart = {
361
368
  | {
362
369
  state: 'input-streaming';
363
370
  input?: unknown;
371
+ /**
372
+ * The raw tool input text received so far.
373
+ *
374
+ * This is used to continue input streaming when a message is persisted
375
+ * and later resumed.
376
+ */
377
+ rawInput?: string;
364
378
  output?: never;
365
379
  errorText?: never;
366
380
  callProviderMetadata?: ProviderMetadata;
@@ -139,6 +139,7 @@ const uiMessagesSchema = lazySchema(() => {
139
139
  toolMetadata: toolMetadataSchema.optional(),
140
140
  state: z.literal('input-streaming'),
141
141
  input: z.unknown().optional(),
142
+ rawInput: z.string().optional(),
142
143
  providerExecuted: z.boolean().optional(),
143
144
  callProviderMetadata: providerMetadataSchema.optional(),
144
145
  output: z.never().optional(),
@@ -235,6 +236,7 @@ const uiMessagesSchema = lazySchema(() => {
235
236
  providerExecuted: z.boolean().optional(),
236
237
  callProviderMetadata: providerMetadataSchema.optional(),
237
238
  input: z.unknown().optional(),
239
+ rawInput: z.string().optional(),
238
240
  output: z.never().optional(),
239
241
  errorText: z.never().optional(),
240
242
  approval: z.never().optional(),