[Bugfix] Align deepseek v4 parser thinking default with tokenizer (#51296)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed Aug 10, 2026 at 15:30 UTC 3f142bd85e962a795c6c484f0249f9c92aed37b2
3 files changed +57 -12
tests/parser/engine/test_deepseek_v4.py
+51 -8
@@ -243,15 +243,29 @@ class TestThinkingModeConfig:
243 cfg = deepseek_v4_config(thinking=False)
244 assert cfg.initial_state.name == "CONTENT"
245
246 - def test_enable_thinking_kwarg(self, mock_tokenizer):
247 - p = DeepSeekV4Parser(
248 - mock_tokenizer, chat_template_kwargs={"enable_thinking": True}
246 + @pytest.mark.parametrize(
247 + ("chat_template_kwargs", "expected_state"),
248 + [
249 + ({}, "REASONING"),
250 + ({"thinking": True}, "REASONING"),
251 + ({"enable_thinking": True}, "REASONING"),
252 + ({"reasoning_effort": "high"}, "REASONING"),
253 + ({"thinking": False}, "CONTENT"),
254 + ({"enable_thinking": False}, "CONTENT"),
255 + (
256 + {"enable_thinking": True, "reasoning_effort": "none"},
257 + "CONTENT",
258 + ),
259 + ],
260 + )
261 + def test_parser_thinking_mode_matches_tokenizer_default(
262 + self, mock_tokenizer, chat_template_kwargs, expected_state
263 + ):
264 + parser = DeepSeekV4Parser(
265 + mock_tokenizer,
266 + chat_template_kwargs=chat_template_kwargs,
267 )
250 - assert p.parser_engine_config.initial_state.name == "REASONING"
251 -
252 - def test_no_thinking_kwarg_defaults_to_content(self, mock_tokenizer):
253 - p = DeepSeekV4Parser(mock_tokenizer)
254 - assert p.parser_engine_config.initial_state.name == "CONTENT"
268 + assert parser.parser_engine_config.initial_state.name == expected_state
269
270 def test_thinking_mode_reasoning_without_tags(self, mock_tokenizer):
271 parser = DeepSeekV4Parser(
@@ -838,6 +852,35 @@ class TestDelegatingParserLargeDelta:
852 args = json.loads(output.tool_calls[0]["arguments"])
853 assert args == {"location": "Berlin", "units": "celsius"}
854
855 + def test_default_thinking_extracts_tool_call_without_think_end(self, dsv4_tokens):
856 + tokens = [
857 + token
858 + for token in dsv4_tokens
859 + if token[0] != _DSV4_FULL_VOCAB[DSML_THINK_END]
860 + ]
861 + tokenizer = MockTokenizer(
862 + vocab=dict(_DSV4_FULL_VOCAB),
863 + tokens=tokens,
864 + )
865 + parser = _DeepSeekV4Delegating(tokenizer)
866 +
867 + deltas = replay_streaming(
868 + parser,
869 + tokens,
870 + chunk_size=1,
871 + finished_on_last=True,
872 + tools=DUMMY_TOOLS,
873 + prompt_token_ids=[_DSV4_FULL_VOCAB[DSML_THINK_START]],
874 + )
875 + output = collect_output(deltas)
876 +
877 + assert "The user wants" in output.reasoning
878 + assert output.content == ""
879 + assert len(output.tool_calls) == 1
880 + assert output.tool_calls[0]["name"] == "get_weather"
881 + args = json.loads(output.tool_calls[0]["arguments"])
882 + assert args == {"location": "Berlin", "units": "celsius"}
883 +
884 def test_eos_drop_token_does_not_swallow_tool_calls(self):
885 """Tool calls must survive when an EOS DROP token's ID is in
886 delta_token_ids but its text is absent from delta_text.
tests/parser/engine/trace_builder.py
+1 -1
@@ -722,7 +722,7 @@ def _dsv4_segments(scenario: Scenario, thinking: bool) -> list[tuple[str, bool]]
722
723 def _build_deepseek_v4(scenario: Scenario, validate: bool = True) -> Sample:
724 thinking = scenario.reasoning is not None
725 - chat_kwargs = {"thinking": True} if thinking else None
725 + chat_kwargs = {"thinking": thinking}
726
727 if thinking:
728 expected_reasoning: str | None = scenario.reasoning or ""
vllm/parser/deepseek_v4.py
+5 -3
@@ -217,10 +217,12 @@ class DeepSeekV4Parser(ParserEngine):
217 **kwargs,
218 ) -> None:
219 chat_kwargs = kwargs.pop("chat_template_kwargs", None) or {}
220 - thinking = (
221 - bool(chat_kwargs.get("thinking") or chat_kwargs.get("enable_thinking"))
222 - and chat_kwargs.get("reasoning_effort") != "none"
220 + thinking = bool(
221 + chat_kwargs.get("thinking") or chat_kwargs.get("enable_thinking")
222 )
223 + if "thinking" not in chat_kwargs and "enable_thinking" not in chat_kwargs:
224 + thinking = True
225 + thinking = thinking and chat_kwargs.get("reasoning_effort") != "none"
226 super().__init__(
227 tokenizer,
228 tools,