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.
- package/CHANGELOG.md +21 -0
- package/dist/index.d.ts +38 -3
- package/dist/index.js +160 -77
- 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-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/13-generate-video.mdx +3 -2
- package/docs/07-reference/02-ai-sdk-ui/index.mdx +5 -5
- package/package.json +4 -4
- 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 +5 -4
- 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 +3 -2
- 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
|
@@ -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';
|
|
@@ -1694,7 +1695,7 @@ class DefaultStreamTextResult<
|
|
|
1694
1695
|
output: await createToolModelOutput({
|
|
1695
1696
|
toolCallId: output.toolCallId,
|
|
1696
1697
|
input: output.input,
|
|
1697
|
-
tool: tools
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
}
|