Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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
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,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,
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. 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)
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
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(
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,
**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