@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 63cc8a55a9
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 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: