Skip to main content
Lesson 2 of 12 · AI Moderation and Security with Mistral

Custom Guardrails: Block Before the Model

Lesson 2 of 12 Updated November 3, 2026 Developer
On this page
  1. The Principle of Custom Guardrails
  2. How It Works
  3. The guardrails Parameter Format
  4. The Eleven Categories
  5. The Three Levels of Application
  6. What Guardrails Return
  7. Choosing the Right Level
  8. Key Takeaways

Lesson rewritten on November 3, 2026. The previous version described a fictional guardrails parameter (a dictionary with enabled), non-existent categories (hate, violence), a missing conversation.send() method in the SDK, and imaginary JSON responses. The official format is a list of moderation_llm_v2 configurations, and the responses below match the documentation. The code has been verified against the mistralai SDK types version 3.7.0. Sources: Moderation & Guardrailing · mistralai Python SDK.

The Principle of Custom Guardrails

Mistral’s Custom Guardrails act as an input filter: they analyze the user’s prompt before it reaches the generation model. If the content exceeds any threshold you’ve set, the request is blocked with an HTTP 403 error. The model never sees the problematic prompt.

Keep two limitations in mind from the start. First, guardrails moderate input only; they do not check what the model generates. That is the role of the Moderation API (Lesson 5). Second, they replace work you would otherwise have to code yourself: calling the Moderation API, comparing scores against your thresholds, and rejecting the request.

How It Works

When you add guardrails to a request, the flow on Mistral’s side is as follows:

  1. Your application sends the request with the guardrails parameter.
  2. The classifier mistral-moderation-2603 scores the prompt in each category.
  3. If a score exceeds its threshold, the API returns a 403 error and the model is not called.
  4. Otherwise, the model processes the request normally.
  5. The response includes a guardrails field containing the evaluated scores.
Flow: prompt → [mistral-moderation-2603] → score > threshold? → 403 (blocked)
                                           → else             → [LLM] → response + scores

The decision relies on a score compared to a threshold you set, category by category, between 0 and 1. A low threshold blocks more content; a threshold of 1 explicitly disables the category.

The guardrails Parameter Format

The parameter is a list. Each element is an object containing a moderation_llm_v2 configuration and, alongside it, the block_on_error option:

guardrails = [
    {
        "block_on_error": True,          # block if moderation itself fails
        "moderation_llm_v2": {
            "custom_category_thresholds": {
                "jailbreaking": 0.3,
                "criminal": 0.2,
            },
            "ignore_other_categories": False,
            "action": "block",
        },
    }
]
KeyLocationRole
custom_category_thresholdsinside moderation_llm_v2Threshold from 0 to 1 per category; 1 disables the category
ignore_other_categoriesinside moderation_llm_v2True: only listed categories are evaluated
actioninside moderation_llm_v2"block" to reject the request if a threshold is exceeded
model_nameinside moderation_llm_v2, optionalModeration model other than the default mistral-moderation-2603
block_on_erroralongside moderation_llm_v2True: the request is blocked if evaluation fails

You can place multiple objects in the list: the request is blocked as soon as any one of them triggers. Each object contains only one moderation_llm_v2 configuration. An older configuration, moderation_llm_v1, still exists in the SDK with categories from the deprecated mistral-moderation-2411 model (deprecated March 31, 2026): do not use it for new projects.

The Eleven Categories

The keys in custom_category_thresholds are fixed; another key, such as violence or hate, corresponds to nothing:

KeyWhat it detects
sexualExplicit sexual content
hate_and_discriminationHate or discrimination based on protected characteristics
violence_and_threatsDescribed, glorified, or threatened physical violence
dangerousExtremely dangerous behaviors
criminalIllegal activities
selfharmSelf-harm, suicide, eating disorders
healthRequests for detailed or personalized medical advice
financialRequests for detailed or personalized financial advice
lawRequests for detailed or personalized legal advice
piiPersonal data requested or shared
jailbreakingAttempts to bypass model rules

Lesson 6 revisits each of these and discusses threshold selection.

The Three Levels of Application

The same parameter is accepted in three places. Your choice determines where your security policy lives and who might forget to apply it.

Level 1: A Chat Completions Request

You pass guardrails in the chat.complete() call. Each request carries its own rules: practical for exploration, but risky at scale, since a single call without the parameter slips through unchecked.

import os

from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "What is the distance from the Earth to the Moon?"}],
    guardrails=[
        {
            "block_on_error": True,
            "moderation_llm_v2": {
                "custom_category_thresholds": {"sexual": 0.1, "selfharm": 0.1},
                "ignore_other_categories": False,
                "action": "block",
            },
        }
    ],
)
print(response.choices[0].message.content)

Use case: Prototyping, one-off calls, tuning thresholds.

Level 2: A Conversation

With the Conversations API, you pass guardrails at startup, in client.beta.conversations.start():

conversation = client.beta.conversations.start(
    model="mistral-large-latest",
    inputs="Hello, can you help me prepare for an interview?",
    guardrails=[
        {
            "block_on_error": True,
            "moderation_llm_v2": {
                "custom_category_thresholds": {
                    "sexual": 0.2,
                    "hate_and_discrimination": 0.1,
                    "violence_and_threats": 0.3,
                    "selfharm": 0.1,
                },
                "action": "block",
            },
        }
    ],
)
for entree in conversation.outputs:
    if entree.type == "message.output":
        print(entree.content)

Subsequent messages are sent with client.beta.conversations.append(conversation_id=conversation.conversation_id, inputs=...). This method does not accept a guardrails parameter in SDK 3.7.0, and the documentation does not specify whether the startup guardrails apply to subsequent turns. For guaranteed protection on every turn, attach guardrails to an agent.

Use case: One-off conversation without an agent, or overriding an agent’s guardrails for a specific request.

Level 3: An Agent

Guardrails passed to client.beta.agents.create() become part of the agent’s definition. According to the documentation, all conversations using this agent inherit them without needing to repeat them. However, a conversation request can override them by passing its own guardrails.

agent = client.beta.agents.create(
    model="mistral-large-latest",
    name="Secure Assistant",
    instructions="You are a professional customer service assistant.",
    guardrails=[
        {
            "block_on_error": True,
            "moderation_llm_v2": {
                "custom_category_thresholds": {
                    "sexual": 0.1,
                    "hate_and_discrimination": 0.1,
                    "violence_and_threats": 0.2,
                    "criminal": 0.1,
                    "selfharm": 0.1,
                    "jailbreaking": 0.3,
                },
                "ignore_other_categories": False,
                "action": "block",
            },
        }
    ],
)

conversation = client.beta.conversations.start(agent_id=agent.id, inputs="Hello!")

Use case: Production, when multiple teams or services call the same assistant.

What Guardrails Return

When the request passes, the API response includes, alongside standard fields, a guardrails field: a list with one result per guardrail. Here is that field from the documentation example:

{
  "guardrails": [
    {
      "moderation_llm_v2": {
        "action": "pass",
        "categories": {
          "sexual": { "score": 0.03, "violated": false },
          "selfharm": { "score": 0.05, "violated": false },
          "violence_and_threats": { "score": 0.0, "violated": false },
          "hate_and_discrimination": { "score": 0.0, "violated": false }
        }
      }
    }
  ]
}

With ignore_other_categories set to True, only the categories listed in your thresholds appear in this result. In the Python SDK, a conversation response exposes this field (conversation.guardrails). The chat.complete() response, however, has no designated field for it in SDK 3.7.0: it gets lost there. If you want to log scores—and it is useful to know later whether your thresholds are too loose or too tight—use conversations or read the raw HTTP response.

When a threshold is exceeded, the API returns a 403 error whose body details the decision, category by category:

{
  "error": { "message": "Content blocked by guardrail", "status": 403 },
  "guardrails": {
    "results": {
      "moderation_llm_v2": {
        "model_name": "mistral-moderation-2603",
        "decisions": {
          "sexual": { "threshold": 0.1, "score": 0.3, "violated": true },
          "selfharm": { "threshold": 0.1, "score": 0.05, "violated": false }
        },
        "violated": true,
        "action": "block"
      }
    }
  }
}

The SDK then raises errors.SDKError, where status_code is 403 and body contains this JSON:

import json

from mistralai.client import errors

try:
    response = client.chat.complete(
        model="mistral-large-latest",
        messages=[{"role": "user", "content": user_message}],
        guardrails=guardrails,
    )
except errors.SDKError as e:
    if e.status_code != 403:
        raise
    decisions = json.loads(e.body)["guardrails"]["results"]["moderation_llm_v2"]["decisions"]
    violated = [name for name, d in decisions.items() if d["violated"]]
    print(f"Request blocked: {violated}")

This payload allows you to explain a block during an appeal: which category, what score, what threshold.

Finally, there is the degraded case. If the evaluation itself fails and block_on_error is True, the request is refused with error code 3201 and the message Request blocked due to error in guardrail evaluation and block_on_error is set to True. This is fail-safe mode: when in doubt, block. With block_on_error set to False (its default value in the SDK), the request would pass unchecked.

Choosing the Right Level

CriterionChat CompletionsConversationAgent
Where guardrails are declaredIn every callAt startup (start)At agent creation
InheritanceNoneUndocumented for subsequent turnsAll agent conversations
Overridable——Yes, by a conversation request
Use caseTests, tuning thresholdsOne-off conversationProduction

In practice, tune your thresholds on Chat Completions requests using real cases, then move the policy to an agent as soon as the code leaves your machine: there will be no more forgotten calls.

Key Takeaways

  • Guardrails block prompts before the model via a 403 error; they do not check output
  • The parameter is a list of objects {"block_on_error": ..., "moderation_llm_v2": {...}}, not a dictionary with enabled
  • Eleven categories with fixed names (hate_and_discrimination, violence_and_threats, …), each with a threshold from 0 to 1; 1 disables
  • Three levels: chat.complete(), conversations.start(), agents.create(); agents pass their guardrails to all their conversations
  • block_on_error=True ensures fail-safe behavior (error 3201)