GLM-4.5 Model Support (#8224)
Co-authored-by: Lifu Huang <lifu.hlf@gmail.com> Co-authored-by: Binyao Jiang <byjiang1996@gmail.com> Co-authored-by: Stefan He <hebiaobuaa@gmail.com>
This commit is contained in:
@@ -43,6 +43,7 @@ class TestEnableThinking(CustomTestCase):
|
||||
"qwen3",
|
||||
],
|
||||
)
|
||||
cls.additional_chat_kwargs = {}
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@@ -59,6 +60,7 @@ class TestEnableThinking(CustomTestCase):
|
||||
"temperature": 0,
|
||||
"separate_reasoning": True,
|
||||
"chat_template_kwargs": {"enable_thinking": True},
|
||||
**self.additional_chat_kwargs,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -82,6 +84,7 @@ class TestEnableThinking(CustomTestCase):
|
||||
"temperature": 0,
|
||||
"separate_reasoning": True,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
**self.additional_chat_kwargs,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -107,6 +110,7 @@ class TestEnableThinking(CustomTestCase):
|
||||
"separate_reasoning": True,
|
||||
"stream": True,
|
||||
"chat_template_kwargs": {"enable_thinking": True},
|
||||
**self.additional_chat_kwargs,
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
@@ -151,6 +155,7 @@ class TestEnableThinking(CustomTestCase):
|
||||
"separate_reasoning": True,
|
||||
"stream": True,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
**self.additional_chat_kwargs,
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
@@ -184,5 +189,55 @@ class TestEnableThinking(CustomTestCase):
|
||||
)
|
||||
|
||||
|
||||
## Skip for ci test
|
||||
# class TestGLM45EnableThinking(TestEnableThinking):
|
||||
# @classmethod
|
||||
# def setUpClass(cls):
|
||||
# # Replace with the model name needed for testing; if not required, reuse DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
# cls.model = "THUDM/GLM-4.5"
|
||||
# cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
# cls.api_key = "sk-1234"
|
||||
# cls.process = popen_launch_server(
|
||||
# cls.model,
|
||||
# cls.base_url,
|
||||
# timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
# api_key=cls.api_key,
|
||||
# other_args=[
|
||||
# "--tool-call-parser",
|
||||
# "glm45",
|
||||
# "--reasoning-parser",
|
||||
# "glm45",
|
||||
# "--tp-size",
|
||||
# "8"
|
||||
# ],
|
||||
# )
|
||||
|
||||
# # Validate whether enable-thinking conflict with tool_calls
|
||||
# cls.additional_chat_kwargs = {
|
||||
# "tools": [
|
||||
# {
|
||||
# "type": "function",
|
||||
# "function": {
|
||||
# "name": "add",
|
||||
# "description": "Compute the sum of two numbers",
|
||||
# "parameters": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "a": {
|
||||
# "type": "int",
|
||||
# "description": "A number",
|
||||
# },
|
||||
# "b": {
|
||||
# "type": "int",
|
||||
# "description": "A number",
|
||||
# },
|
||||
# },
|
||||
# "required": ["a", "b"],
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -223,7 +223,10 @@ class TestOpenAIServerFunctionCalling(CustomTestCase):
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.SYSTEM_MESSAGE},
|
||||
{"role": "user", "content": "What is the temperature in Paris?"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the temperature in Paris in celsius??",
|
||||
},
|
||||
]
|
||||
|
||||
response_stream = client.chat.completions.create(
|
||||
@@ -910,5 +913,40 @@ class TestOpenAIPythonicFunctionCalling(CustomTestCase):
|
||||
)
|
||||
|
||||
|
||||
## Skip for ci test
|
||||
# class TestGLM45ServerFunctionCalling(TestOpenAIServerFunctionCalling):
|
||||
# @classmethod
|
||||
# def setUpClass(cls):
|
||||
# # Replace with the model name needed for testing; if not required, reuse DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
# cls.model = "THUDM/GLM-4.5"
|
||||
# cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
# cls.api_key = "sk-123456"
|
||||
|
||||
# # Start the local OpenAI Server. If necessary, you can add other parameters such as --enable-tools.
|
||||
# cls.process = popen_launch_server(
|
||||
# cls.model,
|
||||
# cls.base_url,
|
||||
# timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
# api_key=cls.api_key,
|
||||
# other_args=[
|
||||
# # If your server needs extra parameters to test function calling, please add them here.
|
||||
# "--tool-call-parser",
|
||||
# "glm45",
|
||||
# "--reasoning-parser",
|
||||
# "glm45",
|
||||
# "--tp-size",
|
||||
# "8"
|
||||
# ],
|
||||
# )
|
||||
# cls.base_url += "/v1"
|
||||
# cls.tokenizer = get_tokenizer(cls.model)
|
||||
|
||||
# # This test is too difficult for GLM4-moe. Skip it from the UT
|
||||
# def test_function_call_required(self):
|
||||
# pass
|
||||
|
||||
# def test_function_calling_multiturn(self):
|
||||
# self._test_function_calling_multiturn()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -6,6 +6,7 @@ from xgrammar import GrammarCompiler, TokenizerInfo
|
||||
from sglang.srt.entrypoints.openai.protocol import Function, Tool
|
||||
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
||||
from sglang.srt.function_call.deepseekv3_detector import DeepSeekV3Detector
|
||||
from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
|
||||
from sglang.srt.function_call.kimik2_detector import KimiK2Detector
|
||||
from sglang.srt.function_call.llama32_detector import Llama32Detector
|
||||
from sglang.srt.function_call.mistral_detector import MistralDetector
|
||||
@@ -510,6 +511,7 @@ class TestEBNFGeneration(unittest.TestCase):
|
||||
self.qwen25_detector = Qwen25Detector()
|
||||
self.qwen3_coder_detector = Qwen3CoderDetector()
|
||||
self.kimik2_detector = KimiK2Detector()
|
||||
self.glm45_detector = Glm4MoeDetector()
|
||||
|
||||
def test_pythonic_detector_ebnf(self):
|
||||
"""Test that the PythonicDetector generates valid EBNF."""
|
||||
@@ -622,6 +624,29 @@ class TestEBNFGeneration(unittest.TestCase):
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile EBNF: {e}")
|
||||
|
||||
def test_glm45_detector_ebnf(self):
|
||||
"""Test that the Glm4MoeDetector generates valid EBNF."""
|
||||
ebnf = self.glm45_detector.build_ebnf(self.tools)
|
||||
self.assertIsNotNone(ebnf)
|
||||
# Check that the EBNF contains expected patterns for XML format
|
||||
self.assertIn('"<tool_call>" function_call "</tool_call>"', ebnf)
|
||||
self.assertIn('"get_weather" "\\n" arguments_get_weather', ebnf)
|
||||
self.assertIn(
|
||||
'"<arg_key>location</arg_key>" "\\n" "<arg_value>" xml_text "</arg_value>" ( "\\n" ( "<arg_key>unit</arg_key>" "\\n" "<arg_value>" ("celsius" | "fahrenheit") "</arg_value>" ) )?',
|
||||
ebnf,
|
||||
)
|
||||
self.assertIn('"search" "\\n" arguments_search', ebnf)
|
||||
self.assertIn(
|
||||
'"<arg_key>query</arg_key>" "\\n" "<arg_value>" xml_text "</arg_value>"',
|
||||
ebnf,
|
||||
)
|
||||
# Validate that the EBNF can be compiled by GrammarCompiler
|
||||
try:
|
||||
ctx = self.grammar_compiler.compile_grammar(ebnf)
|
||||
self.assertIsNotNone(ctx, "EBNF should be valid and compile successfully")
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile EBNF: {e}")
|
||||
|
||||
def test_qwen3_coder_detector_ebnf(self):
|
||||
"""Test that the Qwen3CoderDetector generates valid EBNF."""
|
||||
ebnf = self.qwen3_coder_detector.build_ebnf(self.tools)
|
||||
@@ -1919,5 +1944,164 @@ circle
|
||||
self.assertEqual(params2["dimensions"], {"radius": 5})
|
||||
|
||||
|
||||
class TestGlm4MoeDetector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tools = [
|
||||
Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_weather",
|
||||
description="Get weather information",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string", "description": "City name"},
|
||||
"date": {"type": "string", "description": "Date"},
|
||||
},
|
||||
"required": ["city", "date"],
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
self.detector = Glm4MoeDetector()
|
||||
|
||||
def test_single_tool_call(self):
|
||||
text = (
|
||||
"<tool_call>get_weather\n"
|
||||
"<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n"
|
||||
"</tool_call>"
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].name, "get_weather")
|
||||
self.assertEqual(
|
||||
result.calls[0].parameters, '{"city": "Beijing", "date": "2024-06-27"}'
|
||||
)
|
||||
self.assertEqual(result.normal_text, "")
|
||||
|
||||
def test_multiple_tool_calls(self):
|
||||
text = (
|
||||
"<tool_call>get_weather\n"
|
||||
"<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n"
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n"
|
||||
"</tool_call>"
|
||||
"<tool_call>get_weather\n"
|
||||
"<arg_key>city</arg_key>\n<arg_value>Shanghai</arg_value>\n"
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-28</arg_value>\n"
|
||||
"</tool_call>"
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 2)
|
||||
self.assertEqual(result.calls[0].name, "get_weather")
|
||||
self.assertEqual(
|
||||
result.calls[0].parameters, '{"city": "Beijing", "date": "2024-06-27"}'
|
||||
)
|
||||
self.assertEqual(result.calls[1].name, "get_weather")
|
||||
self.assertEqual(
|
||||
result.calls[1].parameters, '{"city": "Shanghai", "date": "2024-06-28"}'
|
||||
)
|
||||
self.assertEqual(result.normal_text, "")
|
||||
|
||||
def test_streaming_tool_call(self):
|
||||
"""Test streaming incremental parsing of a tool call."""
|
||||
chunks = [
|
||||
"<tool_call>get_weather\n",
|
||||
"<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n",
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n",
|
||||
"</tool_call>",
|
||||
]
|
||||
tool_calls = []
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
for tool_call_chunk in result.calls:
|
||||
if (
|
||||
hasattr(tool_call_chunk, "tool_index")
|
||||
and tool_call_chunk.tool_index is not None
|
||||
):
|
||||
while len(tool_calls) <= tool_call_chunk.tool_index:
|
||||
tool_calls.append({"name": "", "parameters": {}})
|
||||
tc = tool_calls[tool_call_chunk.tool_index]
|
||||
if tool_call_chunk.name:
|
||||
tc["name"] = tool_call_chunk.name
|
||||
if tool_call_chunk.parameters:
|
||||
tc["parameters"] = tool_call_chunk.parameters
|
||||
self.assertEqual(len(tool_calls), 1)
|
||||
self.assertEqual(tool_calls[0]["name"], "get_weather")
|
||||
self.assertEqual(
|
||||
tool_calls[0]["parameters"], '{"city": "Beijing", "date": "2024-06-27"}'
|
||||
)
|
||||
|
||||
def test_streaming_multiple_tool_calls(self):
|
||||
"""Test streaming incremental parsing of multiple tool calls."""
|
||||
chunks = [
|
||||
"<tool_call>get_weather\n",
|
||||
"<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n",
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n",
|
||||
"</tool_call><tool_call>get_weather\n",
|
||||
"<arg_key>city</arg_key>\n<arg_value>Shanghai</arg_value>\n",
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-28</arg_value>\n",
|
||||
"</tool_call>",
|
||||
]
|
||||
tool_calls = []
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
for tool_call_chunk in result.calls:
|
||||
if (
|
||||
hasattr(tool_call_chunk, "tool_index")
|
||||
and tool_call_chunk.tool_index is not None
|
||||
):
|
||||
while len(tool_calls) <= tool_call_chunk.tool_index:
|
||||
tool_calls.append({"name": "", "parameters": {}})
|
||||
tc = tool_calls[tool_call_chunk.tool_index]
|
||||
if tool_call_chunk.name:
|
||||
tc["name"] = tool_call_chunk.name
|
||||
if tool_call_chunk.parameters:
|
||||
tc["parameters"] = tool_call_chunk.parameters
|
||||
self.assertEqual(len(tool_calls), 2)
|
||||
self.assertEqual(tool_calls[0]["name"], "get_weather")
|
||||
self.assertEqual(
|
||||
tool_calls[0]["parameters"], '{"city": "Beijing", "date": "2024-06-27"}'
|
||||
)
|
||||
self.assertEqual(tool_calls[1]["name"], "get_weather")
|
||||
self.assertEqual(
|
||||
tool_calls[1]["parameters"], '{"city": "Shanghai", "date": "2024-06-28"}'
|
||||
)
|
||||
|
||||
def test_tool_call_completion(self):
|
||||
"""Test that the buffer and state are reset after a tool call is completed."""
|
||||
chunks = [
|
||||
"<tool_call>get_weather\n",
|
||||
"<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n",
|
||||
"<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n",
|
||||
"</tool_call>",
|
||||
]
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
||||
self.assertEqual(self.detector.current_tool_id, 1)
|
||||
|
||||
def test_invalid_tool_call(self):
|
||||
"""Test that invalid tool calls are handled correctly."""
|
||||
text = "<tool_call>invalid_func\n<arg_key>city</arg_key>\n<arg_value>Beijing</arg_value>\n</tool_call>"
|
||||
result = self.detector.detect_and_parse(text, self.tools)
|
||||
self.assertEqual(len(result.calls), 0)
|
||||
|
||||
def test_partial_tool_call(self):
|
||||
"""Test parsing a partial tool call that spans multiple chunks."""
|
||||
text1 = "<tool_call>get_weather\n<arg_key>city</arg_key>\n"
|
||||
result1 = self.detector.parse_streaming_increment(text1, self.tools)
|
||||
self.assertEqual(result1.normal_text, "")
|
||||
self.assertEqual(result1.calls, [])
|
||||
self.assertEqual(self.detector._buffer, text1)
|
||||
text2 = "<arg_value>Beijing</arg_value>\n<arg_key>date</arg_key>\n<arg_value>2024-06-27</arg_value>\n</tool_call>"
|
||||
result2 = self.detector.parse_streaming_increment(text2, self.tools)
|
||||
self.assertEqual(len(result2.calls), 1)
|
||||
self.assertEqual(result2.calls[0].name, "get_weather")
|
||||
self.assertEqual(
|
||||
result2.calls[0].parameters, '{"city": "Beijing", "date": "2024-06-27"}'
|
||||
)
|
||||
self.assertEqual(self.detector._buffer, "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user