Vercel AI SDK tool approval: confirm a paid video call
In the AI SDK, set toolApproval to 'user-approval' on a tool that starts a paid video job. The call pauses until your app sends back a decision.

To make the Vercel AI SDK ask before a tool spends money, pass toolApproval to generateText, streamText, or ToolLoopAgent and return 'user-approval' for that tool. The call then returns a tool-approval-request instead of running the tool, and the tool runs only after you send back a tool-approval-response with approved: true.
The AI SDK facts come from its Tool Calling and MCP pages, read 2026-09-29; the page is written for AI SDK 7.x. The Sume facts come from Video Generation and Jobs and results. Sume has no connector for the AI SDK: the tool below is a plain HTTPS call from your server. The tool itself is explained in Vercel AI SDK: generate video with a Sume API tool call.
How do I require approval for one tool?
Give toolApproval a per-tool map. The AI SDK's docs say the older needsApproval property on tool() is deprecated, and that new code should move approval logic to toolApproval. The reason you set is emitted on the approval request so your UI can show it to the approver.
import { generateText, tool } from "ai";
import { z } from "zod";
const startVideo = tool({
description: "Start a Sume video job. Returns a job id.",
inputSchema: z.object({ prompt: z.string(), duration: z.number().optional() }),
execute: async (input, { toolCallId }) => {
const res = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `chat-video-${toolCallId}`,
},
body: JSON.stringify({ model: "sume/auto", ...input }),
});
if (!res.ok) throw new Error(`Sume returned ${res.status}`);
return res.json();
},
});
export const first = (model: Parameters<typeof generateText>[0]["model"]) =>
generateText({
model,
tools: { startVideo },
toolApproval: {
startVideo: { type: "user-approval", reason: "Starting a video spends balance" },
},
prompt: "Make a 5-second vertical clip of a desk lamp.",
});What happens between the request and the run?
Per the AI SDK docs, generateText and streamText do not pause: they complete and return the request, so manual approval takes two calls to the model.
- Call
generateTextorstreamTextwithtoolApproval; the model makes the tool call. - Read
result.contentfor parts of typetool-approval-request. Each carriesapprovalId,toolCall(with the tool name and input), andreason. - Ask your user. Then push a message with
role: 'tool'whose content is atool-approval-responsewith the sameapprovalId,approved: trueorfalse, and an optionalreason. - Call again with the updated messages. If approved, the tool runs; if denied, the model sees the denial.
- Add a system instruction such as "When a tool execution is not approved, do not retry it", which the docs suggest so the model does not ask again.
Which approval statuses can I return?
The per-tool value is a status string, an object with a type and reason, or a function that returns one of them (or undefined).
| Status | What the docs say happens |
|---|---|
'not-applicable' | The tool runs without an approval flow. This is the default. |
'approved' | Records an automatic approval by emitting approval request and response parts, then runs the tool. |
'denied' | Records an automatic denial, then surfaces a denied tool output. |
'user-approval' | Emits an approval request and waits for an explicit response. |
Can I ask only for long or expensive clips?
Yes. The docs show a per-tool async function that receives the tool input and returns 'user-approval' for some inputs and undefined for the rest; their example asks only when a payment amount is above 1000. For Sume, the same shape can ask when duration is set above a limit you choose. Sume reserves a video's cost on submit at provider list times 1.25, plus a 5.5% agent fee by default; how much does an AI video cost covers the math.
What about Sume's MCP tools in the AI SDK?
The AI SDK's MCP client exposes each tool's annotations on metadata.annotations and says the client does not turn them into an approval policy on its own. Its docs give a conservative policy: run a tool automatically only when readOnlyHint is true, and ask for approval otherwise.
In current code, Sume's read tools such as tools_list, jobs_status, and jobs_wait set readOnlyHint: true, while write and paid tools such as generate_video set readOnlyHint: false. That policy therefore runs status reads freely and asks before a paid submit. Sume's docs also say idempotency_key on hosted MCP is for transport and dedup, not human approval, so the approval still has to come from your app.
What does approval not cover?
Approval is one layer. These limits are worth keeping in mind before you rely on it.
- The AI SDK docs say
toolApprovalonly controls tools the SDK executes locally; provider-executed tools run provider-side without considering it. - A denied call still leaves the decision to the model: keep a system instruction that stops retries.
- Approval is not a spend cap. Sume's hosted MCP tools accept an optional
max_spend_usd, enforced only when you pass it; see MCP tools and gates. - Keep
SUME_API_KEYon the server that runsexecute; never put it in client code.
Sources
Related posts
More in Integrations
- WordPress cron not working: schedule AI video jobs reliably
WP-Cron runs only when someone loads a page. Trigger wp-cron.php from a system cron so scheduled AI video jobs start on time.
- WordPress webhook endpoint: receive Sume video callbacks
Register a REST route with register_rest_route, verify the signature on the raw body in PHP, and answer 2xx so Sume stops retrying.
- Workato HTTP connector: call an API with a key and JSON
Workato's HTTP connector calls any HTTP API: a Header auth connection holds the key, and Send request via HTTP POSTs a JSON body and maps the reply.
- wp_remote_post timeout: calling a video API from WordPress
wp_remote_post waits 5 seconds by default. Set timeout yourself, submit the video job, and let a callback deliver the result instead of waiting.
Written by Sume