From e1ac41272ac1773317ef02a91c9d1f3922dfab88 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 28 May 2026 18:11:40 +0800 Subject: [PATCH] fix: preserve reasoning with hosted MCP calls --- .../agent_framework_openai/_chat_client.py | 12 ++++++- .../tests/openai/test_openai_chat_client.py | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 261554fba3..98a1732e3c 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1491,10 +1491,20 @@ def _prepare_message_for_openai( # (replays_local_storage) still need stripping when the request also carries a continuation # marker, since the server-stored items would otherwise duplicate the inline ones. Without # storage, standalone reasoning items are invalid per the API ("reasoning was provided - # without its required following item"), so the reasoning branch always drops. + # without its required following item"). Hosted MCP calls are the exception: if we keep + # the mcp_call item without storage, keep its paired reasoning item too. + has_hosted_mcp_call = any(content.type == "mcp_server_tool_call" for content in message.contents) for content in message.contents: match content.type: case "text_reasoning": + if not request_uses_service_side_storage and has_hosted_mcp_call: + reasoning = self._prepare_content_for_openai( + message.role, + content, + replays_local_storage=replays_local_storage, + ) + if reasoning: + all_messages.append(reasoning) continue case "function_result": if request_uses_service_side_storage: diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index e604742e7e..1710e77e64 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -5603,6 +5603,42 @@ def test_prepare_messages_for_openai_serializes_mcp_server_tool_call_as_mcp_call assert "output" not in item or item["output"] is None +def test_prepare_messages_for_openai_keeps_reasoning_with_mcp_call_without_storage() -> None: + """Hosted MCP calls returned by reasoning models need their paired reasoning item.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[ + Content.from_text_reasoning( + id="rs_abc123", + text="I need to query the hosted MCP server.", + additional_properties={"status": "completed"}, + ), + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + ] + + storage_on = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=True) + assert "reasoning" not in [item.get("type") for item in storage_on] + + storage_off = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False) + types = [item.get("type") for item in storage_off] + assert types == ["reasoning", "mcp_call"] + + reasoning = storage_off[0] + assert reasoning["id"] == "rs_abc123" + assert reasoning["status"] == "completed" + assert reasoning["summary"] == [{"type": "summary_text", "text": "I need to query the hosted MCP server."}] + + def test_prepare_messages_for_openai_coalesces_mcp_call_and_result_into_single_item() -> None: """An mcp_server_tool_call followed by an mcp_server_tool_result with the same call_id (in same or separate Messages) must produce ONE mcp_call