mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(guardrails): deliver an unscannable Anthropic stream as an error frame
This commit is contained in:
parent
3e99d5ff73
commit
f9e4f9f8a6
2 changed files with 22 additions and 17 deletions
|
|
@ -27,7 +27,7 @@ import litellm
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.caching import DualCache
|
||||
from litellm.constants import BEDROCK_APPLY_GUARDRAIL_CHUNK_BUDGET_CHARS
|
||||
from litellm.exceptions import GuardrailRaisedException, ModifyResponseException
|
||||
from litellm.exceptions import ModifyResponseException
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.litellm_core_utils.core_helpers import redact_nested_match_and_regex_keys
|
||||
from litellm.llms.base_llm.guardrail_translation.utils import (
|
||||
|
|
@ -2679,12 +2679,14 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
async for chunk in mock_response:
|
||||
yield chunk
|
||||
elif raw_sse:
|
||||
# Forwarding an unscannable stream would silently disable the guardrail, so fail closed
|
||||
# the way tool_permission does
|
||||
raise GuardrailRaisedException(
|
||||
guardrail_name=self.guardrail_name,
|
||||
message="Streamed response could not be assembled for scanning, blocking it",
|
||||
)
|
||||
# Forwarding an unscannable stream would silently disable the guardrail, so fail closed.
|
||||
# A raise cannot reach the client once a keepalive ping has flushed the headers, so the
|
||||
# refusal travels as a frame, matching how a block is delivered above
|
||||
for error_frame in anthropic_sse_error_frames(
|
||||
f"{self.guardrail_name}: streamed response could not be assembled for scanning, blocking it"
|
||||
):
|
||||
yield error_frame
|
||||
return
|
||||
else:
|
||||
for chunk in all_chunks:
|
||||
yield chunk
|
||||
|
|
|
|||
|
|
@ -2956,23 +2956,27 @@ async def test_streaming_hook_block_stream_keeps_upstream_identity():
|
|||
@pytest.mark.asyncio
|
||||
async def test_streaming_hook_fails_closed_when_raw_sse_cannot_be_assembled():
|
||||
"""An unscannable stream must not be delivered: forwarding it silently disables the guardrail."""
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
|
||||
guardrail = _sse_guardrail()
|
||||
|
||||
async def _unparseable_stream():
|
||||
yield b'data: {"type":"content_block_delta"}\n\n'
|
||||
|
||||
with patch.object(guardrail, "make_bedrock_api_request", new_callable=AsyncMock) as mock_api:
|
||||
with pytest.raises(GuardrailRaisedException):
|
||||
async for _ in guardrail.async_post_call_streaming_iterator_hook(
|
||||
delivered = [
|
||||
chunk
|
||||
async for chunk in guardrail.async_post_call_streaming_iterator_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
response=_unparseable_stream(),
|
||||
request_data={"model": "claude-sonnet-4-5"},
|
||||
):
|
||||
pass
|
||||
)
|
||||
]
|
||||
|
||||
mock_api.assert_not_called()
|
||||
body = b"".join(delivered)
|
||||
# a raise cannot reach the client once a keepalive ping has flushed the headers
|
||||
assert b"event: error" in body
|
||||
assert b"could not be assembled" in body
|
||||
assert b"content_block_delta" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -2981,8 +2985,6 @@ async def test_streaming_hook_fails_closed_when_assembler_raises_api_error():
|
|||
|
||||
That exception message is the exact 500 this fix exists to remove.
|
||||
"""
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
|
||||
guardrail = _sse_guardrail()
|
||||
|
||||
with patch(
|
||||
|
|
@ -2996,8 +2998,9 @@ async def test_streaming_hook_fails_closed_when_assembler_raises_api_error():
|
|||
model="",
|
||||
),
|
||||
):
|
||||
with pytest.raises(GuardrailRaisedException):
|
||||
await _drain_streaming_hook(guardrail)
|
||||
delivered = await _drain_streaming_hook(guardrail)
|
||||
|
||||
assert b"event: error" in b"".join(delivered)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue