diff --git a/litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py b/litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py index e20dd831861..76c3f701049 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py +++ b/litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py @@ -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 diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_deepkeep.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_deepkeep.py index 0e8cdab6784..c7d1d287675 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_deepkeep.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_deepkeep.py @@ -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."""