mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(guardrails): extract tool names from additional_tools input items on Responses API
This commit is contained in:
parent
9ad8698aab
commit
ab912c963d
2 changed files with 77 additions and 10 deletions
|
|
@ -198,16 +198,27 @@ class OpenAIResponsesHandler(BaseTranslation):
|
|||
|
||||
def extract_request_tool_names(self, data: dict) -> List[str]:
|
||||
"""Extract tool names from Responses API request (tools[].name for function
|
||||
and custom, tools[].server_label for mcp)."""
|
||||
names: List[str] = []
|
||||
for tool in data.get("tools") or []:
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
if tool.get("type") in ("function", "custom") and tool.get("name"):
|
||||
names.append(str(tool["name"]))
|
||||
elif tool.get("type") == "mcp" and tool.get("server_label"):
|
||||
names.append(str(tool["server_label"]))
|
||||
return names
|
||||
and custom, tools[].server_label for mcp), including tools nested in
|
||||
additional_tools input items (Codex CLI ships its tool definitions there)."""
|
||||
input_items = data.get("input")
|
||||
additional_tools = (
|
||||
tool
|
||||
for item in (input_items if isinstance(input_items, list) else ())
|
||||
if isinstance(item, dict) and item.get("type") == "additional_tools"
|
||||
for tool in (item.get("tools") or ())
|
||||
)
|
||||
names = (self._responses_tool_name(tool) for tool in (*(data.get("tools") or ()), *additional_tools))
|
||||
return [name for name in names if name]
|
||||
|
||||
@staticmethod
|
||||
def _responses_tool_name(tool: object) -> str | None:
|
||||
if not isinstance(tool, dict):
|
||||
return None
|
||||
if tool.get("type") in ("function", "custom") and tool.get("name"):
|
||||
return str(tool["name"])
|
||||
if tool.get("type") == "mcp" and tool.get("server_label"):
|
||||
return str(tool["server_label"])
|
||||
return None
|
||||
|
||||
def _extract_and_transform_tools(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,40 @@ class TestExtractRequestToolNames:
|
|||
"get_current_weather",
|
||||
]
|
||||
|
||||
def test_openai_responses_additional_tools_input_items(self):
|
||||
"""Codex CLI 0.143+ ships tool definitions inside an additional_tools
|
||||
input item instead of (or alongside) top-level tools; those nested tools
|
||||
must be extracted too or a restricted key could smuggle a disallowed
|
||||
tool past allowlist enforcement (VERIA finding on PR #33228)."""
|
||||
data = {
|
||||
"tools": [{"type": "function", "name": "get_current_weather"}],
|
||||
"input": [
|
||||
{"role": "user", "content": "hi"},
|
||||
{
|
||||
"type": "additional_tools",
|
||||
"role": "developer",
|
||||
"tools": [
|
||||
{"type": "custom", "name": "exec", "description": "x"},
|
||||
{"type": "mcp", "server_label": "dmcp", "server_url": "http://x"},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
assert extract_request_tool_names("/v1/responses", data) == [
|
||||
"get_current_weather",
|
||||
"exec",
|
||||
"dmcp",
|
||||
]
|
||||
|
||||
def test_openai_responses_string_input_ignored(self):
|
||||
data = {
|
||||
"tools": [{"type": "function", "name": "get_current_weather"}],
|
||||
"input": "hi",
|
||||
}
|
||||
assert extract_request_tool_names("/v1/responses", data) == [
|
||||
"get_current_weather"
|
||||
]
|
||||
|
||||
def test_anthropic_tools(self):
|
||||
data = {"tools": [{"name": "get_weather"}, {"name": "run_sql"}]}
|
||||
assert extract_request_tool_names("/v1/messages", data) == [
|
||||
|
|
@ -173,6 +207,28 @@ class TestCheckToolsAllowlist:
|
|||
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
|
||||
assert "restricted_tool" in str(exc_info.value.message)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disallowed_additional_tools_input_item_raises_on_responses_route(self):
|
||||
token = _token(metadata={"allowed_tools": ["other_tool"]})
|
||||
body = {
|
||||
"input": [
|
||||
{
|
||||
"type": "additional_tools",
|
||||
"role": "developer",
|
||||
"tools": [{"type": "custom", "name": "exec"}],
|
||||
},
|
||||
],
|
||||
}
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await check_tools_allowlist(
|
||||
request_body=body,
|
||||
valid_token=token,
|
||||
team_object=None,
|
||||
route="/v1/responses",
|
||||
)
|
||||
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
|
||||
assert "exec" in str(exc_info.value.message)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_team_allowlist_used_when_key_empty(self):
|
||||
token = _token(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue