Commit 3c809f443 for llama.cpp

commit 3c809f44383bcebc318b8cddd8c6d11aaa861e9b
Author: Tarek Dakhran <tarek@liquid.ai>
Date:   Fri Oct 30 01:27:50 2026 +0100

    common : end reasoning budget forcing on an end tag from the prompt (#31374)

diff --git a/common/reasoning-budget.cpp b/common/reasoning-budget.cpp
index 4884299f3..81b7c3e56 100644
--- a/common/reasoning-budget.cpp
+++ b/common/reasoning-budget.cpp
@@ -136,7 +136,8 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to
             // track the end sequence within forced_tokens so it is also reported on DONE
             const int32_t match = ctx->end_matcher.advance(token);
             ctx->force_pos++;
-            if (ctx->force_pos >= ctx->forced_tokens.size()) {
+            // an end tag that is already in the prompt (e.g. "<think></think>") also ends forcing
+            if (match >= 0 || ctx->force_pos >= ctx->forced_tokens.size()) {
                 ctx->state = REASONING_BUDGET_DONE;
                 ctx->end_match = match;
                 COM_TRC("%s", "forced sequence complete, done\n");
diff --git a/tests/test-reasoning-budget.cpp b/tests/test-reasoning-budget.cpp
index 3bcc77e17..a50a10824 100644
--- a/tests/test-reasoning-budget.cpp
+++ b/tests/test-reasoning-budget.cpp
@@ -316,6 +316,23 @@ static void test_reasoning_budget_end_match() {
         llama_sampler_free(sampler);
     }

+    // budget=0 and the prompt already closes the block: the end tag ends forcing before the forced sequence
+    {
+        const std::vector<llama_tokens> end_nl = {{103, 101}, {101}};
+
+        auto * sampler = common_reasoning_budget_init(nullptr, start, end_nl, {103, 101}, 0, REASONING_BUDGET_IDLE);
+
+        llama_sampler_accept(sampler, 100); // FORCING
+        llama_sampler_accept(sampler, 101); // end tag from the prompt, DONE
+        GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE);
+
+        const llama_tokens * matched = common_reasoning_budget_get_end_match(sampler);
+        GGML_ASSERT(matched != nullptr);
+        GGML_ASSERT(*matched == llama_tokens({101}));
+
+        llama_sampler_free(sampler);
+    }
+
     // forced_tokens not ending with a known end sequence records nothing
     {
         auto * sampler = common_reasoning_budget_init(nullptr, start, end, {102}, 0, REASONING_BUDGET_FORCING);