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.
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +58 -5
- package/dist/index.js +168 -79
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +43 -31
- package/dist/internal/index.js.map +1 -1
- package/docs/02-foundations/02-providers-and-models.mdx +57 -52
- package/docs/02-getting-started/00-choosing-a-provider.mdx +1 -16
- package/docs/02-getting-started/01-navigating-the-library.mdx +12 -12
- package/docs/02-getting-started/02-nextjs-app-router.mdx +2 -36
- package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -36
- package/docs/02-getting-started/04-svelte.mdx +2 -34
- package/docs/02-getting-started/05-nuxt.mdx +2 -36
- package/docs/02-getting-started/06-nodejs.mdx +1 -18
- package/docs/02-getting-started/07-expo.mdx +3 -62
- package/docs/02-getting-started/08-tanstack-start.mdx +2 -36
- package/docs/02-getting-started/09-coding-agents.mdx +2 -32
- package/docs/03-agents/07-workflow-agent.mdx +1 -1
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +3 -12
- package/docs/03-ai-sdk-core/16-mcp-tools.mdx +44 -0
- package/docs/03-ai-sdk-core/30-embeddings.mdx +18 -18
- package/docs/03-ai-sdk-core/38-video-generation.mdx +22 -3
- package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +1 -28
- package/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +7 -7
- package/docs/03-ai-sdk-harnesses/06-workflow-utilities.mdx +1 -16
- package/docs/03-ai-sdk-harnesses/08-terminal-ui.mdx +1 -28
- package/docs/04-ai-sdk-ui/01-overview.mdx +5 -5
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/13-generate-video.mdx +3 -2
- package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +1 -1
- package/docs/07-reference/02-ai-sdk-ui/index.mdx +5 -5
- package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +2 -2
- package/package.json +4 -4
- package/src/agent/tool-loop-agent-settings.ts +7 -0
- package/src/generate-text/collect-tool-approvals.ts +17 -4
- package/src/generate-text/execute-tool-call.ts +3 -2
- package/src/generate-text/execute-tools-from-stream.ts +2 -1
- package/src/generate-text/generate-text.ts +14 -5
- package/src/generate-text/index.ts +1 -0
- package/src/generate-text/invoke-tool-callbacks-from-stream.ts +6 -4
- package/src/generate-text/parse-tool-call.ts +5 -4
- package/src/generate-text/resolve-tool-approval.ts +9 -6
- package/src/generate-text/stream-language-model-call.ts +2 -1
- package/src/generate-text/stream-text.ts +12 -3
- package/src/generate-text/to-response-messages.ts +4 -3
- package/src/generate-text/tool-approval-signature.ts +4 -40
- package/src/generate-text/tool-fingerprint.ts +76 -0
- package/src/generate-text/validate-tool-approvals.ts +6 -1
- package/src/generate-video/generate-video.ts +96 -21
- package/src/ui/convert-to-model-messages.ts +3 -2
- package/src/ui/process-ui-message-stream.ts +3 -0
- package/src/ui/validate-ui-messages.ts +2 -1
- package/src/util/canonical-hash.ts +44 -0
- package/src/util/get-own.ts +18 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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<
|
|
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?.
|
|
205
|
-
|
|
206
|
-
|
|
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?.
|
|
212
|
-
|
|
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
|
-
|
|
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 ??
|
|
452
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|