@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / bdc2343454
Fork 0

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