Skip to content

Commit df53984

Browse files
Sahil Chachraclaude
authored andcommitted
Initial release: LLM Safety Middleware v2.0.0
Production-grade async safety proxy for LLM APIs (Ollama, OpenAI-compatible, and custom backends). Multi-layer pipeline: rate limiter, token filter, spaCy rule engine, toxic-bert semantic classifier, post-generation check. - FastAPI server with health/generate/check/statistics endpoints - Async HTTP proxy via httpx with exponential-backoff retry - 74-test suite (pytest-asyncio + respx for HTTP mocking) - Docker + docker-compose, GitHub Actions CI (tests + docker build) - Full documentation: README, QUICKSTART, DEPLOYMENT, PROJECT_OVERVIEW Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit df53984

22 files changed

Lines changed: 5981 additions & 0 deletions

.env.example

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# LLM Safety Middleware — Environment Variables
2+
# ================================================
3+
# Copy this file to .env and fill in your values.
4+
# Never commit .env to git.
5+
#
6+
# cp .env.example .env
7+
8+
# ── LLM Backend ──────────────────────────────────────────────────────────────
9+
# Which backend protocol to use
10+
LLM_BACKEND_TYPE=ollama # ollama | openai | custom
11+
12+
# Remote LLM server URL
13+
LLM_BASE_URL=http://localhost:11434
14+
15+
# Model name
16+
LLM_MODEL=llama2
17+
18+
# Bearer token — required for OpenAI, Together AI, etc.
19+
# Leave blank for Ollama or unauthenticated backends.
20+
LLM_API_KEY=
21+
22+
# Request timeout in seconds
23+
LLM_TIMEOUT_SECONDS=60
24+
25+
# Retry attempts on transient server errors (5xx)
26+
LLM_MAX_RETRIES=3
27+
28+
# Default generation parameters (can be overridden per-request)
29+
LLM_MAX_NEW_TOKENS=512
30+
LLM_TEMPERATURE=0.7
31+
LLM_TOP_P=0.95
32+
33+
# System message prepended to every conversation (OpenAI mode only)
34+
LLM_SYSTEM_PROMPT=
35+
36+
# ── Safety Middleware Server ──────────────────────────────────────────────────
37+
# Bind address
38+
HOST=0.0.0.0
39+
40+
# Listen port
41+
PORT=8000
42+
43+
# Number of uvicorn worker processes
44+
WORKERS=1
45+
46+
# Path to the SafetyConfig JSON file
47+
CONFIG_PATH=config_production.json
48+
49+
# Secret key for the POST /api/v1/statistics/reset admin endpoint.
50+
# Leave blank to disable that endpoint entirely.
51+
ADMIN_API_KEY=
52+
53+
# Comma-separated list of allowed CORS origins.
54+
# Leave blank to disable cross-origin access (recommended for production).
55+
# Example: https://app.example.com,https://staging.example.com
56+
ALLOWED_ORIGINS=

.github/workflows/docker.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Docker Build
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
build:
11+
name: Build Docker image
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Docker Buildx
19+
uses: docker/setup-buildx-action@v3
20+
21+
- name: Build image
22+
uses: docker/build-push-action@v5
23+
with:
24+
context: .
25+
push: false
26+
tags: llm-safety-middleware:ci
27+
cache-from: type=gha
28+
cache-to: type=gha,mode=max
29+
30+
- name: Smoke-test health endpoint
31+
run: |
32+
docker run -d --name smoke-test \
33+
-e LLM_BACKEND_TYPE=ollama \
34+
-e LLM_BASE_URL=http://localhost:99999 \
35+
-e LLM_MODEL=test \
36+
-p 8000:8000 \
37+
llm-safety-middleware:ci
38+
sleep 10
39+
curl -f http://localhost:8000/health
40+
docker stop smoke-test

.github/workflows/tests.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main, master, develop]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
name: Python ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10", "3.11", "3.12"]
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
cache: pip
27+
28+
- name: Install uv
29+
run: pip install uv
30+
31+
- name: Install dependencies
32+
run: uv pip install --system -r requirements.txt
33+
34+
- name: Download spaCy model
35+
run: python -m spacy download en_core_web_sm
36+
37+
- name: Run tests
38+
run: pytest test_safety_pipeline.py -v --tb=short
39+
40+
- name: Upload coverage
41+
if: matrix.python-version == '3.11'
42+
run: |
43+
uv pip install --system pytest-cov
44+
pytest test_safety_pipeline.py --cov=llm_safety_pipeline --cov=api_server \
45+
--cov-report=xml --tb=short -q
46+
47+
- name: Upload coverage to Codecov
48+
if: matrix.python-version == '3.11'
49+
uses: codecov/codecov-action@v4
50+
with:
51+
files: coverage.xml
52+
fail_ci_if_error: false
53+
54+
lint:
55+
name: Lint & Format
56+
runs-on: ubuntu-latest
57+
58+
steps:
59+
- name: Checkout code
60+
uses: actions/checkout@v4
61+
62+
- name: Set up Python
63+
uses: actions/setup-python@v5
64+
with:
65+
python-version: "3.11"
66+
cache: pip
67+
68+
- name: Install linters
69+
run: pip install black isort flake8
70+
71+
- name: Check formatting (black)
72+
run: black --check --diff .
73+
74+
- name: Check import order (isort)
75+
run: isort --check --diff .
76+
77+
- name: Lint (flake8)
78+
run: |
79+
flake8 llm_safety_pipeline.py api_server.py \
80+
--max-line-length=99 \
81+
--extend-ignore=E203,W503

.gitignore

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# ── Python ────────────────────────────────────────────────────────────────
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
*.pyd
7+
8+
# Distribution / packaging
9+
dist/
10+
build/
11+
*.egg-info/
12+
*.egg
13+
MANIFEST
14+
.eggs/
15+
wheels/
16+
17+
# Virtual environments
18+
venv/
19+
.venv/
20+
env/
21+
.env/
22+
ENV/
23+
24+
# ── Testing & coverage ────────────────────────────────────────────────────
25+
.pytest_cache/
26+
.coverage
27+
.coverage.*
28+
coverage.xml
29+
htmlcov/
30+
*.lcov
31+
nosetests.xml
32+
pytest_cache/
33+
34+
# ── Type checkers ─────────────────────────────────────────────────────────
35+
.mypy_cache/
36+
.dmypy.json
37+
dmypy.json
38+
.pytype/
39+
.pyre/
40+
41+
# ── Linters ───────────────────────────────────────────────────────────────
42+
.ruff_cache/
43+
44+
# ── Secrets & environment ─────────────────────────────────────────────────
45+
.env
46+
.env.*
47+
!.env.example
48+
*.pem
49+
*.key
50+
51+
# ── Runtime outputs ───────────────────────────────────────────────────────
52+
logs/
53+
*.log
54+
safety_reports/
55+
test_reports/
56+
57+
# ── Demo artifacts ────────────────────────────────────────────────────────
58+
demo_safety_config.json
59+
60+
# ── HuggingFace / model cache ─────────────────────────────────────────────
61+
*.bin
62+
*.safetensors
63+
models/
64+
model_cache/
65+
66+
# ── Jupyter ───────────────────────────────────────────────────────────────
67+
.ipynb_checkpoints/
68+
*.ipynb
69+
70+
# ── IDEs ──────────────────────────────────────────────────────────────────
71+
.idea/
72+
.vscode/
73+
*.swp
74+
*.swo
75+
*~
76+
77+
# ── OS ────────────────────────────────────────────────────────────────────
78+
.DS_Store
79+
.DS_Store?
80+
Thumbs.db
81+
ehthumbs.db
82+
desktop.ini
83+
84+
# ── Docker ────────────────────────────────────────────────────────────────
85+
# Keep Dockerfile and docker-compose.yml tracked; ignore runtime volumes
86+
docker-volumes/
87+
88+
# ── Claude Code (internal) ────────────────────────────────────────────────
89+
.claude/

CHANGELOG.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
---
9+
10+
## [Unreleased]
11+
12+
---
13+
14+
## [2.0.0] — 2026-02-24
15+
16+
### Breaking Changes
17+
18+
- **Architecture change**: local GPT-2 generator completely removed; the pipeline
19+
is now a pure **async HTTP proxy** to a remote LLM backend (Ollama,
20+
OpenAI-compatible, or custom).
21+
- `SafetyConfig` — removed fields: `llm_model_name`, `max_new_tokens`,
22+
`temperature`, `top_p`, `batch_size`, `max_cache_size`. These are now
23+
configured via `LLMBackendConfig` / environment variables.
24+
- `SafetyPipeline.process()``generate=` keyword argument removed.
25+
`process()` is now input-only (sync, no LLM call).
26+
27+
### Added
28+
29+
- `LLMBackendConfig` dataclass with `from_env()` class method — supports
30+
`ollama`, `openai`, and `custom` backend types.
31+
- `ExternalLLMClient` — async `httpx`-based HTTP client with exponential-backoff
32+
retry (does not retry on 4xx errors).
33+
- `SafetyPipeline.async_process()` — full async pipeline:
34+
input safety checks → remote LLM → output safety checks.
35+
- `SafetyPipeline.async_close()` — graceful shutdown of the underlying
36+
`httpx.AsyncClient`.
37+
- `SafetyPipeline.backend_health()` — async probe of the remote LLM endpoint.
38+
- `RejectionReason.BACKEND_UNAVAILABLE` and `RejectionReason.INPUT_TOO_LONG`.
39+
- `SafetyConfig.max_prompt_length` (default 10 000 characters) — input length guard.
40+
- `GET /health/backend` API endpoint — probes the remote LLM reachability.
41+
- `POST /api/v1/statistics/reset` now requires `X-API-Key: <ADMIN_API_KEY>` header.
42+
- Full async test suite: 74 tests using `pytest-asyncio` and `respx` for httpx mocking.
43+
- `pyproject.toml`, `LICENSE`, `.gitignore`, `.env.example`, `CONTRIBUTING.md`,
44+
`CHANGELOG.md`, GitHub Actions CI workflows.
45+
46+
### Changed
47+
48+
- CPU-bound PyTorch/spaCy inference offloaded to a thread-pool executor so the
49+
asyncio event loop stays unblocked.
50+
- `SafetyReport` check-passed flags changed to `Optional[bool]``None` means
51+
the check was not run (disabled or not reached), preventing false "passed" signals.
52+
- `get_statistics()` now returns a `copy.deepcopy()` to prevent callers from
53+
mutating live counters.
54+
- `device` in `SafetyConfig` now uses `field(default_factory=...)` to detect
55+
CUDA safely at construction time.
56+
- Safe-context regex no longer bypasses all downstream layers — it now defers to
57+
the semantic classifier for a final verdict.
58+
- `SemanticSafetyClassifier` correctly handles `unitary/toxic-bert`'s multi-label
59+
output (sigmoid + `probs.max()`) instead of binary softmax.
60+
- CORS: `allow_credentials=False`; origins opt-in via `ALLOWED_ORIGINS` env var.
61+
- FastAPI `@app.on_event` replaced with `@asynccontextmanager lifespan`.
62+
63+
### Fixed
64+
65+
- `_has_prohibited_entities` now checks both NER labels and plain text, fixing
66+
false-negatives for entities spaCy did not label.
67+
- `_has_harmful_instruction_pattern` uses dependency-tree traversal instead of
68+
a flat token scan, reducing false-positives.
69+
- `add_custom_filter` now properly raises on duplicate filter names.
70+
- `subprocess.run` in model download helper uses `sys.executable` so the correct
71+
Python is invoked inside virtual environments.
72+
73+
---
74+
75+
## [1.0.0] — 2024-01-01
76+
77+
### Added
78+
79+
- Initial release: multi-layer LLM safety pipeline with local GPT-2 generation.
80+
- `TokenLevelFilter`, `RuleEngineFilter`, `SemanticSafetyClassifier`, `SafeLLMGenerator`.
81+
- `SafetyPipeline.process(prompt, generate=True/False)`.
82+
- FastAPI server with `/check`, `/generate`, `/statistics` endpoints.
83+
- Docker + docker-compose support.
84+
85+
---
86+
87+
[Unreleased]: https://github.com/SahilChachra/LLM-Safety-Middleware/compare/v2.0.0...HEAD
88+
[2.0.0]: https://github.com/SahilChachra/LLM-Safety-Middleware/compare/v1.0.0...v2.0.0
89+
[1.0.0]: https://github.com/SahilChachra/LLM-Safety-Middleware/releases/tag/v1.0.0

0 commit comments

Comments
 (0)