Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json

import pytest
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses import ResponseFunctionToolCall, ResponseOutputMessage

from vllm.entrypoints.openai.responses.protocol import ResponsesRequest

Expand Down Expand Up @@ -328,3 +328,52 @@ def test_validator_handles_empty_iterator():

request = ResponsesRequest(**mock_data)
assert request.input == []


def test_assistant_string_content_stays_easyinput():
"""EasyInput assistant message with plain string content is not
coerced into a ResponseOutputMessage."""
request_data = {
"model": "test-model",
"input": [
{"type": "message", "role": "assistant", "content": "hello"},
],
}

request = ResponsesRequest(**request_data)

item = request.input[0]
assert isinstance(item, dict), (
"String-content assistant message should remain a dict (EasyInput), "
f"got {type(item)}"
)
assert item.get("content") == "hello"
assert "id" not in item
assert "status" not in item


def test_assistant_output_style_content_coerced():
"""Assistant message whose content is output-message-shaped (list of
output_text items) should be coerced to ResponseOutputMessage."""
request_data = {
"model": "test-model",
"input": [
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "world"}],
},
],
}

request = ResponsesRequest(**request_data)

item = request.input[0]
assert isinstance(item, ResponseOutputMessage), (
"Output-style assistant message should be coerced to "
f"ResponseOutputMessage, got {type(item)}"
)
assert item.content[0].text == "world"
assert item.content[0].annotations == []
assert item.status == "completed"
assert item.id.startswith("msg_")
10 changes: 10 additions & 0 deletions tests/entrypoints/openai/responses/test_responses_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,16 @@ class TestConstructChatMessagesCombinePolicy:
["call_123", "call_456"],
id="reasoning-output-tool-call",
),
pytest.param(
[
make_reasoning_item(content_text="Let me think"),
{"type": "message", "role": "assistant", "content": "Hello"},
],
"Hello",
"Let me think",
None,
id="reasoning-easyinput-assistant",
),
],
)
def test_assistant_side_items_merge_until_tool_output(
Expand Down
31 changes: 19 additions & 12 deletions vllm/entrypoints/openai/responses/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,31 +525,38 @@ def input_item_parsing(cls, data):
processed_input.append(item)

elif item_type == "message" and item.get("role") == "assistant":
content = item.get("content")
if not isinstance(content, list):
# String content is a valid EasyInputMessageParam,
# do not coerce it to ResponseOutputMessage
processed_input.append(item)
continue

original_item = item
item = dict(item)
if "id" not in item:
item["id"] = f"msg_{random_uuid()}"
if "status" not in item:
item["status"] = "completed"
# ResponseOutputText requires annotations
if isinstance(item.get("content"), list):
new_content = []
for c in item["content"]:
if (
isinstance(c, dict)
and c.get("type") == "output_text"
and "annotations" not in c
):
c = {**c, "annotations": []}
new_content.append(c)
item["content"] = new_content
new_content = []
for c in content:
if (
isinstance(c, dict)
and c.get("type") == "output_text"
and "annotations" not in c
):
c = {**c, "annotations": []}
new_content.append(c)
item["content"] = new_content
try:
processed_input.append(ResponseOutputMessage(**item))
except ValidationError:
logger.debug(
"Failed to parse assistant message to ResponseOutputMessage, "
"leaving for Pydantic validation"
)
processed_input.append(item)
processed_input.append(original_item)

else:
processed_input.append(item)
Expand Down
14 changes: 14 additions & 0 deletions vllm/entrypoints/openai/responses/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ def _construct_message_from_response_item(
content=item.get("output"),
tool_call_id=item.get("call_id"),
)
elif isinstance(item, dict) and item.get("role") == "assistant":
Comment thread
bbrowning marked this conversation as resolved.
Resolved
content = item.get("content")
text: str | None = None
if isinstance(content, str):
text = content
elif isinstance(content, list) and content:
text = content[0].get("text")
if text is not None:
if prev_assistant_msg:
previous_content = prev_assistant_msg.get("content")
if previous_content is None:
prev_assistant_msg["content"] = text
return None
return {"role": "assistant", "content": text}
return item # type: ignore[arg-type]


Expand Down
Loading