mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
fix: drop hosted MCP calls when reasoning is stripped (#6210)
This commit is contained in:
committed by
GitHub
Unverified
parent
4268080c20
commit
bc0e65d716
@@ -1454,10 +1454,21 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
Returns:
|
||||
The prepared chat messages for a request.
|
||||
"""
|
||||
drops_reasoning_without_storage = not request_uses_service_side_storage and any(
|
||||
content.type == "text_reasoning" for message in chat_messages for content in message.contents
|
||||
)
|
||||
drop_mcp_call_ids: set[str] = set()
|
||||
if drops_reasoning_without_storage:
|
||||
for message in chat_messages:
|
||||
for content in message.contents:
|
||||
if content.type == "mcp_server_tool_call" and content.call_id:
|
||||
drop_mcp_call_ids.add(content.call_id)
|
||||
|
||||
list_of_list = [
|
||||
self._prepare_message_for_openai(
|
||||
message,
|
||||
request_uses_service_side_storage=request_uses_service_side_storage,
|
||||
drop_mcp_call_ids=drop_mcp_call_ids,
|
||||
)
|
||||
for message in chat_messages
|
||||
]
|
||||
@@ -1472,6 +1483,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
message: Message,
|
||||
*,
|
||||
request_uses_service_side_storage: bool = True,
|
||||
drop_mcp_call_ids: set[str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Prepare a chat message for the OpenAI Responses API format."""
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
@@ -1491,7 +1503,10 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
# (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 the reasoning branch always drops. When that
|
||||
# happens, `_prepare_messages_for_openai` also drops the paired hosted-MCP IDs across
|
||||
# message boundaries rather than replaying bare MCP items.
|
||||
drop_mcp_call_ids = drop_mcp_call_ids or set()
|
||||
for content in message.contents:
|
||||
match content.type:
|
||||
case "text_reasoning":
|
||||
@@ -1546,7 +1561,10 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
# 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 content.call_id in drop_mcp_call_ids:
|
||||
continue
|
||||
prepared_mcp = self._prepare_content_for_openai(
|
||||
message.role,
|
||||
|
||||
@@ -5648,6 +5648,79 @@ 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_mcp_call_across_reasoning_messages() -> None:
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
messages = [
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_text_reasoning(id="rs_abc123", text="Need a tool call.")],
|
||||
),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user