mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
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:
commit
02cbff4918
4 changed files with 88 additions and 4 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue