Drop-in self-hosted LLM proxy that reversibly redacts PII before LLM calls, including tool-call arguments and multimodal content
Project description
PrivAiTe
Self-hosted PII redaction proxy for LLM APIs.
A drop-in LLM proxy that reversibly replaces PII before it reaches the provider, including inside tool-call arguments and multimodal content, with zero telemetry.
You type: "Je m'appelle Marie Dupont, email marie@acme.com"
LLM sees: "Je m'appelle <PERSON_1>, email <EMAIL_ADDRESS_1>"
LLM says: "Bonjour <PERSON_1>, votre email <EMAIL_ADDRESS_1> est noté."
You see: "Bonjour Marie Dupont, votre email marie@acme.com est noté."
PrivAiTe sits between your app and the model provider. It finds names, emails, phones, cards, IBANs, secrets and more, swaps them for stand-ins before anything leaves your machine, and puts the real values back in the reply. Most tools scan only the plain message text; agent traffic hides PII inside tool-call JSON, and that is the gap PrivAiTe closes. Detection runs locally (two engines, Presidio + OpenAI's open privacy-filter model), and the engine runs three ways: standalone proxy, Open WebUI filter, or LiteLLM guardrail.
This is local pseudonymization, not anonymization, and detection is best-effort rather than a guarantee. You remain the data controller. The Threat model spells out exactly what it protects against and what it does not.
Quick start
Docker (fastest): the detection model is baked in, so it runs offline from the first request.
docker run -d -p 8400:8400 \
-e PRIVAITE_API_KEYS=change-me \
-e OPENAI_API_KEY=sk-... \
ghcr.io/crp4222/privaite
The same image is on Docker Hub too: swap the last line for crp4222/privaite if you prefer pulling from there.
Two keys, two roles: PRIVAITE_API_KEYS is the key your client sends to PrivAiTe (pick any value); OPENAI_API_KEY is your real provider key, which stays in the container and never reaches your client. This exposes gpt-4o-mini and gpt-4o; for any other provider (Ollama, Azure, anything LiteLLM supports), mount a config: configuration.
pip:
pip install privaite
# One spaCy model per scanned language; the default preset scans EN + FR.
python -m spacy download en_core_web_lg && python -m spacy download fr_core_news_md
cat > privaite.yaml <<'EOF'
providers:
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: ${OPENAI_API_KEY}
pii:
enabled: true
preset: onnx # or "light": faster, no model download, classic PII only
EOF
PRIVAITE_API_KEYS=change-me python -m privaite --config privaite.yaml
Connect: point any OpenAI-compatible client at http://localhost:8400/v1 with the key change-me. For Open WebUI: Admin → Settings → Connections → OpenAI API, URL http://localhost:8400/v1 (or http://host.docker.internal:8400/v1 if Open WebUI runs in Docker), key = your PRIVAITE_API_KEYS value. Client snippets (curl, Python, Node) are in examples/. Prefer no separate proxy? Use the in-process Open WebUI filter.
Benchmark
Measured on 120 real documents from the open AI4Privacy pii-masking-200k dataset (458 PII items, labeled by 10 independent auditor agents and cross-checked against the dataset's own mask) across DE, EN, FR, IT, plus 14 clean documents for false positives.
| Solution | Recall (span) | Recall (strict) | False positives | Tool-call protection |
|---|---|---|---|---|
onnx (default) |
84.5% | 80.6% | 2 / 14 | 100% |
light (full Presidio) |
62.4% | 57.9% | 3 / 14 | 100% |
| LiteLLM Presidio guardrail | 70.3% | 65.3% | 3 / 14 | 0.0% |
| LLM Guard (Anonymize) | 76.9% | 74.9% | 5 / 14 | 0.0% |
Read the 100% precisely, it is structural, not absolute: of the PII PrivAiTe detects in plain text, 100% is also removed from tool-call JSON. End to end, its tool-call leak equals its detection misses (15.5% on this corpus with the onnx preset), the same misses flat text has.
Two honesty notes, both favoring caution. LLM Guard's detection model is fine-tuned on the exact dataset behind this corpus, so its recall here is optimistic; PrivAiTe's default model is not (OpenAI's model card states it did not train on it). An out-of-distribution cross-check on two independent corpora confirms the default generalizes: ~84% held on Gretel finance text while the AI4Privacy-tuned model drops to ~62% (OOD_COMPARISON.md).
Per-language and per-entity tables, competitor configs, methodology, reproduction: privaite-bench. Feature comparison: docs/comparison.md.
Presets
| Preset | What runs | Recall* | False positives | Latency | Secrets |
|---|---|---|---|---|---|
onnx (default) |
Presidio + Privacy Filter | 84.5% | 2 / 14 | ~0.5s | yes |
light |
Presidio only | 62.4% | 3 / 14 | ~60ms | no |
max |
onnx + GLiNER | higher OOD | more | ~0.7s | yes |
*Span recall on the AI4Privacy benchmark above. max adds GLiNER (trained on data independent of AI4Privacy): on out-of-distribution corpora it raises recall by several points at the cost of more false positives and a torch dependency (pip install 'privaite[gliner]'); with it selected but not installed, the proxy fails at startup with an install hint rather than silently degrading.
onnx = maximum coverage: secrets, passwords, API keys, unusual names, addresses. light = fastest, near-zero false positives, classic PII only, no addresses/URLs/secrets. How the engines work and what stays off by default: docs/detection.md.
Footgun: do not pin
detectors.presidio.entitiesto a short allowlist on thelightpath. It restricts detection to only those types and roughly halves recall (to ~35%). Leaveentitiesunset; the proxy logs a warning at startup if it detects a low-recall configuration.
What it scans
Before anything is forwarded: messages[].content (plain string or multimodal text parts), tool_calls[].function.arguments and the legacy function_call.arguments (parsed as JSON, scrubbed value by value including bare numeric leaves, keys and function names intact), /v1/completions prompt and suffix, /v1/embeddings input, chat prediction.content (predicted outputs) and web_search_options.user_location. On the way back, values are restored in content, tool calls, reasoning traces, refusals and audio transcripts, streaming included.
NOT scanned (know your surface): messages[].name, top-level user/metadata, tools definitions, JSON object keys. Keep PII out of those fields. Endpoints, strict mode and passthrough caveats: docs/api.md.
Threat model
PrivAiTe performs local pseudonymization, not guaranteed anonymization. Detection runs on your machine; the real ↔ placeholder mapping lives in memory only for the duration of a request and is dropped afterwards.
What it protects against: the LLM provider storing, training on, or logging your raw PII. The provider receives placeholders (<PERSON_1>, …) for everything the detector catches, across message content, tool-call arguments, and multimodal text.
What it does NOT protect against:
- PII the detector misses. Detection is statistical and never 100% (see the benchmark). A name it doesn't recognize reaches the provider. Treat the output as best-effort, not a guarantee.
- Re-identification from context. Even with names replaced, the surrounding text can stay identifying ("the CEO of
<ORG_1>who resigned in March"). - A compromised local machine. The mapping and raw text live in local memory; this is not a defense against a local attacker.
- The provider correlating requests within a session.
For GDPR/HIPAA: treat this as pseudonymization + transfer minimization, not anonymization. If you need irreversible removal, use method: "redact". Audit it on your own data: docs/verify.md.
Alternatives
Keeping PII out of LLM calls is a crowded space, and PrivAiTe is not always the right pick. Based on each project's public docs as of June 2026:
- LiteLLM has a built-in Presidio guardrail, the natural choice if you already run the LiteLLM proxy and want PII handling inline (there are a few open bugs around scrubbing requests and responses).
- Managed/cloud options exist too, such as Microsoft PII Shield and LangChain's gateway redaction.
Where PrivAiTe differs: it anonymizes PII inside tool-call arguments and multimodal content, not just message text (LangChain's gateway docs, for instance, note that tool-call arguments are not scanned), it restores the original values in the response, and it ships a reproducible benchmark. If your traffic is agentic or multimodal, that gap is the reason this exists.
Integrations
- Open WebUI filter (setup, hub listing): an Open WebUI Filter Function running the engine in-process, no separate proxy. Admin Panel → Functions → paste
integrations/openwebui/privaite_filter.py, enable, pick preset and languages in its valves. Covers message text, tool calls and multimodal. - LiteLLM guardrail (setup): a custom guardrail for teams already on the LiteLLM proxy. Mount
integrations/litellm/privaite_guardrail.pynext to yourconfig.yamlto anonymize requests and restore responses inline, including tool-call arguments, which LiteLLM's built-in Presidio guardrail does not scan.
Docs
Also browsable as a site: crp4222.github.io/PrivAiTe.
- How detection works: the two engines, what each catches, what stays off by default, known limitations
- Configuration reference: providers, Docker with custom config, anonymization methods,
block_entities, custom patterns, languages, pinned model revisions - API reference: endpoints, the exact scanned/unscanned surface, strict mode, passthrough caveats
- Verify what gets redacted: audit the proxy on your own data, dry-run inspect endpoint
- Feature comparison and the reproducible benchmark
- Changelog
Development
git clone https://github.com/crp4222/PrivAiTe && cd PrivAiTe
pip install -e ".[dev]"
python -m spacy download en_core_web_lg && python -m spacy download fr_core_news_md
cp .env.example .env # keys
cp config/privaite.example.yaml config/privaite.yaml # providers
python -m privaite --reload # dev mode (auto-reload)
python -m pytest tests/ -v
License
BSD 3-Clause. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file privaite-0.3.3.tar.gz.
File metadata
- Download URL: privaite-0.3.3.tar.gz
- Upload date:
- Size: 62.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97a66744aaeaa1f92f987db1f52d6670213f63213112e58a9be6a0ad1e5dadfa
|
|
| MD5 |
5ee246893964eddd1fa05f9d92e2b4b7
|
|
| BLAKE2b-256 |
fce856d31477616c5f6c61bb62b426bad9efb3a49081d1d059a865bdf0b2c514
|
Provenance
The following attestation bundles were made for privaite-0.3.3.tar.gz:
Publisher:
publish.yml on crp4222/PrivAiTe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
privaite-0.3.3.tar.gz -
Subject digest:
97a66744aaeaa1f92f987db1f52d6670213f63213112e58a9be6a0ad1e5dadfa - Sigstore transparency entry: 2202562029
- Sigstore integration time:
-
Permalink:
crp4222/PrivAiTe@9045f6dc45b4998fc81adcbb35e440a7d99f2c83 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/crp4222
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9045f6dc45b4998fc81adcbb35e440a7d99f2c83 -
Trigger Event:
release
-
Statement type:
File details
Details for the file privaite-0.3.3-py3-none-any.whl.
File metadata
- Download URL: privaite-0.3.3-py3-none-any.whl
- Upload date:
- Size: 69.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f251a91bf7a7416032632418ea7d16aedd62ce169c1fe1f050766ed683f7abe4
|
|
| MD5 |
99f7b56779582ad4e1b124e898022023
|
|
| BLAKE2b-256 |
db78fb2d442e5f6d2131293bf7763adf2ca5b3f98dd2ddb0c2084b6661e6cf65
|
Provenance
The following attestation bundles were made for privaite-0.3.3-py3-none-any.whl:
Publisher:
publish.yml on crp4222/PrivAiTe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
privaite-0.3.3-py3-none-any.whl -
Subject digest:
f251a91bf7a7416032632418ea7d16aedd62ce169c1fe1f050766ed683f7abe4 - Sigstore transparency entry: 2202562345
- Sigstore integration time:
-
Permalink:
crp4222/PrivAiTe@9045f6dc45b4998fc81adcbb35e440a7d99f2c83 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/crp4222
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9045f6dc45b4998fc81adcbb35e440a7d99f2c83 -
Trigger Event:
release
-
Statement type: