mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(responses): treat member-less namespace tools as one flat function in guardrail merge
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
aa1396f8de
commit
7d8beeced7
4 changed files with 39 additions and 11 deletions
|
|
@ -123,7 +123,7 @@
|
|||
"limit": 5
|
||||
},
|
||||
"reportUnnecessaryIsInstance": {
|
||||
"limit": 824
|
||||
"limit": 823
|
||||
},
|
||||
"reportUntypedBaseClass": {
|
||||
"limit": 0
|
||||
|
|
|
|||
|
|
@ -118,9 +118,13 @@ _TOOL_TYPES_NOT_SENT_TO_GUARDRAIL: Final = frozenset(
|
|||
_NAMESPACE_MEMBERS: Final = TypeAdapter(tuple[Mapping[str, object], ...])
|
||||
|
||||
|
||||
def _namespace_members(tool: Mapping[str, object]) -> tuple[Mapping[str, object], ...]:
|
||||
def _namespace_members(tool: Mapping[str, object]) -> tuple[Mapping[str, object], ...] | None:
|
||||
"""``None`` when the namespace has no member list and is flattened to one function named after it."""
|
||||
raw_members: Final = tool.get("tools")
|
||||
if not isinstance(raw_members, Sequence) or isinstance(raw_members, (str, bytes)):
|
||||
return None
|
||||
try:
|
||||
return _NAMESPACE_MEMBERS.validate_python(tool.get("tools") or ())
|
||||
return _NAMESPACE_MEMBERS.validate_python(raw_members)
|
||||
except ValidationError:
|
||||
return ()
|
||||
|
||||
|
|
@ -133,17 +137,21 @@ def _qualified_member_name(namespace: str, member: Mapping[str, object]) -> str:
|
|||
return f"{namespace}__{member.get('name') or ''}"
|
||||
|
||||
|
||||
def _namespace_function_names(tool: Mapping[str, object]) -> tuple[str, ...]:
|
||||
namespace: Final = str(tool.get("name") or "")
|
||||
members: Final = _namespace_members(tool)
|
||||
if members is None:
|
||||
return (namespace,)
|
||||
return tuple(_qualified_member_name(namespace, member) for member in members if _is_function_member(member))
|
||||
|
||||
|
||||
def _flattened_function_names(tools: Sequence[Mapping[str, object]]) -> tuple[str, ...]:
|
||||
"""Names the guardrail sees for ``tools`` once flattened to Chat Completions format."""
|
||||
top_level: Final = tuple(
|
||||
str(tool.get("name") or "") for tool in tools if tool.get("type") in ("function", "custom")
|
||||
)
|
||||
nested: Final = tuple(
|
||||
_qualified_member_name(str(tool.get("name") or ""), member)
|
||||
for tool in tools
|
||||
if tool.get("type") == "namespace"
|
||||
for member in _namespace_members(tool)
|
||||
if _is_function_member(member)
|
||||
name for tool in tools if tool.get("type") == "namespace" for name in _namespace_function_names(tool)
|
||||
)
|
||||
return top_level + nested
|
||||
|
||||
|
|
@ -153,6 +161,8 @@ def _merge_namespace_tool(
|
|||
) -> Mapping[str, object] | None:
|
||||
namespace: Final = str(tool.get("name") or "")
|
||||
members: Final = _namespace_members(tool)
|
||||
if members is None:
|
||||
return tool if namespace in remapped_functions else None
|
||||
surviving: Final = tuple(
|
||||
member
|
||||
for member in members
|
||||
|
|
|
|||
|
|
@ -1388,6 +1388,24 @@ class TestOpenAIResponsesHandlerNamespaceTools:
|
|||
assert result["tools"][2]["name"] == "injected_tool"
|
||||
assert not any(t["name"].startswith("mcp__confluence__") for t in result["tools"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unchanged_guardrail_leaves_custom_mcp_and_memberless_namespace_tools_as_is(self):
|
||||
handler = OpenAIResponsesHandler()
|
||||
tools = [
|
||||
{"type": "custom", "name": "apply_patch", "description": "Apply a patch"},
|
||||
{"type": "mcp", "server_label": "docs", "server_url": "https://mcp.example.com"},
|
||||
{"type": "namespace", "name": "mcp__flat", "description": "Flattened to one function"},
|
||||
]
|
||||
data = {
|
||||
"input": [{"role": "user", "content": "fetch page 1", "type": "message"}],
|
||||
"tools": [dict(t) for t in tools],
|
||||
"model": "gpt-5.3-codex",
|
||||
}
|
||||
|
||||
result = await handler.process_input_messages(data, MockPassThroughGuardrail(guardrail_name="test"))
|
||||
|
||||
assert result["tools"] == tools
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_guardrail_can_still_drop_a_single_namespace_member(self):
|
||||
handler = OpenAIResponsesHandler()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"LIT001": {
|
||||
"limit": 22358
|
||||
"limit": 22352
|
||||
},
|
||||
"LIT002": {
|
||||
"limit": 26775
|
||||
"limit": 26774
|
||||
},
|
||||
"LIT003": {
|
||||
"limit": 269
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"limit": 0
|
||||
},
|
||||
"LIT010": {
|
||||
"limit": 16505
|
||||
"limit": 16504
|
||||
},
|
||||
"LIT011": {
|
||||
"limit": 5535
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue