Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,90 @@ static json common_chat_extra_context() {
return ctx;
}

// Poolside Laguna (laguna_glm_thinking): <system>/<user>/<assistant> 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
// "</think>" before the answer, with NO emitted opening "<think>". Tool calls use
// <tool_call>{"name":..,"arguments":..}</tool_call>. 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 = "</think>";
const std::string TOOL_CALL_START = "<tool_call>";
const std::string TOOL_CALL_END = "</tool_call>";
const std::string GEN_PROMPT = "<assistant>\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 = "<think>"; // 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("<think>") -- Laguna's
// template never contains a literal <think> 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 </think> 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;
}

// <tool_call>\n{"name": <fn>, "arguments": {...}}\n</tool_call>
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_params> common_chat_try_specialized_template(
const common_chat_template & tmpl,
const std::string & src,
Expand Down Expand Up @@ -2640,6 +2724,15 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
return common_chat_params_init_minicpm5(tmpl, params);
}

// Poolside Laguna - <system>/<user>/<assistant> roles, force-opened </think>
// reasoning, <tool_call>{json}</tool_call> 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("<available_tools>") != std::string::npos) {
LOG_DBG("Using specialized template: Laguna\n");
return common_chat_params_init_laguna(tmpl, params);
}

return std::nullopt;
}

Expand Down
1 change: 1 addition & 0 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <map>
Expand Down