[Bugfix] Responses API assistant EasyInputMessageParam input (#44361)
Signed-off-by: Yifan Zong <yzong@redhat.com> Co-authored-by: Ben Browning <bbrownin@redhat.com>
yzong-rh committed
Jun 23, 2026 at 08:54 UTC
2a675a7b9fce6577e8eab937f67c7c1973a54f7f
4 files changed
+93
-13
tests/entrypoints/openai/responses/test_function_call_parsing.py
+50
-1
@@ -5,7 +5,7 @@
5
import json
6
7
import pytest
8
-from openai.types.responses import ResponseFunctionToolCall
8
+from openai.types.responses import ResponseFunctionToolCall, ResponseOutputMessage
9
10
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
11
@@ -328,3 +328,52 @@ def test_validator_handles_empty_iterator():
328
329
request = ResponsesRequest(**mock_data)
330
assert request.input == []
331
+
332
+
333
+def test_assistant_string_content_stays_easyinput():
334
+ """EasyInput assistant message with plain string content is not
335
+ coerced into a ResponseOutputMessage."""
336
+ request_data = {
337
+ "model": "test-model",
338
+ "input": [
339
+ {"type": "message", "role": "assistant", "content": "hello"},
340
+ ],
341
+ }
342
+
343
+ request = ResponsesRequest(**request_data)
344
+
345
+ item = request.input[0]
346
+ assert isinstance(item, dict), (
347
+ "String-content assistant message should remain a dict (EasyInput), "
348
+ f"got {type(item)}"
349
+ )
350
+ assert item.get("content") == "hello"
351
+ assert "id" not in item
352
+ assert "status" not in item
353
+
354
+
355
+def test_assistant_output_style_content_coerced():
356
+ """Assistant message whose content is output-message-shaped (list of
357
+ output_text items) should be coerced to ResponseOutputMessage."""
358
+ request_data = {
359
+ "model": "test-model",
360
+ "input": [
361
+ {
362
+ "type": "message",
363
+ "role": "assistant",
364
+ "content": [{"type": "output_text", "text": "world"}],
365
+ },
366
+ ],
367
+ }
368
+
369
+ request = ResponsesRequest(**request_data)
370
+
371
+ item = request.input[0]
372
+ assert isinstance(item, ResponseOutputMessage), (
373
+ "Output-style assistant message should be coerced to "
374
+ f"ResponseOutputMessage, got {type(item)}"
375
+ )
376
+ assert item.content[0].text == "world"
377
+ assert item.content[0].annotations == []
378
+ assert item.status == "completed"
379
+ assert item.id.startswith("msg_")
tests/entrypoints/openai/responses/test_responses_utils.py
+10
@@ -782,6 +782,16 @@ class TestConstructChatMessagesCombinePolicy:
782
["call_123", "call_456"],
783
id="reasoning-output-tool-call",
784
),
785
+ pytest.param(
786
+ [
787
+ make_reasoning_item(content_text="Let me think"),
788
+ {"type": "message", "role": "assistant", "content": "Hello"},
789
+ ],
790
+ "Hello",
791
+ "Let me think",
792
+ None,
793
+ id="reasoning-easyinput-assistant",
794
+ ),
795
],
796
)
797
def test_assistant_side_items_merge_until_tool_output(
vllm/entrypoints/openai/responses/protocol.py
+19
-12
@@ -525,23 +525,30 @@ class ResponsesRequest(OpenAIBaseModel):
525
processed_input.append(item)
526
527
elif item_type == "message" and item.get("role") == "assistant":
528
+ content = item.get("content")
529
+ if not isinstance(content, list):
530
+ # String content is a valid EasyInputMessageParam,
531
+ # do not coerce it to ResponseOutputMessage
532
+ processed_input.append(item)
533
+ continue
534
+
535
+ original_item = item
536
item = dict(item)
537
if "id" not in item:
538
item["id"] = f"msg_{random_uuid()}"
539
if "status" not in item:
540
item["status"] = "completed"
541
# ResponseOutputText requires annotations
534
- if isinstance(item.get("content"), list):
535
- new_content = []
536
- for c in item["content"]:
537
- if (
538
- isinstance(c, dict)
539
- and c.get("type") == "output_text"
540
- and "annotations" not in c
541
- ):
542
- c = {**c, "annotations": []}
543
- new_content.append(c)
544
- item["content"] = new_content
542
+ new_content = []
543
+ for c in content:
544
+ if (
545
+ isinstance(c, dict)
546
+ and c.get("type") == "output_text"
547
+ and "annotations" not in c
548
+ ):
549
+ c = {**c, "annotations": []}
550
+ new_content.append(c)
551
+ item["content"] = new_content
552
try:
553
processed_input.append(ResponseOutputMessage(**item))
554
except ValidationError:
@@ -549,7 +556,7 @@ class ResponsesRequest(OpenAIBaseModel):
556
"Failed to parse assistant message to ResponseOutputMessage, "
557
"leaving for Pydantic validation"
558
)
552
- processed_input.append(item)
559
+ processed_input.append(original_item)
560
561
else:
562
processed_input.append(item)
vllm/entrypoints/openai/responses/utils.py
+14
@@ -306,6 +306,20 @@ def _construct_message_from_response_item(
306
content=item.get("output"),
307
tool_call_id=item.get("call_id"),
308
)
309
+ elif isinstance(item, dict) and item.get("role") == "assistant":
310
+ content = item.get("content")
311
+ text: str | None = None
312
+ if isinstance(content, str):
313
+ text = content
314
+ elif isinstance(content, list) and content:
315
+ text = content[0].get("text")
316
+ if text is not None:
317
+ if prev_assistant_msg:
318
+ previous_content = prev_assistant_msg.get("content")
319
+ if previous_content is None:
320
+ prev_assistant_msg["content"] = text
321
+ return None
322
+ return {"role": "assistant", "content": text}
323
return item # type: ignore[arg-type]
324
325