ai 7.0.128 → 7.0.130

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 (50) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.d.ts +19 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +173 -39
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +10 -2
  7. package/dist/internal/index.d.ts.map +1 -1
  8. package/dist/internal/index.js +68 -16
  9. package/dist/internal/index.js.map +1 -1
  10. package/docs/02-foundations/06-provider-options.mdx +11 -10
  11. package/docs/03-agents/02-building-agents.mdx +16 -11
  12. package/docs/03-agents/07-workflow-agent.mdx +3 -3
  13. package/docs/03-ai-sdk-core/17-runtime-and-tool-context.mdx +18 -14
  14. package/docs/03-ai-sdk-core/25-settings.mdx +3 -2
  15. package/docs/03-ai-sdk-core/26-reasoning.mdx +12 -5
  16. package/docs/03-ai-sdk-core/32-decisions.mdx +20 -20
  17. package/docs/03-ai-sdk-core/35-image-generation.mdx +2 -0
  18. package/docs/04-ai-sdk-ui/21-error-handling.mdx +28 -3
  19. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +2 -2
  20. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +3 -3
  21. package/docs/07-reference/01-ai-sdk-core/14-decide.mdx +2 -1
  22. package/docs/07-reference/05-ai-sdk-errors/ai-decision-refusal-error.mdx +33 -0
  23. package/package.json +15 -15
  24. package/src/decide/decide.ts +11 -0
  25. package/src/decide/validate-decision.ts +1 -0
  26. package/src/error/index.ts +1 -0
  27. package/src/generate-object/generate-object.ts +10 -0
  28. package/src/generate-object/stream-object.ts +10 -0
  29. package/src/generate-speech/index.ts +13 -1
  30. package/src/generate-text/generate-text.ts +3 -1
  31. package/src/generate-text/stream-text.ts +22 -5
  32. package/src/generate-text/tool-approval-signature.ts +7 -4
  33. package/src/global.ts +2 -1
  34. package/src/logger/deprecations.ts +72 -0
  35. package/src/logger/log-warnings.ts +25 -3
  36. package/src/model/as-language-model-v3.ts +4 -0
  37. package/src/prompt/convert-to-language-model-prompt.ts +7 -1
  38. package/src/realtime/encode-realtime-frame.ts +7 -2
  39. package/src/transcribe/index.ts +13 -1
  40. package/src/types/index.ts +1 -0
  41. package/src/types/language-model.ts +9 -1
  42. package/src/types/usage.ts +21 -0
  43. package/src/ui/chat.ts +16 -8
  44. package/src/ui/convert-to-model-messages.ts +40 -8
  45. package/src/ui/http-chat-transport.ts +5 -3
  46. package/src/ui/process-text-stream.ts +5 -1
  47. package/src/ui/text-stream-chat-transport.ts +3 -1
  48. package/src/ui/ui-messages.ts +8 -0
  49. package/src/ui/unavailable-tool.ts +17 -0
  50. package/src/ui/validate-ui-messages.ts +10 -2
@@ -1099,7 +1099,9 @@ export async function generateText<
1099
1099
  > = {};
1100
1100
  const blockedToolCallIds = new Set<string>();
1101
1101
 
1102
- const generatedFileDataCache = new WeakMap();
1102
+ const generatedFileDataCache: Parameters<
1103
+ typeof convertLanguageModelContent
1104
+ >[0]['generatedFileDataCache'] = new WeakMap();
1103
1105
  const modelCallContent = await convertLanguageModelContent({
1104
1106
  content: currentModelResponse.content,
1105
1107
  toolCalls: stepToolCalls,
@@ -1849,16 +1849,30 @@ class DefaultStreamTextResult<
1849
1849
 
1850
1850
  // resilient stream that handles abort signals and errors:
1851
1851
  const reader = stitchableStream.stream.getReader();
1852
+ const cancelOnAbort = () => {
1853
+ // Result promises must settle before any potentially stalled callback.
1854
+ this.rejectResultPromises(abortSignal?.reason);
1855
+ // Cancelling the reader releases a pending read immediately, even when
1856
+ // the provider body or its cancellation promise does not settle.
1857
+ void reader.cancel(abortSignal?.reason).catch(() => {});
1858
+ };
1859
+ const removeAbortListener = () =>
1860
+ abortSignal?.removeEventListener('abort', cancelOnAbort);
1852
1861
  let stream = new ReadableStream<InternalTextStreamPart<TOOLS>>({
1853
1862
  async start(controller) {
1854
1863
  // send start event:
1855
1864
  controller.enqueue({ type: 'start' });
1865
+ abortSignal?.addEventListener('abort', cancelOnAbort, { once: true });
1866
+ if (abortSignal?.aborted) {
1867
+ cancelOnAbort();
1868
+ }
1856
1869
  },
1857
1870
 
1858
1871
  async pull(controller) {
1859
1872
  // abort handling:
1860
1873
  async function abort() {
1861
1874
  isAborted = true;
1875
+ removeAbortListener();
1862
1876
 
1863
1877
  await notify({
1864
1878
  event: {
@@ -1885,18 +1899,20 @@ class DefaultStreamTextResult<
1885
1899
  try {
1886
1900
  const { done, value } = await reader.read();
1887
1901
 
1888
- if (done) {
1889
- controller.close();
1902
+ if (abortSignal?.aborted) {
1903
+ await abort();
1890
1904
  return;
1891
1905
  }
1892
1906
 
1893
- if (abortSignal?.aborted) {
1894
- await abort();
1907
+ if (done) {
1908
+ removeAbortListener();
1909
+ controller.close();
1895
1910
  return;
1896
1911
  }
1897
1912
 
1898
1913
  controller.enqueue(value);
1899
1914
  } catch (error) {
1915
+ removeAbortListener();
1900
1916
  if (isAbortError(error) && abortSignal?.aborted) {
1901
1917
  await abort();
1902
1918
  } else {
@@ -1907,7 +1923,8 @@ class DefaultStreamTextResult<
1907
1923
  },
1908
1924
 
1909
1925
  cancel(reason) {
1910
- return stitchableStream.stream.cancel(reason);
1926
+ removeAbortListener();
1927
+ return reader.cancel(reason);
1911
1928
  },
1912
1929
  });
1913
1930
 
@@ -3,12 +3,15 @@ import { hashCanonical, toBase64url } from '../util/canonical-hash';
3
3
 
4
4
  const encoder = new TextEncoder();
5
5
 
6
- function fromBase64url(str: string): Uint8Array {
6
+ function fromBase64url(str: string) {
7
7
  return convertBase64ToUint8Array(str);
8
8
  }
9
9
 
10
10
  async function importKey(secret: string | Uint8Array): Promise<CryptoKey> {
11
- const keyData = typeof secret === 'string' ? encoder.encode(secret) : secret;
11
+ const keyData =
12
+ typeof secret === 'string'
13
+ ? encoder.encode(secret)
14
+ : (secret as Uint8Array<ArrayBuffer>);
12
15
  return crypto.subtle.importKey(
13
16
  'raw',
14
17
  keyData,
@@ -26,7 +29,7 @@ function buildPayload(
26
29
  toolCallId: string,
27
30
  toolName: string,
28
31
  inputDigest: string,
29
- ): Uint8Array {
32
+ ) {
30
33
  return encoder.encode(
31
34
  JSON.stringify([
32
35
  'ai-sdk-tool-approval-v1',
@@ -49,7 +52,7 @@ function buildLegacyPayload(
49
52
  toolCallId: string,
50
53
  toolName: string,
51
54
  inputDigest: string,
52
- ): Uint8Array {
55
+ ) {
53
56
  return encoder.encode(
54
57
  `${approvalId}\n${toolCallId}\n${toolName}\n${inputDigest}`,
55
58
  );
package/src/global.ts CHANGED
@@ -18,7 +18,8 @@ declare global {
18
18
  /**
19
19
  * The warning logger to use for the AI SDK.
20
20
  *
21
- * If not set, the default logger is the console.warn function.
21
+ * If not set, warnings use process.emitWarning when available, otherwise
22
+ * console.warn. Deprecations are emitted once per code by the default logger.
22
23
  *
23
24
  * If set to false, no warnings are logged.
24
25
  */
@@ -0,0 +1,72 @@
1
+ // These codes are stable identifiers. Do not rename or reuse an existing code.
2
+ const deprecationCodes = new Map<string, string>([
3
+ ['generateObject', 'AISDK_DEP_GENERATE_OBJECT'],
4
+ ['streamObject', 'AISDK_DEP_STREAM_OBJECT'],
5
+ ['experimental_generateSpeech', 'AISDK_DEP_EXPERIMENTAL_GENERATE_SPEECH'],
6
+ ['experimental_transcribe', 'AISDK_DEP_EXPERIMENTAL_TRANSCRIBE'],
7
+ ['"image" content part', 'AISDK_DEP_IMAGE_CONTENT_PART'],
8
+ [
9
+ '"tool-result" content of type "file-data"',
10
+ 'AISDK_DEP_TOOL_RESULT_FILE_DATA',
11
+ ],
12
+ [
13
+ '"tool-result" content of type "file-url"',
14
+ 'AISDK_DEP_TOOL_RESULT_FILE_URL',
15
+ ],
16
+ ['"tool-result" content of type "file-id"', 'AISDK_DEP_TOOL_RESULT_FILE_ID'],
17
+ [
18
+ '"tool-result" content of type "file-reference"',
19
+ 'AISDK_DEP_TOOL_RESULT_FILE_REFERENCE',
20
+ ],
21
+ [
22
+ '"tool-result" content of type "image-data"',
23
+ 'AISDK_DEP_TOOL_RESULT_IMAGE_DATA',
24
+ ],
25
+ [
26
+ '"tool-result" content of type "image-url"',
27
+ 'AISDK_DEP_TOOL_RESULT_IMAGE_URL',
28
+ ],
29
+ [
30
+ '"tool-result" content of type "image-file-id"',
31
+ 'AISDK_DEP_TOOL_RESULT_IMAGE_FILE_ID',
32
+ ],
33
+ [
34
+ '"tool-result" content of type "image-file-reference"',
35
+ 'AISDK_DEP_TOOL_RESULT_IMAGE_FILE_REFERENCE',
36
+ ],
37
+ [
38
+ 'rawInput in output-error UI message parts',
39
+ 'AISDK_DEP_UI_MESSAGE_RAW_INPUT',
40
+ ],
41
+ ]);
42
+
43
+ /**
44
+ * Escape punctuation and UTF-16 code units without losing case or collapsing
45
+ * distinct settings. Underscores are escaped too, so `__` separates code parts.
46
+ */
47
+ function encodeCodePart(value: string): string {
48
+ return value.replace(
49
+ /[^a-zA-Z0-9]/g,
50
+ character =>
51
+ `_${character.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`,
52
+ );
53
+ }
54
+
55
+ export function getDeprecationCode({
56
+ setting,
57
+ provider,
58
+ }: {
59
+ setting: string;
60
+ provider?: string;
61
+ }): string {
62
+ if (provider == null) {
63
+ return (
64
+ deprecationCodes.get(setting) ??
65
+ `AISDK_DEP_SETTING_${encodeCodePart(setting)}`
66
+ );
67
+ }
68
+
69
+ // Provider codes are scoped to the provider, but not to a particular model or
70
+ // message. This works for third-party providers without changing their spec.
71
+ return `AISDK_DEP_PROVIDER_${encodeCodePart(provider)}__${encodeCodePart(setting)}`;
72
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Warning } from '../types';
2
+ import { getDeprecationCode } from './deprecations';
2
3
 
3
4
  /**
4
5
  * A function for logging warnings.
@@ -87,21 +88,24 @@ export const FIRST_WARNING_INFO_MESSAGE =
87
88
  'AI SDK Warning System: To turn off warning logging, set the AI_SDK_LOG_WARNINGS global to false.';
88
89
 
89
90
  let hasLoggedBefore = false;
91
+ const emittedDeprecationCodes = new Set<string>();
90
92
 
91
93
  function emitWarning({
92
94
  message,
93
95
  type,
96
+ code,
94
97
  }: {
95
98
  message: string;
96
99
  type: 'DeprecationWarning' | 'Warning';
100
+ code?: string;
97
101
  }) {
98
102
  if (
99
103
  typeof process !== 'undefined' &&
100
104
  typeof process.emitWarning === 'function'
101
105
  ) {
102
- process.emitWarning(message, { type });
106
+ process.emitWarning(message, code == null ? { type } : { type, code });
103
107
  } else {
104
- console.warn(message);
108
+ console.warn(code == null ? message : `[${code}] ${message}`);
105
109
  }
106
110
  }
107
111
 
@@ -111,7 +115,9 @@ function emitWarning({
111
115
  * The behavior can be customized via the `AI_SDK_LOG_WARNINGS` global variable:
112
116
  * - If set to `false`, warnings are suppressed.
113
117
  * - If set to a function, that function is called with the warnings.
114
- * - Otherwise, warnings are logged to the console using `console.warn`.
118
+ * - Otherwise, warnings use `process.emitWarning` when available, falling back
119
+ * to `console.warn`. Deprecations have stable codes and are emitted once per
120
+ * code by the default logger. Custom loggers receive every warning.
115
121
  *
116
122
  * @param options - The options containing warnings and context.
117
123
  * @param options.warnings - The warnings to log.
@@ -148,6 +154,20 @@ export const logWarnings: LogWarningsFunction = options => {
148
154
 
149
155
  // default behavior: log warnings via process.emitWarning if available, otherwise console.warn
150
156
  for (const warning of options.warnings) {
157
+ const code =
158
+ warning.type === 'deprecated'
159
+ ? getDeprecationCode({
160
+ setting: warning.setting,
161
+ provider: options.provider,
162
+ })
163
+ : undefined;
164
+ if (code != null) {
165
+ if (emittedDeprecationCodes.has(code)) {
166
+ continue;
167
+ }
168
+ emittedDeprecationCodes.add(code);
169
+ }
170
+
151
171
  const message = formatWarning({
152
172
  warning,
153
173
  provider: options.provider,
@@ -156,6 +176,7 @@ export const logWarnings: LogWarningsFunction = options => {
156
176
  emitWarning({
157
177
  message,
158
178
  type: warning.type === 'deprecated' ? 'DeprecationWarning' : 'Warning',
179
+ code,
159
180
  });
160
181
  }
161
182
  };
@@ -165,4 +186,5 @@ export const logWarnings: LogWarningsFunction = options => {
165
186
  */
166
187
  export const resetLogWarningsState = () => {
167
188
  hasLoggedBefore = false;
189
+ emittedDeprecationCodes.clear();
168
190
  };
@@ -99,5 +99,9 @@ function convertV2UsageToV3(usage: LanguageModelV2Usage): LanguageModelV3Usage {
99
99
  text: undefined,
100
100
  reasoning: usage.reasoningTokens,
101
101
  },
102
+ raw:
103
+ usage.totalTokens == null
104
+ ? undefined
105
+ : { totalTokens: usage.totalTokens },
102
106
  };
103
107
  }
@@ -123,7 +123,13 @@ export async function convertToLanguageModelPrompt({
123
123
  }
124
124
 
125
125
  lastCombinedMessage.content.push(...message.content);
126
- lastCombinedMessage.providerOptions = message.providerOptions;
126
+ lastCombinedMessage.providerOptions =
127
+ lastContentPart == null
128
+ ? mergeObjects(
129
+ lastCombinedMessage.providerOptions,
130
+ message.providerOptions,
131
+ )
132
+ : message.providerOptions;
127
133
  } else {
128
134
  combinedMessages.push(message);
129
135
  }
@@ -3,14 +3,19 @@ export const REALTIME_MAX_FRAME_BYTES = 128 * 1024;
3
3
  export const REALTIME_MAX_BUFFERED_BYTES = 128 * 1024;
4
4
 
5
5
  export type EncodedRealtimeFrame = {
6
- data: string | ArrayBuffer | ArrayBufferView | Blob;
6
+ data: string | ArrayBuffer | ArrayBufferView<ArrayBuffer> | Blob;
7
7
  byteLength: number;
8
8
  };
9
9
 
10
10
  /** Serialize once, measuring the exact payload passed to the browser. */
11
11
  export function encodeRealtimeFrame(value: unknown): EncodedRealtimeFrame {
12
- if (value instanceof ArrayBuffer || ArrayBuffer.isView(value))
12
+ if (value instanceof ArrayBuffer)
13
13
  return { data: value, byteLength: value.byteLength };
14
+ if (ArrayBuffer.isView(value))
15
+ return {
16
+ data: value as ArrayBufferView<ArrayBuffer>,
17
+ byteLength: value.byteLength,
18
+ };
14
19
  if (value instanceof Blob) return { data: value, byteLength: value.size };
15
20
  const data = typeof value === 'string' ? value : JSON.stringify(value);
16
21
  if (data === undefined)
@@ -1,5 +1,6 @@
1
1
  import type { TranscriptionResult } from './transcribe-result';
2
2
  import { transcribe } from './transcribe';
3
+ import { logWarnings } from '../logger/log-warnings';
3
4
 
4
5
  export { transcribe } from './transcribe';
5
6
  export { streamTranscribe as experimental_streamTranscribe } from './stream-transcribe';
@@ -20,7 +21,18 @@ export type {
20
21
  /**
21
22
  * @deprecated Use `transcribe` instead.
22
23
  */
23
- const experimental_transcribe = transcribe;
24
+ const experimental_transcribe: typeof transcribe = options => {
25
+ logWarnings({
26
+ warnings: [
27
+ {
28
+ type: 'deprecated',
29
+ setting: 'experimental_transcribe',
30
+ message: 'Use transcribe instead.',
31
+ },
32
+ ],
33
+ });
34
+ return transcribe(options);
35
+ };
24
36
  export { experimental_transcribe };
25
37
 
26
38
  /**
@@ -7,6 +7,7 @@ export type { ImageModelResponseMetadata } from './image-model-response-metadata
7
7
  export type { JSONValue } from './json-value';
8
8
  export type {
9
9
  CallWarning,
10
+ CallWarningRegistry,
10
11
  FinishReason,
11
12
  LanguageModel,
12
13
  ToolChoice,
@@ -80,11 +80,19 @@ export type FinishReason =
80
80
  | 'error'
81
81
  | 'other';
82
82
 
83
+ /**
84
+ * Registry for call warning variants. Packages that expose additional warning
85
+ * protocols can augment this interface.
86
+ */
87
+ export interface CallWarningRegistry {
88
+ sharedV4: SharedV4Warning;
89
+ }
90
+
83
91
  /**
84
92
  * Warning from the model provider for this call. The call will proceed, but e.g.
85
93
  * some settings might not be supported, which can lead to suboptimal results.
86
94
  */
87
- export type CallWarning = SharedV4Warning;
95
+ export type CallWarning = CallWarningRegistry[keyof CallWarningRegistry];
88
96
 
89
97
  /**
90
98
  * A source that has been used as input to generate the response.
@@ -123,6 +123,12 @@ export function addLanguageModelUsage(
123
123
  usage1: LanguageModelUsage,
124
124
  usage2: LanguageModelUsage,
125
125
  ): LanguageModelUsage {
126
+ const raw = isNullLanguageModelUsage(usage1)
127
+ ? usage2.raw
128
+ : isNullLanguageModelUsage(usage2)
129
+ ? usage1.raw
130
+ : undefined;
131
+
126
132
  return {
127
133
  inputTokens: addTokenCounts(usage1.inputTokens, usage2.inputTokens),
128
134
  inputTokenDetails: {
@@ -151,9 +157,24 @@ export function addLanguageModelUsage(
151
157
  ),
152
158
  },
153
159
  totalTokens: addTokenCounts(usage1.totalTokens, usage2.totalTokens),
160
+ ...(raw == null ? {} : { raw }),
154
161
  };
155
162
  }
156
163
 
164
+ function isNullLanguageModelUsage(usage: LanguageModelUsage): boolean {
165
+ return (
166
+ usage.inputTokens == null &&
167
+ usage.inputTokenDetails?.noCacheTokens == null &&
168
+ usage.inputTokenDetails?.cacheReadTokens == null &&
169
+ usage.inputTokenDetails?.cacheWriteTokens == null &&
170
+ usage.outputTokens == null &&
171
+ usage.outputTokenDetails?.textTokens == null &&
172
+ usage.outputTokenDetails?.reasoningTokens == null &&
173
+ usage.totalTokens == null &&
174
+ usage.raw == null
175
+ );
176
+ }
177
+
157
178
  function addTokenCounts(
158
179
  tokenCount1: number | undefined,
159
180
  tokenCount2: number | undefined,
package/src/ui/chat.ts CHANGED
@@ -315,7 +315,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
315
315
  status: ChatStatus;
316
316
  error?: Error;
317
317
  }) {
318
- if (this.status === status) return;
318
+ if (this.status === status) {
319
+ if (this.error !== error) {
320
+ this.state.error = error;
321
+ }
322
+ return;
323
+ }
319
324
 
320
325
  this.state.status = status;
321
326
  this.state.error = error;
@@ -1010,13 +1015,16 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
1010
1015
 
1011
1016
  isError = true;
1012
1017
 
1013
- // Network errors such as disconnected, timeout, etc.
1014
- if (
1015
- err instanceof TypeError &&
1016
- (err.message.toLowerCase().includes('fetch') ||
1017
- err.message.toLowerCase().includes('network'))
1018
- ) {
1019
- isDisconnect = true;
1018
+ if (err instanceof TypeError) {
1019
+ const message = err.message.toLowerCase();
1020
+
1021
+ isDisconnect =
1022
+ // Chromium request failures; Node.js fetch failures.
1023
+ message.includes('fetch') ||
1024
+ // Firefox request failures; Chromium response-body failures.
1025
+ message.includes('network') ||
1026
+ // Safari/WebKit request and response-body failures.
1027
+ message === 'load failed';
1020
1028
  }
1021
1029
 
1022
1030
  if (isDisconnect) {
@@ -33,6 +33,7 @@ import {
33
33
  type ToolUIPart,
34
34
  type UIMessage,
35
35
  } from './ui-messages';
36
+ import { isToolPartFromUnavailableTool } from './unavailable-tool';
36
37
  import { warnIfUIMessageHasDeprecatedRawInput } from './warn-if-ui-message-has-deprecated-raw-input';
37
38
  /**
38
39
  * Converts an array of UI messages from useChat into an array of ModelMessages that can be used
@@ -85,6 +86,39 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
85
86
  }));
86
87
  }
87
88
 
89
+ async function createModelOutput({
90
+ toolPart,
91
+ toolName,
92
+ output,
93
+ errorMode,
94
+ }: {
95
+ toolPart: ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | DynamicToolUIPart;
96
+ toolName: string;
97
+ output: unknown;
98
+ errorMode: 'none' | 'text' | 'json';
99
+ }) {
100
+ const tool = getOwn(options?.tools, toolName);
101
+
102
+ if (
103
+ errorMode === 'none' &&
104
+ tool == null &&
105
+ isToolPartFromUnavailableTool(toolPart)
106
+ ) {
107
+ return {
108
+ type: 'text' as const,
109
+ value: 'Tool output omitted because the tool is no longer available.',
110
+ };
111
+ }
112
+
113
+ return createToolModelOutput({
114
+ toolCallId: toolPart.toolCallId,
115
+ input: toolPart.input,
116
+ output,
117
+ tool,
118
+ errorMode,
119
+ });
120
+ }
121
+
88
122
  for (const message of messages) {
89
123
  switch (message.role) {
90
124
  case 'system': {
@@ -284,14 +318,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
284
318
  type: 'tool-result',
285
319
  toolCallId: part.toolCallId,
286
320
  toolName,
287
- output: await createToolModelOutput({
288
- toolCallId: part.toolCallId,
289
- input: part.input,
321
+ output: await createModelOutput({
322
+ toolPart: part,
323
+ toolName,
290
324
  output:
291
325
  part.state === 'output-error'
292
326
  ? part.errorText
293
327
  : part.output,
294
- tool: getOwn(options?.tools, toolName),
295
328
  errorMode:
296
329
  part.state === 'output-error' ? 'json' : 'none',
297
330
  }),
@@ -403,14 +436,13 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
403
436
  type: 'tool-result',
404
437
  toolCallId: toolPart.toolCallId,
405
438
  toolName,
406
- output: await createToolModelOutput({
407
- toolCallId: toolPart.toolCallId,
408
- input: toolPart.input,
439
+ output: await createModelOutput({
440
+ toolPart,
441
+ toolName,
409
442
  output:
410
443
  toolPart.state === 'output-error'
411
444
  ? toolPart.errorText
412
445
  : toolPart.output,
413
- tool: getOwn(options?.tools, toolName),
414
446
  errorMode:
415
447
  toolPart.state === 'output-error' ? 'text' : 'none',
416
448
  }),
@@ -13,10 +13,12 @@ import type { UIMessage } from './ui-messages';
13
13
 
14
14
  function appendPathToUrl(url: string, path: string): string {
15
15
  const queryOrFragmentStart = url.search(/[?#]/);
16
+ const urlPath =
17
+ queryOrFragmentStart === -1 ? url : url.slice(0, queryOrFragmentStart);
18
+ const suffix =
19
+ queryOrFragmentStart === -1 ? '' : url.slice(queryOrFragmentStart);
16
20
 
17
- return queryOrFragmentStart === -1
18
- ? `${url}${path}`
19
- : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
21
+ return `${urlPath.endsWith('/') && path.startsWith('/') ? urlPath.slice(0, -1) : urlPath}${path}${suffix}`;
20
22
  }
21
23
 
22
24
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
@@ -5,7 +5,11 @@ export async function processTextStream({
5
5
  stream: ReadableStream<Uint8Array>;
6
6
  onTextPart: (chunk: string) => Promise<void> | void;
7
7
  }): Promise<void> {
8
- const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
8
+ const reader = stream
9
+ .pipeThrough(
10
+ new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>,
11
+ )
12
+ .getReader();
9
13
  while (true) {
10
14
  const { done, value } = await reader.read();
11
15
  if (done) {
@@ -17,7 +17,9 @@ export class TextStreamChatTransport<
17
17
  stream: ReadableStream<Uint8Array<ArrayBufferLike>>,
18
18
  ): ReadableStream<UIMessageChunk> {
19
19
  return transformTextToUiMessageStream({
20
- stream: stream.pipeThrough(new TextDecoderStream()),
20
+ stream: stream.pipeThrough(
21
+ new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>,
22
+ ),
21
23
  });
22
24
  }
23
25
  }
@@ -421,6 +421,14 @@ export type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{
421
421
  export type DynamicToolUIPart = {
422
422
  type: 'dynamic-tool';
423
423
 
424
+ /**
425
+ * `false` when a static tool part was normalized to a dynamic tool part
426
+ * because its current tool schema was unavailable or incompatible.
427
+ *
428
+ * This preserves its static origin across persistence boundaries.
429
+ */
430
+ dynamic?: false;
431
+
424
432
  /**
425
433
  * Name of the tool that is being called.
426
434
  */
@@ -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
+ }