Skip to content

An open-source project by FlowX.AI

Check what crosses into and out of your model.

It runs on CPU with the network interface down. An embeddable Python library: two functions, a policy file a reviewer can read without knowing Python, and an evidence record that holds hashes rather than user text.

Built for teams that run models inside their own perimeter, in more languages than English, and have to prove what was checked.

See the numbers
29detectors in the v1 set
28 msp95 for a PII scan: 87 tokens, about a short paragraph, on one thread
26languages every classifier is scored in
26 of 29need nothing beyond a CPU
A crossingText arrives from outside, passes an inbound check, crosses a trusted area in which two services exchange it without further inspection, passes an outbound check, and leaves.trusted areaplannerexecutorno re-inspectionuntrusted inputchecked output

The two functions

There is no client to construct, no gateway to run, and nothing wraps your model call. You call two functions and decide what to do with what they return.

guarded_turn.py
from flowx_border import scan_input, scan_output, load_policy 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.text

What a stamp contains

Hashes, the resolved policy hash, and which model revision produced each finding. Never the text. The record is canonical JSON, so the hash and the signature reproduce on another machine.

stamp.json
{"detectors":[{"id":"secrets"},   {"id":"pii","model_id":"piiguard","revision":"018e7f0355c0",    "weights_sha256":"3b1f..."}], "direction":"input", "finding_summary":[{"detector_id":"pii","label":"iban",   "score":0.98,"action":"redact"}], "input_hash":"9f2b...","policy_hash":"4c81...", "verdict":"redact"}
stampredact
detectorpii
issued2026-08-11T09:14:22Z
inputsha256:9f2b4c81

Three fields carry the weight: input_hash proves what was scanned without storing it, policy_hash proves which rulebook was in force, and the model revision and weight hash prove which exact model made the call.

The boundary model

Check hard where text crosses the boundary, and do not re-inspect inside. A message passing between two services that already trust each other has been checked once, and checking it again costs latency without changing the answer. The cost of inspecting everything everywhere is the reason teams end up turning inspection off.

The design is borrowed from the Schengen model: internal checks could be abolished only because the external checks became strong, uniform, and governed by one shared rulebook. Check hard at the perimeter, then let traffic move freely inside the area of trust.

The boundary modelText arrives from outside, passes an inbound check on the perimeter of a trusted area, is handled by two services that exchange it between them with no further inspection, and passes an outbound check on the way out.untrusted inputchecked outputareacheckcheckplannerexecutorno re-inspection
The exchange between the two services carries no check, and that is the design. The cost of inspecting everything everywhere is what makes teams turn inspection off.
The five terms used across this site
BorderThe library. Where checks happen.
CodeThe policy file, border-code.yaml. The shared rulebook, named after the Schengen Borders Code.
CrossingA single scan, inbound or outbound.
StampThe signed evidence record attached to a crossing.
AreaThe trust domain. Services that accept each other's stamps.

The 29 detectors

Tiers decide what runs when, so the expensive checks only run once something cheaper has found a reason.

Reading the table. Runs today means installed and callable now. Trained means the model exists but the detector is not yet callable; a policy that asks it to block or redact raises before any scan happens. Needs dependency means an optional extra must be installed. Needs network means the check makes outbound requests and is off unless a policy turns it on. In the F1 column, not a classifier marks rule-based detectors, which are deterministic and measured on latency only. No corpus yet means no evaluation set we trust exists; the cost column says whether its figure is measured or a budget.

28 of the 29 run today. The rest are catalogued and do not run: a policy that asks an unavailable detector to block or redact raises before any scan happens rather than letting text through as if it had been checked. Measured figures are p95 at 87 tokens on 1 thread; where there is no measurement the column shows the budget and says so.
DetectorTierWhat it doesNeedsBacked byMean F1CostStatus
disclosureoutputruns todayT0Reports whether an AI disclosure is present in the output, in 26 languages, and records the affirmative as well as the absence. Recording the affirmative as well as the absence is the artifact a transparency review under Article 50 of the EU AI Act asks for.CPUrulenot a classifier0.038 msmeasuredRuns today
invisible_textinput, outputruns todayT0Characters that are in the text but not on the screen: bidirectional controls, tag characters used to smuggle instructions, zero-width characters used to evade filters.CPUrulenot a classifier0.038 msmeasuredRuns today
secretsinputruns todayT0Credentials in text on its way to the model: named key formats, plus a deliberately conservative entropy rule.CPUrulenot a classifier0.048 msmeasuredRuns today
banned_termsinput, outputruns todayT1Terms the deploying organisation has decided must not appear, matched correctly in 26 languages. The list is policy; none ships.CPUrulenot a classifier0.168 msmeasuredRuns today
code_presentinput, outputruns todayT1Source code in text that should be prose, reported as one finding per shape found: a fence, a shebang, a definition, an import, a script tag, a shell invocation. Each carries its own confidence so a policy can act on a fenced block without acting on a line that merely ends in a brace.CPUrulenot a classifier0.009 msmeasuredRuns today
gibberishinputruns todayT1Input that is not meaningful text.CPUclassifier0.96627.528 msmeasuredRuns today
internal_domainsoutputruns todayT1Internal hostnames appearing in an answer meant for someone outside, in both their Unicode and punycode spellings.CPUrulenot a classifier0.234 msmeasuredRuns today
json_schemaoutputruns today · needs dependencyT1Output that does not satisfy a JSON Schema the policy carries. Point it at the OpenAPI meta-schema and it validates an OpenAPI document.dependencyrulenot a classifier0.001 msmeasuredRuns today
language_idinput, outputruns todayT1Which of the 26 supported languages the text is in, whether that is one the policy permits, and whether an answer is in the same language as the prompt. Reports uncertain rather than guessing on short or mixed text.CPUrulenot a classifier0.362 msmeasuredRuns today
markup_injectioninput, outputruns todayT1Markup in the text that a browser would execute rather than display, found through case folding, entity decoding and compatibility folding.CPUrulenot a classifier0.239 msmeasuredRuns today
output_formatoutputruns todayT1Shape assertions a policy states: JSON, HTML, URL presence, length in graphemes, word count, case, choices, ranges, a regex, reading time.CPUrulenot a classifier0.001 msmeasuredRuns today
output_leakageoutputruns todayT1Personal data in the output that the user did not supply, which is the narrower and more useful question than whether any is present.CPUnerper entity27.991 msmeasuredRuns today
piiinput, outputruns todayT1Personal data in input or output, as named entity spans with checksum validation where the identifier has one.CPUnerper entity27.636 msmeasuredRuns today
postal_codeoutputruns todayT1Postal codes that cannot exist in the countries the product serves: the wrong shape, or outside a published province or department range.CPUrulenot a classifier0.002 msmeasuredRuns today
repetitionoutputruns todayT1Sentences the answer says twice, compared over folded text so a change of case or diacritic spelling does not hide a repeat.CPUrulenot a classifier0.497 msmeasuredRuns today
sql_injectionoutputruns today · needs dependencyT1Generated SQL that does more than the product asked for: a second statement, a forbidden statement kind, a tautology, an unexpected UNION.dependencyrulenot a classifier0.238 msmeasuredRuns today
summary_supportoutputruns todayT1Whether each sentence of a summary appears in the source it summarises, by string overlap rather than by judgement. Useful for an extractive summary, and not a groundedness check: it says so in its own docstring.CPUrulenot a classifier0.921 msmeasuredRuns today
system_prompt_leakageoutputruns todayT1Whether the answer gave away the instructions the model was operating under, by containment against the system prompt and by phrase match in 26 languages.CPUrulenot a classifier5 msbudgetRuns today
token_limitinput, outputruns todayT1Text longer than the token budget of the model it is going to, counted with a tokenizer the policy names and pins: a local file, whose hash is reported as the revision, or an id already carrying a commit. A bare repo id is refused, because an unpinned count cannot be reproduced.CPUrulenot a classifier5 msbudgetRuns today
biasoutputruns todayT2Output carrying bias related to a protected characteristic.CPUclassifier0.97721.327 msmeasuredRuns today
injectioninputruns todayT2Attempts to talk the model out of its instructions.CPUclassifier0.97027.622 msmeasuredRuns today
moderationinput, outputtrainedT2Thirteen hazard categories in one pass, from violent crime to election misinformation. Replaces the capability Llama Guard and ShieldGemma provide, with weights this project can ship.CPUclassifierno corpus yet150 msbudgettrained
nsfwinput, outputruns todayT2Sexual or otherwise not-safe-for-work content.CPUclassifier0.93427.544 msmeasuredRuns today
politenessoutputruns todayT2Whether the tone of an answer is acceptable.CPUclassifier0.96230.319 msmeasuredRuns today
regulated_adviceoutputruns todayT2Output that reads as regulated financial, legal or medical advice.CPUclassifier0.99530.43 msmeasuredRuns today
toxicityinput, outputruns todayT2Abusive or hateful language, in input or output.CPUclassifier0.99227.224 msmeasuredRuns today
groundednessoutputruns todayT3Whether the claims in an answer are supported by the sources it was given.CPUclassifierno corpus yet300 msbudgetRuns today
topic_scopeinputruns todayT3Whether a request is inside the subject matter the product covers.CPUclassifierno corpus yet46.298 msmeasuredRuns today
url_reachabilityoutputruns today · needs networkT3Whether links in the answer resolve to something that answers, with a deadline and a refusal to request private addresses.networkrulenot a classifier3000 msbudgetRuns today

T0

Always runs. Cannot be disabled.

T1

Runs on the standard path.

T2

Runs on the standard path. Can be disabled by policy.

T3

Runs only when a lower tier flags, or when the policy asks for it.

What makes it different

Three claims, each with its measurement underneath it.

It runs where your data already is

CPU is the reference target, not a fallback. Weights are fetched once and cached, and after that a scan works with the network interface down, which a test asserts by making a socket call raise. The cost of that choice is latency you can see, and the benefit is the row below that sends nothing anywhere.

