diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py index 41caf732db0..ba30447683b 100644 --- a/litellm/integrations/custom_logger.py +++ b/litellm/integrations/custom_logger.py @@ -927,6 +927,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: Final = 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 3018f0c4d24..74653b4ec94 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5074,29 +5074,17 @@ class StandardLoggingPayloadSetup: return start_time_float, end_time_float, completion_start_time_float @staticmethod - def append_system_prompt_messages(kwargs: dict | None = None, messages: Any | None = None): - """ - Append system prompt messages to the messages - """ - if kwargs is not None: - if kwargs.get("system") is not None and isinstance(kwargs.get("system"), str): - if messages is None: - return [{"role": "system", "content": kwargs.get("system")}] - elif isinstance(messages, list): - if len(messages) == 0: - return [{"role": "system", "content": kwargs.get("system")}] - # check for duplicates - if messages[0].get("role") == "system" and messages[0].get("content") == kwargs.get("system"): - return messages - messages = [{"role": "system", "content": kwargs.get("system")}] + messages - elif isinstance(messages, str): - messages = [ - {"role": "system", "content": kwargs.get("system")}, - {"role": "user", "content": messages}, - ] - return messages + def get_system_prompt_from_kwargs(kwargs: dict | None = None) -> str | list | dict | None: + if kwargs is None: + return None - return messages + for key in ("system_instructions", "instructions", "system"): + value = kwargs.get(key) + if value is None: + continue + if isinstance(value, (str, list, dict)): + return value + return None @staticmethod def merge_litellm_metadata(litellm_params: Mapping[str, object]) -> dict: @@ -6046,11 +6034,8 @@ def get_standard_logging_object_payload( model_id=_model_id, requester_ip_address=clean_metadata.get("requester_ip_address", None), user_agent=clean_metadata.get("user_agent", None), - messages=truncate_base64_in_messages( - StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=kwargs.get("messages") - ) - ), + messages=truncate_base64_in_messages(kwargs.get("messages")), + system_prompt=StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs=kwargs), response=final_response_obj, model_parameters=ModelParamHelper.get_standard_logging_model_parameters( kwargs.get("optional_params", None) or {} diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 9402d465712..97afe6b329b 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -173,6 +173,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: Final = standard_logging_object.get("response") if response is not None: if isinstance(response, dict) and "output" in response: diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 95429e899c9..7c7f871a0ab 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3254,6 +3254,7 @@ class StandardLoggingPayload(TypedDict): requester_ip_address: str | None user_agent: str | None messages: str | list | dict | None + system_prompt: str | list | dict | None response: str | list | dict | None error_str: str | None error_information: StandardLoggingPayloadErrorInformation | None 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 d8c45d832ce..3a741e762b0 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 @@ -59,6 +59,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!"}}] }, @@ -223,6 +224,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 0222e756ba1..f3e08d3730f 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2465,63 +2465,103 @@ def test_get_usage_as_dict(): assert result == {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0} -def test_append_system_prompt_messages(): - """ - Test append_system_prompt_messages prepends system message from kwargs to messages list. - """ +def test_get_system_prompt_from_kwargs(): from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup - # Test case 1: system in kwargs with existing messages - kwargs = {"system": "You are a helpful assistant"} - messages = [{"role": "user", "content": "Hello"}] - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=messages - ) - assert len(result) == 2 - assert result[0] == {"role": "system", "content": "You are a helpful assistant"} - assert result[1] == {"role": "user", "content": "Hello"} - - # Test case 2: system in kwargs with None messages - kwargs = {"system": "You are a helpful assistant"} - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=None - ) - assert len(result) == 1 - assert result[0] == {"role": "system", "content": "You are a helpful assistant"} - - # Test case 3: system in kwargs with empty messages list - kwargs = {"system": "You are a helpful assistant"} - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=[] - ) - assert len(result) == 1 - assert result[0] == {"role": "system", "content": "You are a helpful assistant"} - - # Test case 4: duplicate system message should not be added - kwargs = {"system": "You are a helpful assistant"} - messages = [ - {"role": "system", "content": "You are a helpful assistant"}, - {"role": "user", "content": "Hello"}, + system_blocks = [ + {"type": "text", "text": "SHAPE-SECRET", "cache_control": {"type": "ephemeral"}}, ] - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=messages + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( + kwargs={"system": system_blocks, "messages": [{"role": "user", "content": "hi"}]} ) - assert len(result) == 2 - assert result[0] == {"role": "system", "content": "You are a helpful assistant"} + assert result == system_blocks - # Test case 5: no system in kwargs returns messages unchanged - kwargs = {} - messages = [{"role": "user", "content": "Hello"}] - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=kwargs, messages=messages - ) - assert result == messages + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs={"system": "Be helpful"}) + assert result == "Be helpful" - # Test case 6: None kwargs returns messages unchanged - result = StandardLoggingPayloadSetup.append_system_prompt_messages( - kwargs=None, messages=messages + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs(kwargs={"instructions": "Follow policy"}) + assert result == "Follow policy" + + gemini_system = [{"role": "system", "content": "Be concise."}] + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( + kwargs={"system_instructions": gemini_system} ) - assert result == messages + assert result == gemini_system + + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( + kwargs={ + "system_instructions": "From Gemini", + "instructions": "From Responses", + "system": "From Anthropic", + } + ) + assert result == "From Gemini" + + result = StandardLoggingPayloadSetup.get_system_prompt_from_kwargs( + kwargs={"system_instructions": [], "instructions": "From Responses"} + ) + assert result == [] + + 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_keeps_system_prompt_separate_from_messages(logging_obj): + import datetime + + from litellm.litellm_core_utils.litellm_logging import get_standard_logging_object_payload + + user_messages = [{"role": "user", "content": "hello"}] + mock_response = { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [{"type": "text", "text": "hi"}], + "model": "claude-sonnet-4-5", + "usage": {"input_tokens": 5, "output_tokens": 2}, + } + now = datetime.datetime.now() + + system_blocks = [ + {"type": "text", "text": "SHAPE-SECRET", "cache_control": {"type": "ephemeral"}}, + ] + list_kwargs = { + "model": "anthropic/claude-sonnet-4-5", + "system": system_blocks, + "messages": user_messages, + "litellm_params": {}, + } + list_payload = get_standard_logging_object_payload( + kwargs=list_kwargs, + init_response_obj=mock_response, + start_time=now, + end_time=now, + logging_obj=logging_obj, + status="success", + ) + + assert list_payload is not None + assert list_payload["system_prompt"] == system_blocks + assert list_payload["messages"] == user_messages + + string_kwargs = { + "model": "anthropic/claude-sonnet-4-5", + "system": "Be helpful", + "messages": user_messages, + "litellm_params": {}, + } + string_payload = get_standard_logging_object_payload( + kwargs=string_kwargs, + init_response_obj=mock_response, + start_time=now, + end_time=now, + logging_obj=logging_obj, + status="success", + ) + + assert string_payload is not None + assert string_payload["system_prompt"] == "Be helpful" + assert string_payload["messages"] == user_messages @pytest.mark.asyncio 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 3be0bae4120..c5599e4dd45 100644 --- a/tests/test_litellm/litellm_core_utils/test_redact_messages.py +++ b/tests/test_litellm/litellm_core_utils/test_redact_messages.py @@ -15,6 +15,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, ) @@ -182,6 +183,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"}, @@ -208,6 +212,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} @@ -858,3 +863,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