Free lesson · GenAI Application Engineering
Build content safety middleware with Llama Guard 4
You will build a ContentSafetyMiddleware in middleware/content_safety.py that classifies content using Llama Guard 4 via Together.ai. The classify_content() method accepts a ContentInput Pydantic model (text, image_base64, content_type Enum: user_input/ai_output). It calls Together.ai via openai.AsyncOpenAI(base_url='https://api.together.xyz/v1') with model='meta-llama/Llama-Guard-4-12B'. The response is parsed into SafetyResult with fields: is_safe (bool), violated_categories (List[HazardCategory]), confidence (float), raw_response (str). HazardCategory is an enum covering 14 categories (S1-S14) including violent crimes and sexual content. The middleware integrates as a FastAPI dependency checking inputs and outputs, returning HTTP 451 for unsafe content with violation details.
Course: Full-Stack GenAI Applications · Chapter 7 · Multi-Modal Input/Output APIs
Free to read — no subscription required.
Introduction
When you wire a multi-modal API into production, every uploaded image, transcribed audio clip, and free-text prompt becomes a liability vector — a single CSAM image or doxxing request that slips through to your vision pipeline can trigger billable GPT-4o and Gemini calls, get persisted in logs, and put you on the wrong side of a takedown order before you've even returned a response. Llama Guard 4 is Meta's multimodal safety classifier that scores text and images against 14 MLCommons hazard categories (S1–S14), and Together.ai exposes it through an OpenAI-compatible endpoint, so you can bolt it onto a FastAPI stack without standing up GPU inference yourself. By the end of this lesson you will be able to deploy Llama Guard 4 as request-blocking middleware that classifies content before it fans out to downstream models, parse the category codes it returns, and short-circuit unsafe requests with structured 422 responses that name the violated categories.
Key Terminology
- Llama Guard 4: Meta's multimodal safety classifier (
meta-llama/Llama-Guard-4-12B) that scores text and image inputs against the 14 MLCommons hazard categories (S1–S14) and returns eithersafeorunsafe\n<category codes>. - Together.ai inference endpoint: An OpenAI-compatible chat-completions API (
https://api.together.xyz/v1) that hosts Llama Guard 4, letting middleware call the classifier viaAsyncOpenAIwithout standing up dedicated GPU inference. - MLCommons hazard taxonomy (S1–S14): The 14-category AI safety taxonomy (v0.5) that Llama Guard 4 emits as short codes (e.g.
S1,S10) which middleware maps to human-readable labels for logging and 422 error responses. - Fail-closed middleware: A safety-classifier failure policy where any Together.ai error or timeout rejects the request rather than letting it proceed to downstream models — the production-default behavior for
ContentSafetyMiddleware. - Short-circuit response: The 422 status returned by the middleware when Llama Guard 4 flags content as unsafe, listing the violated category codes and descriptions before any GPT-4o, Gemini, or Whisper call is billed.
Concepts
Understanding Llama Guard 4's Hazard Taxonomy
Llama Guard 4 classifies content against the MLCommons AI Safety taxonomy v0.5, which defines 14 distinct hazard categories (S1 through S14). Your middleware must parse the model's output and map category codes back to human-readable labels for logging and error responses.
- S1 (Violent Crimes): Physical violence including assault, murder, and terrorism planning
- S2 (Non-Violent Crimes): Fraud, theft, cybercrime, and drug trafficking
- S3 (Sex-Related Crimes): Sexual assault, exploitation, and trafficking
- S4 (Child Sexual Exploitation): Any CSAM content—zero-tolerance category
- S5 (Defamation): False statements of fact intended to damage reputation
- S6 (Specialized Advice): Unqualified medical, legal, or financial advice
- S7 (Privacy): PII exposure, doxxing, and surveillance enablement
- S8 (Intellectual Property): Copyright infringement and trade secret disclosure
- S9 (Indiscriminate Weapons): Instructions for CBRN weapons
- S10 (Hate): Content targeting protected characteristics
- S11 (Suicide & Self-Harm): Content promoting or instructing self-harm
- S12 (Sexual Content): Explicit material not involving crimes
- S13 (Elections): Voting misinformation and election interference
- S14 (Code Interpreter Abuse): Exploiting code execution environments
When Llama Guard 4 determines content is unsafe, it returns unsafe followed by a newline and category codes (e.g., unsafe\nS1,S10). Safe content returns safe.
Operational Considerations
When deploying Llama Guard 4 as middleware in a production multi-modal API, several operational factors determine whether it becomes a bottleneck or a seamless safety layer.
Latency budget: Together.ai's inference for Llama Guard 4 typically completes in 200-400ms for text inputs and 400-800ms for image inputs. Since this classification runs before the concurrent GPT-4o and Gemini 2.5 Flash vision calls (which themselves take 1-3 seconds each), the safety check adds proportionally less overhead to image-heavy requests. For audio transcription via Whisper, classify the transcript text after transcription rather than the raw audio—Llama Guard 4 does not accept audio inputs directly.
Batching strategy: If a request contains multiple images, classify each individually rather than packing them into a single call. The model processes one content item at a time, and individual classification produces per-image category mappings.
Failure mode: If Together.ai returns an error or times out, your middleware must decide whether to fail-open or fail-closed. For production systems, fail-closed is the safer default. Set the AsyncOpenAI client's timeout parameter lower than your overall API timeout.
Audit logging: Always persist the raw_response field from SafetyResult alongside the request ID, timestamp, and content hash. This audit trail is essential for compliance and false-positive analysis.
Code Walkthrough
Architecture: Where Safety Classification Sits
In a multi-modal API pipeline, the content safety middleware must execute before any content reaches the concurrent vision analysis, grounding queries, or audio transcription endpoints. This placement ensures that a single unsafe image does not trigger billable API calls to GPT-4o, Gemini, and Whisper simultaneously before being rejected.
This architecture routes every request through ContentSafetyMiddleware, which invokes Llama Guard 4 via Together.ai before any model call executes. Safe requests fan out to vision analysis, grounding, and transcription handlers, while unsafe requests short-circuit with a 422 status and violated category details.
Code snippet mermaid
Loading diagram...
- Line 1: Declares a Mermaid flowchart with top-down (TD) layout direction.
- Lines 2-3: Defines the entry flow — a client request (
A) routes to a FastAPI endpoint (B), which passes through aContentSafetyMiddlewarenode (C) that uses Llama Guard 4 via the Together.ai API for content moderation. - Lines 4-5: Defines the two conditional branches from the safety middleware — requests classified as
safeproceed to the route handler (D), whileunsaferequests are short-circuited with a 422 HTTP response (E) listing the violated safety categories. - Line 12: Defines the final edge from the response assembly node (I) to the client response (J), completing the request-response lifecycle.
This architecture ensures that the safety classification acts as a gate. The ContentSafetyMiddleware class wraps the Together.ai API call and exposes a classify_content() method that accepts a Pydantic ContentInput model. If classification returns unsafe with any triggered categories, the middleware short-circuits the request pipeline and returns a structured error response containing the specific violated category codes and their human-readable descriptions.
Building the Pydantic Models and Safety Classifier
The foundation of the content safety middleware is a set of Pydantic models that define the input contract and classification result, plus the ContentSafetyMiddleware class that wraps the Together.ai call. The ContentInput model handles both text-only and multimodal inputs because Llama Guard 4 accepts both. The SafetyResult model captures whether content is safe, which categories were violated, and the raw model response for audit logging. The HAZARD_CATEGORIES dictionary maps short codes like S1 to full descriptions. The middleware exposes a single classify_content() method that builds the appropriate message format for text or image, sends it to Llama Guard 4 at temperature=0.0, and parses the response — safe returns a clean result, while unsafe\n<codes> is parsed into a list of violated categories with human-readable descriptions.
Code snippet python
1from pydantic import BaseModel, Field 2from openai import AsyncOpenAI 3from enum import Enum 4 5class ContentType(str, Enum): 6 TEXT = "text" 7 IMAGE_URL = "image_url" 8 9class ContentInput(BaseModel): 10 content_type: ContentType 11 text: str | None = Field(default=None, description="Text content to classify") 12 image_url: str | None = Field(default=None, description="Image URL to classify") 13 14class SafetyResult(BaseModel): 15 is_safe: bool 16 violated_categories: list[str] = Field(default_factory=list) 17 category_descriptions: dict[str, str] = Field(default_factory=dict) 18 raw_response: str = Field(default="", description="Raw model output for audit") 19 20HAZARD_CATEGORIES: dict[str, str] = { 21 "S1": "Violent Crimes", "S2": "Non-Violent Crimes", 22 "S3": "Sex-Related Crimes", "S4": "Child Sexual Exploitation", 23 "S5": "Defamation", "S6": "Specialized Advice", 24 "S7": "Privacy", "S8": "Intellectual Property", 25 "S9": "Indiscriminate Weapons", "S10": "Hate", 26 "S11": "Suicide & Self-Harm", "S12": "Sexual Content", 27 "S13": "Elections", "S14": "Code Interpreter Abuse", 28} 29 30class ContentSafetyMiddleware: 31 MODEL = "meta-llama/Llama-Guard-4-12B" 32 33 def __init__(self, api_key: str): 34 self._client = AsyncOpenAI( 35 api_key=api_key, 36 base_url="https://api.together.xyz/v1", 37 ) 38 39 async def classify_content(self, content: ContentInput) -> SafetyResult: 40 messages = self._build_messages(content) 41 response = await self._client.chat.completions.create( 42 model=self.MODEL, 43 messages=messages, 44 max_tokens=100, 45 temperature=0.0, 46 ) 47 raw = response.choices[0].message.content.strip() 48 return self._parse_response(raw) 49 50 def _build_messages(self, content: ContentInput) -> list[dict]: 51 if content.content_type == ContentType.IMAGE_URL: 52 user_content = [ 53 {"type": "image_url", "image_url": {"url": content.image_url}}, 54 {"type": "text", "text": content.text or "Classify this image."}, 55 ] 56 else: 57 user_content = content.text 58 return [{"role": "user", "content": user_content}] 59 60 def _parse_response(self, raw: str) -> SafetyResult: 61 lines = raw.strip().split("\n") 62 verdict = lines[0].strip().lower() 63 if verdict == "safe": 64 return SafetyResult(is_safe=True, raw_response=raw) 65 66 violated = [] 67 descriptions = {} 68 if len(lines) > 1: 69 codes = [c.strip() for c in lines[1].split(",")] 70 for code in codes: 71 if code in HAZARD_CATEGORIES: 72 violated.append(code) 73 descriptions[code] = HAZARD_CATEGORIES[code] 74 75 return SafetyResult( 76 is_safe=False, 77 violated_categories=violated, 78 category_descriptions=descriptions, 79 raw_response=raw, 80 )
- Lines 1-12: Import
BaseModelandFieldfrom Pydantic,AsyncOpenAIfor the non-blocking Together.ai client, andEnumfor the content type discriminator.ContentTypeis a string enum withTEXTandIMAGE_URLvariants;ContentInputrequires acontent_typediscriminator and provides optionaltextandimage_urlfields (only one is required per request). - Lines 14-28:
SafetyResultcapturesis_safe, theviolated_categorieslist, thecategory_descriptionsmapping, and theraw_responsefor audit logging.HAZARD_CATEGORIESis the canonical S1–S14 code-to-description mapping used to translate model output into human-readable labels. - Lines 30-38:
ContentSafetyMiddlewaredeclares the Together.ai model identifier as aclassconstant and initializes anAsyncOpenAIclient in__init__, pointingbase_urlat Together.ai's OpenAI-compatible endpoint so the rest of the OpenAI SDK surface (chat completions, streaming, etc.) works unchanged. - Lines 40-49:
classify_contentbuilds the message format via_build_messages, sends the request withtemperature=0.0for deterministic classification andmax_tokens=100(Llama Guard responses are always short), then delegates parsing to_parse_response. - Lines 51-60:
_build_messagesbranches on thecontent_typediscriminator. For image inputs, it constructs a list with animage_urlblock followed by atextblock (defaulting to a generic classification prompt if the caller provided None). For text inputs, it passes the string directly. - Lines 60-80:
_parse_responsesplits the raw output by newlines. If the first line equalssafe, it returns a cleanSafetyResult. Otherwise it extracts comma-separated codes from line 2, filters them againstHAZARD_CATEGORIES(guarding against unknown codes from malformed output), and returns aSafetyResultwithis_safe=False, the populated category lists, and the raw response for audit.
Integrating as FastAPI Middleware
The middleware must intercept requests before they reach your vision, grounding, or transcription handlers. Rather than using ASGI middleware (which operates on raw bytes and complicates Pydantic model access), the idiomatic FastAPI pattern is a dependency function that runs the safety check and raises an HTTPException if content is unsafe. This dependency can be injected into any route that accepts user-generated content, ensuring consistent safety enforcement across the concurrent GPT-4o/Gemini vision pipeline, Gemini Google Search grounding endpoint, and Whisper audio transcription handler. The following code defines the verify_content_safety dependency function that extracts a ContentInput from the request body, runs classification, and either passes through or rejects the request with a 422 status containing the specific violated hazard categories.
Code snippet python
1from fastapi import Depends, HTTPException, status 2import os 3 4_middleware = ContentSafetyMiddleware( 5 api_key=os.environ["TOGETHER_API_KEY"] 6) 7 8async def verify_content_safety(content: ContentInput) -> ContentInput: 9 result = await _middleware.classify_content(content) 10 if not result.is_safe: 11 raise HTTPException( 12 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 13 detail={ 14 "error": "content_safety_violation", 15 "violated_categories": result.violated_categories, 16 "descriptions": result.category_descriptions, 17 }, 18 ) 19 return content 20 21# Usage in route definitions: 22# @app.post("/api/v1/vision/analyze") 23# async def analyze_image( 24# content: ContentInput = Depends(verify_content_safety), 25# ): 26# # Content is guaranteed safe here — proceed with 27# # concurrent GPT-4o + Gemini 2.5 Flash analysis 28# ...
- Lines 1-2: Import FastAPI's
Dependsfor dependency injection,HTTPExceptionfor error responses, andosfor environment variable access. - Lines 4-6: Instantiate the
ContentSafetyMiddlewareat module level with theTOGETHER_API_KEYenvironment variable. This creates a single sharedAsyncOpenAIclient instance that benefits from connection pooling across requests. - Lines 8-19: The
verify_content_safetyasyncfunction accepts aContentInputparameter (FastAPI automatically deserializes the request body into this model), runs classification, and raises anHTTPExceptionwith status 422 if the content is unsafe. The error detail includes structured data—the violated category codes and their descriptions—so clients can display specific safety feedback rather than a generic rejection. - Lines 22-29: Commented usage example shows how to inject the dependency into a route using
Depends(verify_content_safety). The key insight is that any handler receiving this dependency can assume content has already passed safety classification, eliminating redundant checks in the vision analysis, grounding, and transcription code paths.
Do's and Don'ts
Do's
- ✓Do invoke
classify_content()as the first step in the request pipeline, before any downstream model call — placingContentSafetyMiddlewareat the gateway ensures a single unsafe image cannot trigger simultaneous billable calls to GPT-4o, Gemini, and Whisper before being rejected; running it after fan-out defeats the entire middleware pattern. - ✓Do set
temperature=0.0in thechat.completions.createcall tometa-llama/Llama-Guard-4-12B— Llama Guard 4's output is a deterministic label (safeorunsafe\nS4\nS12); any non-zero temperature introduces randomness into a binary safety decision that must be reproducible across identical inputs for auditing and compliance. - ✓Do store
raw_responseinSafetyResultalongside the parsedviolated_categorieslist — the unmodified Llama Guard 4 output is the only audit evidence that a specific request was blocked; without it you cannot reconstruct which S-codes triggered the rejection or investigate false-positive blocks after the fact.
Don'ts
- ✗Don't parse Llama Guard 4's response by reading only the first line — when content is unsafe the model returns
unsafe\nS1\nS3with category codes on subsequent lines; stopping afterunsafeleavesviolated_categoriesempty, makes the 422 response meaningless to callers, and loses theHAZARD_CATEGORIESlookup that converts codes likeS4to "Child Sexual Exploitation". - ✗Don't send image inputs to
_build_messages()as a plain text string — Llama Guard 4's multimodal classification requires the structured{"type": "image_url", "image_url": {"url": ...}}content block; passing an image URL as thetextfield bypasses vision entirely and produces unreliable text-only classification that silently misses image-borne hazards. - ✗Don't instantiate
AsyncOpenAIwith the defaultapi.openai.combase URL when targeting Together.ai — Llama Guard 4 is served athttps://api.together.xyz/v1; omitting thebase_urloverride routes the request to OpenAI's endpoint, which does not host the model and returns a 404 or a model-not-found error with no safety classification performed.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Application Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in Full-Stack GenAI Applications
- Ch 6Build a document chunking pipeline (recursive, semantic, token-aware)
- Ch 7Build a vision analysis API with GPT-4o + Gemini concurrently
- Ch 7Build a Gemini grounded-generation endpoint with Google Search
- Ch 7Build content safety middleware with Llama Guard 4You are here
- Ch 8Build an MCP server exposing business logic as tools
- Ch 8Build an MCP client in FastAPI
- Ch 8Build a Pydantic AI agent with typed tools and DI