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 zod

Step 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();
}