From c88aae5bcdf66e0225420a78027ef80e3d58800e Mon Sep 17 00:00:00 2001 From: zentradev-rabih Date: Thu, 23 Jul 2026 09:12:57 -0700 Subject: [PATCH 1/2] fix(build): add missing for std::isfinite in speculative.cpp Building the laguna branch with GCC 13 fails: common/speculative.cpp uses std::isfinite (DFlash non-finite feature guard) without including . --- common/speculative.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/common/speculative.cpp b/common/speculative.cpp index e6ec8f6ac62f..07df3521dd44 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include From 36b174f968c2a27068692386c9ef64d7e7c0f04f Mon Sep 17 00:00:00 2001 From: zentradev-rabih Date: Thu, 23 Jul 2026 09:12:57 -0700 Subject: [PATCH 2/2] feat(chat): Laguna reasoning + tool-call parser detector Laguna GGUFs (laguna_glm_thinking template) match none of the common_chat_params_init_* detectors, so they fall to the generic format and lose the reasoning/answer split -- chain-of-thought leaks into message.content. The assistant turn is generated already inside a reasoning block: the template's reasoning directive is prose, so the model emits its thoughts then a literal before the answer, with NO emitted opening . Add common_chat_params_init_laguna: force-opened reasoning extraction (gated on reasoning_format, NOT on a literal in the template), // roles, and {"name","arguments"} tool calls via standard_json_tools. Dispatch keys on two template-unique strings. Verified on Laguna-S-2.1-Q4_K_M: reasoning_content populated, content clean, tool_calls parsed (get_weather({"city":"Paris"})). --- common/chat.cpp | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/common/chat.cpp b/common/chat.cpp index 22d2ee4a2a11..d146f7c74fdc 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -2550,6 +2550,90 @@ static json common_chat_extra_context() { return ctx; } +// Poolside Laguna (laguna_glm_thinking): // roles. The +// assistant turn is generated already inside a reasoning block -- the template's +// reasoning directive is prose, so the model emits its thoughts and then a literal +// "" before the answer, with NO emitted opening "". Tool calls use +// {"name":..,"arguments":..}. Without this detector the +// template matches none of the specialized parsers, falls to the generic format, +// and the reasoning/answer split is lost (chain-of-thought leaks into content). +static common_chat_params common_chat_params_init_laguna(const common_chat_template & tmpl, + const autoparser::generation_params & inputs) { + common_chat_params data; + + const std::string THINK_END = ""; + const std::string TOOL_CALL_START = ""; + const std::string TOOL_CALL_END = ""; + const std::string GEN_PROMPT = "\n"; + + data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs); + data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs); + data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; + data.supports_thinking = true; + data.thinking_start_tag = ""; // nominal; the block is force-opened by the template + data.thinking_end_tag = THINK_END; + data.preserved_tokens = { TOOL_CALL_START, TOOL_CALL_END, THINK_END }; + + auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); + auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object(); + // NOTE: unlike lfm2 we do NOT gate on tmpl.source().find("") -- Laguna's + // template never contains a literal tag, only a prose directive. + auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; + auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE); + + auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { + auto generation_prompt = p.literal(GEN_PROMPT); + auto end = p.end(); + + // Force-opened reasoning: content up to the first is the thought. + auto reasoning = p.eps(); + if (extract_reasoning) { + reasoning = p.optional(p.reasoning(p.until(THINK_END)) + p.literal(THINK_END)); + } + + if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) { + if (has_response_format) { + auto response_format = p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)); + return generation_prompt + reasoning + response_format + end; + } + return generation_prompt + reasoning + p.content(p.rest()) + end; + } + + // \n{"name": , "arguments": {...}}\n + auto tool_calls = p.standard_json_tools(TOOL_CALL_START, TOOL_CALL_END, inputs.tools, + inputs.parallel_tool_calls, + /* force_tool_calls = */ false, + /* name_key = */ "name", + /* args_key = */ "arguments"); + + auto content = p.content(p.until(TOOL_CALL_START)); + return generation_prompt + reasoning + content + tool_calls + end; + }); + + data.parser = parser.save(); + + if (include_grammar) { + data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); + data.grammar = build_grammar([&](const common_grammar_builder & builder) { + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + auto schema = function.at("parameters"); + builder.resolve_refs(schema); + }); + if (has_response_format) { + auto schema = inputs.json_schema; + builder.resolve_refs(schema); + } + parser.build_grammar(builder, data.grammar_lazy); + }); + data.grammar_triggers = { + { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, TOOL_CALL_START } + }; + } + + return data; +} + std::optional common_chat_try_specialized_template( const common_chat_template & tmpl, const std::string & src, @@ -2640,6 +2724,15 @@ std::optional common_chat_try_specialized_template( return common_chat_params_init_minicpm5(tmpl, params); } + // Poolside Laguna - // roles, force-opened + // reasoning, {json} tools. Keyed on two template-unique + // strings so it never shadows another family. + if (src.find("render_assistant_messages_raw") != std::string::npos && + src.find("") != std::string::npos) { + LOG_DBG("Using specialized template: Laguna\n"); + return common_chat_params_init_laguna(tmpl, params); + } + return std::nullopt; }