Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ def transform_responses_api_request_to_chat_completion_request(
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:
Expand Down Expand Up @@ -2036,7 +2037,7 @@ def _responses_tool_to_chat_form(tool: Mapping[str, object]) -> ResponsesToolCha
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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,45 @@ def test_drops_tool_choice_when_no_tools(self):
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 = {
Expand Down Expand Up @@ -1659,6 +1698,82 @@ def test_transform_computer_use_tools(self):
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 = {
Expand Down
Loading