diff --git a/tests/entrypoints/openai/responses/test_harmony_utils.py b/tests/entrypoints/openai/responses/test_harmony_utils.py index e51538298ff9..5f46b7b710a9 100644 --- a/tests/entrypoints/openai/responses/test_harmony_utils.py +++ b/tests/entrypoints/openai/responses/test_harmony_utils.py @@ -13,6 +13,7 @@ from vllm.entrypoints.openai.responses.harmony import ( harmony_to_response_output, parser_state_to_response_output, + response_input_to_harmony, response_previous_input_to_harmony, ) @@ -461,3 +462,83 @@ def test_parser_state_to_response_output_analysis_channel() -> None: assert len(builtin_items) == 1 assert not isinstance(builtin_items[0], McpCall) assert builtin_items[0].type == "reasoning" + + +class TestResponseInputToHarmonyReasoning: + """Tests for response_input_to_harmony with reasoning type inputs.""" + + def test_reasoning_with_content(self): + """Test reasoning item with populated content field.""" + reasoning_msg = ResponseReasoningItem( + id="rs_001", + type="reasoning", + summary=[], + content=[{"type": "reasoning_content", "text": "thinking step"}], + encrypted_content=None, + ) + + msg = response_input_to_harmony(reasoning_msg, []) + + assert msg.role == Role.ASSISTANT + assert msg.content[0].text == "thinking step" + + def test_reasoning_with_content_none_and_summary(self): + """Test reasoning item with content=None falls back to summary.""" + reasoning_msg = ResponseReasoningItem( + id="rs_002", + type="reasoning", + summary=[{"type": "summary_text", "text": "We need to check the file"}], + content=None, + encrypted_content=None, + ) + + msg = response_input_to_harmony(reasoning_msg, []) + + assert msg.role == Role.ASSISTANT + assert msg.content[0].text == "We need to check the file" + + def test_reasoning_with_content_none_and_multiple_summaries(self): + """Test reasoning item with content=None and multiple summary entries.""" + reasoning_msg = ResponseReasoningItem( + id="rs_003", + type="reasoning", + summary=[ + {"type": "summary_text", "text": "First thought."}, + {"type": "summary_text", "text": "Second thought."}, + ], + content=None, + encrypted_content=None, + ) + + msg = response_input_to_harmony(reasoning_msg, []) + + assert msg.role == Role.ASSISTANT + assert msg.content[0].text == "First thought. Second thought." + + def test_reasoning_with_content_none_and_empty_summary(self): + """Test reasoning item with content=None and empty summary list.""" + reasoning_msg = ResponseReasoningItem( + id="rs_004", + type="reasoning", + summary=[], + content=None, + encrypted_content=None, + ) + + msg = response_input_to_harmony(reasoning_msg, []) + + assert msg.role == Role.ASSISTANT + assert msg.content[0].text == "" + + def test_reasoning_with_content_none_and_no_summary(self): + """Test reasoning item with both content and summary absent.""" + reasoning_msg = { + "id": "rs_005", + "type": "reasoning", + "content": None, + } + + msg = response_input_to_harmony(reasoning_msg, []) + + assert msg.role == Role.ASSISTANT + assert msg.content[0].text == "" diff --git a/vllm/entrypoints/openai/responses/harmony.py b/vllm/entrypoints/openai/responses/harmony.py index 460f310926ad..e2d703b8075e 100644 --- a/vllm/entrypoints/openai/responses/harmony.py +++ b/vllm/entrypoints/openai/responses/harmony.py @@ -172,9 +172,19 @@ def response_input_to_harmony( response_msg["output"], ) elif response_msg["type"] == "reasoning": - content = response_msg["content"] - assert len(content) == 1 - msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"]) + content = response_msg.get("content") + if content is not None: + assert len(content) == 1 + text = content[0]["text"] + else: + # content can be None when encrypted_content is used or + # when only summary is available. Fall back to summary. + summary = response_msg.get("summary") + if summary: + text = " ".join(s.get("text", "") for s in summary) + else: + text = "" + msg = Message.from_role_and_content(Role.ASSISTANT, text) elif response_msg["type"] == "function_call": msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"]) msg = msg.with_channel("commentary")