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

[Bugfix] Fix glm4_moe_tool_parser._is_string_type for /v1/responses FunctionTool format (#39601)

Signed-off-by: Yiyang Liu <37043548+ianliuy@users.noreply.github.com> Signed-off-by: Chauncey <chaunceyjiang@gmail.com> Signed-off-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Chauncey <chaunceyjiang@gmail.com> Co-authored-by: sfeng33 <4florafeng@gmail.com>

Yiyang "Ian" Liu committed May 20, 2026 at 22:58 UTC 050611a3dd19271a3c729788ff69b3470ccfb238
2 files changed +135 -25
tests/tool_parsers/test_glm4_moe_tool_parser.py
+120
@@ -5,6 +5,7 @@ import json
5 from unittest.mock import Mock
6
7 import pytest
8 +from openai.types.responses import FunctionTool
9
10 from vllm.entrypoints.openai.chat_completion.protocol import (
11 ChatCompletionRequest,
@@ -1363,3 +1364,122 @@ def test_stream_interval_content_between_tool_calls(
1364 args1 = json.loads("".join(tools_found[1]["args_fragments"]))
1365 assert args0 == {"city": "Beijing"}
1366 assert args1 == {"city": "Shanghai"}
1367 +
1368 +
1369 +# ── FunctionTool (Responses API) tests ──────────────────────────────
1370 +
1371 +
1372 +@pytest.fixture
1373 +def function_tools():
1374 + return [
1375 + FunctionTool(
1376 + type="function",
1377 + name="get_weather",
1378 + parameters={
1379 + "type": "object",
1380 + "properties": {
1381 + "city": {"type": "string"},
1382 + "unit": {"type": "string"},
1383 + },
1384 + },
1385 + ),
1386 + FunctionTool(
1387 + type="function",
1388 + name="calculate",
1389 + parameters={
1390 + "type": "object",
1391 + "properties": {
1392 + "operation": {"type": "string"},
1393 + "a": {"type": "number"},
1394 + "b": {"type": "number"},
1395 + },
1396 + },
1397 + ),
1398 + ]
1399 +
1400 +
1401 +@pytest.fixture
1402 +def glm4_moe_parser_function_tools(glm4_moe_tokenizer, function_tools):
1403 + return Glm4MoeModelToolParser(glm4_moe_tokenizer, tools=function_tools)
1404 +
1405 +
1406 +@pytest.fixture
1407 +def mock_request_function_tools(function_tools) -> ChatCompletionRequest:
1408 + request = Mock(spec=ChatCompletionRequest)
1409 + request.tools = function_tools
1410 + return request
1411 +
1412 +
1413 +def test_extract_tool_calls_with_function_tool(
1414 + glm4_moe_parser_function_tools, mock_request_function_tools
1415 +):
1416 + model_output = """<tool_call>get_weather
1417 +<arg_key>city</arg_key>
1418 +<arg_value>Dallas</arg_value>
1419 +<arg_key>unit</arg_key>
1420 +<arg_value>fahrenheit</arg_value>
1421 +</tool_call>"""
1422 +
1423 + extracted = glm4_moe_parser_function_tools.extract_tool_calls(
1424 + model_output, request=mock_request_function_tools
1425 + )
1426 + assert extracted.tools_called
1427 + assert len(extracted.tool_calls) == 1
1428 + assert extracted.tool_calls[0].function.name == "get_weather"
1429 + args = json.loads(extracted.tool_calls[0].function.arguments)
1430 + assert args["city"] == "Dallas"
1431 + assert args["unit"] == "fahrenheit"
1432 +
1433 +
1434 +def test_extract_tool_calls_with_function_tool_mixed_types(
1435 + glm4_moe_parser_function_tools, mock_request_function_tools
1436 +):
1437 + model_output = """<tool_call>calculate
1438 +<arg_key>operation</arg_key>
1439 +<arg_value>add</arg_value>
1440 +<arg_key>a</arg_key>
1441 +<arg_value>42</arg_value>
1442 +<arg_key>b</arg_key>
1443 +<arg_value>3.14</arg_value>
1444 +</tool_call>"""
1445 +
1446 + extracted = glm4_moe_parser_function_tools.extract_tool_calls(
1447 + model_output, request=mock_request_function_tools
1448 + )
1449 + assert extracted.tools_called
1450 + args = json.loads(extracted.tool_calls[0].function.arguments)
1451 + assert args["operation"] == "add"
1452 + assert isinstance(args["a"], (int, float))
1453 + assert isinstance(args["b"], float)
1454 +
1455 +
1456 +def test_streaming_with_function_tool(
1457 + glm4_moe_parser_function_tools, mock_request_function_tools
1458 +):
1459 + _reset_streaming_state(glm4_moe_parser_function_tools)
1460 +
1461 + chunks = [
1462 + "<tool_call>get_weather\n",
1463 + "<arg_key>city</arg_key>",
1464 + "<arg_value>Bei",
1465 + "jing",
1466 + "</arg_value>",
1467 + "</tool_call>",
1468 + ]
1469 +
1470 + current_text = ""
1471 + for chunk in chunks:
1472 + current_text += chunk
1473 + glm4_moe_parser_function_tools.extract_tool_calls_streaming(
1474 + previous_text="",
1475 + current_text=current_text,
1476 + delta_text=chunk,
1477 + previous_token_ids=[],
1478 + current_token_ids=[],
1479 + delta_token_ids=[],
1480 + request=mock_request_function_tools,
1481 + )
1482 +
1483 + assert len(glm4_moe_parser_function_tools.prev_tool_call_arr) == 1
1484 + args = json.loads(glm4_moe_parser_function_tools.prev_tool_call_arr[0]["arguments"])
1485 + assert args["city"] == "Beijing"
vllm/tool_parsers/glm4_moe_tool_parser.py
+15 -25
@@ -38,7 +38,11 @@ from vllm.tool_parsers.abstract_tool_parser import (
38 Tool,
39 ToolParser,
40 )
41 -from vllm.tool_parsers.utils import partial_tag_overlap
41 +from vllm.tool_parsers.utils import (
42 + extract_types_from_schema,
43 + find_tool_properties,
44 + partial_tag_overlap,
45 +)
46
47 logger = init_logger(__name__)
48
@@ -123,27 +127,13 @@ class Glm4MoeModelToolParser(ToolParser):
127 return ""
128 return json.dumps(s, ensure_ascii=False)[1:-1]
129
126 - @staticmethod
127 - def _is_string_type(
128 - tool_name: str,
129 - arg_name: str,
130 - tools: list[Tool] | None,
131 - ) -> bool:
132 - if tools is None:
130 + def _is_string_type(self, tool_name: str, arg_name: str) -> bool:
131 + tool_properties = find_tool_properties(self.tools, tool_name)
132 + param_schema = tool_properties.get(arg_name)
133 + if param_schema is None:
134 return False
134 - for tool in tools:
135 - if tool.function.name != tool_name:
136 - continue
137 - if tool.function.parameters is None:
138 - return False
139 - arg_type = (
140 - tool.function.parameters.get("properties", {})
141 - .get(arg_name, {})
142 - .get("type", None)
143 - )
144 - return arg_type == "string"
145 - logger.debug("No tool named '%s'.", tool_name)
146 - return False
135 + param_types = extract_types_from_schema(param_schema)
136 + return set(param_types) - {"null"} == {"string"}
137
138 @staticmethod
139 def _tools_enabled(request: ChatCompletionRequest) -> bool:
@@ -210,7 +200,7 @@ class Glm4MoeModelToolParser(ToolParser):
200 arg_dct: dict[str, Any] = {}
201 for key, value in pairs:
202 arg_key = key.strip()
213 - if self._is_string_type(tc_name, arg_key, self.tools):
203 + if self._is_string_type(tc_name, arg_key):
204 arg_val = value
205 else:
206 arg_val = self._deserialize(value.strip())
@@ -352,7 +342,7 @@ class Glm4MoeModelToolParser(ToolParser):
342 for key, value in pairs:
343 key = key.strip()
344 key_json = json.dumps(key, ensure_ascii=False)
355 - if self._is_string_type(tool_name, key, self.tools):
345 + if self._is_string_type(tool_name, key):
346 # Don't strip string values — whitespace is significant
347 # and must match the partial-value path for diffing.
348 val_json = json.dumps(value, ensure_ascii=False)
@@ -392,7 +382,7 @@ class Glm4MoeModelToolParser(ToolParser):
382 # Tool call finished but </arg_value> is missing
383 # (malformed output). Treat partial as complete value
384 # so the diff naturally closes any open quotes.
395 - if self._is_string_type(tool_name, partial_key, self.tools):
385 + if self._is_string_type(tool_name, partial_key):
386 val_json = json.dumps(partial_content, ensure_ascii=False)
387 else:
388 val_json = json.dumps(
@@ -400,7 +390,7 @@ class Glm4MoeModelToolParser(ToolParser):
390 ensure_ascii=False,
391 )
392 parts.append(f"{key_json}: {val_json}")
403 - elif self._is_string_type(tool_name, partial_key, self.tools):
393 + elif self._is_string_type(tool_name, partial_key):
394 escaped = self._json_escape_string_content(partial_content)
395 # Open quote but no close — more content may arrive
396 parts.append(f'{key_json}: "{escaped}')