Skip to main content
Veto integrates with the Vercel AI SDK through language model middleware. This validates every tool call in both generateText and streamText before execution.

Installation

npm install veto-sdk ai @ai-sdk/openai

Quick Start

With generateText

import { wrapLanguageModel, generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { Veto } from 'veto-sdk';
import { createVetoMiddleware } from 'veto-sdk/integrations/vercel-ai';

const veto = await Veto.init();

// Wrap the model with Veto middleware
const model = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: createVetoMiddleware(veto),
});

const { text } = await generateText({
  model,
  tools: {
    sendEmail: tool({
      description: 'Send an email',
      parameters: z.object({
        to: z.string().email(),
        subject: z.string(),
        body: z.string(),
      }),
      execute: async ({ to, subject, body }) => {
        console.log(`Sending email to ${to}...`);
        return { sent: true };
      },
    }),
  },
  maxSteps: 5,
  prompt: 'Send an email to [email protected] about the meeting',
});

console.log(text);

With streamText

import { streamText } from 'ai';

const result = streamText({
  model, // Same wrapped model from above
  tools: {
    sendEmail: tool({
      description: 'Send an email',
      parameters: z.object({
        to: z.string().email(),
        body: z.string(),
      }),
      execute: async ({ to, body }) => ({ sent: true }),
    }),
  },
  maxSteps: 5,
  prompt: 'Send an email to [email protected]',
});

// Stream the response
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

Middleware Options

The middleware accepts optional callbacks and configuration:
import { createVetoMiddleware } from 'veto-sdk/integrations/vercel-ai';

const middleware = createVetoMiddleware(veto, {
  // Called when a tool call is allowed
  onAllow: (toolName, args) => {
    console.log(`✓ Allowed ${toolName}`);
  },
  
  // Called when a tool call is denied
  onDeny: (toolName, args, reason) => {
    console.error(`✗ Denied ${toolName}: ${reason}`);
  },
  
  // Throw error in streaming mode instead of dropping denied calls
  throwOnDeny: false, // default: false
});

Streaming Behavior

In streaming mode (streamText), denied tool calls are silently dropped from the stream by default. Set throwOnDeny: true to throw an error instead:
const middleware = createVetoMiddleware(veto, {
  throwOnDeny: true,
});

try {
  const result = streamText({ model, tools, prompt });
  for await (const chunk of result.textStream) {
    process.stdout.write(chunk);
  }
} catch (error) {
  if (error instanceof ToolCallDeniedError) {
    console.error(`Tool denied: ${error.toolName}`);
  }
}
In non-streaming mode (generateText), denied tool calls always throw ToolCallDeniedError.

How It Works

The Vercel AI SDK middleware intercepts tool calls at the language model level:
  1. Model generates tool calls in the completion
  2. Middleware intercepts tool calls before SDK executes tool.execute()
  3. Veto validates each tool call against your rules
  4. Decision:
    • Allow: Tool executes with original or modified arguments
    • Deny: In generateText, throws error; in streamText, drops tool call from stream (or throws if throwOnDeny: true)

Argument Modification

Veto can modify tool arguments based on rules. Modified arguments are automatically passed to the tool:
rules:
  - id: enforce-domain
    name: Enforce company email domain
    action: allow
    tools:
      - sendEmail
    modify:
      arguments.to:
        pattern: "@example\\.com$"
        replacement: "@company.com"
// Model calls: sendEmail({ to: "[email protected]" })
// Veto modifies to: sendEmail({ to: "[email protected]" })
// Tool receives the modified argument

Complete Example

Here’s a complete example with tool validation:
import { wrapLanguageModel, generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { Veto } from 'veto-sdk';
import { createVetoMiddleware } from 'veto-sdk/integrations/vercel-ai';

// Initialize Veto
const veto = await Veto.init();

// Create middleware with callbacks
const middleware = createVetoMiddleware(veto, {
  onDeny: (toolName, args, reason) => {
    console.error(`🛑 Blocked ${toolName}: ${reason}`);
  },
});

// Wrap model
const model = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware,
});

// Define tools
const tools = {
  transferFunds: tool({
    description: 'Transfer funds between accounts',
    parameters: z.object({
      from: z.string(),
      to: z.string(),
      amount: z.number(),
    }),
    execute: async ({ from, to, amount }) => {
      console.log(`Transferring $${amount} from ${from} to ${to}`);
      return { success: true, transactionId: '123' };
    },
  }),
  sendEmail: tool({
    description: 'Send an email',
    parameters: z.object({
      to: z.string().email(),
      body: z.string(),
    }),
    execute: async ({ to, body }) => {
      console.log(`Sending email to ${to}`);
      return { sent: true };
    },
  }),
};

// Generate text with tools
const { text, toolCalls, toolResults } = await generateText({
  model,
  tools,
  maxSteps: 5,
  prompt: 'Transfer $500 from checking to savings, then email me confirmation',
});

console.log('Final response:', text);
console.log('Tool calls:', toolCalls);
console.log('Tool results:', toolResults);

Multi-Step Conversations

The middleware works seamlessly with multi-step tool use:
const { text } = await generateText({
  model,
  tools: {
    search: tool({
      description: 'Search the web',
      parameters: z.object({ query: z.string() }),
      execute: async ({ query }) => `Results for: ${query}`,
    }),
    summarize: tool({
      description: 'Summarize text',
      parameters: z.object({ text: z.string() }),
      execute: async ({ text }) => `Summary: ${text.slice(0, 100)}...`,
    }),
  },
  maxSteps: 10, // Allow multiple tool calls
  prompt: 'Search for AI safety research and summarize the findings',
});
Veto validates each tool call in the sequence.

TypeScript API Reference

createVetoMiddleware(veto, options?)

Create a Vercel AI SDK language model middleware that validates tool calls. Parameters:
  • veto: Veto - Initialized Veto instance
  • options?: CreateVetoMiddlewareOptions
    • onAllow?: (toolName: string, args: Record<string, unknown>) => void | Promise<void>
    • onDeny?: (toolName: string, args: Record<string, unknown>, reason: string) => void | Promise<void>
    • throwOnDeny?: boolean - Throw in streaming mode instead of dropping denied calls (default: false)
Returns: VetoVercelMiddleware (compatible with LanguageModelV3Middleware) Middleware Interface:
interface VetoVercelMiddleware {
  specificationVersion: 'v3';
  wrapGenerate?: (options: WrapOptions) => Promise<GenerateResult>;
  wrapStream?: (options: WrapOptions) => Promise<StreamResult>;
}

Error Handling

Generate Mode

In generateText, denied tool calls throw ToolCallDeniedError:
import { ToolCallDeniedError } from 'veto-sdk';

try {
  await generateText({ model, tools, prompt });
} catch (error) {
  if (error instanceof ToolCallDeniedError) {
    console.error('Tool denied:', {
      toolName: error.toolName,
      toolCallId: error.toolCallId,
      reason: error.validationResult?.reason,
      matchedRules: error.validationResult?.matchedRuleIds,
    });
  }
}

Stream Mode

In streamText, denied tool calls are silently dropped by default:
const result = streamText({ model, tools, prompt });

// Tool calls are validated but denials don't interrupt the stream
for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

// Check tool results after streaming
const { toolResults } = await result;
console.log('Executed tools:', toolResults); // Only allowed tools appear here
Set throwOnDeny: true to make streaming mode throw on denials.

Advanced: Custom Tool Names

The middleware automatically extracts tool names from the SDK’s tool call format. If you need custom name mapping, use the onDeny callback:
const middleware = createVetoMiddleware(veto, {
  onDeny: (toolName, args, reason) => {
    // Log to your monitoring system
    logger.warn('Tool call denied', {
      tool: toolName,
      arguments: args,
      reason,
      timestamp: new Date(),
    });
  },
});

Next Steps

Configure Rules

Define validation rules for your tools

Streaming Guide

Best practices for streaming with guardrails

Error Handling

Handle denied tool calls gracefully

API Reference

Full Veto API documentation

Build docs developers (and LLMs) love