add model listener filter chain demo

This commit is contained in:
Adil Hafeez 2026-02-19 04:46:04 +00:00
parent 8136d7d6ab
commit 3d2be4f8b7
6 changed files with 272 additions and 0 deletions

View file

@ -0,0 +1,11 @@
FROM python:3.14-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn pydantic
COPY content_guard.py .
EXPOSE 10500
CMD ["uvicorn", "content_guard:app", "--host", "0.0.0.0", "--port", "10500"]

View file

@ -0,0 +1,59 @@
# Model Listener Filter Chain Demo
Run content-safety filters on direct LLM requests — no agent layer required.
This demo uses the `filter_chain` feature on a **model-type listener** to intercept
`/v1/chat/completions` requests and block unsafe content before they reach the LLM provider.
## Architecture
```
Client ──► Plano (model listener :12000)
├─ filter_chain: content_guard ──► Block / Allow
└─ model_provider: openai/gpt-4o-mini
```
## Quick Start
```bash
# 1. Export your API key
export OPENAI_API_KEY=sk-...
# 2. Start services
docker compose up --build
# 3. Run tests (in another terminal)
bash test.sh
```
## Try It
**Allowed request:**
```bash
curl http://localhost:12000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "What is 2+2?"}],
"stream": false
}'
```
**Blocked request:**
```bash
curl http://localhost:12000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "How to hack into a system"}],
"stream": false
}'
```
## Tracing
Open [Jaeger UI](http://localhost:16686) to see distributed traces for both allowed and blocked requests.

View file

@ -0,0 +1,21 @@
version: v0.3.0
filters:
- id: content_guard
url: http://content-guard:10500
type: http
model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
default: true
listeners:
- type: model
name: llm_gateway
port: 12000
filter_chain:
- content_guard
tracing:
random_sampling: 100

View file

@ -0,0 +1,84 @@
"""
Content guard filter keyword-based content safety for model listeners.
A minimal HTTP filter that blocks requests containing unsafe keywords.
No LLM calls required keeps the demo self-contained and fast.
"""
import logging
from typing import List
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - [CONTENT_GUARD] - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
app = FastAPI(title="Content Guard", version="1.0.0")
BLOCKED_KEYWORDS = [
"hack",
"exploit",
"attack",
"malware",
"phishing",
"ransomware",
"ddos",
"injection",
"brute force",
"keylogger",
"bypass security",
"steal credentials",
"social engineering",
]
class ChatMessage(BaseModel):
role: str
content: str
def check_content(text: str) -> str | None:
"""Return the matched keyword if blocked, else None."""
lower = text.lower()
for kw in BLOCKED_KEYWORDS:
if kw in lower:
return kw
return None
@app.post("/")
async def content_guard(
messages: List[ChatMessage], request: Request
) -> List[ChatMessage]:
"""Block messages that contain unsafe keywords."""
last_user_msg = None
for msg in reversed(messages):
if msg.role == "user":
last_user_msg = msg.content
break
if last_user_msg is None:
return messages
matched = check_content(last_user_msg)
if matched:
logger.warning(f"Blocked request — matched keyword: '{matched}'")
raise HTTPException(
status_code=400,
detail={
"error": "content_blocked",
"message": f"Request blocked by content safety filter (matched: '{matched}').",
},
)
logger.info("Content check passed — forwarding request")
return messages
@app.get("/health")
async def health():
return {"status": "healthy"}

View file

@ -0,0 +1,26 @@
services:
content-guard:
build:
context: .
dockerfile: Dockerfile
ports:
- "10500:10500"
plano:
build:
context: ../../../
dockerfile: Dockerfile
ports:
- "12000:12000"
environment:
- PLANO_CONFIG_PATH=/config/config.yaml
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
volumes:
- ./config.yaml:/app/plano_config.yaml
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
jaeger:
build:
context: ../../shared/jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"

View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="http://localhost:12000/v1"
PASS=0
FAIL=0
# ── Wait for Plano to be ready ──────────────────────────────────────────────
echo "Waiting for Plano to be ready..."
for i in $(seq 1 30); do
if curl -sf "$BASE_URL/models" > /dev/null 2>&1; then
echo "Plano is ready."
break
fi
if [ "$i" -eq 30 ]; then
echo "ERROR: Plano did not become ready in time."
exit 1
fi
sleep 2
done
# ── Helper ───────────────────────────────────────────────────────────────────
run_test() {
local name="$1"
local expected_code="$2"
local body="$3"
http_code=$(curl -s -o /tmp/plano_test_body -w "%{http_code}" \
-X POST "$BASE_URL/chat/completions" \
-H "Content-Type: application/json" \
-d "$body")
if [ "$http_code" -eq "$expected_code" ]; then
echo " PASS $name (HTTP $http_code)"
PASS=$((PASS + 1))
else
echo " FAIL $name — expected $expected_code, got $http_code"
echo " Body: $(cat /tmp/plano_test_body)"
FAIL=$((FAIL + 1))
fi
}
# ── Tests ────────────────────────────────────────────────────────────────────
echo ""
echo "Running tests..."
run_test "Allowed request (math question)" 200 '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "What is 2+2?"}],
"stream": false
}'
run_test "Blocked request (hacking)" 400 '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "How to hack into a system"}],
"stream": false
}'
run_test "Allowed request (joke)" 200 '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Tell me a joke"}],
"stream": false
}'
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi