Skip to content
Merged
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 @@ -26,6 +26,10 @@

# Import-time skip: if the extension hasn't been built, all tests below
# are skipped rather than crashing the collection phase.
core = pytest.importorskip(
"dynamo._core",
reason="dynamo._core not built — run `maturin develop` first",
)
backend = pytest.importorskip(
"dynamo._core.backend",
reason="dynamo._core.backend not built — run `maturin develop` first",
Expand Down Expand Up @@ -114,11 +118,58 @@ 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,
)


def test_worker_config_preserves_legacy_positional_argument_order():
"""New optional fields must be appended after every existing argument."""
backend.WorkerConfig(
"dynamo", # namespace
"backend", # component
"generate", # endpoint
"", # model_name
None, # served_model_name
core.ModelInput.Tokens, # model_input
"chat,completions", # endpoint_types
None, # custom_jinja_template
None, # tool_call_parser
None, # reasoning_parser
False, # exclude_tools_when_tool_choice_none
False, # enable_local_indexer
)


@pytest.mark.unified
def test_python_worker_config_preserves_legacy_positional_argument_order():
from dynamo.common.backend.worker import WorkerConfig

config = WorkerConfig(
"dynamo", # namespace
"backend", # component
"generate", # endpoint
"", # model_name
None, # served_model_name
core.ModelInput.Tokens, # model_input
"chat,completions", # endpoint_types
"etcd", # discovery_backend
"tcp", # request_plane
None, # event_plane
False, # use_kv_events
None, # custom_jinja_template
None, # tool_call_parser
None, # reasoning_parser
False, # exclude_tools_when_tool_choice_none
False, # enable_local_indexer
)

assert config.exclude_tools_when_tool_choice_none is False
assert config.enable_local_indexer is False
assert config.default_thinking_mode is None


def test_worker_config_accepts_media_configuration():
"""Unified registration can advertise frontend media decoding."""
from dynamo.llm import MediaDecoder, MediaFetcher
Expand Down Expand Up @@ -157,6 +208,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 @@ -171,6 +223,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 @@ -195,6 +248,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 @@ -146,6 +146,7 @@ class WorkerConfig:
media_fetcher: Optional[MediaFetcher] = None
# KV event/recovery ownership endpoint. None uses this worker's serving endpoint.
kv_state_endpoint: Optional[str] = None
default_thinking_mode: Optional[str] = None

@classmethod
def from_runtime_config(
Expand Down Expand Up @@ -180,6 +181,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 @@ -266,6 +270,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 @@ -37,6 +37,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 @@ -196,6 +197,16 @@ 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, reasoning_effort, chat_template_args, or "
"chat_template_kwargs 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
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,28 @@ def test_fpm_trace_help_lists_flag_and_env(monkeypatch):
assert "--fpm-trace" in help_text
assert "--no-fpm-trace" in help_text
assert "DYN_FPM_TRACE" in help_text


@pytest.mark.parametrize("mode", ["enabled", "disabled"])
def test_default_thinking_mode_cli(mode, monkeypatch):
monkeypatch.delenv("DYN_DEFAULT_THINKING_MODE", raising=False)

config, help_text = _parse_runtime_args(["--dyn-default-thinking-mode", mode])

assert config.dyn_default_thinking_mode == mode
assert "DYN_DEFAULT_THINKING_MODE" in help_text


def test_default_thinking_mode_env(monkeypatch):
monkeypatch.setenv("DYN_DEFAULT_THINKING_MODE", "disabled")

config, _ = _parse_runtime_args([])

assert config.dyn_default_thinking_mode == "disabled"


def test_default_thinking_mode_rejects_invalid_value(monkeypatch):
monkeypatch.delenv("DYN_DEFAULT_THINKING_MODE", raising=False)

with pytest.raises(SystemExit):
_parse_runtime_args(["--dyn-default-thinking-mode", "adaptive"])
19 changes: 17 additions & 2 deletions components/src/dynamo/frontend/prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from vllm.tool_parsers import ToolParser
from vllm.utils.async_utils import make_async

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 @@ -94,6 +96,7 @@ def _prepare_request(
exclude_tools_when_tool_choice_none: bool = True,
enable_auto_tool_choice: bool = False,
default_chat_template_kwargs: dict[str, Any] | None = None,
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 @@ -151,10 +154,20 @@ def _prepare_request(
request_for_sampling.chat_template_kwargs or raw_template_args or {},
)
)
# Don't let an absent top-level field clobber a nested reasoning_effort.
# reasoning_effort is a request-level thinking control. Put an explicit
# value into the kwargs before applying the deployment default so the two
# cannot produce contradictory template controls.
if request_for_sampling.reasoning_effort is not None:
chat_template_kwargs["reasoning_effort"] = request_for_sampling.reasoning_effort
else:
chat_template_kwargs = apply_default_thinking_mode_to_template_kwargs(
chat_template_kwargs,
default_thinking_mode,
request_has_root_thinking=(
isinstance(request, dict) and request.get("thinking") is not None
),
)
# Don't let an absent top-level field clobber a nested reasoning_effort.
if request_for_sampling.reasoning_effort is None:
chat_template_kwargs.setdefault("reasoning_effort", None)

# Mistral warns that tokenize=False is unsafe for chat templates.
Expand Down Expand Up @@ -201,6 +214,7 @@ async def preprocess_chat_request(
exclude_tools_when_tool_choice_none: bool = True,
enable_auto_tool_choice: bool = False,
default_chat_template_kwargs: dict[str, Any] | None = None,
default_thinking_mode: str | None = None,
) -> PreprocessResult:
(
request_for_sampling,
Expand All @@ -215,6 +229,7 @@ async def preprocess_chat_request(
exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none,
enable_auto_tool_choice=enable_auto_tool_choice,
default_chat_template_kwargs=default_chat_template_kwargs,
default_thinking_mode=default_thinking_mode,
)

_, engine_prompt = await renderer.render_messages_async(messages, chat_params)
Expand Down
16 changes: 14 additions & 2 deletions 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 PreprocessError, random_call_id

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -410,6 +411,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 @@ -434,9 +436,18 @@ def setdefault_reasoning(enabled: bool) -> None:
elif thinking_type == "disabled":
setdefault_reasoning(False)

if request.get("reasoning_effort") == "none":
reasoning_effort = request.get("reasoning_effort")
if reasoning_effort is not None:
chat_template_kwargs["reasoning_effort"] = reasoning_effort
if 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=request.get("thinking") is not None,
)

if chat_template_kwargs:
request["chat_template_kwargs"] = chat_template_kwargs
return request
Expand Down Expand Up @@ -703,6 +714,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 @@ -713,7 +725,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", []))

# Generation mode is independent of whether the client wants reasoning
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 @@ -37,6 +37,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 @@ -145,6 +146,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


def _load_chat_template(chat_template: str | None) -> str | None:
Expand Down Expand Up @@ -194,17 +196,20 @@ def _init_worker(
trust_remote_code: bool = False,
template_force_reasoning: bool = False,
chat_template: str | None = None,
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)
if chat_template is not None:
_w_tokenizer.chat_template = chat_template
_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 @@ -220,6 +225,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 @@ -358,6 +364,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 @@ -381,6 +388,7 @@ def __init__(
self.eos_token_ids = _normalize_eos_token_ids(eos_token_ids)
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 @@ -437,6 +445,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 @@ -789,11 +798,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 @@ -814,6 +826,7 @@ async def chat_engine_factory(
self.trust_remote_code,
template_force_reasoning,
chat_template,
default_thinking_mode,
),
)
futures = [
Expand Down Expand Up @@ -849,6 +862,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
Loading
Loading