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 4, 05:00 UTC
[Bugfix] DeepSeekV32/v4: respect string='true|false' attribute andunwrap arguments/input wrapper (#41801)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com> Co-authored-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Chauncey committed
May 7, 2026 at 05:48 UTC
95582868efd4db0b120e3640bbc61dcfce20d59f
3 files changed
+224
-10
tests/tool_parsers/test_deepseekv32_tool_parser.py
+155
-2
@@ -203,7 +203,14 @@ class TestExtractToolCalls:
203
),
204
)
205
parser = make_parser(tools=[tool])
206
- model_output = build_tool_call("toggle", {"enabled": "true", "count": "42"})
206
+ model_output = (
207
+ f"{FC_START}\n"
208
+ f'{INV_START}toggle">\n'
209
+ f'{PARAM_START}enabled" string="false">true{PARAM_END}\n'
210
+ f'{PARAM_START}count" string="false">42{PARAM_END}\n'
211
+ f"{INV_END}\n"
212
+ f"{FC_END}"
213
+ )
214
result = parser.extract_tool_calls(model_output, None)
215
assert result.tools_called
216
assert len(result.tool_calls) == 1
@@ -212,6 +219,118 @@ class TestExtractToolCalls:
219
assert isinstance(args["enabled"], bool)
220
assert isinstance(args["count"], int)
221
222
+ def test_string_attr_true_preserves_literal_despite_schema(self):
223
+ """string="true" must keep the value as a string even
224
+ if the schema says integer."""
225
+ tool = ChatCompletionToolsParam(
226
+ function=FunctionDefinition(
227
+ name="score",
228
+ parameters={
229
+ "type": "object",
230
+ "properties": {
231
+ "value": {"type": "integer"},
232
+ },
233
+ },
234
+ ),
235
+ )
236
+ parser = make_parser(tools=[tool])
237
+ model_output = (
238
+ f"{FC_START}\n"
239
+ f'{INV_START}score">\n'
240
+ f'{PARAM_START}value" string="true">42{PARAM_END}\n'
241
+ f"{INV_END}\n"
242
+ f"{FC_END}"
243
+ )
244
+ result = parser.extract_tool_calls(model_output, None)
245
+ assert result.tools_called
246
+ args = json.loads(result.tool_calls[0].function.arguments)
247
+ assert args == {"value": "42"}
248
+ assert isinstance(args["value"], str)
249
+
250
+ def test_string_attr_false_allows_schema_conversion(self):
251
+ """string="false" allows the parser to convert via the tool schema."""
252
+ tool = ChatCompletionToolsParam(
253
+ function=FunctionDefinition(
254
+ name="score",
255
+ parameters={
256
+ "type": "object",
257
+ "properties": {
258
+ "value": {"type": "integer"},
259
+ },
260
+ },
261
+ ),
262
+ )
263
+ parser = make_parser(tools=[tool])
264
+ model_output = (
265
+ f"{FC_START}\n"
266
+ f'{INV_START}score">\n'
267
+ f'{PARAM_START}value" string="false">42{PARAM_END}\n'
268
+ f"{INV_END}\n"
269
+ f"{FC_END}"
270
+ )
271
+ result = parser.extract_tool_calls(model_output, None)
272
+ assert result.tools_called
273
+ args = json.loads(result.tool_calls[0].function.arguments)
274
+ assert args == {"value": 42}
275
+ assert isinstance(args["value"], int)
276
+
277
+ def test_arguments_wrapper_repaired(self):
278
+ """A single 'arguments' wrapper parameter must be unwrapped when it
279
+ is not part of the tool schema and the inner object matches schema fields."""
280
+ tool = ChatCompletionToolsParam(
281
+ function=FunctionDefinition(
282
+ name="get_weather",
283
+ parameters={
284
+ "type": "object",
285
+ "properties": {
286
+ "location": {"type": "string"},
287
+ },
288
+ },
289
+ ),
290
+ )
291
+ parser = make_parser(tools=[tool])
292
+ model_output = (
293
+ f"{FC_START}\n"
294
+ f'{INV_START}get_weather">\n'
295
+ f'{PARAM_START}arguments" string="false">'
296
+ f'{{"location":"Beijing"}}'
297
+ f"{PARAM_END}\n"
298
+ f"{INV_END}\n"
299
+ f"{FC_END}"
300
+ )
301
+ result = parser.extract_tool_calls(model_output, None)
302
+ assert result.tools_called
303
+ args = json.loads(result.tool_calls[0].function.arguments)
304
+ assert args == {"location": "Beijing"}
305
+
306
+ def test_input_wrapper_repaired(self):
307
+ """A single 'input' wrapper parameter must be unwrapped similarly."""
308
+ tool = ChatCompletionToolsParam(
309
+ function=FunctionDefinition(
310
+ name="get_weather",
311
+ parameters={
312
+ "type": "object",
313
+ "properties": {
314
+ "location": {"type": "string"},
315
+ },
316
+ },
317
+ ),
318
+ )
319
+ parser = make_parser(tools=[tool])
320
+ model_output = (
321
+ f"{FC_START}\n"
322
+ f'{INV_START}get_weather">\n'
323
+ f'{PARAM_START}input" string="true">'
324
+ f'{{"location":"Beijing"}}'
325
+ f"{PARAM_END}\n"
326
+ f"{INV_END}\n"
327
+ f"{FC_END}"
328
+ )
329
+ result = parser.extract_tool_calls(model_output, None)
330
+ assert result.tools_called
331
+ args = json.loads(result.tool_calls[0].function.arguments)
332
+ assert args == {"location": "Beijing"}
333
+
334
335
# ---------------------------------------------------------------------------
336
# Tests: extract_tool_calls_streaming
@@ -319,11 +438,45 @@ class TestExtractToolCallsStreaming:
438
),
439
)
440
parser = make_parser(tools=[tool])
322
- full_text = build_tool_call("add", {"x": "3", "y": "4"})
441
+ full_text = (
442
+ f"{FC_START}\n"
443
+ f'{INV_START}add">\n'
444
+ f'{PARAM_START}x" string="false">3{PARAM_END}\n'
445
+ f'{PARAM_START}y" string="false">4{PARAM_END}\n'
446
+ f"{INV_END}\n"
447
+ f"{FC_END}"
448
+ )
449
deltas = self._stream(parser, full_text)
450
args_str = self._reconstruct_args(deltas)
451
assert json.loads(args_str) == {"x": 3, "y": 4}
452
453
+ def test_string_attr_true_preserves_literal_in_streaming(self):
454
+ """Streaming: string='true' must keep the value literal despite schema."""
455
+ tool = ChatCompletionToolsParam(
456
+ function=FunctionDefinition(
457
+ name="score",
458
+ parameters={
459
+ "type": "object",
460
+ "properties": {
461
+ "value": {"type": "integer"},
462
+ },
463
+ },
464
+ ),
465
+ )
466
+ parser = make_parser(tools=[tool])
467
+ full_text = (
468
+ f"{FC_START}\n"
469
+ f'{INV_START}score">\n'
470
+ f'{PARAM_START}value" string="true">42{PARAM_END}\n'
471
+ f"{INV_END}\n"
472
+ f"{FC_END}"
473
+ )
474
+ deltas = self._stream(parser, full_text)
475
+ args_str = self._reconstruct_args(deltas)
476
+ args = json.loads(args_str)
477
+ assert args == {"value": "42"}
478
+ assert isinstance(args["value"], str)
479
+
480
def test_multiple_tools_streaming(self, parser):
481
full_text = (
482
f"{FC_START}\n"
tests/tool_parsers/test_deepseekv4_tool_parser.py
+33
@@ -203,3 +203,36 @@ def test_get_vllm_registry_structural_tag_returns_structural_tag(
203
)
204
tag = parser.get_structural_tag(req)
205
assert isinstance(tag, StructuralTag)
206
+
207
+
208
+def test_extract_tool_calls_arguments_wrapper():
209
+ mock_tokenizer = MagicMock()
210
+ mock_tokenizer.get_vocab.return_value = {}
211
+
212
+ tool = ChatCompletionToolsParam(
213
+ type="function",
214
+ function={
215
+ "name": "get_weather",
216
+ "parameters": {
217
+ "type": "object",
218
+ "properties": {"location": {"type": "string"}},
219
+ },
220
+ },
221
+ )
222
+
223
+ parser = DeepSeekV4ToolParser(mock_tokenizer, tools=[tool])
224
+ request = MagicMock()
225
+ request.tools = [tool]
226
+
227
+ model_output = (
228
+ f"{TC_START}"
229
+ f'{INV_START}get_weather">'
230
+ f'{PARAM_START}arguments" string="false">{{"location":"Beijing"}}{PARAM_END}'
231
+ f"{INV_END}"
232
+ f"{TC_END}"
233
+ )
234
+
235
+ result = parser.extract_tool_calls(model_output, request)
236
+ assert result.tools_called
237
+ args = json.loads(result.tool_calls[0].function.arguments)
238
+ assert args == {"location": "Beijing"}
vllm/tool_parsers/deepseekv32_tool_parser.py
+36
-8
@@ -69,7 +69,7 @@ class DeepSeekV32ToolParser(ToolParser):
69
r'<|DSML|invoke\s+name="([^"]+)"\s*>(.*?)</|DSML|invoke>', re.DOTALL
70
)
71
self.parameter_complete_regex = re.compile(
72
- r'<|DSML|parameter\s+name="([^"]+)"\s+string="(?:true|false)"\s*>(.*?)</|DSML|parameter>',
72
+ r'<|DSML|parameter\s+name="([^"]+)"\s+string="(true|false)"\s*>(.*?)</|DSML|parameter>',
73
re.DOTALL,
74
)
75
@@ -101,10 +101,12 @@ class DeepSeekV32ToolParser(ToolParser):
101
"""Generate a unique tool call ID."""
102
return f"call_{uuid.uuid4().hex[:24]}"
103
104
- def _parse_invoke_params(self, invoke_str: str) -> dict:
105
- param_dict = dict()
106
- for param_name, param_val in self.parameter_complete_regex.findall(invoke_str):
107
- param_dict[param_name] = param_val
104
+ def _parse_invoke_params(self, invoke_str: str) -> dict[str, tuple[str, str]]:
105
+ param_dict: dict[str, tuple[str, str]] = {}
106
+ for param_name, string_attr, param_val in self.parameter_complete_regex.findall(
107
+ invoke_str
108
+ ):
109
+ param_dict[param_name] = (param_val, string_attr)
110
return param_dict
111
112
def _convert_param_value_checked(self, value: str, param_type: str) -> Any:
@@ -142,10 +144,32 @@ class DeepSeekV32ToolParser(ToolParser):
144
# return value as fallback
145
return value
146
147
+ @staticmethod
148
+ def _repair_param_dict(
149
+ param_dict: dict[str, Any],
150
+ param_config: dict[str, dict],
151
+ ) -> dict[str, Any]:
152
+ """Unwrap single 'arguments' / 'input' wrappers when the wrapper
153
+ is not part of the requested tool schema and the wrapped object
154
+ matches the schema fields."""
155
+ allowed = set(param_config.keys())
156
+ for wrapper in ("arguments", "input"):
157
+ if set(param_dict.keys()) != {wrapper} or wrapper in allowed:
158
+ continue
159
+ inner = param_dict[wrapper]
160
+ if isinstance(inner, str):
161
+ try:
162
+ inner = json.loads(inner)
163
+ except json.JSONDecodeError:
164
+ return param_dict
165
+ if isinstance(inner, dict) and set(inner.keys()).issubset(allowed):
166
+ return inner
167
+ return param_dict
168
+
169
def _convert_params_with_schema(
170
self,
171
function_name: str,
148
- param_dict: dict[str, str],
172
+ param_dict: dict[str, tuple[str, str]],
173
) -> dict[str, Any]:
174
"""Convert raw string param values using the tool schema types."""
175
param_config: dict = {}
@@ -162,12 +186,16 @@ class DeepSeekV32ToolParser(ToolParser):
186
break
187
188
converted: dict[str, Any] = {}
165
- for name, value in param_dict.items():
189
+ for name, (value, string_attr) in param_dict.items():
190
+ if string_attr == "true":
191
+ converted[name] = value
192
+ continue
193
+
194
param_type = "string"
195
if name in param_config and isinstance(param_config[name], dict):
196
param_type = param_config[name].get("type", "string")
197
converted[name] = self._convert_param_value(value, param_type)
170
- return converted
198
+ return self._repair_param_dict(converted, param_config)
199
200
def extract_tool_calls(
201
self,