DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Content Filter Triggered on Innocent Input

“Content filter triggered” can mean three different things: an input classifier rejected your request before generation, an output classifier stopped generation part-way, or the model itself declined and no filter was involved. They present similarly, they have different fixes, and the status code plus finish_reason separates them in one glance.

Three mechanisms that look identical

Mechanism Description
Input classifier A separate model scores your prompt before the main model sees it. Rejection is an HTTP 400 with a policy-related code, no usage, and no output tokens billed. The main model never ran.
Output classifier Generation starts, a classifier scores the output as it is produced, and generation is cut. HTTP 200, finish_reason of content_filter, partial or empty content, output tokens billed for what was produced.
The model's own refusal No filter at all. HTTP 200, finish_reason of stop, and a complete, well-formed answer that happens to be a polite decline. This is trained behaviour, not a policy system, and it is the only one of the three that prompting can move.

The third is by far the most commonly misdiagnosed. Teams spend days filing policy appeals for something that is not a policy decision, or rewriting prompts to get around a filter that never fired. Check the status code first.

Identifying which one you hit

try:
    r = client.chat.completions.create(...)
except BadRequestError as e:            # HTTP 400 family
    print("INPUT FILTER:", e.status_code, e.body)   # request never ran
else:
    c = r.choices[0]
    print("finish_reason:", c.finish_reason)
    print("content:", repr((c.message.content or "")[:200]))
    print("usage:", r.usage)
    # content_filter        -> output classifier
    # stop + a decline text -> the model refused
    # stop + normal text    -> nothing was filtered
Enter fullscreen mode Exit fullscreen mode

Some providers additionally return per-category annotations on the response — a set of categories with severity levels or boolean flags. Where those exist they are the most useful diagnostic available, because they name which category tripped, and the category is usually not the one you would have guessed. Log them.

The exact field names, category taxonomies and severity scales differ by provider and change between API versions. Read them from the response rather than hard-coding a list; a fixed list of category names is a piece of code that quietly stops matching.

Why benign text trips a classifier

A safety classifier is a small model trained on a distribution of harmful content, run at speed and at enormous volume. It sees a span of text without your application’s context, and it is tuned so that a false positive is cheaper than a false negative. That asymmetry is the whole explanation for most of what follows.

  • Clinical and anatomical language. Medical products hit this constantly. The vocabulary of a symptom description overlaps heavily with the vocabulary the classifier was trained to catch.
  • Security work. Exploit descriptions, malware analysis, phishing-detection prompts and penetration-test notes read like the thing they are defending against, because they quote it.
  • Violence in news, fiction and history. A summarisation product processing news wire copy will hit this on a predictable fraction of stories, and there is nothing wrong with the product.
  • Support conversations near self-harm or abuse. A customer wellbeing product processes exactly the language that is most heavily filtered. This one needs a designed answer, not a workaround.
  • Quoted user content. A moderation classifier reading text your own users wrote, or a RAG system retrieving a document that quotes something offensive, is scored on the quoted text with no signal that it is a quotation.
  • Non-English input. Classifier quality varies by language, and a benign idiom that translates awkwardly can score badly. If the failures cluster in one language, this is likely.
  • Cumulative context. A long conversation can accumulate enough borderline material for the whole to score higher than any single message. The tell is that the failure only appears after N turns, with no individual turn reproducing it.

Finding the span that triggered it

Guessing which sentence caused it wastes more time than measuring. Binary search over the messages, then over the sentences of the offending message.

def find_trigger(parts, send):
    """parts: list of strings. send(text) -> True if it passes.
    Returns the smallest prefix-contiguous subset that still fails."""
    if send("".join(parts)):
        return None                       # nothing here trips it
    lo, hi = 0, len(parts)
    while hi - lo > 1:                    # shrink to the failing half
        mid = (lo + hi) // 2
        if not send("".join(parts[lo:mid])):
            hi = mid
        elif not send("".join(parts[mid:hi])):
            lo = mid
        else:
            break                         # only the combination fails
    return parts[lo:hi]
Enter fullscreen mode Exit fullscreen mode

Two outcomes, both informative. A single span is returned: you have the trigger and can decide what to do about it. Nothing smaller than the whole fails: it is cumulative scoring, and trimming context is the lever rather than editing text.

What to do about each

If it is the model refusing

This is the one prompting genuinely fixes. State the legitimate purpose and the audience in the system prompt — a clinical tool for clinicians, a security tool for defenders — because the model is making a judgement and context changes the judgement. Avoid phrasing that reads like an attempt to get around a rule, which increases refusals rather than reducing them. And measure: refusal rates vary substantially between models on the same prompt, so a different model is a legitimate answer. False refusals goes further, and refusal UX covers what to show the user.

If it is an input or output classifier

  1. Check whether the thresholds are configurable. Some enterprise deployments let you set severity thresholds per category, and a documented business justification can raise them for a specific category. This is the clean fix where it is available, because it does not involve disguising anything.
  2. Mark quoted content as quoted. Delimit user or retrieved text clearly and say in the system prompt that the delimited region is material to be analysed rather than instructions to follow. This helps the main model reliably; whether it helps a separate input classifier depends on the provider, so measure rather than assume.
  3. Reduce what you send. If cumulative context is the cause, sending only the relevant turns fixes it and is a good idea anyway.
  4. Route the affected category elsewhere. Filters differ substantially between providers and between open-weight models run yourself. For a product whose whole domain is filtered — clinical, security, trust and safety — this is the structural answer rather than a workaround.
  5. Appeal, where the provider has a process. Slow, and worth starting in parallel with everything else rather than instead of it.

One thing not to do: obfuscating the text to slip past the classifier — leetspeak, spacing out words, encoding. It is unreliable, it usually violates the terms you agreed to, and it converts a technical problem into an account problem.

Designing for it instead of fighting it

If your domain is one of the ones listed above, filter hits are a permanent operating condition rather than a bug to close. Three things make that livable. Handle the failure explicitly, so a filtered request produces a clear message and a path forward rather than an empty screen. Log the category and the severity on every occurrence, so you can tell a rate change from an anecdote. And measure your own false-positive rate against a held-out set of legitimate inputs, because that number is what tells you whether a provider change has moved the ground under you.

It is also worth separating your own guardrails from the provider’s. A layer you control can be tuned, explained and audited; building a content safety layer and input versus output guardrails cover where each belongs.

Related

Top comments (0)