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 3, 17:45 UTC
fix(tool-parser): preserve "none"/"nil" strings as valid enum values in minimax_m2 (#39599)
Signed-off-by: Yiyang Liu <yiyangliu@microsoft.com> Signed-off-by: Yiyang Liu <37043548+ianliuy@users.noreply.github.com> Signed-off-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: Roger Wang <hey@rogerw.io>
Yiyang "Ian" Liu committed
May 13, 2026 at 17:35 UTC
63cc8a55a97f66077d8c43d62498db72eb4c52e7
2 files changed
+137
-6
tests/tool_parsers/test_minimax_m2_tool_parser.py
+130
@@ -548,3 +548,133 @@ class TestAnyOfNullableParam:
548
parsed = json.loads(tc[0]["arguments"])
549
assert parsed["config"] == {"theme": "dark", "fontSize": 14}
550
assert isinstance(parsed["config"], dict)
551
+
552
+
553
+class TestNoneStringPreservation:
554
+ """Regression tests for #39567: 'none' as a string must not become None."""
555
+
556
+ def test_none_string_preserved_in_enum(self):
557
+ """'none' in an enum must stay as the string 'none', not Python None."""
558
+ tools = [
559
+ ChatCompletionToolsParam(
560
+ function=FunctionDefinition(
561
+ name="set_theme",
562
+ parameters={
563
+ "type": "object",
564
+ "properties": {
565
+ "theme": {
566
+ "type": "string",
567
+ "enum": ["dark", "light", "none"],
568
+ },
569
+ },
570
+ },
571
+ ),
572
+ )
573
+ ]
574
+ parser = MinimaxM2ToolParser(FakeTokenizer(), tools=tools)
575
+
576
+ results = _feed(
577
+ parser,
578
+ [
579
+ '<minimax:tool_call><invoke name="set_theme">'
580
+ '<parameter name="theme">none</parameter>'
581
+ "</invoke></minimax:tool_call>",
582
+ ],
583
+ )
584
+ tc = _collect_tool_calls(results)
585
+ assert len(tc) == 1
586
+ parsed = json.loads(tc[0]["arguments"])
587
+ assert parsed["theme"] == "none"
588
+ assert parsed["theme"] is not None
589
+
590
+ def test_none_string_preserved_plain_string(self):
591
+ """'none' as a plain string param must stay as 'none'."""
592
+ tools = [
593
+ ChatCompletionToolsParam(
594
+ function=FunctionDefinition(
595
+ name="echo",
596
+ parameters={
597
+ "type": "object",
598
+ "properties": {
599
+ "message": {"type": "string"},
600
+ },
601
+ },
602
+ ),
603
+ )
604
+ ]
605
+ parser = MinimaxM2ToolParser(FakeTokenizer(), tools=tools)
606
+
607
+ results = _feed(
608
+ parser,
609
+ [
610
+ '<minimax:tool_call><invoke name="echo">'
611
+ '<parameter name="message">none</parameter>'
612
+ "</invoke></minimax:tool_call>",
613
+ ],
614
+ )
615
+ tc = _collect_tool_calls(results)
616
+ assert len(tc) == 1
617
+ parsed = json.loads(tc[0]["arguments"])
618
+ assert parsed["message"] == "none"
619
+
620
+ def test_null_still_converts_to_none(self):
621
+ """'null' in a nullable param must still become Python None."""
622
+ tools = [
623
+ ChatCompletionToolsParam(
624
+ function=FunctionDefinition(
625
+ name="update_profile",
626
+ parameters={
627
+ "type": "object",
628
+ "properties": {
629
+ "nickname": {
630
+ "anyOf": [{"type": "string"}, {"type": "null"}],
631
+ },
632
+ },
633
+ },
634
+ ),
635
+ )
636
+ ]
637
+ parser = MinimaxM2ToolParser(FakeTokenizer(), tools=tools)
638
+
639
+ results = _feed(
640
+ parser,
641
+ [
642
+ '<minimax:tool_call><invoke name="update_profile">'
643
+ '<parameter name="nickname">null</parameter>'
644
+ "</invoke></minimax:tool_call>",
645
+ ],
646
+ )
647
+ tc = _collect_tool_calls(results)
648
+ assert len(tc) == 1
649
+ parsed = json.loads(tc[0]["arguments"])
650
+ assert parsed["nickname"] is None
651
+
652
+ def test_nil_string_preserved(self):
653
+ """'nil' must stay as the string 'nil', not become None."""
654
+ tools = [
655
+ ChatCompletionToolsParam(
656
+ function=FunctionDefinition(
657
+ name="echo",
658
+ parameters={
659
+ "type": "object",
660
+ "properties": {
661
+ "value": {"type": "string"},
662
+ },
663
+ },
664
+ ),
665
+ )
666
+ ]
667
+ parser = MinimaxM2ToolParser(FakeTokenizer(), tools=tools)
668
+
669
+ results = _feed(
670
+ parser,
671
+ [
672
+ '<minimax:tool_call><invoke name="echo">'
673
+ '<parameter name="value">nil</parameter>'
674
+ "</invoke></minimax:tool_call>",
675
+ ],
676
+ )
677
+ tc = _collect_tool_calls(results)
678
+ assert len(tc) == 1
679
+ parsed = json.loads(tc[0]["arguments"])
680
+ assert parsed["value"] == "nil"
vllm/tool_parsers/minimax_m2_tool_parser.py
+7
-6
@@ -160,16 +160,13 @@ class MinimaxM2ToolParser(ToolParser):
160
Returns:
161
The converted value
162
"""
163
- # Check if the VALUE itself indicates null (not just if null is allowed)
164
- if value.lower() in ("null", "none", "nil"):
165
- return None
166
-
163
# Normalize types
164
normalized_types = [t.lower() for t in param_types]
165
166
# Try each type in order of preference (most specific first, string as fallback)
171
- # Priority: integer > number > boolean > object > array > string
167
+ # Priority: null > integer > number > boolean > object > array > string
168
type_priority = [
169
+ "null",
170
"integer",
171
"int",
172
"number",
@@ -187,7 +184,11 @@ class MinimaxM2ToolParser(ToolParser):
184
if param_type not in normalized_types:
185
continue
186
190
- if param_type in ["string", "str", "text"]:
187
+ if param_type == "null":
188
+ if value.lower() == "null":
189
+ return None
190
+ continue
191
+ elif param_type in ["string", "str", "text"]:
192
return value
193
elif param_type in ["integer", "int"]:
194
try: