fix(guardrails): accept both HTTP 400 and 403 as intentional guardrail blocks

The litellm_content_filter previously raised HTTPException(403) for all
content blocks. _is_guardrail_intervention only recognised 400, so those
blocks were logged as guardrail_failed_to_respond (API failure) rather than
guardrail_intervened in Langfuse, DataDog, and OTEL spans.

This makes the transition backward-compatible: _is_guardrail_intervention
now accepts both 400 AND 403, and adds 6 unit tests covering the new behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
oss-agent-shin 2026-05-22 21:04:23 +00:00
parent c945e0e2c6
commit 6e820a9376
2 changed files with 86 additions and 7 deletions

View file

@ -737,18 +737,23 @@ class CustomGuardrail(CustomLogger):
(this was logged previously as an API failure - guardrail_failed_to_respond).
Guardrails signal intentional blocks by raising:
- HTTPException with status 400 (content policy violation)
- HTTPException with status 400 (content policy violation standard, matches
Bedrock, OpenAI Moderations, Azure Text Moderation, and LakeraAI)
- HTTPException with status 403 (legacy accepted for backward compatibility
with existing deployments that relied on the old content-filter behaviour
before v1.86; new guardrails should raise 400)
- ModifyResponseException (passthrough mode violation)
"""
if isinstance(e, ModifyResponseException):
return True
if (
HTTPException is not None
and isinstance(e, HTTPException)
and e.status_code == 400
):
return True
if HTTPException is not None and isinstance(e, HTTPException):
# 400 is the canonical status for an intentional content-policy block.
# 403 is accepted for backward compatibility: the built-in
# litellm_content_filter previously raised 403, and operators may have
# custom guardrails or error-handlers that also use 403 for blocks.
if e.status_code in (400, 403):
return True
return False
def _process_error(

View file

@ -1190,3 +1190,77 @@ class TestCustomGuardrailSpendLogMatchRedaction:
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
assert slg["guardrail_response"]["filters"][0]["regex"] == "[REDACTED]"
assert raw["filters"][0]["regex"] == r"\d{3}-\d{2}-\d{4}"
class TestIsGuardrailIntervention:
"""Unit tests for CustomGuardrail._is_guardrail_intervention().
Ensures both the canonical (400) and legacy (403) status codes are
recognised as intentional content-policy blocks, while real server errors
and generic exceptions are not.
"""
def test_http_400_is_intervention(self):
"""HTTP 400 is the canonical status for a content-policy block."""
from fastapi import HTTPException
assert (
CustomGuardrail._is_guardrail_intervention(
HTTPException(status_code=400, detail="Content blocked")
)
is True
)
def test_http_403_is_intervention_backward_compat(self):
"""HTTP 403 must still be treated as an intentional block.
Before v1.86, litellm_content_filter raised HTTPException(403) for all
content blocks. Operators may have existing custom guardrails or error
handlers that also use 403. Recognising 403 here avoids changing the
``guardrail_status`` logged to Langfuse / DataDog on upgrade.
"""
from fastapi import HTTPException
assert (
CustomGuardrail._is_guardrail_intervention(
HTTPException(status_code=403, detail="Content blocked (legacy)")
)
is True
)
def test_http_500_is_not_intervention(self):
"""A 500 Internal Server Error is a failure, not an intentional block."""
from fastapi import HTTPException
assert (
CustomGuardrail._is_guardrail_intervention(
HTTPException(status_code=500, detail="Internal error")
)
is False
)
def test_http_404_is_not_intervention(self):
"""A 404 Not Found is not an intentional guardrail block."""
from fastapi import HTTPException
assert (
CustomGuardrail._is_guardrail_intervention(
HTTPException(status_code=404, detail="Not found")
)
is False
)
def test_generic_exception_is_not_intervention(self):
"""A plain ValueError is not a guardrail intervention."""
assert CustomGuardrail._is_guardrail_intervention(ValueError("oops")) is False
def test_modify_response_exception_is_intervention(self):
"""ModifyResponseException always signals an intentional block (passthrough mode)."""
from litellm.exceptions import ModifyResponseException
e = ModifyResponseException(
message="Content blocked",
model="gpt-4",
request_data={"messages": []},
)
assert CustomGuardrail._is_guardrail_intervention(e) is True