Skip to content

Request blocked by provider content policy

The difference from the safety trigger: there the model answered with a refusal, here the provider did not run the request at all — it returned a moderation error (content_filter, responsible_ai_policy_violation, content_policy_violation). To you it looks like a 400/403 in production, and the user then sees a blank screen or a generic error.

Such blocks are almost always about a specific prompt or a specific user input. A push with a truncated and redacted sample gives you exactly what you need to fix it without digging through logs.

The key point is to distinguish a content-policy error from other 400s. Each provider has its own code, so we collect them into one set:

import os, re, requests
NOTIFLY_URL = os.environ["NOTIFLY_URL"]
NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]
POLICY_CODES = {
"content_filter", # OpenAI / Azure
"content_policy_violation", # OpenAI images
"responsible_ai_policy_violation", # Azure
"safety", # Vertex / Gemini
}
def notify(title, msg, prio):
requests.post(f"{NOTIFLY_URL}/message",
params={"token": NOTIFLY_TOKEN},
json={"title": title, "message": msg, "priority": prio},
timeout=5)
# rough redaction: strip e-mails, phones, long digit sequences
def redact(text: str, limit=400) -> str:
text = re.sub(r"[\w.+-]+@[\w-]+\.[\w.-]+", "[email]", text)
text = re.sub(r"\+?\d[\d\s()-]{7,}\d", "[phone]", text)
text = re.sub(r"\b\d{6,}\b", "[num]", text)
return text[:limit]

Now wrap the provider call:

import openai
def ask(user_input: str):
try:
return client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_input}],
)
except openai.BadRequestError as e:
body = getattr(e, "body", {}) or {}
code = (body.get("code")
or body.get("error", {}).get("code")
or "")
if code in POLICY_CODES:
notify("🚫 Content-policy: запрос заблокирован",
f"Provider: openai\nCode: {code}\n\n"
f"Ввод (обезличен):\n{redact(user_input)}",
prio=7)
raise

priority=7 — this is not a security incident but a “fix the prompt/input” signal, so a normal loudness rather than a “ten”.

Deduplication: the same prompt fails hundreds of times

Section titled “Deduplication: the same prompt fails hundreds of times”

If it is your system prompt that is broken, not a one-off user input, the error will fire on every request. You want to send the first case and then a periodic aggregate — the common pattern:

import time
state = {"first_ts": 0, "count": 0}
WINDOW = 3600
def maybe_alert(code, sample):
now = time.time()
if now - state["first_ts"] > WINDOW:
state.update(first_ts=now, count=0)
notify("🚫 Content-policy (первый за час)",
f"Code: {code}\n{sample}", prio=7)
state["count"] += 1
if state["count"] in (25, 250):
notify(f"🚫 Content-policy ×{state['count']}",
"Похоже, задет ваш системный промпт или шаблон — а не разовый ввод.",
prio=9)
  • Anthropicstop_reason == "refusal" in the response (the request ran but the model refused), or a 400 with error.type == "invalid_request_error".
  • Vertex / Gemini — the response has promptFeedback.blockReason and safetyRatings; the block does not raise an exception, you must check it in the body.
  • Providers without a webhook — if moderation arrives only by email, wrap the mailbox in an Email Inbox and alert on the emails.
  • the provider and the exact policy code;
  • the truncated and redacted input (no raw PII in a push!);
  • which category fired, if the provider returns it (violence / hate / …);
  • the session/request id, to find the full log in S3/Sentry.