Vercel AI SDK
Gate tool calls in the Vercel AI SDK (v5/v6) behind Cheqpoint approval — works with useChat, generateText, and streamText in any Next.js app.
Installation
bash
npm install @cheqpoint/sdk ai zodStep 1 — Wrap a tool with Cheqpoint approval
TypeScript
// lib/tools/processRefund.ts
import { tool } from "ai";
import { z } from "zod";
import { CheqpointClient, RejectedError, TimeoutError } from "@cheqpoint/sdk";
const cheqpoint = new CheqpointClient({ connectionKey: process.env.CHEQPOINT_CONNECTION_KEY! });
export const processRefund = tool({
description: "Process a customer refund — requires human approval",
inputSchema: z.object({
orderId: z.string(),
amount: z.number().positive(),
reason: z.string(),
}),
execute: async ({ orderId, amount, reason }) => {
try {
// Blocks until a reviewer decides; throws on decline or timeout
const approval = await cheqpoint.checkpoint({
action: "process_refund",
summary: `Refund $${amount} for order ${orderId}`,
details: { orderId, amount, reason },
riskScore: amount > 500 ? 0.8 : 0.5,
});
// Reviewer may have changed the amount — always prefer modifiedDetails
const payload = { orderId, amount, ...(approval.modifiedDetails ?? {}) } as { orderId: string; amount: number };
const refund = await stripe.refunds.create({
charge: await lookupCharge(payload.orderId),
amount: Math.round(payload.amount * 100),
});
return { success: true, refundId: refund.id, amount: payload.amount };
} catch (err) {
if (err instanceof RejectedError) {
return { success: false, declined: true, reason: err.responseNotes ?? "Declined by reviewer" };
}
if (err instanceof TimeoutError) {
return { success: false, declined: false, reason: "Approval timed out — the request is still awaiting review" };
}
throw err;
}
},
});Step 2 — Use in a Next.js API route
TypeScript
// app/api/chat/route.ts
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from "ai";
import { processRefund } from "@/lib/tools/processRefund";
import { deleteRecord } from "@/lib/tools/deleteRecord";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "openai/gpt-4o",
messages: convertToModelMessages(messages),
tools: { processRefund, deleteRecord },
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}Step 3 — Render tool state in useChat
TypeScript
// app/components/Chat.tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const { messages, sendMessage } = useChat();
const [input, setInput] = useState("");
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{m.parts.map((part, i) => {
if (part.type === "text") return <span key={i}>{part.text}</span>;
if (part.type === "tool-processRefund") {
return (
<div key={i} className="text-xs text-muted-foreground">
{(part.state === "input-streaming" || part.state === "input-available") &&
"⏳ Waiting for human approval…"}
{part.state === "output-available" && "✅ processRefund completed"}
{part.state === "output-error" && "⚠️ processRefund failed"}
</div>
);
}
return null;
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Ask the Assistant..." />
<button type="submit">Send</button>
</form>
</div>
);
}Non-streaming with generateText
TypeScript
import { generateText, stepCountIs } from "ai";
import { processRefund } from "@/lib/tools/processRefund";
const { text, steps } = await generateText({
model: "openai/gpt-4o",
prompt: "Process a $150 refund for order #8821",
tools: { processRefund },
stopWhen: stepCountIs(3),
});
// steps contains each tool call + its approval result
console.log("Final response:", text);Drop-in approval tool (no custom tool needed)
TypeScript
import { streamText, jsonSchema, stepCountIs, convertToModelMessages, type UIMessage } from "ai";
import { CheqpointClient, createCheqpointTool } from "@cheqpoint/sdk";
const cheqpoint = new CheqpointClient({ connectionKey: process.env.CHEQPOINT_CONNECTION_KEY! });
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "openai/gpt-4o",
messages: convertToModelMessages(messages),
tools: {
// The model decides when an action needs sign-off and routes it to your inbox
requestApproval: createCheqpointTool(cheqpoint, { jsonSchema }),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}