Skip to content

Conversation

@avigny
Copy link

@avigny avigny commented Jun 10, 2025

Purpose

Fixes #13622
Fixes #17585
Fixes #20028

This PR is similar to #16096 (hermes tool parser)

In summary

Repairs tool call in streaming mode for (older) models with tokenizer version <v11

The model output is incrementaly parsed with ijson emitting events used to know what is being streamed (what part of the tool call). for more details see _extract_tool_calls_streaming_pre_v11_tokenizer
Quick unit tests added in tests/tool_use/test_mistral_tool_parser.py see test_extract_tool_calls_streaming_pre_v11_tokenizer

Adds support for tool calls in streaming mode for recent models (tokenizer version >=v11)

See _extract_tool_calls_streaming for implementation details
Test added for mistralai/Mistral-Small-3.2-24B-Instruct-2506 in tests/tool_use/test_mistral_tool_parser.py
Quick unit tests added in tests/tool_use/test_mistral_tool_parser.py see test_extract_tool_calls_streaming

Test Plan

I've added a test file tests/tool_use/test_mistral_tool_parser.py for easy and fast testing. This file works similarly as the existing tests/tool_use/test_jamba_tool_parser.py.

This tests the parsing functions with a mocked model output. It allows toeasily test edge cases.

Use pytest tests/tool_use/test_mistral_tool_parser.py to run this test file.

Test added for mistralai/Mistral-Small-3.2-24B-Instruct-2506 in tests/tool_use/test_mistral_tool_parser.py

(Optional) Documentation Update

I believe no documentation update is needed

@github-actions
Copy link

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

Just a reminder: PRs would not trigger full CI run by default. Instead, it would only run fastcheck CI which starts running only a small and essential subset of CI tests to quickly catch errors. You can run other CI tests on top of those by going to your fastcheck build on Buildkite UI (linked in the PR checks section) and unblock them. If you do not have permission to unblock, ping simon-mo or khluu to add you in our Buildkite org.

Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging.

To run CI, PR reviewers can either: Add ready label to the PR or enable auto-merge.

🚀

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @avigny, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue related to streaming tool calls for Mistral models by replacing the previous partial_json_parser-based implementation with a more robust, custom stateful parser. This new approach aims to accurately extract tool call information, including names and arguments, as tokens are streamed, improving the reliability of tool use functionality in streaming mode. The changes include a significant rewrite of the parsing logic and the addition of comprehensive test coverage.

Highlights

  • Refactor Streaming Parser: The core logic for parsing Mistral tool calls during streaming has been completely rewritten.
  • Removed Dependency: The dependency on the partial_json_parser library for streaming tool call extraction has been removed.
  • New Parsing Mechanism: Introduced a custom stateful parsing mechanism using regex and json.JSONDecoder.raw_decode to incrementally extract tool call names and arguments from the raw token stream.
  • Comprehensive Tests: Added a new, extensive test file (tests/tool_use/test_mistral_tool_parser.py) with various test cases covering both streaming and non-streaming scenarios for Mistral tool calls, including single and multiple tool calls with different argument structures.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configureGemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the streaming tool call parsing logic for Mistral models and adds a comprehensive test suite. The core change involves replacing partial_json_parser with a custom regex and json.raw_decode-based approach for more fine-grained control over the streaming process. The new tests cover a variety of scenarios. The review includes stylistic suggestions for the tests and points for consideration regarding complexity and state management in the new parsing logic.

avigny added 4 commits June 11, 2025 10:12
Tests are similar as the ones added for Jamba models in vllm-project#9154

Signed-off-by: avigny <[email protected]>
@avigny avigny force-pushed the mistral-tool-parser-streaming-update branch from c468495 to d6d17c1 Compare June 11, 2025 08:13
@avigny avigny marked this pull request as ready for review June 11, 2025 09:25
@avigny avigny requested a review from aarnphm as a code owner June 11, 2025 09:25
@avigny
Copy link
Author

avigny commented Jun 11, 2025

@hibukipanim I did run the test you provided in your issue description #17585 (comment) and got the following output:

ChoiceDeltaToolCall(index=0, id='j6OY9szTS', function=ChoiceDeltaToolCallFunction(arguments=None, name='mcp_confluence'), type='function')
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='{"', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='query', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='":', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments=' "', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='co', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='ffee', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='",', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments=' "', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='limit', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='":', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments=' ', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='1', name=None), type=None)
ChoiceDeltaToolCall(index=0, id=None, function=ChoiceDeltaToolCallFunction(arguments='}', name=None), type=None)

It seems to fix your issue.
Please let me know If I missed something.

@avigny avigny changed the title Mistral tool parser streaming update [Bugfix] Mistral tool parser streaming update Jun 11, 2025
@PedroMiolaSilva
Copy link

@avigny hey!

I've being trying to test your solution but with no success. This is what I'm doing:

source ../.env
export MODEL_ID=unsloth/Mistral-Small-3.2-24B-Instruct-2506-FP8
export MODEL_ID_PORT=8000
export MODEL_ID_GPU=0

docker run \
--runtime nvidia \
-e VLLM_USE_V1=1 \
--gpus all \
--ipc=host \
-p "${MODEL_ID_PORT}:8000" \
--env "HUGGING_FACE_HUB_TOKEN=${HUGGING_FACE_HUB_TOKEN}" \
--env "HF_HUB_OFFLINE=0" \
-v "${HF_HOME}:/root/.cache/huggingface" \
-v "./mistral_tool_parser.py:/usr/local/lib/python3.12/dist-packages/vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py" \
vllm/vllm-openai:latest \
-v "$(pwd):/app" \
--model ${MODEL_ID} \
--tool-call-parser mistral \
--chat-template /app/template.jinja
--enable-auto-tool-choice \
--limit-mm-per-prompt 'image=1' \
--tokenizer_mode mistral \
--config_format mistral \
--load_format mistral \
--max-model-len 64000 \
--gpu-memory-utilization 0.8

Where template.jinja is this one and mistral_tool_parser.py is the one that you've created.

I'm using this test request:

curl -X POST \
   http://localhost:8000/v1/chat/completions \
   -H "Content-Type: application/json" \
   -d '{
   "model": "unsloth/Mistral-Small-3.2-24B-Instruct-2506-FP8",
   "messages": [
    {"role":"system","content":"You have access to the weather tool. You should call this tool when you think it makes sense"},
     {"role": "user", "content": "What'\''s the weather in New York?"}
   ],
   "tools": [
     {
       "type": "function",
       "function": {
         "name": "get_weather",
         "description": "Get the current weather in a given location",
         "parameters": {
           "type": "object",
           "properties": {
             "location": {
               "type": "string",
               "description": "The city and state, e.g. San Francisco, CA"
             }
           },
           "required": ["location"]
         }
       }
     }
   ]
 }'

When I set stream to false, I'm getting this response:

{"id":"chatcmpl-0dc2b75406114cbcb4f95735ccfdb094","object":"chat.completion","created":1751490167,"model":"unsloth/Mistral-Small-3.2-24B-Instruct-2506-FP8","choices":[{"index":0,"message":{"role":"assistant","reasoning_content":null,"content":"[TOOL_CALLS]get_weather{\"location\": \"New York, NY\"}","tool_calls":[]},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":112,"total_tokens":127,"completion_tokens":15,"prompt_tokens_details":null},"prompt_logprobs":null,"kv_transfer_params":null}

And this error:

ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] Error in extracting tool call from response.
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] Traceback (most recent call last):
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]   File "/usr/local/lib/python3.12/dist-packages/vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py", line 131, in extract_tool_calls
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]     function_call_arr = json.loads(tool_content)
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]                         ^^^^^^^^^^^^^^^^^^^^^^^^
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]   File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]     return _default_decoder.decode(s)
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]            ^^^^^^^^^^^^^^^^^^^^^^^^^^
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]   File "/usr/lib/python3.12/json/decoder.py", line 338, in decode
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]     obj, end = self.raw_decode(s, idx=_w(s, 0).end())
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]   File "/usr/lib/python3.12/json/decoder.py", line 356, in raw_decode
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]     raise JSONDecodeError("Expecting value", s, err.value) from None
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] 
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] During handling of the above exception, another exception occurred:
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] 
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] Traceback (most recent call last):
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]   File "/usr/local/lib/python3.12/dist-packages/vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py", line 137, in extract_tool_calls
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]     raw_tool_call = self.tool_call_regex.findall(tool_content)[0]
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160]                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^
ERROR 07-02 14:00:22 [mistral_tool_parser.py:160] IndexError: list index out of range

When I set stream=true, I dont receive any errors, but the response does not have tool calls:

data: {"id":"chatcmpl-028934e8ee754938943457f631313546","object":"chat.completion.chunk","created":1751490269,"model":"unsloth/Mistral-Small-3.2-24B-Instruct-2506-FP8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}

Am I doing something wrong here?

@rdlh
Copy link

rdlh commented Jul 3, 2025

Looks liks this PR unfortunately don't fix issues on Mistral Small 3.2.

API Call :

{
    "stream": false,
    "temperature": 0.15,
    "top_p": 1.0,
    "tool_choice": "auto",
    "model": "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
    "messages": [
        {
            "role": "user",
            "content": "Hi ! What's the result of 95478415 / 4571 ?"
        }
    ],
    "tools": [
        {
            "type":"function",
            "function": {
            "name":"calculator",
            "description":"Perform a basic calculation using ruby syntax for arithmetic operations.",
            "parameters": {
                "type":"object",
                "properties": {
                "calculation": {
                    "type":"string",
                    "description":"A basic arithmetic calculation in python language (e.g., \"2+2\", \"10*3\", \"45/9\").",
                    "required":["calculation"]
                }
                },
                "required":["calculation"]
            }
            }
        }
    ]
}

Still have this error :

ERROR 07-03 01:55:20 [mistral_tool_parser.py:166] Error in extracting tool call from response.
ERROR 07-03 01:55:20 [mistral_tool_parser.py:166] Traceback (most recent call last):
ERROR 07-03 01:55:20 [mistral_tool_parser.py:166]     function_call_arr = json.loads(tool_content)

Here are some logs :

=== model_output ===
[TOOL_CALLS]calculator{"calculation": "95478415 / 4571"}
=== tool_content ===
calculator{"calculation": "95478415 / 4571"}

Please note that this issue is NOT happening when using "tool_choice": "required".

@avigny
Copy link
Author

avigny commented Jul 3, 2025

Yes you're both right!
I believe I did branch out and started working on my fix before the changes introduced by #19193 which introduced the use of fn_name_regex from the model tokenizer.
I'll try to port this to the extract_tool_calls_streaming method.

Thanks for finding this!

@gaby
Copy link

gaby commented Jul 4, 2025

Any update on getting this merge?

@DarkLight1337
Copy link
Member

cc @aarnphm

@sjuxax
Copy link
Contributor

sjuxax commented Jul 4, 2025

So I did more complete testing and found this wasn't working that well after all -- I was getting the same errors reported above. Not sure what happened on initial testing. But, I've since taken it and have a working implementation, for streaming at least, at https://github.com/sjuxax/vllm/tree/Mistral3.2-tool-call-fix. I'm going to cherry-pick it onto #20471 in a sec. Then using that branch should work with quantized HF models and tool calling.

Copy link

@PedroMiolaSilva PedroMiolaSilva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think replacing lines 127:139 with this below will fix it for non-streaming:

            #First, use the tool call token to split, and we discard the first item, because it is empty
            raw_tool_calls = model_output.split(self.bot_token)[1:] 
            function_call_arr = []
            for raw_tool_call in raw_tool_calls:
                tool_name = raw_tool_call.split("{")[0]
                tool_arguments_begin = raw_tool_call.find("{")
                tool_arguments = raw_tool_call[tool_arguments_begin:]
                function_call_arr.append({
                                        "name": tool_name,
                                        "arguments": json.loads(tool_arguments)
                })

sjuxax pushed a commit to sjuxax/vllm that referenced this pull request Jul 4, 2025
@mergify mergify bot removed the needs-rebase label Oct 7, 2025
@mergify
Copy link

mergify bot commented Oct 16, 2025

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @avigny.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify bot added the needs-rebase label Oct 16, 2025
…-streaming-update

# Conflicts:
#	vllm/entrypoints/openai/tool_parsers/mistral_tool_parser.py
@mergify mergify bot removed the needs-rebase label Oct 16, 2025
Signed-off-by: avigny <[email protected]>
@avigny
Copy link
Author

avigny commented Oct 16, 2025

About the errors during the streaming parsing I've added a try catch to be like other parsers.
In case of an error, the streaming parser will return None as other tool parsers do. I don't think there is an easy way to return the whole model output without duplicated tokens or missing tokens as in some cases tokens are buffered before being returned in a delta tool call...

except Exception:
logger.exception("Error trying to handle streaming tool call.")
logger.debug(
"Skipping chunk as a result of tool streaming extraction error"
)
return None

except Exception:
logger.exception("Error trying to handle streaming tool call.")
return None # do not stream a delta. skip this token ID.

@bbrowning
Copy link
Contributor

@avigny I didn't review all of the commits from scratch again, but the latest commits checking just for the bot_token_id (vs text) and error handling look good to me. Thanks for getting this in good shape!!

@avigny avigny requested a review from hmellor October 24, 2025 12:38
@avigny avigny force-pushed the mistral-tool-parser-streaming-update branch from ed1a188 to 47ce613 Compare October 24, 2025 12:48
@alew3
Copy link

alew3 commented Oct 26, 2025

@avigny I can see you are putting in a lot of work getting mistral tool calling to work! Thanks for all your effort! Can you update what the current status is?

@avigny
Copy link
Author

avigny commented Oct 27, 2025

@avigny I can see you are putting in a lot of work getting mistral tool calling to work! Thanks for all your effort! Can you update what the current status is?

Hi @alew3, I'm essentially waiting for a code owner green light to get this PR merged :)

@DarkLight1337
Copy link
Member

cc @chaunceyjiang can you take a look?

Copy link
Collaborator

@patrickvonplaten patrickvonplaten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool!

@hmellor hmellor dismissed their stale review October 31, 2025 15:42

Large model test is no longer being added

@alew3
Copy link

alew3 commented Oct 31, 2025

Looking forwarding for this merge!

@Blake-Martin-code
Copy link

If this gets merged and incorporated in a new release of the docker vLLM image by early next week, y'all are for real goated. That would be perfect timing for me

Happy Halloween!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status
Status: No status