Key takeaways
- On July 9, 2026, LLM Guard went read-only, about a year after Palo Alto Networks closed its acquisition of Protect AI. With Robust Intelligence at Cisco and Lakera at Check Point, the category’s three most prominent detection stacks now sit inside closed platforms, on roadmaps set by companies a hundred times their size. Consolidation, then abandonment. For an institution building sovereign AI, that is a vendor in the request path it cannot accept.
- border is FlowX.AI’s open-source replacement: two functions, scan_input and scan_output, 30 detectors in four tiers, 27 of which need nothing beyond a CPU and the base install. No GPU means no quota request, no driver matrix and no queue behind the model itself. Once the weights are cached, a scan runs with the network interface down, and the test suite proves it by making outbound socket calls raise.
- Every scan produces an evidence record you can sign with your own key: hashes, a per-detector attestation with model revision and weights checksum, the policy hash, the verdict. Never the user’s text.
- 26 languages, the 24 official languages of the European Union plus Turkish and Azerbaijani, every classifier scored separately in each one, with the weak rows published. Apache-2.0, open weights on Hugging Face, pip install flowx-border. This is 0.7.0 and we are saying so. What we want most is people trying to break the detectors.
TL;DR: On July 9, 2026, LLM Guard, the reference open-source library for scanning LLM inputs and outputs, was archived. Two months on it is still being downloaded about 174,000 times a month from PyPI. The archive came just under a year after Palo Alto Networks completed its acquisition of Protect AI, the library’s publisher. If you run LLMs inside your own perimeter, in a regulated industry, in a language that is not English, the number of maintained open-source options that pass the five checks below quietly went to zero. So we built the thing we needed and published it.
Sovereign AI is not a model you host. It is every check on the way in and out of that model running where you run it, on weights you can read, under a license nobody can revoke, producing evidence you keep.
What LLM Guard was, and why it mattered
If you have never had to put controls around a language model, the name means nothing, so it is worth saying plainly what was lost. LLM Guard was an open-source Python library, MIT licensed, published by Protect AI. You composed it out of scanners, one per risk, and ran them over a prompt before it reached the model and over the answer before it reached the user. Prompt injection, secrets in a prompt, toxicity, gibberish, banned topics and competitors, token limits, personal data replaced with placeholders on the way in. On the way out: bias, sensitive data, factual consistency against the context, relevance, unreachable links, output that had to be valid JSON. Twenty-seven scanners by the end. Two calls, scan_prompt and scan_output, each returning the sanitized text, a per-scanner pass or fail, and a risk score.
What made it essential was not that it was clever. It was that it was installable. One pip install put a full input and output inspection stack inside your own perimeter, with the models pulled from Hugging Face and cached locally, no API key, no gateway, no egress and no per-call fee. For anyone who had to answer an architecture review question of the form “what inspects the prompt, and where does it run”, LLM Guard was the answer that passed: a named, permissively licensed component with the risks it addressed mapping recognizably onto the OWASP Top 10 for LLM applications. It became the default reference in enterprise designs because the alternatives with comparable coverage were commercial platforms, and a platform is a vendor in the request path.
That is what makes the archive more than a project going quiet. A library at about 174,000 downloads a month is load-bearing in a lot of architectures. Its being read-only does not break anything today, and that is the trap: pip install llm-guard still works, so nothing fails until a dependency needs a security patch, a transformers release moves under it, or somebody asks which model revision produced a decision from eight months ago and there is nobody to ask. For a regulated institution, an unmaintained component in the request path is a finding waiting to be written up, not a convenience.
What happened to the category
Robust Intelligence went to Cisco in 2024. Lakera went to Check Point in 2025, in a deal announced in September; the price was not disclosed and was reported at roughly 300 million dollars. Protect AI went to Palo Alto Networks, and the acquisition closed on July 22, 2025. Eleven and a half months later, on July 9, 2026, Protect AI’s LLM Guard repository was archived. The code is still there and the MIT license still applies, and pip install llm-guard still works. There are no more commits, no more merged pull requests, no more issue triage, and the models it depended on are no longer maintained.
The category’s three most prominent detection stacks were acquired within roughly a year of each other, and each now sits inside a closed platform, on a roadmap set by a company a hundred times its size. The pattern is consolidation followed by abandonment: the library gets bought for the team and the platform around it, and the open-source project that made it worth buying goes read-only within the year. That is a reasonable outcome for those companies. It is a problem for a specific kind of buyer: a bank, an insurer or a public body that cannot send prompts and answers to a hosted vendor, has been told by its risk function to show which checks ran, and whose customers do not all write in English.
What sovereign AI needs from a guardrail
Sovereign is an overused word, so here is what we mean by it. An AI system is sovereign when the institution running it controls every component in the request path: the model, the checks around the model, the weights and code they run on, and the evidence they leave behind. A guardrail that phones home, or that a vendor can archive, breaks that on its own. Before the first line of code we wrote down what a library would have to do to keep it intact. Five things.
- Run where the model runs. Inside the perimeter, on a CPU. No gateway, no proxy, no API key held on our behalf.
- No network at scan time. Once the weights are cached, nothing leaves the machine. Not the text, not a hash, not a usage counter.
- Every language our customers write in, measured one at a time. Not translated from English and averaged into a number that hides the languages that fail.
- Evidence a risk function can archive. Which checks ran, on which model revisions, with what result, verifiable later, without the record becoming a second copy of the data it was protecting.
- A license and weights that cannot be acquired out from under us. Apache-2.0, open weights, nothing to buy.
These are the boundary-check half of the five tests we hold mission-critical AI to. None of the maintained open-source options we evaluated met all five at once. So we built one, and the library’s own documentation is written to the same standard: every count is generated from the code, every score carries the number of examples behind it, and a claim the code cannot back fails a test.
Two functions, thirty detectors
scan_input and scan_output. Both take the text, a policy and an optional context, and both return a decision: a verdict of allow, redact, block or flag, the text after any redaction, one finding per detection, the evidence record, and which tiers ran. The library does not sit in front of your model, hold your API key, or make the call for you.
from flowx_border import load_policy, scan_input, scan_output
policy = load_policy("border-code.yaml")
# Inbound. The decision carries the text, possibly redacted.
crossing = scan_input(user_text, policy)
if crossing.verdict == "block":
return refuse(crossing.evidence.record_id)
answer = your_model.complete(crossing.text)
# Outbound. Same shape, other direction.
out = scan_output(answer, policy)
archive(out.evidence)
return out.textPolicy is data, never code. A YAML file names each detector, what to do when it fires, its threshold and its options, and a fail mode per tier: a detector that errors in tier 0 blocks the scan; one that errors in a higher tier is recorded and the scan continues. Two policies ship, a general default and one tuned for banking, insurance and financial services.
Detectors run in four tiers. Tier 0 always runs and cannot be disabled. Tiers 1 and 2 run on the standard path. Tier 3 runs only when a lower tier flags, or when the policy says always.
| Tier | Runs | Detectors |
|---|---|---|
| T0 | always, cannot be disabled | secrets · invisible_text · disclosure |
| T1 | standard path | pii · output_leakage · language_id · encoded_payload · markup_injection · code_present · system_prompt_leakage · token_limit · banned_terms · internal_domains · json_schema · output_format · postal_code · repetition · sql_injection · summary_support · gibberish |
| T2 | standard path, may be disabled per policy | injection · moderation (thirteen hazard categories) · toxicity · nsfw · bias · politeness · regulated_advice |
| T3 | only when a lower tier flags | groundedness · topic_scope · url_reachability |
Thirty detectors. Twenty-seven need nothing beyond a CPU and the base install. The other three declare what they need: two want a parser that ships as an optional extra, and one, link reachability, needs the network and says so, because a third party in the latency path of every request is a decision a deployment should make on purpose. A detector that cannot run reports that it could not run instead of passing quietly, and a policy that asks an unavailable detector to block raises before any scan happens.
Check hard at the boundary, not inside
The governing idea is a boundary-inspection model. Text is inspected where it crosses into the trusted area and again where it leaves. Services inside the area exchange messages without re-inspecting each other, because every message that reached them already carries a record of what was checked. Five words carry the model.
- border is the library, where checks happen.
- Code is the policy file,
border-code.yaml, the shared rulebook. - Crossing is a single scan, inbound or outbound.
- Stamp is the evidence record attached to a crossing.
- Area is the trust domain: the services that accept each other’s stamps.
Nothing leaves the machine
Model weights are fetched once, on first load, and cached. After that, both functions work with the network interface down. This is asserted, not promised. The test suite patches outbound socket connections and name resolution to raise for every test, then runs real scans through the whole public API with every loaded detector under that guard. A test that genuinely needs the network, such as a first-run download, has to mark itself, and the marker lifts the guard for that test alone. The Hugging Face client is set to offline mode as well, because a cached file plus a revalidation request is not the same thing as offline.
Nothing is sent anywhere. No telemetry, no usage counters, no crash reports. And a scan is deterministic: given the same inputs and the same model revisions, it returns the same decision and the same record on another machine. No sampling, no temperature, no clock-dependent behavior inside a scan.
CPU is the reference target, not a fallback, and that deserves more than a spec-sheet line, because it is the difference between a guardrail you can actually deploy and one you plan to deploy. Twenty-seven of the 30 detectors run on the processor your application is already running on: same container, same pod, same machine, nothing to provision. A component that needed a GPU would need a capacity request, a node pool, a driver and toolkit version that agrees with everything else on the box, and a place in the queue. In most institutions the GPUs are the scarce, budgeted, oversubscribed resource, and they are there to serve the model. A guardrail asking for one either waits behind the model’s own batches or takes throughput away from them, which is a strange trade to make for a check that is supposed to be unconditional. Practically, for whoever has to ship this: adding border to a request path is a pip install and a YAML file on infrastructure that already exists, not a hardware conversation. Its cost is tens of milliseconds of one thread on hardware already paid for, so nobody has to defend a GPU line item for a safety control. And it runs in the places GPUs are not: an on-premise datacenter, a branch office, an air-gapped environment, a laptop, a CI job that runs the policy tests on every commit. The constraint also shapes the library: needing to fit in a CPU latency budget is exactly why the classifiers are encoders and not generative models, and why the one model that does want a GPU, cee-pii, has to declare it when the policy loads.
Evidence, never text
Every scan produces one record. It is the artifact that makes a decision reviewable six months later by somebody who was not there, the same discipline we argued for in Override by Design: the audit trail has to exist before the system goes live, not be reconstructed after.
{"record_id": "0191c4e2-…", "timestamp": "2026-09-03T09:14:02Z",
"direction": "input", "library_version": "0.7.0",
"policy_id": "bfsi", "policy_hash": "4c81…",
"detectors": [{"id": "secrets"},
{"id": "pii", "model_id": "piiguard",
"revision": "018e7f0355c0", "weights_sha256": "3b1f…"}],
"input_hash": "9f2b…",
"finding_summary": [{"detector_id": "pii", "label": "iban",
"score": 0.98, "action": "redact"}],
"verdict": "redact"}The record ties together a record id and a UTC timestamp, the direction, the policy id and a hash over the resolved policy, the library version, one attestation per detector with its model id, revision and the checksum of the weights that ran, a hash of the input, the verdict, and a summary of each finding. The model revision is the field that does the long-term work: it is what lets somebody asking “how well did this work in Hungarian” resolve a decision to the evaluation table published for exactly those weights.
It never contains the text. It contains hashes, and there is a test for it. The cost of that choice is real: you cannot reconstruct what was said from a record. You can only prove that a given text was the one checked. The benefit is that the record can be archived in a system scoped for audit logs instead of one scoped for customer data, and it does not become a second copy of the thing you redacted. Signing is optional and uses a key you own. The library never holds a signing key, and serialization is canonical JSON, so a hash computed on one machine matches one computed on another.
This does not make anyone compliant with anything. It records which checks ran and what they found. Obligations under any AI regulation sit with the provider or deployer of a system, not with a library inside it.
26 languages, scored one at a time
The supported set is the 24 official languages of the European Union plus Turkish and Azerbaijani. Every classifier is scored separately in all 26, at its calibrated threshold, on corpora generated per language as opposed to translated. The table below shows one macro score per detector; the 26 per-language rows behind each of them are published too, because an average hides the tail and the tail is the point.
Twenty-six is a strange-looking number, so here is where the line comes from. Twenty-four of them are the official languages of the Union, a list we did not draw: it is fixed by the Union’s founding language regulation and amended each time a member state joins. That matters more than it sounds. Any boundary we invented ourselves, the ten biggest languages in Europe, the ones our largest customers use, would be a product decision somebody could reasonably argue with, and it would quietly move whenever commercial priorities moved. An externally governed list cannot be lobbied. It is also the operationally correct one for this audience: it is the set the Union publishes its own law in, and the set in which an institution in the single market can be obliged to deal with a customer in their own language. Turkish and Azerbaijani are then added on top, and those two are our own judgment and not anyone’s statute.
Which answers the question people actually ask, usually with Icelandic. Iceland is in the European Economic Area and in the single market, Icelandic is a national language of a European state, and it is not in the set. Neither is Norwegian, for the same reason: the list stops at Union membership, and both countries are in the EEA without being in the Union. That is a boundary, not an assessment of the language or of anyone who speaks it. Nothing in the code is fixed at 26 either. Adding a language is a measurement job: generate a corpus in it, calibrate each classifier’s threshold against that corpus, then publish 27 per-language rows per detector instead of 26, and keep publishing them. It also has to be a language the base encoder actually saw in pretraining, which is not a given. Maltese is the documented case where it was not, and we say so in the language coverage page instead of letting a macro average bury it. So the honest answer to “why not Icelandic” is that it is a corpus and a calibration run away, and until somebody has done that work and published the rows, claiming the language would be the kind of unbacked coverage claim this library exists to avoid.
| Detector | Metric | Macro across 26 | Test cases |
|---|---|---|---|
| toxicity | F1 | 0.992 | 518 |
| gibberish | F1 | 0.992 | 779 |
| injection | F1 | 0.989 | 1,080 |
| regulated_advice | F1 | 0.986 | 572 |
| bias | F1 | 0.983 | 2,064 |
| politeness | F1 | 0.978 | 545 |
| nsfw | F1 | 0.974 | 622 |
| moderation | F1 | 0.971 | 1,454 |
| groundedness | exact match | 0.898 | 3,214 |
| topic_scope | top-1 accuracy | 0.865 | 175 |
Two rows there come with a warning attached, which is the reason they are in the table instead of on a slide. topic_scope is last on merit: all 26 of its per-language splits hold fewer than ten evaluated examples, so every individual language score in it is indicative and not measured, and it runs at the policy default because no calibrated threshold has been recorded for it yet. groundedness is scored on pairs, a grounded and a not-grounded answer to the same question, which is a harder question than whether a detector fires, and its 0.898 is not comparable to an F1 above it.
One detector is deliberately not in that table, and it is the one most people will run first. pii’s default model, piiguard, is a token classifier, so it is not scored on whether it fired: it is scored on spans, and a span counts only when its type and both of its boundaries are exactly right. On that basis it reaches 0.998 F1 with 0.997 precision over 4,372 gold spans across the same 26 languages, covering eight entity types, card numbers, dates, email addresses, IBANs, locations, national identifiers, names and phone numbers, of which four score a clean 1.0. Its weakest languages are French at 0.987, Irish at 0.988 and Czech at 0.988, so the tail here is shallow. Putting that number in the same column as a firing F1 would be the averaging trick this section exists to refuse, which is why it sits in its own paragraph with its metric spelled out.
Scores are per language and ask whether the detector fires at all; the per-label breakdown, the calibration notes and the caveats sit on the benchmarks page, generated from the same JSON the tests check. Latency is measured the same way: at a 396-character reference input on one thread and a CPU, a PII scan runs at about 23 ms at the 95th percentile, the classifiers at about 19 ms, and the rule-based detectors well under a millisecond. A latency figure quoted without its input attached is not reproducible, so the library does not quote one.
The models
Eleven open-weight models back the default detector set, published under the flowxai organization on Hugging Face with Apache-2.0 declared in each card. One is a token classifier for personal data, piiguard, covering card numbers, dates, email addresses, IBANs, national identifiers, names and phone numbers. Identifiers are generated checksum-valid in training, so an IBAN that fails its mod-97 check is not reported as an IBAN. Ten are sequence classifiers on the same 278M-parameter multilingual encoder, one per model-backed detector, moderation included: it covers thirteen hazard categories in one pass, the capability Llama Guard and ShieldGemma provide, with weights you can ship. A twelfth, cee-pii, is the policy-selectable second model for personal data; the scorecard below is why it exists.
One scorecard, hand-verified
Numbers on a benchmarks page are one thing. Here is what happened when a production data-privacy workflow in one of our deployments needed a better person-name detector and we measured five candidates on the same thirteen real documents: legal filings, court rulings, a press release, two encyclopedia biographies and a technical guide. Precision was verified by hand, span by span, across 119 distinct strings, after the scripted classifier we tried first was caught overcounting. Speed and memory were measured on one CPU thread, relative to the incumbent; the GPU row was measured separately on a synthetic corpus with the same word-count distribution, because the real documents never left the machine they were on.
| Candidate | Precision | Speed, 1 CPU thread | Memory, 1 CPU thread |
|---|---|---|---|
| spaCy small pipeline (incumbent) | 0.229 | 1x | 1x (831 MB) |
| spaCy medium pipeline | 0.238 | ~1x | ~1.3x |
| Community BERT-base NER fine-tune | 0.189 | ~3.5x slower | ~1x |
| GLiNER zero-shot (gliner_multi-v2.1) | 0.317 | ~11x slower | ~4.5x |
| flowxai/cee-pii (our GLiNER fine-tune) | 0.915 to 0.924 | ~10x slower | ~3.7x |
| flowxai/cee-pii on one GPU (L4) | 0.915 to 0.924 | 44x faster than its own CPU run; same order of magnitude as the incumbent | not measured |
Two of the three challengers did not beat the incumbent. The community fine-tune was worse than the pipeline it was meant to replace, because its false positives, pronouns and role nouns, scored in the same range as real names, so no threshold separates them. Zero-shot GLiNER swung from near-perfect on biographies to near zero on statute text. Our own fine-tune was the only candidate whose false positives were narrow and categorizable, organization names tagged as a person and redaction artifacts, instead of an open-ended flood of common nouns, and it scored 1.0 precision on nine of the thirteen documents.
Both models are in border. piiguard, an encoder token classifier that quantizes to INT8 and answers in about 23 ms on one CPU thread, is the default. Since 0.5.0, cee-pii is the second model behind the same detector, selected by name in the policy: pii: { options: { model: cee-pii } }. It ships as an fp32 ONNX export of the GLiNER model, its 34 entity types mapped onto border’s, and it was verified against the original PyTorch model at export time: seven fixtures, zero span mismatches, a maximum score drift of 0.00001. It declares that it needs a GPU, and the library reports that when the policy loads, instead of leaving it to be discovered from a latency graph in production.
Where border sits inside FlowX.AI
border is the open-source half of something FlowX.AI already runs in production. The FlowX.AI Platform’s data-privacy service uses cee-pii, the model above, when it applies personal-data detection to text inside customer workflows, and border ships the same model as its second PII option so that anyone can run the same check on their own hardware. The rest of the platform’s controls, and the regulatory frameworks it maps evidence to, are on the platform security page. The library stands on its own: nothing in it calls the platform, and nothing in the platform is needed to use it.
Coming from LLM Guard
A compatibility adapter keeps LLM Guard’s scan_prompt and scan_output signatures and their tuple return shape, so a migration starts as an import change. Twenty-five of the 27 scanners LLM Guard shipped map onto a detector; the two that do not, sentiment and URL reputation, are listed as unsupported, because neither has an equivalent here and pretending otherwise would be worse than saying no. The table is generated from the adapter, so it cannot drift from what the code does. A scanner with no equivalent raises; it does not pass. A shim that accepted a scanner and did nothing would leave you believing something was blocked, and the only way to find out otherwise would be an incident.
One pair is worth knowing about before you migrate. LLM Guard’s anonymize and de-anonymize scanners replaced entities with placeholders and then put them back. border does not put them back. Redaction is one way, because a vault mapping placeholders to real values is a second copy of the data you were trying not to expose. The migration guide has the full mapping.
This is 0.7.0 and we are saying so
The first release went to PyPI on August 17, 2026. Version 0.7.0, the eleventh, followed on September 14. Nothing structural has moved in that time: still two functions, still 30 detectors in four tiers, still 26 languages. What changes in a point release is the detail, and the detail is where these libraries are usually wrong. 0.7.0 added a ninth entity type for personal data, organisation, which only cee-pii can produce, so a policy that names it while running piiguard is refused by name instead of quietly finding none. The advice that ships with it is to flag the type and not redact it, because a company name is not personal data: 16 of the 234 ordinary rows in our sweep carry one, so redacting it would damage 7 percent of ordinary business prose with findings that are every one of them correct. The repository keeps an open-issues file in which everything has a number attached and something that produces it, because an issue list without measurements becomes a wish list.
What we want most right now is people trying to break the detectors and telling us where they fail. Bring a prompt injection in Czech, a phone number format we mangled, a support question that a hazard classifier misreads. One request: do not put real personal data in an issue. This library exists to find it, so send a synthetic equivalent with the same shape.
Apache-2.0. Open weights on Hugging Face. pip install flowx-border.
Links
- Siteborder.flowx.ai: install, the two functions, detectors, benchmarks, docs
- Codegithub.com/flowx-ai/border, Apache-2.0
- Packagepypi.org/project/flowx-border
- Weightshuggingface.co/flowxai: the eleven models behind the detectors
- PlatformFlowX.AI platform security: the controls and framework mappings around the same checks
- Contextprotectai/llm-guard, archived July 9, 2026