- what is measured
- the ensemble the gateway compiles, scored at byte offsets
- the command
pistra guardrails eval- what a reference pins
- a HuggingFace commit, and the Hub's own digests
- where the graph runs
- in this process, or on a KServe v2 server
Six kinds of detector run in one ensemble: patterns with checksums, a transformer token classifier, a sequence classifier, embeddings scored against your own example sentences, a chat model handed a policy in prose, and an analyzer on another machine. A rule reads whichever of them it names, so an expensive one can run in shadow beside a cheap one that enforces.
The model is your choice, not a list we approve. Any checkpoint on the Hub is a reference plus a commit, verified against the Hub's own digests on the way in, and it runs in this process or on a KServe server with the GPU. What that costs you is the subject of this page: a number measured on your traffic, through the path the gateway actually runs.
Four stages after the checkpoint
A model on a hub carries precision and recall on its card, and those describe the model. What a rule acts on comes out of four stages after it. The tokenizer splits text into vocabulary items. The window scheme cuts a sequence past max_tokens into overlapping windows, 512 tokens with a stride of 14 by default, so an entity at a window edge is seen once with context on both sides. The decoder folds subword scores back into words and lands them on the request's own bytes. The threshold turns a score into a verdict.
pistra writes all four. A token-classification model computes logits and nothing else: the softmax, the BIO grouping, the subword fold and the byte arithmetic are the pipeline code HuggingFace writes in Python, which Presidio inherits by calling that pipeline and a bare graph call inherits none of. Because the decoding is pistra's, the number a rule acts on is the binary's rather than the checkpoint's, and it has to be measured that way.
The harness compiles what the gateway compiles
pistra guardrails eval runs labelled text through the same inspection a request gets. The config is compiled the way the gateway compiles it, models are fetched and verified, the native libraries load, the thresholds apply, and each case is presented as one chat turn. A case carries spans at byte offsets or a set of labels, and an empty label list is a clean text that counts against a detector which flags it. A case can also say where in the request it sits: an injection arriving inside a retrieved document is a tool_result case, and a config whose detectors read only user scores a miss on it.
| flag | what the run is then evidence of |
|---|---|
-sweep | The average precision, the area under the precision-recall curve with no threshold in it, which is the number two checkpoints are compared on. Then precision, recall and F1 at each 0.05, which is where the threshold of the one you keep is read off. |
-invariance | Whether the verdict depends on the language. The same meaning in every column of a row has the same right verdict in each, so a verdict that changes between them is a defect with no gold needed. |
-detector | One member's findings alone. Without it every finding counts, shadow-mode detectors included, because shadow is where a detector is measured. |
-types | Which entity types or labels are scored. A person-only corpus is not charged for the locations the ensemble also found. |
Spans are scored twice. Exact is the same bytes and the same type. Relaxed is any overlap of the same type, one finding per marked span, and it is what a redaction has to achieve. The gap between the two columns is the offset arithmetic's own error: a clitic swallowed, a comma kept, a surname dropped because the tokenizer glued a question mark to it. A tag on a case names a slice, and the report repeats its totals for each, so a change that breaks one hazard shows on that line and nowhere else.
The measurement happens before a rule may read the detector. mode: shadow runs a member and records what it found without letting policy act on it, so a deny threshold is chosen from the traffic the deployment carries. Run a detector in shadow mode, measure a detector.
Why it runs through the binary
Two lines of the Gulf-names report moved without the model changing. A name inside an English sentence went from 0.57 to 1.00 exact F1, and a name after a title from 0.90 to 1.00. The decoder moved both.
XLM-R's tokenizer keeps الشمري? as one word, and the subword it scored most confidently was the question mark, saying nothing is here. Under the default aggregation the punctuation decided the word and the name was lost. Punctuation at a word's edge is now a word of its own before any aggregation runs. A token-level F1 printed beside a checkpoint never shows that, which is the whole reason the measurement goes through the gateway.
Any checkpoint, pinned to a commit
pistra models pin OWNER/NAME resolves a HuggingFace repository to a commit and prints the declaration to paste. A gateway accepts a commit and refuses a branch, because a branch lets the artifact behind a detector change under a configuration that did not. On the first build each node lists that commit, takes the Hub's own digests, a sha256 for every LFS file and a git blob id for the rest, fetches the files and verifies every byte. The resolved manifest is cached under the commit, so a restart resolves with no network.
A gated repository or a mirror takes an endpoint following the HF_ENDPOINT convention that Artifactory and Nexus proxies present, and a token that is a credential like every other: a literal is refused wherever the document travels, and it is sent only to that endpoint, never to a host the download redirects to. An air-gapped host takes the cache directory copied across, or a file list written down with a sha256 and a size each. Either way the digests are what the gateway believes, and the host is only where the bytes are. A detector pointed at model: and tokenizer: paths directly still works, and makes the operator rather than the document the thing that vouches for the file. Pin a model from the Hub.
Nothing in the ensemble is a checkpoint you have to accept. The models pistra ships are declared the way yours are, on a commit with digests, and a rule picks between them one rule at a time. The numbers published beside them describe the build that runs them, which is what the reports are.
Where the graph runs
Because the decoding is pistra's, where the graph runs is a deployment question rather than a correctness one. Three transports, and the column that matters is the last.
| detector | tokenizer | inference | who decides the spans |
|---|---|---|---|
type: nlp | here | here | pistra |
type: nlp, with nlp.remote | here | on the server | pistra |
type: remote | on the server | on the server | the analyzer |
nlp.remote ships token ids to a box that has a GPU and gets logits back, over the KServe v2 predict protocol that Triton, KServe, Seldon and OpenVINO Model Server all serve. Only the tokenizer and the label map are fetched, the graph is never downloaded, and the repository need not hold one. Every piece of span arithmetic stays where the bytes are. A GPU is worth having on one box and not on every gateway replica, and the offsets deciding which bytes get redacted are worth keeping in the process holding them.
type: remote is the opposite trade. It speaks presidio-analyzer's /analyze, so an existing Presidio deployment is a detector with no adapter to write, and what comes back is that analyzer's idea of where an entity begins. Presidio counts offsets in code points and pistra counts bytes, so they are translated at that edge and nowhere else, and offsets: utf16 declares the third convention for an analyzer written in Java, JavaScript or .NET. The annotation record is Presidio's RecognizerResult throughout, so what the ensemble emits is valid input to presidio-anonymizer and the presidio-research evaluators.
The two native libraries, the tokenizer shim and ONNX Runtime, are both reached by dlopen. A deployment without either keeps every other tier and fails at startup with an error naming the missing one. There is deliberately no setting that uses a library if it happens to find one: that would make one configuration mean two different things on two machines, with nothing in the logs to say which you got.
Six types, one ensemble
| type | what it is | what it needs |
|---|---|---|
pii | patterns with checksums and context lift, generated from Presidio's recognizers | nothing. |
nlp | a transformer token classifier, for the entities a pattern cannot describe | a tokenizer, and a graph here or on a server |
classify | a sequence classifier: Prompt Guard, ProtectAI's injection DeBERTa, a moderation checkpoint | the same head as nlp |
embed | a handful of your own example sentences per topic, scored by cosine to the nearest | the same head as nlp |
llm | a chat model handed a policy in prose and a closed list of labels | a provider you already configured |
remote | an analyzer on another machine | a presidio-analyzer /analyze endpoint |
A rule names the members it reads, so an expensive detector can run in shadow beside a cheap one that enforces, and a schedule says whether anybody waits for it. Every detector that can enumerate what it emits does so, and the union is the vocabulary a configuration speaks: an alphabet entry or an MCP server's restore list naming a type nothing emits is refused at load rather than failing quietly at runtime. The guardrail pipeline, schedules.
What this does not do
- The gateway cannot check what a model server runs. With
nlp.remotethe pinned commit fixes the tokenizer and the labels. Whether the box returning the logits holds that commit's graph is a pairing you keep. - A remote analyzer decides its own spans.
type: remoteinherits the analyzer's boundaries along with its findings, and one with noentitiesfilter cannot say what it emits, so it leaves the vocabulary open. An open vocabulary refuses nothing. - Most Hub repositories hold no graph. They hold PyTorch weights. A repository without an ONNX export is refused with the
optimum-clicommand in the message, and running that export and pushing the result is yours. - A set nobody else can write for you. The public corpora are news and encyclopaedia text, with no chat turns, no Gulf names and no identifier beside a name. A hundred sentences in the register your users write is an afternoon's work, and it is the only number that is about your deployment.
- The harness scores text somebody labelled. It says what a detector does on the cases in the file. What it does on live traffic is a different question, and
mode: shadowis where that one is answered.
The argument
- Why the number comes from the binary. A checkpoint's card reports precision on the checkpoint. What enforces is that checkpoint behind a tokenizer, a window scheme, a decoder and a threshold. One rule about where a word ends, in the last of those, took a score from 0.57 to 1.00 with the model byte for byte unchanged.
- Calling a detector. Presidio, Lakera, Prisma AIRS, Guardrails AI, NeMo. A gateway can call any of them, and what comes back is a verdict about the string it handed over. Deciding on that verdict is still the job of whatever holds the connection.