Skip to content

Configuring Content Filter Severity Levels on Azure OpenAI

10 min read · updated August 11, 2026

The default filter is a resource-wide decision that somebody made for you. A custom configuration is a per-deployment object, which means the summarisation service and the customer-facing chatbot do not have to share a threshold.

The shape of a filter configuration

Azure OpenAI content filtering runs as a separate classification pass on both sides of the model: once over the prompt before it is submitted, once over the completion before it is returned. The configuration that governs it is an ARM resource in its own right, Microsoft.CognitiveServices/accounts/raiPolicies, created on the Azure OpenAI account and then referenced by name from one or more deployments.

That last part is the useful bit and it is easy to miss: the policy lives at resource scope, the association lives at deployment scope. One resource can hold several policies, and each deployment names exactly one. So the correct unit of change is a new deployment with a new policy, not an edit to the policy every deployment shares.

Each policy is a list of content filter entries. An entry names a category, says whether it applies to the prompt or the completion, sets a severity threshold, and says whether exceeding that threshold blocks the request or merely records it.

Four levels, and only three you can set

Microsoft’s content filter documentation describes four harm categories — hate, sexual, violence and self-harm — classified across four severity levels: safe, low, medium and high. Content classified as safe is labelled in the annotation output but is never filtered and is not configurable, so in practice the threshold you choose is one of three.

  • Low — the strictest setting. Anything above safe is caught. Expect false positives on clinical, legal and news-adjacent text; the self-harm category in particular will fire on medical documentation.
  • Medium — the default position for most categories. Catches medium and high.
  • High — only the most severe content is filtered. This is the loosest setting available without a separate approval.

Turning a filter off entirely is not a threshold. Microsoft documents that only customers approved for modified content filtering have full control including the ability to disable filters, which is a separate application. Plan around three levels, not four and an off switch.

The optional detections

The four harm categories are not the whole policy. Alongside them sit a set of detections that are off by default, graded differently, and answer to different concerns. They are worth enumerating because “we configured the content filter” usually means the four categories and nothing else, and the omissions are the ones with legal rather than reputational consequences.

  • Prompt shields — two separate detections. One looks for jailbreak attempts in the user prompt; the other looks for indirect attacks, meaning injected instructions embedded in documents you placed in the context rather than in anything the user typed. The second is the one that matters for any retrieval-augmented system, because your threat model there includes whoever wrote the document you indexed.
  • Protected material for text and for code — two more detections, covering known copyrighted text and code matching public repositories. The text one is what fires on song lyrics. It is also tied to Microsoft’s Customer Copyright Commitment, so switching it off is a commercial decision as much as a technical one.
  • Groundedness — detects completions unsupported by the source material supplied in the context. Only meaningful where you are supplying sources; on an ungrounded chat deployment it has nothing to compare against.

These behave as flags rather than graded severities. In annotations they appear as a detected and filtered pair — for example "protected_material_text": {"detected": true, "filtered": true} — so code written to read a severity string from every category will find none here and may treat the field as absent.

Annotate without blocking

The most useful configuration for a system going into production is one that blocks nothing and records everything. Microsoft’s RAI policy API exposes this as a per-entry setting: with blocking disabled, the category still runs and still returns annotations, but the request is not rejected.

Run that for a week against real traffic and you get the one number nobody can guess in advance — how many of your genuine requests a low threshold would have rejected. Setting thresholds first and discovering the false-positive rate from support tickets is the expensive ordering.

Annotate-only mode is a safety decision, not just a diagnostic one. While it is in force, harmful completions reach your application and it is on you to act on the annotation. Use it deliberately and for a bounded period.

Create the policy and attach it

  1. Create the RAI policy on the account. This is a control-plane PUT to /providers/Microsoft.CognitiveServices/accounts/{account}/raiPolicies/{name}, with a body listing one entry per category, each naming the category, whether it applies to prompt or completion, its severity threshold, and whether it blocks. Creating one requires the Cognitive Services Contributor role — Microsoft documents that neither Cognitive Services OpenAI User nor Cognitive Services OpenAI Contributor can create customised guardrails.
  2. Attach it to a deployment by setting raiPolicyName in the deployment’s properties. In Bicep that sits alongside the model block:
resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = {
  parent: openAiAccount
  name: 'chat-strict'
  sku: {
    name: 'GlobalStandard'
    capacity: 100
  }
  properties: {
    model: {
      format: 'OpenAI'
      name: 'gpt-4.1'
      version: '2025-04-14'
    }
    raiPolicyName: 'strict-customer-facing'
  }
}
  1. Verify by sending a request you expect to be caught and one you expect to pass, and reading the annotations rather than trusting the absence of an error.

Read the result back

A blocked prompt comes back as an HTTP 400 with an error whose code is content_filter and whose inner error carries the per-category results. A blocked completion is different and catches people out: it is an HTTP 200 with "finish_reason": "content_filter" on the choice. Client code that only inspects the status code will treat a filtered completion as a successful empty answer.

Both cases are billed. Microsoft states that a prompt filtered at status 400 is charged for prompt evaluation, and a completion filtered at status 200 is charged for both prompt and completion tokens generated before filtering. A tight filter on a chatty model is a real line on the invoice.

On a successful call, annotations arrive under prompt_filter_results for the input and content_filter_results on each choice for the output, each category carrying a filtered boolean and a severity string. Log the severities, not just the booleans — the distribution of low against safe over a week is what tells you whether your threshold has any margin.

Streaming changes the timing of all of this rather than the content. The default streaming behaviour buffers completion content, runs the filter over each buffer and releases it, so you receive chunks rather than tokens; an Asynchronous Filter configuration removes the buffer and delivers the filtering signal late. That trade-off, and the annotation offsets it introduces, is the subject of the streaming page.