10025050010002500ms per docpiiguard, on device$0 /1kgemini-3.1-flash-lite$0.053 /1kgpt-5.4-nano$0.032 /1kgpt-5.4-mini$0.159 /1kclaude-haiku-4-5$0.769 /1kgemini-3.5-flash$0.219 /1klower is better, log scale
Milliseconds per document over 30 documents, from artifacts/bench-piiguard.json, OpenNER training repo. The filled marker is the only row that runs on your own hardware, which is also the only row where no document leaves your network. On this set our model and three of the five hosted models all score an F1 of 1.0. A benchmark everyone passes is not measuring quality, it is measuring that the set is synthetic and in distribution. Read the latency and cost columns, not the accuracy ones, and treat a harder evaluation as outstanding work. The hosted rows answer "why not just call an API"; for same-class comparisons against Presidio and Llama Guard, see the benchmarks page.

Detection that starts multilingual

The PII model is trained across all 26 target languages at once, with dates as a seventh entity type, and each country's national identifier generated so it passes its own checksum. Not an English model with translation bolted on: span-level F1 0.998, weakest language French at 0.977, and the full table ships in the model card.

Each of the 7 classifiers is scored separately in all 26 languages rather than in aggregate, and every row is published, including the ones that fail. All 7 tables, per language.

0815enrohupldefrazbgcsdaelesetfigahritltlvmtnlptskslsvtr
26 of 26target languages are declared by at least one published model
Models declaring each language, from the Hugging Face repository tags. The border classifiers are verified against their training configs and carry per-language evaluations in their cards; for the rest, read this as coverage intent, not accuracy.
Table view
LanguageModels
en15
ro14
hu11
pl11
de10
fr10
az9
bg9
cs9
da9
el9
es9
et9
fi9
ga9
hr9
it9
lt9
lv9
mt9
nl9
pt9
sk9
sl9
sv9
tr9

A record, not a boolean

Every crossing produces a stamp: which detectors ran, which model revision and weight hash each used, the resolved policy hash, and hashes of the text. Optionally signed with a key you hold and the library never stores. The record above in section 01 is the whole of it.

What a scan costs

Every latency figure here describes 87 tokens of prose on 1 thread, the library default, so a scan does not take cores from the application it runs inside. Cost is close to linear at 1.663 ms per token, and every model-backed detector shares one base model, so they cost the same at the same length.

075150225300ms168794128input length, tokens225 ms budget94-token window2nd pass +33.25 ms1 thread
Cost is linear inside a window and steps at each boundary: within one 94-token window the slope is 1.663 ms per token, and text past it needs a second forward pass. The 225 ms line is the per-scan budget, not a limit on input length: longer text costs more passes, each budgeted on its own.

The models

The models this library ships: the PII tagger and eight classifiers, one per model-backed detector. All on Hugging Face under Apache-2.0, each carrying its per-language evaluation in its card, and small enough to run on a CPU you already own. Browse our models.

bias

Sequence classification

Backs the bias detector on the output side.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

gibberish

Sequence classification

Backs the gibberish detector: unreadable input short-circuits the tiers above it.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

injection

Sequence classification

Backs the injection detector, for the text a retrieval tool fetched as much as the text a user typed.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

nsfw

Sequence classification

Backs the nsfw detector on input and output.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

piiguard

Token classification, PII spans

Seven entity types: card, date, email, IBAN, national ID, person, phone. Identifiers are generated checksum-valid in training, so an IBAN that fails mod-97 is not reported as an IBAN.

Parameters
278M
Languages
all 26
Artifacts
ONNX, safetensors

Apache-2.0

pii: { model: piiguard }

politeness

Sequence classification

Backs the politeness detector on the output side.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

regulated-advice

Sequence classification

Backs the regulated_advice detector: the line between explaining an instrument and recommending it.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

topic-scope

Sequence classification

A bi-encoder for the topic_scope retrieval task. Runs today at 46 ms p95 against a 300 ms budget.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

toxicity

Sequence classification

Backs the toxicity detector on both sides of the boundary.

Parameters
278M
Languages
all 26
Artifacts
ONNX, ONNX INT8

Apache-2.0

What it is not

Not a gateway

It does not sit in front of your model, it does not hold your traffic, and it does not wrap your model call. If it stops working, your application still runs, it just stops producing evidence.

Not compliance

Nothing here is certified, and no library can be. Obligations under the EU AI Act sit with the provider or deployer of a system, not with a dependency it installs. What this produces is an auditable record of which checks ran and what they found, which supports the evidence requirements of a governance process you run yourself. What it does support: the disclosure detector records whether an AI disclosure was present in each output, in 26 languages, which is evidence a transparency process can file.

Not a security review

It reads text and reports on text. It knows nothing about your authentication, your tool permissions, or what your agent is allowed to do with the answer it got.

It does not pretend

A detector whose model is not published raises an error naming what is missing rather than returning an empty result, because a check that silently passes is worse than one that is absent.

Install it and read the record

Weights are fetched once and cached. After that a scan needs no network, and nothing you scan reaches FlowX.AI.

Read the benchmarks

Python 3.11 or newer. Apache-2.0.

The package is flowx-border, the import is flowx_border, the GitHub org is flowx-ai, and the models live under flowxai on Hugging Face. Four spellings, one project.