Repository navigation
[Bugfix] Handle empty or refusal content when reconstructing Responses input messages - #49130
Conversation
|
Friendly ping — let me know if there's anything I should update here. |
|
This pull request has merge conflicts that must be resolved before it can be |
|
Thanks for this. One adjacent problem in the same function that this PR could fix with little extra work:
Joining the text parts instead of picking output_text = "".join(
part.text for part in item.content if part.type == "output_text"
)(and Happy to send this as a follow-up instead if you'd rather keep this PR narrow. |
a7b52a9 to
bc1ab66
Compare
|
Rebased on the latest main (resolved a trivial add/add in the test file — the new |
|
@TimurRakhmatullin86 Yes, please fold it in — joining all text parts instead of |
bc1ab66 to
3634c43
Compare
|
Folded it in, thanks @chudinovAI — it now joins all |
…s input messages _construct_message_from_response_item unconditionally accessed content[0].text for both ResponseOutputMessage and ResponseReasoningItem, crashing with an IndexError when content was empty and silently truncating multi-part content. The same content[0] truncation affected the assistant easy-input dict path. Fix by joining all text parts: - ResponseOutputMessage: join output_text parts, fall back to refusal text for refusal-only messages, empty string for truly empty content. - ResponseReasoningItem: join all reasoning parts with newlines, matching the convention in responses/harmony.py. - Easy-input assistant dicts: join text from all content parts. Add tests covering empty content, refusal-only, multi-part, and easy-input dict multi-part cases.
|
@chudinovAI thanks for the suggestion — folded it in. The rebased version now joins all text parts in all three paths:
Added a test for the easy-input dict multi-part case as well. Rebase onto current main resolves the merge conflicts. |
3634c43 to
10d156f
Compare
Purpose
The Responses API returns an unhandled HTTP 500 when a client replays a prior assistant
turn whose content is a refusal or is empty.
_construct_message_from_response_item(vllm/entrypoints/openai/responses/utils.py) readsitem.content[0].textunguarded for aResponseOutputMessage:Two ordinary inputs break this:
contentisList[Content]with no minimum length, so an assistant message withcontent=[]raisesIndexError.ResponseOutputRefusal(fieldsrefusal/type, notext),so replaying a refusal turn raises
AttributeError.The request-level
input_item_parsingvalidator coerces assistant dicts with list contentinto typed
ResponseOutputMessages, so both reach this branch and are not caught by theguarded dict-fallback path. Its sibling reasoning branch already guards (
elif item.content and len(item.content) >= 1); this branch now does too.Empty content →
""; a refusal block → its refusal text; a normal text block → unchanged.Test Plan
Added
TestOutputMessageContenttotests/entrypoints/openai/responses/test_responses_utils.py:test_empty_content_yields_empty_stringtest_refusal_content_uses_refusal_textTest Result
Both new tests pass with the fix and fail against the current code (
IndexError/AttributeErrorrespectively). Verified the exact old vs. new expressions against the realopenai(2.46.0) types: for a text block both yield the same string; for a refusal block theold code raises
AttributeErrorwhile the new returns the refusal text; for empty content theold code raises
IndexErrorwhile the new returns"".ruff checkandruff format --checkpass on both files. (
test_responses_utils.pyis a CPU-only module — no GPU required.)