* chat : split specialized parsers into common/parsers Move the 14 dedicated template parsers out of chat.cpp into one file each under common/parsers, mirroring the src/models split. chat.cpp keeps the template detection in common_chat_try_specialized_template() and drops from 3915 to 1513 lines. common/parsers/parsers.h holds the shared helpers and one declaration per parser. foreach_function/foreach_parameter become inline there since nothing in chat.cpp uses them any more; common_chat_template_direct_apply_impl and common_chat_template_generation_prompt_impl lose static and carry their default arguments in the header. Parser-specific helpers move with their parser: is_lfm2_template, deepseek_v4_sort_tool_results and the gemma4 turn builder. No functional change. Assisted-by: Claude Opus 5 * chat : enumerate parser sources instead of globbing file(GLOB) does not re-run CMake when a source file is added or removed, so an incremental build silently keeps building the old set. List the parsers in common/parsers/sources.cmake and include it from common/CMakeLists.txt. Assisted-by: Claude Opus 5 * split helpers, add newlines
168 lines
8.1 KiB
C++
168 lines
8.1 KiB
C++
#include "parsers.h"
|
|
|
|
common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & tmpl,
|
|
const autoparser::generation_params & inputs) {
|
|
common_chat_params data;
|
|
|
|
// Copy reasoning to the "thinking" field as expected by the gpt-oss template
|
|
auto adjusted_messages = json::array();
|
|
for (auto msg : inputs.messages) {
|
|
if (msg.contains("reasoning_content") && msg.at("reasoning_content").is_string()) {
|
|
msg["thinking"] = msg.at("reasoning_content");
|
|
if (msg.contains("tool_calls") && msg.at("tool_calls").is_array() && !msg.at("tool_calls").empty()) {
|
|
msg.erase("content");
|
|
}
|
|
}
|
|
adjusted_messages.push_back(msg);
|
|
}
|
|
|
|
auto prompt = common_chat_template_direct_apply_impl(tmpl, inputs, /* messages_override= */ adjusted_messages);
|
|
|
|
// Check if we need to replace the return token with end token during
|
|
// inference and without generation prompt. For more details see:
|
|
// https://github.com/ggml-org/llama.cpp/issues/15417
|
|
if (inputs.is_inference && !inputs.add_generation_prompt) {
|
|
static constexpr std::string_view return_token = "<|return|>";
|
|
static constexpr std::string_view end_token = "<|end|>";
|
|
if (size_t pos = prompt.rfind(return_token); pos != std::string::npos) {
|
|
prompt.replace(pos, return_token.length(), end_token);
|
|
}
|
|
}
|
|
|
|
data.prompt = prompt;
|
|
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, /* messages_override= */ adjusted_messages);
|
|
data.message_delimiters = {
|
|
{ COMMON_CHAT_ROLE_ASSISTANT, "<|start|>assistant" },
|
|
{ COMMON_CHAT_ROLE_USER, "<|start|>user" },
|
|
{ COMMON_CHAT_ROLE_SYSTEM, "<|start|>developer" },
|
|
{ COMMON_CHAT_ROLE_SYSTEM, "<|start|>system" },
|
|
{ COMMON_CHAT_ROLE_TOOL, "<|start|>functions" },
|
|
};
|
|
|
|
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
|
data.supports_thinking = true;
|
|
|
|
data.thinking_start_tag = "<|channel|>analysis<|message|>";
|
|
data.thinking_end_tags = {"<|end|>"};
|
|
|
|
// These special tokens are required to parse properly, so we include them
|
|
// even if parse_tool_calls is false.
|
|
data.preserved_tokens = {
|
|
"<|channel|>", "<|constrain|>", "<|message|>", "<|start|>", "<|end|>",
|
|
};
|
|
|
|
// Adjust prompt for continuation
|
|
if (inputs.has_continuation()) {
|
|
const auto & msg = inputs.continue_msg;
|
|
|
|
data.generation_prompt = "<|start|>assistant<|channel|>analysis<|message|>" + msg.reasoning_content;
|
|
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
|
data.generation_prompt += "<|end|><|start|>assistant<|channel|>final<|message|>" + msg.render_content();
|
|
}
|
|
|
|
data.prompt += data.generation_prompt;
|
|
}
|
|
|
|
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
|
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
|
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
|
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
|
|
|
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
|
auto start = p.rule("start", p.literal("<|start|>assistant"));
|
|
auto end = p.rule("end", p.literal("<|end|>"));
|
|
auto content = p.rule("message-content", p.until("<|end|>"));
|
|
auto channel = p.literal("<|channel|>") + (p.literal("commentary") | p.literal("analysis"));
|
|
auto constrain_type = p.chars("[A-Za-z0-9_-]", 1, -1);
|
|
|
|
// Occasionally, gpt-oss-20b will prefix channels with this commentary
|
|
auto stray_commentary = p.optional(p.literal("<|channel|>commentary") + p.optional(p.literal(" to=assistant")));
|
|
auto start_analysis = stray_commentary + p.literal("<|channel|>analysis<|message|>");
|
|
|
|
if (extract_reasoning) {
|
|
p.rule("analysis", start_analysis + p.reasoning(content) + end);
|
|
} else {
|
|
p.rule("analysis", p.content(start_analysis + content + end));
|
|
}
|
|
|
|
auto analysis = p.ref("analysis");
|
|
auto preamble = p.rule("preamble", p.literal("<|channel|>commentary<|message|>") + p.content(content) + end);
|
|
auto final_msg = p.rule("final", stray_commentary + p.literal("<|channel|>final<|message|>") + p.content(content));
|
|
|
|
// Consume any unsolicited tool calls, e.g. builtin functions
|
|
auto unsolicited = p.rule("unsolicited", p.atomic(p.optional(channel) + p.literal(" to=") + content + end));
|
|
|
|
auto any = p.rule("any", preamble | analysis);
|
|
|
|
if (has_response_format) {
|
|
auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
|
|
auto response_format = p.rule("response-format",
|
|
p.literal("<|channel|>final") + constraint + p.literal("<|message|>") +
|
|
p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)));
|
|
|
|
return p.zero_or_more(start + analysis) + start + response_format;
|
|
}
|
|
|
|
if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
|
|
auto tool_choice = p.choice();
|
|
|
|
foreach_function(inputs.tools, [&](const json & tool) {
|
|
const auto & function = tool.at("function");
|
|
std::string name = function.at("name");
|
|
const auto & params = function.at("parameters");
|
|
|
|
auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name));
|
|
auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
|
|
auto args = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", params));
|
|
|
|
// recipient in role header
|
|
// <|start|>assistant to=functions.NAME<|channel|>(commentary|analysis)[constraint]<|message|>ARGS
|
|
auto tool_in_role = p.tool(p.tool_open(func_name + channel + constraint + p.literal("<|message|>")) + args);
|
|
|
|
// recipient in channel header
|
|
// <|channel|>(commentary|analysis) to=functions.NAME[constraint]<|message|>ARGS
|
|
auto tool_in_channel = p.tool(p.tool_open(channel + func_name + constraint + p.literal("<|message|>")) + args);
|
|
|
|
tool_choice |= p.rule("tool-" + name, tool_in_role | tool_in_channel);
|
|
});
|
|
|
|
auto tool_call = p.trigger_rule("tool-call", tool_choice);
|
|
|
|
if (inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED) {
|
|
return p.zero_or_more(start + any) + start + tool_call;
|
|
}
|
|
|
|
return p.zero_or_more(start + any) + start + (tool_call | final_msg);
|
|
}
|
|
|
|
return p.zero_or_more(start + any) + start + (final_msg | unsolicited);
|
|
});
|
|
|
|
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_PATTERN, "^\\s+to$" },
|
|
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "^<\\|channel\\|>(?:commentary|analysis)\\s+to=functions$" },
|
|
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "<\\|start\\|>assistant(\\s+to)" },
|
|
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "<\\|start\\|>assistant(<\\|channel\\|>(?:commentary|analysis)\\s+to)" }
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|