Metadata-Version: 2.4
Name: privaite
Version: 0.8.1
Summary: Drop-in self-hosted LLM proxy that reversibly redacts PII before LLM calls, including tool-call arguments and multimodal content
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/crp4222/PrivAiTe
Project-URL: Repository, https://github.com/crp4222/PrivAiTe
Project-URL: Documentation, https://crp4222.github.io/PrivAiTe/
Project-URL: Issues, https://github.com/crp4222/PrivAiTe/issues
Project-URL: Changelog, https://github.com/crp4222/PrivAiTe/blob/main/CHANGELOG.md
Keywords: privacy,pii,pii-redaction,redaction,anonymization,de-identification,data-masking,llm,llm-security,proxy,openai,chatgpt,gdpr,presidio,guardrails,self-hosted
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Framework :: FastAPI
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Internet :: Proxy Servers
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Natural Language :: English
Classifier: Natural Language :: French
Classifier: Natural Language :: German
Classifier: Natural Language :: Spanish
Classifier: Natural Language :: Italian
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.115.0
Requires-Dist: uvicorn[standard]>=0.30.0
Requires-Dist: sse-starlette>=2.0.0
Requires-Dist: litellm>=1.84.0
Requires-Dist: pydantic>=2.4
Requires-Dist: pyyaml>=6.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: presidio-analyzer>=2.2
Requires-Dist: presidio-anonymizer>=2.2
Requires-Dist: spacy>=3.7
Requires-Dist: onnxruntime>=1.17
Requires-Dist: transformers>=5.3
Requires-Dist: huggingface_hub>=0.23
Requires-Dist: faker>=25.0
Requires-Dist: click>=8.0
Provides-Extra: ml
Requires-Dist: torch>=2.6; extra == "ml"
Provides-Extra: gliner
Requires-Dist: gliner>=0.2.13; extra == "gliner"
Requires-Dist: torch>=2.6; extra == "gliner"
Provides-Extra: ane
Requires-Dist: coremltools>=9.0; sys_platform == "darwin" and extra == "ane"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: pytest-httpx>=0.30; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: coverage>=7.0; extra == "dev"
Requires-Dist: ruff==0.16.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Dynamic: license-file

# PrivAiTe

**Your coding agent reads your `.env` files, logs and command output, and everything it reads goes to the LLM provider.** PrivAiTe is a local proxy that replaces the secrets and personal data in every request before it leaves your machine, then puts the personal data back in the reply.

[![CI](https://github.com/crp4222/PrivAiTe/actions/workflows/ci.yml/badge.svg)](https://github.com/crp4222/PrivAiTe/actions) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-BSD--3--Clause-green.svg)](https://github.com/crp4222/PrivAiTe/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/privaite.svg)](https://pypi.org/project/privaite/)

```text
# your agent reads and sends:
OPENAI_API_KEY=sk-demo-fake000
DB_PASSWORD=demo-pass-4821
ADMIN_EMAIL=marie@example.com
# the provider receives:
OPENAI_API_KEY=[SECRET]
DB_PASSWORD=[SECRET]
ADMIN_EMAIL=<EMAIL_ADDRESS_1>
```

That is real engine output with the shipped config, not a mock-up. In the reply, `<EMAIL_ADDRESS_1>` becomes the real address again, streaming included. `[SECRET]` does not come back: the shipped configs redact secrets and mask card numbers for good, a [per-type setting](https://github.com/crp4222/PrivAiTe/blob/main/docs/configuration.md#entity-overrides-per-type-methods). Detection runs locally and is best-effort, not a guarantee: the [threat model](https://github.com/crp4222/PrivAiTe#threat-model) says what it does not cover.

Since 0.8.0, PrivAiTe also ships its own detection model, [`crp4222/privaite-pii-minilm`](https://huggingface.co/crp4222/privaite-pii-minilm), behind two opt-in presets: `ultrafast` runs it on the Apple Neural Engine at a median 17 ms per document, and `minilm` runs it on CPU anywhere. On the public corpora the model never trained on, both mask more direct identifiers than the default ([benchmark](https://github.com/crp4222/PrivAiTe#benchmark): 82.9% and 82.7% against 72.7%). The default does not change unless you pick one.

## See it work in one command, no API key

```bash
docker run --rm \
  ghcr.io/crp4222/privaite:0.8.1 \
  python -m privaite verify
```

```text
  value                                          where                  direct   proxied
  Marie Dupont                                   message text             LEAK     clean
  marie.dupont@example.invalid                   tool-call argument       LEAK     clean
  4111 1111 1111 1111                            tool-call argument       LEAK     clean
  SERVICE_API_KEY=sk-demo-0000-not-a-real-key    tool output              LEAK     clean
RESULT: PASSED, every planted value was replaced before the request left.
```

It starts a throwaway provider on 127.0.0.1, sends the same agent-shaped request straight to it and then through PrivAiTe, and prints what each request body contained: every planted value shows `LEAK` when sent directly and `clean` through the proxy. About 20 seconds on a laptop CPU once the image is pulled (it carries the detection model and takes about 3 GB on disk). With pip, `privaite verify` does the same after the install below; its first run downloads the model (about 900 MB). It exits non-zero if a value leaks: [docs/verify.md](https://github.com/crp4222/PrivAiTe/blob/main/docs/verify.md).

## Quick start

**Docker:** the detection model is baked in, so it runs offline. `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.
```bash
docker run -d -p 8400:8400 \
  -e PRIVAITE_API_KEYS=change-me \
  -e OPENAI_API_KEY=sk-... \
  ghcr.io/crp4222/privaite:0.8.1
```

**pip:** the same proxy with the same shipped config, which exposes `gpt-4o-mini` and `gpt-4o`.
```bash
python -m pip install --upgrade "privaite>=0.8.1"
python -m spacy download en_core_web_lg && python -m spacy download fr_core_news_md
curl -fsSLO https://raw.githubusercontent.com/crp4222/PrivAiTe/v0.8.1/config/privaite.openai.yaml
OPENAI_API_KEY=sk-... PRIVAITE_API_KEYS=change-me python -m privaite --config privaite.openai.yaml
```

**Connect:** point any OpenAI-compatible client at `http://localhost:8400/v1` with the key `change-me` (from Open WebUI running in Docker: `http://host.docker.internal:8400/v1`). For Ollama, Azure or any other provider, [write your own config](https://github.com/crp4222/PrivAiTe/blob/main/docs/configuration.md) and [mount it in Docker](https://github.com/crp4222/PrivAiTe/blob/main/docs/configuration.md#docker-with-a-custom-config). The image is also on Docker Hub as `crp4222/privaite:0.8.1`, and client snippets are in [`examples/`](https://github.com/crp4222/PrivAiTe/tree/main/examples/).

## Claude Code and Codex

Gateway mode (opt-in, off by default) speaks the native protocols of agent CLIs. It scrubs each request, tool results and tool-call arguments included, relays the CLI's own auth upstream untouched, and restores the reply.
```yaml
gateway:
  enabled: true
  anthropic:
    base_url: "https://api.anthropic.com/v1"
```

Then run `ANTHROPIC_BASE_URL=http://localhost:8400 claude`. Also set `pii.detection_cache.enabled: true`: agent CLIs resend the whole conversation every turn, and the cache avoids re-scanning it (the [threat model](https://github.com/crp4222/PrivAiTe/blob/main/docs/threat-model.md) spells out its memory tradeoff). Codex support is beta; setup, scanned surface and limits are in [docs/gateway.md](https://github.com/crp4222/PrivAiTe/blob/main/docs/gateway.md). In wire-level captures of real sessions on a repository with 24 planted values, Claude Code sent 24 of 24 to its provider and Codex 20 of 24 when run directly. Through the gateway, none reached it on the small fixture and 2 of 24 did on a larger session: [the measurement](https://github.com/crp4222/PrivAiTe/blob/main/docs/agent-leak-measurement.md).

- **Measured, not promised.** Those numbers do not establish zero leaks on arbitrary agent traffic.
- **It protects the egress, not the agent.** The CLI keeps the real values in its own context and local transcripts.
- **The agent's own prompt is not scanned.** The `system` and `instructions` fields pass through as-is, and Claude Code puts your `CLAUDE.md` there.
- **The gateway routes are open.** They accept a request that carries no `PRIVAITE_API_KEYS` value, and the server binds `0.0.0.0` by default with no rate limit: bind it to localhost or keep the port off untrusted networks.

## What it does

- **It reads inside the JSON.** Tool-call arguments are parsed and scrubbed value by value; tool results and multimodal text parts are scanned like any message.
- **It restores the reply, streaming included**, in content, tool calls and reasoning.
- **It fails closed and stays local.** If detection errors, the request is blocked, not forwarded. Detection (Presidio plus OpenAI's open privacy-filter model) runs on your machine, with no telemetry: the [outbound connections](https://github.com/crp4222/PrivAiTe/blob/main/docs/configuration.md#outbound-connections) page lists what the dependencies tried to send and how each is turned off.

## Benchmark

| Solution | Direct identifiers fully masked | Precision | Latency (median) | Left in tool-call arguments |
|---|---|---|---|---|
| `minilm` (opt-in) | **82.9%** | 83.5% | 88 ms | **8.1%** |
| `ultrafast` (opt-in, Neural Engine) | 82.7% | **85.4%** | **17 ms** | 10.8% |
| `onnx` (default) | 72.7% | 66.5% | 196 ms | 13.5% |
| `light` | 44.8% | 64.1% | 60 ms | 56.8% |
| LiteLLM Presidio guardrail\* | 65.5% | 73.6% | 60 ms | 100% |
| LLM Guard (Anonymize) | 46.4% | 51.9% | 256 ms | n/a, library |

Macro average over the 3 corpora of [privaite-bench](https://github.com/crp4222/privaite-bench) that PrivAiTe's model never trained on, 627 English documents labeled by their authors: TAB (127 court judgments labeled by law students), PIILO (200 student essays labeled by humans) and Privy (300 synthetic API payloads). The benchmark's 5 other corpora are left out of this table because the model trained on their train splits; none of the other systems lists these 3 in its training data. **Direct identifiers** are names, emails, phone numbers, ID, card and account numbers, secrets and addresses; one counts only when every character of it is masked. **Precision** is the share of masked characters that belong to a labeled entity, on TAB and Privy (PIILO labels only part of its personal data). **Latency** is per document on an Apple M1 Pro, on CPU except `ultrafast`. **Left in tool-call arguments** is the share of the 37 direct identifiers in Privy's 40 JSON records that still reach the provider when the record is sent inside a tool-call argument, a small sample: LiteLLM's guardrail does not parse tool-call JSON, and LLM Guard is a library with no notion of a request. \*Its detection is Presidio's: the first three columns are Presidio 2.2.364 alone, in-process, without the guardrail's HTTP hop.

`onnx` and `light` were measured on 0.7.2; 0.8.0 and 0.8.1 leave their detection on these corpora unchanged. All 8 corpora, 25 systems, per-type tables and how to rerun them: [RESULTS.md](https://github.com/crp4222/privaite-bench/blob/main/RESULTS.md). Details on the model, false positives on code included: [detection](https://github.com/crp4222/PrivAiTe/blob/main/docs/detection.md#privaite-minilm-opt-in).

## Integrations

- **Open WebUI filter** ([setup](https://github.com/crp4222/PrivAiTe/blob/main/integrations/openwebui/README.md), [hub listing](https://openwebui.com/posts/privaite_pii_anonymizer_351aa088)): the engine in-process, no separate proxy. Open WebUI sends its title, tag and follow-up calls without running filters, so with a cloud model use the standalone proxy instead: it sees those calls too.
- **LiteLLM guardrail** ([setup](https://github.com/crp4222/PrivAiTe/blob/main/integrations/litellm/README.md)): a custom guardrail for teams already on the LiteLLM proxy. It anonymizes requests and restores responses inline, including tool-call arguments, which LiteLLM's built-in Presidio guardrail does not scan.

## Threat model

PrivAiTe does **local pseudonymization**, not anonymization: the real-to-placeholder mapping lives in memory for the duration of a request. It protects against the provider storing, training on or logging your raw values: for everything the detector catches, the provider receives placeholders, in message text, tool-call arguments and multimodal text. It does not protect against:

- **Missed detections.** Detection is statistical; a value it does not recognize reaches the provider (see the [benchmark](https://github.com/crp4222/PrivAiTe#benchmark)).
- **Unrecognized secret formats:** unknown field names, encoded or split values, bare values without their field context.
- **Re-identification.** The surrounding text can stay identifying ("the CEO of `<ORG_1>` who resigned in March"), and the provider can correlate requests within a session.
- **A compromised local machine.** The mapping and the raw text live in local memory.
- **A model inventing a value** instead of copying a placeholder: nothing can be restored then.
- **The agent itself, in gateway mode.** The CLI keeps the real values locally, and its own prompt is relayed unscanned.

For GDPR or HIPAA, treat this as pseudonymization plus transfer minimization; you remain the data controller. The full version, including what the optional detection cache keeps in memory: [docs/threat-model.md](https://github.com/crp4222/PrivAiTe/blob/main/docs/threat-model.md).

## Docs

- **Presets:** `onnx` is the default. `light` is faster and needs no model download, but it misses names the default finds: the demo above fails with `--preset light`. `minilm` and `ultrafast` are opt-in: they run PrivAiTe's own model in place of the Privacy Filter, `ultrafast` on the Apple Neural Engine for agent CLIs on a Mac. Nothing changes unless you set one. [Presets](https://github.com/crp4222/PrivAiTe/blob/main/docs/detection.md#presets)
- **Your own types and hard blocks:** regex `custom_patterns`, an `allow_list` for strings that only look like PII, a per-type fate, and `block_entities` for what must never leave. [Your policy, your types](https://github.com/crp4222/PrivAiTe/blob/main/docs/policy.md)
- **What is scanned and what is not:** the exact request fields. [API reference](https://github.com/crp4222/PrivAiTe/blob/main/docs/api.md#what-gets-anonymized)
- **Everything else:** [detection](https://github.com/crp4222/PrivAiTe/blob/main/docs/detection.md), [configuration](https://github.com/crp4222/PrivAiTe/blob/main/docs/configuration.md), [redacting PII before an LLM call](https://github.com/crp4222/PrivAiTe/blob/main/docs/redact-pii-before-llm.md), [changelog](https://github.com/crp4222/PrivAiTe/blob/main/CHANGELOG.md), and the same pages as a site: [crp4222.github.io/PrivAiTe](https://crp4222.github.io/PrivAiTe/). To develop: clone, `pip install -e ".[dev]"`, download the two spaCy models above, then `python -m pytest tests/`.

## License

BSD 3-Clause. See [LICENSE](https://github.com/crp4222/PrivAiTe/blob/main/LICENSE).
