Developer Guides
·IdenticAPI

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):

APIRoleWhere guardrails go
generateTextSingle completionBefore call (input guard); after result (output guard)
streamTextStreaming completionBuffer in Route Handler; guard assembled text in onFinish
tool / toolsTool definitionsagent_action guard before executing execute functions
maxSteps / agent loopsMulti-step tool useInput guard each turn; action guard each tool call
toDataStreamResponseWire format to clientOnly 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:

  1. Guard assembled messages before the next generateText / streamText call
  2. Guard each tool execute before side effects
  3. 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-patternWhy
Guard in Client ComponentsExposes API keys; bypassable
Assume useChat hook secures inputHook is client transport; server must enforce
Invent SDK middleware pluginsNot in public API; brittle across versions
Run output_safety on user messages onlyWrong stage; use prompt_injection / pii_secrets
Skip guard on tool results in messagesIndirect 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 blockgenerateText not invoked (mock provider)
  • Output block → client does not receive flagged text in non-streaming path
  • Tool execute with destructive args → agent_action blocks before CRM call

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