Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def test_worker_config_accepts_parser_runtime_settings():
namespace="dynamo",
tool_call_parser="kimi_k2",
reasoning_parser="kimi_k25",
default_thinking_mode="disabled",
exclude_tools_when_tool_choice_none=False,
enable_local_indexer=False,
)
Expand Down Expand Up @@ -146,6 +147,7 @@ def test_python_worker_config_from_runtime_config_copies_parser_settings():
runtime_cfg.custom_jinja_template = None
runtime_cfg.dyn_tool_call_parser = "kimi_k2"
runtime_cfg.dyn_reasoning_parser = "kimi_k25"
runtime_cfg.dyn_default_thinking_mode = "disabled"
runtime_cfg.exclude_tools_when_tool_choice_none = False
runtime_cfg.enable_local_indexer = False
runtime_cfg.dyn_enable_structural_tag = True
Expand All @@ -160,6 +162,7 @@ def test_python_worker_config_from_runtime_config_copies_parser_settings():

assert config.tool_call_parser == "kimi_k2"
assert config.reasoning_parser == "kimi_k25"
assert config.default_thinking_mode == "disabled"
assert config.exclude_tools_when_tool_choice_none is False
assert config.enable_local_indexer is False
assert config.structural_tag_mode == "on"
Expand All @@ -184,6 +187,7 @@ class _BareRuntime:
assert cfg.endpoint_types == "chat,completions"
assert cfg.use_kv_events is False
assert cfg.custom_jinja_template is None
assert cfg.default_thinking_mode is None
assert cfg.structural_tag_mode == "off"
assert cfg.structural_tag_scope == "auto"
assert cfg.structural_tag_schema == "auto"
Expand Down
5 changes: 5 additions & 0 deletions components/src/dynamo/common/backend/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class WorkerConfig:
custom_jinja_template: Optional[str] = None
tool_call_parser: Optional[str] = None
reasoning_parser: Optional[str] = None
default_thinking_mode: Optional[str] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep WorkerConfig append-only.

Line 120 inserts default_thinking_mode before existing optional fields, but this class already documents that new fields must be appended to preserve positional callers. Any positional WorkerConfig(...) call that used exclude_tools_when_tool_choice_none or anything after it will now silently bind the wrong values.

Suggested fix
-    default_thinking_mode: Optional[str] = None
     exclude_tools_when_tool_choice_none: bool = True
     enable_local_indexer: bool = True
@@
     # Appended at the END of the dataclass to keep positional callers
     # working -- inserting mid-class would silently shift downstream args.
     route_to_encoder: bool = False
