Skip to content

Why Safety Filters Flag Ordinary Hindi Religious Terms

10 min read · updated August 11, 2026

A prompt asking for a summary of a passage from the Ramayana, or a product description for a puja kit, comes back as content_policy_violation. Nothing in it is unsafe. The block is real, it is reproducible, and it is caused by three separate mechanisms that need three different responses.

The error you are looking at

The wording differs by provider, and the difference tells you which system rejected the request. These are the shapes you will see:

// OpenAI-style prompt rejection: HTTP 400
{
  "error": {
    "message": "Your request was rejected as a result of our safety system.",
    "type": "invalid_request_error",
    "code": "content_policy_violation"
  }
}

// Azure OpenAI: HTTP 400 on the prompt, or a completion cut short
{
  "error": {
    "code": "content_filter",
    "message": "The response was filtered due to the prompt triggering Azure OpenAI's content management policy."
  }
}

// Gemini: HTTP 200 with no candidate text at all
{
  "promptFeedback": {
    "blockReason": "SAFETY",
    "safetyRatings": [
      { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "MEDIUM" }
    ]
  }
}

The last one is the one that causes production incidents, because the HTTP status is 200. If your client reads candidates[0].content.parts[0].text without checking promptFeedback.blockReason, a blocked Hindi request looks exactly like an empty answer and ships as a blank field.

Three different collisions

Transliteration collisions

Romanised Hindi is a string of Latin letters, and a keyword-based classifier or denylist compares it against strings from other languages. Devotional vocabulary is full of words that are ordinary English tokens or near-misses for flagged ones. Bhang, the cannabis preparation consumed during Holi and Mahashivratri, is genuinely a drug term and genuinely a religious-observance term in the same six letters. Bali means a ritual offering and is a place name and a homograph in several other languages. Names of deities and epic figures overlap with English words and with each other once the diacritics are gone.

The key property of this collision is that it disappears in Devanagari. If your product romanises Hindi before sending it — many do, for tokeniser-cost reasons — the romanisation is creating the collision. See Devanagari to Latin transliteration for why the mapping is lossy in both directions.

The content genuinely is about violence

This is the largest cause and the least discussed. Hindu religious literature is, in substantial part, narrative about war, killing, sacrifice and the destruction of demons. The Mahabharata is a war epic. The Devi Mahatmya is a battle text. A faithful summary of a passage in which a goddess beheads an asura contains beheading, and a topical violence classifier has a strong feature for beheading and no feature at all for “this is scripture”.

Vocabulary compounds it: trishul is a trident, gada is a mace, chakra is a discus weapon, asura and rakshasa are demons. In a devotional context these are iconography. In a bag of features they are a weapons list.

Tokenisation and score variance

Moderation classifiers are small models with small vocabularies. On Devanagari input, much of the text falls back to byte-level tokens, so the classifier sees a long sequence of low-information units rather than words — the effect described in Hindi token costs. Its features are degenerate, its scores cluster near the middle of the range, and a fixed threshold then produces effectively arbitrary decisions on borderline items. This is why the same prompt can pass and then fail after a trivial edit.

Why the classifier is not malfunctioning

It is worth being precise about this, because the diagnosis determines the fix. The classifier was trained to answer “does this text describe violence?” and on a passage about a demon-slaying it answers yes, correctly. The question you wanted answered is “is this text harmful?”, and no one trained it on that, because separating scriptural narrative from incitement requires a feature — provenance and register — that its training data does not contain in Hindi.

So this is not a bug to report and wait on. It is a category error in the system design, and it has to be handled on your side of the API with context, configuration and fallback. Treating it as a defect in the provider produces a ticket; treating it as a mismatch between the question asked and the question needed produces a working product.

Finding out what actually fired

  1. Call the moderation endpoint directly with the same text, before calling the model. OpenAI and Google both expose one, and it returns per-category scores rather than a boolean. You need the category, not the verdict.
  2. Determine whether the block was on the prompt or the completion. On Azure this is the difference between a 400 and a truncated response with finish_reason of content_filter; on Gemini it is promptFeedback.blockReason against candidates[0].finishReason. A prompt block means your input text is the problem; a completion block means the model’s own output is, which is a different fix.
  3. Bisect the text. Split the prompt in half and resubmit each half. Repeat until you have the smallest span that still blocks. This usually takes four or five calls and tells you immediately whether you are looking at a term collision or a narrative-content block.
  4. Test the same span in Devanagari and in romanisation. If only the romanised form blocks, it is a transliteration collision and the fix is to stop romanising.
  5. Record every blocked span in a regression file with the category and score. Without it you cannot distinguish a fix from a threshold that happened to move.

Getting the request through

  • Send the source script. Devanagari removes the entire transliteration collision class at no cost beyond tokens.
  • Put the framing where the filter can see it. The classifier scores the text it is given, and on most providers that is the whole prompt. Naming the source and task inside the same field — that this is a passage from a named epic and you want a summary — gives it context it would otherwise not have. This helps against narrative-content blocks and does nothing against term collisions, which is another reason to know which one you have.
  • Configure per category, not globally. Where the provider exposes thresholds, raise only the one that fired. Gemini takes explicit safety settings on the request:
{
  "contents": [{ "role": "user", "parts": [{ "text": "..." }] }],
  "safetySettings": [
    { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_ONLY_HIGH" },
    { "category": "HARM_CATEGORY_HARASSMENT",        "threshold": "BLOCK_MEDIUM_AND_ABOVE" }
  ]
}

Azure’s content filter is configured on the deployment rather than per request, and some categories cannot be disabled at all; annotate-only mode, where available, returns the category annotations without blocking, which is the right setting for a diagnostic environment and a deliberate risk decision for production.

  • Fail loudly on Gemini. Check blockReason and finishReason on every response and turn a block into an error your code handles. Never let it become an empty string.
  • Have a fallback path. Filters are independently trained per provider, so a prompt that a strict violence classifier rejects frequently passes elsewhere. A deterministic retry-then-fall-back policy is more reliable than prompt rewording, which is a guess.
Category names, thresholds and which of them can be adjusted are set by each provider and change between API versions. Check the current documentation before hard-coding a category string; the fields above are the shapes at the time of writing.