ai 7.0.119 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.d.ts +30 -3
  3. package/dist/index.js +132 -36
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +1 -1
  6. package/docs/02-foundations/02-providers-and-models.mdx +1 -0
  7. package/docs/02-getting-started/00-choosing-a-provider.mdx +4 -4
  8. package/docs/02-getting-started/02-nextjs-app-router.mdx +2 -2
  9. package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -2
  10. package/docs/02-getting-started/04-svelte.mdx +6 -6
  11. package/docs/02-getting-started/05-nuxt.mdx +6 -6
  12. package/docs/02-getting-started/06-nodejs.mdx +2 -2
  13. package/docs/02-getting-started/07-expo.mdx +2 -2
  14. package/docs/02-getting-started/08-tanstack-start.mdx +2 -2
  15. package/docs/03-agents/06-policy-tool-approvals.mdx +2 -2
  16. package/docs/03-agents/07-workflow-agent.mdx +10 -10
  17. package/docs/03-ai-sdk-core/20-prompt-engineering.mdx +1 -1
  18. package/docs/03-ai-sdk-core/26-reasoning.mdx +1 -1
  19. package/docs/03-ai-sdk-core/45-provider-management.mdx +4 -4
  20. package/docs/04-ai-sdk-ui/02-chatbot.mdx +10 -1
  21. package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +8 -0
  22. package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +10 -3
  23. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +10 -1
  24. package/docs/05-ai-sdk-rsc/04-multistep-interfaces.mdx +1 -1
  25. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
  26. package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +8 -8
  27. package/docs/08-migration-guides/29-migration-guide-4-0.mdx +5 -5
  28. package/docs/08-migration-guides/37-migration-guide-3-3.mdx +1 -1
  29. package/package.json +11 -11
  30. package/src/embed/embed-many.ts +5 -3
  31. package/src/generate-image/generate-image.ts +8 -3
  32. package/src/generate-text/smooth-stream.ts +24 -9
  33. package/src/generate-text/stream-text-result.ts +5 -0
  34. package/src/generate-video/generate-video.ts +6 -4
  35. package/src/registry/provider-registry.ts +1 -1
  36. package/src/ui/chat.ts +45 -7
  37. package/src/ui/http-chat-transport.ts +19 -3
  38. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +15 -3
  39. package/src/ui/process-ui-message-stream.ts +59 -18
  40. package/src/ui/ui-messages.ts +14 -0
  41. package/src/ui/validate-ui-messages.ts +2 -0
  42. package/src/ui-message-stream/to-ui-message-chunk.ts +9 -1
  43. package/src/ui-message-stream/to-ui-message-stream.ts +1 -1
  44. package/src/util/set-own.ts +16 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.119",
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.97",
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.98",
51
- "@ai-sdk/deepseek": "3.0.55",
52
- "@ai-sdk/google": "4.0.83",
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
@@ -263,6 +263,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
263
263
  private pendingApprovalMessageId: string | undefined;
264
264
  private activeResponse: ActiveResponse<UI_MESSAGE> | undefined = undefined;
265
265
  private activeResumeRequest: ActiveResumeRequest | undefined = undefined;
266
+ private resumableStreamState:
267
+ | StreamingUIMessageState<UI_MESSAGE>
268
+ | undefined = undefined;
266
269
  private jobExecutor = new SerialJobExecutor();
267
270
 
268
271
  constructor({
@@ -723,6 +726,10 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
723
726
  trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
724
727
  messageId?: string;
725
728
  } & ChatRequestOptions) {
729
+ if (trigger !== 'resume-stream') {
730
+ this.resumableStreamState = undefined;
731
+ }
732
+
726
733
  const abortController = new AbortController();
727
734
  const activeResumeRequest =
728
735
  trigger === 'resume-stream' ? { abortController } : undefined;
@@ -760,6 +767,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
760
767
  await reconnect?.cancel().catch(() => {});
761
768
  if (isCurrentRequest()) {
762
769
  this.setStatus({ status: 'ready' });
770
+ this.resumableStreamState = undefined;
763
771
  }
764
772
  clearActiveResumeRequest();
765
773
  return;
@@ -767,6 +775,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
767
775
 
768
776
  if (reconnect == null) {
769
777
  this.setStatus({ status: 'ready' });
778
+ this.resumableStreamState = undefined;
770
779
  clearActiveResumeRequest();
771
780
  return; // no active stream found, so we do not resume
772
781
  }
@@ -779,6 +788,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
779
788
  ) {
780
789
  if (isCurrentRequest()) {
781
790
  this.setStatus({ status: 'ready' });
791
+ this.resumableStreamState = undefined;
782
792
  }
783
793
  clearActiveResumeRequest();
784
794
  return;
@@ -808,6 +818,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
808
818
  responseMessageIndex === -1
809
819
  ? lastMessage
810
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;
811
829
  const usesEarlierAssistantMessage =
812
830
  responseMessageIndex !== -1 &&
813
831
  responseMessageIndex < this.state.messages.length - 1 &&
@@ -820,13 +838,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
820
838
 
821
839
  try {
822
840
  const response = {
823
- state: createStreamingUIMessageState({
824
- lastMessage:
825
- trigger === 'resume-stream' || trigger === 'regenerate-message'
826
- ? undefined
827
- : this.state.snapshot(responseMessage),
828
- messageId: this.generateId(),
829
- }),
841
+ state:
842
+ trigger === 'resume-stream' && this.resumableStreamState != null
843
+ ? this.resumableStreamState
844
+ : createStreamingUIMessageState({
845
+ lastMessage:
846
+ trigger === 'resume-stream'
847
+ ? resumableResponseMessage
848
+ : trigger === 'regenerate-message'
849
+ ? undefined
850
+ : this.state.snapshot(responseMessage),
851
+ messageId: this.generateId(),
852
+ }),
830
853
  abortController,
831
854
  } as ActiveResponse<UI_MESSAGE>;
832
855
 
@@ -916,12 +939,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
916
939
  if (isAbort) {
917
940
  if (isCurrentRequest()) {
918
941
  this.setStatus({ status: 'ready' });
942
+ if (this.resumableStreamState === response.state) {
943
+ this.resumableStreamState = undefined;
944
+ }
919
945
  }
920
946
  return null;
921
947
  }
922
948
 
923
949
  if (isCurrentRequest()) {
924
950
  this.setStatus({ status: 'ready' });
951
+ if (this.resumableStreamState === response.state) {
952
+ this.resumableStreamState = undefined;
953
+ }
925
954
  }
926
955
  } catch (err) {
927
956
  // Ignore abort errors as they are expected.
@@ -929,6 +958,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
929
958
  isAbort = true;
930
959
  if (isCurrentRequest()) {
931
960
  this.setStatus({ status: 'ready' });
961
+ if (this.resumableStreamState === activeResponse?.state) {
962
+ this.resumableStreamState = undefined;
963
+ }
932
964
  }
933
965
  return null;
934
966
  }
@@ -948,6 +980,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
948
980
  isDisconnect = true;
949
981
  }
