ai 7.0.120 → 7.0.122

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.
@@ -90,7 +90,7 @@ import {
90
90
  } from "@ai-sdk/provider-utils";
91
91
 
92
92
  // src/version.ts
93
- var VERSION = true ? "7.0.120" : "0.0.0-test";
93
+ var VERSION = true ? "7.0.122" : "0.0.0-test";
94
94
 
95
95
  // src/util/download/download.ts
96
96
  var download = async ({
@@ -758,6 +758,15 @@ With `streamText`, you can control how error messages and usage information are
758
758
 
759
759
  By default, the error message is masked for security reasons.
760
760
  The default error message is "An error occurred."
761
+
762
+ Tool execution errors marked `providerExecuted: true` are an exception. They
763
+ bypass the UI stream's `onError` callback: strings are forwarded unchanged, and
764
+ other error values are serialized with `JSON.stringify`. This preserves provider
765
+ error codes when chat history is converted back into model messages, as well as
766
+ error messages from harness runtimes. Stream errors and invalid tool calls still
767
+ use `onError`, even for provider-executed tools. Do not rely on this callback to
768
+ redact provider-executed tool execution errors before they reach the client.
769
+
761
770
  You can forward error messages or send your own error message by providing an `onError` function:
762
771
 
763
772
  ```ts filename="app/api/chat/route.ts" highlight="21-35"
@@ -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
  ### Migrating deprecated tool `rawInput`
96
104
 
97
105
  Older persisted tool parts in the `output-error` state may contain `rawInput`.
@@ -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
@@ -220,6 +224,9 @@ The diagram above shows the complete lifecycle of a resumable stream:
220
224
 
221
225
  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`:
222
226
 
227
+ The callback receives the original, unencoded ID. Its returned `api` URL is used
228
+ verbatim, so encode and validate any ID you include in a custom URL yourself.
229
+
223
230
  ```tsx filename="app/chat/[chatId]/chat.tsx"
224
231
  import { useChat } from '@ai-sdk/react';
225
232
  import { DefaultChatTransport } from 'ai';
@@ -233,10 +240,10 @@ export function Chat({ chatData, resume }) {
233
240
  // Customize reconnect settings (optional)
234
241
  prepareReconnectToStreamRequest: ({ id }) => {
235
242
  return {
236
- api: `/api/chat/${id}/stream`, // Default pattern
243
+ api: `/api/chat/${encodeURIComponent(id)}/stream`,
237
244
  // Or use a different pattern:
238
- // api: `/api/streams/${id}/resume`,
239
- // api: `/api/resume-chat?id=${id}`,
245
+ // api: `/api/streams/${encodeURIComponent(id)}/resume`,
246
+ // api: `/api/resume-chat?id=${encodeURIComponent(id)}`,
240
247
  credentials: 'include', // Include cookies/auth
241
248
  headers: {
242
249
  Authorization: 'Bearer token',
@@ -780,7 +780,16 @@ export async function POST(req: Request) {
780
780
  Language models can make errors when calling tools.
781
781
  By default, these errors are masked for security reasons, and show up as "An error occurred" in the UI.
782
782
 
783
- To surface the errors, you can use the `onError` function when calling `toUIMessageResponse`.
783
+ Tool execution errors marked `providerExecuted: true` bypass this masking and the
784
+ UI stream's `onError` callback. String errors pass through unchanged; other error
785
+ values are serialized with `JSON.stringify`. The original error data is needed
786
+ when converting persisted UI messages back into model messages, for example to
787
+ preserve Anthropic tool error codes. Harness runtime tool errors also retain
788
+ their original messages. The callback still handles invalid tool calls and
789
+ stream errors, regardless of whether a tool is provider-executed.
790
+
791
+ To customize masked errors, pass `onError` to `toUIMessageStream`. This callback
792
+ does not redact provider-executed tool execution errors sent to the client.
784
793
 
785
794
  ```tsx
786
795
  export function errorHandler(error: unknown) {
@@ -219,7 +219,7 @@ This page pulls in the current UI State using the `useUIState` hook, which is th
219
219
 
220
220
  In this example, to call the next tool, the user must respond with plain text. **Given you are streaming a React component, you can add a button to trigger the next step in the conversation**.
221
221
 
222
- To add user interaction, you will have to convert the component into a client component and use the `useAction` hook to trigger the next step in the conversation.
222
+ To add user interaction, you will have to convert the component into a client component and use the `useActions` hook to trigger the next step in the conversation.
223
223
 
224
224
  ```tsx filename="components/flights.tsx"
225
225
  'use client';
@@ -4133,7 +4133,7 @@ To see `streamText` in action, check out [these examples](#examples).
4133
4133
  type: '(error: unknown) => string',
4134
4134
  isOptional: true,
4135
4135
  description:
4136
- 'Process an error, e.g. to log it. Returns error message to include in the data stream. Defaults to () => "An error occurred."',
4136
+ '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 and harness runtime error messages. Stream errors and invalid tool calls still use this callback.',
4137
4137
  },
4138
4138
  {
4139
4139
  name: 'consumeSseStream',
@@ -27,8 +27,8 @@ description: Learn how to upgrade AI SDK 3.4 to 4.0.
27
27
  You need to update the following packages to the following versions in your `package.json` file(s):
28
28
 
29
29
  - `ai` package: `4.0.*`
30
- - `ai-sdk@provider-utils` package: `2.0.*`
31
- - `ai-sdk/*` packages: `1.0.*` (other `@ai-sdk` packages)
30
+ - `@ai-sdk/provider-utils` package: `2.0.*`
31
+ - `@ai-sdk/*` packages: `1.0.*` (other `@ai-sdk/*` packages)
32
32
 
33
33
  ## Codemods
34
34
 
@@ -518,7 +518,7 @@ import { Provider } from 'ai';
518
518
  providers](/docs/reference/ai-sdk-core/provider-registry#setup) instead.
519
519
  </Note>
520
520
 
521
- #### Removed `experimental_​createModelRegistry` function
521
+ #### Removed `experimental_createModelRegistry` function
522
522
 
523
523
  The `experimental_createModelRegistry` function has been removed.
524
524
  Please use the `experimental_createProviderRegistry` function instead.
@@ -621,7 +621,7 @@ const { text, response } = await generateText({
621
621
  const responseMessages = response.messages;
622
622
  ```
623
623
 
624
- ### Removed `experimental_​continuationSteps` option
624
+ ### Removed `experimental_continuationSteps` option
625
625
 
626
626
  The `experimental_continuationSteps` option has been removed from the `generateText` function.
627
627
  Please use the `experimental_continueSteps` option instead.
@@ -965,7 +965,7 @@ return AssistantResponse(
965
965
  );
966
966
  ```
967
967
 
968
- #### Removed `experimental_​AssistantResponse` export
968
+ #### Removed `experimental_AssistantResponse` export
969
969
 
970
970
  <Note type="warning">
971
971
  There is no codemod available for this change. Please review and update your
@@ -18,7 +18,7 @@ introducing significant improvements and new features across the AI SDK and its
18
18
 
19
19
  ## New Features
20
20
 
21
- ### Open Telemetry Support
21
+ ### OpenTelemetry Support
22
22
 
23
23
  - Added experimental [OpenTelemetry support](/docs/ai-sdk-core/telemetry) for all [AI SDK Core functions](/docs/ai-sdk-core/overview#ai-sdk-core-functions), enabling better observability and tracing capabilities.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.120",
3
+ "version": "7.0.122",
4
4
  "type": "module",
5
5
  "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.",
6
6
  "license": "Apache-2.0",
@@ -42,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.98",
45
+ "@ai-sdk/gateway": "4.0.100",
46
46
  "@ai-sdk/provider": "4.0.19",
47
- "@ai-sdk/provider-utils": "5.0.50"
47
+ "@ai-sdk/provider-utils": "5.0.51"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.99",
51
- "@ai-sdk/deepseek": "3.0.55",
52
- "@ai-sdk/google": "4.0.84",
53
- "@ai-sdk/groq": "4.0.51",
54
- "@ai-sdk/huggingface": "2.0.58",
55
- "@ai-sdk/moonshotai": "3.0.59",
56
- "@ai-sdk/openai": "4.0.79",
50
+ "@ai-sdk/amazon-bedrock": "5.0.100",
51
+ "@ai-sdk/deepseek": "3.0.56",
52
+ "@ai-sdk/google": "4.0.85",
53
+ "@ai-sdk/groq": "4.0.52",
54
+ "@ai-sdk/huggingface": "2.0.59",
55
+ "@ai-sdk/moonshotai": "3.0.60",
56
+ "@ai-sdk/openai": "4.0.80",
57
57
  "@ai-sdk/test-server": "2.0.2",
58
- "@ai-sdk/xai": "5.0.11",
58
+ "@ai-sdk/xai": "5.0.12",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -14,6 +14,8 @@ import type { TelemetryOptions } from '../telemetry/telemetry-options';
14
14
  import type { Embedding, EmbeddingModel, ProviderMetadata } from '../types';
15
15
  import type { Warning } from '../types/warning';
16
16
  import type { Callback } from '../util/callback';
17
+ import { getOwn } from '../util/get-own';
18
+ import { setOwn } from '../util/set-own';
17
19
  import { notify } from '../util/notify';
18
20
  import { prepareRetries } from '../util/prepare-retries';
19
21
  import { splitArray } from '../util/split-array';
@@ -427,10 +429,10 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
427
429
  for (const [providerName, metadata] of Object.entries(
428
430
  result.providerMetadata,
429
431
  )) {
430
- providerMetadata[providerName] = {
431
- ...providerMetadata[providerName],
432
+ setOwn(providerMetadata, providerName, {
433
+ ...getOwn(providerMetadata, providerName),
432
434
  ...metadata,
433
- };
435
+ });
434
436
  }
435
437
  }
436
438
  }
@@ -25,6 +25,8 @@ import type { ImageModel } from '../types/image-model';
25
25
  import type { ImageModelResponseMetadata } from '../types/image-model-response-metadata';
26
26
  import { addImageModelUsage, type ImageModelUsage } from '../types/usage';
27
27
  import type { Warning } from '../types/warning';
28
+ import { getOwn } from '../util/get-own';
29
+ import { setOwn } from '../util/set-own';
28
30
  import { prepareRetries } from '../util/prepare-retries';
29
31
  import { RetryError } from '../util/retry-error';
30
32
  import { VERSION } from '../version';
@@ -324,8 +326,11 @@ export async function generateImage({
324
326
  .images;
325
327
  }
326
328
  } else {
327
- providerMetadata[providerName] ??= { images: [] };
328
- providerMetadata[providerName].images.push(...metadata.images);
329
+ const currentEntry = getOwn(providerMetadata, providerName) ?? {
330
+ images: [],
331
+ };
332
+ currentEntry.images.push(...metadata.images);
333
+ setOwn(providerMetadata, providerName, currentEntry);
329
334
  }
330
335
  }
331
336
  }
@@ -395,7 +400,7 @@ export function getImageProviderMetadata(
395
400
  const value = metadata.images?.[imageIndex];
396
401
 
397
402
  if (isJSONObject(value) && !Array.isArray(value)) {
398
- (imageMetadata ??= {})[providerName] = value;
403
+ setOwn((imageMetadata ??= {}), providerName, value);
399
404
  }
400
405
  }
401
406
 
@@ -159,10 +159,20 @@ export function smoothStream<TOOLS extends ToolSet>({
159
159
  return;
160
160
  }
161
161
 
162
- // Flush buffer when type or id changes
162
+ if (chunk.text.length === 0 && chunk.providerMetadata != null) {
163
+ flushBuffer(controller);
164
+ controller.enqueue(chunk);
165
+ return;
166
+ }
167
+
168
+ // Flush at metadata boundaries because one output part cannot preserve
169
+ // metadata from multiple input deltas.
163
170
  if (
164
- (chunk.type !== type || chunk.id !== id) &&
165
- (buffer.length > 0 || providerMetadata != null)
171
+ buffer.length > 0 &&
172
+ (chunk.type !== type ||
173
+ chunk.id !== id ||
174
+ providerMetadata != null ||
175
+ chunk.providerMetadata != null)
166
176
  ) {
167
177
  flushBuffer(controller);
168
178
  }
@@ -170,20 +180,25 @@ export function smoothStream<TOOLS extends ToolSet>({
170
180
  buffer += chunk.text;
171
181
  id = chunk.id;
172
182
  type = chunk.type;
173
-
174
- // Preserve providerMetadata (e.g., Anthropic thinking signatures)
175
- if (chunk.providerMetadata != null) {
176
- providerMetadata = chunk.providerMetadata;
177
- }
183
+ providerMetadata = chunk.providerMetadata;
178
184
 
179
185
  let match;
180
186
 
181
187
  while ((match = detectChunk(buffer)) != null) {
182
- controller.enqueue({ type, text: match, id });
188
+ controller.enqueue({
189
+ type,
190
+ text: match,
191
+ id,
192
+ ...(providerMetadata != null ? { providerMetadata } : {}),
193
+ });
183
194
  buffer = buffer.slice(match.length);
184
195
 
185
196
  await delay(isDocumentHidden() ? null : delayInMs);
186
197
  }
198
+
199
+ if (buffer.length === 0) {
200
+ providerMetadata = undefined;
201
+ }
187
202
  },
188
203
  });
189
204
  };
@@ -104,6 +104,11 @@ export type UIMessageStreamOptions<UI_MESSAGE extends UIMessage> = {
104
104
  /**
105
105
  * Process an error, e.g. to log it. Default to `() => 'An error occurred.'`.
106
106
  *
107
+ * Tool execution errors marked `providerExecuted: true` bypass this callback.
108
+ * Their strings pass through unchanged; other values are JSON-stringified to
109
+ * preserve provider error data for model-message round trips and harness
110
+ * runtime error messages. Stream errors and invalid tool calls still use it.
111
+ *
107
112
  * @returns error message to include in the data stream.
108
113
  */
109
114
  onError?: (error: unknown) => string;
@@ -29,6 +29,8 @@ import { resolveVideoModel } from '../model/resolve-model';
29
29
  import type { VideoModel } from '../types/video-model';
30
30
  import type { VideoModelResponseMetadata } from '../types/video-model-response-metadata';
31
31
  import type { Warning } from '../types/warning';
32
+ import { getOwn } from '../util/get-own';
33
+ import { setOwn } from '../util/set-own';
32
34
  import { createDownload } from '../util/download/create-download';
33
35
  import { prepareRetries } from '../util/prepare-retries';
34
36
  import { VERSION } from '../version';
@@ -697,17 +699,17 @@ function mergeProviderMetadata(
697
699
  source: SharedV4ProviderMetadata,
698
700
  ): void {
699
701
  for (const [providerName, metadataValue] of Object.entries(source)) {
700
- const existingMetadata = target[providerName];
702
+ const existingMetadata = getOwn(target, providerName);
701
703
  if (
702
704
  existingMetadata != null &&
703
705
  typeof existingMetadata === 'object' &&
704
706
  metadataValue != null &&
705
707
  typeof metadataValue === 'object'
706
708
  ) {
707
- target[providerName] = {
709
+ setOwn(target, providerName, {
708
710
  ...existingMetadata,
709
711
  ...metadataValue,
710
- };
712
+ });
711
713
 
712
714
  if (
713
715
  'videos' in existingMetadata &&
@@ -721,7 +723,7 @@ function mergeProviderMetadata(
721
723
  ];
722
724
  }
723
725
  } else {
724
- target[providerName] = metadataValue;
726
+ setOwn(target, providerName, metadataValue);
725
727
  }
726
728
  }
727
729
  }
@@ -213,7 +213,7 @@ class DefaultProviderRegistry<
213
213
  keyof PROVIDERS,
214
214
  ProviderV4 & ProviderWithOptionalVideoModel & EvaluationProvider
215
215
  >
216
- > = {};
216
+ > = Object.create(null);
217
217
  private separator: SEPARATOR;
218
218
  private languageModelMiddleware?:
219
219
  | LanguageModelMiddleware
package/src/ui/chat.ts CHANGED
@@ -818,6 +818,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
818
818
  responseMessageIndex === -1
819
819
  ? lastMessage
820
820
  : this.state.messages[responseMessageIndex];
821
+ const resumableResponseMessage =
822
+ trigger === 'resume-stream' &&
823
+ responseMessage?.role === 'assistant' &&
824
+ responseMessage.parts.some(
825
+ part => isToolUIPart(part) && part.state === 'input-streaming',
826
+ )
827
+ ? this.state.snapshot(responseMessage)
828
+ : undefined;
821
829
  const usesEarlierAssistantMessage =
822
830
  responseMessageIndex !== -1 &&
823
831
  responseMessageIndex < this.state.messages.length - 1 &&
@@ -835,10 +843,11 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
835
843
  ? this.resumableStreamState
836
844
  : createStreamingUIMessageState({
837
845
  lastMessage:
838
- trigger === 'resume-stream' ||
839
- trigger === 'regenerate-message'
840
- ? undefined
841
- : this.state.snapshot(responseMessage),
846
+ trigger === 'resume-stream'
847
+ ? resumableResponseMessage
848
+ : trigger === 'regenerate-message'
849
+ ? undefined
850
+ : this.state.snapshot(responseMessage),
842
851
  messageId: this.generateId(),
843
852
  }),
844
853
  abortController,
@@ -5,6 +5,7 @@ import {
5
5
  type Resolvable,
6
6
  } from '@ai-sdk/provider-utils';
7
7
  import { EmptyResponseBodyError } from '@ai-sdk/provider';
8
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
8
9
  import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
9
10
  import type { ChatTransport } from './chat-transport';
10
11
  import { createUIApiCallError } from './create-ui-api-call-error';
@@ -247,9 +248,24 @@ export abstract class HttpChatTransport<
247
248
  requestMetadata: options.metadata,
248
249
  });
249
250
 
250
- const api =
251
- preparedRequest?.api ??
252
- appendPathToUrl(this.api, `/${options.chatId}/stream`);
251
+ let api = preparedRequest?.api;
252
+ if (api == null) {
253
+ // encodeURIComponent leaves dot segments unchanged, and URL parsers
254
+ // normalize them even when their dots are percent-encoded.
255
+ if (options.chatId === '.' || options.chatId === '..') {
256
+ throw new InvalidArgumentError({
257
+ parameter: 'chatId',
258
+ value: options.chatId,
259
+ message:
260
+ 'Chat IDs must not be "." or ".." when using the default reconnect URL.',
261
+ });
262
+ }
263
+
264
+ api = appendPathToUrl(
265
+ this.api,
266
+ `/${encodeURIComponent(options.chatId)}/stream`,
267
+ );
268
+ }
253
269
  const headers =
254
270
  preparedRequest?.headers !== undefined
255
271
  ? normalizeHeaders(preparedRequest.headers)
@@ -2,7 +2,9 @@ import { isToolUIPart, type UIMessage } from './ui-messages';
2
2
  /**
3
3
  * Check if the last message is an assistant message with completed tool calls.
4
4
  * The last step of the message must have at least one tool invocation and
5
- * all tool invocations must have a result.
5
+ * all tool invocations must have a result. Completed model text may follow
6
+ * the tool invocation because client-side tool results update parts in place,
7
+ * but trailing text without a completed stream state prevents resubmission.
6
8
  */
7
9
  export function lastAssistantMessageIsCompleteWithToolCalls({
8
10
  messages,
@@ -23,13 +25,23 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
23
25
  return part.type === 'step-start' ? index : lastIndex;
24
26
  }, -1);
25
27
 
26
- const lastStepToolInvocations = message.parts
27
- .slice(lastStepStartIndex + 1)
28
+ const lastStepParts = message.parts.slice(lastStepStartIndex + 1);
29
+
30
+ const lastStepToolInvocations = lastStepParts
28
31
  .filter(isToolUIPart)
29
32
  .filter(part => !part.providerExecuted);
30
33
 
34
+ const lastToolInvocationIndex = lastStepParts.reduce(
35
+ (lastIndex, part, index) =>
36
+ isToolUIPart(part) && !part.providerExecuted ? index : lastIndex,
37
+ -1,
38
+ );
39
+
31
40
  return (
32
41
  lastStepToolInvocations.length > 0 &&
42
+ !lastStepParts
43
+ .slice(lastToolInvocationIndex + 1)
44
+ .some(part => part.type === 'text' && part.state !== 'done') &&
33
45
  lastStepToolInvocations.every(
34
46
  part =>
35
47
  (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
 
@@ -185,6 +218,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
185
218
  | {
186
219
  state: 'input-streaming';
187
220
  input: unknown;
221
+ rawInput?: string;
188
222
  providerExecuted?: boolean;
189
223
  providerMetadata?: ProviderMetadata;
190
224
  }
@@ -300,6 +334,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
300
334
  | {
301
335
  state: 'input-streaming';
302
336
  input: unknown;
337
+ rawInput?: string;
303
338
  providerMetadata?: ProviderMetadata;
304
339
  }
305
340
  | {
@@ -340,7 +375,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
340
375
  anyPart.input = anyOptions.input;
341
376
  anyPart.output = anyOptions.output;
342
377
  anyPart.errorText = anyOptions.errorText;
343
- anyPart.rawInput = anyOptions.rawInput ?? anyPart.rawInput;
378
+ anyPart.rawInput = anyOptions.rawInput;
344
379
  anyPart.preliminary = anyOptions.preliminary;
345
380
  if (options.title !== undefined) {
346
381
  anyPart.title = options.title;
@@ -645,6 +680,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
645
680
  toolName: partialToolCall.toolName,
646
681
  state: 'input-streaming',
647
682
  input: partialArgs,
683
+ rawInput: partialToolCall.text,
648
684
  title: partialToolCall.title,
649
685
  toolMetadata: partialToolCall.toolMetadata,
650
686
  });
@@ -654,6 +690,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
654
690
  toolName: partialToolCall.toolName,
655
691
  state: 'input-streaming',
656
692
  input: partialArgs,
693
+ rawInput: partialToolCall.text,
657
694
  title: partialToolCall.title,
658
695
  toolMetadata: partialToolCall.toolMetadata,
659
696
  });
@@ -297,6 +297,13 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
297
297
  | {
298
298
  state: 'input-streaming';
299
299
  input?: DeepPartial<asUITool<TOOL>['input']> | undefined;
300
+ /**
301
+ * The raw tool input text received so far.
302
+ *
303
+ * This is used to continue input streaming when a message is persisted
304
+ * and later resumed.
305
+ */
306
+ rawInput?: string;
300
307
  output?: never;
301
308
  errorText?: never;
302
309
  callProviderMetadata?: ProviderMetadata;
@@ -434,6 +441,13 @@ export type DynamicToolUIPart = {
434
441
  | {
435
442
  state: 'input-streaming';
436
443
  input?: unknown;
444
+ /**
445
+ * The raw tool input text received so far.
446
+ *
447
+ * This is used to continue input streaming when a message is persisted
448
+ * and later resumed.
449
+ */
450
+ rawInput?: string;
437
451
  output?: never;
438
452
  errorText?: never;
439
453
  callProviderMetadata?: ProviderMetadata;
@@ -154,6 +154,7 @@ const uiMessagesSchema = lazySchema(() => {
154
154
  toolMetadata: toolMetadataSchema.optional(),
155
155
  state: z.literal('input-streaming'),
156
156
  input: z.unknown().optional(),
157
+ rawInput: z.string().optional(),
157
158
  providerExecuted: z.boolean().optional(),
158
159
  callProviderMetadata: providerMetadataSchema.optional(),
159
160
  output: z.never().optional(),
@@ -257,6 +258,7 @@ const uiMessagesSchema = lazySchema(() => {
257
258
  providerExecuted: z.boolean().optional(),
258
259
  callProviderMetadata: providerMetadataSchema.optional(),
259
260
  input: z.unknown().optional(),
261
+ rawInput: z.string().optional(),
260
262
  output: z.never().optional(),
261
263
  errorText: z.never().optional(),
262
264
  approval: z.never().optional(),