mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge pull request #14707 from ARajan1084/bedrock-guardrail-silent-failure-correction
fix: Bedrock guardrail silent failure correction
This commit is contained in:
commit
9a6b1651d2
3 changed files with 33 additions and 109 deletions
|
|
@ -384,9 +384,6 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
)
|
||||
#########################################################
|
||||
if response.status_code == 200:
|
||||
# check if the response contains an error
|
||||
if self._check_bedrock_response_for_exception(response=response):
|
||||
raise self._get_http_exception_for_failed_guardrail(response)
|
||||
# check if the response was flagged
|
||||
_json_response = response.json()
|
||||
redacted_response = _redact_pii_matches(_json_response)
|
||||
|
|
@ -452,19 +449,6 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
return "success"
|
||||
return "failure"
|
||||
|
||||
def _get_http_exception_for_failed_guardrail(
|
||||
self, response: httpx.Response
|
||||
) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Guardrail application failed.",
|
||||
"bedrock_guardrail_response": json.loads(
|
||||
response.content.decode("utf-8")
|
||||
).get("Output", {}),
|
||||
},
|
||||
)
|
||||
|
||||
def _get_http_exception_for_blocked_guardrail(
|
||||
self, response: BedrockGuardrailResponse
|
||||
) -> HTTPException:
|
||||
|
|
|
|||
|
|
@ -1049,76 +1049,3 @@ async def test_bedrock_guardrail_parameter_takes_precedence_over_env(monkeypatch
|
|||
), f"Expected parameter endpoint to take precedence. Got: {prepped_request.url}"
|
||||
|
||||
print(f"Parameter precedence test passed. URL: {prepped_request.url}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_guardrail_200_with_exception_in_output_raises_and_logs_failure():
|
||||
"""
|
||||
When Bedrock returns HTTP 200 but the body contains Output.__type with 'Exception',
|
||||
the guardrail should:
|
||||
- raise an HTTPException(400) with the Output payload in detail
|
||||
- log the request trace with guardrail_status='failure'
|
||||
"""
|
||||
guardrail = BedrockGuardrail(
|
||||
guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT"
|
||||
)
|
||||
|
||||
# Mock a Bedrock "success" HTTP status but an Exception embedded in the body
|
||||
payload = {
|
||||
"Output": {
|
||||
"__type": "com.amazonaws#InternalServerException",
|
||||
"message": "Something went wrong upstream",
|
||||
},
|
||||
"action": "NONE",
|
||||
}
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.content = json.dumps(payload).encode("utf-8")
|
||||
mock_resp.text = json.dumps(payload)
|
||||
mock_resp.json.return_value = payload
|
||||
|
||||
# Minimal request data
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}
|
||||
|
||||
# Mock creds and request prep
|
||||
mock_credentials = MagicMock()
|
||||
mock_credentials.access_key = "ak"
|
||||
mock_credentials.secret_key = "sk"
|
||||
mock_credentials.token = None
|
||||
|
||||
with patch.object(
|
||||
guardrail.async_handler, "post", new_callable=AsyncMock
|
||||
) as mock_post, patch.object(
|
||||
guardrail, "_load_credentials", return_value=(mock_credentials, "us-east-1")
|
||||
), patch.object(
|
||||
guardrail,
|
||||
"_prepare_request",
|
||||
return_value=MagicMock(url="http://example", headers={}, body=b""),
|
||||
), patch.object(
|
||||
guardrail, "add_standard_logging_guardrail_information_to_request_data"
|
||||
) as mock_add_trace:
|
||||
mock_post.return_value = mock_resp
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await guardrail.make_bedrock_api_request(
|
||||
source="INPUT",
|
||||
messages=request_data["messages"],
|
||||
request_data=request_data,
|
||||
)
|
||||
|
||||
# 1) Raised HTTPException with 400 status
|
||||
err = excinfo.value
|
||||
assert err.status_code == 400
|
||||
assert err.detail["error"] == "Guardrail application failed."
|
||||
|
||||
# 2) Detail includes the Output object from the Bedrock body
|
||||
assert err.detail["bedrock_guardrail_response"] == payload["Output"]
|
||||
|
||||
# 3) Trace logging received a 'failure' status
|
||||
assert mock_add_trace.called
|
||||
_, kwargs = mock_add_trace.call_args
|
||||
assert kwargs["guardrail_status"] == "failure"
|
||||
# And the JSON passed to tracing is the same response we received
|
||||
assert kwargs["guardrail_json_response"] == payload
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState } from "react";
|
||||
import { Tooltip } from "antd";
|
||||
import PresidioDetectedEntities from "./PresidioDetectedEntities";
|
||||
import BedrockGuardrailDetails, {
|
||||
BedrockGuardrailResponse,
|
||||
} from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails"
|
||||
} from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails";
|
||||
|
||||
interface RecognitionMetadata {
|
||||
recognizer_name: string;
|
||||
|
|
@ -44,9 +45,13 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
// Default to presidio for backwards compatibility
|
||||
const guardrailProvider = data.guardrail_provider ?? "presidio";
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
const isSuccess =
|
||||
typeof data.guardrail_status === "string" &&
|
||||
data.guardrail_status.toLowerCase() === "success";
|
||||
|
||||
const tooltipTitle = isSuccess ? null : "Guardrail failed to run.";
|
||||
|
||||
// Calculate total masked entities
|
||||
const totalMaskedEntities = data.masked_entity_count ?
|
||||
|
|
@ -73,13 +78,18 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium">Guardrail Information</h3>
|
||||
<span className={`ml-3 px-2 py-1 rounded-md text-xs font-medium inline-block ${
|
||||
data.guardrail_status === "success"
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{data.guardrail_status}
|
||||
</span>
|
||||
|
||||
{/* Header status chip with tooltip */}
|
||||
<Tooltip title={tooltipTitle} placement="top" arrow destroyTooltipOnHide>
|
||||
<span
|
||||
className={`ml-3 px-2 py-1 rounded-md text-xs font-medium inline-block ${
|
||||
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800 cursor-help"
|
||||
}`}
|
||||
>
|
||||
{data.guardrail_status}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{totalMaskedEntities > 0 && (
|
||||
<span className="ml-3 px-2 py-1 bg-blue-50 text-blue-700 rounded-md text-xs font-medium">
|
||||
{totalMaskedEntities} masked {totalMaskedEntities === 1 ? 'entity' : 'entities'}
|
||||
|
|
@ -104,15 +114,18 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Status:</span>
|
||||
<span className={`px-2 py-1 rounded-md text-xs font-medium inline-block ${
|
||||
data.guardrail_status === "success"
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{data.guardrail_status}
|
||||
</span>
|
||||
<Tooltip title={tooltipTitle} placement="top" arrow destroyTooltipOnHide>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-md text-xs font-medium inline-block ${
|
||||
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800 cursor-help"
|
||||
}`}
|
||||
>
|
||||
{data.guardrail_status}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Start Time:</span>
|
||||
|
|
@ -158,6 +171,6 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default GuardrailViewer;
|
||||
export default GuardrailViewer;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue