@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 0de5333989
Fork 0

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