Views
No views yet
toxicity detector for border, an embeddable library that inspects the text going into and coming out of an LLM and returns a structured decision plus an audit-grade evidence record.flowxai/toxicity on the hub. It is one detector of 28, and it is not a general purpose toxicity classifier: it was trained for this library's policy, is read at the operating point below, and reports through the evidence record rather than returning a bare score.insult, threat, identity_attack, harassmentonnx/model.int8.onnx, 535 MB, opset 17macro_f1 objective.pip install flowx-border1# policy.yaml
2policy_id: default
3version: 1
4
5detectors:
6 toxicity:
7 enabled: true
8 on_fail: flag
9 threshold: 0.811from flowx_border import load_policy, scan_input, scan_output
2
3policy = load_policy("policy.yaml")
4
5decision = scan_input(user_text, policy)
6decision = scan_output(model_answer, policy)
7
8print(decision.verdict) # allow | flag | redact | block
9print([f.label for f in decision.findings if f.detector_id == "toxicity"])
10print(decision.evidence.record_id)scan_input and scan_output is where it fires. It is T2, so it runs on the standard path and can be disabled per policy. Its budget is 225 ms at 87 tokens on one CPU thread.onnxruntime directly. Two things you then own yourself, and they are the reason the library exists: the operating point above is not in the graph, and neither is the chunking. Inputs longer than the trained window have to be split and recombined, or the scores past it are extrapolation.1import onnxruntime as ort
2from huggingface_hub import hf_hub_download
3from tokenizers import Tokenizer
4
5repo = "flowxai/toxicity"
6session = ort.InferenceSession(hf_hub_download(repo, "onnx/model.int8.onnx"))
7tokenizer = Tokenizer.from_file(hf_hub_download(repo, "tokenizer.json"))| Language | Support | P | R | F1 | Note |
|---|---|---|---|---|---|
az Azerbaijani | 20 | 1.000 | 1.000 | 1.000 | |
cs Czech | 20 | 1.000 | 1.000 | 1.000 | |
de German | 20 | 1.000 | 1.000 | 1.000 | |
el Greek | 19 | 1.000 | 1.000 | 1.000 | |
en English | 20 | 1.000 | 1.000 | 1.000 | |
es Spanish | 20 | 1.000 | 1.000 | 1.000 | |
et Estonian | 20 | 1.000 | 1.000 | 1.000 | |
fi Finnish | 20 | 1.000 | 1.000 | 1.000 | |
ga Irish | 19 | 1.000 | 1.000 | 1.000 | |
hr Croatian | 20 | 1.000 | 1.000 | 1.000 | |
it Italian | 20 | 1.000 | 1.000 | 1.000 | |
lv Latvian | 20 | 1.000 | 1.000 | 1.000 | |
nl Dutch | 20 | 1.000 | 1.000 | 1.000 | |
pl Polish | 20 | 1.000 | 1.000 | 1.000 | |
pt Portuguese | 20 | 1.000 | 1.000 | 1.000 | |
ro Romanian | 20 | 1.000 | 1.000 | 1.000 | |
sk Slovak | 20 | 1.000 | 1.000 | 1.000 | |
sl Slovenian | 20 | 1.000 | 1.000 | 1.000 | |
tr Turkish | 20 | 1.000 | 1.000 | 1.000 | |
bg Bulgarian | 20 | 0.952 | 1.000 | 0.976 | |
da Danish | 20 | 0.952 | 1.000 | 0.976 | |
lt Lithuanian | 20 | 0.952 | 1.000 | 0.976 | |
fr French | 20 | 1.000 | 0.950 | 0.974 | |
hu Hungarian | 20 | 1.000 | 0.950 | 0.974 | |
mt Maltese | 20 | 0.909 | 1.000 | 0.952 | not in base model pretraining |
sv Swedish | 20 | 0.950 | 0.950 | 0.950 |
sv Swedish: F1 0.950mt Maltese: F1 0.952 (absent from XLM-R pretraining, which is a base-model limit)fr French: F1 0.974| Recipe | Size | Mean logit drift | Decisions changed |
|---|---|---|---|
| all ops (the usual default) | 279 MB | 0.68 | 51 / 300 |
| MatMul only | 856 MB | 0.64 | 48 / 300 |
| Gather only, what ships here | 535 MB | 0.0036 | 0 / 300 |
sigmoid_at_threshold. A quantised model that answers differently is a different detector, so this is measured rather than assumed.nsfw detector scored 0.000 in Maltese, was blamed on the base model, and went to 1.000 with perfect precision and recall when its corpus went from 2 positives per language to 10. Nothing about the model changed. So where a language scores badly here, read the support column first.