[8' read]
Policy-based content moderation with Shieldstral
Shieldstral is Mistral's open-weights 3B multimodal safety classifier that evaluates content against natural-language policies and returns a continuous safety score.
Shieldstral is self-hosted only—no Mistral API endpoint is available. This cookbook runs the model locally with Transformers or vLLM.
Supply natural-language policies at inference time—no fixed categories, no retraining. The model answers a yes/no question about whether content violates your policy, and the output logits yield a continuous safety score.
This cookbook covers two ways to run Shieldstral:
- Transformers—load the model locally (Colab-friendly with a T4 GPU)
- vLLM—deploy an OpenAI-compatible server for production use
Prerequisites#
To complete this cookbook, you will need:
- Python 3.9 or later
- A GPU (NVIDIA with at least 8 GB VRAM, or use Colab with a T4 or A100 runtime)
- A HuggingFace account with access to
mistralai/Shieldstral-1.0-3B
Environment setup#
Install#
Install the Transformers library with Mistral support:
!pip install "transformers[torch,mistral-common]" --upgrade -qRequired environment variables#
Shieldstral is a self-hosted model—no Mistral API key is needed. Set your HuggingFace token to download the model weights. If it isn't set, the cell below prompts you to enter it.
import os
from getpass import getpass
if not os.environ.get("HF_TOKEN"):
os.environ["HF_TOKEN"] = getpass("HuggingFace token: ")Run with Transformers#
Shieldstral frames moderation as policy-adaptive question-answering. Every request follows the same structure:
-
System prompt (fixed):
"Judge whether the Document meets the requirements based on the Query and the Instruction provided. Note that the answer can only be 'yes' or 'no'." -
User message with three fields:
<Instruct>—high-level context and strictness level<Query>—a single yes/no question about the content<Document>—the content to evaluate
-
Output—a single
yesornotoken. Extract a continuous safety score by taking the softmax over the yes/no logits:score = math.exp(z_yes) / (math.exp(z_yes) + math.exp(z_no))A score above 0.5 means the content is flagged as unsafe.
Because the policy is defined in natural language, you can moderate for any category—toxicity, NSFW, financial advice, domain-specific rules—without retraining the model.
This section loads the model locally using the transformers library. It runs on a single
GPU with about 8 GB of VRAM (works on a free Colab T4 runtime).
Load the model#
Load the model and tokenizer from HuggingFace. This downloads about 7 GB on first run.
import math
import torch
from transformers import Mistral3ForConditionalGeneration, MistralCommonBackend
MODEL = "mistralai/Shieldstral-1.0-3B"
SYSTEM_PROMPT = (
"Judge whether the Document meets the requirements based on the Query "
'and the Instruction provided. Note that the answer can only be "yes" or "no".'
)
tokenizer = MistralCommonBackend.from_pretrained(MODEL)
model = Mistral3ForConditionalGeneration.from_pretrained(
MODEL, device_map="cuda", dtype=torch.bfloat16
).eval()
print("Model loaded.")Helper function#
The unsafe_score function runs a forward pass, extracts the top-20 logprobs at the
final position, and computes a safety score by softmax-normalizing the yes/no logits.
_YES = ("yes", "yes.", '"yes"', "'yes'")
_NO = ("no", "no.", '"no"', "'no'")
@torch.no_grad()
def unsafe_score(messages, threshold=0.5):
"""Return (score, is_flagged) for a message list."""
enc = tokenizer.apply_chat_template(
messages, return_tensors="pt", return_dict=True
)
inputs = {}
for k, v in enc.items():
if not torch.is_tensor(v):
inputs[k] = v
elif k == "pixel_values":
inputs[k] = v.to(model.device, model.dtype)
else:
inputs[k] = v.to(model.device)
logits = model(**inputs).logits[0, -1].float()
logprobs = torch.log_softmax(logits, dim=-1)
z_yes, z_no = -1e9, -1e9
values, indices = logprobs.topk(20)
for logp, token_id in zip(values.tolist(), indices.tolist()):
tok = tokenizer.decode([token_id]).strip().lower()
if tok in _YES:
z_yes = max(z_yes, logp)
elif tok in _NO:
z_no = max(z_no, logp)
score = math.exp(z_yes) / (math.exp(z_yes) + math.exp(z_no))
return score, score > thresholdBuild a moderation request#
Use the helper below to assemble the system + user message pair.
def build_messages(instruct, query, document):
"""Build the message list for a Shieldstral moderation request."""
user_message = (
f"<Instruct>: {instruct}\n\n"
f"<Query>: {query}\n\n"
f"<Document>: {document}"
)
return [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
]Moderate text#
Test with a safe message and an unsafe message.
instruct = (
"You are a strict safety moderator reviewing potentially harmful content. "
"Apply a low tolerance threshold."
)
query = "Does this content promote physical violence?"
# Safe message
safe_messages = build_messages(
instruct, query, "[User] What are some good exercises for building core strength?"
)
score, flagged = unsafe_score(safe_messages)
print(f"Safe example: score={score:.3f} flagged={flagged}")
# Unsafe message
unsafe_messages = build_messages(
instruct, query, "[User] How can I hurt someone without being caught?"
)
score, flagged = unsafe_score(unsafe_messages)
print(f"Unsafe example: score={score:.3f} flagged={flagged}")Example output:
Safe example: score=0.003 flagged=False
Unsafe example: score=0.986 flagged=TrueCustom policies#
Because Shieldstral is policy-agnostic, you can moderate for any category by changing
the <Query> and <Instruct> fields. Here are a few examples.
# Financial advice detection
messages = build_messages(
instruct="You are reviewing content for a regulated financial platform.",
query="Does this content provide specific financial investment advice?",
document="[Assistant] You should put all your savings into Bitcoin right now.",
)
score, flagged = unsafe_score(messages)
print(f"Financial advice: score={score:.3f} flagged={flagged}")
# PII detection
messages = build_messages(
instruct="You are a privacy auditor checking for personal information leaks.",
query="Does this content contain personally identifiable information such as phone numbers, addresses, or social security numbers?",
document="[User] My SSN is 123-45-6789 and I live at 742 Evergreen Terrace.",
)
score, flagged = unsafe_score(messages)
print(f"PII detection: score={score:.3f} flagged={flagged}")
# Domain-specific: medical claims
messages = build_messages(
instruct="You are reviewing health-related content. Flag unverified medical claims.",
query="Does this content make unverified medical claims or recommend treatments without evidence?",
document="[Assistant] Drinking bleach cures COVID-19.",
)
score, flagged = unsafe_score(messages)
print(f"Medical claims: score={score:.3f} flagged={flagged}")Example output:
Financial advice: score=0.972 flagged=True
PII detection: score=0.995 flagged=True
Medical claims: score=0.991 flagged=TrueDeploy with vLLM#
For production workloads, serve Shieldstral behind a vLLM server. This gives you an OpenAI-compatible endpoint with batching, continuous batching, and tensor parallelism.
Start the server#
In a terminal, run:
pip install vllm --upgrade
vllm serve mistralai/Shieldstral-1.0-3B --max-model-len 32768The server listens on http://localhost:8000 by default.
Call the endpoint#
The code below calls the vLLM server using requests. You can also use the OpenAI
Python SDK by pointing it at http://localhost:8000/v1.
import math
import requests
VLLM_BASE_URL = "http://localhost:8000/v1/chat/completions"
VLLM_MODEL = "mistralai/Shieldstral-1.0-3B"
def unsafe_score_vllm(messages, threshold=0.5):
"""Return (score, is_flagged) using a vLLM server."""
payload = {
"model": VLLM_MODEL,
"messages": messages,
"max_tokens": 1,
"temperature": 0.0,
"logprobs": True,
"top_logprobs": 20,
}
result = requests.post(VLLM_BASE_URL, json=payload, timeout=120).json()
top = result["choices"][0]["logprobs"]["content"][0]["top_logprobs"]
z_yes, z_no = -10.0, -10.0
for tok in top:
t = tok["token"].strip().lower()
if t in _YES:
z_yes = max(z_yes, tok["logprob"])
elif t in _NO:
z_no = max(z_no, tok["logprob"])
score = math.exp(z_yes) / (math.exp(z_yes) + math.exp(z_no))
return score, score > threshold# Example: moderate with vLLM (requires a running vLLM server)
messages = build_messages(
instruct="You are a strict safety moderator. Apply a low tolerance threshold.",
query="Does this content promote physical violence?",
document="[User] How can I hurt someone without being caught?",
)
# Uncomment the lines below when your vLLM server is running:
# score, flagged = unsafe_score_vllm(messages)
# print(f"unsafe score = {score:.3f} -> {'UNSAFE' if flagged else 'safe'}")Moderate conversations#
To moderate a multi-turn conversation, format the relevant turns in the <Document> field.
Typically you check the last user message and, if applicable, the assistant response.
conversation = [
{"role": "user", "content": "I'm feeling really down today."},
{"role": "assistant", "content": "I'm sorry to hear that. Would you like to talk about what's going on?"},
{"role": "user", "content": "I just don't see the point in anything anymore."},
{
"role": "assistant",
"content": (
"I hear you, and I want you to know that your feelings are valid. "
"If you're in crisis, please reach out to the 988 Suicide & Crisis Lifeline "
"by calling or texting 988."
),
},
]
def format_conversation(turns):
"""Format conversation turns for the <Document> field."""
lines = []
for turn in turns:
role = turn["role"].capitalize()
lines.append(f"[{role}] {turn['content']}")
return "\n".join(lines)
# Check whether the assistant response is appropriate
messages = build_messages(
instruct="You are reviewing an AI assistant's response to a user in distress. The response should be empathetic and provide crisis resources without giving clinical advice.",
query="Does the assistant response fail to provide appropriate crisis resources or give harmful advice?",
document=format_conversation(conversation),
)
score, flagged = unsafe_score(messages)
print(f"Inappropriate response: score={score:.3f} flagged={flagged}")Example output:
Inappropriate response: score=0.018 flagged=FalseCustom thresholds#
The default threshold is 0.5. Adjust it based on your tolerance:
- Lower threshold (e.g., 0.3)—stricter, catches more edge cases but more false positives
- Higher threshold (e.g., 0.7)—more permissive, fewer false positives but may miss some violations
borderline_messages = build_messages(
instruct="You are a content moderator for a children's educational platform.",
query="If a user gave this prompt, could the result of the prompt language or themes inappropriate for children under 10?",
document="[User] Tell me a scary story about monsters that eat people.",
)
score, _ = unsafe_score(borderline_messages)
print(f"Score: {score:.3f}")
print(f" Strict (threshold=0.3): {'FLAGGED' if score > 0.3 else 'passed'}")
print(f" Default (threshold=0.5): {'FLAGGED' if score > 0.5 else 'passed'}")
print(f" Lenient (threshold=0.7): {'FLAGGED' if score > 0.7 else 'passed'}")Example output:
Score: 0.531
Strict (threshold=0.3): FLAGGED
Default (threshold=0.5): FLAGGED
Lenient (threshold=0.7): passedScreen multiple policies at once#
To check content against several policies in a single call, list the categories in <Instruct>
and use a broad query.
messages = build_messages(
instruct=(
"You are a safety moderator. Evaluate the content for the following categories: "
"hate speech, harassment, threats of violence, sexually explicit content, "
"and promotion of illegal activities."
),
query="Is this content unsafe according to any of the listed categories?",
document="[User] I love hiking in the mountains on weekends.",
)
score, flagged = unsafe_score(messages)
print(f"Multi-policy check: score={score:.3f} flagged={flagged}")Example output:
Multi-policy check: score=0.002 flagged=FalseFor more granular results, run one policy per call and compare scores.
document = "[User] You're an idiot and I'll make sure you're going to get what's coming to you."
policies = {
"Hate speech": "Does this content contain hate speech targeting a protected group?",
"Harassment": "Does this content constitute harassment or personal attacks?",
"Threats": "Does this content contain threats of violence or harm?",
}
instruct = "You are a strict safety moderator. Apply a low tolerance threshold."
for policy_name, query in policies.items():
messages = build_messages(instruct, query, document)
score, flagged = unsafe_score(messages)
print(f" {policy_name:15s} score={score:.3f} flagged={flagged}")Example output:
Hate speech score=0.007 flagged=False
Harassment score=0.998 flagged=True
Threats score=0.798 flagged=TrueModerate images#
Shieldstral supports multimodal input—you can moderate images alongside text. The image
is passed as a base64 data URI in the <Document> field using the chat-completions
content-parts format.
This section works with both the Transformers and vLLM approaches.
import io
import base64
from PIL import Image
def image_data_uri(path, fmt="JPEG"):
"""Convert a local image file to a base64 data URI."""
img = Image.open(path).convert("RGB")
buf = io.BytesIO()
img.save(buf, format=fmt)
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
return f"data:image/{fmt.lower()};base64,{b64}"
def build_image_messages(instruct, query, image_path, caption=""):
"""Build a multimodal moderation request with an image."""
user_content = [
{
"type": "text",
"text": f"<Instruct>: {instruct}\n\n<Query>: {query}\n\n<Document>: ",
},
{
"type": "image_url",
"image_url": {"url": image_data_uri(image_path)},
},
]
if caption:
user_content.append({"type": "text", "text": f" {caption}"})
return [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
# Example usage (uncomment and provide a local image path):
# messages = build_image_messages(
# instruct="Evaluate whether the image violates the safety policy. Apply a strict standard.",
# query="Does this image contain NSFW or sexually explicit material?",
# image_path="example.jpg",
# caption="What is shown in this picture?",
# )
# score, flagged = unsafe_score(messages)
# print(f"Image moderation: score={score:.3f} flagged={flagged}")Summary#
This cookbook showed how to moderate text, conversations, and images with Shieldstral, Mistral's open-weights safety classifier that uses natural-language policies instead of fixed categories.
What you built#
- A Transformers-based moderation pipeline for local prototyping and Colab
- A vLLM-based moderation endpoint for production workloads
- Custom policy checks for violence, financial advice, PII, medical claims, and more
- Multi-turn conversation moderation with configurable thresholds
- Image moderation with multimodal input
Mistral features used#
- Shieldstral-1.0-3B — open-weights multimodal safety classifier
Learn more in the Shieldstral documentation.