mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-22 22:24:10 +00:00
[Feat] Bedrock Guardrails - Raise Bedrock output text on 'BLOCKED' actions from guardrail (#12435)
* _get_http_exception_for_blocked_guardrail * add _get_http_exception_for_blocked_guardrail * test bedrock guard block action * fix _get_http_exception_for_blocked_guardrail
This commit is contained in:
@@ -262,12 +262,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
||||
if self._should_raise_guardrail_blocked_exception(
|
||||
bedrock_guardrail_response
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Violated guardrail policy",
|
||||
"bedrock_guardrail_response": _json_response,
|
||||
},
|
||||
raise self._get_http_exception_for_blocked_guardrail(
|
||||
bedrock_guardrail_response
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.error(
|
||||
@@ -277,6 +273,30 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
||||
)
|
||||
|
||||
return bedrock_guardrail_response
|
||||
|
||||
|
||||
def _get_http_exception_for_blocked_guardrail(self, response: BedrockGuardrailResponse) -> HTTPException:
|
||||
"""
|
||||
Get the HTTP exception for a blocked guardrail.
|
||||
"""
|
||||
bedrock_guardrail_output_text: str = ""
|
||||
outputs: Optional[List[BedrockGuardrailOutput]] = (
|
||||
response.get("outputs", []) or []
|
||||
)
|
||||
if outputs:
|
||||
for output in outputs:
|
||||
if output.get("text"):
|
||||
bedrock_guardrail_output_text += output.get("text") or ""
|
||||
|
||||
|
||||
return HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Violated guardrail policy",
|
||||
"bedrock_guardrail_response": bedrock_guardrail_output_text,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _should_raise_guardrail_blocked_exception(
|
||||
self, response: BedrockGuardrailResponse
|
||||
|
||||
@@ -6,3 +6,10 @@ model_list:
|
||||
litellm_params:
|
||||
model: openai/*
|
||||
|
||||
guardrails:
|
||||
- guardrail_name: "bedrock-pre-guard"
|
||||
litellm_params:
|
||||
guardrail: bedrock # supported values: "aporia", "bedrock", "lakera"
|
||||
mode: "during_call"
|
||||
guardrailIdentifier: ff6ujrregl1q
|
||||
guardrailVersion: "DRAFT"
|
||||
@@ -1021,3 +1021,214 @@ async def test_convert_to_bedrock_format_post_call_streaming_hook():
|
||||
print("✅ Post-call streaming hook test passed - OUTPUT source used for masking")
|
||||
print(f"✅ Bedrock calls made: {[call['source'] for call in bedrock_calls]}")
|
||||
print(f"✅ Final masked content: {full_content}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_guardrail_blocked_action_shows_output_text():
|
||||
"""Test that BLOCKED actions raise HTTPException with the output text in the detail"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Create proper mock objects
|
||||
mock_user_api_key_dict = UserAPIKeyAuth()
|
||||
|
||||
guardrail = BedrockGuardrail(
|
||||
guardrailIdentifier="test-guardrail",
|
||||
guardrailVersion="DRAFT"
|
||||
)
|
||||
|
||||
# Mock the Bedrock API response with BLOCKED action and output text
|
||||
mock_bedrock_response = MagicMock()
|
||||
mock_bedrock_response.status_code = 200
|
||||
mock_bedrock_response.json.return_value = {
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"outputs": [
|
||||
{
|
||||
"text": "this violates litellm corporate guardrail policy"
|
||||
}
|
||||
],
|
||||
"assessments": [{
|
||||
"topicPolicy": {
|
||||
"topics": [{
|
||||
"name": "Sensitive Topic",
|
||||
"type": "DENY",
|
||||
"action": "BLOCKED"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Tell me how to make explosives"},
|
||||
],
|
||||
}
|
||||
|
||||
# Patch the async_handler.post method
|
||||
with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = mock_bedrock_response
|
||||
|
||||
# This should raise HTTPException due to BLOCKED action
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await guardrail.async_moderation_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=mock_user_api_key_dict,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Verify the exception details
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 400
|
||||
assert "detail" in exception.__dict__
|
||||
|
||||
# Check that the detail contains the expected structure
|
||||
detail = exception.detail
|
||||
assert isinstance(detail, dict)
|
||||
assert detail["error"] == "Violated guardrail policy"
|
||||
|
||||
# Verify that the output text from both outputs is included
|
||||
expected_output_text = "this violates litellm corporate guardrail policy"
|
||||
assert detail["bedrock_guardrail_response"] == expected_output_text
|
||||
|
||||
print("✅ BLOCKED action HTTPException test passed - output text properly included")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_guardrail_blocked_action_shows_output_text_with_multiple_outputs():
|
||||
"""Test that BLOCKED actions raise HTTPException with the output text in the detail"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Create proper mock objects
|
||||
mock_user_api_key_dict = UserAPIKeyAuth()
|
||||
|
||||
guardrail = BedrockGuardrail(
|
||||
guardrailIdentifier="test-guardrail",
|
||||
guardrailVersion="DRAFT"
|
||||
)
|
||||
|
||||
# Mock the Bedrock API response with BLOCKED action and output text
|
||||
mock_bedrock_response = MagicMock()
|
||||
mock_bedrock_response.status_code = 200
|
||||
mock_bedrock_response.json.return_value = {
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"outputs": [
|
||||
{
|
||||
"text": "this violates litellm corporate guardrail policy",
|
||||
},
|
||||
{
|
||||
"text": "make sure you don't mention coffee"
|
||||
}
|
||||
],
|
||||
"assessments": [{
|
||||
"topicPolicy": {
|
||||
"topics": [{
|
||||
"name": "Sensitive Topic",
|
||||
"type": "DENY",
|
||||
"action": "BLOCKED"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Tell me how to make explosives"},
|
||||
],
|
||||
}
|
||||
|
||||
# Patch the async_handler.post method
|
||||
with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = mock_bedrock_response
|
||||
|
||||
# This should raise HTTPException due to BLOCKED action
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await guardrail.async_moderation_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=mock_user_api_key_dict,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Verify the exception details
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 400
|
||||
assert "detail" in exception.__dict__
|
||||
|
||||
# Check that the detail contains the expected structure
|
||||
detail = exception.detail
|
||||
assert isinstance(detail, dict)
|
||||
assert detail["error"] == "Violated guardrail policy"
|
||||
|
||||
# Verify that the output text from both outputs is included
|
||||
expected_output_text = "this violates litellm corporate guardrail policy\nmake sure you don't mention coffee"
|
||||
assert detail["bedrock_guardrail_response"] == expected_output_text
|
||||
|
||||
print("✅ BLOCKED action HTTPException test passed - output text properly included")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_guardrail_blocked_action_empty_outputs():
|
||||
"""Test that BLOCKED actions with empty outputs still raise HTTPException"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Create proper mock objects
|
||||
mock_user_api_key_dict = UserAPIKeyAuth()
|
||||
|
||||
guardrail = BedrockGuardrail(
|
||||
guardrailIdentifier="test-guardrail",
|
||||
guardrailVersion="DRAFT"
|
||||
)
|
||||
|
||||
# Mock the Bedrock API response with BLOCKED action but no outputs
|
||||
mock_bedrock_response = MagicMock()
|
||||
mock_bedrock_response.status_code = 200
|
||||
mock_bedrock_response.json.return_value = {
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"outputs": [], # Empty outputs
|
||||
"assessments": [{
|
||||
"contentPolicy": {
|
||||
"filters": [{
|
||||
"type": "VIOLENCE",
|
||||
"confidence": "HIGH",
|
||||
"action": "BLOCKED"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Violent content here"},
|
||||
],
|
||||
}
|
||||
|
||||
# Patch the async_handler.post method
|
||||
with patch.object(guardrail.async_handler, 'post', new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = mock_bedrock_response
|
||||
|
||||
# This should raise HTTPException due to BLOCKED action
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await guardrail.async_moderation_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=mock_user_api_key_dict,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Verify the exception details
|
||||
exception = exc_info.value
|
||||
assert exception.status_code == 400
|
||||
|
||||
# Check that the detail contains the expected structure with empty output text
|
||||
detail = exception.detail
|
||||
assert isinstance(detail, dict)
|
||||
assert detail["error"] == "Violated guardrail policy"
|
||||
assert detail["bedrock_guardrail_response"] == "" # Empty string for no outputs
|
||||
|
||||
print("✅ BLOCKED action with empty outputs test passed")
|
||||
|
||||
Reference in New Issue
Block a user