fix: redaction headers ignored when sent via proxy (#20740)

* fix: redaction headers ignored when sent via proxy

When requests go through the proxy, `litellm_params["litellm_metadata"]`
is always set (even when `None`), so `get_metadata_variable_name_from_kwargs`
always returns "litellm_metadata". The redaction code then reads `None`
instead of the actual metadata dict that contains the headers.

Add a fallback to read from `metadata` when `litellm_metadata` is not a
dict, so `x-litellm-enable-message-redaction` and related headers work
correctly in the proxy flow.

Fixes #20739

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: normalize non-dict metadata after fallback in redact_messages

After falling back from litellm_metadata to metadata, ensure the value
is always a dict so .get("headers") never raises on None/non-dict inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tsachi Shushan 2026-02-10 06:05:53 +02:00 committed by Sameer Kankute
parent 8f242c42a1
commit f77eeba186
2 changed files with 152 additions and 2 deletions

View file

@ -140,9 +140,14 @@ def should_redact_message_logging(model_call_details: dict) -> bool:
metadata_field = get_metadata_variable_name_from_kwargs(litellm_params)
metadata = litellm_params.get(metadata_field, {})
if not isinstance(metadata, dict):
# Fall back: litellm_metadata was None, try metadata
metadata = litellm_params.get("metadata", {})
if not isinstance(metadata, dict):
metadata = {}
# Get headers from the metadata
request_headers = metadata.get("headers", {}) if isinstance(metadata, dict) else {}
request_headers = metadata.get("headers", {})
# Check for headers that explicitly control redaction
if request_headers and bool(

View file

@ -0,0 +1,145 @@
"""
Tests for litellm.litellm_core_utils.redact_messages.should_redact_message_logging
Covers the proxy flow where headers arrive in litellm_params["metadata"]["headers"]
but litellm_params["litellm_metadata"] is None.
"""
import pytest
import litellm
from litellm.litellm_core_utils.redact_messages import should_redact_message_logging
@pytest.fixture(autouse=True)
def _reset_global_redaction():
"""Ensure the global setting is off for every test."""
original = litellm.turn_off_message_logging
litellm.turn_off_message_logging = False
yield
litellm.turn_off_message_logging = original
def _make_model_call_details(
metadata_headers=None,
litellm_metadata=None,
metadata=None,
standard_callback_dynamic_params=None,
):
"""Build a model_call_details dict that mimics real proxy/SDK flows."""
litellm_params = {}
if metadata is not None:
litellm_params["metadata"] = metadata
elif metadata_headers is not None:
litellm_params["metadata"] = {"headers": metadata_headers}
else:
litellm_params["metadata"] = {}
# get_litellm_params always sets this key (even when value is None)
litellm_params["litellm_metadata"] = litellm_metadata
details = {"litellm_params": litellm_params}
if standard_callback_dynamic_params is not None:
details["standard_callback_dynamic_params"] = standard_callback_dynamic_params
return details
class TestShouldRedactMessageLogging:
"""Unit tests for should_redact_message_logging()."""
# ---- proxy flow: headers in metadata, litellm_metadata is None ----
def test_enable_redaction_via_x_header_proxy_flow(self):
"""x-litellm-enable-message-redaction header should enable redaction
even when litellm_metadata is None (proxy path)."""
details = _make_model_call_details(
metadata_headers={"x-litellm-enable-message-redaction": "true"},
litellm_metadata=None,
)
assert should_redact_message_logging(details) is True
def test_enable_redaction_via_old_header_proxy_flow(self):
"""litellm-enable-message-redaction header should enable redaction
even when litellm_metadata is None (proxy path)."""
details = _make_model_call_details(
metadata_headers={"litellm-enable-message-redaction": "true"},
litellm_metadata=None,
)
assert should_redact_message_logging(details) is True
def test_disable_redaction_via_header_proxy_flow(self):
"""litellm-disable-message-redaction should suppress redaction
even when global setting is on, and litellm_metadata is None."""
litellm.turn_off_message_logging = True
details = _make_model_call_details(
metadata_headers={"litellm-disable-message-redaction": "true"},
litellm_metadata=None,
)
assert should_redact_message_logging(details) is False
# ---- SDK direct-call flow: headers in litellm_metadata ----
def test_enable_redaction_via_header_in_litellm_metadata(self):
"""Headers inside litellm_metadata (SDK direct call) should work."""
details = _make_model_call_details(
litellm_metadata={"headers": {"x-litellm-enable-message-redaction": "true"}},
)
assert should_redact_message_logging(details) is True
# ---- no headers at all ----
def test_no_headers_defaults_to_global_off(self):
"""Without headers, falls back to global setting (False)."""
details = _make_model_call_details(
metadata_headers=None,
litellm_metadata=None,
)
assert should_redact_message_logging(details) is False
def test_no_headers_global_on(self):
"""Without headers, respects global turn_off_message_logging=True."""
litellm.turn_off_message_logging = True
details = _make_model_call_details(
metadata_headers=None,
litellm_metadata=None,
)
assert should_redact_message_logging(details) is True
# ---- dynamic params take precedence ----
def test_dynamic_param_enables_redaction(self):
"""Dynamic turn_off_message_logging=True should enable redaction."""
details = _make_model_call_details(
metadata_headers={},
litellm_metadata=None,
standard_callback_dynamic_params={"turn_off_message_logging": True},
)
assert should_redact_message_logging(details) is True
def test_dynamic_param_false_overrides_header(self):
"""Dynamic turn_off_message_logging=False should take precedence over enable header."""
details = _make_model_call_details(
metadata_headers={"x-litellm-enable-message-redaction": "true"},
litellm_metadata=None,
standard_callback_dynamic_params={"turn_off_message_logging": False},
)
assert should_redact_message_logging(details) is False
# ---- non-dict metadata safety ----
def test_both_metadata_fields_none(self):
"""When both litellm_metadata and metadata are None, should not raise."""
details = _make_model_call_details(
metadata=None,
litellm_metadata=None,
)
assert should_redact_message_logging(details) is False
def test_both_metadata_fields_none_global_on(self):
"""When both metadata fields are None but global is on, should still return True."""
litellm.turn_off_message_logging = True
details = _make_model_call_details(
metadata=None,
litellm_metadata=None,
)
assert should_redact_message_logging(details) is True