Skip to the content.

How PrivAiTe detects PII locally, with Presidio and OpenAI’s privacy-filter model

PrivAiTe uses two detection engines that can run together or separately.

Presidio (Microsoft): regex + spaCy NER

The default engine. Handles structured PII through pattern matching and basic NER.

What it detects How
Emails Regex
Phone numbers Regex + international format validation
Credit cards Regex + Luhn checksum
IBAN Regex + checksum validation
IP addresses Regex
US SSN Regex + format validation
Person names (capitalized, 2+ words) spaCy NER, only kept if all words are capitalized
Person names (lowercase or single word) Contextual regex, only after “je m’appelle X”, “my name is X”, “ich heiße X”, “Nom: X”, etc. A cue that only means “I am” (“je suis”, “I’m”) additionally requires a capitalized name
Dates (FR/DE) Custom regex, “15 mars 1987”, “3. März 1990”, under a French or German configuration
Structured secrets (0.5.0) PrivAiTe patterns for credential assignments, URI passwords and Authorization bearer values

Presidio is faster than the contextual model and produces few false positives on the clean benchmark documents. It misses names that spaCy doesn’t recognize and arbitrary passwords without a recognized field name or URI/header structure.

OpenAI Privacy Filter: contextual ML model

OpenAI’s open-source PII model (1.5B params, 50M active, Apache 2.0). Runs locally via ONNX Runtime (about 900MB, no PyTorch needed).

What it adds over Presidio How
Person names (any format, any case) ML NER, understands context, not just capitalization
Passwords and secrets Detects “SuperSecret2024!”, API keys like “sk-proj-…”
Account numbers Detects bank account numbers, policy numbers, etc.
Dates (all languages) ML-based, not limited to FR/DE regex

The Privacy Filter adds model-inference cost and occasionally flags technical identifiers as account numbers (e.g., “CMD-2024-98765”). It runs concurrently with Presidio, which handles structured formats while the Privacy Filter handles contextual NER. Cost depends on input length; the benchmark reports the combined engine latency.

Since 0.7.1, PrivAiTe slices the full tokenization into overlapping ONNX windows itself. Newer Transformers and tokenizers releases could silently return only the first window when asked for overflow, leaving the rest of a long request unscanned. Coverage checks now reject omitted tokens or source text before returning any detections. Tokenizers that add special tokens are refused at startup.

The model is fetched into the Hugging Face cache. Since huggingface_hub 1.33, that cache keeps the model and its weights file in two different folders, which ONNX Runtime refuses to load. PrivAiTe then hard-links the two files side by side under models--openai--privacy-filter/privaite-onnx/ in the same cache: no second download and no extra disk space, so a cache filled in advance for offline use keeps working. A copy is made only on a filesystem that refuses hard links, and deleting the model from the cache deletes that folder with it.

ONNX variants

Two exports of the model are supported, selected with onnx_variant (configuration). They hold the same 4-bit weights; q4 computes in fp32 and q4f16 in fp16. On the earlier comparative benchmark (120 AI4Privacy documents, since replaced by the 8-corpus one), CPU execution provider:

Variant Recall Recall (strict) False positives Gretel recall One 1,024-token window Peak memory
q4 (default) 85.2% 81.7% 2 / 14 62.9% 776 ms 6.3 GB
q4f16 84.9% 81.0% 2 / 14 62.9% 1,118 ms 5.3 GB

q4 runs a window 1.4x to 1.6x faster than q4f16 (1,024 and 512 tokens), and the whole benchmark finished faster with it in every run. The window figures come from one process that alternates the variants, so machine load cannot favour one of them; the full benchmark time also includes Presidio and moves with the load, so the seconds are in the full report and the ratio is what to read.

246 of the 256 anonymized outputs (benchmark, clean documents, Gretel finance corpus, long code and log inputs) are identical between the two. The 10 that differ are span boundaries moving in both directions; no labelled value is lost, and q4 catches one more. Measured on an Apple Silicon CPU; x86 and GPU sessions were not measured, and fp16 is where a GPU is expected to favour q4f16.

The int8 quantized export is faster still but lost 13 labelled values for 8 gained, doubled the false positives and peaked at 8.8 GB, so it is not recommended.

Why two engines?

Neither is perfect alone:

Presets

Preset What runs Direct identifiers* Precision Latency Secrets
onnx (default) Presidio + Privacy Filter 72.7% 66.5% 196 ms yes
light Presidio + built-in rules 44.8% 64.1% 60 ms structured formats
max onnx + GLiNER 82.0% 49.7% 447 ms yes
minilm (opt-in) Presidio + PrivAiTe MiniLM on CPU 82.9% 83.5% 88 ms yes
ultrafast (opt-in) Presidio without spaCy + PrivAiTe MiniLM on the Neural Engine (macOS) 82.7% 85.4% 17 ms yes

*Share of direct identifiers fully masked, macro average over the 3 public corpora of privaite-bench that PrivAiTe’s model never trained on (627 English documents, benchmark); precision is the share of masked characters that belong to a labeled entity. Latencies are medians per document on an Apple M1 Pro, on CPU except ultrafast, not large agent-request latency guarantees. onnx, light and max were measured on 0.7.2, and 0.8.0 leaves their detection unchanged. max adds GLiNER: more identifiers masked than onnx, at lower precision, more than twice the latency 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 combines contextual recognition with structured rules. light uses Presidio and the same structured-secret rules; it has no contextual Privacy Filter model, and needs no model download beyond the spaCy language models. It misses names that no cue introduces when the text is not in the first configured language: privaite verify --preset light reports the English name of its own demo as leaked.

Footgun: do not pin detectors.presidio.entities to a short allowlist on the light path. It restricts detection to only those types and cuts recall sharply. Leave entities unset; the proxy logs a warning at startup if it detects a low-recall configuration.

PrivAiTe MiniLM (opt-in)

The minilm and ultrafast presets replace the Privacy Filter with crp4222/privaite-pii-minilm, a multilingual MiniLM (117M parameters, 15 types) trained for this proxy on 17 public datasets whose licenses allow commercial use. Presidio, the structured-secret rules and the entity allowlist stay as under onnx. Nothing changes unless you set one of them; how to is in the configuration reference.

Measured through PrivAiTe’s engine on the 3 corpora of privaite-bench the model never trained on: TAB, PIILO and Privy, 627 English documents (CPU threads 8, Apple M1 Pro):

Preset Direct identifiers fully masked Precision Median per document Per 1,000 characters
onnx (default) 72.7% 66.5% 196 ms 128 ms
minilm 82.9% 83.5% 88 ms 49 ms
ultrafast 82.7% 85.4% 17 ms 11 ms

Direct identifiers are names, emails, phone numbers, IDs, card and account numbers, secrets and addresses. The model trained on the train splits of the benchmark’s 5 other corpora (Nemotron-PII, OpenPII, both Gretel sets and MAPA), so its scores there are left out. The latencies are the published privaite-bench runs. The model card gives its scores on nine test sets from sources it never saw, in several languages, against five other open PII models.

What changes in practice:

What’s NOT detected by default

The default onnx preset does detect personal addresses (as LOCATION) and personal URLs (as URL) through the Privacy Filter model, and replaces them. What stays off by default are Presidio’s broad recognizers for those types, because they cause heavy false positives:

The light preset has no contextual Privacy Filter model. 0.5.0 adds the same structured-secret rules to every preset that enables Presidio. Broad password recognition still requires an ML detector; these rules do not make light equivalent to onnx.

Structured credentials and overlapping types

The new rules supplement detection; they never short-circuit the NLP engines. They recognize common assignment names (api_key, apiKey, access_token, refresh_token, auth_token, client_secret, password, passwd, smtp_secret, presented_key) and underscore-prefixed environment variants such as OPENAI_API_KEY and DB_PASSWORD, case-insensitively. They also recognize passwords in scheme://user:password@host and plaintext Authorization: Bearer ... headers.

Since 0.7.2, three more shapes are covered, because the model missed them on some .env files:

Since 0.7.1, these rules also cover credentials passed as command-line options: --password, --passwd, --api-key, --client-secret, the token options and --mot-de-passe, with an optional prefix (--db-password), and quoted or unquoted values separated by whitespace or =. What follows the option in help text or prose is not a value and is left alone: a metavariable (<value>, PASSWORD), a variable reference ($DB_PASSWORD, which stands for the secret and must survive for the command to work) or a plain word (“use --password to set it”). Commands inside JavaScript or JSON strings are scanned with their quotes and backslashes decoded, up to two string layers. Detected values are replaced at their original source positions, so restoration preserves the command’s exact syntax, including escaped quotes. Other encoded payloads remain outside these rules.

Since 0.7.1, a credential these rules identified is also treated as a secret wherever the same request repeats it afterwards, whatever surrounds it. An agent that was handed a restored password repeats it bare, in its reasoning, in the next command or in a tool output, and re-detecting it there is not reliable. This is deliberately narrow:

Disabling StructuredSecretRecognizer disables the propagation with it.

For quoted assignments, only the value is replaced: api_key="demo-only" becomes api_key="[SECRET]" with the shipped redaction policy. Escaped quotes are included in the value. Bare values extend to whitespace; punctuation may be part of a password and is not stripped. Use quotes when a comma or brace must be unambiguously preserved as syntax.

On .env files and shell exports, the contextual model’s spans do not always follow the values (fitted to the line since 0.7.2): a password runs on through the host and into the next line, or a fragment of a variable name is flagged by itself. Before merging, a model span of type SECRET, URL or EMAIL_ADDRESS is therefore fitted to the line: it is cut where the next line assigns an upper-case name (behind the same line numbers and markers as above), a piece that stays inside such a name is dropped, a span that starts in the name is cut to the value, and a password in a URI loses the : before it and stops at the @ that closes it. With the shipped config, DATABASE_URL=postgres://shop:demo-pass-4821@db.internal:5432/shop goes out as DATABASE_URL=postgres://shop:[SECRET]@db.internal:5432/shop. Only structure is given back, never a character of a value, and a name that itself looks like a token (one long word with a digit) is not treated as a name. Lower-case names are left as the detector returned them: user=tag@example.com is also one valid address.

Overlapping detections now respect policy before confidence: a blocked type wins over other types; a type configured with redact or mask wins over a reversible type. Equal-policy matches use the configured overlap resolution. The union still covers every detected character, so an overlapping EMAIL detection can widen a URI password’s redaction to include the host. The cache fingerprint includes this policy; it cannot replay an obsolete winner.

These changes shipped in 0.5.0; 0.4.x does not have them. The earlier agent-session measurements remain historical results; an offline replay of the synthetic fixture is not a new live-agent measurement. The regression replay report contains the before/after counts, individual timings and reproduction commands.

Experimental model evaluation

The Privy and Kiji benchmark checks the 0.5.0 source on 300 synthetic protocol traces, the existing 120-document multilingual corpus, clean inputs, and long-log regressions. Kiji is an adapter in the benchmark repository, not a supported PrivAiTe preset.

On Privy’s 491 annotated spans, the current onnx stack fully removes 258 (52.55%). Replacing Privacy Filter with the tested Kiji ONNX artifact while keeping the same Presidio configuration removes 147 (29.94%), with lower character precision and lower latency. Password coverage drops from 12/15 to 2/15. These results support retaining the current default; they also expose its remaining misses, including three passwords in SQL traces. A successful replay of the planted log credentials does not establish detection of arbitrary protocol data. The report gives per-type counts, artifact limitations, latency, and the results of adding Kiji alongside the existing engines.

Known limitations