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>
This commit is contained in:
devin-ai-integration[bot] 2026-08-05 16:08:24 +00:00 committed by GitHub
parent 24dbd2b2db
commit fb34efc487
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 0 deletions

View file

@ -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

View file

@ -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