mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(deepkeep): apply guardrail tool/tool_call redactions from API response
When DeepKeep returns GUARDRAIL_INTERVENED with redacted tools or tool_calls, the previous code ignored those redactions and forwarded the original (potentially sensitive) values to the model — a guardrail bypass for content embedded in tool schemas or function arguments. Fix: prefer response_json["tools"] / response_json["tool_calls"] when present, falling back to the originals only when the guardrail did not return replacements — consistent with the existing pattern for texts and images. Refactor _build_return_inputs() into a private static helper to keep apply_guardrail() under the PLR0915 statement limit (50). Adds test_apply_guardrail_applies_tool_redactions_from_response to assert that redacted tool payloads from the API response are used.
This commit is contained in:
parent
36f95d8d9f
commit
82c6b2f181
2 changed files with 93 additions and 15 deletions
|
|
@ -238,6 +238,36 @@ class DeepKeepGuardrail(CustomGuardrail):
|
|||
verbose_proxy_logger.error("DeepKeep guardrail API error: %s", str(error))
|
||||
raise DeepKeepGuardrailAPIError(f"DeepKeep guardrail API failed: {str(error)}")
|
||||
|
||||
@staticmethod
|
||||
def _build_return_inputs(
|
||||
*,
|
||||
response_json: Dict[str, Any],
|
||||
texts: list,
|
||||
images: Optional[Any],
|
||||
tools: Optional[Any],
|
||||
tool_calls: Optional[Any],
|
||||
structured_messages: Optional[Any],
|
||||
) -> GenericGuardrailAPIInputs:
|
||||
"""Merge original inputs with any guardrail-modified values from the API response."""
|
||||
return_inputs = GenericGuardrailAPIInputs(texts=texts)
|
||||
if response_json.get("texts"):
|
||||
return_inputs["texts"] = response_json["texts"]
|
||||
if response_json.get("images"):
|
||||
return_inputs["images"] = response_json["images"]
|
||||
elif images:
|
||||
return_inputs["images"] = images
|
||||
if response_json.get("tools"):
|
||||
return_inputs["tools"] = response_json["tools"]
|
||||
elif tools:
|
||||
return_inputs["tools"] = tools
|
||||
if response_json.get("tool_calls"):
|
||||
return_inputs["tool_calls"] = response_json["tool_calls"]
|
||||
elif tool_calls:
|
||||
return_inputs["tool_calls"] = tool_calls
|
||||
if structured_messages:
|
||||
return_inputs["structured_messages"] = structured_messages
|
||||
return return_inputs
|
||||
|
||||
@log_guardrail_information
|
||||
async def apply_guardrail(
|
||||
self,
|
||||
|
|
@ -336,21 +366,14 @@ class DeepKeepGuardrail(CustomGuardrail):
|
|||
should_wrap_with_default_message=False,
|
||||
)
|
||||
|
||||
# Build return inputs – apply any modifications from GUARDRAIL_INTERVENED
|
||||
return_inputs = GenericGuardrailAPIInputs(texts=texts)
|
||||
if response_json.get("texts"):
|
||||
return_inputs["texts"] = response_json["texts"]
|
||||
if response_json.get("images"):
|
||||
return_inputs["images"] = response_json["images"]
|
||||
elif images:
|
||||
return_inputs["images"] = images
|
||||
if tools:
|
||||
return_inputs["tools"] = tools
|
||||
if tool_calls:
|
||||
return_inputs["tool_calls"] = tool_calls
|
||||
if structured_messages:
|
||||
return_inputs["structured_messages"] = structured_messages
|
||||
return return_inputs
|
||||
return self._build_return_inputs(
|
||||
response_json=response_json,
|
||||
texts=texts,
|
||||
images=images,
|
||||
tools=tools,
|
||||
tool_calls=tool_calls,
|
||||
structured_messages=structured_messages,
|
||||
)
|
||||
|
||||
except GuardrailRaisedException:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -529,6 +529,61 @@ class TestDeepKeepGuardrail:
|
|||
assert result["tool_calls"] == sample_tool_calls
|
||||
assert result["structured_messages"] == sample_structured
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_guardrail_applies_tool_redactions_from_response(self):
|
||||
"""should use redacted tools/tool_calls from response when GUARDRAIL_INTERVENED returns them."""
|
||||
guardrail = DeepKeepGuardrail(
|
||||
api_key="test-key",
|
||||
api_base="https://test.deepkeep.ai",
|
||||
firewall_id="fw-123",
|
||||
guardrail_name="test",
|
||||
event_hook="pre_call",
|
||||
)
|
||||
|
||||
redacted_tools = [{"type": "function", "function": {"name": "get_data", "description": "[REDACTED]"}}]
|
||||
redacted_tool_calls = [{"id": "call_1", "type": "function", "function": {"name": "get_data", "arguments": "{}"}}]
|
||||
|
||||
mock_response = Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"action": "GUARDRAIL_INTERVENED",
|
||||
"blocked_reason": None,
|
||||
"texts": None,
|
||||
"images": None,
|
||||
"tools": redacted_tools,
|
||||
"tool_calls": redacted_tool_calls,
|
||||
},
|
||||
request=Request(
|
||||
"POST",
|
||||
"https://test.deepkeep.ai/v3/openai/beta/litellm_basic_guardrail_api",
|
||||
),
|
||||
)
|
||||
|
||||
original_tools = [{"type": "function", "function": {"name": "get_data", "description": "sensitive info"}}]
|
||||
original_tool_calls = [{"id": "call_1", "type": "function", "function": {"name": "get_data", "arguments": '{"secret": "value"}'}}]
|
||||
|
||||
with patch.object(
|
||||
guardrail.async_handler,
|
||||
"post",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_response,
|
||||
):
|
||||
result = await guardrail.apply_guardrail(
|
||||
inputs={
|
||||
"texts": ["run the tool"],
|
||||
"tools": original_tools,
|
||||
"tool_calls": original_tool_calls,
|
||||
},
|
||||
request_data={"metadata": {}},
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
# Redacted versions from the API response must be used, not the originals
|
||||
assert result["tools"] == redacted_tools
|
||||
assert result["tool_calls"] == redacted_tool_calls
|
||||
assert result["tools"] != original_tools
|
||||
assert result["tool_calls"] != original_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