From fb34efc48780d72a384cf87845fa7639f3875b6e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:08:24 +0000 Subject: [PATCH] fix(presidio): propagate masked messages into standard_logging_object in logging_only mode async_logging_hook masked only kwargs[messages], but the standard_logging_object is built earlier in the pipeline and holds a separate copy that downstream logging callbacks read from, so logging_only mode still leaked unmasked PII to observability platforms. Mirror the masked messages into standard_logging_object[messages] too. Fixes #35951 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../guardrails/guardrail_hooks/presidio.py | 5 ++ .../guardrail_hooks/test_presidio.py | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 2a3b90a70df..ac54564e80d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -21,6 +21,7 @@ import aiohttp import litellm from litellm import get_secret from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.logging_utils import truncate_base64_in_messages from litellm.types.utils import GenericGuardrailAPIInputs if TYPE_CHECKING: @@ -829,6 +830,10 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): verbose_proxy_logger.debug("Presidio PII Masking: Redacted pii message: %s", messages) kwargs["messages"] = messages + standard_logging_object: Final = kwargs.get("standard_logging_object") + if isinstance(standard_logging_object, dict) and standard_logging_object.get("messages") is not None: + standard_logging_object["messages"] = truncate_base64_in_messages(messages) + return kwargs, result async def async_post_call_success_hook( # type: ignore 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 253d989f203..36b34dc79fe 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -584,6 +584,54 @@ async def test_logging_hook_multiple_content_items(presidio_guardrail): print("✓ Logging hook multiple content items test passed") +@pytest.mark.asyncio +async def test_logging_hook_propagates_masked_messages_into_standard_logging_object( + presidio_guardrail, +): + """ + The standard_logging_object is built before async_logging_hook runs and holds + a separate copy of the messages. Downstream logging callbacks (Langfuse, Datadog, + etc.) read from standard_logging_object["messages"], so the hook must propagate the + masked messages there too. Regression test for issue #35951. + """ + test_kwargs = { + "messages": [ + { + "role": "user", + "content": "My credit card is 4111-1111-1111-1111 and email test@example.com", + } + ], + "model": "gpt-4", + "standard_logging_object": { + "messages": [ + { + "role": "user", + "content": "My credit card is 4111-1111-1111-1111 and email test@example.com", + } + ], + }, + } + + async def mock_check_pii(text, output_parse_pii, presidio_config, request_data): + return text.replace("4111-1111-1111-1111", "[CREDIT_CARD]").replace( + "test@example.com", "[EMAIL]" + ) + + presidio_guardrail.check_pii = mock_check_pii + + result_kwargs, _ = await presidio_guardrail.async_logging_hook( + kwargs=test_kwargs, + result={"choices": [{"message": {"content": "Response"}}]}, + call_type="completion", + ) + + slo_content = result_kwargs["standard_logging_object"]["messages"][0]["content"] + assert "[CREDIT_CARD]" in slo_content + assert "[EMAIL]" in slo_content + assert "4111-1111-1111-1111" not in slo_content + assert "test@example.com" not in slo_content + + @pytest.mark.asyncio async def test_logging_only_does_not_mask_pre_call_request( mock_user_api_key, mock_cache