fix(guardrails): log mixed-stream passthrough and cover flush path

Add explicit warnings when Presidio apply_to_output skips masking for mixed/unknown stream event shapes, and add regression coverage for the mixed stream flush path to ensure chunk order is preserved and warnings are emitted.

Made-with: Cursor
This commit is contained in:
Sameer Kankute 2026-04-30 17:08:12 +05:30
parent 5622dd299d
commit b4f4a931f7
No known key found for this signature in database
2 changed files with 65 additions and 0 deletions

View file

@ -1174,12 +1174,23 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail):
else:
if all_chunks:
# Flush buffered chunks and switch to transparent passthrough for this stream shape.
# NOTE: these buffered chunks are emitted unmasked because this
# stream mixed chunk types and cannot be safely reconstructed.
verbose_proxy_logger.warning(
"Presidio apply_to_output: mixed stream detected (ModelResponseStream + unknown event). "
"Flushing %d buffered chunks without PII masking and switching to transparent passthrough.",
len(all_chunks),
)
for buffered_chunk in all_chunks:
yield buffered_chunk
all_chunks = []
passthrough_due_to_unknown_stream_shape = True
yield chunk
if passthrough_due_to_unknown_stream_shape:
verbose_proxy_logger.warning(
"Presidio apply_to_output: streaming response contained unknown event objects "
"(e.g. /v1/responses events). Output PII masking was skipped for this response."
)
return
if not all_chunks:
verbose_proxy_logger.warning(

View file

@ -2162,6 +2162,60 @@ async def test_apply_to_output_streaming_unknown_events_passthrough():
]
@pytest.mark.asyncio
async def test_apply_to_output_streaming_mixed_chunks_flushes_and_warns():
"""
Regression test for mixed stream shape:
a buffered ModelResponseStream chunk followed by unknown responses-style
events should be preserved, and masking skip should be visible via warnings.
"""
guardrail = _OPTIONAL_PresidioPIIMasking(
mock_testing=True,
apply_to_output=True,
)
class FakeResponsesEvent:
def __init__(self, event_type: str):
self.type = event_type
model_chunk = ModelResponseStream(
id="chatcmpl-mixed-1",
choices=[],
created=1,
model="gpt-4",
object="chat.completion.chunk",
system_fingerprint=None,
)
response_completed = FakeResponsesEvent("response.completed")
async def mock_stream():
yield model_chunk
yield response_completed
mock_user_api_key = UserAPIKeyAuth(api_key="test-key")
received = []
with patch(
"litellm.proxy.guardrails.guardrail_hooks.presidio.verbose_proxy_logger"
) as mock_logger:
async for chunk in guardrail.async_post_call_streaming_iterator_hook(
user_api_key_dict=mock_user_api_key,
response=mock_stream(),
request_data={},
):
received.append(chunk)
# Preserve original ordering across mixed stream types.
assert received == [model_chunk, response_completed]
# Two warnings are expected:
# 1) mixed stream detected + unmasked flush
# 2) passthrough mode skipped output masking
assert mock_logger.warning.call_count == 2
warning_messages = [call.args[0] for call in mock_logger.warning.call_args_list]
assert any("mixed stream detected" in msg for msg in warning_messages)
assert any("unknown event objects" in msg for msg in warning_messages)
# ---------------------------------------------------------------------------
# Fix 4: apply_guardrail unmask path for input_type="response"
# ---------------------------------------------------------------------------