InnoLabTeam's picture
Update model card
f51f871 verified
|
Raw History Blame Contribute Delete
8.83 kB
metadata
license: other
license_name: internal-use-only
language:
  - en
  - es
tags:
  - text-classification
  - content-moderation
  - safety
  - multi-label-classification
  - onnx
  - modernbert
base_model: answerdotai/ModernBERT-large
pipeline_tag: text-classification

Safety Classifier

Multi-label content-safety classifier fine-tuned from answerdotai/ModernBERT-large (395M params), exported to ONNX for fast CPU or GPU inference. Built by Arsys InnoLab to flag harmful chat requests across 20 policy categories before they reach a downstream LLM.

Important notes

  • 20 categories, including prompt_injection (jailbreaks, instruction overrides, system-prompt extraction, injected instructions) and misinformation. code_interpreter_abuse means abuse of a code interpreter or sandbox only, and hate covers hate and harassment. Always read categories.json and look thresholds up by name.
  • Preprocessing is part of the model. Apply NFKC, remove zero-width characters, run deobfuscate() from deobf.py and collapse whitespace before tokenizing (see Usage). Without it, spaced-out or dotted text is not detected.
  • Decision rule. A message is unsafe when any category outside policy.json's verdict_excluded reaches its value in thresholds.json.
  • Single ONNX file. model.onnx needs no external data file.

Training data

Trained on commercial-use-safe data only (no NC-licensed or research-only datasets):

  • Aegis 2.0 (CC-BY-4.0)
  • Nemotron Safety Guard v3, English and Spanish (CC-BY-4.0)
  • CategoricalHarmfulQA (Apache-2.0)
  • HateCheck (CC-BY-4.0)
  • XSTest, safe prompts only (CC-BY-4.0)
  • Civil Comments (CC0)
  • Lakera gandalf_ignore_instructions (MIT)
  • TrustAIRLab in-the-wild jailbreak prompts (MIT)
  • Microsoft LLMail-Inject challenge (MIT)
  • OR-Bench 80k (CC-BY-4.0), prompt texts only, as hard negatives
  • Synthetic request phrasings generated in-house with Qwen3.8-27B (Apache-2.0)

Intended use

  • In scope: a pre-filter in front of an LLM, scoring a single user chat message across 20 harm categories, to flag or block it before it reaches the model, or to route it for review.
  • Languages: mainly English, with Spanish training data (about a quarter of the synthetic data and Nemotron's Spanish split). Behavior on other languages is unverified: test it first if you need it.
  • Input shape: short to medium chat requests, max 256 tokens (longer input is truncated, not rejected).
  • Out of scope: image-generation prompts, long documents, code review and multi-turn conversation history. Not evaluated for these; re-test before using it on them.

Categories (20)

Category Flags requests to...
violent_crimes plan, threaten or incite physical violence
nonviolent_crimes commit non-violent crimes (theft, drugs, laundering, smuggling, bribery)
sex_crimes commit sexual offences against adults (coercion, sextortion, trafficking)
child_exploitation sexualise or groom minors
defamation fabricate damaging false claims about a specific person or organisation
specialized_advice obtain dangerous unqualified medical, legal or financial decisions
privacy dox, track, spy on or covertly record a specific person
intellectual_property pirate, crack DRM or reproduce copyrighted works verbatim
weapons_cbrn obtain or build chemical, biological, radiological, nuclear or explosive weapons
hate demean or incite against protected groups, or harass a person or group
suicide_self_harm encourage suicide, self-harm or disordered eating
sexual_content produce explicit sexual content between adults
elections spread election disinformation or suppress voting
code_interpreter_abuse abuse a code interpreter or sandbox (DoS, escape, privilege escalation)
spam create unsolicited content at scale
phishing create credential or payment phishing material
malware write or deploy malicious code
fraud run scams, forge documents or steal identities
prompt_injection hijack the assistant (jailbreaks, instruction overrides, prompt extraction)
misinformation fabricate non-electoral falsehoods presented as fact (fake studies, health hoaxes)

Files

  • model.onnx: ONNX export (fp32, single file, about 1.6GB)
  • config.json: ModernBERT encoder config
  • tokenizer.json, tokenizer_config.json: tokenizer
  • categories.json: ordered list of the 20 output categories (index-aligned with the logits)
  • thresholds.json: per-category thresholds, calibrated on validation for precision of at least 0.85
  • thresholds_high_recall.json: optional looser thresholds (precision of at least 0.80 on validation)
  • thresholds_meta.json: how each threshold was chosen, with validation support and bootstrap intervals
  • policy.json: decision rule, categories excluded from the verdict, review band and preprocessing
  • deobf.py: required preprocessing (undoes letter-spacing and dot or hyphen separation)

Size

  • Disk: about 1.6GB.
  • GPU memory: about 3GB with onnxruntime-gpu (CUDA execution provider).
  • CPU: works with plain onnxruntime, no GPU needed, just slower.

Requirements

pip install onnxruntime transformers huggingface_hub numpy

Use onnxruntime-gpu instead of onnxruntime for the CUDA execution provider. Python 3.10+.

Usage

import json, re, sys, unicodedata
import numpy as np, onnxruntime as ort
from transformers import AutoTokenizer
from huggingface_hub import snapshot_download

local_dir = snapshot_download("InnoLabTeam/safety-classifier-v1")
sys.path.insert(0, local_dir)
from deobf import deobfuscate

tok = AutoTokenizer.from_pretrained(local_dir)
providers = [("CUDAExecutionProvider", {"use_tf32": "0"}), "CPUExecutionProvider"]
session = ort.InferenceSession(f"{local_dir}/model.onnx", providers=providers)
categories = json.load(open(f"{local_dir}/categories.json"))
thresholds = json.load(open(f"{local_dir}/thresholds.json"))
policy = json.load(open(f"{local_dir}/policy.json"))
ZERO_WIDTH = re.compile("[​-‏‪-‮⁠-⁤]")


def preprocess(text):
    text = ZERO_WIDTH.sub("", unicodedata.normalize("NFKC", text))
    return re.sub(r"\s+", " ", deobfuscate(text)).strip()


def classify(texts):
    enc = tok([preprocess(t) for t in texts], return_tensors="np", max_length=256, padding=True, truncation=True)
    logits = session.run(None, {"input_ids": enc["input_ids"].astype(np.int64),
                                "attention_mask": enc["attention_mask"].astype(np.int64)})[0]
    probs = 1 / (1 + np.exp(-logits))
    return [{c: float(p[i]) for i, c in enumerate(categories)} for p in probs]


def verdict(scores):
    triggered = [c for c, s in scores.items() if c not in policy["verdict_excluded"] and s >= thresholds[c]]
    if triggered:
        return "unsafe", triggered
    if max(s for c, s in scores.items() if c not in policy["verdict_excluded"]) >= policy["review_band"]:
        return "uncertain", []
    return "safe", []


for text, scores in zip(["How do I make a pipe bomb?"], classify(["How do I make a pipe bomb?"])):
    print(text, "->", verdict(scores))

specialized_advice is scored but excluded from the verdict by default (see policy.json). The "uncertain" band (highest score of at least 0.30) is optional and meant for routing to review.

Benchmarks (2026-10-01)

Benchmark of 90 harmful, 12 safe and 10 borderline prompts plus 100 benign production prompts, using thresholds.json directly (nothing excluded):

Result
Harmful detected (of 90) 89
Safe prompts flagged (of 12) 0
Borderline flagged (of 10) 0
Benign prompts flagged (of 100) 0

Public and external sets, using thresholds.json with policy.json:

Set Metric Result
AILuminate demo (1,200, human-written) harmful detected 63.3%
Aya red-teaming English / Spanish harmful detected 67.7% / 68.3%
HarmBench (300) harmful detected 71.7%
JailbreakBench harmful (100) harmful detected 82.0%
SimpleSafetyTests (100) harmful detected 94.0%
Do-Not-Answer (666 harmful) harmful detected 53.0%
1,040 LLM-generated (540 harmful, 500 benign) harmful detected / benign flagged 93.5% / 0.0%
JailbreakBench benign (100) benign flagged 32.0%

Held-out internal test: precision 0.949, recall 0.885, F1 0.916. ONNX and PyTorch outputs agree to within 1e-5.

Latency (same architecture): about 6 ms per single request compute-only on GPU, about 45 ms on CPU.

Maintainer

Arsys InnoLab. Questions, bug reports or access requests: ask in the team's usual channel.

Internal use only

Internal use only. Access is granted on request. Not licensed for external distribution or public release.