[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>
This commit is contained in:
ai-jz
2026-10-01 19:20:57 +00:00
committed by GitHub
co-authored by ai-jz Flora Feng
parent d848c4ed3d
commit 2a52715ef7
6 changed files with 59 additions and 85 deletions
@@ -37,7 +37,6 @@ def pairs_of_event_types() -> dict[str, str]:
"response.content_part.done": "response.content_part.added",
"response.output_text.done": "response.output_text.delta",
"response.reasoning_text.done": "response.reasoning_text.delta",
"response.reasoning_part.done": "response.reasoning_part.added",
"response.mcp_call_arguments.done": "response.mcp_call_arguments.delta",
"response.mcp_call.completed": "response.mcp_call.in_progress",
"response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa: E501
@@ -250,11 +249,8 @@ def _validate_field_consistency(events: list) -> None:
active_content_index = None
continue
# --- content_part / reasoning_part added: sets content_index
if etype in (
"response.content_part.added",
"response.reasoning_part.added",
):
# --- content_part added: sets content_index
if etype == "response.content_part.added":
_assert_item_fields(event, etype, active_item_id, active_output_index)
content_index = getattr(event, "content_index", None)
if active_content_index is None:
@@ -340,12 +340,12 @@ async def test_function_calling_with_streaming_types(
# response.created
# -> response.in_progress
# -> response.output_item.added
# -> response.reasoning_part.added
# -> response.content_part.added
# -> response.reasoning_text.delta
# ....
# -> response.reasoning_text.delta
# -> response.reasoning_text.done
# -> response.reasoning_part.done
# -> response.content_part.done
# -> response.output_item.done
# -> response.output_item.added
# -> response.content_part.added
@@ -368,12 +368,12 @@ async def test_function_calling_with_streaming_types(
# response.created
# -> response.in_progress
# -> response.output_item.added
# -> response.reasoning_part.added
# -> response.content_part.added
# -> response.reasoning_text.delta
# ....
# -> response.reasoning_text.delta
# -> response.reasoning_text.done
# -> response.reasoning_part.done
# -> response.content_part.done
# -> response.output_item.done
# -> response.output_item.added
# -> response.content_part.added
@@ -424,7 +424,6 @@ async def test_function_calling_with_streaming_types(
"response.output_text.done": "response.output_text.delta",
"response.content_part.done": "response.content_part.added",
"response.reasoning_text.done": "response.reasoning_text.delta",
"response.reasoning_part.done": "response.reasoning_part.added",
"response.function_call_arguments.done": "response.function_call_arguments.delta", # noqa
}
@@ -467,10 +467,7 @@ async def test_streaming(client: OpenAI, model_name: str, background: bool):
assert event.item_id == current_item_id
# Verify content indices
if event.type in [
"response.content_part.added",
"response.reasoning_part.added",
]:
if event.type == "response.content_part.added":
assert event.content_index == current_content_index + 1
current_content_index = event.content_index
elif event.type in [
@@ -1,8 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from openai.types.responses import ResponseFunctionWebSearch
import pytest
from openai.types.responses import ResponseFunctionWebSearch, ResponseStreamEvent
from openai_harmony import Message, Role
from pydantic import TypeAdapter
from vllm.entrypoints.generate.base.protocol import (
DeltaFunctionCall,
@@ -14,6 +16,8 @@ from vllm.entrypoints.openai.responses.streaming_events import (
StreamingState,
_StateType,
emit_browser_tool_events,
emit_reasoning_delta_events,
emit_reasoning_done_events,
split_delta,
)
@@ -40,6 +44,41 @@ def test_browser_find_uses_responses_action_type():
assert event.item.action.url == "cursor:42"
@pytest.mark.parametrize("harmony", [False, True], ids=["simple", "harmony"])
def test_reasoning_content_parts_use_sdk_events(harmony):
text = "Let me think."
if harmony:
state = StreamingState()
events = emit_reasoning_delta_events(text, state)
events.extend(emit_reasoning_done_events(text, state))
else:
processor = SimpleStreamingEventProcessor()
events = _run_through_processor(processor, DeltaMessage(reasoning=text))
events.extend(processor.close_current())
assert [event.type for event in events] == [
"response.output_item.added",
"response.content_part.added",
"response.reasoning_text.delta",
"response.reasoning_text.done",
"response.content_part.done",
"response.output_item.done",
]
added, part_added, delta, text_done, part_done, done = events
assert part_added.part.type == part_done.part.type == "reasoning_text"
assert part_added.part.text == ""
assert delta.delta == text_done.text == part_done.part.text == text
assert done.item.content[0].text == text
assert done.item.id == added.item.id
for event in events[1:-1]:
assert event.item_id == added.item.id
assert event.content_index == part_added.content_index
validator = TypeAdapter(ResponseStreamEvent)
for event in events:
assert event.output_index == added.output_index
validator.validate_python(event.model_dump())
def _make_tool_call(
index: int, name: str | None = None, arguments: str | None = None
) -> DeltaToolCall:
@@ -42,9 +42,6 @@ from openai.types.responses import (
ResponseInProgressEvent as OpenAIResponseInProgressEvent,
)
from openai.types.responses.response import IncompleteDetails, ToolChoice
from openai.types.responses.response_reasoning_item import (
Content as ResponseReasoningTextContent,
)
from openai.types.responses.tool import Tool
from openai.types.shared import Metadata, Reasoning
from openai_harmony import Message as OpenAIHarmonyMessage
@@ -845,50 +842,6 @@ class ResponsesResponse(OpenAIBaseModel):
)
# TODO: this code can be removed once
# https://github.com/openai/openai-python/issues/2634 has been resolved
class ResponseReasoningPartDoneEvent(OpenAIBaseModel):
content_index: int
"""The index of the content part that is done."""
item_id: str
"""The ID of the output item that the content part was added to."""
output_index: int
"""The index of the output item that the content part was added to."""
part: ResponseReasoningTextContent
"""The content part that is done."""
sequence_number: int
"""The sequence number of this event."""
type: Literal["response.reasoning_part.done"]
"""The type of the event. Always `response.reasoning_part.done`."""
# TODO: this code can be removed once
# https://github.com/openai/openai-python/issues/2634 has been resolved
class ResponseReasoningPartAddedEvent(OpenAIBaseModel):
content_index: int
"""The index of the content part that is done."""
item_id: str
"""The ID of the output item that the content part was added to."""
output_index: int
"""The index of the output item that the content part was added to."""
part: ResponseReasoningTextContent
"""The content part that is done."""
sequence_number: int
"""The sequence number of this event."""
type: Literal["response.reasoning_part.added"]
"""The type of the event. Always `response.reasoning_part.added`."""
# vLLM Streaming Events
# Note: we override the response type with the vLLM ResponsesResponse type
class ResponseCompletedEvent(OpenAIResponseCompletedEvent):
@@ -913,8 +866,6 @@ StreamingResponsesResponse: TypeAlias = (
| ResponseContentPartDoneEvent
| ResponseReasoningTextDeltaEvent
| ResponseReasoningTextDoneEvent
| ResponseReasoningPartAddedEvent
| ResponseReasoningPartDoneEvent
| ResponseCodeInterpreterCallInProgressEvent
| ResponseCodeInterpreterCallCodeDeltaEvent
| ResponseWebSearchCallInProgressEvent
@@ -70,8 +70,6 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
is_function_recipient,
)
from vllm.entrypoints.openai.responses.protocol import (
ResponseReasoningPartAddedEvent,
ResponseReasoningPartDoneEvent,
StreamingResponsesResponse,
)
from vllm.entrypoints.openai.responses.utils import (
@@ -220,16 +218,13 @@ def emit_reasoning_delta_events(
)
state.current_content_index += 1
events.append(
ResponseReasoningPartAddedEvent(
type="response.reasoning_part.added",
ResponseContentPartAddedEvent(
type="response.content_part.added",
sequence_number=-1,
output_index=state.current_output_index,
item_id=state.current_item_id,
content_index=state.current_content_index,
part=ResponseReasoningTextContent(
text="",
type="reasoning_text",
),
part={"text": "", "type": "reasoning_text"},
)
)
events.append(
@@ -457,13 +452,13 @@ def emit_reasoning_done_events(
)
)
events.append(
ResponseReasoningPartDoneEvent(
type="response.reasoning_part.done",
ResponseContentPartDoneEvent(
type="response.content_part.done",
sequence_number=-1,
item_id=state.current_item_id,
output_index=state.current_output_index,
content_index=state.current_content_index,
part=content,
part=content.model_dump(),
)
)
events.append(
@@ -969,16 +964,13 @@ def emit_simple_reasoning_open(
status="in_progress",
),
),
ResponseReasoningPartAddedEvent(
type="response.reasoning_part.added",
ResponseContentPartAddedEvent(
type="response.content_part.added",
sequence_number=-1,
output_index=state.output_index,
item_id=state.current_item_id,
content_index=state.content_index,
part=ResponseReasoningTextContent(
text="",
type="reasoning_text",
),
part={"text": "", "type": "reasoning_text"},
),
]
@@ -1016,13 +1008,13 @@ def emit_simple_reasoning_done(
content_index=state.content_index,
text=state.accumulated_text,
),
ResponseReasoningPartDoneEvent(
type="response.reasoning_part.done",
ResponseContentPartDoneEvent(
type="response.content_part.done",
sequence_number=-1,
item_id=state.current_item_id,
output_index=state.output_index,
content_index=state.content_index,
part=part,
part=part.model_dump(),
),
ResponseOutputItemDoneEvent(
type="response.output_item.done",