diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 4491982d4c9..c9bcb9ec729 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -8,6 +8,7 @@ # Thank you users! We ❤️ you! - Krrish & Ishaan import asyncio +import contextlib import copy import inspect import itertools @@ -28,6 +29,7 @@ from litellm.litellm_core_utils.core_helpers import ( ) from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( initialize_standard_callback_dynamic_params, + iter_client_callback_metadata_dicts, ) from litellm.litellm_core_utils.served_output_texts import SERVED_OUTPUT_TEXTS_KEY from litellm.llms.base_llm.guardrail_translation.utils import stream_item_field, stream_item_items @@ -896,12 +898,12 @@ def should_redact_failed_request(request_data: Mapping[str, object]) -> bool: """ The redaction decision for proxy failure hooks, which get the request body but not its logging object: the same metadata, headers and dynamic params the logging object would read. When the - logging object would reject the request's callback params, the global setting decides. + logging object would reject another of the request's callback params, the request's own + ``turn_off_message_logging`` still counts, and when it would reject that too the global setting decides. """ body: Final = dict(request_data) - try: - dynamic_params: Final = initialize_standard_callback_dynamic_params(body) - except ValueError: + dynamic_params: Final = _request_callback_params(body) + if dynamic_params is None: return litellm.turn_off_message_logging is True return should_redact_failure_logging( { @@ -911,6 +913,27 @@ def should_redact_failed_request(request_data: Mapping[str, object]) -> bool: ) +def _request_callback_params(body: Mapping[str, object]) -> StandardCallbackDynamicParams | None: + """ + The callback params the logging object reads from ``body``. When it would reject one of them, only the + request's own ``turn_off_message_logging``, and ``None`` when it would reject that too. + """ + with contextlib.suppress(ValueError): + return initialize_standard_callback_dynamic_params(dict(body)) + sources: Final[tuple[Mapping[str, object], ...]] = ( + body, + *(metadata for _, metadata in iter_client_callback_metadata_dicts(body)), + ) + redaction_choice: Final = next( + (source["turn_off_message_logging"] for source in sources if "turn_off_message_logging" in source), None + ) + with contextlib.suppress(ValueError): + return initialize_standard_callback_dynamic_params( + {} if redaction_choice is None else {"turn_off_message_logging": redaction_choice} + ) + return None + + def scrubbed_error_information_for_request( error_information: StandardLoggingPayloadErrorInformation, request_data: Mapping[str, object] ) -> StandardLoggingPayloadErrorInformation: diff --git a/tests/unit/litellm_core_utils/test_redact_messages.py b/tests/unit/litellm_core_utils/test_redact_messages.py index 71a76e5eaf2..b0852389186 100644 --- a/tests/unit/litellm_core_utils/test_redact_messages.py +++ b/tests/unit/litellm_core_utils/test_redact_messages.py @@ -2357,6 +2357,22 @@ def test_should_redact_failed_request_falls_back_to_the_global_setting_for_an_un assert should_redact_failed_request(request_data) is global_redaction +@pytest.mark.parametrize( + "request_data", + [ + {"turn_off_message_logging": True, "langfuse_host": "os.environ/LANGFUSE_HOST"}, + {"metadata": {"turn_off_message_logging": True, "langfuse_host": "os.environ/LANGFUSE_HOST"}}, + ], + ids=["body_flag", "metadata_flag"], +) +def test_should_redact_failed_request_keeps_the_request_opt_in_past_a_rejected_callback_param( + monkeypatch: pytest.MonkeyPatch, request_data: dict +) -> None: + monkeypatch.setattr(litellm, "turn_off_message_logging", False) + + assert should_redact_failed_request(request_data) is True + + def test_should_redact_failed_request_reads_the_request_setting_past_non_mapping_headers( monkeypatch: pytest.MonkeyPatch, ) -> None: