From fe945f9d512c7d4c5cea96fa45b3557b71dd6e8b Mon Sep 17 00:00:00 2001 From: Marco Molinati Date: Tue, 1 Sep 2026 16:53:49 +0200 Subject: [PATCH] [Bugfix][Frontend] Flatten list-form function_call_output on Responses API `ResponsesRequest.input` validates items against the OpenAI SDK types, where `FunctionCallOutput.output` is `Union[str, ResponseFunctionCallOutputItemListParam]`, so a tool result may legitimately arrive as a list of content parts. The Harmony conversion passed that value straight into `Message.from_author_and_content`, which expects a string, and the request failed with: 1 validation error for Message content.0 Input should be a valid dictionary or instance of Content [type=model_type, input_value=[{'text': '...', 'type': 'input_text'}], input_type=list] The Chat Completions path already flattens list content for the tool role (`parser/harmony_utils.py`, added in #25593 for #25001). Apply the same `flatten_input_text_content` call to the Responses path so both surfaces accept either form. A string output is returned unchanged and an empty list becomes an empty string, matching the Chat Completions behaviour. Signed-off-by: Marco Molinati --- .../openai/responses/test_harmony_utils.py | 63 +++++++++++++++++++ vllm/entrypoints/openai/responses/harmony.py | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/entrypoints/openai/responses/test_harmony_utils.py b/tests/entrypoints/openai/responses/test_harmony_utils.py index bd4a46741d88..dd391fda27eb 100644 --- a/tests/entrypoints/openai/responses/test_harmony_utils.py +++ b/tests/entrypoints/openai/responses/test_harmony_utils.py @@ -14,6 +14,7 @@ from vllm.entrypoints.openai.responses.harmony import ( harmony_to_response_output, + response_input_to_harmony, response_previous_input_to_harmony, ) @@ -93,6 +94,68 @@ def test_tool_message_with_empty_content(self): assert messages[0].content[0].text == "" +class TestResponseInputToHarmony: + """Tests for the Responses API response_input_to_harmony function.""" + + @staticmethod + def _tool_call() -> ResponseFunctionToolCall: + return ResponseFunctionToolCall( + type="function_call", + call_id="call_1", + name="get_weather", + arguments='{"city": "Paris"}', + ) + + def test_function_call_output_with_string_output(self): + """Test parsing a function call output with string output.""" + response_msg = { + "type": "function_call_output", + "call_id": "call_1", + "output": "The weather in Paris is sunny, 18°C", + } + + msg = response_input_to_harmony(response_msg, [self._tool_call()]) + + assert msg.author.role == Role.TOOL + assert msg.author.name == "functions.get_weather" + assert msg.content[0].text == "The weather in Paris is sunny, 18°C" + + def test_function_call_output_with_list_output(self): + """Test parsing a function call output with a content part list. + + The Responses API accepts `output` as either a string or a list of + content parts, so the parts must be flattened into a single string + before the Harmony message is built. + """ + response_msg = { + "type": "function_call_output", + "call_id": "call_1", + "output": [ + {"type": "input_text", "text": "The weather in Paris "}, + {"type": "input_text", "text": "is sunny, 18°C"}, + ], + } + + msg = response_input_to_harmony(response_msg, [self._tool_call()]) + + assert msg.author.role == Role.TOOL + assert msg.author.name == "functions.get_weather" + assert msg.content[0].text == "The weather in Paris is sunny, 18°C" + + def test_function_call_output_with_empty_list_output(self): + """Test parsing a function call output with an empty content list.""" + response_msg = { + "type": "function_call_output", + "call_id": "call_1", + "output": [], + } + + msg = response_input_to_harmony(response_msg, [self._tool_call()]) + + assert msg.author.role == Role.TOOL + assert msg.content[0].text == "" + + class TestHarmonyToResponseOutput: """Tests for harmony_to_response_output function.""" diff --git a/vllm/entrypoints/openai/responses/harmony.py b/vllm/entrypoints/openai/responses/harmony.py index 60a9f65f4486..e2e4be3c0fc3 100644 --- a/vllm/entrypoints/openai/responses/harmony.py +++ b/vllm/entrypoints/openai/responses/harmony.py @@ -187,7 +187,7 @@ def response_input_to_harmony( raise ValueError(f"No call message found for {call_id}") msg = Message.from_author_and_content( Author.new(Role.TOOL, f"functions.{call_response.name}"), - response_msg["output"], + flatten_input_text_content(response_msg["output"]) or "", ) msg = msg.with_channel("commentary") msg = msg.with_recipient("assistant")