Repository navigation
fix(llm): strip reasoning before an orphan close tag from prefilled templates (#4926) - #4927
Merged
nicoloboschi merged 2 commits intoOct 5, 2026
Conversation
…emplates (vectorize-io#4926) Chat templates that prefill <think> into the generation prompt (Qwen3 thinking models, DeepSeek-R1-0528, Spark-X2.5) make the completion read "reasoning</think>answer" when the server runs without a reasoning parser. Neither the closed-block nor the unclosed-block rule matches that, so free-form output stored the chain of thought verbatim. Drop everything up to the first close tag when no open tag precedes it, unless the response already opened as JSON or a code fence, where the tag is a quoted literal. Signed-off-by: FenjuFu <[email protected]>
The docstring claimed the guard protects a response that "already opened as JSON or a code fence", but it only tests the start of the text before the close tag -- a prose preamble ahead of a fenced body defeats it, so a `</think>` literal in that answer is still read as a boundary. Widening the guard to any fence before the tag would be worse: it would stop stripping the common case where the reasoning itself drafts a fenced block. State the narrow check and the accepted consequence instead of implying a broader one.
nicoloboschi
force-pushed
the
fix/strip-orphan-think-close-tag
branch
from
October 5, 2026 11:45
4b8f23f to
213fef0
Compare
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.
Fixes #4926
What
_strip_reasoning_tagsnow also handles a reasoning block whose open tag was part of the prompt, not the completion.Chat templates that prefill
<think>into the generation prompt (Qwen3 thinking models, DeepSeek-R1-0528, Spark-X2.5) make the model's output start mid-thought:reasoning</think>answer. Behind an OpenAI-compatible server that runs without a reasoning parser, that is exactly what lands inmessage.content. Spark-X2.5's template defaultsenable_thinkingto true and emits<think>in the generation prompt, and the vLLM command in its README passes no--reasoning-parser, so a self-hosted Spark-X2.5 hits this out of the box.Neither existing rule matches that shape — the closed-block regex needs the open tag, and the unclosed rule anchors on it too — so:
</think>still in it;_strip_code_fencesfalls back to the outer{...}span, which breaks as soon as the reasoning drafts a brace.How
Per tag pair, before the existing closed/unclosed passes: if the first close tag has no matching open tag before it, drop everything up to and including it.
The guard that keeps #3426's intent: this is skipped when the text before the close tag starts with
{,[or a code fence. A response that already opened as JSON is the answer, so a</think>inside it is a quoted literal, not a reasoning boundary. Prefilled reasoning is prose, so it never takes that branch.All three call sites (
callstructured + free-form, native Ollama,openai-responses) go through the same helper, so no call-site changes.Testing
tests/test_strip_reasoning_tags.py— 8 new cases: prose and multi-line mental-model leaks, JSON after reasoning that itself drafted braces (the case the outer-span fallback gets wrong), an empty prefilled block, the</thinking>and|endthink|variants,</think>quoted inside bare and fenced JSON (kept verbatim), and an orphan head followed by a normal closed block. The existing 19 cases pass unchanged (27/27 total). Against the old implementation the six stripping cases fail and the two keep-verbatim cases pass, so the latter pin the #3426 behaviour rather than the new branch.ruff checkandruff format --checkare clean on both files.Known trade-off
A free-form answer that mentions
</think>in prose, without<think>before it and without starting as JSON, would be cut at that mention. That needs a model to talk about the closing tag alone, which seemed a much smaller risk than leaking reasoning into every mental model for this family of templates — happy to tighten it if you'd rather key it on something else.