From b99c44af93c8ebd0c7f3d7029a6eccc7f3ea9383 Mon Sep 17 00:00:00 2001 From: Nicholas Couture Date: Fri, 24 Apr 2026 15:40:47 +1000 Subject: [PATCH] fix: extract human-readable message from dict detail in SSE error frame --- .../guardrail_hooks/bedrock_guardrails.py | 8 +++++- .../test_bedrock_guardrails.py | 28 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index 5c9baf21fff..4d0ecd1bb3d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -1332,9 +1332,15 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): # An async generator cannot propagate an exception as an HTTP error response # after the 200 header has been written. Convert hard-block exceptions to # in-band SSE error frames so the client receives a structured error. + # _get_http_exception_for_blocked_guardrail raises with a dict detail; + # extract the human-readable "error" key + if isinstance(e.detail, dict): + message = e.detail.get("error", str(e.detail)) + else: + message = str(e.detail) error_data = { "error": { - "message": str(e.detail), + "message": message, "code": e.status_code, "type": "guardrail_violation", } 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 5f391767608..0fb6628346b 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 @@ -2218,7 +2218,16 @@ async def test_streaming_hook_converts_http_exception_to_sse_error_frame_paralle for c in chunks: yield c - hard_block = HTTPException(status_code=400, detail="Violated guardrail policy") + # Use the production dict-detail shape from _get_http_exception_for_blocked_guardrail + hard_block = HTTPException( + status_code=400, + detail={ + "error": "Violated guardrail policy", + "bedrock_guardrail_response": "", + "guardrailIdentifier": "test-id", + "guardrailVersion": "DRAFT", + }, + ) with patch.object( guardrail, "make_bedrock_api_request", AsyncMock(side_effect=hard_block) @@ -2240,7 +2249,8 @@ async def test_streaming_hook_converts_http_exception_to_sse_error_frame_paralle payload = _json.loads(error_frame[len("data: "):].rstrip()) assert payload["error"]["code"] == 400 assert payload["error"]["type"] == "guardrail_violation" - assert "Violated guardrail policy" in payload["error"]["message"] + # Must be a clean string, not a Python dict repr + assert payload["error"]["message"] == "Violated guardrail policy" @pytest.mark.asyncio @@ -2271,7 +2281,16 @@ async def test_streaming_hook_converts_http_exception_to_sse_error_frame_output_ for c in chunks: yield c - hard_block = HTTPException(status_code=400, detail="Violated guardrail policy") + # Use the production dict-detail shape from _get_http_exception_for_blocked_guardrail + hard_block = HTTPException( + status_code=400, + detail={ + "error": "Violated guardrail policy", + "bedrock_guardrail_response": "", + "guardrailIdentifier": "test-id", + "guardrailVersion": "DRAFT", + }, + ) with patch.object( guardrail, "make_bedrock_api_request", AsyncMock(side_effect=hard_block) @@ -2292,4 +2311,5 @@ async def test_streaming_hook_converts_http_exception_to_sse_error_frame_output_ payload = _json.loads(error_frame[len("data: "):].rstrip()) assert payload["error"]["code"] == 400 assert payload["error"]["type"] == "guardrail_violation" - assert "Violated guardrail policy" in payload["error"]["message"] + # Must be a clean string, not a Python dict repr + assert payload["error"]["message"] == "Violated guardrail policy"