@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 5c213d2899
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 11, 18:45 UTC

[BUGFIX] Mistral tool call parser v11+ (#30332)

Signed-off-by: juliendenize <julien.denize@mistral.ai>

Julien Denize committed Dec 9, 2025 at 15:55 UTC 5c213d2899f5a2d439c8d771a0abc156a5412a2b
2 files changed +32 -20
tests/tool_use/test_mistral_tool_parser.py
+16
@@ -615,6 +615,7 @@ def test_extract_tool_calls_streaming(
615 "single_tool_weather",
616 "multiple_tool_calls",
617 "content_before_tool",
618 + "complex",
619 ],
620 argnames=["model_output", "expected_tool_calls", "expected_content"],
621 argvalues=[
@@ -673,6 +674,21 @@ def test_extract_tool_calls_streaming(
674 ],
675 "bla",
676 ),
677 + (
678 + # Complex
679 + """[TOOL_CALLS]bash{"command": "print(\\"hello world!\\")\\nre.compile(r\'{}\')"}""", # noqa: E501
680 + [
681 + ToolCall(
682 + function=FunctionCall(
683 + name="bash",
684 + arguments=json.dumps(
685 + {"command": "print(\"hello world!\")\nre.compile(r'{}')"}
686 + ),
687 + )
688 + )
689 + ],
690 + "",
691 + ),
692 ],
693 )
694 def test_extract_tool_calls_streaming_one_chunk(
vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py
+16 -20
@@ -99,12 +99,7 @@ class MistralToolParser(ToolParser):
99 self.bot_token = "[TOOL_CALLS]"
100 self.bot_token_id = self.vocab.get(self.bot_token)
101 self.tool_call_regex = re.compile(r"\[{.*}\]", re.DOTALL)
102 - if not _is_pre_v11_tokeniser(self.model_tokenizer):
103 - self.fn_name_regex = re.compile(
104 - r"([a-zA-Z0-9_-]+)(\{[\s\S]*?\}+)", re.DOTALL
105 - )
106 - else:
107 - self.fn_name_regex = None
102 + self._is_pre_v11 = _is_pre_v11_tokeniser(self.model_tokenizer)
103
104 if self.bot_token_id is None:
105 raise RuntimeError(
@@ -148,23 +143,24 @@ class MistralToolParser(ToolParser):
143 tool_content = model_output.replace(self.bot_token, "").strip()
144
145 try:
151 - # we first try to directly load the json as parsing very nested
152 - # jsons is difficult
146 try:
154 - if self.fn_name_regex:
147 + if not self._is_pre_v11:
148 function_call_arr = []
149 for single_tool_content in model_output.split(self.bot_token):
157 - matches = self.fn_name_regex.findall(single_tool_content)
158 -
159 - for match in matches:
160 - fn_name = match[0]
161 - args = match[1]
162 -
163 - # fn_name is encoded outside serialized json dump
164 - # only arguments are serialized
165 - function_call_arr.append(
166 - {"name": fn_name, "arguments": json.loads(args)}
167 - )
150 + if "{" not in single_tool_content:
151 + continue
152 +
153 + end_name = single_tool_content.find("{")
154 + fn_name, args = (
155 + single_tool_content[:end_name],
156 + single_tool_content[end_name:],
157 + )
158 +
159 + # fn_name is encoded outside serialized json dump
160 + # only arguments are serialized
161 + function_call_arr.append(
162 + {"name": fn_name, "arguments": json.loads(args)}
163 + )
164 else:
165 function_call_arr = json.loads(tool_content)
166 except json.JSONDecodeError: