Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
22 changes: 12 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
Comment thread
indrajit96 marked this conversation as resolved.
Resolved
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,17 @@ 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,
Comment thread
indrajit96 marked this conversation as resolved.
Resolved
Comment thread
indrajit96 marked this conversation as resolved.
Resolved
help="Enable structural tag guided decoding for tool calls when the configured "
Comment thread
indrajit96 marked this conversation as resolved.
Resolved
"parser and backend support it. Explicitly disabling this flag is "
"authoritative for Rust, Python vLLM, and Python SGLang frontend "
"preprocessing.",
)
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 +318,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 only for tools with strict=true; "
"syntactically constrained but schema-unconstrained for all other tools. "
"'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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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
66 changes: 54 additions & 12 deletions components/src/dynamo/frontend/prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@
from dynamo.frontend.vllm_protocol import DeltaFunctionCall, DeltaMessage, DeltaToolCall
from dynamo.llm.exceptions import InvalidArgument

from .structural_tag_policy import effective_tool_strict, should_attempt_structural_tag
from .thinking import apply_default_thinking_mode_to_template_kwargs
from .utils import legacy_guided_decoding

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

if TYPE_CHECKING:
from vllm.config import ModelConfig

Expand Down Expand Up @@ -204,35 +210,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 = (
"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 +282,31 @@ 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 = (
get_model_structural_tag(
Comment thread
indrajit96 marked this conversation as resolved.
Resolved
model=structural_tag_model,
tools=request_for_tag.tools,
tool_choice=request_for_tag.tool_choice,
reasoning=False,
)
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