diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 7423e55b62..298935459b 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -138,6 +138,22 @@ def add_missing_spend_metadata_to_litellm_metadata( return litellm_metadata +def get_metadata_variable_name_from_kwargs( + kwargs: dict, +) -> str: + """ + Helper to return what the "metadata" field should be called in the request data + + - New endpoints return `litellm_metadata` + - Old endpoints return `metadata` + + Context: + - LiteLLM used `metadata` as an internal field for storing metadata + - OpenAI then started using this field for their metadata + - LiteLLM is now moving to using `litellm_metadata` for our metadata + """ + return "litellm_metadata" if "litellm_metadata" in kwargs else "metadata" + def get_litellm_metadata_from_kwargs(kwargs: dict): """ Helper to get litellm metadata from all litellm request kwargs diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 5ac38949e2..4237f00546 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -14,6 +14,9 @@ import litellm from litellm.integrations.custom_logger import CustomLogger from litellm.secret_managers.main import str_to_bool from litellm.types.utils import StandardCallbackDynamicParams +from litellm.litellm_core_utils.core_helpers import ( + get_metadata_variable_name_from_kwargs, +) import asyncio if TYPE_CHECKING: @@ -107,11 +110,13 @@ def should_redact_message_logging(model_call_details: dict) -> bool: """ Determine if message logging should be redacted. """ - _request_headers = ( - model_call_details.get("litellm_params", {}).get("metadata", {}) or {} - ) - - request_headers = _request_headers.get("headers", {}) + litellm_params = model_call_details.get("litellm_params", {}) + + metadata_field = get_metadata_variable_name_from_kwargs(litellm_params) + metadata = litellm_params.get(metadata_field, {}) + + # Get headers from the metadata + request_headers = metadata.get("headers", {}) if isinstance(metadata, dict) else {} possible_request_headers = [ "litellm-enable-message-redaction", # old header. maintain backwards compatibility diff --git a/litellm/router.py b/litellm/router.py index 5b6fc56f29..d772bcdbe4 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -54,7 +54,10 @@ from litellm.caching.caching import ( from litellm.constants import DEFAULT_MAX_LRU_CACHE_SIZE from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.asyncify import run_async_function -from litellm.litellm_core_utils.core_helpers import _get_parent_otel_span_from_kwargs +from litellm.litellm_core_utils.core_helpers import ( + _get_parent_otel_span_from_kwargs, + get_metadata_variable_name_from_kwargs, +) from litellm.litellm_core_utils.coroutine_checker import coroutine_checker from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.litellm_core_utils.dd_tracing import tracer @@ -4756,7 +4759,7 @@ class Router: - OpenAI then started using this field for their metadata - LiteLLM is now moving to using `litellm_metadata` for our metadata """ - return "litellm_metadata" if "litellm_metadata" in kwargs else "metadata" + return get_metadata_variable_name_from_kwargs(kwargs) def log_retry(self, kwargs: dict, e: Exception) -> dict: """ diff --git a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py index 227b57220e..6b13c877ce 100644 --- a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py +++ b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py @@ -279,3 +279,89 @@ async def test_redaction_with_streaming_response(): "logged standard logging payload for streaming with coroutine handling", json.dumps(standard_logging_payload, indent=2), ) + + +@pytest.mark.asyncio +async def test_disable_redaction_header_responses_api(): + """ + Test that LiteLLM-Disable-Message-Redaction header works for Responses API. + + This test verifies the fix for the issue where the header wasn't respected + because Responses API uses 'litellm_metadata' instead of 'metadata'. + """ + litellm.turn_off_message_logging = True + test_custom_logger = TestCustomLogger() + litellm.callbacks = [test_custom_logger] + + # Mock a ResponsesAPIResponse-style response + mock_response = { + "output": [{"text": "This is a test response"}], + "model": "gpt-3.5-turbo", + "usage": {"input_tokens": 5, "output_tokens": 5, "total_tokens": 10} + } + + # Pass the header via litellm_metadata (as the proxy does for Responses API) + response = await litellm.aresponses( + model="gpt-3.5-turbo", + input="hi", + mock_response=mock_response, + litellm_metadata={ + "headers": { + "litellm-disable-message-redaction": "true" + } + } + ) + + await asyncio.sleep(1) + standard_logging_payload = test_custom_logger.logged_standard_logging_payload + assert standard_logging_payload is not None + + # Verify that messages are NOT redacted because the header was set + print( + "logged standard logging payload for ResponsesAPI with disable header", + json.dumps(standard_logging_payload, indent=2, default=str), + ) + + # The content should NOT be redacted + assert standard_logging_payload["response"] != {"text": "redacted-by-litellm"} + assert standard_logging_payload["messages"][0]["content"] == "hi" + + +@pytest.mark.asyncio +async def test_redaction_with_metadata_completion_api(): + """ + Test redaction behavior with metadata field for Completion API. + + This test verifies that get_metadata_variable_name_from_kwargs properly + selects the appropriate metadata field for header detection. + """ + litellm.turn_off_message_logging = True + test_custom_logger = TestCustomLogger() + litellm.callbacks = [test_custom_logger] + + # When metadata is passed, the system uses get_metadata_variable_name_from_kwargs + # to determine which field to check + response = await litellm.acompletion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "hi"}], + mock_response="hello", + metadata={ + "headers": { + "litellm-disable-message-redaction": "true" + } + } + ) + + await asyncio.sleep(1) + standard_logging_payload = test_custom_logger.logged_standard_logging_payload + assert standard_logging_payload is not None + + print( + "logged standard logging payload for Completion API with metadata", + json.dumps(standard_logging_payload, indent=2), + ) + + # Verify the helper function works correctly - with get_metadata_variable_name_from_kwargs, + # the system checks the appropriate field for headers + assert standard_logging_payload["response"] == {"text": "redacted-by-litellm"} + assert standard_logging_payload["messages"][0]["content"] == "redacted-by-litellm"