Skip to content

[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
mainfrom
richard/parity-fix-drop-incomplete-call-markup
Closed

richard-for-bugen[bot] wants to merge 2 commits into
mainfrom
richard/parity-fix-drop-incomplete-call-markup

Conversation

@richard-for-bugen

@richard-for-bugen richard-for-bugen Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

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, DelegatingParser returning tool_call_info.content; pinned by tests/parser/engine/test_parser_engine.py::TestTruncatedToolOpenerStreamParity).

Rust today. Every tool parser's finish() fails with the generic ParsingFailed("incomplete … tool call"), and UnifiedParserState::finish (rust/src/chat/src/output/default/unified.rs) treats every finish error alike: it calls parser.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 as content. For Python's own test input "Checking the weather. <tool_call>\n<function" with qwen3_coder, Rust returns content: "Checking the weather. \n<function"; Python returns "Checking the weather. ".

This PR.

  • ToolParserError::IncompleteToolCall { parser } and UnifiedParserError::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_call covers both, including a tool parser's error carried through CombinedParser.
  • UnifiedParserState::finish drops 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's invalid Llama JSON) still falls back to text as before.
  • Applies to every family and every finish reason, per the decision on this item; Python drops only for its parser-engine families and keeps raw content for legacy ones (e.g. 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.md assigns to vllm-chat, so the drop sits in UnifiedParserState::finish, the one consumer of UnifiedParser::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: content is 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_tokens only; 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 in kimi_k3.rs finish.

Not covered:

  • The hf and 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 in close_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.
  • A call whose header already streamed (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_call snapshots now show the typed error's message).
  • vllm-chat: 494 passed. New unified_stream_drops_incomplete_tool_parser_call_at_eos (Qwen Coder through CombinedParser, Python's input, in four deltas and in one) and unified_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; replaces unified_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_text still pins the fallback for other finish errors.
  • CI command set (cargo fmt --check, cargo sort --check, cargo deny check bans, workspace clippy --all-targets --all-features -D warnings, workspace nextest --all-features) on 9b5e1a2: all green, nextest 2430 passed, 13 skipped.

Codex review

Approve with nits (gpt-6.1-sol, high). It independently rebuilt vllm-parser and vllm-chat (1118 passed), reproduced red-green on bugen/main, and ran Python's seven TestTruncatedToolOpenerStreamParity cases. No layering or correctness findings.

  • P3, test coverage: the replaced Gemma 4 test covered an output that is only an incomplete call, and the new tests both had preceding text. Fixed: the Gemma 4 test now runs with an empty and a non-empty prefix (red-green checked for the empty one).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CJckC9t9pN9Pi3BkC8tpAv

richard-for-bugen Bot and others added 2 commits November 3, 2026 10:44
… 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
richard-for-bugen Bot force-pushed the richard/parity-fix-drop-incomplete-call-markup branch from 9b5e1a2 to c19039f Compare November 3, 2026 10:44
@richard-for-bugen

Copy link
Copy Markdown
Author

Rebased onto main eb409c9aa2 for promotion. The stateless Responses API (vllm-project#63902, merged today) added responses_streaming_emits_function_call_events, which pinned the leak this PR fixes: the trailing }\n</tool_call text as an extra message item. c19039f updates that expectation (two output items, the leaked item's six events gone); no production change. All 52 responses tests pass; workspace nextest 2511 passed.

@richard-for-bugen

Copy link
Copy Markdown
Author

Promoted as vllm-project#65375.

@richard-for-bugen

Copy link
Copy Markdown
Author

Merged upstream as vllm-project#65375; closing.

@richard-for-bugen richard-for-bugen Bot closed this Nov 4, 2026
@richard-for-bugen
richard-for-bugen Bot deleted the richard/parity-fix-drop-incomplete-call-markup branch November 4, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant