A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 20:15 UTC
Fix GLM4 parser tests (#34905)
Signed-off-by: Robin Nabel <opensource@nabel.co> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
Robin Nabel committed
Feb 24, 2026 at 14:27 UTC
0de53339894ef2cef20512e31b4b8e0d83dcb6de
1 file changed
+67
-45
tests/tool_parsers/test_glm4_moe_tool_parser.py
+67
-45
@@ -1,19 +1,22 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-# ruff: noqa: E501
3
4
import json
5
+from unittest.mock import Mock
6
7
import pytest
8
9
-from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
9
+from vllm.entrypoints.openai.chat_completion.protocol import (
10
+ ChatCompletionRequest,
11
+ ChatCompletionToolsParam,
12
+ FunctionDefinition,
13
+)
14
from vllm.entrypoints.openai.engine.protocol import FunctionCall, ToolCall
15
from vllm.tokenizers import get_tokenizer
16
from vllm.tool_parsers.glm4_moe_tool_parser import (
17
Glm4MoeModelToolParser,
18
)
19
16
-pytest.skip("skip glm4_moe parser test", allow_module_level=True)
20
# Use a common model that is likely to be available
21
MODEL = "zai-org/GLM-4.5"
22
@@ -28,6 +31,20 @@ def glm4_moe_tool_parser(glm4_moe_tokenizer):
31
return Glm4MoeModelToolParser(glm4_moe_tokenizer)
32
33
34
+@pytest.fixture
35
+def mock_request() -> ChatCompletionRequest:
36
+ request = Mock(spec=ChatCompletionRequest)
37
+ request.tools = [ # GLM45 parser needs this attribute to enable tool parsing.
38
+ ChatCompletionToolsParam(
39
+ function=FunctionDefinition(
40
+ name="get_weather",
41
+ parameters={"city": {"type": "string"}},
42
+ ),
43
+ ),
44
+ ]
45
+ return request
46
+
47
+
48
def assert_tool_calls(
49
actual_tool_calls: list[ToolCall], expected_tool_calls: list[ToolCall]
50
):
@@ -47,10 +64,10 @@ def assert_tool_calls(
64
assert actual_args == expected_args
65
66
50
-def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
67
+def test_extract_tool_calls_no_tools(glm4_moe_tool_parser, mock_request):
68
model_output = "This is a test"
69
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
53
- model_output, request=None
70
+ model_output, request=mock_request
71
) # type: ignore[arg-type]
72
assert not extracted_tool_calls.tools_called
73
assert extracted_tool_calls.tool_calls == []
@@ -90,7 +107,7 @@ def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
107
)
108
)
109
],
93
- None,
110
+ "",
111
),
112
(
113
"""<tool_call>get_current_weather
@@ -135,7 +152,7 @@ def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
152
)
153
),
154
],
138
- None,
155
+ "",
156
),
157
(
158
"""I'll help you check the weather. <tool_call>get_current_weather
@@ -160,7 +177,7 @@ def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
177
)
178
)
179
],
163
- "I'll help you check the weather.",
180
+ "I'll help you check the weather. ",
181
),
182
(
183
"""<tool_call>get_current_weather
@@ -185,7 +202,7 @@ def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
202
)
203
)
204
],
188
- None,
205
+ "",
206
),
207
(
208
"""I will help you get the weather.<tool_call>get_weather
@@ -212,10 +229,14 @@ def test_extract_tool_calls_no_tools(glm4_moe_tool_parser):
229
],
230
)
231
def test_extract_tool_calls(
215
- glm4_moe_tool_parser, model_output, expected_tool_calls, expected_content
232
+ glm4_moe_tool_parser,
233
+ mock_request,
234
+ model_output,
235
+ expected_tool_calls,
236
+ expected_content,
237
):
238
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
218
- model_output, request=None
239
+ model_output, request=mock_request
240
) # type: ignore[arg-type]
241
assert extracted_tool_calls.tools_called
242
assert_tool_calls(extracted_tool_calls.tool_calls, expected_tool_calls)
@@ -223,7 +244,7 @@ def test_extract_tool_calls(
244
assert extracted_tool_calls.content == expected_content
245
246
226
-def test_extract_tool_calls_with_thinking_tags(glm4_moe_tool_parser):
247
+def test_extract_tool_calls_with_thinking_tags(glm4_moe_tool_parser, mock_request):
248
"""Test tool extraction when thinking tags are present."""
249
model_output = """<think>I want to get the weather.</think>
250
@@ -236,7 +257,7 @@ I will help you get the weather.
257
</tool_call>"""
258
259
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
239
- model_output, request=None
260
+ model_output, request=mock_request
261
) # type: ignore[arg-type]
262
263
assert extracted_tool_calls.tools_called
@@ -245,11 +266,12 @@ I will help you get the weather.
266
267
expected_content = """<think>I want to get the weather.</think>
268
248
-I will help you get the weather."""
269
+I will help you get the weather.
270
+"""
271
assert extracted_tool_calls.content == expected_content
272
273
252
-def test_extract_tool_calls_malformed_xml(glm4_moe_tool_parser):
274
+def test_extract_tool_calls_malformed_xml(glm4_moe_tool_parser, mock_request):
275
"""Test that malformed XML is handled gracefully."""
276
model_output = """<tool_call>get_weather
277
<arg_key>city</arg_key>
@@ -259,7 +281,7 @@ def test_extract_tool_calls_malformed_xml(glm4_moe_tool_parser):
281
</tool_call>"""
282
283
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
262
- model_output, request=None
284
+ model_output, request=mock_request
285
) # type: ignore[arg-type]
286
287
# Should handle malformed XML gracefully
@@ -269,13 +291,13 @@ def test_extract_tool_calls_malformed_xml(glm4_moe_tool_parser):
291
assert isinstance(extracted_tool_calls.tool_calls, list)
292
293
272
-def test_extract_tool_calls_empty_arguments(glm4_moe_tool_parser):
294
+def test_extract_tool_calls_empty_arguments(glm4_moe_tool_parser, mock_request):
295
"""Test tool calls with no arguments."""
296
model_output = """<tool_call>get_current_time
297
</tool_call>"""
298
299
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
278
- model_output, request=None
300
+ model_output, request=mock_request
301
) # type: ignore[arg-type]
302
303
assert extracted_tool_calls.tools_called
@@ -285,7 +307,7 @@ def test_extract_tool_calls_empty_arguments(glm4_moe_tool_parser):
307
assert extracted_tool_calls.tool_calls[0].function.arguments == "{}"
308
309
288
-def test_extract_tool_calls_mixed_content(glm4_moe_tool_parser):
310
+def test_extract_tool_calls_mixed_content(glm4_moe_tool_parser, mock_request):
311
"""Test extraction with mixed content and multiple tool calls."""
312
model_output = """I will help you get the weather info.
313
@@ -306,7 +328,7 @@ meaningwhile, I will also check the weather in Shanghai.
328
</tool_call>"""
329
330
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
309
- model_output, request=None
331
+ model_output, request=mock_request
332
) # type: ignore[arg-type]
333
334
assert extracted_tool_calls.tools_called
@@ -325,10 +347,10 @@ meaningwhile, I will also check the weather in Shanghai.
347
assert args2["date"] == "2025-08-01"
348
349
# Content should be everything before the first tool call
328
- assert extracted_tool_calls.content == "I will help you get the weather info."
350
+ assert extracted_tool_calls.content == "I will help you get the weather info.\n\n"
351
352
331
-def test_streaming_basic_functionality(glm4_moe_tool_parser):
353
+def test_streaming_basic_functionality(glm4_moe_tool_parser, mock_request):
354
"""Test basic streaming functionality."""
355
# Reset streaming state
356
glm4_moe_tool_parser.current_tool_name_sent = False
@@ -353,7 +375,7 @@ def test_streaming_basic_functionality(glm4_moe_tool_parser):
375
previous_token_ids=[],
376
current_token_ids=[tool_call_start_id, tool_call_end_id],
377
delta_token_ids=[tool_call_end_id],
356
- request=None,
378
+ request=mock_request,
379
)
380
381
# The result behavior depends on the streaming state
@@ -361,7 +383,7 @@ def test_streaming_basic_functionality(glm4_moe_tool_parser):
383
assert result is None or hasattr(result, "tool_calls") or hasattr(result, "content")
384
385
364
-def test_streaming_no_tool_calls(glm4_moe_tool_parser):
386
+def test_streaming_no_tool_calls(glm4_moe_tool_parser, mock_request):
387
"""Test streaming when there are no tool calls."""
388
current_text = "This is just regular text without any tool calls."
389
@@ -372,7 +394,7 @@ def test_streaming_no_tool_calls(glm4_moe_tool_parser):
394
previous_token_ids=[],
395
current_token_ids=[],
396
delta_token_ids=[],
375
- request=None,
397
+ request=mock_request,
398
)
399
400
# Should return the delta text as content
@@ -381,7 +403,7 @@ def test_streaming_no_tool_calls(glm4_moe_tool_parser):
403
assert result.content == " without any tool calls."
404
405
384
-def test_streaming_with_content_before_tool_calls(glm4_moe_tool_parser):
406
+def test_streaming_with_content_before_tool_calls(glm4_moe_tool_parser, mock_request):
407
"""Test streaming when there's content before tool calls."""
408
# Reset streaming state
409
glm4_moe_tool_parser.current_tool_name_sent = False
@@ -398,16 +420,16 @@ def test_streaming_with_content_before_tool_calls(glm4_moe_tool_parser):
420
previous_token_ids=[],
421
current_token_ids=[],
422
delta_token_ids=[],
401
- request=None,
423
+ request=mock_request,
424
)
425
426
# Should return content when no tool call tokens are detected
427
assert result is not None
428
assert hasattr(result, "content")
407
- assert result.content == "get the weather.<tool_call>"
429
+ assert result.content == "get the weather."
430
431
410
-def test_extract_tool_calls_special_characters(glm4_moe_tool_parser):
432
+def test_extract_tool_calls_special_characters(glm4_moe_tool_parser, mock_request):
433
"""Test tool calls with special characters and unicode."""
434
model_output = """<tool_call>send_message
435
<arg_key>recipient</arg_key>
@@ -419,7 +441,7 @@ def test_extract_tool_calls_special_characters(glm4_moe_tool_parser):
441
</tool_call>"""
442
443
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
422
- model_output, request=None
444
+ model_output, request=mock_request
445
) # type: ignore[arg-type]
446
447
assert extracted_tool_calls.tools_called
@@ -432,7 +454,7 @@ def test_extract_tool_calls_special_characters(glm4_moe_tool_parser):
454
assert args["priority"] == "high"
455
456
435
-def test_extract_tool_calls_incomplete_tool_call(glm4_moe_tool_parser):
457
+def test_extract_tool_calls_incomplete_tool_call(glm4_moe_tool_parser, mock_request):
458
"""Test incomplete tool calls (missing closing tag)."""
459
model_output = """<tool_call>get_weather
460
<arg_key>city</arg_key>
@@ -441,7 +463,7 @@ def test_extract_tool_calls_incomplete_tool_call(glm4_moe_tool_parser):
463
<arg_value>2025-08-01</arg_value>"""
464
465
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
444
- model_output, request=None
466
+ model_output, request=mock_request
467
) # type: ignore[arg-type]
468
469
# Incomplete tool calls should not be extracted
@@ -467,7 +489,7 @@ def _reset_streaming_state(parser):
489
parser._seen_keys = []
490
491
470
-def test_streaming_incremental_string_value(glm4_moe_tool_parser):
492
+def test_streaming_incremental_string_value(glm4_moe_tool_parser, mock_request):
493
"""Test incremental streaming of string argument values."""
494
_reset_streaming_state(glm4_moe_tool_parser)
495
@@ -492,7 +514,7 @@ def test_streaming_incremental_string_value(glm4_moe_tool_parser):
514
previous_token_ids=[],
515
current_token_ids=[],
516
delta_token_ids=[],
495
- request=None,
517
+ request=mock_request,
518
)
519
if result is not None and hasattr(result, "tool_calls") and result.tool_calls:
520
for tc in result.tool_calls:
@@ -516,7 +538,7 @@ def test_streaming_incremental_string_value(glm4_moe_tool_parser):
538
assert "get_weather" in combined or "name:get_weather" in combined
539
540
519
-def test_streaming_empty_tool_call(glm4_moe_tool_parser):
541
+def test_streaming_empty_tool_call(glm4_moe_tool_parser, mock_request):
542
"""Test that empty tool calls don't cause infinite loops."""
543
_reset_streaming_state(glm4_moe_tool_parser)
544
@@ -528,7 +550,7 @@ def test_streaming_empty_tool_call(glm4_moe_tool_parser):
550
previous_token_ids=[],
551
current_token_ids=[],
552
delta_token_ids=[],
531
- request=None,
553
+ request=mock_request,
554
)
555
556
# Should not hang and should return something (None or content)
@@ -538,7 +560,7 @@ def test_streaming_empty_tool_call(glm4_moe_tool_parser):
560
assert glm4_moe_tool_parser.current_tool_id == -1
561
562
541
-def test_streaming_prev_tool_call_arr_finalization(glm4_moe_tool_parser):
563
+def test_streaming_prev_tool_call_arr_finalization(glm4_moe_tool_parser, mock_request):
564
"""Test that prev_tool_call_arr contains parsed dict after tool call."""
565
_reset_streaming_state(glm4_moe_tool_parser)
566
@@ -558,7 +580,7 @@ def test_streaming_prev_tool_call_arr_finalization(glm4_moe_tool_parser):
580
previous_token_ids=[],
581
current_token_ids=[],
582
delta_token_ids=[],
561
- request=None,
583
+ request=mock_request,
584
)
585
586
# After the tool call completes, prev_tool_call_arr should have parsed dict
@@ -571,7 +593,7 @@ def test_streaming_prev_tool_call_arr_finalization(glm4_moe_tool_parser):
593
assert args.get("city") == "Beijing"
594
595
574
-def test_streaming_multiple_tool_calls_sequential(glm4_moe_tool_parser):
596
+def test_streaming_multiple_tool_calls_sequential(glm4_moe_tool_parser, mock_request):
597
"""Test streaming multiple sequential tool calls."""
598
_reset_streaming_state(glm4_moe_tool_parser)
599
@@ -595,7 +617,7 @@ def test_streaming_multiple_tool_calls_sequential(glm4_moe_tool_parser):
617
previous_token_ids=[],
618
current_token_ids=[],
619
delta_token_ids=[],
598
- request=None,
620
+ request=mock_request,
621
)
622
623
# Should have two tool calls in prev_tool_call_arr
@@ -604,7 +626,7 @@ def test_streaming_multiple_tool_calls_sequential(glm4_moe_tool_parser):
626
assert glm4_moe_tool_parser.prev_tool_call_arr[1]["arguments"]["city"] == "Shanghai"
627
628
607
-def test_streaming_json_escape_in_string(glm4_moe_tool_parser):
629
+def test_streaming_json_escape_in_string(glm4_moe_tool_parser, mock_request):
630
"""Test that special characters in string values are properly escaped."""
631
_reset_streaming_state(glm4_moe_tool_parser)
632
@@ -624,7 +646,7 @@ def test_streaming_json_escape_in_string(glm4_moe_tool_parser):
646
previous_token_ids=[],
647
current_token_ids=[],
648
delta_token_ids=[],
627
- request=None,
649
+ request=mock_request,
650
)
651
652
# The streamed_args_for_tool should contain valid JSON
@@ -691,7 +713,7 @@ if __name__ == "__main__":
713
},
714
}
715
],
694
- )
716
+ ) # type: ignore
717
718
# Simulate token-based streaming (special tags as single tokens)
719
chunks = [
@@ -746,7 +768,7 @@ if __name__ == "__main__":
768
assert "def bubble_sort" in parsed["content"]
769
770
749
-def test_extract_tool_calls_numeric_deserialization(glm4_moe_tool_parser):
771
+def test_extract_tool_calls_numeric_deserialization(glm4_moe_tool_parser, mock_request):
772
"""Test that numeric arguments are deserialized as numbers, not strings."""
773
model_output = """<tool_call>calculate
774
<arg_key>operation</arg_key>
@@ -760,7 +782,7 @@ def test_extract_tool_calls_numeric_deserialization(glm4_moe_tool_parser):
782
</tool_call>"""
783
784
extracted_tool_calls = glm4_moe_tool_parser.extract_tool_calls(
763
- model_output, request=None
785
+ model_output, request=mock_request
786
) # type: ignore[arg-type]
787
788
assert extracted_tool_calls.tools_called