Cheap checks on everything. Expensive ones only on what they flag.
A classifier returns a signal. A rule decides what to do with it, and in what order to ask.
Rules plus a model you host.
Cheapest first, and the rest never run
One shipped ruleset asks four questions in order. Every model here is open and self-hosted, reached at one endpoint you configure.
| Question | Model | Kind |
|---|---|---|
| Toxicity | unitary/toxic-bert | trained, one dimension |
| Sentiment | cardiffnlp/twitter-roberta-base-sentiment-latest | trained, one dimension |
| Illegal topics | KoalaAI/Text-Moderation | trained, multi-label |
| Is this trying to make the model discredit us | mDeBERTa-v3-base-xnli | zero-shot: you type the criterion |
The zero-shot model that answers the last question is the expensive one.
The engine computes a signal only when a rule first reads it. Once an earlier check fires, the later ones are never computed and their calls are never made. A toxic message is caught by the first question and never reaches the fourth.
You own that ordering. If your traffic looks different, you reorder the checks and backtest the change like any other.
Three kinds of classifier, one interface
Returns a number. Trained for one dimension, fast enough to run on everything. The first three rows above.
Takes a written criterion. You describe what you want caught in a sentence and the model answers yes or no. The last row.
Reads a whole rulebook. A larger model handed your actual policy document. Most accurate, most expensive, held for the cases the cheaper checks cannot decide.
Text, images and audio: what is covered depends on the classifier you plug in.
Bring what you already run
Open models, the vendors you already pay for, and models you host yourself are all just signals. Nothing has to leave your environment: everything in the table above runs locally. An existing moderation vendor becomes another input.
udf_profiles:
toxicity_scan:
extends: classifier/text
params:
endpoint:
$env: CLASSIFIERS_ENDPOINT
model: "unitary/toxic-bert"
suspicious_labels: ["threat", "severe_toxic", "insult", "obscene"]
threshold: "{{ constants.toxicity_threshold }}"
discredit_intent_scan:
extends: classifier/zero_shot
params:
endpoint:
$env: CLASSIFIERS_ENDPOINT
model: "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7"
suspicious_labels:
- "asking for reasons why the organization is not trusted" Swapping a classifier is a change to one block: the model is named in one place, and rules read the signal without learning who computed it. The second profile: a zero-shot check takes the criterion as a sentence you type, so a harm pattern you noticed this morning is covered this morning, with nobody training anything.