diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index a3138b417dc..ec139020704 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -501,7 +501,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): request_data = {} # Store pii_tokens in metadata to avoid leaking to LLM providers. # Providers like Anthropic reject unknown top-level fields. - if "metadata" not in request_data: + if not request_data.get("metadata"): request_data["metadata"] = {} if "pii_tokens" not in request_data["metadata"]: request_data["metadata"]["pii_tokens"] = {} diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py index 55137176a60..57f55fadb22 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -1742,3 +1742,47 @@ def test_event_hook_no_expansion_when_already_post_call(): ) # Should remain a string "post_call", not expanded to a list assert guardrail.event_hook == "post_call" + + +@pytest.mark.asyncio +async def test_metadata_none_does_not_crash(): + """ + Regression test: if metadata is explicitly None in request_data, + the guardrail must not crash with TypeError on the write or read path. + """ + guardrail = _OPTIONAL_PresidioPIIMasking( + mock_testing=True, + output_parse_pii=True, + ) + + token_key = "_abc123def456" + # metadata explicitly None — must not crash + request_data = { + "model": "gpt-3.5-turbo", + "metadata": None, + } + + response = ModelResponse( + choices=[ + Choices( + message=Message( + role="assistant", + content=f"Hello {token_key}, how can I help you?", + ), + index=0, + finish_reason="stop", + ) + ] + ) + + # Should not raise TypeError + await guardrail._process_response_for_pii( + response=response, + request_data=request_data, + mode="unmask", + ) + + # No pii_tokens to unmask, so content stays as-is + assert ( + response.choices[0].message.content == f"Hello {token_key}, how can I help you?" + )