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:
Model generates tool calls in the completion
Middleware intercepts tool calls before SDK executes tool.execute()
Veto validates each tool call against your rules
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"
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.
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