Commit 3c311796f for llama.cpp
commit 3c311796f2743669e8a0a41a37751654cda627ed
Author: Alen Požun <kalyx.solutions@proton.me>
Date: Sat Nov 14 23:33:35 2026 +0100
chat : allow tool calls inside reasoning blocks for Ministral-3 (#32271)
* chat: allow tool calls inside reasoning blocks for Ministral-3 and Qwen3
Fixes #29248
Fixes #20837
* chat: address review feedback for Ministral-3 reasoning end
- Remove <function= from Qwen3 (intentionally not used outside outdated coder models)
- Ministral-3: use choice between [/THINK] and peek([TOOL_CALLS]) instead of optional
* chat: add [TOOL_CALLS] to Ministral-3 thinking_end_tags
* chat: simplify Ministral-3 reasoning end choice
* chat: restore peek lookahead for Ministral-3 tool call start
* chat: keep [TOOL_CALLS] only in parser, not in thinking_end_tags
* cont : revert tool call tag removal and update tests
---------
Co-authored-by: Alen Požun <kukirin-ui@users.noreply.github.com>
Co-authored-by: Alde Rojas <hello@alde.dev>
diff --git a/common/parsers/ministral3.cpp b/common/parsers/ministral3.cpp
index c865a3cae..9a37d0299 100644
--- a/common/parsers/ministral3.cpp
+++ b/common/parsers/ministral3.cpp
@@ -50,7 +50,7 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat
data.supports_thinking = true;
data.thinking_start_tag = "[THINK]";
- data.thinking_end_tags = {"[/THINK]"};
+ data.thinking_end_tags = {"[/THINK]", "[TOOL_CALLS]"};
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, /* messages_override = */ adjusted_messages);
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, /* messages_override = */ adjusted_messages);
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
@@ -75,7 +75,7 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat
data.parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
auto generation_prompt = p.eps();
auto reasoning =
- extract_reasoning ? p.optional("[THINK]" + p.reasoning(p.until("[/THINK]")) + "[/THINK]") : p.eps();
+ extract_reasoning ? p.optional("[THINK]" + p.reasoning(p.until_one_of({"[/THINK]", "[TOOL_CALLS]"})) + p.choice({p.literal("[/THINK]"), p.peek(p.literal("[TOOL_CALLS]"))})) : p.eps();
// Response format parser
if (has_response_format) {
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index 556cf1b2a..62a5117e7 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -2734,6 +2734,16 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.expect(message_assist_call_thoughts)
.run();
+ // Reasoning is not closed, the tool call starts inside it
+ tst.test(
+ "[THINK]I'm\nthinking"
+ R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})")
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
+ .enable_thinking(true)
+ .tools({ special_function_tool })
+ .expect(message_assist_call_thoughts)
+ .run();
+
tst.test(R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})"
R"([TOOL_CALLS]special_function_with_opt[ARGS]{"arg1": 1, "arg2": 2})")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
@@ -2771,20 +2781,6 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.expect_content(R"({"amount": 123.45, "date": "2025-12-03"})")
.run();
- // fake tool call marker in reasoning
- tst.test(
- "[THINK]Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more[/THINK]"
- R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})")
- .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
- .enable_thinking(true)
- .tools({ special_function_tool })
- .expect_reasoning("Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more")
- .expect_tool_calls({
- { "special_function", R"({"arg1": 1})", {} },
- })
- .expect_reconstruction()
- .run();
-
// Continuation tests
tst.test("world!\nWhat's up?")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)