From 5b080e20c4cb69bb5a6e2c971e44b6d605fa0313 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 18:57:15 -0700 Subject: [PATCH 1/5] removes HTTP exception for faulty guardrail --- .../guardrail_hooks/bedrock_guardrails.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index be394b5ad98..462582b1f35 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -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: From 5eff9f6999fd85285f4bbc1916e85ecd43fcbefc Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 19:11:55 -0700 Subject: [PATCH 2/5] added tooltip for guardrail failure state --- .../GuardrailViewer/GuardrailViewer.tsx | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx index 96d1da91627..ebacda497c8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -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) => {

Guardrail Information

- - {data.guardrail_status} - + + {/* Header status chip with tooltip */} + + + {data.guardrail_status} + + + {totalMaskedEntities > 0 && ( {totalMaskedEntities} masked {totalMaskedEntities === 1 ? 'entity' : 'entities'} @@ -104,15 +114,18 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
Status: - - {data.guardrail_status} - + + + {data.guardrail_status} + +
+
Start Time: @@ -158,6 +171,6 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => { )}
); -} +}; -export default GuardrailViewer; \ No newline at end of file +export default GuardrailViewer; From ad663e5240c68d23c1ed3554274e97c5f47e54bd Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 19:37:15 -0700 Subject: [PATCH 3/5] removed unnecessary test --- .../test_bedrock_guardrails.py | 75 +------------------ 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py index d5e624df52c..ffe375e141c 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py @@ -1048,77 +1048,4 @@ async def test_bedrock_guardrail_parameter_takes_precedence_over_env(monkeypatch prepped_request.url == expected_url ), 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 + print(f"Parameter precedence test passed. URL: {prepped_request.url}") \ No newline at end of file From ed4bd504ea1904dcae1d9b7e442a5e2acc395a9c Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 19:45:13 -0700 Subject: [PATCH 4/5] Revert "removed unnecessary test" This reverts commit ad663e5240c68d23c1ed3554274e97c5f47e54bd. --- .../test_bedrock_guardrails.py | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py index ffe375e141c..d5e624df52c 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py @@ -1048,4 +1048,77 @@ async def test_bedrock_guardrail_parameter_takes_precedence_over_env(monkeypatch prepped_request.url == expected_url ), f"Expected parameter endpoint to take precedence. Got: {prepped_request.url}" - print(f"Parameter precedence test passed. URL: {prepped_request.url}") \ No newline at end of file + 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 From 14b3cc2f95434bac7b47d8dab5ba4facd1694fd8 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 18 Sep 2025 19:48:16 -0700 Subject: [PATCH 5/5] Update test_bedrock_guardrails.py --- .../test_bedrock_guardrails.py | 73 ------------------- 1 file changed, 73 deletions(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py index d5e624df52c..612d78fa6f0 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py @@ -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