Python: Fix single-tool input handling in OpenAIResponsesClient._prepare_tools_for_openai (#4312)

* Fix OpenAIResponsesClient mishandling single-tool inputs (#4304)

Use normalize_tools() in _prepare_tools_for_openai to wrap single tools
(FunctionTool or dict) in a list before iteration, consistent with the
chat client implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback for #4304

- Use precise type annotation matching normalize_tools/OpenAIChatClient signature
  instead of collapsed Sequence[Any] | Any | None
- Move emptiness guard after normalize_tools() call so single falsy tool
  objects are not silently swallowed
- Import ToolTypes for the type annotation
- Expand test_prepare_tools_for_openai_single_function_tool assertions to
  verify parameters, strict, and parameter schema fields
- Add test_prepare_tools_for_openai_none to verify None input returns []

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-02-27 07:09:28 +09:00
committed by GitHub
Unverified
parent 6f7e55c430
commit ff124c44a9
2 changed files with 55 additions and 4 deletions
@@ -43,6 +43,8 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
ToolTypes,
normalize_tools,
)
from .._types import (
Annotation,
@@ -425,21 +427,24 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
# region Prep methods
def _prepare_tools_for_openai(self, tools: Sequence[Any] | None) -> list[Any]:
def _prepare_tools_for_openai(
self, tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
) -> list[Any]:
"""Prepare tools for the OpenAI Responses API.
Converts FunctionTool to Responses API format. All other tools pass through unchanged.
Args:
tools: Sequence of tools to prepare.
tools: A single tool or sequence of tools to prepare.
Returns:
List of tool parameters ready for the OpenAI API.
"""
if not tools:
tools_list = normalize_tools(tools)
if not tools_list:
return []
response_tools: list[Any] = []
for tool in tools:
for tool in tools_list:
if isinstance(tool, FunctionTool):
params = tool.parameters()
params["additionalProperties"] = False
@@ -1193,6 +1193,52 @@ def test_prepare_tools_for_openai_with_mcp() -> None:
assert "require_approval" in mcp
def test_prepare_tools_for_openai_single_function_tool() -> None:
"""Test that a single FunctionTool (not wrapped in a list) is handled correctly."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
@tool
def hello(name: str) -> str:
"""Say hello."""
return name
resp_tools = client._prepare_tools_for_openai(hello)
assert isinstance(resp_tools, list)
assert len(resp_tools) == 1
tool_def = resp_tools[0]
assert tool_def["type"] == "function"
assert tool_def["name"] == "hello"
assert tool_def["strict"] is False
assert "parameters" in tool_def
params = tool_def["parameters"]
assert isinstance(params, dict)
assert params.get("type") == "object"
assert "properties" in params
assert "name" in params["properties"]
assert params["properties"]["name"]["type"] == "string"
def test_prepare_tools_for_openai_single_dict_tool() -> None:
"""Test that a single dict tool (not wrapped in a list) is handled correctly."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
web_tool = OpenAIResponsesClient.get_web_search_tool(search_context_size="low")
resp_tools = client._prepare_tools_for_openai(web_tool)
assert isinstance(resp_tools, list)
assert len(resp_tools) == 1
assert "type" in resp_tools[0]
assert resp_tools[0]["search_context_size"] == "low"
def test_prepare_tools_for_openai_none() -> None:
"""Test that passing None returns an empty list."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
resp_tools = client._prepare_tools_for_openai(None)
assert isinstance(resp_tools, list)
assert len(resp_tools) == 0
def test_parse_response_from_openai_with_mcp_approval_request() -> None:
"""Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")