Merge pull request #39964 from BerriAI/litellm_lit_7050_redact_failure_traceback

fix(proxy): redact provider keys from pass-through failure tracebacks
This commit is contained in:
Mateo Wang 2026-09-05 16:48:04 -07:00 committed by GitHub
commit 02cbff4918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 4 deletions

View file

@ -5675,8 +5675,8 @@ class StandardLoggingPayloadSetup:
error_code=error_status,
error_class=error_class,
llm_provider=_llm_provider_in_exception,
traceback=traceback_info,
error_message=error_message,
traceback=_redact_string(traceback_info),
error_message=_redact_string(error_message),
error_rate_limit_category=rate_limit_category,
error_rate_limit_type=rate_limit_type,
error_budget_entity_type=budget_error.entity_type if budget_error else None,

View file

@ -2568,6 +2568,8 @@ class ProxyLogging:
# Remove before callbacks iterate — not serialisable
request_data.pop("litellm_logging_obj", None)
redacted_traceback_str: Final = _redact_string(traceback_str) if traceback_str is not None else None
# Track the first HTTPException returned or raised by any callback
transformed_exception: HTTPException | None = None
@ -2586,7 +2588,7 @@ class ProxyLogging:
request_data=request_data,
user_api_key_dict=user_api_key_dict,
original_exception=original_exception,
traceback_str=traceback_str,
traceback_str=redacted_traceback_str,
)
# If callback returned an HTTPException, use it (first one wins)
if isinstance(hook_result, HTTPException) and transformed_exception is None:

View file

@ -6457,3 +6457,32 @@ def test_get_status_fields_ranks_guardrail_flagged_between_success_and_intervene
assert _get_status_fields(
"success", [flagged, {"guardrail_status": "guardrail_intervened"}], None
)["guardrail_status"] == "guardrail_intervened"
def test_get_error_information_redacts_provider_key_from_upstream_url():
"""A pass-through upstream failure logs the httpx traceback, whose message
quotes the upstream URL with the provider key in its query string. That
key must never reach spend logs or logging callbacks."""
import traceback
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
provider_key = "AIza" + "S" * 35
upstream_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini:generateContent?key={provider_key}"
response = httpx.Response(400, request=httpx.Request("POST", upstream_url))
try:
response.raise_for_status()
except httpx.HTTPStatusError as caught:
upstream_error = caught
upstream_traceback = traceback.format_exc()
assert provider_key in upstream_traceback
result = StandardLoggingPayloadSetup.get_error_information(
original_exception=upstream_error, traceback_str=upstream_traceback
)
assert provider_key not in result["traceback"]
assert provider_key not in result["error_message"]
assert "REDACTED" in result["traceback"]
assert "REDACTED" in result["error_message"]
assert result["error_code"] == "400"

View file

@ -6,7 +6,8 @@ from fastapi import HTTPException
from litellm.caching.caching import DualCache
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy._types import ProxyErrorTypes
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import ProxyErrorTypes, UserAPIKeyAuth
from litellm.proxy.utils import ProxyLogging
from litellm.types.guardrails import GuardrailEventHooks
@ -1919,3 +1920,55 @@ async def test_proxy_only_error_5xx_keeps_traceback_and_runs_sync_callbacks(monk
Logging.failure_handler = orig_sync_failure
assert "test_proxy_utils" in captured["async_traceback"]
class _TracebackRecordingLogger(CustomLogger):
def __init__(self) -> None:
super().__init__()
self.received_traceback: str | None = None
async def async_post_call_failure_hook(
self,
request_data: dict,
original_exception: Exception,
user_api_key_dict: UserAPIKeyAuth,
traceback_str: str | None = None,
) -> HTTPException | None:
self.received_traceback = traceback_str
return None
@pytest.mark.asyncio
async def test_post_call_failure_hook_redacts_traceback_before_callbacks(monkeypatch):
"""A pass-through upstream failure hands the hook the httpx traceback, whose
message quotes the upstream URL with the provider key in its query string.
Every callback, custom loggers included, must receive it redacted."""
import traceback
from unittest.mock import AsyncMock, patch
import httpx
provider_key = "AIza" + "S" * 35
upstream_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini:generateContent?key={provider_key}"
response = httpx.Response(400, request=httpx.Request("POST", upstream_url))
try:
response.raise_for_status()
except httpx.HTTPStatusError:
upstream_traceback = traceback.format_exc()
assert provider_key in upstream_traceback
recorder = _TracebackRecordingLogger()
monkeypatch.setattr(litellm, "callbacks", [recorder])
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
proxy_logging_obj.alert_types = []
with patch.object(proxy_logging_obj, "update_request_status", new=AsyncMock()):
await proxy_logging_obj.post_call_failure_hook(
request_data={"metadata": {}},
original_exception=HTTPException(status_code=400, detail="Upstream passthrough request failed with status 400"),
user_api_key_dict=UserAPIKeyAuth(),
traceback_str=upstream_traceback,
)
assert recorder.received_traceback is not None
assert provider_key not in recorder.received_traceback
assert "REDACTED" in recorder.received_traceback