950
982
 
983
+ if (isDisconnect) {
984
+ this.resumableStreamState = activeResponse?.state;
985
+ } else if (this.resumableStreamState === activeResponse?.state) {
986
+ this.resumableStreamState = undefined;
987
+ }
988
+
951
989
  if (this.onError && err instanceof Error) {
952
990
  this.onError(err);
953
991
  }
@@ -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
  });
@@ -816,7 +853,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
816
853
  providerExecuted: chunk.providerExecuted,
817
854
  providerMetadata: chunk.providerMetadata,
818
855
  title: toolInvocation.title,
819
- toolMetadata: toolInvocation.toolMetadata,
856
+ toolMetadata:
857
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
820
858
  },
821
859
  toolInvocation,
822
860
  );
@@ -832,7 +870,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
832
870
  preliminary: chunk.preliminary,
833
871
  providerMetadata: chunk.providerMetadata,
834
872
  title: toolInvocation.title,
835
- toolMetadata: toolInvocation.toolMetadata,
873
+ toolMetadata:
874
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
836
875
  },
837
876
  toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
838
877
  );
@@ -856,7 +895,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
856
895
  providerExecuted: chunk.providerExecuted,
857
896
  providerMetadata: chunk.providerMetadata,
858
897
  title: toolInvocation.title,
859
- toolMetadata: toolInvocation.toolMetadata,
898
+ toolMetadata:
899
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
860
900
  },
861
901
  toolInvocation,
862
902
  );
@@ -872,7 +912,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
872
912
  providerExecuted: chunk.providerExecuted,
873
913
  providerMetadata: chunk.providerMetadata,
874
914
  title: toolInvocation.title,
875
- toolMetadata: toolInvocation.toolMetadata,
915
+ toolMetadata:
916
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
876
917
  },
877
918
  toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
878
919
  );
@@ -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(),
@@ -18,6 +18,11 @@ export type ToUIMessageChunkOptions<
18
18
  sendSources?: boolean;
19
19
  sendStart?: boolean;
20
20
  sendFinish?: boolean;
21
+ /**
22
+ * Formats stream errors, invalid tool calls, and application-executed tool
23
+ * errors. Provider-executed tool execution errors bypass this callback so
24
+ * their original error data survives model-message round trips.
25
+ */
21
26
  onError?: (error: unknown) => string;
22
27
  messageMetadata?: InferUIMessageMetadata<UI_MESSAGE>;
23
28
  responseMessageId?: string;
@@ -40,7 +45,7 @@ export function toUIMessageChunk<
40
45
  sendSources = false,
41
46
  sendStart = true,
42
47
  sendFinish = true,
43
- onError = () => 'An error occurred.', // prevent leaking server error details to the client by default
48
+ onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
44
49
  messageMetadata,
45
50
  responseMessageId,
46
51
  }: ToUIMessageChunkOptions<TOOLS, UI_MESSAGE> = {},
@@ -305,6 +310,9 @@ export function toUIMessageChunk<
305
310
  case 'tool-error': {
306
311
  const dynamic = isDynamic(part);
307
312
 
313
+ // Preserve provider error codes for model-message round trips and harness
314
+ // runtime error messages. These execution errors intentionally bypass
315
+ // onError; invalid tool calls and stream errors still go through it.
308
316
  return {
309
317
  type: 'tool-output-error',
310
318
  toolCallId: part.toolCallId,
@@ -26,7 +26,7 @@ export function toUIMessageStream<
26
26
  sendSources = false,
27
27
  sendStart = true,
28
28
  sendFinish = true,
29
- onError = () => 'An error occurred.', // prevent leaking server error details to the client by default
29
+ onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
30
30
  messageMetadata,
31
31
  originalMessages,
32
32
  generateMessageId,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Writes an enumerable own property without invoking inherited setters such as
3
+ * `Object.prototype.__proto__`. Keeps the target object's prototype unchanged.
4
+ */
5
+ export function setOwn<T>(
6
+ object: Record<string, T>,
7
+ key: string,
8
+ value: T,
9
+ ): void {
10
+ Object.defineProperty(object, key, {
11
+ value,
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ });
16
+ }