Conversation
…ssing
Qwen3Detector forwards every constructor argument to the base class
except tool_start_token. The base BaseReasoningFormatDetector already
contains a fallback that, when in reasoning state and a tool start
token appears, forwards the tool call to normal_text instead of
keeping it inside reasoning_content. Because Qwen3Detector never sets
this token, the fallback is silently disabled for every Qwen3 family
model that uses the qwen3 reasoning parser.
In production this is reproducible with Qwen3.5-27B served via SGLang:
1. force_reasoning=True (e.g. enable_thinking=True request, the
default for Qwen3-style chat templates that auto-prepend <think>)
2. The model emits <tool_call>...</tool_call> directly without
first emitting </think> (long contexts, certain tool-call code
templates, or when the model decides not to think before acting)
3. The entire response is silently routed into reasoning_content,
content is null, and tool_calls is null
4. Downstream the function-call parser sees no tool calls and
LangChain-style frameworks raise a Pydantic validation error
("ToolMessage tool_call_id None") because the framework tries
to construct a ToolMessage from the empty tool_call
Fix: pass tool_start_token="<tool_call>" to the base class so the
existing fallback is wired up. The fallback only triggers when
_in_reasoning=True, so behavior is unchanged for enable_thinking=False
requests and for normal flows that include a proper </think>.
Added two regression tests under TestQwen3ForcedReasoningDetector
covering both the non-streaming and streaming paths.
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
This was referenced Jul 26, 2026
Merged
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
Qwen3Detectorforwards every constructor argument to the base class excepttool_start_token. The baseBaseReasoningFormatDetectoralready contains a fallback that, when in reasoning state and a tool start token appears, forwards the tool call tonormal_textinstead of keeping it insidereasoning_content. BecauseQwen3Detectornever sets this token, the fallback is silently disabled for every Qwen3-family model that uses theqwen3reasoning parser.Reproduction (production)
This is reproducible with Qwen3.5-27B (FP8, hybrid Mamba) served via SGLang:
force_reasoning=True— e.g. anenable_thinking=Truerequest, which is the default routing for Qwen3-style chat templates that auto-prepend<think>\ninadd_generation_prompt.<tool_call>...</tool_call>directly without first emitting</think>. This happens with long contexts, certain code-generation tool calls, or when the model decides not to think before acting.reasoning_content.contentisnull,tool_callsisnull,finish_reasonisstop.Tool 'None' is not defined in the tools list.Downstream, LangChain-style frameworks raise a Pydantic validation error (ToolMessage tool_call_id None) because they try to construct a tool message from the empty tool call.The user-facing symptom is that the agent loop silently breaks mid-conversation: the model is producing valid
<tool_call>XML, but it never reaches the function-call parser.Root cause
python/sglang/srt/parser/reasoning_parser.py:BaseReasoningFormatDetector.__init__acceptstool_start_tokenand uses it in bothdetect_and_parseandparse_streaming_incrementto escape from reasoning mode early.Qwen3Detector.__init__does not passtool_start_tokentosuper().__init__, so it staysNoneand the fallback is dead code for Qwen3 models.Fix
One line: pass
tool_start_token="<tool_call>"to the base class.The fallback only triggers when
_in_reasoning=True, so behavior is unchanged forenable_thinking=Falserequests and for normal flows that include a proper</think>.Tests
Added two regression tests under
TestQwen3ForcedReasoningDetector:test_detect_and_parse_tool_call_without_think_close(non-streaming)test_streaming_tool_call_without_think_close(streaming)Both verify that when
force_reasoning=Trueand the model emits<tool_call>without first closing</think>, the tool call is correctly split intonormal_textand the parser flips out of reasoning state.Verified
Tested standalone against Qwen3.5-27B-FP8 + tool-call-parser=qwen3_coder + reasoning-parser=qwen3:
reasoning_content, downstream sees emptytool_calls.tool_calls[0].id,tool_calls[0].name,tool_calls[0].argumentsall populated correctly.reasoning_contentstill captures the actual reasoning text when present.<think>...</think>...flow unchanged.enable_thinking=Falseflow unchanged (untouched code path, since_in_reasoningstartsFalse).Checklist