-
Notifications
You must be signed in to change notification settings - Fork 13.6k
common: Generalized XML-style tool-call parsing with streaming support (GLM 4.5/4.6 + MiniMax M2 + SeedOSS) #16932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
I'm looking forward to get this PR merged! @hksdpc255 Does it require a custom jinja template from the previous PR or it works good as is? |
|
For now, I’d recommend using a custom template if you’re running more complex workloads. |
|
FYI I've updated (my fork of) Minja w/ support for GLM 4.6's template. |
|
@ochafik Excellent work! Once llama.cpp syncs your changes, some parts of this PR can be safely removed. However, there are still a few small patches needed — for example, replacing |
|
Currently, the official Minimax-M2 chat template fails to run tool calls because |
@hksdpc255 Both should be supported. The confusing error you probably got was because minja implements As for And please feel free to file bugs on https://github.com/ochafik/minja, it's should be cleaner to add syntax support there than to patch things up in llama.cpp. |
|
@ochafik Thank you for pointing that out. I’m currently applying your suggested fix in llama.cpp and will test whether it works as expected. Thanks again for the help! |
|
Good news! The Minimax M2 tool call is now working. I’ll push the fix later. |
|
Model: unsloth's UD-Q3_K_XL |
|
Hi @hksdpc255 , Model: unsloth--MiniMax-M2-GGUF Q8_0 ./llama-cli \
-m /models/hub/models--unsloth--MiniMax-M2-GGUF/snapshots/*/Q8_0/MiniMax-M2-Q8_0-00001-of-00005.gguf \
-ngl 99 \
-sm layer \
-ts 1,1,1,1,1,1,1,1 \
-c 78000 \
-t 16 \
--jinja \
-iOutput: > what is the capital of france?
Okay, the user asked a straightforward question: "What is the capital of France?" This is basic geography knowledge, so the answer should be simple. I don't need to overcomplicate things.
Hmm, maybe the user is just testing if I know basic facts, or perhaps they're new to this kind of question. Either way, the response should be clear and concise. No need for extra details unless they ask follow-ups.
I recall that Paris is the capital of France. It's one of the most well-known capitals globally, so this should be an easy one. The user might be a student working on homework, or someone prepping for trivia. Or maybe they're just curious—either way, I should confirm it confidently.
No signs of confusion or deeper needs here. The question is very direct. I'll just state the answer plainly. If they want more info later, like landmarks or history, they'll ask. For now, keep it simple: Paris is the capital.
Wait, should I add that it's also a major cultural hub? Nah, overcomplicating it. Just the fact. Done.
</think>
The capital of France is **Paris**.
Paris is not only the political center but also a major cultural, economic, and gastronomic hub, famous for landmarks like the Eiffel Tower, the Louvre Museum, Notre-Dame Cathedral, and the Champs-Élysées. |
|
@emuchogu Sorry, I haven’t tested it with If you want I’m not sure whether |
|
I’ve reverted my previous PR (reasoning-format-minimax-m2) and merged PR #16932 into my testing-branch16 for isolated testing. Without this PR :Streaming, no initial <think> tag in the output: Curl without streaming no initial <think> tag in the output : With this PR :Streaming : Curl without streaming, no initial <think> tag in the output : |
|
Oh! It seems you’re using non-streaming mode. I can now reproduce your issue with Let me dig into what’s happening… |
Yes, exactly: it works correctly in streaming mode (tested through the SvelteUI, which specifically designed to be debug-friendly without needing curl -N), but not in non-streaming mode. |
|
Toolcall debug on SvelteUI with your #16932 + #16618 :) Custom JSON :
|
|
@ServeurpersoCom The problem is that I added some code that makes it fall back to llama.cpp’s original parser when there are no tools, so the new parser is never called. Lines 2748 to 2753 in af5216e
Simply deleting the code above should fix the issue. I’ll run more tests before pushing a new commit.
|
I’ve successfully tested it without these lines of code and confirmed it works as expected for streaming / non streaming / reasoning_content / toolcall |
|
I just realized this, and it seems strange: shouldn’t --reasoning-format none completely bypass any parsing logic instead of still going through it? It’s meant to be the raw passthrough mode for observing the model’s native output. The .cpp files are already becoming huge and monolithic, making them harder to touch or refactor safely. The --reasoning-format options are also poorly named and not very explicit. In the long run, a modular templating system would help avoid piling up even more C++ parsing code. If this work is meant to unify several next-generation parsers, maybe we could add a new keyword to --reasoning-format instead? It’s important to keep none as a truly no-parsing mode, since it’s essential for debugging new models. Also, the current "auto" mode is actually just "deepseek" in practice, so it might be clearer to rename or document it that way to avoid confusion: and your unified detection logic could be implemented directly under auto (or deepseek, since they’re basically aliases) ? |
|
I feel like this PR is a mixed bag. I like the core idea and I think it's high time we implemented something like that (as in a more general parser for the XML-style tool calling models). On the other hand, I feel like there are things here which simply add to the chaos already present in the chat parsing code. First of all, the code is very hacky - including stuff like tampering with Jinja templates to remove / patch specific fragments. I feel like that's very risky and error-prone. Second of all, some of the functionalities duplicate already existing code (like try_parse_reasoning). I don't really know why the code handles tool calling and reasoning in one huge code block - the way I see it, thinking parsing was generally working correctly and there were no problems with it, the problems were with the tool calling. |
This part will be removed once the issue is fixed in the upstream Minja project.
I handle the reasoning content manually because:
|
I understand the concern. I’ve been very cautious with the Jinja template patching. It only replaces code segments that are explicitly verified, while leaving everything else unchanged. |
|
I’ve tested that the official chat template works with latest Minja for both GLM4.6 and MiniMax-M2. |
@MikeLP Now, official jinja template should works for this PR. I've tested this on Zed editor with |
@ServeurpersoCom My understanding of If the goal is to completely bypass all parsing logic, wouldn’t it make more sense to use the legacy |
I get your point, but the original purpose of --reasoning-format none was to disable all reasoning and tool parsing logic while keeping the chat API active it’s a debugging flag for raw model behavior, not a partial parsing mode. Switching to /v1/completions isn’t a practical alternative, since modern chat templates depend on structured roles. Also, this parameter is encapsulated in the /v1/chat/completions API request: the SvelteUI client uses auto by default and switches to none when debug mode is toggled at runtime; it’s handled dynamically. And using none to plug in a new model parser just reinforces the confusion around those parameters: auto is basically the same as deepseek, and deepseek-legacy behaves like an OpenAI reasoning_content + unparsed content clone: it’s a total mess to make sense of. |
As an active user of llama.cpp and a developer building products around it, I don't care how hacky the template parser is. If I can't call tools properly with the model, everything else looks useless. Any future errors can be fixed, and the code can be refactored. |
I’m fine with having experimental or even hacky parsers, as long as they live in separate files or modules. That way, they can be easily rewritten or replaced later without breaking serious users’ setups. The current chat parsing code is already fragile enough: mixing more experimental logic into it only makes future maintenance and debugging harder. A clean separation would let us iterate faster without destabilizing production workflows. |
That's completely another point and I agree with you. There's a huge difference between "We won't merge it because it's hacky or bad, and we'll return back to this issue someday later" and "Let's clean it up, fix the issues, move the code to a separate folder/file/module, and be good to merge it." |
|
Anyway, the global template patching has been removed since Minja now correctly handles these situations. So, the most “hacky” part of this PR is gone. : ) As for the reasoning parsing, I couldn’t find a easy way to make Currently, the only approach I found that works is to manually generate the Another possible approach would be to move my In my view, parsing logic should ideally stay in Overall, I think |
|
@hksdpc255 I'm trying to figure out how to use this pr. I've cloned hksdpc255:xml_toolcall and used it to run Unsloth's Do I need to specify the chat template file, or use some different options when starting the llama server? You were so helpful in helping me trackdown the server crashing issues with GLM 4.5 (which I'm still using until I can figure out how to get Minimax M2 working properly). Also, after solving the tool calling crashes with GLM 4.5, I was never able to get GLM 4.5 working with MCP servers in nearly all coding agents. I'm hoping to get Minimax M2 working with BOTH tool calling and MCP servers. |
|
@aaronnewsome It should work out of the box, for both with the official chat template and with Unsloth’s template. |
|
I've checked out and built I start llama server with: In my first quick test, using vscode latest, cline latest, I asked it to create a quick instruction md file for how to deploy a container. Then asked it to add the md to git, commit and push. All seemed to go ok. I really like that Cline does much better at reading the terminal output of the commands. GLM would consistently read the first output, then fail from remaining commands (yes, I've tried all the hacks I could find). Minimax-M2 seemed to do much better. I also appreciate how much faster Minimax-M2 is on the same hardware - now you can see why I'm so keen to get this model running to replace GLM 4.5 Air (the only GLM 4.6 I could get running on my system was the Q2, which performed horribly, got lost in code frequently etc). Cline is also able to use MCP with MiniMax (tested with context7). Most importantly, I was able to use OpenCode with MiniMax-M2. Something that always gave me problems with GLM 4.5-Air (although I still haven't tried any diff edits with OpenCode, which reliably fail with GLM 4.5-Air).
Thanks for everything you do @hksdpc255 to help bring these tools to all of us who prefer to use local LLM. So far, in my own testing, Minimax-M2 beats ANYTHING that will run on my rig - so if the testing continues to go well, I'll never spin up GLM 4.5-Air again. UPDATE: I was even able to use chrome-devtools mcp AND the take_screenshot tool. it uses a ridiculous amount of memory, consumed the entire context in the chat (even using all of the system DRAM), but Minimax was able to take the screenshot and the analysis of the image data was right on, no errors even though it took forever. I'm impressed. |
|
@hksdpc255 You've put a lot of good work in this PR and I'm starting to get convinced that it should supercede mine, but I'd ask you to do two things: -> remove the template patching code. They way this is done is that you put the proper template in |
|
@aaronnewsome Do you mean the task stops during the tool-call observation loop, or that it fails when handling parallel tool calls? |
|
@pwilkin Thank you for reviewing my code. The template patching logic was removed after your initial review. The only remaining patch now targets the buggy official Minimax-M2 template (see https://github.com/ochafik/minja/pull/7#issuecomment-3478459580\) ), which ensures that the official template works correctly. So, do you mean that removing this code causes the unmodified official template to stop working? Also, before I move my code into a separate file, I’d like to ask for your opinion: do you think it would be a good idea to make parse_msg_with_xml_tool_calls a member of common_chat_msg_parser? |







Generalized and streaming-capable XML-style tool-call parsing with grammar enforcement and automatic template fixing.
Based on PR #15904, this patch introduces a generalized implementation for almost all XML-style tool-call formats.
Grammar-constrained tool-call outputs
Tool-call messages generated by the model are now strictly validated against a defined grammar.
A new automatic grammar generator simplifies the process of creating grammars for new models.
This ensures that all tool-call outputs are well-formed, structurally consistent, and reliably parsed.
Streaming support for tool-call parsing
The parser now supports streaming parsing, enabling incremental processing of tool-call messages as they are generated.
This enhancement improves responsiveness and allows real-time interaction during model inference.
Automatic chat-template fixing
A lightweight Jinja2-based patcher has been added to automatically fix official chat templates before use.
With this change, official templates now work out of the box, eliminating the need for custom modifications.
In-context reasoning
The parser now supports multiple reasoning blocks within a single generation, even when interleaved with tool calls.
All reasoning content is preserved. No information is lost during parsing or streaming.
Additional Notes
--reasoning-format none-lv 1in the command line to enable more detailed logging.