refactor(proxy): always redact credential-named fields in stored spend-log payloads

This commit is contained in:
Yucheng He 2026-09-30 10:23:38 -07:00
parent 8f46ffc942
commit 9ffc144f2a
2 changed files with 12 additions and 14 deletions

View file

@ -1095,16 +1095,14 @@ def _sanitize_request_body_for_spend_logs_payload(
request_body: Mapping[str, object],
visited: set | None = None,
max_string_length_prompt_in_db: int | None = None,
redact_credentials: bool = False,
) -> dict:
"""
Recursively sanitize request body to prevent logging large base64 strings or other large values.
Truncates strings longer than MAX_STRING_LENGTH_PROMPT_IN_DB characters and handles nested dictionaries.
At every nesting level, also strips keys listed in _SENSITIVE_REQUEST_BODY_KEYS (e.g. secret_fields,
which holds raw HTTP headers including Authorization tokens). With ``redact_credentials``, string
values under keys SensitiveDataMasker classifies as credentials are replaced with
REDACTED_BY_LITELM_STRING.
which holds raw HTTP headers including Authorization tokens), and replaces string values under keys
SensitiveDataMasker classifies as credentials with REDACTED_BY_LITELM_STRING.
"""
from litellm.constants import (
LITELLM_TRUNCATED_PAYLOAD_FIELD,
@ -1124,9 +1122,7 @@ def _sanitize_request_body_for_spend_logs_payload(
def _sanitize_value(value: object) -> object:
if isinstance(value, Mapping):
return _sanitize_request_body_for_spend_logs_payload(
value, visited, max_string_length_prompt_in_db, redact_credentials
)
return _sanitize_request_body_for_spend_logs_payload(value, visited, max_string_length_prompt_in_db)
elif isinstance(value, list):
return [_sanitize_value(item) for item in value]
elif isinstance(value, str):
@ -1164,7 +1160,7 @@ def _sanitize_request_body_for_spend_logs_payload(
return value
return {
k: REDACTED_BY_LITELM_STRING if redact_credentials and _is_request_body_credential(k, v) else _sanitize_value(v)
k: REDACTED_BY_LITELM_STRING if _is_request_body_credential(k, v) else _sanitize_value(v)
for k, v in request_body.items()
if k not in _SENSITIVE_REQUEST_BODY_KEYS
}
@ -1582,7 +1578,7 @@ def _get_proxy_server_request_for_spend_logs_payload(
_request_body = _convert_mapping_to_json_serializable(without_classifier_audit(_request_body))
perform_redaction(model_call_details=_request_body, result=None)
_request_body = _sanitize_request_body_for_spend_logs_payload(_request_body, redact_credentials=True)
_request_body = _sanitize_request_body_for_spend_logs_payload(_request_body)
_request_body_json_str: Final = safe_dumps(_request_body)
if LITELLM_TRUNCATED_PAYLOAD_FIELD in _request_body_json_str:
verbose_proxy_logger.info(

View file

@ -612,7 +612,7 @@ def test_sanitize_request_body_for_spend_logs_payload_mixed_types():
request_body = {
"text": long_string,
"number": 42,
"nested": {"list": ["short", long_string], "dict": {"key": long_string}},
"nested": {"list": ["short", long_string], "dict": {"value": long_string}},
}
sanitized = _sanitize_request_body_for_spend_logs_payload(request_body)
@ -631,7 +631,7 @@ def test_sanitize_request_body_for_spend_logs_payload_mixed_types():
assert sanitized["number"] == 42
assert sanitized["nested"]["list"][0] == "short"
assert len(sanitized["nested"]["list"][1]) == expected_length
assert len(sanitized["nested"]["dict"]["key"]) == expected_length
assert len(sanitized["nested"]["dict"]["value"]) == expected_length
def test_sanitize_request_body_for_spend_logs_payload_uses_runtime_env_override(
@ -2781,10 +2781,12 @@ def test_proxy_server_request_payload_redacts_provider_credentials(mock_should_s
assert parsed["messages"] == [{"role": "user", "content": "hello"}]
def test_sanitize_request_body_keeps_credential_named_fields_by_default() -> None:
response: Final = {"system_fingerprint": "fp_123", "usage": {"prompt_tokens": 1}}
def test_sanitize_response_redacts_credential_named_fields() -> None:
response: Final = {"access_token": "canary-oauth-token", "usage": {"prompt_tokens": 1}}
assert _sanitize_request_body_for_spend_logs_payload({"response": response}) == {"response": response}
assert _sanitize_request_body_for_spend_logs_payload({"response": response}) == {
"response": {"access_token": REDACTED_BY_LITELM_STRING, "usage": {"prompt_tokens": 1}}
}
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")