Support GLM-4.7 function calling via structural tags (#28149)
Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com> Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com>
This commit is contained in:
co-authored by
Xinyuan Tong
Xinyuan Tong
parent
37ed10bd24
commit
f79a6b5c33
@@ -3,10 +3,15 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from functools import lru_cache
|
||||||
|
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
from sglang.srt.entrypoints.openai.protocol import Tool
|
from sglang.srt.entrypoints.openai.protocol import Tool, ToolChoice
|
||||||
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
from sglang.srt.function_call.base_format_detector import (
|
||||||
|
BaseFormatDetector,
|
||||||
|
StructuralTag,
|
||||||
|
get_model_structural_tag,
|
||||||
|
)
|
||||||
from sglang.srt.function_call.core_types import (
|
from sglang.srt.function_call.core_types import (
|
||||||
StreamingParseResult,
|
StreamingParseResult,
|
||||||
ToolCallItem,
|
ToolCallItem,
|
||||||
@@ -17,6 +22,21 @@ from sglang.srt.function_call.utils import infer_type_from_json_schema
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _glm47_native_structural_tag_available() -> bool:
|
||||||
|
# "glm_4_7" is only registered in newer xgrammar, so the import can succeed
|
||||||
|
# while the model name stays unknown. Probe once and fall back if absent.
|
||||||
|
if get_model_structural_tag is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
get_model_structural_tag(
|
||||||
|
model="glm_4_7", tools=[], tool_choice="auto", reasoning=False
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class StreamState(str, Enum):
|
class StreamState(str, Enum):
|
||||||
"""State machine states for XML to JSON streaming conversion."""
|
"""State machine states for XML to JSON streaming conversion."""
|
||||||
|
|
||||||
@@ -781,7 +801,22 @@ class Glm47MoeDetector(BaseFormatDetector):
|
|||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
def supports_structural_tag(self) -> bool:
|
def supports_structural_tag(self) -> bool:
|
||||||
return False
|
return _glm47_native_structural_tag_available()
|
||||||
|
|
||||||
|
def get_structural_tag(
|
||||||
|
self,
|
||||||
|
tools: Union[List[Tool], None] = None,
|
||||||
|
tool_choice: Union[ToolChoice, Literal["auto", "required"]] = "auto",
|
||||||
|
thinking_mode: bool = False,
|
||||||
|
) -> Optional[StructuralTag]:
|
||||||
|
if not self.supports_structural_tag():
|
||||||
|
return None
|
||||||
|
return super().get_structural_tag(
|
||||||
|
tools=tools, tool_choice=tool_choice, thinking_mode=thinking_mode
|
||||||
|
)
|
||||||
|
|
||||||
def structure_info(self) -> _GetInfoFunc:
|
def structure_info(self) -> _GetInfoFunc:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_structural_tag_name(self) -> str:
|
||||||
|
return "glm_4_7"
|
||||||
|
|||||||
@@ -3360,6 +3360,69 @@ class TestGlm47MoeDetector(unittest.TestCase):
|
|||||||
self.assertEqual(params["old_string"], " indented code")
|
self.assertEqual(params["old_string"], " indented code")
|
||||||
self.assertEqual(params["new_string"], " also indented")
|
self.assertEqual(params["new_string"], " also indented")
|
||||||
|
|
||||||
|
def test_get_model_structural_tag(self):
|
||||||
|
"""GLM-4.7/GLM-5 use xgrammar's native "glm_4_7" structural tag."""
|
||||||
|
import xgrammar as xgr
|
||||||
|
|
||||||
|
self.assertTrue(self.detector.supports_structural_tag())
|
||||||
|
self.assertEqual(self.detector.get_structural_tag_name(), "glm_4_7")
|
||||||
|
|
||||||
|
# thinking_mode=True keeps the </think> reasoning prefix.
|
||||||
|
structural_tag = self.detector.get_structural_tag(
|
||||||
|
self.tools, thinking_mode=True
|
||||||
|
)
|
||||||
|
self.assertIsInstance(structural_tag, xgr.StructuralTag)
|
||||||
|
self.assertIsInstance(
|
||||||
|
xgr.Grammar.from_structural_tag(structural_tag), xgr.Grammar
|
||||||
|
)
|
||||||
|
serialized = structural_tag.model_dump_json()
|
||||||
|
self.assertIn("glm_xml", serialized)
|
||||||
|
self.assertIn("<tool_call>", serialized)
|
||||||
|
self.assertIn("</think>", serialized)
|
||||||
|
|
||||||
|
# thinking_mode=False drops the reasoning prefix (ReasonerGrammarBackend
|
||||||
|
# owns </think> when --reasoning-parser is configured).
|
||||||
|
structural_tag = self.detector.get_structural_tag(
|
||||||
|
self.tools, thinking_mode=False
|
||||||
|
)
|
||||||
|
self.assertIsInstance(structural_tag, xgr.StructuralTag)
|
||||||
|
self.assertIsInstance(
|
||||||
|
xgr.Grammar.from_structural_tag(structural_tag), xgr.Grammar
|
||||||
|
)
|
||||||
|
self.assertNotEqual("sequence", structural_tag.model_dump()["format"]["type"])
|
||||||
|
|
||||||
|
# tool_choice="required" must still compile to a grammar.
|
||||||
|
structural_tag = self.detector.get_structural_tag(
|
||||||
|
self.tools, thinking_mode=True, tool_choice="required"
|
||||||
|
)
|
||||||
|
self.assertIsInstance(structural_tag, xgr.StructuralTag)
|
||||||
|
self.assertIsInstance(
|
||||||
|
xgr.Grammar.from_structural_tag(structural_tag), xgr.Grammar
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_required_tool_choice_falls_back_when_native_tag_is_unavailable(self):
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from sglang.srt.function_call.function_call_parser import FunctionCallParser
|
||||||
|
from sglang.srt.function_call.glm47_moe_detector import (
|
||||||
|
_glm47_native_structural_tag_available,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"sglang.srt.function_call.glm47_moe_detector.get_model_structural_tag",
|
||||||
|
None,
|
||||||
|
):
|
||||||
|
_glm47_native_structural_tag_available.cache_clear()
|
||||||
|
self.assertFalse(self.detector.supports_structural_tag())
|
||||||
|
self.assertIsNone(self.detector.get_structural_tag(self.tools))
|
||||||
|
|
||||||
|
parser = FunctionCallParser(self.tools, "glm47")
|
||||||
|
constraint = parser.get_structure_constraint("required")
|
||||||
|
|
||||||
|
self.assertIsNotNone(constraint)
|
||||||
|
self.assertEqual("json_schema", constraint[0])
|
||||||
|
_glm47_native_structural_tag_available.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
class TestJsonArrayParser(unittest.TestCase):
|
class TestJsonArrayParser(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user