Repository navigation
[Rust Frontend] Accept refusal content parts in assistant messages - #65621
Merged
BugenZhao merged 2 commits intoNov 5, 2026
Merged
Conversation
OpenAI's chat API allows `refusal` parts in assistant message content (`ChatCompletionContentPartRefusalParam`), and the Python frontend replays them as text. The Rust frontend rejected them with a deserialization error. Lower them onto assistant text blocks, include them in the `echo` text like Python, and reject them in other roles with OpenAI's message. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Isotr0py
approved these changes
Nov 5, 2026
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #100214 for commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
OpenAI's chat API allows a
refusalcontent part in assistant messages (ChatCompletionContentPartRefusalParam; the assistantcontentarray istext | refusal), so a client that replays a model's refusal as structured history sends it. The Python frontend accepts it and replays the refusal string as text. The Rust frontend rejects the whole request at deserialization (unknown variant 'refusal'), becauseContentPartonly knowstext, the three media URLs andinput_audio.This PR adds
ContentPart::Refusaland lowers it where the role is known.Claims
AssistantContentBlock::Text, like a text part, so every renderer sees it as the turn's text (the same choice the Rust Responses conversion already makes forrefusalinput parts).echoincludes it, as Python's echo of the parsed last message does.Validation
Python
vllm launch rendervs Rustvllm-rs renderwith this commit, both onQwen/Qwen3-0.6B(HF renderer),POST /v1/chat/completions/render(measured on the same commit over main39dbf51839):[{"type":"refusal","refusal":"I can't."}]: identical token IDs on both, and identical to the same turn sent as"content": "I can't.".[{"type":"refusal",...}]: Python renders it as text; Rust returns the 400 above (intended, per OpenAI; see Details).New tests in
vllm-serverroutes::openai::chat_completions::convert, both failing onmain(the request does not deserialize):prepare_chat_request_replays_assistant_refusal_parts_as_text: a JSON request whose assistant message is one refusal part (OpenAI's documented shape), withecho; asserts one assistant text block and the echo text.prepare_chat_request_rejects_refusal_parts_outside_assistant_messages: a user refusal part returns OpenAI's message.Commands from
.buildkite/scripts/run-rust-frontend-cargo-ci.sh, on this branch (mainc58e7f6d05+ this commit):Model evaluation: none run. The change only affects requests that were previously rejected, and the token-ID comparison above checks the rendered prompt directly.
Details
Layering. The role-dependent rule lives in the server's DTO lowering (
convert_assistant_text_blocks/convert_contentinchat_completions/convert.rs), the only place that knows the role;vllm-chatalready has the text block it maps onto.Scope and intentional deviations from Python. Python also accepts
input_text,output_text,input_image,thinkingand bare-string parts on chat completions, but only because its chat parser is shared with the Responses path (MM_PARSER_MAPinvllm/renderers/chat_utils/chat_message.py). A live probe of the OpenAI API (2026-11-03, one request per case) settles which are contract:/v1/chat/completionsrefusalin an assistant messagerefusalin a user messageinput_text,output_text(any role)input_imageSo only the assistant
refusalpart is implemented, and arefusalpart outside assistant messages follows OpenAI rather than Python. Clients with Responses-shaped content can use the Rust/v1/responsesroute (#63902), which accepts all of these. Python's Mistralthinkingpart (#21333) is out of scope here.Harmony. Python's Harmony path (gpt-oss) drops an assistant refusal part (
harmony_utils.py,TODO: Support refusal); Rust's Harmony renderer keeps it as text like every other renderer.Known difference not changed here. An assistant message with several text-like parts is joined with
"\n"by Python and concatenated by Rust's HF renderer ([text "Sorry.", refusal "I can't."]rendersSorry.\nI can't.vsSorry.I can't.). The same already holds for multiple text parts and is a separate fix.Not a duplicate. No open PR adds
refusalcontent parts to the Rust frontend. Open PRs mentioning refusal parts target Python paths: Harmony (#55479) and Responses input reconstruction (#49130, #48237). #56648 also extends Rust chat content types but addsimage_embedsinvllm-chat, not refusal parts.AI assistance (Claude Code) was used to prepare this change; I reviewed every changed line.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CJckCAkBsrWKV6peRwSdih