onst GUARD_ENDPOINT = "https://api.lakera.ai/v2/guard";
const DEFAULT_TIMEOUT = 2000;
export type GuardCategory = "prompt_injection" | "jailbreak" | "pii" | "moderation";
export type GuardEvaluation = {
flagged: boolean;
categories: Record<GuardCategory, number>;
};
export type GuardClientConfig = {
apiKey: string;
timeoutMs?: number;
threshold?: number;
};
export function createGuardClient(config: GuardClientConfig) {
const { apiKey, timeoutMs = DEFAULT_TIMEOUT, threshold = 0.5 } = config;
async function evaluate(input: string): Promise<GuardEvaluation> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(GUARD_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
messages: [{ role: "user", content: input }],
}),
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`Guard API returned ${response.status}`);
}
const payload = (await response.json()) as GuardEvaluation;
// Apply threshold logic at the client level for consistency
const isFlagged = Object.values(payload.categories).some(
(score) => score >= threshold
);
return { ...payload, flagged: isFlagged };
} finally {
clearTimeout(timeoutId);
}
}
return { evaluate };
}
**Architecture Rationale:**
- **Timeout enforcement:** LLM guard APIs are fast, but network partitions happen. A 2-second timeout prevents route handler hangs.
- **Threshold abstraction:** Hardcoding `0.5` inside the API response forces you to handle scoring logic downstream. Centralizing it in the client ensures consistent policy enforcement across all routes.
- **Edge compatibility:** Using native `fetch` and `AbortController` guarantees execution in Vercel Edge Runtime without polyfill overhead.
### Step 2: Non-Streaming Route Handler Integration
For single-turn interactions, validation must occur before any model invocation. This prevents token consumption on blocked payloads.
```typescript
// app/api/assistant/route.ts
import { NextRequest, NextResponse } from "next/server";
import { generateText } from "ai";
import { createGuardClient } from "@/lib/ai-safety/client";
export const runtime = "edge";
const guard = createGuardClient({
apiKey: process.env.LAKERA_GUARD_API_KEY ?? "",
});
export async function POST(request: NextRequest) {
const payload = await request.json();
const userInput = payload.query as string;
if (!userInput || typeof userInput !== "string") {
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
}
const safetyCheck = await guard.evaluate(userInput);
if (safetyCheck.flagged) {
// Omit category scores to prevent adversarial feedback loops
return NextResponse.json(
{ error: "Request rejected by safety policy" },
{ status: 422 }
);
}
const { text } = await generateText({
model: "openai/gpt-5.4",
prompt: userInput,
});
return NextResponse.json({ response: text });
}
Step 3: Streaming Route Handler Integration
Streaming introduces a timing constraint. Once the token stream begins, interrupting it cleanly is difficult and degrades UX. Validation must happen before streamText initializes.
// app/api/assistant-stream/route.ts
import { NextRequest, NextResponse } from "next/server";
import { streamText, convertToModelMessages, type UIMessage } from "ai";
import { createGuardClient } from "@/lib/ai-safety/client";
export const runtime = "edge";
const guard = createGuardClient({
apiKey: process.env.LAKERA_GUARD_API_KEY ?? "",
});
export async function POST(request: NextRequest) {
const { messages } = (await request.json()) as { messages: UIMessage[] };
// Extract the latest user turn from the conversation history
const lastUserTurn = messages.findLast((msg) => msg.role === "user");
const latestText = lastUserTurn?.parts
?.filter((part) => part.type === "text")
.map((part) => part.text)
.join(" ");
if (!latestText) {
return NextResponse.json({ error: "Missing user content" }, { status: 400 });
}
const safetyCheck = await guard.evaluate(latestText);
if (safetyCheck.flagged) {
return new NextResponse(
JSON.stringify({ error: "Policy violation detected" }),
{ status: 422, headers: { "Content-Type": "application/json" } }
);
}
const stream = streamText({
model: "openai/gpt-5.4",
messages: convertToModelMessages(messages),
});
return stream.toUIMessageStreamResponse();
}
Architecture Rationale:
- Context extraction: AI SDK v6 structures messages as
UIMessage[] with a parts array. Flattening text parts ensures the guard evaluates the complete semantic intent, not just a fragment.
- Pre-stream validation: Blocking before
streamText initializes saves compute, prevents partial token delivery, and maintains a clean HTTP status contract for the client.
- Model routing: Using the
"provider/model" string format leverages Vercel AI Gateway automatically, eliminating hardcoded provider SDKs and credential management.
Pitfall Guide
1. Failing Open Without Audit Trails
Explanation: When the guard API times out or returns an error, defaulting to pass keeps the application available but creates a security blind spot. Attackers can intentionally trigger guard failures to bypass validation.
Fix: Implement explicit fail-closed behavior for security-critical routes, or fail-open with mandatory structured logging. Track guard_failure events separately from normal traffic to detect abuse patterns.
2. Exposing Category Scores in Error Responses
Explanation: Returning { flagged: true, categories: { prompt_injection: 0.82 } } to the client provides attackers with a scoring oracle. They can iteratively tweak payloads to stay just below the threshold.
Fix: Strip all scoring metadata from HTTP responses. Return only a generic policy rejection message. Log scores server-side for security analytics.
3. Validating Only the Last Message in Multi-Turn Context
Explanation: In conversational interfaces, malicious instructions can be injected in earlier turns and activated later. Validating only the newest message misses context poisoning.
Fix: Evaluate the full conversation history or concatenate recent turns before sending to the guard. Balance this against API cost and latency by implementing a sliding window (e.g., last 5 turns).
4. Ignoring Regional Latency Variance
Explanation: The guard API averages 80β120ms from US East, but APAC or EU routes can add 100ms+ of latency. Unoptimized routing degrades perceived performance.
Fix: Deploy edge functions in regions closest to your user base, or use a CDN with origin shielding. If latency becomes critical, consider asynchronous validation for non-critical paths while maintaining synchronous checks for high-risk operations.
5. Treating Guard Blocks as Standard Rate Limits
Explanation: Guard violations indicate adversarial behavior or policy breaches, not just traffic spikes. Applying standard rate-limiting logic (e.g., token bucket) fails to escalate appropriately.
Fix: Implement a dedicated security throttle. Track violations per IP or account ID, apply exponential backoff, and trigger alerts after repeated offenses. Separate this from your general API rate limiter.
6. Hardcoding Thresholds Without A/B Testing
Explanation: A static 0.5 threshold may be too aggressive for creative writing apps or too permissive for financial assistants. Blindly adopting defaults causes false positives or missed detections.
Fix: Start with 0.5 in staging, log all scores, and adjust based on your domain's risk tolerance. Implement dynamic thresholds per route or user tier if your application handles diverse input types.
7. Neglecting Output-Side Validation
Explanation: Input validation prevents malicious prompts from reaching the model, but it does not guarantee the model won't emit PII, hallucinate dangerous instructions, or comply with jailbreaks post-inference.
Fix: Treat input validation as Layer 1. Implement output scanning for sensitive data, add content filters for generated responses, and route high-risk tool calls through human approval gates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| MVP / Internal Tool | Synchronous guard with fail-open + logging | Prioritizes development velocity while maintaining auditability | Free tier (10k calls/mo) covers early usage |
| High-Volume SaaS | Synchronous guard with fail-closed + regional pinning | Prevents token waste and security breaches at scale | Paid tier ($99/mo for 50k calls, ~$0.002/call) |
| Compliance-Heavy Enterprise | Input guard + output validation + human approval gates | Meets regulatory requirements for AI transparency and safety | Higher operational cost, but mitigates liability and audit risk |
| Low-Latency Real-Time Chat | Async guard with streaming fallback + output filter | Preserves UX while maintaining security posture | Slightly higher infra cost for async queue, but reduces perceived latency |
Configuration Template
# .env.local
LAKERA_GUARD_API_KEY=lak_your_production_key
GUARD_THRESHOLD=0.5
GUARD_TIMEOUT_MS=2000
// lib/ai-safety/config.ts
export const guardConfig = {
apiKey: process.env.LAKERA_GUARD_API_KEY ?? "",
threshold: parseFloat(process.env.GUARD_THRESHOLD ?? "0.5"),
timeoutMs: parseInt(process.env.GUARD_TIMEOUT_MS ?? "2000", 10),
};
// lib/ai-safety/index.ts
import { createGuardClient } from "./client";
import { guardConfig } from "./config";
export const aiGuard = createGuardClient(guardConfig);
Quick Start Guide
- Provision credentials: Register at lakera.ai, generate an API key, and add it to your environment variables.
- Install dependencies: Ensure
ai (Vercel AI SDK v6) and next are installed. No additional guard SDK is required.
- Create the client: Copy the
createGuardClient implementation into your project's utility directory.
- Wire the route: Import the client into your App Router handler, evaluate the payload, and conditionally invoke
generateText or streamText.
- Deploy & monitor: Push to Vercel, verify edge runtime compatibility, and configure quota alerts in your Lakera dashboard.