[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",