@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 95582868ef
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 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,