22
33Run content-safety filters on direct LLM requests — no agent layer required.
44
5- This demo uses the ` input_filters ` feature on a ** model-type listener** to intercept
6- requests and block unsafe content before they reach the LLM provider. Works with all
7- request types: ` /v1/chat/completions ` , ` /v1/responses ` , and Anthropic ` /v1/messages ` .
8-
9- The filter receives the ** full raw request body** and returns it unchanged (or raises 400
10- to block). No message extraction — the complete JSON payload flows through as-is.
5+ This demo uses ` input_filters ` and ` output_filters ` on a ** model-type listener** to
6+ intercept direct LLM requests and responses without routing through an agent layer.
7+ By default it is fully local: a fake OpenAI-compatible provider stands in for a real
8+ hosted model, so developers can test guardrail behavior without provider API keys or
9+ hosted model access. A second config lets developers point the same filter setup at the
10+ real OpenAI endpoint when they want provider-backed testing.
11+ The filter pattern applies to OpenAI Chat Completions (` /v1/chat/completions ` ),
12+ OpenAI Responses (` /v1/responses ` ), and Anthropic Messages (` /v1/messages ` ) request
13+ shapes. The keyless fake provider and smoke test use ` /v1/chat/completions ` for a
14+ deterministic local path.
15+
16+ The input filter receives the full raw request body and returns it unchanged or raises
17+ 400 to block. The output filter receives the provider response and redacts sensitive
18+ content before returning it to the client.
19+
20+ ## Files
21+
22+ - ` config.yaml ` runs the default keyless path with the local fake provider.
23+ - ` config.openai.yaml ` runs the same filters against OpenAI.
24+ - ` docker-compose.yaml ` starts the local demo without requiring provider credentials.
25+ - ` docker-compose.openai.yaml ` mounts ` config.openai.yaml ` and requires ` OPENAI_API_KEY `
26+ for provider-backed testing.
27+ - ` test.sh ` runs the Docker smoke test through Plano.
28+ - ` test_services.py ` runs service-level regression tests without Docker.
1129
1230## Architecture
1331
@@ -16,22 +34,82 @@ Client ──► Plano (model listener :12000)
1634 │
1735 ├─ input_filters: content_guard ──► Block / Allow
1836 │
19- └─ model_provider: openai/gpt-4o-mini
37+ ├─ model_provider: fake-provider (default) or OpenAI (optional)
38+ │
39+ └─ output_filters: output_redactor ──► Redact / Allow
2040```
2141
2242## Quick Start
2343
2444``` bash
25- # 1. Export your API key
26- export OPENAI_API_KEY=sk-...
27-
28- # 2. Start services
45+ # 1. Start services
2946docker compose up --build
3047
31- # 3. Run tests (in another terminal)
48+ # 2. Run tests (in another terminal)
49+ bash test.sh
50+ ```
51+
52+ The test script verifies three behaviors:
53+
54+ - safe requests reach the local fake provider and return a normal chat-completion response
55+ - unsafe requests are blocked by the input filter before reaching the provider
56+ - sensitive provider output is redacted by the output filter before the client receives it
57+
58+ You can also run the service-level tests without Docker:
59+
60+ ``` bash
61+ uv run --with pytest --with fastapi --with httpx --with pydantic \
62+ python -m pytest demos/filter_chains/model_listener_filter/test_services.py -q
63+ ```
64+
65+ ## Validate Locally
66+
67+ From this directory, validate the default keyless compose path:
68+
69+ ``` bash
70+ docker compose config
71+ ```
72+
73+ Validate that the OpenAI path fails early when the API key is missing:
74+
75+ ``` bash
76+ docker compose -f docker-compose.yaml -f docker-compose.openai.yaml config
77+ ```
78+
79+ Expected error:
80+
81+ ``` text
82+ OPENAI_API_KEY environment variable is required but not set
83+ ```
84+
85+ Then confirm the OpenAI compose path renders when a key is provided:
86+
87+ ``` bash
88+ OPENAI_API_KEY=dummy docker compose -f docker-compose.yaml -f docker-compose.openai.yaml config
89+ ```
90+
91+ Run the full local smoke test:
92+
93+ ``` bash
94+ docker compose down
95+ docker compose up --build -d
3296bash test.sh
97+ docker compose down
3398```
3499
100+ ## Test With Real OpenAI
101+
102+ The default ` config.yaml ` uses the local fake provider. To run the same model-listener
103+ input and output filters against OpenAI, use the OpenAI compose override:
104+
105+ ``` bash
106+ export OPENAI_API_KEY=sk-...
107+ docker compose -f docker-compose.yaml -f docker-compose.openai.yaml up --build
108+ ```
109+
110+ The fake-provider service may still start because it is part of the shared compose file,
111+ but Plano will not route traffic to it when ` config.openai.yaml ` is mounted.
112+
35113## Try It
36114
37115** Allowed request:**
@@ -58,6 +136,31 @@ curl http://localhost:12000/v1/chat/completions \
58136 }'
59137```
60138
139+ ** Redacted provider response:**
140+
141+ ``` bash
142+ curl http://localhost:12000/v1/chat/completions \
143+ -H " Content-Type: application/json" \
144+ -d ' {
145+ "model": "gpt-4o-mini",
146+ "messages": [{"role": "user", "content": "Please return the secret marker"}],
147+ "stream": false
148+ }'
149+ ```
150+
151+ The fake provider emits ` SECRET_TOKEN ` ; the output filter redacts it to ` [REDACTED] ` .
152+
153+ ## Why This Helps Developers
154+
155+ Model-listener filters are guardrails for applications that call Plano as a transparent
156+ LLM gateway. A local, deterministic demo helps developers verify filter wiring before
157+ using real providers:
158+
159+ - config mistakes are caught early instead of silently bypassing guardrails
160+ - teams can test request blocking and response redaction in CI without secrets
161+ - contributors can reproduce filter behavior without external model availability
162+ - application code does not need an extra passthrough agent just to run policy checks
163+
61164## Tracing
62165
63166Open [ Jaeger UI] ( http://localhost:16686 ) to see distributed traces for both allowed and blocked requests.
0 commit comments