[Feature]: Presidio output_parse_pii — derive placeholders from the value so tokens stay stable across requests
The Feature
With output_parse_pii: true, the Presidio guardrail builds its unmask table in
request_data["metadata"]["pii_tokens"] during the pre_call hook and numbers
placeholders with a counter that starts over on every request. Unmasking is
then a literal string replace against that table
(PresidioPII._unmask_pii_text, called from _process_response_for_pii).
That makes tokens meaningful only inside the single request that produced them. In a multi-turn or agentic conversation the message list grows between requests, so the same value is assigned a different index each time:
request 1 (3 messages): Alice Brenner -> <PERSON_2>
request 2 (7 messages): Alice Brenner -> <PERSON_5>Whenever the model emits a placeholder it picked up from earlier context rather
than from the current request's masked text — quoting its own previous turn,
echoing a tool result the client stored, or copying a block it was asked to
revise — the token is not a key in the current table. _unmask_pii_text finds
no match and passes it through verbatim. The client receives <PERSON_2> as
literal text.
This is worse than a cosmetic glitch for agentic use. Once such a string is written to a file, a wiki page or a vector store, it is permanently unresolvable: nothing records what it stood for, and on the next read it is no longer PII, so it is never masked or restored again. We hit exactly this — a knowledge-base agent persisted placeholders into its notes, and the original values are not recoverable.
Request: derive the placeholder suffix deterministically from the entity
value instead of from a per-request counter, e.g. the first few hex characters
of a keyed hash of (entity_type, value):
Alice Brenner -> <PERSON_a91c4f> # identical in every requestA stable derivation means:
- a token stays valid across requests, so late or quoted placeholders still resolve;
- two different values can never collide onto one token;
- the mapping does not have to be carried anywhere — it can be recomputed.
A config flag (presidio_stable_tokens: true, defaulting to the current
behaviour) would keep this backwards compatible for anyone parsing the
<TYPE_N> shape.
Motivation, pitch
Masking that cannot be reliably reversed is worse than no masking for agent workloads: it silently corrupts the content the agent produces, and the damage is permanent once persisted. Deterministic tokens remove the entire class of failure without changing the guardrail's protection properties — the provider still only ever sees an opaque placeholder.
Related: #31959 describes the same root cause within a single request (per-message counters colliding). A value-derived token fixes that case too, and additionally covers the cross-request case, which no within-request numbering fix can address.
LiteLLM version
1.99.1
Are you a ML Ops Team?
No
Twitter / LinkedIn details
No response
Source: BerriAI/litellm