Fix tool call constrained decoding and parsing for models with native formats (#21593)

This commit is contained in:
Xinyuan Tong
2026-04-10 20:37:23 -07:00
committed by GitHub
parent c2821dfbe9
commit 7c6db40540
9 changed files with 306 additions and 61 deletions
@@ -270,7 +270,7 @@ class BaseFormatDetector(ABC):
cur_arguments = current_tool_call.get("arguments")
res = StreamingParseResult()
if cur_arguments:
if cur_arguments is not None:
# Calculate how much of the arguments we've already streamed
sent = len(self.streamed_args_for_tool[self.current_tool_id])
cur_args_json = json.dumps(cur_arguments, ensure_ascii=False)
@@ -203,7 +203,9 @@ class DeepSeekV3Detector(BaseFormatDetector):
def structure_info(self) -> _GetInfoFunc:
return lambda name: StructureInfo(
begin=">" + name + "\n```json\n",
end="\n```<",
trigger=">" + name + "\n```json\n",
begin="<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>"
+ name
+ "\n```json\n",
end="\n```<|tool▁call▁end|><|tool▁calls▁end|>",
trigger="<|tool▁calls▁begin|>",
)
@@ -3,6 +3,7 @@ from typing import Dict, List, Literal, Optional, Set, Tuple, Type, Union
from sglang.srt.entrypoints.openai.protocol import (
LegacyStructuralTagResponseFormat,
StructuralTagResponseFormat,
StructuresResponseFormat,
Tool,
ToolCallConstraint,
@@ -32,7 +33,10 @@ from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
from sglang.srt.function_call.qwen25_detector import Qwen25Detector
from sglang.srt.function_call.step3_detector import Step3Detector
from sglang.srt.function_call.trinity_detector import TrinityDetector
from sglang.srt.function_call.utils import get_json_schema_constraint
from sglang.srt.function_call.utils import (
_get_tool_schema_defs,
get_json_schema_constraint,
)
logger = logging.getLogger(__name__)
@@ -146,12 +150,24 @@ class FunctionCallParser:
return final_normal_text, final_calls
def get_structure_tag(self) -> LegacyStructuralTagResponseFormat:
def get_structure_tag(
self, at_least_one: bool = False
) -> StructuralTagResponseFormat:
"""
Generate a structural tag response format for all available tools.
This creates the necessary structural tags that guide the model's output format.
Args:
at_least_one: If True, the grammar forces at least one tool call
(no free text allowed). Used for required/named tool_choice.
Raises:
ValueError: If tools have conflicting $defs schemas.
"""
# Validate $defs consistency before building structural tags
_get_tool_schema_defs(self.tools)
tool_structures: List[StructuresResponseFormat] = list()
tool_trigger_set: Set[str] = set()
@@ -183,6 +199,7 @@ class FunctionCallParser:
type="structural_tag",
structures=tool_structures,
triggers=list(tool_trigger_set),
at_least_one=at_least_one,
)
def get_structure_constraint(
@@ -203,16 +220,23 @@ class FunctionCallParser:
"""
# NOTE: structural_tag only supports JSON-compatible content between the begin and end.
# It cannot parse or validate function call Pythonic or XML-ish syntax.
if (
self.detector.supports_structural_tag()
and tool_choice == "auto"
and (
any(tool.function.strict for tool in self.tools)
or self.tool_strict_level >= ToolStrictLevel.FUNCTION
if self.detector.supports_structural_tag():
# For "required"/named: always use structural_tag to preserve the
# model's native tool call format. Schema is only included when
# strict=True, per OpenAI protocol semantics.
# For "auto": only constrain when strict is enabled.
is_required = tool_choice == "required" or isinstance(
tool_choice, ToolChoice
)
):
tag = self.get_structure_tag()
return ("structural_tag", tag)
if is_required or (
tool_choice == "auto"
and (
any(tool.function.strict for tool in self.tools)
or self.tool_strict_level >= ToolStrictLevel.FUNCTION
)
):
tag = self.get_structure_tag(at_least_one=is_required)
return ("structural_tag", tag)
elif tool_choice == "required" or isinstance(tool_choice, ToolChoice):
json_schema = get_json_schema_constraint(
self.tools, tool_choice, parallel_tool_calls=parallel_tool_calls