From 152e080b6ab5e0fd2ef4484c131bbf9897366918 Mon Sep 17 00:00:00 2001 From: Julien BODIN <137780842+JulienJBO@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:51:55 +0200 Subject: [PATCH] Add Mistral [THINK]/[/THINK] thinking format (mistral3 arch) (#2380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reasoning budget derived from reasoning_effort never applied to Mistral models. gpttype_adapter.cpp picks the think delimiters from a switch on the model architecture, and mistral3 has no case, so it falls back to / . Those are not vocabulary tokens for Ministral-3, so TokenizeString returns more than one token each, the expected_start/end_tokens guard clears all three vectors, and apply_reasoning_budget() returns at its first if. The parameter is accepted, converted and passed down to the sampler, then dropped on a size check, with nothing logged. Adding the mistral3 case arms the budget. [THINK] and [/THINK] are single vocabulary tokens (ids 34 and 35 on Ministral-3), so the size guard passes. The thinkformats entry is a separate fix for a separate defect: without it the thinking block was never split out, so it leaked into content with its [THINK] marker still in it, instead of going to reasoning_content. Measured on Ministral-3-14B-Reasoning-2512 (IQ4_XS, ctx 8192, --jinja), 5 real prompts x 3 samples per cell, max_tokens 3000 (so a 750-token budget at "low"): reasoning_effort thinking words before thinking words after none 311 - 2314 7 (the forced-close phrase) low 340 - 2255 521 - 574 Forced closes: 0/15 before, 14/15 after at "low" and 15/15 at "none". Three samples per cell because this model's variance at temperature 0.7 spans a factor of 4 on an identical payload — a single sample per cell cannot tell an effect from noise. No regression on a non-reasoning mistral3 model: Ministral-3-8B-Instruct with reasoning_effort "low" returns finish_reason "stop", a normal answer and zero forced closes, since apply_reasoning_budget() bails out when the start marker never appears. --- gpttype_adapter.cpp | 5 +++++ koboldcpp.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gpttype_adapter.cpp b/gpttype_adapter.cpp index 9840e5c4a..6098f6eab 100644 --- a/gpttype_adapter.cpp +++ b/gpttype_adapter.cpp @@ -5859,6 +5859,11 @@ generation_outputs gpttype_generate(const generation_inputs inputs) end = "<|END_THINKING|>"; budget_exceeded = "\n(Reasoning budget exceeded)\nTime to respond now.\n<|END_THINKING|>"; break; + case llm_arch::LLM_ARCH_MISTRAL3: + start = "[THINK]"; + end = "[/THINK]"; + budget_exceeded = "\n(Reasoning budget exceeded)\nTime to respond now.\n[/THINK]"; + break; default: break; } diff --git a/koboldcpp.py b/koboldcpp.py index 9ac7aba1c..fd3f967a1 100644 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -186,7 +186,8 @@ thinkformats = [{"start":"<|channel|>analysis<|message|>","end":"<|start|>assist {"start":"","end":""}, {"start":"","end":""}, {"start":"<|START_THINKING|>","end":"<|END_THINKING|>"}, - {"start":"<|channel>thought","end":""}] + {"start":"<|channel>thought","end":""}, + {"start":"[THINK]","end":"[/THINK]"}] tool_call_pairs = [ #third element is optional str to match in chat template before we use this pair, fourth element is whether its stream-handleable ("", "", None, True), #qwen, glm ("", "", None, True), #seed oss