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,