From 33377bc11e2cf3ddb38d9c2f4515c2f0f8e452d3 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 27 May 2026 20:33:05 +0800 Subject: [PATCH] fix: keep reasoning with hosted MCP calls --- .../agent_framework_openai/_chat_client.py | 14 ++- .../tests/openai/test_openai_chat_client.py | 96 +++++++++++++++++++ 2 files changed, 109 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 8257678584..f09e0b5ab8 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1484,10 +1484,22 @@ 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"), so only keep reasoning inline when it is paired + # with a hosted MCP call in the same assistant message. + has_hosted_mcp_call = any( + item.type == "mcp_server_tool_call" and getattr(item, "call_id", None) for item 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: + prepared_reasoning = self._prepare_content_for_openai( + message.role, + content, + replays_local_storage=replays_local_storage, + ) + if prepared_reasoning: + all_messages.append(prepared_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 31c3c26fe0..cb6e62f5a0 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -5518,6 +5518,63 @@ 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_hosted_mcp_call_storage_off() -> None: + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[ + Content.from_text_reasoning( + id="rs_abc123", + text="Checking the hosted MCP tool", + additional_properties={"status": "completed"}, + ), + Content.from_mcp_server_tool_call( + call_id="mcp_def456", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + ] + + result = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False) + + types = [item.get("type") for item in result if isinstance(item, dict)] + assert types == ["reasoning", "mcp_call"] + assert result[0]["id"] == "rs_abc123" + assert result[1]["id"] == "mcp_def456" + + +def test_prepare_messages_for_openai_drops_reasoning_and_hosted_mcp_call_with_storage() -> None: + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[ + Content.from_text_reasoning( + id="rs_abc123", + text="Checking the hosted MCP tool", + additional_properties={"status": "completed"}, + ), + Content.from_mcp_server_tool_call( + call_id="mcp_def456", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + ] + + result = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=True) + + assert result == [] + + 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 @@ -5563,6 +5620,45 @@ def test_prepare_messages_for_openai_coalesces_mcp_call_and_result_into_single_i assert fco_items == [], f"unexpected orphan function_call_output items: {fco_items}" +def test_prepare_messages_for_openai_keeps_reasoning_with_coalesced_hosted_mcp_result() -> None: + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[ + Content.from_text_reasoning( + id="rs_abc123", + text="Need the MCP result", + additional_properties={"status": "completed"}, + ), + Content.from_mcp_server_tool_call( + call_id="mcp_def456", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_def456", + output=[Content.from_text(text="found 10 cats")], + ) + ], + ), + ] + + result = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False) + + assert [item.get("type") for item in result if isinstance(item, dict)] == ["reasoning", "mcp_call"] + assert result[0]["id"] == "rs_abc123" + assert result[1]["id"] == "mcp_def456" + assert result[1]["output"] == "found 10 cats" + + def test_prepare_messages_for_openai_drops_orphan_mcp_server_tool_result() -> None: """When an mcp_server_tool_result has no matching mcp_server_tool_call in the message list, it must be dropped, NOT serialized as a