ai 7.0.18 → 7.0.19

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 (48) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.ts +38 -3
  3. package/dist/index.js +160 -77
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +43 -31
  6. package/dist/internal/index.js.map +1 -1
  7. package/docs/02-foundations/02-providers-and-models.mdx +57 -52
  8. package/docs/02-getting-started/00-choosing-a-provider.mdx +1 -16
  9. package/docs/02-getting-started/01-navigating-the-library.mdx +12 -12
  10. package/docs/02-getting-started/02-nextjs-app-router.mdx +2 -36
  11. package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -36
  12. package/docs/02-getting-started/04-svelte.mdx +2 -34
  13. package/docs/02-getting-started/05-nuxt.mdx +2 -36
  14. package/docs/02-getting-started/06-nodejs.mdx +1 -18
  15. package/docs/02-getting-started/07-expo.mdx +3 -62
  16. package/docs/02-getting-started/08-tanstack-start.mdx +2 -36
  17. package/docs/02-getting-started/09-coding-agents.mdx +2 -32
  18. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +44 -0
  19. package/docs/03-ai-sdk-core/30-embeddings.mdx +18 -18
  20. package/docs/03-ai-sdk-core/38-video-generation.mdx +22 -3
  21. package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +1 -28
  22. package/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +7 -7
  23. package/docs/03-ai-sdk-harnesses/06-workflow-utilities.mdx +1 -16
  24. package/docs/03-ai-sdk-harnesses/08-terminal-ui.mdx +1 -28
  25. package/docs/04-ai-sdk-ui/01-overview.mdx +5 -5
  26. package/docs/07-reference/01-ai-sdk-core/13-generate-video.mdx +3 -2
  27. package/docs/07-reference/02-ai-sdk-ui/index.mdx +5 -5
  28. package/package.json +4 -4
  29. package/src/generate-text/collect-tool-approvals.ts +17 -4
  30. package/src/generate-text/execute-tool-call.ts +3 -2
  31. package/src/generate-text/execute-tools-from-stream.ts +2 -1
  32. package/src/generate-text/generate-text.ts +5 -4
  33. package/src/generate-text/index.ts +1 -0
  34. package/src/generate-text/invoke-tool-callbacks-from-stream.ts +6 -4
  35. package/src/generate-text/parse-tool-call.ts +5 -4
  36. package/src/generate-text/resolve-tool-approval.ts +9 -6
  37. package/src/generate-text/stream-language-model-call.ts +2 -1
  38. package/src/generate-text/stream-text.ts +3 -2
  39. package/src/generate-text/to-response-messages.ts +4 -3
  40. package/src/generate-text/tool-approval-signature.ts +4 -40
  41. package/src/generate-text/tool-fingerprint.ts +76 -0
  42. package/src/generate-text/validate-tool-approvals.ts +6 -1
  43. package/src/generate-video/generate-video.ts +96 -21
  44. package/src/ui/convert-to-model-messages.ts +3 -2
  45. package/src/ui/process-ui-message-stream.ts +3 -0
  46. package/src/ui/validate-ui-messages.ts +2 -1
  47. package/src/util/canonical-hash.ts +44 -0
  48. package/src/util/get-own.ts +18 -0
@@ -9,6 +9,7 @@ import type {
9
9
  ToolApprovalConfiguration,
10
10
  ToolApprovalStatus,
11
11
  } from './tool-approval-configuration';
12
+ import { getOwn } from '../util/get-own';
12
13
  import type { TypedToolCall } from './tool-call';
13
14
  import { validateToolContext } from './validate-tool-context';
14
15
 
@@ -75,13 +76,17 @@ export async function resolveToolApproval<
75
76
  }
76
77
 
77
78
  const toolName = toolCall.toolName;
78
- const tool = tools?.[toolName];
79
+ // `toolName` can be client-controlled, so look tools/approvals up by own
80
+ // property only: a name that matches an inherited object property (e.g.
81
+ // `constructor`, `toString`) resolves to "no such tool"/"unconfigured"
82
+ // instead of a value on `Object.prototype`.
83
+ const tool = getOwn(tools, toolName);
79
84
 
80
85
  // assume that the input has been validated and matches the tool's input schema
81
86
  const input = toolCall.input as InferToolInput<TOOLS[keyof TOOLS]>;
82
87
 
83
88
  // user-defined per-tool approval
84
- const userDefinedToolApprovalStatus = toolApproval?.[toolName];
89
+ const userDefinedToolApprovalStatus = getOwn(toolApproval, toolName);
85
90
  if (userDefinedToolApprovalStatus != null) {
86
91
  const approvalStatus: ToolApprovalStatus | undefined =
87
92
  typeof userDefinedToolApprovalStatus === 'function'
@@ -90,8 +95,7 @@ export async function resolveToolApproval<
90
95
  messages,
91
96
  toolContext: await validateToolContext({
92
97
  toolName,
93
- context:
94
- toolsContext?.[toolName as keyof InferToolSetContext<TOOLS>],
98
+ context: getOwn(toolsContext, toolName),
95
99
  contextSchema: tool?.contextSchema,
96
100
  }),
97
101
  runtimeContext,
@@ -113,8 +117,7 @@ export async function resolveToolApproval<
113
117
  messages,
114
118
  context: await validateToolContext({
115
119
  toolName,
116
- context:
117
- toolsContext?.[toolName as keyof InferToolSetContext<TOOLS>],
120
+ context: getOwn(toolsContext, toolName),
118
121
  contextSchema: tool?.contextSchema,
119
122
  }),
120
123
  })
@@ -16,6 +16,7 @@ import {
16
16
  } from '@ai-sdk/provider-utils';
17
17
  import { ToolCallNotFoundForApprovalError } from '../error/tool-call-not-found-for-approval-error';
18
18
  import { resolveLanguageModel } from '../model/resolve-model';
19
+ import { getOwn } from '../util/get-own';
19
20
  import type { Instructions, Prompt } from '../prompt';
20
21
  import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
21
22
  import type { LanguageModelCallOptions } from '../prompt/language-model-call-options';
@@ -715,7 +716,7 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
715
716
  }
716
717
 
717
718
  case 'tool-input-start': {
718
- const tool = tools?.[chunk.toolName];
719
+ const tool = getOwn(tools, chunk.toolName);
719
720
 
720
721
  controller.enqueue({
721
722
  ...chunk,
@@ -74,6 +74,7 @@ import { consumeStream } from '../util/consume-stream';
74
74
  import { createIdMap } from '../util/create-id-map';
75
75
  import { createStitchableStream } from '../util/create-stitchable-stream';
76
76
  import type { DownloadFunction } from '../util/download/download-function';
77
+ import { getOwn } from '../util/get-own';
77
78
  import { mergeAbortSignals } from '../util/merge-abort-signals';
78
79
  import { mergeObjects } from '../util/merge-objects';
79
80
  import { notify } from '../util/notify';
@@ -1694,7 +1695,7 @@ class DefaultStreamTextResult<
1694
1695
  output: await createToolModelOutput({
1695
1696
  toolCallId: output.toolCallId,
1696
1697
  input: output.input,
1697
- tool: tools?.[output.toolName],
1698
+ tool: getOwn(tools, output.toolName),
1698
1699
  output:
1699
1700
  output.type === 'tool-result'
1700
1701
  ? output.output
@@ -2240,7 +2241,7 @@ class DefaultStreamTextResult<
2240
2241
  // the client tool's result is sent back.
2241
2242
  for (const toolCall of stepToolCalls) {
2242
2243
  if (toolCall.providerExecuted !== true) continue;
2243
- const tool = tools?.[toolCall.toolName];
2244
+ const tool = getOwn(tools, toolCall.toolName);
2244
2245
  if (
2245
2246
  tool?.type === 'provider' &&
2246
2247
  tool.supportsDeferredResults
@@ -5,6 +5,7 @@ import type {
5
5
  ToolModelMessage,
6
6
  } from '../prompt';
7
7
  import { createToolModelOutput } from '../prompt/create-tool-model-output';
8
+ import { getOwn } from '../util/get-own';
8
9
  import type { ContentPart } from './content-part';
9
10
  import type { ToolSet } from '@ai-sdk/provider-utils';
10
11
 
@@ -97,7 +98,7 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
97
98
  const output = await createToolModelOutput({
98
99
  toolCallId: part.toolCallId,
99
100
  input: part.input,
100
- tool: tools?.[part.toolName],
101
+ tool: getOwn(tools, part.toolName),
101
102
  output: part.output,
102
103
  errorMode: 'none',
103
104
  });
@@ -114,7 +115,7 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
114
115
  const output = await createToolModelOutput({
115
116
  toolCallId: part.toolCallId,
116
117
  input: part.input,
117
- tool: tools?.[part.toolName],
118
+ tool: getOwn(tools, part.toolName),
118
119
  output: part.error,
119
120
  errorMode: 'json',
120
121
  });
@@ -189,7 +190,7 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
189
190
  const output = await createToolModelOutput({
190
191
  toolCallId: part.toolCallId,
191
192
  input: part.input,
192
- tool: tools?.[part.toolName],
193
+ tool: getOwn(tools, part.toolName),
193
194
  output: part.type === 'tool-result' ? part.output : part.error,
194
195
  errorMode: part.type === 'tool-error' ? 'text' : 'none',
195
196
  });
@@ -1,35 +1,8 @@
1
- import {
2
- convertBase64ToUint8Array,
3
- convertUint8ArrayToBase64,
4
- } from '@ai-sdk/provider-utils';
1
+ import { convertBase64ToUint8Array } from '@ai-sdk/provider-utils';
2
+ import { hashCanonical, toBase64url } from '../util/canonical-hash';
5
3
 
6
4
  const encoder = new TextEncoder();
7
5
 
8
- function canonicalJSON(value: unknown): string {
9
- if (value === null || value === undefined) {
10
- return JSON.stringify(value);
11
- }
12
- if (typeof value !== 'object') {
13
- return JSON.stringify(value);
14
- }
15
- if (Array.isArray(value)) {
16
- return `[${value.map(canonicalJSON).join(',')}]`;
17
- }
18
- const keys = Object.keys(value as Record<string, unknown>).sort();
19
- const entries = keys.map(
20
- k =>
21
- `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`,
22
- );
23
- return `{${entries.join(',')}}`;
24
- }
25
-
26
- function toBase64url(bytes: Uint8Array): string {
27
- return convertUint8ArrayToBase64(bytes)
28
- .replace(/\+/g, '-')
29
- .replace(/\//g, '_')
30
- .replace(/=+$/g, '');
31
- }
32
-
33
6
  function fromBase64url(str: string): Uint8Array {
34
7
  return convertBase64ToUint8Array(str);
35
8
  }
@@ -45,15 +18,6 @@ async function importKey(secret: string | Uint8Array): Promise<CryptoKey> {
45
18
  );
46
19
  }
47
20
 
48
- async function hashInput(input: unknown): Promise<string> {
49
- const canonical = canonicalJSON(input);
50
- const digest = await crypto.subtle.digest(
51
- 'SHA-256',
52
- encoder.encode(canonical),
53
- );
54
- return toBase64url(new Uint8Array(digest));
55
- }
56
-
57
21
  function buildPayload(
58
22
  approvalId: string,
59
23
  toolCallId: string,
@@ -79,7 +43,7 @@ export async function signToolApproval({
79
43
  input: unknown;
80
44
  }): Promise<string> {
81
45
  const key = await importKey(secret);
82
- const inputDigest = await hashInput(input);
46
+ const inputDigest = await hashCanonical(input);
83
47
  const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
84
48
  const sig = await crypto.subtle.sign('HMAC', key, payload);
85
49
  return toBase64url(new Uint8Array(sig));
@@ -101,7 +65,7 @@ export async function verifyToolApprovalSignature({
101
65
  input: unknown;
102
66
  }): Promise<boolean> {
103
67
  const key = await importKey(secret);
104
- const inputDigest = await hashInput(input);
68
+ const inputDigest = await hashCanonical(input);
105
69
  const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
106
70
  const sigBytes = fromBase64url(signature);
107
71
  return crypto.subtle.verify('HMAC', key, sigBytes, payload);
@@ -0,0 +1,76 @@
1
+ import { asSchema, type ToolSet } from '@ai-sdk/provider-utils';
2
+ import { hashCanonical } from '../util/canonical-hash';
3
+
4
+ /**
5
+ * Tag a tool description for hashing. A function description is developer-owned
6
+ * (evaluated per call from local context), not a server-controlled "rug pull"
7
+ * vector, so only its presence is pinned, not its identity. The tagged shape
8
+ * keeps a literal string equal to some placeholder from ever hashing like a
9
+ * function.
10
+ */
11
+ function tagDescription(description: unknown) {
12
+ if (typeof description === 'string') {
13
+ return { type: 'string', value: description } as const;
14
+ }
15
+ if (description == null) {
16
+ return { type: 'none' } as const;
17
+ }
18
+ return { type: 'function' } as const;
19
+ }
20
+
21
+ /**
22
+ * Fingerprint the server-controlled, security-relevant fields of each tool in a
23
+ * `ToolSet`: `description` (string form only), the resolved input JSON schema,
24
+ * and `title`. Returns a map of tool name to a stable digest.
25
+ *
26
+ * Capture a baseline at trust time (first connect, human-reviewed) and compare
27
+ * later fetches with {@link detectToolDrift} to catch MCP tool-definition drift
28
+ * ("rug pull"). Baseline storage and the drift response are the app's concern.
29
+ */
30
+ export async function fingerprintTools(
31
+ tools: ToolSet,
32
+ ): Promise<Record<string, string>> {
33
+ const entries = await Promise.all(
34
+ Object.keys(tools).map(async name => {
35
+ const tool = tools[name];
36
+ const digest = await hashCanonical({
37
+ description: tagDescription(tool.description),
38
+ inputSchema: await asSchema(tool.inputSchema).jsonSchema,
39
+ title: tool.title,
40
+ });
41
+ return [name, digest] as const;
42
+ }),
43
+ );
44
+ return Object.fromEntries(entries);
45
+ }
46
+
47
+ /**
48
+ * Pure diff of two fingerprint maps produced by {@link fingerprintTools}.
49
+ * `added`/`removed` are tools present in only one map; `changed` are tools whose
50
+ * pinned definition differs. Uses own-property lookups so a tool literally named
51
+ * `constructor` or `toString` diffs correctly.
52
+ */
53
+ export function detectToolDrift(
54
+ current: Record<string, string>,
55
+ baseline: Record<string, string>,
56
+ ): { added: string[]; removed: string[]; changed: string[] } {
57
+ const added: string[] = [];
58
+ const removed: string[] = [];
59
+ const changed: string[] = [];
60
+
61
+ for (const name of Object.keys(current)) {
62
+ if (!Object.hasOwn(baseline, name)) {
63
+ added.push(name);
64
+ } else if (current[name] !== baseline[name]) {
65
+ changed.push(name);
66
+ }
67
+ }
68
+
69
+ for (const name of Object.keys(baseline)) {
70
+ if (!Object.hasOwn(current, name)) {
71
+ removed.push(name);
72
+ }
73
+ }
74
+
75
+ return { added, removed, changed };
76
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from '@ai-sdk/provider-utils';
10
10
  import { InvalidToolApprovalSignatureError } from '../error/invalid-tool-approval-signature-error';
11
11
  import { InvalidToolInputError } from '../error/invalid-tool-input-error';
12
+ import { getOwn } from '../util/get-own';
12
13
  import type { CollectedToolApprovals } from './collect-tool-approvals';
13
14
  import { resolveToolApproval } from './resolve-tool-approval';
14
15
  import { verifyToolApprovalSignature } from './tool-approval-signature';
@@ -47,7 +48,11 @@ export async function validateApprovedToolApprovals<
47
48
 
48
49
  for (const approval of approvedToolApprovals) {
49
50
  const { toolCall, approvalRequest } = approval;
50
- const tool = tools?.[toolCall.toolName];
51
+ // Look up the tool by own property only: `toolName` comes from
52
+ // client-supplied history, so a name matching an inherited object property
53
+ // (e.g. `constructor`, `toString`) must resolve to "no such tool" rather
54
+ // than a prototype value that would silently skip input validation below.
55
+ const tool = getOwn(tools, toolCall.toolName);
51
56
 
52
57
  if (toolApprovalSecret != null) {
53
58
  if (approvalRequest.signature == null) {
@@ -28,7 +28,6 @@ import { prepareRetries } from '../util/prepare-retries';
28
28
  import { VERSION } from '../version';
29
29
  import type { GenerateVideoResult } from './generate-video-result';
30
30
  import { splitDataUrl } from '../prompt/split-data-url';
31
- import { convertDataContentToUint8Array } from '../prompt/data-content';
32
31
 
33
32
  export type GenerateVideoPrompt =
34
33
  | string
@@ -49,7 +48,7 @@ export type GenerateVideoPrompt =
49
48
  * @param fps - Frames per second for the video.
50
49
  * @param seed - Seed for the video generation.
51
50
  * @param frameImages - Role-tagged image inputs for image-to-video and first-last-frame generation.
52
- * @param inputReferences - Reference image inputs for reference-to-video generation.
51
+ * @param inputReferences - Reference image or video inputs for reference-to-video generation.
53
52
  * @param generateAudio - Whether the model should generate audio alongside the video.
54
53
  * @param providerOptions - Additional provider-specific options that are passed through to the provider
55
54
  * as body parameters.
@@ -141,9 +140,26 @@ export async function experimental_generateVideo({
141
140
  }>;
142
141
 
143
142
  /**
144
- * Reference image inputs for reference-to-video generation.
143
+ * Reference inputs for reference-to-video generation.
144
+ *
145
+ * Each entry may be a plain image/video ({@link DataContent}), or an object
146
+ * form that carries an explicit `mediaType`.
145
147
  */
146
- inputReferences?: Array<DataContent>;
148
+ inputReferences?: Array<
149
+ | DataContent
150
+ | {
151
+ /**
152
+ * The reference image or video.
153
+ */
154
+ data: DataContent;
155
+
156
+ /**
157
+ * The media type of the reference (e.g. 'image/png',
158
+ * 'video/mp4').
159
+ */
160
+ mediaType?: string;
161
+ }
162
+ >;
147
163
 
148
164
  /**
149
165
  * Whether the model should generate audio alongside the video.
@@ -201,16 +217,19 @@ export async function experimental_generateVideo({
201
217
 
202
218
  const normalizedFrameImages:
203
219
  | Array<Experimental_VideoModelV4FrameImage>
204
- | undefined = frameImages?.map(frame => ({
205
- image: normalizeImageData(frame.image),
206
- frameType: frame.frameType,
207
- }));
220
+ | undefined = frameImages?.flatMap(frame => {
221
+ const normalizedImage = normalizeImageData(frame.image);
222
+ return normalizedImage != null
223
+ ? [{ image: normalizedImage, frameType: frame.frameType }]
224
+ : [];
225
+ });
208
226
 
209
227
  const normalizedInputReferences:
210
228
  | Array<Experimental_VideoModelV4File>
211
- | undefined = inputReferences?.map(reference =>
212
- normalizeImageData(reference),
213
- );
229
+ | undefined = inputReferences?.flatMap(reference => {
230
+ const normalized = normalizeReferenceData(reference);
231
+ return normalized != null ? [normalized] : [];
232
+ });
214
233
 
215
234
  const effectiveInputReferences =
216
235
  normalizedFrameImages != null && normalizedFrameImages.length > 0
@@ -426,13 +445,24 @@ function normalizePrompt(promptArg: GenerateVideoPrompt): {
426
445
  };
427
446
  }
428
447
 
448
+ function detectFileMediaType(
449
+ data: Uint8Array,
450
+ restrictToImages: boolean,
451
+ ): string {
452
+ const detected = restrictToImages
453
+ ? detectMediaType({ data, topLevelType: 'image' })
454
+ : detectMediaType({ data });
455
+ return detected ?? 'image/png';
456
+ }
457
+
429
458
  /**
430
459
  * Normalizes a {@link DataContent} image into a {@link Experimental_VideoModelV4File}.
431
460
  * Accepts a URL string, a data URL, a base64 string, or binary image data.
432
461
  */
433
462
  function normalizeImageData(
434
463
  dataContent: DataContent,
435
- ): Experimental_VideoModelV4File {
464
+ { restrictToImages = true }: { restrictToImages?: boolean } = {},
465
+ ): Experimental_VideoModelV4File | undefined {
436
466
  if (typeof dataContent === 'string') {
437
467
  if (
438
468
  dataContent.startsWith('http://') ||
@@ -446,28 +476,73 @@ function normalizeImageData(
446
476
 
447
477
  if (dataContent.startsWith('data:')) {
448
478
  const { mediaType, base64Content } = splitDataUrl(dataContent);
479
+ const data = convertBase64ToUint8Array(base64Content ?? '');
449
480
  return {
450
481
  type: 'file',
451
- mediaType: mediaType ?? 'image/png',
452
- data: convertBase64ToUint8Array(base64Content ?? ''),
482
+ mediaType: mediaType ?? detectFileMediaType(data, restrictToImages),
483
+ data,
453
484
  };
454
485
  }
455
486
 
456
487
  const bytes = convertBase64ToUint8Array(dataContent);
457
488
  return {
458
489
  type: 'file',
459
- mediaType:
460
- detectMediaType({ data: bytes, topLevelType: 'image' }) ?? 'image/png',
490
+ mediaType: detectFileMediaType(bytes, restrictToImages),
461
491
  data: bytes,
462
492
  };
463
493
  }
464
494
 
465
- const bytes = convertDataContentToUint8Array(dataContent);
495
+ if (dataContent instanceof Uint8Array || dataContent instanceof ArrayBuffer) {
496
+ const bytes =
497
+ dataContent instanceof Uint8Array
498
+ ? dataContent
499
+ : new Uint8Array(dataContent);
500
+ return {
501
+ type: 'file',
502
+ mediaType: detectFileMediaType(bytes, restrictToImages),
503
+ data: bytes,
504
+ };
505
+ }
506
+
507
+ return undefined;
508
+ }
509
+
510
+ /**
511
+ * Normalizes a reference input into a {@link Experimental_VideoModelV4File},
512
+ * accepting either a plain {@link DataContent} or the object form that carries
513
+ * an explicit `mediaType`.
514
+ */
515
+ function normalizeReferenceData(
516
+ reference:
517
+ | DataContent
518
+ | {
519
+ data: DataContent;
520
+ mediaType?: string;
521
+ },
522
+ ): Experimental_VideoModelV4File | undefined {
523
+ const isObjectForm =
524
+ typeof reference === 'object' &&
525
+ reference != null &&
526
+ !(reference instanceof Uint8Array) &&
527
+ !(reference instanceof ArrayBuffer) &&
528
+ 'data' in reference;
529
+
530
+ if (!isObjectForm) {
531
+ return normalizeImageData(reference as DataContent, {
532
+ restrictToImages: false,
533
+ });
534
+ }
535
+
536
+ const normalized = normalizeImageData(reference.data, {
537
+ restrictToImages: false,
538
+ });
539
+ if (normalized == null) {
540
+ return normalized;
541
+ }
542
+
466
543
  return {
467
- type: 'file',
468
- mediaType:
469
- detectMediaType({ data: bytes, topLevelType: 'image' }) ?? 'image/png',
470
- data: bytes,
544
+ ...normalized,
545
+ ...(reference.mediaType != null ? { mediaType: reference.mediaType } : {}),
471
546
  };
472
547
  }
473
548
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '@ai-sdk/provider-utils';
12
12
  import { createToolModelOutput } from '../prompt/create-tool-model-output';
13
13
  import { MessageConversionError } from '../prompt/message-conversion-error';
14
+ import { getOwn } from '../util/get-own';
14
15
  import {
15
16
  getToolName,
16
17
  isCustomContentUIPart,
@@ -256,7 +257,7 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
256
257
  part.state === 'output-error'
257
258
  ? part.errorText
258
259
  : part.output,
259
- tool: options?.tools?.[toolName],
260
+ tool: getOwn(options?.tools, toolName),
260
261
  errorMode:
261
262
  part.state === 'output-error' ? 'json' : 'none',
262
263
  }),
@@ -375,7 +376,7 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
375
376
  toolPart.state === 'output-error'
376
377
  ? toolPart.errorText
377
378
  : toolPart.output,
378
- tool: options?.tools?.[toolName],
379
+ tool: getOwn(options?.tools, toolName),
379
380
  errorMode:
380
381
  toolPart.state === 'output-error' ? 'text' : 'none',
381
382
  }),
@@ -731,6 +731,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
731
731
  approved: chunk.approved,
732
732
  ...(chunk.reason != null ? { reason: chunk.reason } : {}),
733
733
  ...(approval.isAutomatic === true ? { isAutomatic: true } : {}),
734
+ ...(approval.signature != null
735
+ ? { signature: approval.signature }
736
+ : {}),
734
737
  };
735
738
  if (chunk.providerExecuted != null) {
736
739
  toolInvocation.providerExecuted = chunk.providerExecuted;
@@ -9,6 +9,7 @@ import {
9
9
  import { z } from 'zod/v4';
10
10
  import { InvalidArgumentError } from '../error';
11
11
  import { jsonValueSchema } from '../types/json-value';
12
+ import { getOwn } from '../util/get-own';
12
13
  import { providerMetadataSchema } from '../types/provider-metadata';
13
14
  import type {
14
15
  DataUIPart,
@@ -456,7 +457,7 @@ export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({
456
457
  InferUIMessageTools<UI_MESSAGE>
457
458
  >;
458
459
  const toolName = toolPart.type.slice(5);
459
- const tool = tools[toolName];
460
+ const tool = getOwn(tools, toolName);
460
461
 
461
462
  if (
462
463
  !tool &&
@@ -0,0 +1,44 @@
1
+ import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';
2
+
3
+ const encoder = new TextEncoder();
4
+
5
+ /**
6
+ * Deterministic JSON serialization: object keys are sorted so that two
7
+ * structurally-equal values always produce the same string regardless of key
8
+ * insertion order. Used as the input to content hashing.
9
+ */
10
+ export function canonicalJSON(value: unknown): string {
11
+ if (value === null || value === undefined) {
12
+ return JSON.stringify(value);
13
+ }
14
+ if (typeof value !== 'object') {
15
+ return JSON.stringify(value);
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return `[${value.map(canonicalJSON).join(',')}]`;
19
+ }
20
+ const keys = Object.keys(value as Record<string, unknown>).sort();
21
+ const entries = keys.map(
22
+ k =>
23
+ `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`,
24
+ );
25
+ return `{${entries.join(',')}}`;
26
+ }
27
+
28
+ export function toBase64url(bytes: Uint8Array): string {
29
+ return convertUint8ArrayToBase64(bytes)
30
+ .replace(/\+/g, '-')
31
+ .replace(/\//g, '_')
32
+ .replace(/=+$/g, '');
33
+ }
34
+
35
+ /**
36
+ * Canonical SHA-256 digest (base64url) of an arbitrary JSON-serializable value.
37
+ */
38
+ export async function hashCanonical(value: unknown): Promise<string> {
39
+ const digest = await crypto.subtle.digest(
40
+ 'SHA-256',
41
+ encoder.encode(canonicalJSON(value)),
42
+ );
43
+ return toBase64url(new Uint8Array(digest));
44
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Reads a property by an untrusted key, ignoring inherited prototype members.
3
+ *
4
+ * Tool sets, tool contexts, and similar lookup objects are indexed by names
5
+ * that can come from model output or client-supplied message history. Plain
6
+ * bracket access (`obj[name]`) resolves names such as `constructor`,
7
+ * `toString`, or `__proto__` to values on `Object.prototype`, which would slip
8
+ * past the `== null` / `!value` guards that treat an unknown name as "not
9
+ * present". This helper returns `undefined` unless `key` is an own property.
10
+ */
11
+ export function getOwn<T extends object>(
12
+ obj: T | undefined | null,
13
+ key: string,
14
+ ): T[keyof T] | undefined {
15
+ return obj != null && Object.hasOwn(obj, key)
16
+ ? obj[key as keyof T]
17
+ : undefined;
18
+ }