A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 6, 19:15 UTC
[Bugfix] Fix KeyError in parse_response_input for reasoning items with optional content (#34499)
Signed-off-by: jeonsworld <jeonsworld@gmail.com>
Eunkwang Jeon committed
Mar 13, 2026 at 01:13 UTC
bdc23434543762c8ffc71a103dc7770a038a9724
3 files changed
+101
-6
tests/entrypoints/openai/parser/test_harmony_utils.py
+87
@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
14
parse_chat_output,
15
)
16
from vllm.entrypoints.openai.responses.harmony import (
17
+ response_input_to_harmony,
18
response_previous_input_to_harmony,
19
)
20
@@ -841,3 +842,89 @@ class TestGetSystemMessage:
842
assert channel in valid_channels, (
843
f"{channel} missing when with_custom_tools={with_tools}"
844
)
845
+
846
+
847
+class TestResponseInputToHarmonyReasoningItem:
848
+ """Tests for response_input_to_harmony handling of reasoning input items.
849
+
850
+ Per the OpenAI spec, ResponseReasoningItem.content is
851
+ Optional[List[Content]] = None. Clients like langchain-openai may omit
852
+ this field when constructing multi-turn input from previous responses.
853
+
854
+ Reasoning items with content are converted to Harmony messages on the
855
+ 'analysis' channel. All content items are concatenated. Items without
856
+ content return None (skipped by the caller).
857
+ """
858
+
859
+ def test_reasoning_with_single_content(self):
860
+ """Test reasoning item with a single content entry."""
861
+ item = {
862
+ "type": "reasoning",
863
+ "id": "rs_123",
864
+ "content": [{"type": "reasoning_text", "text": "Thinking step by step"}],
865
+ }
866
+
867
+ msg = response_input_to_harmony(item, prev_responses=[])
868
+
869
+ assert msg is not None
870
+ assert msg.author.role == Role.ASSISTANT
871
+ assert msg.content[0].text == "Thinking step by step"
872
+ assert msg.channel == "analysis"
873
+
874
+ def test_reasoning_with_multiple_content_items(self):
875
+ """Test reasoning item with multiple content entries concatenated."""
876
+ item = {
877
+ "type": "reasoning",
878
+ "id": "rs_123",
879
+ "content": [
880
+ {"type": "reasoning_text", "text": "First, let me analyze"},
881
+ {"type": "reasoning_text", "text": "Second, I should consider"},
882
+ {"type": "reasoning_text", "text": "Finally, the answer is"},
883
+ ],
884
+ }
885
+
886
+ msg = response_input_to_harmony(item, prev_responses=[])
887
+
888
+ assert msg is not None
889
+ assert msg.author.role == Role.ASSISTANT
890
+ assert msg.content[0].text == (
891
+ "First, let me analyze\nSecond, I should consider\nFinally, the answer is"
892
+ )
893
+ assert msg.channel == "analysis"
894
+
895
+ def test_reasoning_without_content_returns_none(self):
896
+ """Test reasoning item without content field returns None."""
897
+ item = {
898
+ "type": "reasoning",
899
+ "id": "rs_123",
900
+ "summary": [{"type": "summary_text", "text": "Thinking about math"}],
901
+ }
902
+
903
+ msg = response_input_to_harmony(item, prev_responses=[])
904
+
905
+ assert msg is None
906
+
907
+ def test_reasoning_with_none_content_returns_none(self):
908
+ """Test reasoning item with content=None returns None."""
909
+ item = {
910
+ "type": "reasoning",
911
+ "id": "rs_123",
912
+ "content": None,
913
+ "summary": [{"type": "summary_text", "text": "Thinking about math"}],
914
+ }
915
+
916
+ msg = response_input_to_harmony(item, prev_responses=[])
917
+
918
+ assert msg is None
919
+
920
+ def test_reasoning_with_empty_content_returns_none(self):
921
+ """Test reasoning item with empty content list returns None."""
922
+ item = {
923
+ "type": "reasoning",
924
+ "id": "rs_123",
925
+ "content": [],
926
+ }
927
+
928
+ msg = response_input_to_harmony(item, prev_responses=[])
929
+
930
+ assert msg is None
vllm/entrypoints/openai/responses/harmony.py
+13
-5
@@ -138,8 +138,12 @@ def _parse_chat_format_message(chat_msg: dict) -> list[Message]:
138
def response_input_to_harmony(
139
response_msg: ResponseInputOutputItem,
140
prev_responses: list[ResponseOutputItem | ResponseReasoningItem],
141
-) -> Message:
142
- """Convert a single ResponseInputOutputItem into a Harmony Message."""
141
+) -> Message | None:
142
+ """Convert a single ResponseInputOutputItem into a Harmony Message.
143
+
144
+ Returns None for reasoning items with empty or absent content so
145
+ the caller can skip them.
146
+ """
147
if not isinstance(response_msg, dict):
148
response_msg = response_msg.model_dump()
149
if "type" not in response_msg or response_msg["type"] == "message":
@@ -172,9 +176,13 @@ def response_input_to_harmony(
176
response_msg["output"],
177
)
178
elif response_msg["type"] == "reasoning":
175
- content = response_msg["content"]
176
- assert len(content) == 1
177
- msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"])
179
+ content = response_msg.get("content")
180
+ if content and len(content) >= 1:
181
+ reasoning_text = "\n".join(item["text"] for item in content)
182
+ msg = Message.from_role_and_content(Role.ASSISTANT, reasoning_text)
183
+ msg = msg.with_channel("analysis")
184
+ else:
185
+ return None
186
elif response_msg["type"] == "function_call":
187
msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"])
188
msg = msg.with_channel("commentary")
vllm/entrypoints/openai/responses/serving.py
+1
-1
@@ -1086,7 +1086,7 @@ class OpenAIServingResponses(OpenAIServing):
1086
prev_outputs = []
1087
for response_msg in request.input:
1088
new_msg = response_input_to_harmony(response_msg, prev_outputs)
1089
- if new_msg.author.role != "system":
1089
+ if new_msg is not None and new_msg.author.role != "system":
1090
messages.append(new_msg)
1091
1092
# User passes in a tool call request and its output. We need