From bc81ccf9d9fd97e6c02a534cdecefe95d512aa1f Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:28:54 +0000 Subject: [PATCH] fix(logging): redact system_prompt when message logging is off Include system_prompt in _redact_standard_logging_object and CustomLogger.redact_standard_logging_payload_from_model_call_details so list-form system blocks do not bypass turn_off_message_logging. Also remove new docstring/comments flagged in PR review. --- litellm/integrations/custom_logger.py | 6 +++ litellm/litellm_core_utils/litellm_logging.py | 7 --- litellm/litellm_core_utils/redact_messages.py | 3 ++ ...tandard_logging_payload_excluded_fields.py | 2 + .../test_litellm_logging.py | 8 --- .../test_redact_messages.py | 49 +++++++++++++++++++ 6 files changed, 60 insertions(+), 15 deletions(-) diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py index 8b831b55da3..a270ec8dcca 100644 --- a/litellm/integrations/custom_logger.py +++ b/litellm/integrations/custom_logger.py @@ -885,6 +885,12 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac if "messages" not in (excluded_fields or []) and standard_logging_object_copy.get("messages") is not None: standard_logging_object_copy["messages"] = [Message(content=redacted_str).model_dump()] + if ( + "system_prompt" not in (excluded_fields or []) + and standard_logging_object_copy.get("system_prompt") is not None + ): + standard_logging_object_copy["system_prompt"] = redacted_str + if "response" not in (excluded_fields or []) and standard_logging_object_copy.get("response") is not None: response = standard_logging_object_copy["response"] # Check if this is a ResponsesAPIResponse (has "output" field) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 2ce41fc4937..1b3039318b4 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -4557,13 +4557,6 @@ class StandardLoggingPayloadSetup: @staticmethod def get_system_prompt_from_kwargs(kwargs: Optional[Dict] = None) -> Optional[Union[str, list, dict]]: - """ - Return the system prompt kwargs as sent by the client, without reshaping. - - Coalesces the kwarg names used across call paths (Vertex Gemini, Responses API, - Anthropic Messages). Uses `is not None` checks so falsy values like [] do not - fall through to a different kwarg. - """ if kwargs is None: return None diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 43181e7f5ff..e99912487cf 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -169,6 +169,9 @@ def _redact_standard_logging_object(model_call_details: dict): if standard_logging_object.get("messages") is not None: standard_logging_object["messages"] = [{"role": "user", "content": redacted_str}] + if standard_logging_object.get("system_prompt") is not None: + standard_logging_object["system_prompt"] = redacted_str + response = standard_logging_object.get("response") if response is not None: if isinstance(response, dict) and "output" in response: diff --git a/tests/logging_callback_tests/test_standard_logging_payload_excluded_fields.py b/tests/logging_callback_tests/test_standard_logging_payload_excluded_fields.py index 4088bdd2cf7..ffec83c20ef 100644 --- a/tests/logging_callback_tests/test_standard_logging_payload_excluded_fields.py +++ b/tests/logging_callback_tests/test_standard_logging_payload_excluded_fields.py @@ -62,6 +62,7 @@ def create_sample_standard_logging_payload() -> Dict: "requester_ip_address": None, "user_agent": None, "messages": [{"role": "user", "content": "Hello, this is sensitive data!"}], + "system_prompt": [{"type": "text", "text": "sensitive system prompt"}], "response": { "choices": [{"message": {"content": "This is a sensitive response!"}}] }, @@ -226,6 +227,7 @@ class TestStandardLoggingPayloadExcludedFields: assert ( result["standard_logging_object"]["messages"][0]["content"] == redacted_str ) + assert result["standard_logging_object"]["system_prompt"] == redacted_str assert ( result["standard_logging_object"]["response"]["choices"][0]["message"][ "content" diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index aaeb83f8c2e..e38131cd135 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2166,7 +2166,6 @@ def test_append_system_prompt_messages(): def test_get_system_prompt_from_kwargs(): from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup - # Anthropic Messages list-form system blocks system_blocks = [ {"type": "text", "text": "SHAPE-SECRET", "cache_control": {"type": "ephemeral"}}, ] @@ -2175,22 +2174,18 @@ def test_get_system_prompt_from_kwargs(): ) assert result == system_blocks - # String system kwarg result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs={"system": "Be helpful"}) assert result == "Be helpful" - # Responses API instructions result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs={"instructions": "Follow policy"}) assert result == "Follow policy" - # Vertex Gemini system_instructions gemini_system = [{"role": "system", "content": "Be concise."}] result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( kwargs={"system_instructions": gemini_system} ) assert result == gemini_system - # system_instructions wins over instructions and system result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( kwargs={ "system_instructions": "From Gemini", @@ -2200,19 +2195,16 @@ def test_get_system_prompt_from_kwargs(): ) assert result == "From Gemini" - # Empty list should not fall through to instructions result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( kwargs={"system_instructions": [], "instructions": "From Responses"} ) assert result == [] - # No system kwargs assert StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs={}) is None assert StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs=None) is None def test_get_standard_logging_object_payload_includes_system_prompt_for_list_system(logging_obj): - """List-form Anthropic system blocks must appear on the payload without mutating messages.""" import datetime from litellm.litellm_core_utils.litellm_logging import get_standard_logging_object_payload diff --git a/tests/test_litellm/litellm_core_utils/test_redact_messages.py b/tests/test_litellm/litellm_core_utils/test_redact_messages.py index e1ffabb3515..cb32abd2b6a 100644 --- a/tests/test_litellm/litellm_core_utils/test_redact_messages.py +++ b/tests/test_litellm/litellm_core_utils/test_redact_messages.py @@ -14,6 +14,7 @@ from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.redact_messages import ( _redact_responses_api_output, perform_redaction, + redact_message_input_output_from_logging, redact_streaming_responses_for_custom_logger, should_redact_message_logging, ) @@ -181,6 +182,9 @@ class TestPerformRedaction: "input": "sensitive input", "standard_logging_object": { "messages": [{"role": "user", "content": "sensitive input"}], + "system_prompt": [ + {"type": "text", "text": "SHAPE-SECRET", "cache_control": {"type": "ephemeral"}}, + ], "response": { "output": [ {"text": "top-level text"}, @@ -207,6 +211,7 @@ class TestPerformRedaction: ] assert details["prompt"] == "" assert details["input"] == "" + assert details["standard_logging_object"]["system_prompt"] == "redacted-by-litellm" logged_response = details["standard_logging_object"]["response"] assert logged_response["usage"] == {"total_tokens": 1} @@ -720,3 +725,47 @@ class TestRedactStreamingResponsesForCustomLogger: assert result_details is model_call_details assert response_obj.choices[0].message.content == "secret content" + + +class TestSystemPromptRedaction: + REDACTED = "redacted-by-litellm" + SYSTEM_PROMPT = [{"type": "text", "text": "SHAPE-SECRET"}] + + def _details_with_system_prompt(self, **extra): + details = { + "messages": [{"role": "user", "content": "hi"}], + "standard_logging_object": { + "messages": [{"role": "user", "content": "hi"}], + "system_prompt": self.SYSTEM_PROMPT, + "response": {"choices": [{"message": {"content": "secret"}}]}, + }, + "litellm_params": {"metadata": {}}, + } + details.update(extra) + return details + + def test_global_turn_off_message_logging_redacts_system_prompt(self): + litellm.turn_off_message_logging = True + details = self._details_with_system_prompt() + + redact_message_input_output_from_logging(details, result=None) + + assert details["standard_logging_object"]["system_prompt"] == self.REDACTED + + def test_request_level_dynamic_param_redacts_system_prompt(self): + details = self._details_with_system_prompt( + standard_callback_dynamic_params={"turn_off_message_logging": True} + ) + + redact_message_input_output_from_logging(details, result=None) + + assert details["standard_logging_object"]["system_prompt"] == self.REDACTED + + def test_per_callback_turn_off_message_logging_redacts_system_prompt(self): + details = self._details_with_system_prompt() + logger = CustomLogger(turn_off_message_logging=True) + + result = logger.redact_standard_logging_payload_from_model_call_details(details) + + assert result["standard_logging_object"]["system_prompt"] == self.REDACTED + assert details["standard_logging_object"]["system_prompt"] == self.SYSTEM_PROMPT