Vercel AI SDK

Gate tool calls in the Vercel AI SDK behind Cheqpoint approval — works with useChat, generateText, and streamText in any Next.js app.

Installation

bash
npm install @cheqpoint/sdk ai

Step 1 — Wrap a tool with Cheqpoint approval

TypeScript
// lib/tools/processRefund.ts
import { tool } from "ai";
import { z } from "zod";
import { CheqpointClient, RejectedError } 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",
  parameters: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
    reason: z.string(),
  }),
  execute: async ({ orderId, amount, reason }) => {
    const approval = await cheqpoint.checkpoint({
      action: "process_refund",
      summary: `Refund $${amount} for order ${orderId}`,
      details: { orderId, amount, reason },
      riskLevel: amount > 500 ? "high" : "medium",
    });

    if (approval.status !== "APPROVED") {
      throw new RejectedError(`Refund declined: ${approval.decisionNote ?? "No reason provided"}`);
    }

    // Reviewer may have changed the amount — always prefer modifiedDetails
    const payload = approval.modifiedDetails ?? { orderId, amount };
    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 };
  },
});

Step 2 — Use in a Next.js API route

TypeScript
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { processRefund } from "@/lib/tools/processRefund";
import { deleteRecord } from "@/lib/tools/deleteRecord";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: openai("gpt-4o"),
    messages,
    tools: { processRefund, deleteRecord },
    maxSteps: 5,
  });

  return result.toDataStreamResponse();
}

Step 3 — Render tool state in useChat

TypeScript
// app/components/Chat.tsx
"use client";

import { useChat } from "ai/react";

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
          {/* Show tool invocations */}
          {m.toolInvocations?.map((tool) => (
            <div key={tool.toolCallId} className="text-xs text-muted-foreground">
              {tool.state === "call" && `⏳ Waiting for approval: ${tool.toolName}...`}
              {tool.state === "result" && `✅ ${tool.toolName} completed`}
            </div>
          ))}
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} placeholder="Ask the Assistant..." />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Non-streaming with generateText

TypeScript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
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 },
  maxSteps: 3,
});

// steps contains each tool call + its approval result
console.log("Final response:", text);