Repository navigation
[Rust Frontend] Drop incomplete tool-call markup at the end of output like Python - #92
Closed
richard-for-bugen[bot] wants to merge 2 commits into
Closed
richard-for-bugen[bot] wants to merge 2 commits into
richard-for-bugen[bot] wants to merge 2 commits into
Conversation
BugenZhao
approved these changes
Nov 3, 2026
… like Python When generation stops inside a tool call (max_tokens, a stop string, or EOS before the closer), every tool parser's finish() failed with a generic parsing error, and the unified output stage re-emitted the parser's buffer as assistant text. Clients got the truncated markup (for example "\n<function") as message content. Python's parser engine drops it and keeps only the text before the call, in streaming and non-streaming (vllm-project#47562). Tool and unified parsers now report this case with a typed IncompleteToolCall error, and the chat output stage drops the buffer on that error only. A parse failure of complete markup still falls back to text. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
…ream test The stateless Responses API (vllm-project#63902) pinned the trailing `}\n</tool_call` text the tool parser leaked at the end of output as an extra message item. With incomplete tool-call markup dropped at the end of output, the stream ends after the function call item. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
richard-for-bugen
Bot
force-pushed
the
richard/parity-fix-drop-incomplete-call-markup
branch
from
November 3, 2026 10:44
9b5e1a2 to
c19039f
Compare
Author
|
Rebased onto |
Author
|
Promoted as vllm-project#65375. |
Author
|
Merged upstream as vllm-project#65375; closing. |
richard-for-bugen
Bot
deleted the
richard/parity-fix-drop-incomplete-call-markup
branch
November 4, 2026 19:06
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.
Motivation
Python. When generation stops inside a tool call (
max_tokens, a stop string, or EOS before the closer), the parser engine drops the unfinished tool-call markup and keeps the text before it, in both streaming and non-streaming (vllm-project#47562,DelegatingParserreturningtool_call_info.content; pinned bytests/parser/engine/test_parser_engine.py::TestTruncatedToolOpenerStreamParity).Rust today. Every tool parser's
finish()fails with the genericParsingFailed("incomplete … tool call"), andUnifiedParserState::finish(rust/src/chat/src/output/default/unified.rs) treats every finish error alike: it callsparser.reset()and re-emits the buffer as assistant text (added by vllm-project#47289 so a truncated-only output would not come back empty). Clients get the truncated markup ascontent. For Python's own test input"Checking the weather. <tool_call>\n<function"withqwen3_coder, Rust returnscontent: "Checking the weather. \n<function"; Python returns"Checking the weather. ".This PR.
ToolParserError::IncompleteToolCall { parser }andUnifiedParserError::IncompleteToolCall { parser }replace the generic error at every "output ended inside a call" site: the JSON family (Hermes, Qwen3 JSON, Mistral, Phi-4-mini, InternLM2 through the shared parser; Llama JSON; Granite 4), DeepSeek V3/V3.1 JSON, DeepSeek DSML (V3.2/V4/V4.1), GLM-4.5/4.7, Kimi K2, Qwen Coder (and Seed-OSS, Step-5, MiMo through it), MiniMax M2/M3, HY, and the unified Gemma 4, Inkling, Kimi K3 and Muse parsers.UnifiedParserError::is_incomplete_tool_callcovers both, including a tool parser's error carried throughCombinedParser.UnifiedParserState::finishdrops the parser buffer on that error only. Events already emitted stand, so text before the call is kept. Any other finish error (malformed complete markup, for example Llama'sinvalid Llama JSON) still falls back to text as before.hermes).Ownership: the parser owns recognizing that the output ended inside a call (it alone knows its mode), so it reports a typed error; what to show the client is output policy, which
rust/AGENTS.mdassigns tovllm-chat, so the drop sits inUnifiedParserState::finish, the one consumer ofUnifiedParser::finish. Streaming and non-streaming agree by construction: non-streaming assembles the same event stream.Target check. Intent: vllm-project#47562 is a deliberate fix with regression tests ("must drop the incomplete markup, matching the streaming path"). Consistency: Python drops in both stream modes for its parser-engine families; legacy families still return raw content in non-streaming, which this PR does not copy (dropping everywhere was the decision). Contract:
contentis the assistant's text; half a tool-call opener is neither text nor a call, and a client cannot act on it.Overlaps vllm-project#62290 (DSML at
max_tokensonly; first-time contributor), which this covers for every family. Not a duplicate of vllm-project#52886, which recovers malformed Kimi K3 calls at outer boundaries mid-stream; the two touch neighboring lines inkimi_k3.rsfinish.Not covered:
hfand Beam template parsers: a tool-call region still open at EOF is closed and parsed by the template's content rules, and whether that is truncation or a legitimate EOS close depends on the template. A follow-up check on 2026-11-01 found this needs no change: a parse failure drops the body inclose_current, so the markup never reaches the text. A call whose header was already streamed stays, which is the accepted divergence recorded for truncated calls.ToolCallStart, partial arguments) before the cut stays in the response with truncated arguments, as streaming clients already saw it. Python's non-streaming path drops such a call; retracting a streamed call is a separate question.Validation
vllm-parser: 624 passed (existing*_finish_fails_incomplete_tool_callsnapshots now show the typed error's message).vllm-chat: 494 passed. Newunified_stream_drops_incomplete_tool_parser_call_at_eos(Qwen Coder throughCombinedParser, Python's input, in four deltas and in one) andunified_stream_drops_incomplete_gemma4_tool_call_at_eos(a unified parser, with and without preceding text, so an output that is only a truncated call ends with no content; replacesunified_stream_recovers_incomplete_gemma4_tool_call_at_eos, which pinned the old fallback). Red-green: without the new match arm both fail, with"Checking the weather. \n<function"and the full Gemma 4 markup as text.unified_stream_finish_error_recovers_buffered_textstill pins the fallback for other finish errors.cargo fmt --check,cargo sort --check,cargo deny check bans, workspaceclippy --all-targets --all-features -D warnings, workspacenextest --all-features) on 9b5e1a2: all green, nextest 2430 passed, 13 skipped.Codex review
Approve with nits (
gpt-6.1-sol, high). It independently rebuiltvllm-parserandvllm-chat(1118 passed), reproduced red-green onbugen/main, and ran Python's sevenTestTruncatedToolOpenerStreamParitycases. No layering or correctness findings.🤖 Generated with Claude Code
https://claude.ai/code/session_01CJckC9t9pN9Pi3BkC8tpAv