ai 6.0.301 → 6.0.302

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.
@@ -135,6 +135,12 @@ export function addLanguageModelUsage(
135
135
  usage1: LanguageModelUsage,
136
136
  usage2: LanguageModelUsage,
137
137
  ): LanguageModelUsage {
138
+ const raw = isNullLanguageModelUsage(usage1)
139
+ ? usage2.raw
140
+ : isNullLanguageModelUsage(usage2)
141
+ ? usage1.raw
142
+ : undefined;
143
+
138
144
  return {
139
145
  inputTokens: addTokenCounts(usage1.inputTokens, usage2.inputTokens),
140
146
  inputTokenDetails: {
@@ -171,9 +177,24 @@ export function addLanguageModelUsage(
171
177
  usage1.cachedInputTokens,
172
178
  usage2.cachedInputTokens,
173
179
  ),
180
+ ...(raw == null ? {} : { raw }),
174
181
  };
175
182
  }
176
183
 
184
+ function isNullLanguageModelUsage(usage: LanguageModelUsage): boolean {
185
+ return (
186
+ usage.inputTokens == null &&
187
+ usage.inputTokenDetails?.noCacheTokens == null &&
188
+ usage.inputTokenDetails?.cacheReadTokens == null &&
189
+ usage.inputTokenDetails?.cacheWriteTokens == null &&
190
+ usage.outputTokens == null &&
191
+ usage.outputTokenDetails?.textTokens == null &&
192
+ usage.outputTokenDetails?.reasoningTokens == null &&
193
+ usage.totalTokens == null &&
194
+ usage.raw == null
195
+ );
196
+ }
197
+
177
198
  function addTokenCounts(
178
199
  tokenCount1: number | undefined,
179
200
  tokenCount2: number | undefined,
package/src/ui/chat.ts CHANGED
@@ -314,7 +314,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
314
314
  status: ChatStatus;
315
315
  error?: Error;
316
316
  }) {
317
- if (this.status === status) return;
317
+ if (this.status === status) {
318
+ if (this.error !== error) {
319
+ this.state.error = error;
320
+ }
321
+ return;
322
+ }
318
323
 
319
324
  this.state.status = status;
320
325
  this.state.error = error;
@@ -10,6 +10,7 @@ import {
10
10
  import type { ToolSet } from '../generate-text/tool-set';
11
11
  import { createToolModelOutput } from '../prompt/create-tool-model-output';
12
12
  import { MessageConversionError } from '../prompt/message-conversion-error';
13
+ import { getOwn } from '../util/get-own';
13
14
  import {
14
15
  getToolName,
15
16
  isDataUIPart,
@@ -27,6 +28,7 @@ import {
27
28
  type ToolUIPart,
28
29
  type UIMessage,
29
30
  } from './ui-messages';
31
+ import { isToolPartFromUnavailableTool } from './unavailable-tool';
30
32
 
31
33
  /**
32
34
  * Converts an array of UI messages from useChat into an array of ModelMessages that can be used
@@ -65,6 +67,39 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
65
67
  }));
66
68
  }
67
69
 
70
+ async function createModelOutput({
71
+ toolPart,
72
+ toolName,
73
+ output,
74
+ errorMode,
75
+ }: {
76
+ toolPart: ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | DynamicToolUIPart;
77
+ toolName: string;
78
+ output: unknown;
79
+ errorMode: 'none' | 'text' | 'json';
80
+ }) {
81
+ const tool = getOwn(options?.tools, toolName);
82
+
83
+ if (
84
+ errorMode === 'none' &&
85
+ tool == null &&
86
+ isToolPartFromUnavailableTool(toolPart)
87
+ ) {
88
+ return {
89
+ type: 'text' as const,
90
+ value: 'Tool output omitted because the tool is no longer available.',
91
+ };
92
+ }
93
+
94
+ return createToolModelOutput({
95
+ toolCallId: toolPart.toolCallId,
96
+ input: toolPart.input,
97
+ output,
98
+ tool,
99
+ errorMode,
100
+ });
101
+ }
102
+
68
103
  for (const message of messages) {
69
104
  switch (message.role) {
70
105
  case 'system': {
@@ -231,14 +266,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
231
266
  type: 'tool-result',
232
267
  toolCallId: part.toolCallId,
233
268
  toolName,
234
- output: await createToolModelOutput({
235
- toolCallId: part.toolCallId,
236
- input: part.input,
269
+ output: await createModelOutput({
270
+ toolPart: part,
271
+ toolName,
237
272
  output:
238
273
  part.state === 'output-error'
239
274
  ? part.errorText
240
275
  : part.output,
241
- tool: options?.tools?.[toolName],
242
276
  errorMode:
243
277
  part.state === 'output-error' ? 'json' : 'none',
244
278
  }),
@@ -331,14 +365,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
331
365
  type: 'tool-result',
332
366
  toolCallId: toolPart.toolCallId,
333
367
  toolName,
334
- output: await createToolModelOutput({
335
- toolCallId: toolPart.toolCallId,
336
- input: toolPart.input,
368
+ output: await createModelOutput({
369
+ toolPart,
370
+ toolName,
337
371
  output:
338
372
  toolPart.state === 'output-error'
339
373
  ? toolPart.errorText
340
374
  : toolPart.output,
341
- tool: options?.tools?.[toolName],
342
375
  errorMode:
343
376
  toolPart.state === 'output-error' ? 'text' : 'none',
344
377
  }),
@@ -11,10 +11,12 @@ import type { UIMessage } from './ui-messages';
11
11
 
12
12
  function appendPathToUrl(url: string, path: string): string {
13
13
  const queryOrFragmentStart = url.search(/[?#]/);
14
+ const urlPath =
15
+ queryOrFragmentStart === -1 ? url : url.slice(0, queryOrFragmentStart);
16
+ const suffix =
17
+ queryOrFragmentStart === -1 ? '' : url.slice(queryOrFragmentStart);
14
18
 
15
- return queryOrFragmentStart === -1
16
- ? `${url}${path}`
17
- : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
19
+ return `${urlPath.endsWith('/') && path.startsWith('/') ? urlPath.slice(0, -1) : urlPath}${path}${suffix}`;
18
20
  }
19
21
 
20
22
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
@@ -348,6 +348,14 @@ export type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{
348
348
  export type DynamicToolUIPart = {
349
349
  type: 'dynamic-tool';
350
350
 
351
+ /**
352
+ * `false` when a static tool part was normalized to a dynamic tool part
353
+ * because its current tool schema was unavailable or incompatible.
354
+ *
355
+ * This preserves its static origin across persistence boundaries.
356
+ */
357
+ dynamic?: false;
358
+
351
359
  /**
352
360
  * Name of the tool that is being called.
353
361
  */
@@ -0,0 +1,17 @@
1
+ import type { DynamicToolUIPart, ToolUIPart } from './ui-messages';
2
+
3
+ export function markToolPartAsUnavailable(
4
+ part: DynamicToolUIPart,
5
+ ): DynamicToolUIPart {
6
+ // Preserve the static tool's provenance across persistence boundaries so
7
+ // model-message conversion never falls back to its unfiltered raw output.
8
+ part.dynamic = false;
9
+
10
+ return part;
11
+ }
12
+
13
+ export function isToolPartFromUnavailableTool(
14
+ part: ToolUIPart | DynamicToolUIPart,
15
+ ): boolean {
16
+ return part.type === 'dynamic-tool' && part.dynamic === false;
17
+ }
@@ -15,6 +15,7 @@ import {
15
15
  import { z } from 'zod/v4';
16
16
  import { InvalidArgumentError } from '../error';
17
17
  import { jsonValueSchema } from '../types/json-value';
18
+ import { getOwn } from '../util/get-own';
18
19
  import { isDeepEqualData } from '../util/is-deep-equal-data';
19
20
  import { providerMetadataSchema } from '../types/provider-metadata';
20
21
  import type {
@@ -25,6 +26,7 @@ import type {
25
26
  ToolUIPart,
26
27
  UIMessage,
27
28
  } from './ui-messages';
29
+ import { markToolPartAsUnavailable } from './unavailable-tool';
28
30
 
29
31
  const toolMetadataSchema: z.ZodType<JSONObject> = z.record(
30
32
  z.string(),
@@ -43,11 +45,11 @@ function isEmptyObject(value: unknown): value is Record<string, never> {
43
45
  function asDynamicToolPart(toolPart: ToolUIPart): DynamicToolUIPart {
44
46
  const { type, ...part } = toolPart;
45
47
 
46
- return {
48
+ return markToolPartAsUnavailable({
47
49
  ...part,
48
50
  type: 'dynamic-tool',
49
51
  toolName: type.slice(5),
50
- } as DynamicToolUIPart;
52
+ } as DynamicToolUIPart);
51
53
  }
52
54
 
53
55
  function getToolPartInputSchemaInput(
@@ -134,6 +136,7 @@ const uiMessagesSchema = lazySchema(() => {
134
136
  }),
135
137
  z.object({
136
138
  type: z.literal('dynamic-tool'),
139
+ dynamic: z.literal(false).optional(),
137
140
  toolName: z.string(),
138
141
  toolCallId: z.string(),
139
142
  toolMetadata: toolMetadataSchema.optional(),
@@ -148,6 +151,7 @@ const uiMessagesSchema = lazySchema(() => {
148
151
  }),
149
152
  z.object({
150
153
  type: z.literal('dynamic-tool'),
154
+ dynamic: z.literal(false).optional(),
151
155
  toolName: z.string(),
152
156
  toolCallId: z.string(),
153
157
  toolMetadata: toolMetadataSchema.optional(),
@@ -161,6 +165,7 @@ const uiMessagesSchema = lazySchema(() => {
161
165
  }),
162
166
  z.object({
163
167
  type: z.literal('dynamic-tool'),
168
+ dynamic: z.literal(false).optional(),
164
169
  toolName: z.string(),
165
170
  toolCallId: z.string(),
166
171
  toolMetadata: toolMetadataSchema.optional(),
@@ -174,6 +179,7 @@ const uiMessagesSchema = lazySchema(() => {
174
179
  }),
175
180
  z.object({
176
181
  type: z.literal('dynamic-tool'),
182
+ dynamic: z.literal(false).optional(),
177
183
  toolName: z.string(),
178
184
  toolCallId: z.string(),
179
185
  toolMetadata: toolMetadataSchema.optional(),
@@ -187,6 +193,7 @@ const uiMessagesSchema = lazySchema(() => {
187
193
  }),
188
194
  z.object({
189
195
  type: z.literal('dynamic-tool'),
196
+ dynamic: z.literal(false).optional(),
190
197
  toolName: z.string(),
191
198
  toolCallId: z.string(),
192
199
  toolMetadata: toolMetadataSchema.optional(),
@@ -202,6 +209,7 @@ const uiMessagesSchema = lazySchema(() => {
202
209
  }),
203
210
  z.object({
204
211
  type: z.literal('dynamic-tool'),
212
+ dynamic: z.literal(false).optional(),
205
213
  toolName: z.string(),
206
214
  toolCallId: z.string(),
207
215
  toolMetadata: toolMetadataSchema.optional(),
@@ -217,6 +225,7 @@ const uiMessagesSchema = lazySchema(() => {
217
225
  }),
218
226
  z.object({
219
227
  type: z.literal('dynamic-tool'),
228
+ dynamic: z.literal(false).optional(),
220
229
  toolName: z.string(),
221
230
  toolCallId: z.string(),
222
231
  toolMetadata: toolMetadataSchema.optional(),
@@ -451,7 +460,7 @@ async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
451
460
  InferUIMessageTools<UI_MESSAGE>
452
461
  >;
453
462
  const toolName = toolPart.type.slice(5);
454
- const tool = tools?.[toolName];
463
+ const tool = getOwn(tools, toolName);
455
464
  const isTerminal =
456
465
  toolPart.state === 'output-available' ||
457
466
  toolPart.state === 'output-error' ||