Skip to main content
CID222 Docs

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.

NormaliserInputOutput
ROT13zvff frpergmiss secret
Base64cGFzc3dvcmQ=password
Leetspeakp@55w0rdpassword
Unicodepasswordpassword
CasePASSWORDpassword

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.

DetectorModelEmits
PII regexPatterns from the input-filter rowsEMAIL, PHONE, TC_KIMLIK, IBAN, secrets, and the rest of the pattern catalogue
PII NERONNX multilingual DistilBERT21 native entity labels including PERSON_NAME, LOCATION, ORGANIZATION
ToxicityONNX mDeBERTa, 13 labelsviolence, hate, self_harm, sexual_content, crime, cyber_crimes, weapons, and six more
Jailbreak and injectionONNX mDeBERTa, 2 labels, sigmoid so both can firejailbreak, injection
Injection scanlibinjection, deterministicsql_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

ActionBehaviour
maskThe entity is replaced with its placeholder, for example [EMAIL], and the request continues with the sanitised text
rejectThe request is blocked. On the chat stream this is {"type":"content_rejected"} followed by [DONE], not an HTTP error
flagThe detection is recorded; the content passes through unchanged
allowNothing 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, it and nl. 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 filter parameter 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.

Last updated on

On this page

Download PDF