mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(guardrails): redact the participant name on a message
`name` on a user or assistant turn identifies a person and was going to the provider intact. The proxy this integrates with already redacts it, so the integration was the weaker of the two. On a tool or function turn the same field carries the function's name, which has to arrive unchanged or the call stops routing. That case is skipped, and a test asserts the value is never even sent to the shield.
This commit is contained in:
parent
a0abb9a499
commit
6536d61adf
2 changed files with 41 additions and 0 deletions
|
|
@ -122,6 +122,18 @@ def _collect_content(container: MutableRequest, slots: _SlotSink) -> None:
|
|||
_collect(part, "text", slots)
|
||||
|
||||
|
||||
def _collect_participant_name(message: MutableRequest, slots: _SlotSink) -> None:
|
||||
"""Redacts `name` where it identifies a person, never where it names a function.
|
||||
|
||||
On a user or assistant turn `name` is the participant, which is personal data.
|
||||
On a tool or function turn the same field carries the function's name and has
|
||||
to reach the provider unchanged, or the call no longer routes.
|
||||
"""
|
||||
if message.get("role") in ("tool", "function"):
|
||||
return
|
||||
_collect(message, "name", slots)
|
||||
|
||||
|
||||
def _collect_tool_arguments(message: MutableRequest, slots: _SlotSink) -> None:
|
||||
"""Tool arguments carry the values a user asked the model to act on."""
|
||||
for tool_call in message.get("tool_calls") or ():
|
||||
|
|
@ -311,6 +323,7 @@ class LLMShieldProxyGuardrail(CustomGuardrail):
|
|||
for message in data.get("messages") or ():
|
||||
if isinstance(message, dict):
|
||||
_collect_content(message, slots)
|
||||
_collect_participant_name(message, slots)
|
||||
_collect_tool_arguments(message, slots)
|
||||
_collect_responses_fields(data, slots)
|
||||
_collect_prompt(data, slots)
|
||||
|
|
|
|||
|
|
@ -330,6 +330,34 @@ class TestRequestCoverage:
|
|||
|
||||
assert data["input"] == ["[EMAIL_1]", "[PHONE_1]"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_participant_name_is_redacted(self):
|
||||
"""`name` on a user turn identifies a person."""
|
||||
guardrail = _guardrail()
|
||||
mock = _mock_post(guardrail, {"texts": ["hi", "[PERSON_1]"]})
|
||||
|
||||
data = {"messages": [{"role": "user", "name": "Jane Doe", "content": "hi"}]}
|
||||
await guardrail.async_pre_call_hook(user_api_key_dict=None, cache=None, data=data, call_type="completion")
|
||||
|
||||
assert mock.call_args_list[0].kwargs["json"]["texts"] == ["hi", "Jane Doe"]
|
||||
assert data["messages"][0]["name"] == "[PERSON_1]"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_function_name_is_left_alone(self):
|
||||
"""On a tool turn the same field is the function name.
|
||||
|
||||
Redacting it would stop the call routing, so this asserts it is never sent
|
||||
to the shield at all.
|
||||
"""
|
||||
guardrail = _guardrail()
|
||||
mock = _mock_post(guardrail, {"texts": ["result"]})
|
||||
|
||||
data = {"messages": [{"role": "tool", "name": "get_weather", "content": "result"}]}
|
||||
await guardrail.async_pre_call_hook(user_api_key_dict=None, cache=None, data=data, call_type="completion")
|
||||
|
||||
assert data["messages"][0]["name"] == "get_weather"
|
||||
assert mock.call_args_list[0].kwargs["json"]["texts"] == ["result"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_every_shape_in_one_request_is_redacted(self):
|
||||
guardrail = _guardrail()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue