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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ dynamo-memory = { path = "lib/memory", version = "1.6.0" }
dynamo-mocker = { path = "lib/mocker", version = "1.6.0" }
dynamo-kv-router = { path = "lib/kv-router", version = "1.6.0" }
dynamo-protocols = { version = "=6.1.0" }
dynamo-parsers = { version = "9.2.6" }
dynamo-parsers = { version = "=9.2.7" }

# Published on crates.io. To see all available versions:
# web: https://crates.io/crates/dynamo-parsers-v2/versions
Expand Down
23 changes: 13 additions & 10 deletions components/src/dynamo/common/configuration/groups/runtime_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class DynamoRuntimeConfig(ConfigBase):
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"
dyn_enable_structural_tag: bool = True
dyn_structural_tag_scope: str = "always"
dyn_structural_tag_schema: str = "auto"
custom_jinja_template: Optional[str] = None
endpoint_types: str
Expand Down Expand Up @@ -293,16 +293,18 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
g,
flag_name="--dyn-enable-structural-tag",
env_var="DYN_ENABLE_STRUCTURAL_TAG",
default=False,
help="Enable structural tag guided decoding for tool calls. "
"Named Kimi K3 tool_choice requests always activate their required "
"XTML structural tag even when this flag is off.",
default=True,
help="Enable structural tag guided decoding for tool calls when the configured "
"parser and backend support it. Disabling this flag suppresses optional "
"guidance; Rust retains native tags for Kimi K2 required/named and "
"Kimi K3 named tool choices. Python vLLM and SGLang respect the opt-out. "
"Unsupported schema constructs can cause backend grammar compilation errors.",
)
add_argument(
g,
flag_name="--dyn-structural-tag-scope",
env_var="DYN_STRUCTURAL_TAG_SCOPE",
default="auto",
default="always",
choices=["auto", "always"],
help="Controls when structural tags are activated. "
"'auto': for required/named tool_choice, and if any tool has strict=true "
Expand All @@ -317,9 +319,10 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
default="auto",
choices=["auto", "strict"],
help="Controls parameter schema strictness inside structural tags. "
"'auto': real schema unless the tool explicitly sets strict=false; "
"strict=false tools are syntactically constrained but schema-unconstrained. "
"'strict': real parameter schema for all tools.",
"'auto': declared parameter schema for tools with strict omitted or true; "
"tools with strict=false keep the native tool envelope constrained but use "
"schema-unconstrained arguments. "
"'strict': declared parameter schema for all tools, overriding strict=false.",
)
add_argument(
g,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,35 @@ def _parse_runtime_args(argv: list[str]) -> tuple[DynamoRuntimeConfig, str]:
return config, parser.format_help()


def test_structural_tags_default_on_for_supported_parsers(monkeypatch):
monkeypatch.delenv("DYN_ENABLE_STRUCTURAL_TAG", raising=False)
monkeypatch.delenv("DYN_STRUCTURAL_TAG_SCOPE", raising=False)
monkeypatch.delenv("DYN_STRUCTURAL_TAG_SCHEMA", raising=False)

config, _ = _parse_runtime_args([])

assert config.dyn_enable_structural_tag is True
assert config.dyn_structural_tag_scope == "always"
assert config.dyn_structural_tag_schema == "auto"


def test_structural_tag_schema_help_describes_omitted_strict(monkeypatch):
monkeypatch.delenv("DYN_STRUCTURAL_TAG_SCHEMA", raising=False)

_, help_text = _parse_runtime_args([])

assert "strict omitted or true" in help_text
assert "overriding strict=false" in help_text


def test_structural_tag_global_opt_out(monkeypatch):
monkeypatch.setenv("DYN_ENABLE_STRUCTURAL_TAG", "false")

config, _ = _parse_runtime_args([])

assert config.dyn_enable_structural_tag is False


def test_fpm_trace_defaults_disabled(monkeypatch):
monkeypatch.delenv("DYN_FPM_TRACE", raising=False)

Expand Down
81 changes: 69 additions & 12 deletions components/src/dynamo/frontend/prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,22 @@
from dynamo.frontend.vllm_protocol import DeltaFunctionCall, DeltaMessage, DeltaToolCall
from dynamo.llm.exceptions import InvalidArgument

from .structural_tag_policy import (
ToolChoiceKind,
effective_tool_strict,
should_attempt_structural_tag,
)
from .thinking import apply_default_thinking_mode_to_template_kwargs
from .utils import legacy_guided_decoding

get_model_structural_tag: Any
try:
from vllm.tool_parsers import structural_tag_registry
except ImportError: # Older supported vLLM releases do not expose this registry.
get_model_structural_tag = None
else:
get_model_structural_tag = structural_tag_registry.get_model_structural_tag

if TYPE_CHECKING:
from vllm.config import ModelConfig

Expand Down Expand Up @@ -204,35 +217,47 @@ def _should_build_tool_call_guidance(
# enforced guarantee. sglang_prepost.py has the same gap.
if tool_choice == "none":
return False
if _is_forced_tool_choice(tool_choice):
return True
if structural_tag_mode != "on":
return False
if tool_choice != "auto":
return False
if structural_tag_scope == "always":
return True
# An explicit single-call request is enough to attempt structural-tag
# guidance. Forced-choice JSON guidance also bounds its array schema below.
explicit_single_call = (
"parallel_tool_calls" in request.model_fields_set
and request.parallel_tool_calls is False
)
return explicit_single_call or any(_tool_is_strict(tool) for tool in request.tools)
tool_choice_kind: ToolChoiceKind = (
"required"
if tool_choice == "required"
else "named"
if _is_named_tool_choice(tool_choice)
else "auto"
if tool_choice == "auto"
else "other"
)
attempt_structural_tag = should_attempt_structural_tag(
mode=structural_tag_mode,
scope=structural_tag_scope,
tool_choice_kind=tool_choice_kind,
has_tools=True,
any_explicit_strict=any(_tool_is_strict(tool) for tool in request.tools),
parallel_tool_calls_explicitly_false=explicit_single_call,
)
# Forced choices retain the generic JSON fallback when structural tags are
# disabled or unavailable.
return attempt_structural_tag or _is_forced_tool_choice(tool_choice)


def _request_for_vllm_structural_tag(
request: ChatCompletionRequest,
*,
structural_tag_schema: str,
) -> ChatCompletionRequest:
strict_schema = structural_tag_schema == "strict"
tools = [
tool.model_copy(
update={
"function": tool.function.model_copy(
update={
"strict": True if strict_schema else tool.function.strict,
"strict": effective_tool_strict(
tool.function.strict, structural_tag_schema
),
}
)
}
Expand Down Expand Up @@ -264,7 +289,39 @@ def build_tool_call_guided_decoding(
request,
structural_tag_schema=structural_tag_schema,
)
structural_tag = tool_parser.get_structural_tag(request_for_tag)
try:
structural_tag_model = getattr(tool_parser, "structural_tag_model", None)
structural_tag_kwargs = {}
if structural_tag_model == "hy_v4" and get_model_structural_tag is not None:
# HYV4's parser detects checkpoint-specific tokens at construction.
# Keep its suffix while Dynamo owns activation of the registry path.
structural_tag_kwargs["token_suffix"] = getattr(
tool_parser, "_extractor"
).token_suffix
structural_tag = (
get_model_structural_tag(
model=structural_tag_model,
tools=request_for_tag.tools,
tool_choice=request_for_tag.tool_choice,
reasoning=False,
**structural_tag_kwargs,
)
if structural_tag_model is not None
and get_model_structural_tag is not None
else tool_parser.get_structural_tag(request_for_tag)
)
except (
AttributeError,
KeyError,
NotImplementedError,
RuntimeError,
TypeError,
ValueError,
):
# Parser implementations are third-party capability providers. A
# failed structural-tag build must preserve the pre-existing JSON
# or unconstrained compatibility path for the request.
structural_tag = None
if structural_tag is not None:
tag_value = (
structural_tag.model_dump()
Expand Down
Loading
Loading