Repository navigation
Conversation
Long-context degeneration makes DeepSeek V4/V4.1 emit an orphan structural closer (a bare </|DSML|parameter>, </|DSML|invoke> or </|DSML|tool_calls>) while no tool call is open. The engine's no-transition fallback in StreamingParserEngine._on_terminal echoed that terminal verbatim into the client-visible TEXT chunk, so a response could end with a stray '</|DSML|parameter>'. Add a declarative absorb_terminals set to ParserEngineConfig and drop those terminals when they reach a plain content state with no transition. The absorb only applies in the tool-owning pass (not skip_tool_parsing), so the reasoning pass keeps passing the markup through for the downstream tool adapter to parse. Verified against the DeepSeek V4.1 serving engine (chunked and non-streaming): the orphan closer no longer reaches content, and the delegating reasoning + tool adapter path still parses every parameter. Signed-off-by: Eran Sandler <eran@sandler.co.il>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
Purpose
Reported on a DeepSeek-V4.1-Flash deployment: responses intermittently end with a stray closing tag (for example
</|DSML|parameter>) appended to the completed answer. It reproduces only under long-context / degenerate generation, which is exactly when the model emits orphan DSML fragments.Root cause is in the engine's no-transition fallback:
StreamingParserEngine._on_terminalcalls_emit_for_state(value, ...)whenever(state, terminal)has no transition. InCONTENT/REASONINGthat echoes the raw terminal text as aTEXT_CHUNK/REASONING_CHUNK.deepseek_v4_configdefines transitions only for theTOOL_*states, so an orphan structural closer that arrives in a plain content state is emitted verbatim into the client-visible content.Fix
absorb_terminalsset toParserEngineConfig._on_terminal, drop a declared terminal that reaches a plain content state (CONTENT/REASONING) with no transition.not self.skip_tool_parsing, and declarePARAM_CLOSE,INVOKE_END,TOOL_ENDindeepseek_v4_config.The
skip_tool_parsingguard is essential. In the delegating architecture the reasoning pass runs withskip_tool_parsing=Trueand must pass tool markup through verbatim so the downstream tool adapter can parse it. Absorbing there starves the tool parser.Why not state-machine transitions
The obvious alternative — adding
(CONTENT, "PARAM_CLOSE")-style transitions - is unsafe here. Those transitions also fire in the reasoning pass, which then drops the closers from the reasoning-stripped content handed to the tool adapter. On the real engine that reproduces astest_registered_adapters_parse_complete_outputlosing every parameter after the first ({"city": ...}instead of{"city": ..., "count": ...}) andTestDelegatingParserLargeDeltafailing. The engine-level rule with the skip-pass guard keeps the reasoning pass untouched.Only the tool-owning pass absorbs; reasoning-channel orphans keep their existing behaviour.
Tests
tests/parser/engine/test_deepseek_v4.py::TestOrphanStructuralCloserAbsorption- non-streaming and streaming, all three closers, plus a normal-tool-call regression.tests/parser/engine/test_deepseek_v41.py::test_orphan_structural_closer_is_absorbed- spaced V4.1 dialect, thinking on and off.Verification
Ran both test modules against a real DeepSeek-V4.1 serving engine (vLLM dev build,
--tool-call-parser deepseek_v41 --reasoning-parser deepseek_v41):Related
#51914, #52645 and #49117 cover missing or malformed DSML start wrappers / orphan-invoke recovery. This change is complementary and addresses the orphan closer leak into content, which those do not.