mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
fix(guardrails): stop logging the request payload as guardrail_response on pre_call hooks
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
16db51e2cf
commit
24962ef054
3 changed files with 76 additions and 15 deletions
|
|
@ -1235,17 +1235,12 @@ class CustomGuardrail(CustomLogger):
|
|||
|
||||
This gets logged on downsteam Langfuse, DataDog, etc.
|
||||
"""
|
||||
# Convert None to empty dict to satisfy type requirements
|
||||
guardrail_response: dict[str, object] | str = {} if response is None else response
|
||||
|
||||
# For apply_guardrail functions in custom_code_guardrail scenario,
|
||||
# simplify the logged response to "allow", "deny", or "mask"
|
||||
if original_inputs is not None and isinstance(response, dict):
|
||||
# Check if inputs were modified by comparing them
|
||||
if self._inputs_were_modified(original_inputs, response):
|
||||
guardrail_response = "mask"
|
||||
else:
|
||||
guardrail_response = "allow"
|
||||
guardrail_response: Final = self._summarize_guardrail_response(
|
||||
response=response,
|
||||
request_data=request_data,
|
||||
event_type=event_type,
|
||||
original_inputs=original_inputs,
|
||||
)
|
||||
|
||||
verbose_logger.debug("Guardrail response: %s", response)
|
||||
|
||||
|
|
@ -1260,6 +1255,28 @@ class CustomGuardrail(CustomLogger):
|
|||
)
|
||||
return response
|
||||
|
||||
def _summarize_guardrail_response(
|
||||
self,
|
||||
response: dict | None, # mutable-ok: matches _process_response signature
|
||||
request_data: dict, # mutable-ok: matches _process_response signature
|
||||
event_type: GuardrailEventHooks | None,
|
||||
original_inputs: dict | None, # mutable-ok: matches _process_response signature
|
||||
) -> dict | str: # mutable-ok: returns the caller's response unchanged
|
||||
"""Reduce a hook's return value to what is safe to log as ``guardrail_response``.
|
||||
|
||||
``apply_guardrail`` returns the (possibly masked) inputs and ``async_pre_call_hook``
|
||||
returns the (possibly modified) request payload. Neither is a provider verdict, and
|
||||
logging them verbatim ships the user's prompt to every logging sink (OTEL spans,
|
||||
Datadog, spend logs), so both collapse to ``"allow"`` / ``"mask"``.
|
||||
"""
|
||||
if response is None:
|
||||
return {}
|
||||
if original_inputs is not None:
|
||||
return "mask" if self._inputs_were_modified(original_inputs, response) else "allow"
|
||||
if event_type == GuardrailEventHooks.pre_call:
|
||||
return "mask" if self._inputs_were_modified(request_data, response) else "allow"
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _is_guardrail_intervention(e: Exception) -> bool:
|
||||
"""Retained spelling for existing callers; prefer ``is_guardrail_intervention``."""
|
||||
|
|
|
|||
|
|
@ -338,10 +338,11 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai
|
|||
estimated cost) and the ``azure`` provider label to the recorded guardrail
|
||||
information. Follows the OpenAI moderation override pattern
|
||||
(openai/moderations.py)."""
|
||||
guardrail_response: Final[dict | str] = ( # mutable-ok: mirrors CustomGuardrail._process_response
|
||||
("mask" if self._inputs_were_modified(original_inputs, response) else "allow")
|
||||
if original_inputs is not None and isinstance(response, dict)
|
||||
else ({} if response is None else response) # mutable-ok: empty placeholder, never mutated
|
||||
guardrail_response: Final = self._summarize_guardrail_response(
|
||||
response=response,
|
||||
request_data=request_data,
|
||||
event_type=event_type,
|
||||
original_inputs=original_inputs,
|
||||
)
|
||||
self.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response=guardrail_response,
|
||||
|
|
|
|||
|
|
@ -2506,3 +2506,46 @@ class TestCustomGuardrailPostCallSuccessDeploymentHook:
|
|||
)
|
||||
|
||||
assert result is replacement
|
||||
|
||||
|
||||
class TestPreCallHookResponseIsNotLoggedVerbatim:
|
||||
"""Regression for LIT-6935: a pre_call hook returning the request payload leaked the prompt
|
||||
into ``guardrail_response`` and from there onto OTEL guardrail spans."""
|
||||
|
||||
@staticmethod
|
||||
def _logged_response(request_data: dict) -> object:
|
||||
entries = request_data["litellm_metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(entries) == 1
|
||||
return entries[0]["guardrail_response"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_call_hook_returning_request_logs_allow(self):
|
||||
class PassthroughGuardrail(CustomGuardrail):
|
||||
@log_guardrail_information
|
||||
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
|
||||
return data
|
||||
|
||||
guardrail = PassthroughGuardrail(guardrail_name="g")
|
||||
data = {"model": "gpt-4.1-mini", "input": "SECRET_PROMPT", "litellm_metadata": {}}
|
||||
|
||||
await guardrail.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(), cache=None, data=data, call_type="aresponses"
|
||||
)
|
||||
|
||||
assert self._logged_response(data) == "allow"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_call_hook_returning_modified_copy_logs_mask(self):
|
||||
class MaskingGuardrail(CustomGuardrail):
|
||||
@log_guardrail_information
|
||||
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
|
||||
return {**data, "input": "[MASKED]"}
|
||||
|
||||
guardrail = MaskingGuardrail(guardrail_name="g")
|
||||
data = {"model": "gpt-4.1-mini", "input": "SECRET_PROMPT", "litellm_metadata": {}}
|
||||
|
||||
await guardrail.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(), cache=None, data=data, call_type="aresponses"
|
||||
)
|
||||
|
||||
assert self._logged_response(data) == "mask"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue