A PII-aware LLM proxy that transparently detects and redacts sensitive information before requests reach the LLM provider.
- Anonymize: Before reaching the LLM provider, sensitive input from the client is parsed and replaced with opaque placeholders using the open-source Presidio framework. The request content is checked against your configured entity patterns and built-in presets.
- Forward: The anonymized request is forwarded to the upstream LLM provider.
- Deanonymize: When the response comes back, placeholders are replaced with the original values to preserve context.
Client ──request──► PIILLMShield (127.0.0.1:8888) ──redacted request──► LLM provider
Client ◄──restored─ PIILLMShield (127.0.0.1:8888) ◄──response────────── LLM provider
| Provider | Format |
|---|---|
| OpenAI | messages format, choices[].delta streaming |
| OpenRouter / Mistral / vLLM | OpenAI-compatible |
| Anthropic | content_block_delta streaming |
| Gemini | contents format, candidates[].parts streaming |
| Ollama | native message.content |
| Mode | Description |
|---|---|
| DEFAULT | All built-in recognizers (shipped with Presidio, based on the configured language) |
| HYBRID | Built-in recognizers + custom recognizers |
| CUSTOM | Only allowed built-in recognizers + custom recognizers |
Detection methods:
- Regex-Based: Pattern matching with custom regular expressions.
- Wordlists: Deny-list matching for specific terms.
- NLP-Based: Contextual entity recognition using spaCy for natural language processing.
A web dashboard is available to manage providers, entities, and analyzer configuration. A simulator is also provided to test custom recognizers.
- Anonymized inputs: Total number of PII entities detected and replaced with placeholders across all requests.
- Deanonymized OK: Number of LLM responses where all placeholders were successfully replaced back to their original values (input-level success).
- Deanonymized failures: Number of LLM responses where some placeholders could not be replaced (e.g., the LLM modified or invented a placeholder that doesn't exist in memory).
- Placeholders replaced: Total count of individual placeholders successfully swapped back to originals across all responses.
- Placeholders failed: Total count of individual placeholders that remained unreplaced across all responses.
git clone https://github.com/froyo75/PIILLMShield.git
cd PIILLMShieldCreate a new Python virtual environment, activate it, and install the dependencies:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtDownload the spaCy NLP model (required for entity recognition):
# Default model (recommended for production, ~560MB)
python -m spacy download en_core_web_lg
# Lightweight alternative (faster, lower recall, ~12MB)
python -m spacy download en_core_web_sm
# Transformer model (best quality, slow on CPU, ~450MB)
python -m spacy download en_core_web_trfNote: If using the
spacyengine (default), only the model set innlp_model(e.g.,en_core_web_lg) is required. Thetokenizer_model(e.g.,en_core_web_sm) is only needed if you switch to thetransformersengine.
Build the UI from the ui directory:
cd ui
npm install
node scripts/gen-icons.mjs # generate PWA icons
npm run build
cd ..The built UI (
ui/dist/) is served automatically by the Python backend.
Use the provided .env.example file to create your .env configuration, then start the app.
cp .env.example .envStart the shield proxy:
python main.pyThe dashboard is now available at http://127.0.0.1:8888.
Configure your favorite LLM client to use the proxy. The base URL depends on the client:
- Most clients (Open WebUI, LibreChat, OpenAI SDK, etc.) set the base URL to
http://127.0.0.1:8888(/v1is appended automatically). - Some clients (OpenCode with
@ai-sdk/openai-compatible) set the base URL tohttp://127.0.0.1:8888/v1.
{
"$schema": "https://opencode.ai/config.json",
"model": "pii-proxy/z-ai/glm-5.2",
"provider": {
"pii-proxy": {
"npm": "@ai-sdk/openai-compatible",
"name": "PII Proxy",
"options": {
"baseURL": "http://127.0.0.1:8888/v1"
},
"models": {
"z-ai/glm-5.2": {}
}
}
},
"share": "disabled",
"experimental": {
"openTelemetry": false
},
"permission": {
"edit": "ask",
"bash": "ask",
"webfetch": "ask",
"tools": {
"write": "ask",
"patch": "ask"
},
"external_directory": "deny"
}
}Build and start the app using Docker Compose:
docker compose up -d
