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.
Catch the moderation error in code
Section titled “Catch the moderation error in code”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 sequencesdef 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) raisepriority=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 timestate = {"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)Other providers
Section titled “Other providers”- Anthropic —
stop_reason == "refusal"in the response (the request ran but the model refused), or a400witherror.type == "invalid_request_error". - Vertex / Gemini — the response has
promptFeedback.blockReasonandsafetyRatings; 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.
What to put in the alert text
Section titled “What to put in the alert text”- 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.