diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index e812b03385d..c1701724c92 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -286,6 +286,7 @@ from litellm.proxy.litellm_pre_call_utils import ( LiteLLMProxyRequestSetup, add_litellm_data_to_request, get_metadata_variable_name, + redact_proxy_server_request_body_on_guardrail_block, refresh_proxy_server_request_body_snapshot, reject_url_valued_destination, sanitize_for_log, @@ -2670,12 +2671,17 @@ class ProxyBaseLLMRequestProcessing: prefetch_model = self.data.get("model") if llm_router is not None and isinstance(prefetch_model, str): RoutingContext(llm_router).arm_read_prefetch(prefetch_model, self.data) - self.data = await proxy_logging_obj.pre_call_hook( - user_api_key_dict=user_api_key_dict, - data=self.data, - call_type=route_type, - skip_guardrails=skip_guardrails, - ) + try: + self.data = await proxy_logging_obj.pre_call_hook( + user_api_key_dict=user_api_key_dict, + data=self.data, + call_type=route_type, + skip_guardrails=skip_guardrails, + ) + except Exception: + if isinstance(self.data, dict): + redact_proxy_server_request_body_on_guardrail_block(self.data) + raise await _enforce_guardrail_added_tag_budgets( data=self.data, tags_before_guardrails=self._tags_before_guardrails, diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 4444d6d25f9..6a83e17f52b 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -2608,6 +2608,26 @@ def refresh_proxy_server_request_body_snapshot( ) +def redact_proxy_server_request_body_on_guardrail_block( + data: MutableMapping[str, object], +) -> None: + """ + Redact prompt/messages fields in data['proxy_server_request']['body'] when a pre-call + guardrail blocks a request (#50127). + This ensures that raw PII or secret content that triggered a guardrail rejection is not + persisted in plain text in LiteLLM_SpendLogs via async_post_call_failure_hook. + """ + proxy_server_request: Final = data.get("proxy_server_request") + if not isinstance(proxy_server_request, dict): + return + body: Final = proxy_server_request.get("body") + if not isinstance(body, dict): + return + for field in ("messages", "prompt", "input"): + if field in body: + body[field] = "[REDACTED - BLOCKED BY GUARDRAIL]" + + async def add_litellm_data_to_request( data: dict, request: Request, diff --git a/tests/unit/proxy/test_litellm_pre_call_utils.py b/tests/unit/proxy/test_litellm_pre_call_utils.py index 91c87c63067..a0f8043b8f7 100644 --- a/tests/unit/proxy/test_litellm_pre_call_utils.py +++ b/tests/unit/proxy/test_litellm_pre_call_utils.py @@ -1391,6 +1391,37 @@ def test_refresh_proxy_server_request_body_snapshot_picks_up_guardrail_masking() assert "123-45-6789" not in json.dumps(refreshed_body) +def test_redact_proxy_server_request_body_on_guardrail_block(): + """ + Regression: When a pre-call guardrail blocks a request (e.g. secret or PII detected), + pre_call_hook raises an exception. The raw snapshot in proxy_server_request['body'] + must be redacted so raw blocked text is not written into SpendLogs in plain text (#50127). + """ + from litellm.proxy.litellm_pre_call_utils import ( + redact_proxy_server_request_body_on_guardrail_block, + ) + + data = { + "model": "gpt-4", + "messages": [{"role": "user", "content": "secret password is supersecret"}], + "proxy_server_request": { + "url": "http://localhost/v1/chat/completions", + "body": { + "model": "gpt-4", + "messages": [{"role": "user", "content": "secret password is supersecret"}], + "prompt": "supersecret prompt", + }, + }, + } + + redact_proxy_server_request_body_on_guardrail_block(data) + + body = data["proxy_server_request"]["body"] + assert body["messages"] == "[REDACTED - BLOCKED BY GUARDRAIL]" + assert body["prompt"] == "[REDACTED - BLOCKED BY GUARDRAIL]" + assert "supersecret" not in json.dumps(body) + + @pytest.mark.asyncio async def test_add_litellm_data_to_request_strips_string_encoded_admin_injection(): """Regression: metadata arriving as a JSON string (multipart/form-data or