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
13 changes: 9 additions & 4 deletions python/packages/core/agent_framework/openai/_responses_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
ToolTypes,
normalize_tools,
)
from .._types import (
Annotation,
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions python/packages/core/tests/openai/test_openai_responses_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down