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)