diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index c31c4323157..989777e9412 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -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, diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index accf7b720fb..b4e4dfeae67 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -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: diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 16a99713a06..0f44cbbb2a7 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -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" diff --git a/tests/test_litellm/proxy/test_proxy_utils.py b/tests/test_litellm/proxy/test_proxy_utils.py index f16c6c937d0..dcf18e773e8 100644 --- a/tests/test_litellm/proxy/test_proxy_utils.py +++ b/tests/test_litellm/proxy/test_proxy_utils.py @@ -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