Python: Fix streaming path to emit mcp_server_tool_result on output_item.done instead of output_item.added (#4821)

* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Remove premature mcp_server_tool_result emission from the
response.output_item.added/mcp_call handler — at that point the MCP
server has not yet responded and output is always None.

Add a handler for response.mcp_call.completed that emits
mcp_server_tool_result with the actual tool output, matching the
non-streaming path behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Stop eagerly emitting mcp_server_tool_result on response.output_item.added
(when output is always None). Instead, handle response.output_item.done for
mcp_call items, which carries the full McpCall with populated output.

This matches the non-streaming path which guards with 'if item.output is not
None' before emitting the result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix test docstring to match actual implementation event name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback and raw_representation consistency (#4814)

- Add call_id fallback in response.output_item.done mcp_call handler to
  match the output_item.added handler pattern
- Use done_item instead of event for raw_representation to keep
  consistent with other output_item branches and non-streaming path
- Add test for call_id fallback when id attribute is missing
- Add raw_representation assertions to existing done handler tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback for non-streaming path and test coverage (#4814)

- Apply defensive call_id fallback (getattr with id/call_id/empty) to
  non-streaming mcp_call path for consistency with streaming path
- Add raw_representation assertion to call_id fallback test
- Add test for empty-string fallback when neither id nor call_id exist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-03-25 19:09:22 -07:00
committed by GitHub
Unverified
parent c1435ac201
commit dc27740f1a
2 changed files with 136 additions and 32 deletions
@@ -1728,7 +1728,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
)
)
case "mcp_call":
call_id = item.id
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
contents.append(
Content.from_mcp_server_tool_call(
call_id=call_id,
@@ -2118,27 +2118,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
raw_representation=event_item,
)
)
result_output = (
getattr(event_item, "result", None)
or getattr(event_item, "output", None)
or getattr(event_item, "outputs", None)
)
parsed_output: list[Content] | None = None
if result_output:
normalized = ( # pyright: ignore[reportUnknownVariableType]
result_output
if isinstance(result_output, Sequence)
and not isinstance(result_output, (str, bytes, MutableMapping))
else [result_output]
)
parsed_output = [Content.from_dict(output_item) for output_item in normalized] # pyright: ignore[reportArgumentType,reportUnknownVariableType]
contents.append(
Content.from_mcp_server_tool_result(
call_id=call_id,
output=parsed_output,
raw_representation=event_item,
)
)
# Result deferred to response.output_item.done
case "code_interpreter_call": # ResponseOutputCodeInterpreterCall
call_id = getattr(event_item, "call_id", None) or getattr(event_item, "id", None)
outputs: list[Content] = []
@@ -2408,6 +2388,21 @@ class RawOpenAIChatClient( # type: ignore[misc]
)
else:
logger.debug("Unparsed annotation type in streaming: %s", ann_type)
case "response.output_item.done":
done_item = event.item
if getattr(done_item, "type", None) == "mcp_call":
call_id = getattr(done_item, "id", None) or getattr(done_item, "call_id", None) or ""
output_text = getattr(done_item, "output", None)
parsed_output: list[Content] | None = (
[Content.from_text(text=output_text)] if isinstance(output_text, str) else None
)
contents.append(
Content.from_mcp_server_tool_result(
call_id=call_id,
output=parsed_output,
raw_representation=done_item,
)
)
case _:
logger.debug("Unparsed event of type: %s: %s", event.type, event)