[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:
co-authored by
ai-jz
Flora Feng
parent
d848c4ed3d
commit
2a52715ef7
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user