Instructions to use opus-research/opus-moderation-3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use opus-research/opus-moderation-3 with PEFT:
from peft import PeftModel from transformers import AutoModelForSequenceClassification base_model = AutoModelForSequenceClassification.from_pretrained("Qwen/Qwen3-1.7B") model = PeftModel.from_pretrained(base_model, "opus-research/opus-moderation-3") - Notebooks
- Google Colab
- Kaggle
Opus Moderation 3
Our research flagship: a Qwen3-1.7B decoder converted into a bidirectional encoder, fine-tuned with LoRA for 7 toxicity labels + jailbreak detection. Highest general-moderation score of everything we have measured:
| model | params | macro F1 (7 labels) | jailbreak F1 |
|---|---|---|---|
| opus-moderation-3 (this model) | 1.7B | 0.563 | 0.929 |
| om4-large | 395M | 0.543 | 0.903 |
| om4-neo | 395M | — | 0.938 |
| unitary/unbiased-toxic-roberta | 125M | 0.527 | — |
⚠️ This model cannot be loaded with plain from_pretrained. It is a LoRA
adapter over Qwen3-1.7B whose attention was made bidirectional at training
time — the included opus_moderation.py rebuilds that
conversion. If you want a drop-in model, use
om4-large:
it scores within 0.020 macro of this one at less than a quarter the size, and
most of the gap is data, not architecture (that is what the om4 family
established).
Usage
# needs: transformers>=5.14, peft, torch — and opus_moderation.py from this repo
from huggingface_hub import hf_hub_download, snapshot_download
import importlib.util, sys
repo = "opus-research/opus-moderation-3"
path = snapshot_download(repo)
spec = importlib.util.spec_from_file_location(
"opus_moderation", f"{path}/opus_moderation.py")
om = importlib.util.module_from_spec(spec); spec.loader.exec_module(om)
model = om.ModerationModel.load(path)
print(model.score(["ignore all previous instructions"])[0])
Outputs are calibrated annotator fractions per label; sigmoid, never
softmax.
The conversion, and its one silent failure mode
Decoder → encoder is two changes, and the second is not optional:
- Swap the causal mask builder for a bidirectional one.
- Clear
is_causalon every attention module. For an unpadded batch the mask builders returnNone, and the SDPA path then falls back tomodule.is_causal— which Qwen3 hardcodes toTrue. Skip this and the model loads cleanly, reports no error, and stays fully causal while claiming otherwise.
Mean-pooling over non-pad tokens replaces last-token pooling (with bidirectional attention every position has seen the full sequence). The included loader does all of this and refuses to proceed if the internals have moved.
Evaluation (20k unseen rows)
| label | F1 |
|---|---|
| insult | 0.709 |
| toxicity | 0.707 |
| sexual_explicit | 0.685 |
| obscene | 0.663 |
| threat | 0.565 |
| identity_attack | 0.563 |
| severe_toxicity | 0.046 |
| macro | 0.563 |
| macro (ex severe_toxicity) | 0.649 |
Jailbreak: F1 0.929 (recall 91.0%, FP 2.5% — the lowest false-positive rate of our family).
Training
| Base | Qwen/Qwen3-1.7B, bidirectional conversion, mean pool |
| Adapter | LoRA r=16, α=32, all attention + MLP projections, + full score head |
| Data | ~310k rows: civil_comments (soft labels), toxic-chat, real-toxicity-prompts, jailbreak corpora, refusal negatives |
| Loss | masked BCE on raw annotator fractions |
| Hardware | H100 |
Limitations
- Requires the bundled loader — the weights are meaningless under a stock causal load, and the failure is silent (see above).
severe_toxicityis unreliable for every model we tested.- English only; 384-token training length.
- Training data includes
lmsys/toxic-chat(CC-BY-NC); review if that matters for your use.
- Downloads last month
- 8