diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index fa140ee0b7..5ba0bbc686 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -43,6 +43,8 @@ FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, + ToolTypes, + normalize_tools, ) from .._types import ( Annotation, @@ -425,21 +427,24 @@ def _get_conversation_id( # 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 diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index 12e5b42d6d..7eaae1e776 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -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")