diff --git a/litellm/litellm_core_utils/sensitive_data_masker.py b/litellm/litellm_core_utils/sensitive_data_masker.py index 08432ba20c6..15b2c879224 100644 --- a/litellm/litellm_core_utils/sensitive_data_masker.py +++ b/litellm/litellm_core_utils/sensitive_data_masker.py @@ -3,7 +3,7 @@ from typing import Any, Final from pydantic import BaseModel -from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH, DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER from litellm.litellm_core_utils.secret_redaction import REDACTED @@ -226,9 +226,10 @@ def redact_credentials_in_payload(data: Mapping[str, object]) -> Mapping[str, ob and non-string secrets are covered too, which is what a payload rendered straight to stdout needs. ``None`` is preserved so an unset credential still reads as unset, and lists and tuples are rebuilt element by element so a - credential nested inside one is caught as well. A container sitting at the - recursion limit is replaced wholesale rather than passed through, so nesting a - payload deeper than the limit hides it instead of exposing it. + credential nested inside one is caught as well. The walk is bounded only to stop + runaway recursion, and a container sitting at that bound is replaced wholesale + rather than passed through, so burying a credential deeper than the walk goes + hides it instead of exposing it. """ return _redact_mapping(data, 0) @@ -242,7 +243,7 @@ def _redact_entry(key: str, value: object, depth: int) -> object: return REDACTED if not isinstance(value, (Mapping, list, tuple)): return value - if depth >= DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER: + if depth >= DEFAULT_MAX_RECURSE_DEPTH: return REDACTED if isinstance(value, Mapping): return _redact_mapping(value, depth + 1) diff --git a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py index 917ec1fced8..fadc4ca49e9 100644 --- a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py +++ b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py @@ -380,12 +380,12 @@ def test_redact_credentials_in_payload_hides_containers_at_the_recursion_limit(w buries a credential deeper than the limit must get the container hidden rather than handed back verbatim. Nesting through lists costs depth twice as fast as nesting through mappings, so both shapes are pushed well past the limit here.""" - from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload fake_key = "sk-fake-lit6835-past-the-limit" node = {"api_key": fake_key} - for _ in range(2 * DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + 1): + for _ in range(2 * DEFAULT_MAX_RECURSE_DEPTH + 1): node = {"extra_body": node} if wrap == "mapping" else {"providers": [node]} result = redact_credentials_in_payload({**node, "max_tokens": 17}) @@ -393,3 +393,40 @@ def test_redact_credentials_in_payload_hides_containers_at_the_recursion_limit(w assert fake_key not in str(result) assert "REDACTED" in str(result) assert result["max_tokens"] == 17 + + +def test_redact_credentials_in_payload_leaves_a_realistic_tool_schema_intact(): + """The bound must not eat ordinary payloads: a tool whose JSON schema nests an array of + objects inside a nested object is what agent traffic looks like, and the verbose line is + useless if those leaves come back as REDACTED.""" + from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload + + tool = { + "type": "function", + "function": { + "name": "search_orders", + "parameters": { + "type": "object", + "properties": { + "filters": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": {"sku": {"type": "string"}, "qty": {"type": "integer"}}, + }, + } + }, + } + }, + }, + }, + } + + result = redact_credentials_in_payload({"model": "gpt-4o-mini", "tools": [tool], "api_key": "sk-fake-lit6835"}) + + assert "REDACTED" not in str(result["tools"]) + assert result["tools"][0] == tool + assert result["api_key"] == "REDACTED"