ai 7.0.18 → 7.0.20

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 (56) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.d.ts +58 -5
  3. package/dist/index.js +168 -79
  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-agents/07-workflow-agent.mdx +1 -1
  19. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +3 -12
  20. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +44 -0
  21. package/docs/03-ai-sdk-core/30-embeddings.mdx +18 -18
  22. package/docs/03-ai-sdk-core/38-video-generation.mdx +22 -3
  23. package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +1 -28
  24. package/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +7 -7
  25. package/docs/03-ai-sdk-harnesses/06-workflow-utilities.mdx +1 -16
  26. package/docs/03-ai-sdk-harnesses/08-terminal-ui.mdx +1 -28
  27. package/docs/04-ai-sdk-ui/01-overview.mdx +5 -5
  28. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +1 -1
  29. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
  30. package/docs/07-reference/01-ai-sdk-core/13-generate-video.mdx +3 -2
  31. package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +1 -1
  32. package/docs/07-reference/02-ai-sdk-ui/index.mdx +5 -5
  33. package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +2 -2
  34. package/package.json +4 -4
  35. package/src/agent/tool-loop-agent-settings.ts +7 -0
  36. package/src/generate-text/collect-tool-approvals.ts +17 -4
  37. package/src/generate-text/execute-tool-call.ts +3 -2
  38. package/src/generate-text/execute-tools-from-stream.ts +2 -1
  39. package/src/generate-text/generate-text.ts +14 -5
  40. package/src/generate-text/index.ts +1 -0
  41. package/src/generate-text/invoke-tool-callbacks-from-stream.ts +6 -4
  42. package/src/generate-text/parse-tool-call.ts +5 -4
  43. package/src/generate-text/resolve-tool-approval.ts +9 -6
  44. package/src/generate-text/stream-language-model-call.ts +2 -1
  45. package/src/generate-text/stream-text.ts +12 -3
  46. package/src/generate-text/to-response-messages.ts +4 -3
  47. package/src/generate-text/tool-approval-signature.ts +4 -40
  48. package/src/generate-text/tool-fingerprint.ts +76 -0
  49. package/src/generate-text/validate-tool-approvals.ts +6 -1
  50. package/src/generate-video/generate-video.ts +96 -21
  51. package/src/ui/convert-to-model-messages.ts +3 -2
  52. package/src/ui/process-ui-message-stream.ts +3 -0
  53. package/src/ui/validate-ui-messages.ts +2 -1
  54. package/src/util/canonical-hash.ts +44 -0
  55. package/src/util/get-own.ts +18 -0
  56. package/src/util/write-to-server-response.ts +9 -0
@@ -50,6 +50,7 @@ import {
50
50
  type LanguageModelUsage,
51
51
  } from '../types/usage';
52
52
  import type { DownloadFunction } from '../util/download/download-function';
53
+ import { getOwn } from '../util/get-own';
53
54
  import { mergeAbortSignals } from '../util/merge-abort-signals';
54
55
  import { mergeObjects } from '../util/merge-objects';
55
56
  import { now as originalNow } from '../util/now';
@@ -247,7 +248,8 @@ export async function generateText<
247
248
  activeTools,
248
249
  toolOrder,
249
250
  prepareStep,
250
- experimental_repairToolCall: repairToolCall,
251
+ experimental_repairToolCall,
252
+ repairToolCall = experimental_repairToolCall,
251
253
  experimental_refineToolInput: refineToolInput,
252
254
  experimental_download: download,
253
255
  runtimeContext = {} as RUNTIME_CONTEXT,
@@ -377,6 +379,13 @@ export async function generateText<
377
379
  /**
378
380
  * A function that attempts to repair a tool call that failed to parse.
379
381
  */
382
+ repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>;
383
+
384
+ /**
385
+ * A function that attempts to repair a tool call that failed to parse.
386
+ *
387
+ * @deprecated Use `repairToolCall` instead.
388
+ */
380
389
  experimental_repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>;
381
390
 
382
391
  /**
@@ -717,7 +726,7 @@ export async function generateText<
717
726
  const modelOutput = await createToolModelOutput({
718
727
  toolCallId: output.toolCallId,
719
728
  input: output.input,
720
- tool: tools?.[output.toolName],
729
+ tool: getOwn(tools, output.toolName),
721
730
  output:
722
731
  output.type === 'tool-result' ? output.output : output.error,
723
732
  errorMode: output.type === 'tool-error' ? 'text' : 'none',
@@ -1040,7 +1049,7 @@ export async function generateText<
1040
1049
  continue; // ignore invalid tool calls
1041
1050
  }
1042
1051
 
1043
- const tool = tools?.[toolCall.toolName];
1052
+ const tool = getOwn(tools, toolCall.toolName);
1044
1053
 
1045
1054
  if (tool == null) {
1046
1055
  // ignore tool calls for tools that are not available,
@@ -1234,7 +1243,7 @@ export async function generateText<
1234
1243
  // the client tool's result is sent back.
1235
1244
  for (const toolCall of stepToolCalls) {
1236
1245
  if (!toolCall.providerExecuted) continue;
1237
- const tool = tools?.[toolCall.toolName];
1246
+ const tool = getOwn(tools, toolCall.toolName);
1238
1247
  if (tool?.type === 'provider' && tool.supportsDeferredResults) {
1239
1248
  // Check if this tool call already has a result in the current response
1240
1249
  const hasResultInResponse = currentModelResponse.content.some(
@@ -1699,7 +1708,7 @@ function asContent<TOOLS extends ToolSet>({
1699
1708
  // result may be deferred to a later turn. In this case, there's no matching tool-call
1700
1709
  // in the current response.
1701
1710
  if (toolCall == null) {
1702
- const tool = tools?.[part.toolName];
1711
+ const tool = getOwn(tools, part.toolName);
1703
1712
  const supportsDeferredResults =
1704
1713
  tool?.type === 'provider' && tool.supportsDeferredResults;
1705
1714
 
@@ -80,6 +80,7 @@ export type {
80
80
  ToolApprovalConfiguration,
81
81
  ToolApprovalStatus,
82
82
  } from './tool-approval-configuration';
83
+ export { detectToolDrift, fingerprintTools } from './tool-fingerprint';
83
84
  export type { ToolApprovalRequestOutput } from './tool-approval-request-output';
84
85
  export type { ToolApprovalResponseOutput } from './tool-approval-response-output';
85
86
  export type {
@@ -1,4 +1,6 @@
1
1
  import type { Context, ModelMessage, ToolSet } from '@ai-sdk/provider-utils';
2
+ import { createIdMap } from '../util/create-id-map';
3
+ import { getOwn } from '../util/get-own';
2
4
  import type { LanguageModelStreamPart } from './stream-language-model-call';
3
5
 
4
6
  export function invokeToolCallbacksFromStream<
@@ -19,7 +21,7 @@ export function invokeToolCallbacksFromStream<
19
21
  }): ReadableStream<LanguageModelStreamPart<TOOLS>> {
20
22
  if (tools == null) return stream;
21
23
 
22
- const ongoingToolCallToolNames: Record<string, string> = {};
24
+ const ongoingToolCallToolNames: Record<string, string> = createIdMap();
23
25
 
24
26
  return stream.pipeThrough(
25
27
  new TransformStream({
@@ -30,7 +32,7 @@ export function invokeToolCallbacksFromStream<
30
32
  case 'tool-input-start': {
31
33
  ongoingToolCallToolNames[chunk.id] = chunk.toolName;
32
34
 
33
- const tool = tools?.[chunk.toolName];
35
+ const tool = getOwn(tools, chunk.toolName);
34
36
  if (tool?.onInputStart != null) {
35
37
  await tool.onInputStart({
36
38
  toolCallId: chunk.id,
@@ -45,7 +47,7 @@ export function invokeToolCallbacksFromStream<
45
47
 
46
48
  case 'tool-input-delta': {
47
49
  const toolName = ongoingToolCallToolNames[chunk.id];
48
- const tool = tools?.[toolName];
50
+ const tool = getOwn(tools, toolName);
49
51
 
50
52
  if (tool?.onInputDelta != null) {
51
53
  await tool.onInputDelta({
@@ -62,7 +64,7 @@ export function invokeToolCallbacksFromStream<
62
64
 
63
65
  case 'tool-call': {
64
66
  const toolName = ongoingToolCallToolNames[chunk.toolCallId];
65
- const tool = tools?.[toolName];
67
+ const tool = getOwn(tools, toolName);
66
68
 
67
69
  delete ongoingToolCallToolNames[chunk.toolCallId];
68
70
 
@@ -11,6 +11,7 @@ import { InvalidToolInputError } from '../error/invalid-tool-input-error';
11
11
  import { NoSuchToolError } from '../error/no-such-tool-error';
12
12
  import { ToolCallRepairError } from '../error/tool-call-repair-error';
13
13
  import type { Instructions } from '../prompt';
14
+ import { getOwn } from '../util/get-own';
14
15
  import type { DynamicToolCall, TypedToolCall } from './tool-call';
15
16
  import type { ToolCallRepairFunction } from './tool-call-repair-function';
16
17
  import type { ToolInputRefinement } from './tool-input-refinement';
@@ -66,7 +67,7 @@ export async function parseToolCall<TOOLS extends ToolSet>({
66
67
  toolCall,
67
68
  tools,
68
69
  inputSchema: async ({ toolName }) => {
69
- const { inputSchema } = tools[toolName];
70
+ const inputSchema = getOwn(tools, toolName)?.inputSchema;
70
71
  return await asSchema(inputSchema).jsonSchema;
71
72
  },
72
73
  instructions,
@@ -95,7 +96,7 @@ export async function parseToolCall<TOOLS extends ToolSet>({
95
96
  // use parsed input when possible
96
97
  const parsedInput = await safeParseJSON({ text: toolCall.input });
97
98
  const input = parsedInput.success ? parsedInput.value : toolCall.input;
98
- const tool = tools?.[toolCall.toolName];
99
+ const tool = getOwn(tools, toolCall.toolName);
99
100
 
100
101
  // TODO AI SDK 6: special invalid tool call parts
101
102
  return {
@@ -121,7 +122,7 @@ async function refineParsedToolCallInput<TOOLS extends ToolSet>({
121
122
  toolCall: TypedToolCall<TOOLS>;
122
123
  refineToolInput: ToolInputRefinement<TOOLS> | undefined;
123
124
  }): Promise<TypedToolCall<TOOLS>> {
124
- const refine = refineToolInput?.[toolCall.toolName];
125
+ const refine = getOwn(refineToolInput, toolCall.toolName);
125
126
 
126
127
  if (refine == null) {
127
128
  return toolCall;
@@ -169,7 +170,7 @@ async function doParseToolCall<TOOLS extends ToolSet>({
169
170
  }): Promise<TypedToolCall<TOOLS>> {
170
171
  const toolName = toolCall.toolName as keyof TOOLS & string;
171
172
 
172
- const tool = tools[toolName];
173
+ const tool = getOwn(tools, toolName);
173
174
 
174
175
  if (tool == null) {
175
176
  // provider-executed dynamic tools are not part of our list of tools:
@@ -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';
@@ -355,7 +356,8 @@ export function streamText<
355
356
  providerOptions,
356
357
  activeTools,
357
358
  toolOrder,
358
- experimental_repairToolCall: repairToolCall,
359
+ experimental_repairToolCall,
360
+ repairToolCall = experimental_repairToolCall,
359
361
  experimental_refineToolInput: refineToolInput,
360
362
  experimental_transform: transform,
361
363
  experimental_download: download,
@@ -493,6 +495,13 @@ export function streamText<
493
495
  /**
494
496
  * A function that attempts to repair a tool call that failed to parse.
495
497
  */
498
+ repairToolCall?: ToolCallRepairFunction<TOOLS>;
499
+
500
+ /**
501
+ * A function that attempts to repair a tool call that failed to parse.
502
+ *
503
+ * @deprecated Use `repairToolCall` instead.
504
+ */
496
505
  experimental_repairToolCall?: ToolCallRepairFunction<TOOLS>;
497
506
 
498
507
  /**
@@ -1694,7 +1703,7 @@ class DefaultStreamTextResult<
1694
1703
  output: await createToolModelOutput({
1695
1704
  toolCallId: output.toolCallId,
1696
1705
  input: output.input,
1697
- tool: tools?.[output.toolName],
1706
+ tool: getOwn(tools, output.toolName),
1698
1707
  output:
1699
1708
  output.type === 'tool-result'
1700
1709
  ? output.output
@@ -2240,7 +2249,7 @@ class DefaultStreamTextResult<
2240
2249
  // the client tool's result is sent back.
2241
2250
  for (const toolCall of stepToolCalls) {
2242
2251
  if (toolCall.providerExecuted !== true) continue;
2243
- const tool = tools?.[toolCall.toolName];
2252
+ const tool = getOwn(tools, toolCall.toolName);
2244
2253
  if (
2245
2254
  tool?.type === 'provider' &&
2246
2255
  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