+    default_thinking_mode: Optional[str] = None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default_thinking_mode: Optional[str] = None
exclude_tools_when_tool_choice_none: bool = True
enable_local_indexer: bool = True
# Appended at the END of the dataclass to keep positional callers
# working -- inserting mid-class would silently shift downstream args.
route_to_encoder: bool = False
default_thinking_mode: Optional[str] = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/src/dynamo/common/backend/worker.py` at line 120, Move the new
WorkerConfig field default_thinking_mode to the end of the class so the
dataclass stays append-only and existing positional callers keep binding
correctly. Update the WorkerConfig definition by appending default_thinking_mode
after the current optional fields, rather than inserting it before
exclude_tools_when_tool_choice_none or any existing parameters.

exclude_tools_when_tool_choice_none: bool = True
enable_local_indexer: bool = True
# Operator-level kill switch for KV-aware-routing publishers. When False,
Expand Down Expand Up @@ -175,6 +176,9 @@ def from_runtime_config(
),
"tool_call_parser": getattr(runtime_cfg, "dyn_tool_call_parser", None),
"reasoning_parser": getattr(runtime_cfg, "dyn_reasoning_parser", None),
"default_thinking_mode": getattr(
runtime_cfg, "dyn_default_thinking_mode", None
),
"exclude_tools_when_tool_choice_none": getattr(
runtime_cfg, "exclude_tools_when_tool_choice_none", True
),
Expand Down Expand Up @@ -260,6 +264,7 @@ async def run(self) -> None:
custom_jinja_template=self.config.custom_jinja_template,
tool_call_parser=self.config.tool_call_parser,
reasoning_parser=self.config.reasoning_parser,
default_thinking_mode=self.config.default_thinking_mode,
exclude_tools_when_tool_choice_none=(
self.config.exclude_tools_when_tool_choice_none
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class DynamoRuntimeConfig(ConfigBase):

dyn_tool_call_parser: Optional[str] = None
dyn_reasoning_parser: Optional[str] = None
dyn_default_thinking_mode: Optional[str] = None
exclude_tools_when_tool_choice_none: bool = True
dyn_enable_structural_tag: bool = False
dyn_structural_tag_scope: str = "auto"
Expand Down Expand Up @@ -160,6 +161,15 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
help="Reasoning parser name for the model. If not specified, no reasoning parsing is performed.",
choices=get_reasoning_parser_names(),
)
add_argument(
g,
flag_name="--dyn-default-thinking-mode",
env_var="DYN_DEFAULT_THINKING_MODE",
default=None,
choices=["enabled", "disabled"],
help="Deployment-level default thinking mode for chat templates. "
"Client request thinking/chat_template_args values override this default.",
)
# NOTE: This flag also exists in FrontendArgGroup (frontend_args.py).
# Both definitions are needed: this one controls the Rust-native chat
# template path (oai.rs), while the frontend copy controls the Python
Expand Down
10 changes: 10 additions & 0 deletions components/src/dynamo/frontend/prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from vllm.tool_parsers import ToolParser
from vllm.utils.async_utils import AsyncMicrobatchTokenizer

from .thinking import apply_default_thinking_mode_to_template_kwargs


class _Renderer(Protocol):
"""Structural type for vLLM's chat-template renderer."""
Expand Down Expand Up @@ -90,6 +92,7 @@ def _prepare_request(
tool_parser_class: type[ToolParser] | None,
exclude_tools_when_tool_choice_none: bool = True,
enable_auto_tool_choice: bool = False,
default_thinking_mode: str | None = None,
) -> tuple[ChatCompletionRequest, ToolParser | None, dict[str, Any], Any, ChatParams]:
"""Validate request and build arguments for template rendering.

Expand Down Expand Up @@ -134,6 +137,11 @@ def _prepare_request(
else None
)
chat_template_kwargs = dict(request_for_sampling.chat_template_kwargs or {})
chat_template_kwargs = apply_default_thinking_mode_to_template_kwargs(
chat_template_kwargs,
default_thinking_mode,
request_has_root_thinking=(isinstance(request, dict) and "thinking" in request),
)
chat_template_kwargs["reasoning_effort"] = request_for_sampling.reasoning_effort
Comment on lines +140 to 145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Behavioral difference between SGLang and vLLM/Rust in how reasoning_effort interacts with default_thinking_mode

In the SGLang path, _normalize_openai_thinking_template_kwargs (components/src/dynamo/frontend/sglang_prepost.py:366-401) processes reasoning_effort: "none" into thinking: False / enable_thinking: False via setdefault_reasoning(False) BEFORE apply_default_thinking_mode_to_template_kwargs runs. This means reasoning_effort: "none" effectively blocks any deployment default from being applied.

In the vLLM path (components/src/dynamo/frontend/prepost.py:139-145) and the Rust preprocessor (lib/llm/src/preprocessor.rs:402-442), reasoning_effort is NOT checked before the default is applied — it's simply added as a separate template kwarg afterward. So a request with reasoning_effort: "none" + default_thinking_mode: "enabled" would end up with both enable_thinking: true (from the default) and reasoning_effort: "none" in the template kwargs. Whether this is a problem depends on the chat template's precedence rules for these fields.

This is likely intentional — SGLang normalizes OpenAI thinking controls into template flags, while vLLM/Rust let the template interpret reasoning_effort directly — but it's worth confirming the desired behavior for mixed-signal scenarios.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


# Mistral warns that tokenize=False is unsafe for chat templates.
Expand Down Expand Up @@ -178,6 +186,7 @@ async def preprocess_chat_request(
tool_parser_class: type[ToolParser] | None,
exclude_tools_when_tool_choice_none: bool = True,
enable_auto_tool_choice: bool = False,
default_thinking_mode: str | None = None,
) -> PreprocessResult:
(
request_for_sampling,
Expand All @@ -191,6 +200,7 @@ async def preprocess_chat_request(
tool_parser_class=tool_parser_class,
exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none,
enable_auto_tool_choice=enable_auto_tool_choice,
default_thinking_mode=default_thinking_mode,
)

_, engine_prompt = await renderer.render_messages_async(messages, chat_params)
Expand Down
11 changes: 10 additions & 1 deletion components/src/dynamo/frontend/sglang_prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from sglang.srt.parser.reasoning_parser import ReasoningParser

from .thinking import apply_default_thinking_mode_to_template_kwargs
from .utils import random_call_id

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -364,6 +365,7 @@ def _flatten_message_content(content: Any) -> Any:

def _normalize_openai_thinking_template_kwargs(
request: dict[str, Any],
default_thinking_mode: str | None = None,
) -> dict[str, Any]:
request = copy.copy(request)
chat_template_kwargs = dict(
Expand All @@ -388,6 +390,12 @@ def setdefault_reasoning(enabled: bool) -> None:
if request.get("reasoning_effort") == "none":
setdefault_reasoning(False)

chat_template_kwargs = apply_default_thinking_mode_to_template_kwargs(
chat_template_kwargs,
default_thinking_mode,
request_has_root_thinking="thinking" in request,
)

if chat_template_kwargs:
request["chat_template_kwargs"] = chat_template_kwargs
return request
Expand Down Expand Up @@ -613,6 +621,7 @@ def preprocess_chat_request(
reasoning_parser_name: str | None,
exclude_tools_when_tool_choice_none: bool = True,
template_force_reasoning: bool = False,
default_thinking_mode: str | None = None,
) -> SglangPreprocessResult:
"""Preprocess a chat request using SGLang tokenizer and parser APIs.

Expand All @@ -623,7 +632,7 @@ def preprocess_chat_request(

Synchronous -- suitable for both main-process and worker-process execution.
"""
request = _normalize_openai_thinking_template_kwargs(request)
request = _normalize_openai_thinking_template_kwargs(request, default_thinking_mode)
messages = _materialize_messages(request.get("messages", []))

# Per-request client escape hatch: skip reasoning parsing entirely when
Expand Down
14 changes: 14 additions & 0 deletions components/src/dynamo/frontend/sglang_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
detect_force_reasoning_from_template,
preprocess_chat_request,
)
from .thinking import runtime_default_thinking_mode
from .utils import (
PreprocessError,
extract_mm_urls,
Expand Down Expand Up @@ -118,6 +119,7 @@ def _map_finish_reason(raw: str | None) -> str | None:
_w_reasoning_parser_name: str | None = None
_w_exclude_tools_when_tool_choice_none: bool = True
_w_template_force_reasoning: bool = False
_w_default_thinking_mode: str | None = None


@dataclass
Expand All @@ -142,15 +144,18 @@ def _init_worker(
exclude_tools_when_tool_choice_none: bool = True,
trust_remote_code: bool = False,
template_force_reasoning: bool = False,
default_thinking_mode: str | None = None,
) -> None:
"""Initialize a worker process with its own tokenizer."""
global _w_tokenizer, _w_tool_call_parser_name, _w_reasoning_parser_name
global _w_exclude_tools_when_tool_choice_none, _w_template_force_reasoning
global _w_default_thinking_mode
_w_tokenizer = _load_tokenizer(model_path, trust_remote_code)
_w_tool_call_parser_name = tool_call_parser_name
_w_reasoning_parser_name = reasoning_parser_name
_w_exclude_tools_when_tool_choice_none = exclude_tools_when_tool_choice_none
_w_template_force_reasoning = template_force_reasoning
_w_default_thinking_mode = default_thinking_mode


def _preprocess_worker(
Expand All @@ -166,6 +171,7 @@ def _preprocess_worker(
reasoning_parser_name=_w_reasoning_parser_name,
exclude_tools_when_tool_choice_none=_w_exclude_tools_when_tool_choice_none,
template_force_reasoning=_w_template_force_reasoning,
default_thinking_mode=_w_default_thinking_mode,
)

n = request.get("n", 1)
Expand Down Expand Up @@ -293,6 +299,7 @@ def __init__(
preprocess_pool: ProcessPoolExecutor | None = None,
preprocess_workers: int = 0,
stream_interval: int = 1,
default_thinking_mode: str | None = None,
):
self.tokenizer = tokenizer
# Detect force_reasoning once from the chat template, matching
Expand All @@ -316,6 +323,7 @@ def __init__(
self.eos_token_id = eos_token_id
self.debug_perf = debug_perf
self.stream_interval = stream_interval
self.default_thinking_mode = default_thinking_mode
self.preprocess_pool = preprocess_pool
if preprocess_pool is not None:
self._worker_semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
Expand Down Expand Up @@ -372,6 +380,7 @@ async def _generator_inner(
reasoning_parser_name=self.reasoning_parser_name,
exclude_tools_when_tool_choice_none=self.exclude_tools_when_tool_choice_none,
template_force_reasoning=self.template_force_reasoning,
default_thinking_mode=self.default_thinking_mode,
)

if self.debug_perf:
Expand Down Expand Up @@ -694,11 +703,14 @@ async def chat_engine_factory(
self.reasoning_parser_name
or _runtime_config_parser_name(mdc, "reasoning_parser")
)
default_thinking_mode = runtime_default_thinking_mode(mdc.runtime_config())

if tool_call_parser_name:
logger.info("SGLang tool call parser: %s", tool_call_parser_name)
if reasoning_parser_name:
logger.info("SGLang reasoning parser: %s", reasoning_parser_name)
if default_thinking_mode:
logger.info("SGLang default thinking mode: %s", default_thinking_mode)

preprocess_pool = None
preprocess_workers = self.config.preprocess_workers
Expand All @@ -718,6 +730,7 @@ async def chat_engine_factory(
self.config.exclude_tools_when_tool_choice_none,
self.trust_remote_code,
template_force_reasoning,
default_thinking_mode,
),
)
futures = [
Expand Down Expand Up @@ -753,6 +766,7 @@ async def chat_engine_factory(
preprocess_pool=preprocess_pool,
preprocess_workers=preprocess_workers,
stream_interval=self.stream_interval,
default_thinking_mode=default_thinking_mode,
)
gen.exclude_tools_when_tool_choice_none = (
self.config.exclude_tools_when_tool_choice_none
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,63 @@ def apply_chat_template(self, messages, **kwargs):
assert result.request["chat_template_kwargs"]["enable_thinking"] is True
assert result.force_reasoning is True

def test_default_thinking_mode_disabled_reaches_generic_chat_template(self):
captured = {}

class CapturingTokenizer:
chat_template = "template"

def apply_chat_template(self, messages, **kwargs):
captured["kwargs"] = kwargs
return [1, 2, 3]

request = {
"model": "generic-model",
"messages": [{"role": "user", "content": "Hello"}],
}

result = preprocess_chat_request(
request,
tokenizer=CapturingTokenizer(),
tool_call_parser_name=None,
reasoning_parser_name=None,
default_thinking_mode="disabled",
)

assert result.prompt_token_ids == [1, 2, 3]
assert captured["kwargs"]["thinking"] is False
assert captured["kwargs"]["enable_thinking"] is False
assert captured["kwargs"]["thinking_mode"] == "disabled"
assert result.request["chat_template_kwargs"]["thinking_mode"] == "disabled"
assert "chat_template_kwargs" not in request

def test_default_thinking_mode_does_not_override_request_kwargs(self):
captured = {}

class CapturingTokenizer:
chat_template = "template"

def apply_chat_template(self, messages, **kwargs):
captured["kwargs"] = kwargs
return [1, 2, 3]

result = preprocess_chat_request(
{
"model": "generic-model",
"messages": [{"role": "user", "content": "Hello"}],
"chat_template_kwargs": {"enable_thinking": True},
},
tokenizer=CapturingTokenizer(),
tool_call_parser_name=None,
reasoning_parser_name=None,
default_thinking_mode="disabled",
)

assert result.prompt_token_ids == [1, 2, 3]
assert captured["kwargs"]["enable_thinking"] is True
assert "thinking" not in captured["kwargs"]
assert "thinking_mode" not in captured["kwargs"]

def test_deepseek_v4_named_tool_choice_filters_encoder_tools(self, monkeypatch):
captured = {}
fake_module = types.ModuleType("sglang.srt.entrypoints.openai.encoding_dsv4")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,39 @@ def test_reasoning_effort_forwarded_to_template_kwargs(self, tokenizer):
)
assert chat_params.chat_template_kwargs.get("reasoning_effort") == "low"

def test_default_thinking_mode_disabled_reaches_template_kwargs(self, tokenizer):
_, _, chat_template_kwargs, _, chat_params = _prepare_request(
{
"model": MODEL,
"messages": self._messages(),
},
tokenizer=tokenizer,
tool_parser_class=None,
default_thinking_mode="disabled",
)

for kwargs in (chat_template_kwargs, chat_params.chat_template_kwargs):
assert kwargs["thinking"] is False
assert kwargs["enable_thinking"] is False
assert kwargs["thinking_mode"] == "disabled"

def test_default_thinking_mode_does_not_override_request_kwargs(self, tokenizer):
_, _, chat_template_kwargs, _, chat_params = _prepare_request(
{
"model": MODEL,
"messages": self._messages(),
"chat_template_kwargs": {"enable_thinking": True},
},
tokenizer=tokenizer,
tool_parser_class=None,
default_thinking_mode="disabled",
)

for kwargs in (chat_template_kwargs, chat_params.chat_template_kwargs):
assert kwargs["enable_thinking"] is True
assert "thinking" not in kwargs
assert "thinking_mode" not in kwargs


@pytest.mark.parametrize(
("runtime_config", "expected"),
Expand Down
Loading
Loading