The content safety pipeline
What CID222 runs over every prompt and every reply, in what order, and what turns a set of detections into one action.
- Version: 0.4
- Role: admin_user, normal_user, viewer
Every prompt and every reply passes through the same pipeline: decode, detect in parallel, validate, decide, act. The pipeline is CID222's own — custom ONNX models plus CID's decision maker.
The problem
A single detector is either too loud or too quiet. Regex alone misses a name it has never seen and fires on a number that happens to look like an ID. A classifier alone has no way to prove a card number is real, and no way to explain itself. And an attacker who knows one detector exists will encode around it — base64, homoglyphs, leetspeak — so a detector reading raw input is reading the wrong string. What is needed is several detectors over one decoded text, a way to confirm or demote what each of them found, and a single rule for turning disagreement into one action.
How CID222 does it
1. Language detection
The text is identified before the models see it, so each detector receives a language hint rather
than guessing. The toxicity and attack models cover eight languages: en, tr, de, fr, es,
ar, it and nl. The PII model is multilingual on a single set of weights.
2. Normalisation
Evasion decoders run before any detection, so a detector reads what the text means rather than how it was disguised.
| Normaliser | Input | Output |
|---|---|---|
| ROT13 | zvff frperg | miss secret |
| Base64 | cGFzc3dvcmQ= | password |
| Leetspeak | p@55w0rd | password |
| Unicode | password | password |
| Case | PASSWORD | password |
The detection response reports which decoders fired in normalization.applied. It never returns
the original text.
3. Parallel detection
Five detectors run at once over the normalised text.
| Detector | Model | Emits |
|---|---|---|
| PII regex | Patterns from the input-filter rows | EMAIL, PHONE, TC_KIMLIK, IBAN, secrets, and the rest of the pattern catalogue |
| PII NER | ONNX multilingual DistilBERT | 21 native entity labels including PERSON_NAME, LOCATION, ORGANIZATION |
| Toxicity | ONNX mDeBERTa, 13 labels | violence, hate, self_harm, sexual_content, crime, cyber_crimes, weapons, and six more |
| Jailbreak and injection | ONNX mDeBERTa, 2 labels, sigmoid so both can fire | jailbreak, injection |
| Injection scan | libinjection, deterministic | sql_injection, xss |
Code safety runs alongside them when the content is code: a rule layer for secrets and destructive commands, and an auditor that reports one detection per CWE.
Note
There is no semantic router in front of these detectors and no sampling: a detector that runs,
runs on the whole text, every time. The one thing that removes a detector is your own policy —
the gateway resolves a detector plan per tenant first, and a family whose every filter resolves
to allow for that tenant is not called at all.
4. Validation
Each detection is checked before it is trusted. A checksum rule confirms the structure of a numeric identifier: Luhn plus brand and length for a card, ISO 13616 mod-97 for an IBAN, the national algorithm for a Turkish ID, tax number or US SSN, and a province-range check for a Turkish postal code or licence plate.
The verdict is three-state. valid confirms the detection. invalid drops it, or demotes its
action from mask to flag when the rule says so. unknown — the value is not this validator's
shape — behaves exactly as if no validator ran. That fail-open third state is what keeps a
multi-shape type safe: a bank-account field also carries bare account numbers, and a national-ID
field also carries document numbers such as B1234567, and neither may be thrown away because a
digit-only checksum could not read it.
A validated entity passes only if its final confidence reaches 0.6. A detection that arrives
with no confidence is assumed to be 0.8.
5. Confidence boosting
A detection gains 0.1, clamped at 1.0, when a context keyword registered for its entity type
appears within 50 characters of the span. The keyword-to-type mapping is loaded from the active
filter rows, so an administrator extends it as data rather than as code.
Warning
The boost is context-keyword driven only. Two detectors agreeing on the same span does not raise confidence — overlapping detections are merged, and the merge does not add a bonus.
6. The decision
The decision maker resolves each detection against the rule that owns its type, applies any per-tenant or per-group override to that rule's action, and folds the results into one verdict for the request. The strongest action wins:
reject > mask > flag > allow
| Action | Behaviour |
|---|---|
mask | The entity is replaced with its placeholder, for example [EMAIL], and the request continues with the sanitised text |
reject | The request is blocked. On the chat stream this is {"type":"content_rejected"} followed by [DONE], not an HTTP error |
flag | The detection is recorded; the content passes through unchanged |
allow | Nothing happens. On an override, allow is how a rule is switched off for one scope |
Thresholds live with the models, not in the gateway. The toxicity service carries a tuned
block / flag / log_only triple per label, and the attack service carries one per label too.
Both files are baked into their service image and have no environment override, so retuning means
rebuilding the image.
The output side
The reply is filtered before the client sees any of it, which is why the stream delivers the whole response as one event.
- Tier 1, in the request path. The same detection service runs over the generated text for PII and toxicity. A response decision maker applies the output rules, which have their own action set, and the reply is masked, warned about, or rejected outright.
- Tier 2, in the background. When the request supplied RAG contexts, a groundedness check runs against them after the fact. It writes a detection record and a session warning. It is not a stream event, and its verdict distinguishes "checked and grounded" from "not checked" rather than conflating them.
Limits and known gaps
- The pipeline uses CID's own models and decision maker. It does not use Presidio, spaCy, or
an OPA/Rego policy engine, and it never has. Policy is filter rows plus overrides in PostgreSQL,
evaluated by
DecisionMakerService. Anything claiming otherwise is describing a different product. - Accuracy is conditional and is not a single number. It varies with entity type, language, text length and the filter configuration in force. Structured identifiers with a checksum behave close to deterministically; free-text entities depend on the NER model and its surrounding context. Quote the qualified figures in performance and accuracy, never a bare percentage.
- Coverage outside the eight trained languages is unmeasured. The toxicity and attack models
are trained on
en,tr,de,fr,es,ar,itandnl. A prompt in another language still reaches them and still produces a score, and that score has no measured reliability. - Normalisation is a decoder set, not a proof. ROT13, base64, leetspeak, Unicode and case are covered. An encoding not on that list reaches the detectors unmodified.
- The
filterparameter on the detection endpoint does nothing. It is validated and then discarded before the service is called. Scope behaviour with an override instead. - A broken pattern protects nothing, silently. A stored regex that fails to compile is skipped at load time. The gateway logs it and surfaces it to the dashboard, but the filter still shows as active.
- Detections are not enforced where no tenant resolves. The override pass is a no-op without a tenant, so on the ingress paths that cannot resolve one, the base action applies whatever a department configured.
- Chunked and resumable uploads are refused, not inspected. Nothing buffers a body across requests, so a fragmented upload cannot be reassembled and scanned.
Related
- PII detection — the entity catalogue and how masking works.
- Content detection API — call this pipeline directly.
- Architecture — where the pipeline sits in a request.
Last updated on