Merge pull request #41953 from BerriAI/litellm_bridge_drop_tool_search

fix(responses): drop tool_search and local_shell in the chat completions bridge
This commit is contained in:
Mateo Wang 2026-09-19 04:42:28 -07:00 committed by GitHub
commit 40f51df90f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 117 additions and 1 deletions

View file

@ -466,6 +466,7 @@ class LiteLLMCompletionResponsesConfig:
if not tools:
litellm_completion_request.pop("tool_choice", None)
litellm_completion_request.pop("tools", None)
litellm_completion_request.pop("parallel_tool_calls", None)
# Responses API `Completed` events require usage, we pass `stream_options` to litellm.completion to include usage
if stream is True:
@ -2036,7 +2037,7 @@ class LiteLLMCompletionResponsesConfig:
if tool_type == "custom":
converted: Final = convert_custom_tool_to_function_tool(tool)
return ResponsesToolChatForm(chat_tools=() if converted is None else (converted,), web_search_options=None)
if tool_type in ("computer_use", "image_generation", "shell"):
if tool_type in ("computer_use", "image_generation", "local_shell", "shell", "tool_search"):
verbose_logger.warning(
"Dropping Responses API tool of type '%s': it has no Chat Completions "
"equivalent and the target provider would reject the request.",

View file

@ -1248,6 +1248,45 @@ class TestFunctionCallTransformation:
assert "tool_choice" not in result
assert "tools" not in result
def test_parallel_tool_calls_dropped_when_no_chat_tools_remain(self) -> None:
transform: Final = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request
codex_tool_search: Final = {
"type": "tool_search",
"execution": "client",
"description": "Searches over deferred tool metadata with BM25.",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
}
function_tool: Final = {
"type": "function",
"name": "get_goal",
"description": "Returns the current goal.",
"parameters": {"type": "object", "properties": {}},
"strict": True,
}
empty_tools_result: Final = transform(
model="azure/gpt-5.4-mini",
input="Reply with just the word pong.",
responses_api_request={"tools": [], "parallel_tool_calls": True},
custom_llm_provider="azure",
)
hosted_only_result: Final = transform(
model="azure/gpt-5.4-mini",
input="Reply with just the word pong.",
responses_api_request={"tools": [codex_tool_search], "parallel_tool_calls": True},
custom_llm_provider="azure",
)
function_tools_result: Final = transform(
model="azure/gpt-5.4-mini",
input="Reply with just the word pong.",
responses_api_request={"tools": [function_tool], "parallel_tool_calls": True},
custom_llm_provider="azure",
)
assert "parallel_tool_calls" not in empty_tools_result
assert "parallel_tool_calls" not in hosted_only_result
assert function_tools_result["parallel_tool_calls"] is True
def test_function_call_without_call_id_fallback_to_id(self):
"""Test that function_call items can use 'id' field when 'call_id' is missing"""
function_call_item = {
@ -1659,6 +1698,82 @@ class TestToolTransformation:
assert len(result_tools) == 0
assert web_search_options is None
def test_transform_codex_tools_drops_hosted_tool_search(self) -> None:
codex_tools: Final = [
{
"type": "function",
"name": "exec_command",
"description": "Runs a command in a PTY.",
"parameters": {"type": "object", "properties": {"cmd": {"type": "string"}}, "required": ["cmd"]},
"strict": True,
},
{
"type": "function",
"name": "write_stdin",
"description": "Writes characters to an existing session's stdin.",
"parameters": {
"type": "object",
"properties": {"session_id": {"type": "number"}, "chars": {"type": "string"}},
"required": ["session_id", "chars"],
},
"strict": True,
},
{
"type": "custom",
"name": "apply_patch",
"description": "The `apply_patch` tool can be used to edit files.",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": 'start: begin_patch hunk+ end_patch\nbegin_patch: "*** Begin Patch" LF\n',
},
},
{
"type": "tool_search",
"execution": "client",
"description": (
"# Tool discovery\n\nSearches over deferred tool metadata with BM25 and exposes matching tools "
"for the next model call.\n\nYou have access to tools from the following sources:\n"
"- Multi-agent tools: Spawn and manage sub-agents.\nSome of the tools may not have been provided "
"to you upfront, and you should use this tool (`tool_search`) to search for the required tools. "
"For MCP tool discovery, always use `tool_search` instead of `list_mcp_resources` or "
"`list_mcp_resource_templates`."
),
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "number", "description": "Maximum number of tools to return. Defaults to 8."},
"query": {"type": "string", "description": "Search query for deferred tools."},
},
"required": ["query"],
"additionalProperties": False,
},
},
{"type": "web_search", "external_web_access": False, "search_content_types": ["text", "image"]},
]
function_and_custom_count: Final = sum(1 for tool in codex_tools if tool["type"] in ("function", "custom"))
(
result_tools,
web_search_options,
) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(tools=codex_tools)
assert not any(tool.get("type") == "tool_search" for tool in result_tools)
assert all(tool.get("type") == "function" for tool in result_tools)
assert len(result_tools) == function_and_custom_count
assert web_search_options is not None
def test_transform_local_shell_tools_dropped(self) -> None:
(
result_tools,
web_search_options,
) = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
tools=[{"type": "local_shell"}]
)
assert result_tools == []
assert web_search_options is None
def test_transform_custom_tools_to_function_tools(self):
"""Test that custom (freeform/grammar) tools are converted to function tools"""
custom_tool = {