mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(guardrails): backfill resolved hook onto self-recorded guardrail entries
Guardrails like Presidio record their guardrail entry inside apply_guardrail (without event_type), so the decorator's resolved hook never reached the logged entry. Backfill the resolved event_type onto entries recorded during the invocation when they carry the raw-config fallback mode.
This commit is contained in:
parent
bb81b1dae0
commit
ef6d14fbe4
2 changed files with 86 additions and 0 deletions
|
|
@ -1148,6 +1148,44 @@ def log_guardrail_information(func):
|
|||
total += len(entries)
|
||||
return total
|
||||
|
||||
def _backfill_event_type_on_recorded_entries(
|
||||
guardrail: "CustomGuardrail",
|
||||
request_data: dict,
|
||||
entries_before: int,
|
||||
event_type: Optional[GuardrailEventHooks],
|
||||
) -> None:
|
||||
"""Overwrite the raw-config fallback mode on entries the wrapped
|
||||
function recorded itself.
|
||||
|
||||
Guardrails like Presidio call
|
||||
``add_standard_logging_guardrail_information_to_request_data``
|
||||
internally (without ``event_type``), so those entries fall back to
|
||||
the configured ``event_hook``. The wrapper is the only place that
|
||||
knows the concrete hook that fired, so patch it onto the entries
|
||||
added during this invocation. Entries whose mode differs from the
|
||||
fallback were set deliberately and are left untouched.
|
||||
"""
|
||||
if event_type is None:
|
||||
return
|
||||
fallback_mode = guardrail._resolve_logged_guardrail_mode(None)
|
||||
seen = 0
|
||||
for container_key in ("metadata", "litellm_metadata"):
|
||||
container = request_data.get(container_key)
|
||||
if not isinstance(container, dict):
|
||||
continue
|
||||
entries = container.get("standard_logging_guardrail_information")
|
||||
if not isinstance(entries, list):
|
||||
continue
|
||||
for entry in entries:
|
||||
seen += 1
|
||||
if seen <= entries_before or not isinstance(entry, dict):
|
||||
continue
|
||||
if (
|
||||
entry.get("guardrail_name") == guardrail.guardrail_name
|
||||
and entry.get("guardrail_mode") == fallback_mode
|
||||
):
|
||||
entry["guardrail_mode"] = event_type
|
||||
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
start_time = datetime.now() # Move start_time inside the wrapper
|
||||
|
|
@ -1165,6 +1203,7 @@ def log_guardrail_information(func):
|
|||
try:
|
||||
response = await func(*args, **kwargs)
|
||||
if _count_recorded_guardrail_entries(request_data) > entries_before:
|
||||
_backfill_event_type_on_recorded_entries(self, request_data, entries_before, event_type)
|
||||
return response
|
||||
return self._process_response(
|
||||
response=response,
|
||||
|
|
@ -1177,6 +1216,7 @@ def log_guardrail_information(func):
|
|||
)
|
||||
except Exception as e:
|
||||
if _count_recorded_guardrail_entries(request_data) > entries_before:
|
||||
_backfill_event_type_on_recorded_entries(self, request_data, entries_before, event_type)
|
||||
raise
|
||||
return self._process_error(
|
||||
e=e,
|
||||
|
|
@ -1206,6 +1246,7 @@ def log_guardrail_information(func):
|
|||
try:
|
||||
response = func(*args, **kwargs)
|
||||
if _count_recorded_guardrail_entries(request_data) > entries_before:
|
||||
_backfill_event_type_on_recorded_entries(self, request_data, entries_before, event_type)
|
||||
return response
|
||||
return self._process_response(
|
||||
response=response,
|
||||
|
|
@ -1216,6 +1257,7 @@ def log_guardrail_information(func):
|
|||
)
|
||||
except Exception as e:
|
||||
if _count_recorded_guardrail_entries(request_data) > entries_before:
|
||||
_backfill_event_type_on_recorded_entries(self, request_data, entries_before, event_type)
|
||||
raise
|
||||
return self._process_error(
|
||||
e=e,
|
||||
|
|
|
|||
|
|
@ -1513,6 +1513,50 @@ class TestEventTypeLogging:
|
|||
assert "pre_call" in mode
|
||||
assert "post_call" in mode
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_recording_guardrail_gets_resolved_mode_backfilled(self):
|
||||
"""Guardrails like Presidio record their own guardrail entry inside
|
||||
apply_guardrail (without event_type), so the entry falls back to the
|
||||
raw config. The decorator must backfill the resolved hook onto those
|
||||
entries."""
|
||||
from litellm.integrations.custom_guardrail import log_guardrail_information
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
|
||||
class SelfRecordingGuardrail(CustomGuardrail):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
guardrail_name="test_self_recording",
|
||||
event_hook=[
|
||||
GuardrailEventHooks.pre_call,
|
||||
GuardrailEventHooks.post_call,
|
||||
],
|
||||
)
|
||||
|
||||
@log_guardrail_information
|
||||
async def apply_guardrail(self, inputs, request_data, input_type, **kwargs):
|
||||
# mimic presidio's check_pii: record the entry internally,
|
||||
# with no event_type available at this depth
|
||||
self.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response={"result": "ok"},
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
event_type=None,
|
||||
)
|
||||
return inputs
|
||||
|
||||
guardrail = SelfRecordingGuardrail()
|
||||
request_data = {"metadata": {}}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["hi"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
logged_info = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(logged_info) == 1
|
||||
assert logged_info[0]["guardrail_mode"] == GuardrailEventHooks.pre_call
|
||||
|
||||
|
||||
class TestTracingFieldsPopulation:
|
||||
"""Verify add_standard_logging_guardrail_information_to_request_data passes tracing_detail fields."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue