mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(deepkeep): honour empty-list replacements in _build_return_inputs
When DeepKeep returns GUARDRAIL_INTERVENED with an intentional empty replacement (e.g. texts:[], tool_calls:[]) the previous truthiness check treated [] as absent and forwarded the original content downstream — a guardrail bypass for any case where the firewall wants to fully clear a field. Fix: replace all response_json.get(field) truthiness checks with 'is not None' comparisons so that an empty list is respected as a deliberate replacement. Applies to texts, images, tools, tool_calls, and the original-input fallback guards. Adds test_apply_guardrail_honours_empty_list_replacements.
This commit is contained in:
parent
e9780752ea
commit
46e090224f
2 changed files with 65 additions and 8 deletions
|
|
@ -248,23 +248,29 @@ class DeepKeepGuardrail(CustomGuardrail):
|
|||
tool_calls: Any | None,
|
||||
structured_messages: Any | None,
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
"""Merge original inputs with any guardrail-modified values from the API response."""
|
||||
"""Merge original inputs with any guardrail-modified values from the API response.
|
||||
|
||||
Presence is checked with ``is not None`` (not truthiness) so that an
|
||||
intentional empty-list replacement such as ``texts: []`` or
|
||||
``tool_calls: []`` is honoured and forwarded downstream rather than
|
||||
silently discarded in favour of the original content.
|
||||
"""
|
||||
return_inputs = GenericGuardrailAPIInputs(texts=texts)
|
||||
if response_json.get("texts"):
|
||||
if response_json.get("texts") is not None:
|
||||
return_inputs["texts"] = response_json["texts"]
|
||||
if response_json.get("images"):
|
||||
if response_json.get("images") is not None:
|
||||
return_inputs["images"] = response_json["images"]
|
||||
elif images:
|
||||
elif images is not None:
|
||||
return_inputs["images"] = images
|
||||
if response_json.get("tools") is not None:
|
||||
return_inputs["tools"] = response_json["tools"]
|
||||
elif tools:
|
||||
elif tools is not None:
|
||||
return_inputs["tools"] = tools
|
||||
if response_json.get("tool_calls"):
|
||||
if response_json.get("tool_calls") is not None:
|
||||
return_inputs["tool_calls"] = response_json["tool_calls"]
|
||||
elif tool_calls:
|
||||
elif tool_calls is not None:
|
||||
return_inputs["tool_calls"] = tool_calls
|
||||
if structured_messages:
|
||||
if structured_messages is not None:
|
||||
return_inputs["structured_messages"] = structured_messages
|
||||
return return_inputs
|
||||
|
||||
|
|
|
|||
|
|
@ -584,6 +584,57 @@ class TestDeepKeepGuardrail:
|
|||
assert result["tools"] != original_tools
|
||||
assert result["tool_calls"] != original_tool_calls
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_guardrail_honours_empty_list_replacements(self):
|
||||
"""Empty-list replacements from the API must clear the field, not fall back to originals."""
|
||||
guardrail = DeepKeepGuardrail(
|
||||
api_key="test-key",
|
||||
api_base="https://test.deepkeep.ai",
|
||||
firewall_id="fw-123",
|
||||
guardrail_name="test",
|
||||
event_hook="pre_call",
|
||||
)
|
||||
|
||||
mock_response = Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"blocked_reason": None,
|
||||
# DeepKeep clears all content entirely
|
||||
"texts": [],
|
||||
"images": [],
|
||||
"tools": [],
|
||||
"tool_calls": [],
|
||||
},
|
||||
request=Request(
|
||||
"POST",
|
||||
"https://test.deepkeep.ai/v3/openai/beta/litellm_basic_guardrail_api",
|
||||
),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
guardrail.async_handler,
|
||||
"post",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_response,
|
||||
):
|
||||
result = await guardrail.apply_guardrail(
|
||||
inputs={
|
||||
"texts": ["sensitive content that should be cleared"],
|
||||
"tools": [{"type": "function", "function": {"name": "leak_data"}}],
|
||||
"tool_calls": [{"id": "call_1", "type": "function"}],
|
||||
"images": ["data:image/png;base64,abc"],
|
||||
},
|
||||
request_data={"metadata": {}},
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
# Empty-list replacements must be used — not the original non-empty values
|
||||
assert result["texts"] == []
|
||||
assert result.get("images") == []
|
||||
assert result.get("tools") == []
|
||||
assert result.get("tool_calls") == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_firewall_id_in_payload(self):
|
||||
"""should include firewall_id in additional_provider_specific_params."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue