Fix tool call constrained decoding and parsing for models with native formats (#21593)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user