mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): redact provider keys from pass-through failure tracebacks
A failed pass-through call logged the httpx traceback, whose message quotes the upstream URL with the provider API key in its query string, into the spend log's error information and into every failure callback. The error information built for logging now redacts its traceback and error message, and the traceback is redacted once before the failure callbacks receive it.
This commit is contained in:
parent
bf51dea36b
commit
4ec5a6761c
4 changed files with 83 additions and 3 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,6 +6,7 @@ from fastapi import HTTPException
|
|||
|
||||
from litellm.caching.caching import DualCache
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.proxy._types import ProxyErrorTypes
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
|
@ -1919,3 +1920,51 @@ 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):
|
||||
super().__init__()
|
||||
self.received_traceback: str | None = None
|
||||
|
||||
async def async_post_call_failure_hook(self, request_data, original_exception, user_api_key_dict, traceback_str=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
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
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