From b20225ebafcda17ba0075876d88eefdf740b47e1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 30 May 2026 23:22:27 +0800 Subject: [PATCH] fix: drop hosted MCP calls when reasoning is stripped --- .../agent_framework_openai/_chat_client.py | 8 ++++- .../tests/openai/test_openai_chat_client.py | 35 +++++++++++++++++++ 2 files changed, 42 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..ed5872af21 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1492,6 +1492,9 @@ def _prepare_message_for_openai( # 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. + drops_reasoning_without_storage = not request_uses_service_side_storage and any( + content.type == "text_reasoning" for content in message.contents + ) for content in message.contents: match content.type: case "text_reasoning": @@ -1546,7 +1549,10 @@ def _prepare_message_for_openai( # server-side `id`, so under continuation it would duplicate # the prior response's items (#3295). Drop the call here; the # orphan result is dropped by the coalesce step that follows. - if request_uses_service_side_storage: + # + # Without storage, a reasoning + hosted-MCP pair cannot be replayed + # partially: reasoning is stripped above, and a bare mcp_call is rejected. + if request_uses_service_side_storage or drops_reasoning_without_storage: continue prepared_mcp = self._prepare_content_for_openai( message.role, 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..2d036f42bc 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -5648,6 +5648,41 @@ 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_drops_mcp_call_when_paired_reasoning_is_stripped() -> 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 server."), + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 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 "reasoning" not in types + assert "mcp_call" not in types + assert "function_call_output" not in types + + 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