Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions litellm/litellm_core_utils/redact_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Comment on lines 140 to +143
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-dict metadata fallback
If litellm_params[metadata_field] is present but not a dict (e.g. None), the code falls back to litellm_params["metadata"] without re-validating it’s a dict. If metadata is also None/non-dict, metadata.get("headers") will raise. Consider normalizing after the fallback (e.g., metadata = metadata if isinstance(metadata, dict) else {}) so the function always handles non-dict inputs safely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed in 37f9bb7 — added a second isinstance check after the fallback so metadata is always normalized to {} when both fields are non-dict. Also added tests for the case where both litellm_metadata and metadata are None.

# 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(
Expand Down
145 changes: 145 additions & 0 deletions tests/test_litellm/litellm_core_utils/test_redact_messages.py
Original file line number Diff line number Diff line change
@@ -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
Loading