From 1e5ae915c814041e77dd4f710b89022c97c687bd Mon Sep 17 00:00:00 2001 From: yucheng Date: Sat, 31 Oct 2026 14:24:00 -0700 Subject: [PATCH] fix(proxy): redact a refused request as soon as the pre-call walk ends --- litellm/proxy/common_request_processing.py | 4 +--- .../proxy/test_common_request_processing.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index cd67e6fbd60..273efd62f71 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -2633,10 +2633,9 @@ class ProxyBaseLLMRequestProcessing: skip_guardrails=skip_guardrails, routing_probe=routing_probe, ) - except Exception: + finally: if routing_probe is not None and routing_probe.refusal is not None: self._redact_request_its_skipped_guardrails_never_scanned() - raise await _enforce_guardrail_added_tag_budgets( data=self.data, tags_before_guardrails=self._tags_before_guardrails, @@ -3078,7 +3077,6 @@ class ProxyBaseLLMRequestProcessing: phase_event("litellm.request.pre_call_completed") refusal: Final = None if self._routing_probe is None else self._routing_probe.refusal if refusal is not None: - self._redact_request_its_skipped_guardrails_never_scanned() raise refusal _post_call_guardrails_active: Final = self._has_post_call_guardrails() diff --git a/tests/unit/proxy/test_common_request_processing.py b/tests/unit/proxy/test_common_request_processing.py index 26f0fd6727e..b477b9479d9 100644 --- a/tests/unit/proxy/test_common_request_processing.py +++ b/tests/unit/proxy/test_common_request_processing.py @@ -12991,6 +12991,13 @@ class _ModelMappingHook(CustomLogger): return data +class _TaggingHook(CustomLogger): + """A customer callback that adds a tag to the request.""" + + async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type): + return {**data, "metadata": {**(data.get("metadata") or {}), "tags": ["over-budget-tag"]}} + + class _EmailMaskingHook(CustomLogger): async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type): _mask_emails(data) @@ -13419,6 +13426,21 @@ class TestUnroutableModelFailureLogHidesUnscannedContent: assert (first.status, limited.status, guardrail.calls) == (400, 429, 0) assert _shows_no_raw_content(limited), limited.failure_dumps + @pytest.mark.asyncio + async def test_failure_log_hides_content_when_a_tag_budget_rejects_after_a_skipped_guardrail( + self, monkeypatch: pytest.MonkeyPatch + ): + async def over_budget(**_: object) -> None: + raise litellm.BudgetExceededError(current_cost=10.0, max_budget=5.0) + + monkeypatch.setattr(litellm.proxy.common_request_processing, "tag_max_budget_check_for_tags", over_budget) + guardrail: Final = _VendorGuardrail(behavior="mask") + + sent: Final = await _send(monkeypatch, [_TaggingHook(), guardrail]) + + assert (sent.status, guardrail.calls) == (422, 0), sent.message + assert _shows_no_raw_content(sent), sent.failure_dumps + @pytest.mark.asyncio async def test_built_in_hook_after_a_skipped_guardrail_is_not_sent_the_content_it_would_have_masked( self, monkeypatch: pytest.MonkeyPatch