mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(panw_prisma_airs): scan tool call arguments that arrive already parsed
The tool call slice types arguments as a string, so a client posting parsed JSON failed validation and the whole tool call, name included, read as unscannable and was skipped without ever reaching AIRS. The OpenAI request path forwards client-supplied tool_calls verbatim, so that shape is reachable. Coerce non-string arguments instead of rejecting them, so the content is scanned.
This commit is contained in:
parent
a477c3e841
commit
f6aff5f089
2 changed files with 45 additions and 1 deletions
|
|
@ -15,7 +15,7 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
|
|
@ -65,6 +65,20 @@ class _ToolCallFunctionSlice(BaseModel):
|
|||
name: str | None = None
|
||||
arguments: str | None = None
|
||||
|
||||
@field_validator("arguments", mode="before")
|
||||
@classmethod
|
||||
def _coerce_arguments(cls, value: object) -> str | None:
|
||||
"""Accept arguments that are already-parsed JSON.
|
||||
|
||||
The OpenAI request path forwards client-supplied ``tool_calls`` verbatim, so a
|
||||
client can post a dict here. Rejecting it would make the whole tool call read as
|
||||
unscannable and skip it silently, which is the one outcome a scanner must never
|
||||
have.
|
||||
"""
|
||||
if value is None or isinstance(value, str):
|
||||
return value
|
||||
return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
||||
|
||||
|
||||
class _ToolCallSlice(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
||||
|
|
|
|||
|
|
@ -2762,6 +2762,36 @@ class TestPanwAirsToolCallContentScan:
|
|||
|
||||
assert mock_api.call_args.kwargs["content"] == "list_items"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parsed_dict_arguments_are_still_scanned(self, handler):
|
||||
"""A client can post tool call arguments as already-parsed JSON.
|
||||
|
||||
The OpenAI request path forwards client-supplied tool calls verbatim, so this
|
||||
shape reaches the scanner. It must be scanned, not dropped as unreadable, or the
|
||||
content is a silent bypass.
|
||||
"""
|
||||
|
||||
tool_call = {
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "exfiltrate", "arguments": {"ssn": "123-45-6789"}},
|
||||
}
|
||||
|
||||
with patch.object(handler, "_call_panw_api", new_callable=AsyncMock) as mock_api:
|
||||
mock_api.return_value = {"action": "allow", "category": "benign"}
|
||||
await handler._scan_tool_calls_for_guardrail(
|
||||
tool_calls=[tool_call],
|
||||
is_response=False,
|
||||
metadata={"user": "test", "model": "gpt-4"},
|
||||
call_id="test-call-id",
|
||||
request_data={"litellm_call_id": "test-call-id"},
|
||||
start_time=datetime.now(),
|
||||
)
|
||||
|
||||
mock_api.assert_called_once()
|
||||
assert "123-45-6789" in mock_api.call_args.kwargs["content"]
|
||||
assert "exfiltrate" in mock_api.call_args.kwargs["content"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_tool_call_is_skipped(self, handler):
|
||||
"""Custom tool calls carry no function payload, so they are skipped instead of crashing."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue