Unverified Commit bdc23434 authored by Eunkwang Jeon's avatar Eunkwang Jeon Committed by GitHub
Browse files

[Bugfix] Fix KeyError in parse_response_input for reasoning items with optional content (#34499)


Signed-off-by: default avatarjeonsworld <jeonsworld@gmail.com>
parent f444c05c
...@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.parser.harmony_utils import ( ...@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
parse_chat_output, parse_chat_output,
) )
from vllm.entrypoints.openai.responses.harmony import ( from vllm.entrypoints.openai.responses.harmony import (
response_input_to_harmony,
response_previous_input_to_harmony, response_previous_input_to_harmony,
) )
...@@ -841,3 +842,89 @@ class TestGetSystemMessage: ...@@ -841,3 +842,89 @@ class TestGetSystemMessage:
assert channel in valid_channels, ( assert channel in valid_channels, (
f"{channel} missing when with_custom_tools={with_tools}" f"{channel} missing when with_custom_tools={with_tools}"
) )
class TestResponseInputToHarmonyReasoningItem:
"""Tests for response_input_to_harmony handling of reasoning input items.
Per the OpenAI spec, ResponseReasoningItem.content is
Optional[List[Content]] = None. Clients like langchain-openai may omit
this field when constructing multi-turn input from previous responses.
Reasoning items with content are converted to Harmony messages on the
'analysis' channel. All content items are concatenated. Items without
content return None (skipped by the caller).
"""
def test_reasoning_with_single_content(self):
"""Test reasoning item with a single content entry."""
item = {
"type": "reasoning",
"id": "rs_123",
"content": [{"type": "reasoning_text", "text": "Thinking step by step"}],
}
msg = response_input_to_harmony(item, prev_responses=[])
assert msg is not None
assert msg.author.role == Role.ASSISTANT
assert msg.content[0].text == "Thinking step by step"
assert msg.channel == "analysis"
def test_reasoning_with_multiple_content_items(self):
"""Test reasoning item with multiple content entries concatenated."""
item = {
"type": "reasoning",
"id": "rs_123",
"content": [
{"type": "reasoning_text", "text": "First, let me analyze"},
{"type": "reasoning_text", "text": "Second, I should consider"},
{"type": "reasoning_text", "text": "Finally, the answer is"},
],
}
msg = response_input_to_harmony(item, prev_responses=[])
assert msg is not None
assert msg.author.role == Role.ASSISTANT
assert msg.content[0].text == (
"First, let me analyze\nSecond, I should consider\nFinally, the answer is"
)
assert msg.channel == "analysis"
def test_reasoning_without_content_returns_none(self):
"""Test reasoning item without content field returns None."""
item = {
"type": "reasoning",
"id": "rs_123",
"summary": [{"type": "summary_text", "text": "Thinking about math"}],
}
msg = response_input_to_harmony(item, prev_responses=[])
assert msg is None
def test_reasoning_with_none_content_returns_none(self):
"""Test reasoning item with content=None returns None."""
item = {
"type": "reasoning",
"id": "rs_123",
"content": None,
"summary": [{"type": "summary_text", "text": "Thinking about math"}],
}
msg = response_input_to_harmony(item, prev_responses=[])
assert msg is None
def test_reasoning_with_empty_content_returns_none(self):
"""Test reasoning item with empty content list returns None."""
item = {
"type": "reasoning",
"id": "rs_123",
"content": [],
}
msg = response_input_to_harmony(item, prev_responses=[])
assert msg is None
...@@ -138,8 +138,12 @@ def _parse_chat_format_message(chat_msg: dict) -> list[Message]: ...@@ -138,8 +138,12 @@ def _parse_chat_format_message(chat_msg: dict) -> list[Message]:
def response_input_to_harmony( def response_input_to_harmony(
response_msg: ResponseInputOutputItem, response_msg: ResponseInputOutputItem,
prev_responses: list[ResponseOutputItem | ResponseReasoningItem], prev_responses: list[ResponseOutputItem | ResponseReasoningItem],
) -> Message: ) -> Message | None:
"""Convert a single ResponseInputOutputItem into a Harmony Message.""" """Convert a single ResponseInputOutputItem into a Harmony Message.
Returns None for reasoning items with empty or absent content so
the caller can skip them.
"""
if not isinstance(response_msg, dict): if not isinstance(response_msg, dict):
response_msg = response_msg.model_dump() response_msg = response_msg.model_dump()
if "type" not in response_msg or response_msg["type"] == "message": if "type" not in response_msg or response_msg["type"] == "message":
...@@ -172,9 +176,13 @@ def response_input_to_harmony( ...@@ -172,9 +176,13 @@ def response_input_to_harmony(
response_msg["output"], response_msg["output"],
) )
elif response_msg["type"] == "reasoning": elif response_msg["type"] == "reasoning":
content = response_msg["content"] content = response_msg.get("content")
assert len(content) == 1 if content and len(content) >= 1:
msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"]) reasoning_text = "\n".join(item["text"] for item in content)
msg = Message.from_role_and_content(Role.ASSISTANT, reasoning_text)
msg = msg.with_channel("analysis")
else:
return None
elif response_msg["type"] == "function_call": elif response_msg["type"] == "function_call":
msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"]) msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"])
msg = msg.with_channel("commentary") msg = msg.with_channel("commentary")
......
...@@ -1086,7 +1086,7 @@ class OpenAIServingResponses(OpenAIServing): ...@@ -1086,7 +1086,7 @@ class OpenAIServingResponses(OpenAIServing):
prev_outputs = [] prev_outputs = []
for response_msg in request.input: for response_msg in request.input:
new_msg = response_input_to_harmony(response_msg, prev_outputs) new_msg = response_input_to_harmony(response_msg, prev_outputs)
if new_msg.author.role != "system": if new_msg is not None and new_msg.author.role != "system":
messages.append(new_msg) messages.append(new_msg)
# User passes in a tool call request and its output. We need # User passes in a tool call request and its output. We need
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment