mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
c945e0e2c6
commit
6e820a9376
2 changed files with 86 additions and 7 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue