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}')