fix(security): redact error_message from spend logs when prompt logging is disabled

This commit is contained in:
Josh 2026-04-17 17:19:53 -04:00
parent 72a461ba4a
commit baef9f1574
2 changed files with 69 additions and 0 deletions

View file

@ -137,6 +137,17 @@ def _get_spend_logs_metadata(
clean_metadata["litellm_overhead_time_ms"] = litellm_overhead_time_ms
clean_metadata["cost_breakdown"] = cost_breakdown
# Redact error_message when prompt/response storage is disabled.
# str(exception) for provider errors (e.g. BadRequestError) includes the raw
# response body (Received={...}), which violates the no-content-logging policy.
if not _should_store_prompts_and_responses_in_spend_logs():
error_info = clean_metadata.get("error_information")
if isinstance(error_info, dict) and error_info.get("error_message"):
clean_metadata["error_information"] = {
**error_info,
"error_message": None,
}
return clean_metadata

View file

@ -1469,3 +1469,61 @@ class TestIsMasterKey:
master = "sk-master-key-123"
hashed = hash_token(master)
assert _is_master_key(api_key=hashed, _master_key=master) is True
# ---------------------------------------------------------------------------
# error_message redaction in spend logs
# ---------------------------------------------------------------------------
STORE_PROMPTS_PATH = "litellm.proxy.spend_tracking.spend_tracking_utils._should_store_prompts_and_responses_in_spend_logs"
def _failure_metadata_with_error(error_msg: str) -> dict:
return {
"user_api_key": "sk-test",
"status": "failure",
"error_information": {
"error_code": "400",
"error_class": "BadRequestError",
"llm_provider": "vertex_ai",
"traceback": "Traceback ...",
"error_message": error_msg,
},
}
@patch(STORE_PROMPTS_PATH, return_value=False)
def test_error_message_redacted_when_store_prompts_disabled(mock_store):
"""error_message must be None when store_prompts_in_spend_logs is False."""
raw = 'litellm.BadRequestError: Received={"candidates": [{"content": {"role": "model", "parts": [{"text": "secret content"}]}}]}'
result = _get_spend_logs_metadata(_failure_metadata_with_error(raw))
assert result["error_information"]["error_message"] is None
@patch(STORE_PROMPTS_PATH, return_value=True)
def test_error_message_kept_when_store_prompts_enabled(mock_store):
"""error_message must be preserved when store_prompts_in_spend_logs is True."""
raw = 'litellm.BadRequestError: Received={"candidates": [{"content": {"role": "model"}}]}'
result = _get_spend_logs_metadata(_failure_metadata_with_error(raw))
assert result["error_information"]["error_message"] == raw
@patch(STORE_PROMPTS_PATH, return_value=False)
def test_other_error_fields_preserved_when_message_redacted(mock_store):
"""error_code, error_class, llm_provider, and traceback must survive redaction."""
raw = "error with content"
result = _get_spend_logs_metadata(_failure_metadata_with_error(raw))
info = result["error_information"]
assert info["error_code"] == "400"
assert info["error_class"] == "BadRequestError"
assert info["llm_provider"] == "vertex_ai"
assert info["traceback"] == "Traceback ..."
assert info["error_message"] is None
@patch(STORE_PROMPTS_PATH, return_value=False)
def test_no_error_when_error_information_is_none(mock_store):
"""No crash when error_information is absent (success path)."""
metadata = {"user_api_key": "sk-test", "status": "success"}
result = _get_spend_logs_metadata(metadata)
assert result.get("error_information") is None