How to Use Jev for Content Moderation (Consistent Decisions)
TL;DR: Moderation is not one hard problem — it is many small decisions. Run a
choicecall over a flat category list, fall back to ajudgmentcall on the specific policy rule when confidence is low, and store therationaleas an audit record for every removal. Consistency comes from the fixed question format; explainability comes from the rationale. Flow, thresholds, and appeal path below.
Category design first
Two levels, kept deliberately simple:
- Level 1 — choice call: exactly one category from a fixed list, e.g.
spam,harassment,adult,violence,none. - Level 2 — judgment call per rule: when Level 1 is
unclearor confidence is below threshold, ask a yes/no question against one specific rule (“Does this text contain a credible threat?”). Example fixture:
{"answer":"yes","confidence":0.97,"rationale":"The message threatens physical harm contingent on a payment."}
This mirrors how human moderation teams work: triage first, rule-by-rule review for the hard cases. It also keeps each question small, which is exactly what a judgment model wants.
Moderation flow
Content submitted
|
v
Jev choice call (category: spam / harassment / adult / violence / none)
|
+--> category = none and confidence high --> publish
|
+--> confidence low or category uncertain
| |
| v
| Jev judgment call on the specific policy rule (yes / no / unclear)
| |
| +--> clear yes / no --> act, store rationale
| |
| +--> still unclear --> human review queue
|
+--> clear violation --> remove, store rationale, notify with appeal link
Every removal stores: content id, category, answer, confidence, rationale, and model version. That tuple is your audit trail.
Minimal implementation
import os, json, requests
URL = "https://openrouter.ai/api/v1/chat/completions"
# Confirm the exact model slug on the OpenRouter model page.
MODEL = "typesafe/jev-1.13"
CATEGORIES = ["spam", "harassment", "adult", "violence", "none"]
def call(system: str, text: str) -> dict:
resp = requests.post(
URL,
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json={"model": MODEL, "temperature": 0, "messages": [
{"role": "system", "content": system},
{"role": "user", "content": text}]},
timeout=30,
)
resp.raise_for_status()
# example fixture (judgment):
# {"answer":"yes","confidence":0.97,"rationale":"..."}
return json.loads(resp.json()["choices"][0]["message"]["content"])
def moderate(text: str, low=0.85) -> dict:
c = call("Pick exactly one category from: "
+ ", ".join(CATEGORIES) + ".", text)
if c["answer"] != "none" and c["confidence"] >= low:
return {"action": "remove", **c}
if c["confidence"] >= low:
return {"action": "publish", **c}
j = call("Does this text violate the harassment policy? "
"Answer yes, no, or unclear.", text)
if j["answer"] == "unclear" or j["confidence"] < low:
return {"action": "human-review", **j}
return {"action": "remove" if j["answer"] == "yes" else "publish", **j}
Thresholds and the appeal path
| Stage | Setting | Recommendation |
|---|---|---|
| Level 1 choice | Auto-act threshold | confidence >= 0.85 for both remove and publish |
| Level 2 judgment | Borderline rule check | Fire when Level 1 is low-confidence; act only on clear yes/no |
| Human queue | Trigger | Any unclear answer, or confidence below threshold after Level 2 |
| Audit record | Always stored | Content id, category, answer, confidence, rationale, model version |
| Appeals | User-facing | Notification includes the rationale summary and a one-click appeal |
For why the question format matters this much, see the guide on state and questions, and for threshold tuning see confidence and fallback. The multilingual review-moderation case study shows this two-level design holding up across languages.
Applies to Jev 1.13.
FAQ
- How do I structure moderation categories? A flat list for the first choice call (spam, harassment, adult, violence, none), plus a per-rule judgment call for borderline cases — not one giant multi-label prompt.
- Why keep the rationale? It is the audit record: it explains each removal to users, appeals reviewers, and regulators in a way manual notes never consistently do.
- What happens when confidence is low? A second judgment call on the specific rule, then a human review queue if it is still unclear — with the rationale attached either way.