AI Security with the Vercel AI SDK
AI security with the Vercel AI SDK — where to place guardrails around messages, generation, tool calls, and streamed output.
Secure Vercel AI SDK pipelines by placing IdenticAPI Unified Guard calls in your application code around generateText, streamText, and tool execution — not by relying on undocumented or non-existent SDK security middleware.
The Vercel AI SDK (ai package) provides model abstractions, streaming helpers, and tool-loop utilities. It does not ship built-in guardrail hooks or a first-class "security middleware" API. Guardrails belong in Next.js Route Handlers, Server Actions, or your backend service — the same layers that invoke the SDK.
For Next.js-specific file layout, see AI Guardrails in Next.js. For OpenAI placement details, see Secure OpenAI API Inputs and Outputs.
What the Vercel AI SDK actually provides
Relevant SDK surfaces (current ai package patterns):
| API | Role | Where guardrails go |
|---|---|---|
generateText | Single completion | Before call (input guard); after result (output guard) |
streamText | Streaming completion | Buffer in Route Handler; guard assembled text in onFinish |
tool / tools | Tool definitions | agent_action guard before executing execute functions |
maxSteps / agent loops | Multi-step tool use | Input guard each turn; action guard each tool call |
toDataStreamResponse | Wire format to client | Only after output guard approves content |
Do not invent experimental_guardrails or similar — they are not part of the public SDK contract. Your security layer is explicit fetch to POST /api/v1/guard.
Reference architecture
Client → POST /api/chat (Route Handler)
├── build messages[] (AI SDK format)
├── flatten → Unified Guard INPUT
├── streamText / generateText (AI SDK)
├── onFinish / await result → Unified Guard OUTPUT
└── return UIMessage / data stream
API keys: IDENTICAPI_API_KEY and provider keys stay server-side.
Unified Guard request schema
type UnifiedGuardRequest = {
text?: string;
checks: Array<
"prompt_injection" | "pii_secrets" | "output_safety" | "agent_action"
>;
redact?: boolean;
agent_action?: {
tool_name: string;
action: string;
arguments?: Record<string, unknown>;
context?: string;
policy_id?: string;
};
};
POST https://www.identicapi.com/api/v1/guard with Authorization: Bearer idapi_test_your_key_here.
Server-only guard helper
lib/unified-guard.ts (import only from server modules):
const BASE = "https://www.identicapi.com";
export async function runUnifiedGuard(body: UnifiedGuardRequest) {
const res = await fetch(`${BASE}/api/v1/guard`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IDENTICAPI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body),
cache: "no-store"
});
if (!res.ok) throw new Error(`Guard failed: ${res.status}`);
return res.json() as Promise<{
decision: "allow" | "review" | "block";
request_id: string;
checks: unknown[];
}>;
}
export const FALLBACK = "I can't share that response. Please try again.";
generateText with input and output guards
app/api/chat/route.ts:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { runUnifiedGuard, FALLBACK } from "@/lib/unified-guard";
export const runtime = "nodejs";
function flattenMessages(messages: { role: string; content: string }[]) {
return messages.map((m) => `${m.role}: ${m.content}`).join("\n");
}
export async function POST(req: Request) {
const { messages } = await req.json();
const promptText = flattenMessages(messages);
const inputGuard = await runUnifiedGuard({
text: promptText,
checks: ["prompt_injection", "pii_secrets"],
redact: true
});
if (inputGuard.decision !== "allow") {
return Response.json({ role: "assistant", content: FALLBACK });
}
const result = await generateText({
model: openai("gpt-4o-mini"),
messages
});
const outputGuard = await runUnifiedGuard({
text: result.text,
checks: ["output_safety", "pii_secrets"]
});
if (outputGuard.decision !== "allow") {
return Response.json({ role: "assistant", content: FALLBACK });
}
return Response.json({ role: "assistant", content: result.text });
}
streamText: guard in onFinish
The SDK's onFinish callback runs when streaming completes — use it to moderate the full text before persisting or forwarding to other systems:
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { runUnifiedGuard, FALLBACK } from "@/lib/unified-guard";
export async function POST(req: Request) {
const { messages } = await req.json();
const inputGuard = await runUnifiedGuard({
text: flattenMessages(messages),
checks: ["prompt_injection", "pii_secrets"],
redact: true
});
if (inputGuard.decision !== "allow") {
return Response.json({ role: "assistant", content: FALLBACK });
}
const result = streamText({
model: openai("gpt-4o-mini"),
messages,
async onFinish({ text }) {
const outputGuard = await runUnifiedGuard({
text,
checks: ["output_safety", "pii_secrets"]
});
if (outputGuard.decision !== "allow") {
// Log request_id; do not store raw text
await logBlockedCompletion(outputGuard.request_id);
return;
}
await saveApprovedMessage(text);
}
});
return result.toDataStreamResponse();
}
Important: onFinish runs after the client may have received stream chunks. For strict fail-closed UX, buffer server-side and use non-streaming generateText, or implement a server-side buffer that withholds toDataStreamResponse until output guard passes. See Moderating AI Chatbot Responses.
Tool execution guard
Wrap execute functions — the SDK invokes these server-side:
import { tool } from "ai";
import { z } from "zod";
import { runUnifiedGuard } from "@/lib/unified-guard";
const deleteRecord = tool({
description: "Delete a customer record",
parameters: z.object({ id: z.string() }),
execute: async ({ id }) => {
const guard = await runUnifiedGuard({
checks: ["agent_action"],
agent_action: {
tool_name: "crm",
action: "delete",
arguments: { id }
}
});
if (guard.decision !== "allow") {
throw new Error("Action blocked by policy");
}
return await crmDelete(id);
}
});
Authorization and schema validation remain in your code — Validate AI Tool Calls.
Multi-step agents (maxSteps)
Each agent loop iteration should:
- Guard assembled messages before the next
generateText/streamTextcall - Guard each tool
executebefore side effects - Guard final user-visible text before response
Do not assume one input guard at conversation start covers later tool-injected content.
What not to do
| Anti-pattern | Why |
|---|---|
| Guard in Client Components | Exposes API keys; bypassable |
Assume useChat hook secures input | Hook is client transport; server must enforce |
| Invent SDK middleware plugins | Not in public API; brittle across versions |
Run output_safety on user messages only | Wrong stage; use prompt_injection / pii_secrets |
| Skip guard on tool results in messages | Indirect injection vector |
Provider packages
@ai-sdk/openai, @ai-sdk/anthropic, and @ai-sdk/google are transport adapters. Guard placement is identical regardless of provider package — see Anthropic Claude Guardrails and Gemini Guardrails.
Error handling
When Unified Guard returns HTTP errors, fail closed for public chat:
try {
await runUnifiedGuard({ ... });
} catch {
return Response.json({ role: "assistant", content: FALLBACK }, { status: 503 });
}
Document policy — Fail Open vs Fail Closed.
Testing
Mock runUnifiedGuard in Route Handler tests. Assert:
- Input
block→generateTextnot invoked (mock provider) - Output
block→ client does not receive flagged text in non-streaming path - Tool
executewith destructive args →agent_actionblocks before CRM call
Related resources
Summary
Vercel AI SDK security is application-layer: call POST /api/v1/guard around SDK entry points in Route Handlers, use onFinish or post-generateText hooks for output screening, and guard tool execute functions with agent_action. Do not rely on non-existent SDK middleware — own the hooks in server code. Secure Vercel AI SDK pipelines · Documentation
Frequently asked questions
Does the Vercel AI SDK include built-in security middleware?
No. The ai package does not ship guardrail hooks. Place Unified Guard calls in Route Handlers or Server Actions around generateText, streamText, and tool execute functions.
Where should output guardrails run with streamText?
On the assembled completion text — typically in onFinish — before persisting or treating content as approved. For strict fail-closed UX, buffer server-side instead of forwarding raw stream chunks before moderation.
How do I secure AI SDK tool execution?
Wrap tool execute functions with Unified Guard agent_action checks before side effects. Keep schema validation and authorization in your application code.
Can I call Unified Guard from useChat on the client?
Do not use production API keys in client code. Guard calls belong in server Route Handlers that useChat posts to.
Do guardrails differ across @ai-sdk/openai, anthropic, and google?
Placement is identical. Provider packages are transport adapters; POST /api/v1/guard with the same checks schema at input, output, and tool boundaries.
Related reading
- How to Add AI Guardrails to a Next.js App
Add AI guardrails to Next.js App Router apps — server-side Unified Guard calls, input/output checks, and keeping API key…
- How to Validate AI Tool Calls Before Execution
Validate AI tool calls with schema checks, permission evaluation, secret scanning, and policy decisions before allow, re…
- How to Secure OpenAI API Inputs and Outputs
Secure OpenAI API inputs and outputs with pre-model and post-model guardrails — injection screening, PII detection, and …