@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 57fef4e0bf
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, 09:15 UTC

[Refactor] Extract shared coerce_to_schema_type utility from Minimax M2 tool parser (#43006)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed May 18, 2026 at 17:55 UTC 57fef4e0bf0bfaddf117dfdc9367e1fb957b423f
3 files changed +247 -77
tests/tool_parsers/test_utils.py new
+148
@@ -0,0 +1,148 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +
6 +from vllm.tool_parsers.utils import coerce_to_schema_type
7 +
8 +
9 +class TestCoerceToSchemaType:
10 + class TestNullHandling:
11 + def test_null_converted_when_type_is_null(self):
12 + assert coerce_to_schema_type("null", "null") is None
13 +
14 + def test_null_converted_when_null_in_type_list(self):
15 + assert coerce_to_schema_type("null", ["string", "null"]) is None
16 +
17 + def test_null_preserved_as_string_when_type_is_string(self):
18 + assert coerce_to_schema_type("null", "string") == "null"
19 +
20 + def test_null_case_insensitive(self):
21 + assert coerce_to_schema_type("NULL", "null") is None
22 + assert coerce_to_schema_type("Null", "null") is None
23 +
24 + def test_none_string_never_converted(self):
25 + assert coerce_to_schema_type("none", "null") == "none"
26 + assert coerce_to_schema_type("none", "string") == "none"
27 + assert coerce_to_schema_type("none", ["string", "null"]) == "none"
28 +
29 + def test_nil_string_never_converted(self):
30 + assert coerce_to_schema_type("nil", "string") == "nil"
31 + assert coerce_to_schema_type("nil", ["string", "null"]) == "nil"
32 +
33 + def test_non_null_value_with_null_type(self):
34 + assert coerce_to_schema_type("hello", ["null", "string"]) == "hello"
35 +
36 + class TestStringType:
37 + def test_string_type(self):
38 + assert coerce_to_schema_type("hello", "string") == "hello"
39 +
40 + def test_str_alias(self):
41 + assert coerce_to_schema_type("hello", "str") == "hello"
42 +
43 + def test_text_alias(self):
44 + assert coerce_to_schema_type("hello", "text") == "hello"
45 +
46 + def test_varchar_alias(self):
47 + assert coerce_to_schema_type("hello", "varchar") == "hello"
48 +
49 + def test_char_alias(self):
50 + assert coerce_to_schema_type("x", "char") == "x"
51 +
52 + def test_enum_alias(self):
53 + assert coerce_to_schema_type("option_a", "enum") == "option_a"
54 +
55 + class TestIntegerType:
56 + def test_integer_type(self):
57 + assert coerce_to_schema_type("42", "integer") == 42
58 +
59 + def test_int_alias(self):
60 + assert coerce_to_schema_type("42", "int") == 42
61 +
62 + def test_negative_integer(self):
63 + assert coerce_to_schema_type("-7", "integer") == -7
64 +
65 + def test_invalid_integer_fallback(self):
66 + assert coerce_to_schema_type("not_a_number", "integer") == "not_a_number"
67 +
68 + def test_uint32_alias(self):
69 + assert coerce_to_schema_type("5", "uint32") == 5
70 +
71 + def test_long_alias(self):
72 + assert coerce_to_schema_type("100", "long") == 100
73 +
74 + class TestNumberType:
75 + def test_number_type(self):
76 + assert coerce_to_schema_type("3.14", "number") == 3.14
77 +
78 + def test_float_alias(self):
79 + assert coerce_to_schema_type("2.5", "float") == 2.5
80 +
81 + def test_double_alias(self):
82 + assert coerce_to_schema_type("2.5", "double") == 2.5
83 +
84 + def test_whole_float_returns_int(self):
85 + assert coerce_to_schema_type("5.0", "number") == 5
86 + assert isinstance(coerce_to_schema_type("5.0", "number"), int)
87 +
88 + def test_invalid_number_fallback(self):
89 + assert coerce_to_schema_type("abc", "number") == "abc"
90 +
91 + class TestBooleanType:
92 + def test_true(self):
93 + assert coerce_to_schema_type("true", "boolean") is True
94 +
95 + def test_false(self):
96 + assert coerce_to_schema_type("false", "boolean") is False
97 +
98 + def test_bool_alias(self):
99 + assert coerce_to_schema_type("true", "bool") is True
100 +
101 + def test_one_is_true(self):
102 + assert coerce_to_schema_type("1", "boolean") is True
103 +
104 + def test_zero_is_false(self):
105 + assert coerce_to_schema_type("0", "boolean") is False
106 +
107 + def test_invalid_boolean_fallback(self):
108 + assert coerce_to_schema_type("maybe", "boolean") == "maybe"
109 +
110 + class TestObjectArrayType:
111 + def test_object_type(self):
112 + assert coerce_to_schema_type('{"a": 1}', "object") == {"a": 1}
113 +
114 + def test_array_type(self):
115 + assert coerce_to_schema_type("[1, 2, 3]", "array") == [1, 2, 3]
116 +
117 + def test_invalid_json_fallback(self):
118 + assert coerce_to_schema_type("not json", "object") == "not json"
119 +
120 + def test_dict_alias(self):
121 + assert coerce_to_schema_type('{"k": "v"}', "dict") == {"k": "v"}
122 +
123 + def test_list_alias(self):
124 + assert coerce_to_schema_type("[1]", "list") == [1]
125 +
126 + class TestMultiType:
127 + def test_null_takes_priority_over_string(self):
128 + assert coerce_to_schema_type("null", ["string", "null"]) is None
129 +
130 + def test_integer_tried_before_string(self):
131 + assert coerce_to_schema_type("42", ["integer", "string"]) == 42
132 +
133 + def test_falls_through_to_string(self):
134 + assert coerce_to_schema_type("hello", ["integer", "string"]) == "hello"
135 +
136 + class TestFallback:
137 + def test_unknown_type_returns_string(self):
138 + assert coerce_to_schema_type("hello", "unknown_type") == "hello"
139 +
140 + def test_json_fallback_for_unknown_type(self):
141 + assert coerce_to_schema_type('{"a": 1}', "unknown_type") == {"a": 1}
142 +
143 + @pytest.mark.parametrize("schema_type", ["string", "str", "text"])
144 + def test_string_types_preserve_value(self, schema_type):
145 + assert coerce_to_schema_type("anything", schema_type) == "anything"
146 +
147 + def test_unrecognized_type_falls_back_to_json(self):
148 + assert coerce_to_schema_type("42", "interval") == 42
vllm/tool_parsers/minimax_m2_tool_parser.py
+2 -77
@@ -25,6 +25,7 @@ from vllm.tool_parsers.abstract_tool_parser import (
25 Tool,
26 ToolParser,
27 )
28 +from vllm.tool_parsers.utils import coerce_to_schema_type
29
30 logger = init_logger(__name__)
31
@@ -146,80 +147,6 @@ class MinimaxM2ToolParser(ToolParser):
147
148 return list(types)
149
149 - def _convert_param_value_with_types(
150 - self, value: str, param_types: list[str]
151 - ) -> Any:
152 - """
153 - Convert parameter value to the correct type based on a list of possible types.
154 - Tries each type in order until one succeeds.
155 -
156 - Args:
157 - value: The string value to convert
158 - param_types: List of possible type strings
159 -
160 - Returns:
161 - The converted value
162 - """
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)
167 - # Priority: null > integer > number > boolean > object > array > string
168 - type_priority = [
169 - "null",
170 - "integer",
171 - "int",
172 - "number",
173 - "float",
174 - "boolean",
175 - "bool",
176 - "object",
177 - "array",
178 - "string",
179 - "str",
180 - "text",
181 - ]
182 -
183 - for param_type in type_priority:
184 - if param_type not in normalized_types:
185 - continue
186 -
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:
195 - return int(value)
196 - except (ValueError, TypeError):
197 - continue
198 - elif param_type in ["number", "float"]:
199 - try:
200 - val = float(value)
201 - return val if val != int(val) else int(val)
202 - except (ValueError, TypeError):
203 - continue
204 - elif param_type in ["boolean", "bool"]:
205 - lower_val = value.lower().strip()
206 - if lower_val in ["true", "1", "yes", "on"]:
207 - return True
208 - elif lower_val in ["false", "0", "no", "off"]:
209 - return False
210 - continue
211 - elif param_type in ["object", "array"]:
212 - try:
213 - return json.loads(value)
214 - except json.JSONDecodeError:
215 - continue
216 -
217 - # Fallback: try JSON parse, then return as string
218 - try:
219 - return json.loads(value)
220 - except json.JSONDecodeError:
221 - return value
222 -
150 def _get_param_types_from_config(
151 self, param_name: str, param_config: dict
152 ) -> list[str]:
@@ -280,9 +207,7 @@ class MinimaxM2ToolParser(ToolParser):
207 param_type = self._get_param_types_from_config(param_name, param_config)
208
209 # Convert value
283 - param_dict[param_name] = self._convert_param_value_with_types(
284 - param_value, param_type
285 - )
210 + param_dict[param_name] = coerce_to_schema_type(param_value, param_type)
211
212 return ToolCall(
213 type="function",
vllm/tool_parsers/utils.py
+97
@@ -450,6 +450,103 @@ def make_valid_python(text: str) -> tuple[str, str] | None:
450 return candidate, added_text
451
452
453 +_TYPE_ALIASES: dict[str, str] = {
454 + "str": "string",
455 + "text": "string",
456 + "varchar": "string",
457 + "char": "string",
458 + "enum": "string",
459 + "int": "integer",
460 + "int32": "integer",
461 + "int64": "integer",
462 + "uint": "integer",
463 + "uint32": "integer",
464 + "uint64": "integer",
465 + "long": "integer",
466 + "short": "integer",
467 + "unsigned": "integer",
468 + "float": "number",
469 + "float32": "number",
470 + "float64": "number",
471 + "double": "number",
472 + "bool": "boolean",
473 + "dict": "object",
474 + "arr": "array",
475 + "list": "array",
476 + "sequence": "array",
477 +}
478 +
479 +
480 +def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any:
481 + """Best-effort coercion of a raw string value to a JSON Schema type.
482 +
483 + Tries each type in priority order (null > integer > number > boolean >
484 + object > array > string) and returns the first successful coercion.
485 + Falls back to the original string when no coercion succeeds.
486 +
487 + Args:
488 + value: The raw string value from the model output.
489 + schema_type: One or more JSON Schema type strings
490 + (e.g. ``"string"`` or ``["string", "null"]``).
491 + """
492 + if isinstance(schema_type, str):
493 + schema_type = [schema_type]
494 +
495 + normalized_types = {
496 + _TYPE_ALIASES.get(key, key) for t in schema_type for key in [t.strip().lower()]
497 + }
498 +
499 + # Priority: null > integer > number > boolean > object > array > string
500 + type_priority = [
501 + "null",
502 + "integer",
503 + "number",
504 + "boolean",
505 + "object",
506 + "array",
507 + "string",
508 + ]
509 +
510 + for candidate_type in type_priority:
511 + if candidate_type not in normalized_types:
512 + continue
513 +
514 + if candidate_type == "null":
515 + if value.lower() == "null":
516 + return None
517 + continue
518 + if candidate_type == "string":
519 + return value
520 + if candidate_type == "integer":
521 + try:
522 + return int(value)
523 + except (ValueError, TypeError):
524 + continue
525 + if candidate_type == "number":
526 + try:
527 + val = float(value)
528 + return val if val != int(val) else int(val)
529 + except (ValueError, TypeError):
530 + continue
531 + if candidate_type == "boolean":
532 + lower_val = value.lower().strip()
533 + if lower_val in ("true", "1"):
534 + return True
535 + if lower_val in ("false", "0"):
536 + return False
537 + continue
538 + if candidate_type in ("object", "array"):
539 + try:
540 + return json.loads(value)
541 + except (json.JSONDecodeError, ValueError, TypeError):
542 + continue
543 +
544 + try:
545 + return json.loads(value)
546 + except (json.JSONDecodeError, ValueError):
547 + return value
548 +
549 +
550 def compute_tool_delta(
551 previously_sent_args: str,
552 new_call: ToolCall,