This commit is contained in:
devin-ai-integration[bot] 2026-08-27 19:33:02 -05:00 committed by GitHub
commit 55e1373f96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 136 additions and 1 deletions

View file

@ -0,0 +1,27 @@
"""
Utilities for handling OpenAI Responses API 'namespace' tools when bridging to Chat
Completions providers.
A namespace tool is a grouping container: it carries no callable schema of its own and
holds its callable tools under ``tools``. Chat Completions has no equivalent container,
so the bridge replaces each namespace with the tools it contains, which then go through
the same conversion as any top level tool. Namespaces are flat in practice, and a
namespace that somehow contains another one keeps the inner container, which the
conversion then drops as an unsupported type.
"""
from collections.abc import Sequence
from typing import TypeAlias
from openai.types.responses.tool_param import FunctionToolParam
from litellm.types.llms.openai import OpenAIMcpServerTool
ResponsesAPITool: TypeAlias = FunctionToolParam | OpenAIMcpServerTool
def flatten_namespace_tools(tools: Sequence[ResponsesAPITool]) -> tuple[ResponsesAPITool, ...]:
"""Replace every namespace tool with the tools it contains."""
return tuple(
nested for tool in tools for nested in (tool.get("tools") or () if tool.get("type") == "namespace" else (tool,))
)

View file

@ -96,6 +96,7 @@ from .custom_tools import (
unwrap_custom_tool_arguments,
validated_allowed_callers,
)
from .namespace_tools import flatten_namespace_tools
NamespaceNameMap: TypeAlias = Mapping[str, tuple[str, str]]
NamespaceTool: TypeAlias = Mapping[str, object]
@ -1848,7 +1849,7 @@ class LiteLLMCompletionResponsesConfig:
LiteLLMCompletionResponsesConfig._validate_namespace_name_collisions(tools)
chat_completion_tools: Final[list[ChatCompletionToolParam | OpenAIMcpServerTool]] = []
web_search_options: OpenAIWebSearchOptions | None = None
for tool in tools:
for tool in flatten_namespace_tools(tools):
if tool.get("type") == "mcp":
chat_completion_tools.append(cast(OpenAIMcpServerTool, tool))
elif tool.get("type") == "web_search_preview" or tool.get("type") == "web_search":

View file

@ -1870,6 +1870,113 @@ class TestToolTransformation:
== "string"
)
def test_transform_namespace_tool_expands_nested_function_tools(self):
"""
Regression for issue 2 of #35878: a namespace tool is a grouping container whose
nested tools are ordinary function tools. Dropping the container dropped every
nested tool with it, so a bridged client lost the sub-agent tools it declared.
"""
namespace_tool = {
"type": "namespace",
"name": "collaboration",
"description": "sub-agent management",
"tools": [
{
"type": "function",
"name": "spawn_agent",
"description": "start a sub-agent",
"parameters": {
"type": "object",
"properties": {"task": {"type": "string"}},
"required": ["task"],
},
},
{
"type": "function",
"name": "close_agent",
"description": "stop a sub-agent",
},
],
}
(
result_tools,
web_search_options,
) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
tools=[namespace_tool]
)
assert [tool["type"] for tool in result_tools] == ["function", "function"]
assert [tool["function"]["name"] for tool in result_tools] == [
"spawn_agent",
"close_agent",
]
assert result_tools[0]["function"]["parameters"]["properties"] == {
"task": {"type": "string"}
}
assert result_tools[1]["function"]["parameters"] == {"type": "object"}
assert not any(tool.get("type") == "namespace" for tool in result_tools)
assert web_search_options is None
def test_transform_namespace_tool_alongside_top_level_tools(self):
"""Nested tools land in the flat list next to top-level tools, and unsupported
built-ins inside the namespace are still dropped rather than passed through."""
tools = [
{
"type": "function",
"name": "shell_command",
"parameters": {"type": "object", "properties": {}},
},
{
"type": "namespace",
"name": "codex_app",
"tools": [
{"type": "function", "name": "read_file"},
{"type": "image_generation", "output_format": "png"},
],
},
]
(
result_tools,
_,
) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
tools=tools
)
assert [tool["function"]["name"] for tool in result_tools] == [
"shell_command",
"read_file",
]
def test_transform_nested_namespace_container_is_dropped(self):
"""A namespace inside a namespace is not something clients send, but its inner
container must never reach the provider as a tool without a function schema."""
tools = [
{
"type": "namespace",
"name": "outer",
"tools": [
{"type": "function", "name": "outer_tool"},
{
"type": "namespace",
"name": "inner",
"tools": [{"type": "function", "name": "inner_tool"}],
},
],
}
]
(
result_tools,
_,
) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
tools=tools
)
assert [tool["function"]["name"] for tool in result_tools] == ["outer_tool"]
assert not any(tool.get("type") == "namespace" for tool in result_tools)
def test_bedrock_anthropic_drops_derived_web_search_options(self):
"""
Regression for LIT-3858: a Responses web_search tool becomes a derived