[Bugfix][Responses] Use standard reasoning content-part events (#59652)

Signed-off-by: ai-jz <ai-jz@users.noreply.github.com> Co-authored-by: ai-jz <ai-jz@users.noreply.github.com> Co-authored-by: Flora Feng <4florafeng@gmail.com>

ai-jz committed Oct 1, 2026 at 12:20 UTC 2a52715ef7ca533a0ce287e14dc1c0702bdc2e8d
6 files changed +59 -85
tests/entrypoints/openai/responses/conftest.py
+2 -6
@@ -37,7 +37,6 @@ def pairs_of_event_types() -> dict[str, str]:
37 "response.content_part.done": "response.content_part.added",
38 "response.output_text.done": "response.output_text.delta",
39 "response.reasoning_text.done": "response.reasoning_text.delta",
40 - "response.reasoning_part.done": "response.reasoning_part.added",
40 "response.mcp_call_arguments.done": "response.mcp_call_arguments.delta",
41 "response.mcp_call.completed": "response.mcp_call.in_progress",
42 "response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa: E501
@@ -250,11 +249,8 @@ def _validate_field_consistency(events: list) -> None:
249 active_content_index = None
250 continue
251
253 - # --- content_part / reasoning_part added: sets content_index
254 - if etype in (
255 - "response.content_part.added",
256 - "response.reasoning_part.added",
257 - ):
252 + # --- content_part added: sets content_index
253 + if etype == "response.content_part.added":
254 _assert_item_fields(event, etype, active_item_id, active_output_index)
255 content_index = getattr(event, "content_index", None)
256 if active_content_index is None:
tests/entrypoints/openai/responses/test_function_call.py
+4 -5
@@ -340,12 +340,12 @@ async def test_function_calling_with_streaming_types(
340 # response.created
341 # -> response.in_progress
342 # -> response.output_item.added
343 - # -> response.reasoning_part.added
343 + # -> response.content_part.added
344 # -> response.reasoning_text.delta
345 # ....
346 # -> response.reasoning_text.delta
347 # -> response.reasoning_text.done
348 - # -> response.reasoning_part.done
348 + # -> response.content_part.done
349 # -> response.output_item.done
350 # -> response.output_item.added
351 # -> response.content_part.added
@@ -368,12 +368,12 @@ async def test_function_calling_with_streaming_types(
368 # response.created
369 # -> response.in_progress
370 # -> response.output_item.added
371 - # -> response.reasoning_part.added
371 + # -> response.content_part.added
372 # -> response.reasoning_text.delta
373 # ....
374 # -> response.reasoning_text.delta
375 # -> response.reasoning_text.done
376 - # -> response.reasoning_part.done
376 + # -> response.content_part.done
377 # -> response.output_item.done
378 # -> response.output_item.added
379 # -> response.content_part.added
@@ -424,7 +424,6 @@ async def test_function_calling_with_streaming_types(
424 "response.output_text.done": "response.output_text.delta",
425 "response.content_part.done": "response.content_part.added",
426 "response.reasoning_text.done": "response.reasoning_text.delta",
427 - "response.reasoning_part.done": "response.reasoning_part.added",
427 "response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa
428 }
429
tests/entrypoints/openai/responses/test_harmony.py
+1 -4
@@ -467,10 +467,7 @@ async def test_streaming(client: OpenAI, model_name: str, background: bool):
467 assert event.item_id == current_item_id
468
469 # Verify content indices
470 - if event.type in [
471 - "response.content_part.added",
472 - "response.reasoning_part.added",
473 - ]:
470 + if event.type == "response.content_part.added":
471 assert event.content_index == current_content_index + 1
472 current_content_index = event.content_index
473 elif event.type in [
tests/entrypoints/openai/responses/test_streaming_events.py
+40 -1
@@ -1,8 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from openai.types.responses import ResponseFunctionWebSearch
4 +import pytest
5 +from openai.types.responses import ResponseFunctionWebSearch, ResponseStreamEvent
6 from openai_harmony import Message, Role
7 +from pydantic import TypeAdapter
8
9 from vllm.entrypoints.generate.base.protocol import (
10 DeltaFunctionCall,
@@ -14,6 +16,8 @@ from vllm.entrypoints.openai.responses.streaming_events import (
16 StreamingState,
17 _StateType,
18 emit_browser_tool_events,
19 + emit_reasoning_delta_events,
20 + emit_reasoning_done_events,
21 split_delta,
22 )
23
@@ -40,6 +44,41 @@ def test_browser_find_uses_responses_action_type():
44 assert event.item.action.url == "cursor:42"
45
46
47 +@pytest.mark.parametrize("harmony", [False, True], ids=["simple", "harmony"])
48 +def test_reasoning_content_parts_use_sdk_events(harmony):
49 + text = "Let me think."
50 + if harmony:
51 + state = StreamingState()
52 + events = emit_reasoning_delta_events(text, state)
53 + events.extend(emit_reasoning_done_events(text, state))
54 + else:
55 + processor = SimpleStreamingEventProcessor()
56 + events = _run_through_processor(processor, DeltaMessage(reasoning=text))
57 + events.extend(processor.close_current())
58 +
59 + assert [event.type for event in events] == [
60 + "response.output_item.added",
61 + "response.content_part.added",
62 + "response.reasoning_text.delta",
63 + "response.reasoning_text.done",
64 + "response.content_part.done",
65 + "response.output_item.done",
66 + ]
67 + added, part_added, delta, text_done, part_done, done = events
68 + assert part_added.part.type == part_done.part.type == "reasoning_text"
69 + assert part_added.part.text == ""
70 + assert delta.delta == text_done.text == part_done.part.text == text
71 + assert done.item.content[0].text == text
72 + assert done.item.id == added.item.id
73 + for event in events[1:-1]:
74 + assert event.item_id == added.item.id
75 + assert event.content_index == part_added.content_index
76 + validator = TypeAdapter(ResponseStreamEvent)
77 + for event in events:
78 + assert event.output_index == added.output_index
79 + validator.validate_python(event.model_dump())
80 +
81 +
82 def _make_tool_call(
83 index: int, name: str | None = None, arguments: str | None = None
84 ) -> DeltaToolCall:
vllm/entrypoints/openai/responses/protocol.py
-49
@@ -42,9 +42,6 @@ from openai.types.responses import (
42 ResponseInProgressEvent as OpenAIResponseInProgressEvent,
43 )
44 from openai.types.responses.response import IncompleteDetails, ToolChoice
45 -from openai.types.responses.response_reasoning_item import (
46 - Content as ResponseReasoningTextContent,
47 -)
45 from openai.types.responses.tool import Tool
46 from openai.types.shared import Metadata, Reasoning
47 from openai_harmony import Message as OpenAIHarmonyMessage
@@ -845,50 +842,6 @@ class ResponsesResponse(OpenAIBaseModel):
842 )
843
844
848 -# TODO: this code can be removed once
849 -# https://github.com/openai/openai-python/issues/2634 has been resolved
850 -class ResponseReasoningPartDoneEvent(OpenAIBaseModel):
851 - content_index: int
852 - """The index of the content part that is done."""
853 -
854 - item_id: str
855 - """The ID of the output item that the content part was added to."""
856 -
857 - output_index: int
858 - """The index of the output item that the content part was added to."""
859 -
860 - part: ResponseReasoningTextContent
861 - """The content part that is done."""
862 -
863 - sequence_number: int
864 - """The sequence number of this event."""
865 -
866 - type: Literal["response.reasoning_part.done"]
867 - """The type of the event. Always `response.reasoning_part.done`."""
868 -
869 -
870 -# TODO: this code can be removed once
871 -# https://github.com/openai/openai-python/issues/2634 has been resolved
872 -class ResponseReasoningPartAddedEvent(OpenAIBaseModel):
873 - content_index: int
874 - """The index of the content part that is done."""
875 -
876 - item_id: str
877 - """The ID of the output item that the content part was added to."""
878 -
879 - output_index: int
880 - """The index of the output item that the content part was added to."""
881 -
882 - part: ResponseReasoningTextContent
883 - """The content part that is done."""
884 -
885 - sequence_number: int
886 - """The sequence number of this event."""
887 -
888 - type: Literal["response.reasoning_part.added"]
889 - """The type of the event. Always `response.reasoning_part.added`."""
890 -
891 -
845 # vLLM Streaming Events
846 # Note: we override the response type with the vLLM ResponsesResponse type
847 class ResponseCompletedEvent(OpenAIResponseCompletedEvent):
@@ -913,8 +866,6 @@ StreamingResponsesResponse: TypeAlias = (
866 | ResponseContentPartDoneEvent
867 | ResponseReasoningTextDeltaEvent
868 | ResponseReasoningTextDoneEvent
916 - | ResponseReasoningPartAddedEvent
917 - | ResponseReasoningPartDoneEvent
869 | ResponseCodeInterpreterCallInProgressEvent
870 | ResponseCodeInterpreterCallCodeDeltaEvent
871 | ResponseWebSearchCallInProgressEvent
vllm/entrypoints/openai/responses/streaming_events.py
+12 -20
@@ -70,8 +70,6 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
70 is_function_recipient,
71 )
72 from vllm.entrypoints.openai.responses.protocol import (
73 - ResponseReasoningPartAddedEvent,
74 - ResponseReasoningPartDoneEvent,
73 StreamingResponsesResponse,
74 )
75 from vllm.entrypoints.openai.responses.utils import (
@@ -220,16 +218,13 @@ def emit_reasoning_delta_events(
218 )
219 state.current_content_index += 1
220 events.append(
223 - ResponseReasoningPartAddedEvent(
224 - type="response.reasoning_part.added",
221 + ResponseContentPartAddedEvent(
222 + type="response.content_part.added",
223 sequence_number=-1,
224 output_index=state.current_output_index,
225 item_id=state.current_item_id,
226 content_index=state.current_content_index,
229 - part=ResponseReasoningTextContent(
230 - text="",
231 - type="reasoning_text",
232 - ),
227 + part={"text": "", "type": "reasoning_text"},
228 )
229 )
230 events.append(
@@ -457,13 +452,13 @@ def emit_reasoning_done_events(
452 )
453 )
454 events.append(
460 - ResponseReasoningPartDoneEvent(
461 - type="response.reasoning_part.done",
455 + ResponseContentPartDoneEvent(
456 + type="response.content_part.done",
457 sequence_number=-1,
458 item_id=state.current_item_id,
459 output_index=state.current_output_index,
460 content_index=state.current_content_index,
466 - part=content,
461 + part=content.model_dump(),
462 )
463 )
464 events.append(
@@ -969,16 +964,13 @@ def emit_simple_reasoning_open(
964 status="in_progress",
965 ),
966 ),
972 - ResponseReasoningPartAddedEvent(
973 - type="response.reasoning_part.added",
967 + ResponseContentPartAddedEvent(
968 + type="response.content_part.added",
969 sequence_number=-1,
970 output_index=state.output_index,
971 item_id=state.current_item_id,
972 content_index=state.content_index,
978 - part=ResponseReasoningTextContent(
979 - text="",
980 - type="reasoning_text",
981 - ),
973 + part={"text": "", "type": "reasoning_text"},
974 ),
975 ]
976
@@ -1016,13 +1008,13 @@ def emit_simple_reasoning_done(
1008 content_index=state.content_index,
1009 text=state.accumulated_text,
1010 ),
1019 - ResponseReasoningPartDoneEvent(
1020 - type="response.reasoning_part.done",
1011 + ResponseContentPartDoneEvent(
1012 + type="response.content_part.done",
1013 sequence_number=-1,
1014 item_id=state.current_item_id,
1015 output_index=state.output_index,
1016 content_index=state.content_index,
1025 - part=part,
1017 + part=part.model_dump(),
1018 ),
1019 ResponseOutputItemDoneEvent(
1020 type="response.output_item.done",