A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 2, 18:15 UTC
[Bugfix] Map reasoning_effort to enable_thinking in chat template kwargs (#43401)
Signed-off-by: Ashwin Giridharan <girida@amazon.com> Signed-off-by: Chauncey <chaunceyjiang@gmail.com> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
Ashwin Giridharan committed
May 27, 2026 at 05:39 UTC
52a31ccecca203effd490a5b496dc5f8d9496654
4 files changed
+178
-13
docs/features/reasoning_outputs.md
+38
-1
@@ -15,6 +15,7 @@ vLLM currently supports the following reasoning models:
15
| ------------ | ----------- | ---------------- | ----------- |
16
| [Cohere Command A Reasoning](https://huggingface.co/CohereLabs/command-a-reasoning-08-2025) | `cohere_command3` | `json`, `regex` | ✅ |
17
| [DeepSeek R1 series](https://huggingface.co/collections/deepseek-ai/deepseek-r1-678e1e131c0169c0bc89728d) | `deepseek_r1` | `json`, `regex` | ❌ |
18
+| [Gemma 4 series](https://huggingface.co/google/gemma-4-26B-A4B-it) | `gemma4` | `json`, `regex` | ✅ |
19
| [DeepSeek-V3.1](https://huggingface.co/collections/deepseek-ai/deepseek-v31-68a491bed32bd77e7fca048f) | `deepseek_v3` | `json`, `regex` | ❌ |
20
| [ERNIE-4.5-VL series](https://huggingface.co/baidu/ERNIE-4.5-VL-28B-A3B-PT) | `ernie45` | `json`, `regex` | ❌ |
21
| [ERNIE-4.5-21B-A3B-Thinking](https://huggingface.co/baidu/ERNIE-4.5-21B-A3B-Thinking) | `ernie45` | `json`, `regex` | ✅ |
@@ -29,6 +30,7 @@ vLLM currently supports the following reasoning models:
30
!!! note
31
IBM Granite 3.2 and DeepSeek-V3.1 reasoning is disabled by default; to enable it, you must also pass `thinking=True` in your `chat_template_kwargs`.
32
The reasoning feature for the Qwen3 series is enabled by default. To disable it, you must pass `enable_thinking=False` in your `chat_template_kwargs`.
33
+ Gemma 4 reasoning is disabled by default; to enable it, pass `enable_thinking=True` in your `chat_template_kwargs` or set `reasoning_effort` (which enables it automatically).
34
DeepSeek-V3.1 tool calling is supported in non-thinking mode.
35
Holo2 reasoning is enabled by default. To disable it, you must also pass `thinking=False` in your `chat_template_kwargs`.
36
@@ -314,9 +316,44 @@ for output in outputs:
316
print("text:", output.outputs[0].text)
317
```
318
319
+## Automatic `enable_thinking` Activation
320
+
321
+Some models (such as Gemma 4, DeepSeek-V4-Pro and IBM Granite 3.2) require `enable_thinking: true` in their chat template kwargs to activate thinking mode — without it, reasoning tokens are never generated regardless of other settings.
322
+
323
+When you set `reasoning_effort` in a Chat Completions request (or `reasoning.effort` in a Responses API request), vLLM automatically injects `enable_thinking` into the chat template kwargs:
324
+
325
+- `reasoning_effort` = `"low"`, `"medium"`, or `"high"` → `enable_thinking = true`
326
+- `reasoning_effort` = `"none"` → `enable_thinking = false`
327
+- `reasoning_effort` not set → `enable_thinking` is not injected (preserves existing behavior)
328
+
329
+This means you no longer need to manually pass `chat_template_kwargs: {"enable_thinking": true}` when using `reasoning_effort` — it is handled automatically.
330
+
331
+!!! note
332
+ If you explicitly set `enable_thinking` in `chat_template_kwargs`, your value takes priority over the automatic injection. This allows you to override the behavior if needed.
333
+
334
+ For models whose templates don't declare `enable_thinking` (e.g., DeepSeek R1), the injected kwarg is harmlessly filtered out by `resolve_chat_template_kwargs`.
335
+
336
+### Example
337
+
338
+```python
339
+from openai import OpenAI
340
+
341
+client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
342
+
343
+# reasoning_effort automatically enables thinking for models that need it
344
+response = client.chat.completions.create(
345
+ model="google/gemma-4-26B-A4B-it",
346
+ messages=[{"role": "user", "content": "What is 15 * 37?"}],
347
+ reasoning_effort="high", # Automatically sets enable_thinking=true
348
+)
349
+
350
+print(response.choices[0].message.reasoning)
351
+print(response.choices[0].message.content)
352
+```
353
+
354
## Limitations
355
319
-- The reasoning content is only available for online serving's chat completion endpoint (`/v1/chat/completions`).
356
+- The reasoning content is only available for online serving's chat completion endpoint (`/v1/chat/completions`), Anthropic Messages API (`/v1/messages`) and the Responses API (`/v1/responses`).
357
358
## How to support a new reasoning model
359
tests/entrypoints/openai/test_reasoning_enable_thinking.py
new
+107
@@ -0,0 +1,107 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Unit tests for reasoning_effort -> enable_thinking mapping.
4
+
5
+Models like Gemma4 require enable_thinking=True in chat_template_kwargs to
6
+activate thinking mode. This mapping ensures that when a user requests
7
+reasoning (via reasoning_effort or reasoning.effort), the template kwarg
8
+is injected automatically.
9
+"""
10
+
11
+import pytest
12
+from openai.types.shared import Reasoning
13
+
14
+from vllm.entrypoints.openai.chat_completion.protocol import (
15
+ ChatCompletionRequest,
16
+)
17
+from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
18
+
19
+
20
+def _build_chat_request(**kwargs) -> ChatCompletionRequest:
21
+ defaults = dict(
22
+ model="test-model",
23
+ messages=[{"role": "user", "content": "Hello"}],
24
+ )
25
+ defaults.update(kwargs)
26
+ return ChatCompletionRequest(**defaults)
27
+
28
+
29
+def _build_responses_request(**kwargs) -> ResponsesRequest:
30
+ defaults = dict(
31
+ model="test-model",
32
+ input=[{"role": "user", "content": "Hello"}],
33
+ )
34
+ defaults.update(kwargs)
35
+ return ResponsesRequest(**defaults)
36
+
37
+
38
+class TestChatCompletionReasoningEffort:
39
+ """Chat Completions: reasoning_effort -> enable_thinking."""
40
+
41
+ @pytest.mark.parametrize("effort", ["low", "medium", "high"])
42
+ def test_non_none_effort_injects_enable_thinking_true(self, effort):
43
+ request = _build_chat_request(reasoning_effort=effort)
44
+ params = request.build_chat_params(None, "auto")
45
+ assert params.chat_template_kwargs["enable_thinking"] is True
46
+
47
+ def test_none_effort_injects_enable_thinking_false(self):
48
+ request = _build_chat_request(reasoning_effort="none")
49
+ params = request.build_chat_params(None, "auto")
50
+ assert params.chat_template_kwargs["enable_thinking"] is False
51
+
52
+ def test_no_effort_does_not_inject(self):
53
+ request = _build_chat_request()
54
+ params = request.build_chat_params(None, "auto")
55
+ assert "enable_thinking" not in params.chat_template_kwargs
56
+
57
+ def test_explicit_user_kwarg_not_overridden(self):
58
+ request = _build_chat_request(
59
+ reasoning_effort="high",
60
+ chat_template_kwargs={"enable_thinking": False},
61
+ )
62
+ params = request.build_chat_params(None, "auto")
63
+ assert params.chat_template_kwargs["enable_thinking"] is False
64
+
65
+ def test_reasoning_effort_still_in_kwargs(self):
66
+ request = _build_chat_request(reasoning_effort="high")
67
+ params = request.build_chat_params(None, "auto")
68
+ assert params.chat_template_kwargs["reasoning_effort"] == "high"
69
+
70
+
71
+class TestResponsesReasoningEffort:
72
+ """Responses API: reasoning.effort -> enable_thinking."""
73
+
74
+ @pytest.mark.parametrize("effort", ["low", "medium", "high"])
75
+ def test_non_none_effort_injects_enable_thinking_true(self, effort):
76
+ request = _build_responses_request(
77
+ reasoning=Reasoning(effort=effort),
78
+ )
79
+ params = request.build_chat_params(None, "auto")
80
+ assert params.chat_template_kwargs["enable_thinking"] is True
81
+
82
+ def test_none_effort_injects_enable_thinking_false(self):
83
+ request = _build_responses_request(
84
+ reasoning=Reasoning(effort="none"),
85
+ )
86
+ params = request.build_chat_params(None, "auto")
87
+ assert params.chat_template_kwargs["enable_thinking"] is False
88
+
89
+ def test_no_reasoning_does_not_inject(self):
90
+ request = _build_responses_request()
91
+ params = request.build_chat_params(None, "auto")
92
+ assert "enable_thinking" not in params.chat_template_kwargs
93
+
94
+ def test_explicit_user_kwarg_not_overridden(self):
95
+ request = _build_responses_request(
96
+ reasoning=Reasoning(effort="high"),
97
+ chat_template_kwargs={"enable_thinking": False},
98
+ )
99
+ params = request.build_chat_params(None, "auto")
100
+ assert params.chat_template_kwargs["enable_thinking"] is False
101
+
102
+ def test_reasoning_effort_still_in_kwargs(self):
103
+ request = _build_responses_request(
104
+ reasoning=Reasoning(effort="high"),
105
+ )
106
+ params = request.build_chat_params(None, "auto")
107
+ assert params.chat_template_kwargs["reasoning_effort"] == "high"
vllm/entrypoints/openai/chat_completion/protocol.py
+16
-6
@@ -473,17 +473,27 @@ class ChatCompletionRequest(OpenAIBaseModel):
473
default_template: str | None,
474
default_template_content_format: ChatTemplateContentFormatOption,
475
) -> ChatParams:
476
+ extra_kwargs: dict[str, Any] = dict(
477
+ add_generation_prompt=self.add_generation_prompt,
478
+ continue_final_message=self.continue_final_message,
479
+ documents=self.documents,
480
+ reasoning_effort=self.reasoning_effort,
481
+ )
482
+
483
+ # When reasoning is requested, activate thinking for models whose
484
+ # chat templates require explicit opt-in (e.g., Gemma4 defaults
485
+ # enable_thinking to false). For templates that don't declare the
486
+ # variable, resolve_chat_template_kwargs filters it out harmlessly.
487
+ user_kwargs = self.chat_template_kwargs or {}
488
+ if self.reasoning_effort is not None and "enable_thinking" not in user_kwargs:
489
+ extra_kwargs["enable_thinking"] = self.reasoning_effort != "none"
490
+
491
return ChatParams(
492
chat_template=self.chat_template or default_template,
493
chat_template_content_format=default_template_content_format,
494
chat_template_kwargs=merge_kwargs(
495
self.chat_template_kwargs,
481
- dict(
482
- add_generation_prompt=self.add_generation_prompt,
483
- continue_final_message=self.continue_final_message,
484
- documents=self.documents,
485
- reasoning_effort=self.reasoning_effort,
486
- ),
496
+ extra_kwargs,
497
),
498
media_io_kwargs=self.media_io_kwargs,
499
)
vllm/entrypoints/openai/responses/protocol.py
+17
-6
@@ -298,17 +298,28 @@ class ResponsesRequest(OpenAIBaseModel):
298
continue_final = should_continue_final_message(self.input)
299
300
reasoning = self.reasoning
301
+ reasoning_effort = None if reasoning is None else reasoning.effort
302
+
303
+ extra_kwargs: dict[str, Any] = dict(
304
+ add_generation_prompt=not continue_final,
305
+ continue_final_message=continue_final,
306
+ reasoning_effort=reasoning_effort,
307
+ )
308
+
309
+ # When reasoning is requested, activate thinking for models whose
310
+ # chat templates require explicit opt-in (e.g., Gemma4 defaults
311
+ # enable_thinking to false). For templates that don't declare the
312
+ # variable, resolve_chat_template_kwargs filters it out harmlessly.
313
+ user_kwargs = self.chat_template_kwargs or {}
314
+ if reasoning_effort is not None and "enable_thinking" not in user_kwargs:
315
+ extra_kwargs["enable_thinking"] = reasoning_effort != "none"
316
317
return ChatParams(
318
chat_template=default_template,
319
chat_template_content_format=default_template_content_format,
305
- chat_template_kwargs=merge_kwargs( # To remove unset values
320
+ chat_template_kwargs=merge_kwargs(
321
self.chat_template_kwargs,
307
- dict(
308
- add_generation_prompt=not continue_final,
309
- continue_final_message=continue_final,
310
- reasoning_effort=None if reasoning is None else reasoning.effort,
311
- ),
322
+ extra_kwargs,
323
),
324
media_io_kwargs=self.media_io_kwargs,
325
)