66#include <nlohmann/json.hpp>
71using json = nlohmann::ordered_json;
75inline constexpr const char*
kGGUFKey =
"paperkiln.chat_template";
76inline constexpr const char*
kV1 =
"paperkiln/1";
77inline constexpr const char*
kLegacyWord =
"paperkiln/legacy-word";
78inline constexpr const char*
kV2 =
"paperkiln/2";
79inline const std::array<const char*, 4>
kV1Specials = {
"<unk>",
"<|endoftext|>",
"<|user|>",
"<|assistant|>"};
82inline const std::array<const char*, 8>
kV2Control = {
"<|system|>",
"<|tool_call|>",
"<|tool|>",
"<|spare_3|>",
83 "<|spare_4|>",
"<|spare_5|>",
"<|spare_6|>",
"<|spare_7|>"};
85struct Error : std::runtime_error {
86 using std::runtime_error::runtime_error;
142 static const char*
known[] = {
"name",
"version",
"bos",
"content",
"system",
"user",
"assistant",
143 "generation_prompt",
"generation_prompt_nothink",
"stop",
"past_assistant",
144 "default_system",
"reserved",
"markers",
"match",
"architectures",
145 "tool",
"tool_call",
"tools"};
146 if (!
r.is_object())
throw Error(
"chat template: the record is not a JSON object");
147 for (
auto it =
r.begin();
it !=
r.end(); ++
it) {
148 const std::string
k =
it.key();
149 bool ok = !
k.empty() &&
k[0] ==
'_';
150 for (
const char* q :
known) ok = ok ||
k == q;
151 if (!ok)
throw Error(
"chat template field '" +
k +
"' is not implemented");
153 for (
const char*
k : {
"name",
"version",
"user",
"assistant",
"generation_prompt",
"stop"})
154 if (!
r.contains(
k))
throw Error(std::string(
"chat template has no '") +
k +
"'");
158 throw Error(
"chat template version must be an integer or a string");
159 t.version = json::parse(
r[
"version"].
dump());
160 t.bos =
r.value(
"bos", std::string());
161 t.content =
r.value(
"content", std::string(
"raw"));
162 if (
t.content !=
"raw" &&
t.content !=
"trim")
163 throw Error(
"chat template content '" +
t.content +
"' is not implemented (raw, trim)");
164 if (
r.contains(
"system")) {
165 const auto&
s =
r[
"system"];
166 t.system_mode =
s.value(
"mode", std::string(
"none"));
167 if (
t.system_mode !=
"fold" &&
t.system_mode !=
"turn" &&
t.system_mode !=
"none")
168 throw Error(
"chat template system mode '" +
t.system_mode +
"' is not implemented (fold, turn, none)");
169 t.system_join =
s.value(
"join", std::string());
170 t.system_open =
s.value(
"open", std::string());
171 t.system_close =
s.value(
"close", std::string());
173 auto side = [&](
const char*
k, std::string&
open, std::string&
close) {
174 const auto&
s =
r[
k];
175 for (
auto it =
s.begin();
it !=
s.end(); ++
it)
176 if (
it.key() !=
"open" &&
it.key() !=
"close")
177 throw Error(std::string(
"chat template ") +
k +
" takes open and close only");
178 open =
s.value(
"open", std::string());
179 close =
s.value(
"close", std::string());
181 side(
"user",
t.user_open,
t.user_close);
182 side(
"assistant",
t.assistant_open,
t.assistant_close);
183 auto pair = [&](
const char*
k,
const char*
a,
const char*
b, std::string&
x, std::string&
y) {
184 const auto&
s =
r[
k];
185 if (!
s.is_object())
throw Error(std::string(
"chat template ") +
k +
" is an object");
186 for (
auto it =
s.begin();
it !=
s.end(); ++
it)
187 if (
it.key() !=
a &&
it.key() !=
b)
188 throw Error(std::string(
"chat template ") +
k +
" takes " +
a +
" and " +
b +
" only");
189 x =
s.value(
a, std::string());
190 y =
s.value(
b, std::string());
192 if (
r.contains(
"tool") !=
r.contains(
"tool_call"))
throw Error(
"chat template tool and tool_call come together");
193 if (
r.contains(
"tools") && !
r.contains(
"tool"))
throw Error(
"chat template tools needs tool and tool_call");
194 if (
r.contains(
"tool")) {
195 if (
t.system_mode !=
"turn")
throw Error(
"chat template tool turns need system mode turn");
197 pair(
"tool",
"open",
"close",
t.tool_open,
t.tool_close);
198 pair(
"tool_call",
"open",
"join",
t.tool_call_open,
t.tool_call_join);
200 if (
r.contains(
"tools")) {
202 pair(
"tools",
"join",
"sep",
t.tools_join,
t.tools_sep);
205 if (
r.contains(
"generation_prompt_nothink")) {
206 t.has_nothink =
true;
207 t.generation_prompt_nothink =
r[
"generation_prompt_nothink"].template
get<std::string>();
209 auto strs = [&](
const J&
a) {
210 std::vector<std::string>
v;
214 t.stop_eos =
strs(
r[
"stop"].
value(
"eos", J::array()));
215 t.stop_turn =
strs(
r[
"stop"].
value(
"turn", J::array()));
216 t.past_assistant =
r.value(
"past_assistant", std::string(
"keep"));
217 if (
t.past_assistant !=
"keep" &&
t.past_assistant !=
"strip_think")
218 throw Error(
"chat template past_assistant '" +
t.past_assistant +
"' is not implemented");
219 if (
r.contains(
"default_system")) {
220 t.has_default_system =
true;
223 if (
r.contains(
"reserved"))
t.reserved =
strs(
r[
"reserved"]);
224 if (
r.contains(
"markers"))
t.markers =
strs(
r[
"markers"]);
225 if (
r.contains(
"match"))
t.match =
strs(
r[
"match"]);
226 if (
r.contains(
"architectures"))
t.architectures =
strs(
r[
"architectures"]);
234 t.
name =
"paperkiln";
237 t.system_join =
"\n\n";
238 t.user_open =
"<|user|>";
239 t.assistant_open =
"<|assistant|>";
240 t.assistant_close =
"<|endoftext|>";
241 t.generation_prompt =
"<|assistant|>";
242 t.stop_eos = {
"<|endoftext|>"};
243 t.stop_turn = {
"<|user|>",
"<|assistant|>"};
244 t.reserved = {
"<|endoftext|>",
"<|user|>",
"<|assistant|>"};
252 t.system_mode =
"turn";
253 t.system_join.clear();
254 t.system_open =
"<|system|>";
256 t.tool_open =
"<|tool|>";
257 t.tool_call_open =
"<|tool_call|>";
258 t.tool_call_join =
"\n";
260 t.tools_join =
"\n\n";
262 t.stop_turn = {
"<|user|>",
"<|assistant|>",
"<|system|>",
"<|tool|>"};
272 t.
name =
"paperkiln";
273 t.version =
"legacy-word";
275 t.system_join =
"\n\n";
276 t.user_open =
"user: ";
278 t.assistant_open =
"assistant: ";
279 t.assistant_close =
" ";
280 t.generation_prompt =
"assistant:";
281 t.stop_turn = {
"user:",
"assistant:"};
290 t.system_mode =
"turn";
291 t.system_open =
"<|im_start|>system\n";
292 t.system_close =
"<|im_end|>\n";
293 t.user_open =
"<|im_start|>user\n";
294 t.user_close =
"<|im_end|>\n";
295 t.assistant_open =
"<|im_start|>assistant\n";
296 t.assistant_close =
"<|im_end|>\n";
297 t.generation_prompt =
"<|im_start|>assistant\n";
298 t.stop_eos = {
"<|im_end|>",
"<|endoftext|>"};
299 t.reserved = {
"<|im_start|>",
"<|im_end|>",
"<|endoftext|>"};
300 t.markers = {
"<|im_start|>",
"<|im_end|>"};
302 t.has_default_system =
true;
303 t.default_system =
"You are Qwen, created by Alibaba Cloud. You are a helpful assistant.";
304 t.match = {
"d5495a1e5db0611132a97e46a65dbb64a642a499421228b9c8b93229097fa9a4"};
305 t.architectures = {
"qwen2"};
307 t.past_assistant =
"strip_think";
308 t.has_nothink =
true;
309 t.generation_prompt_nothink =
"<|im_start|>assistant\n<think>\n\n</think>\n\n";
310 t.match = {
"57f1fd00f0013a2be96aa79b857391f27e23df5b5f847072b524c897e24d0361"};
311 t.architectures = {
"qwen3"};
322 t.system_mode =
"fold";
323 t.system_join =
"\n\n";
324 t.user_open =
"<start_of_turn>user\n";
325 t.user_close =
"<end_of_turn>\n";
326 t.assistant_open =
"<start_of_turn>model\n";
327 t.assistant_close =
"<end_of_turn>\n";
328 t.generation_prompt =
"<start_of_turn>model\n";
329 t.stop_eos = {
"<end_of_turn>",
"<eos>"};
330 t.reserved = {
"<start_of_turn>",
"<end_of_turn>",
"<bos>",
"<eos>"};
331 t.markers = {
"<start_of_turn>",
"<end_of_turn>"};
332 t.match = {
"7de1c58e208eda46e9c7f86397df37ec49883aeece39fb961e0a6b24088dd3c4"};
333 t.architectures = {
"gemma3"};
344 if (
t.id() ==
id)
return t;
347 throw Error(
"no chat template '" +
id +
"' (known: " +
known +
")");
372 if (!
a.is_array())
throw Error(
"a conversation needs a messages list");
373 std::vector<Message>
out;
374 for (
const auto&
m :
a) {
375 if (!
m.is_object() || !
m.contains(
"content") || !
m[
"content"].is_string())
376 throw Error(
"every message needs a role and a string content");
378 x.
role =
m.value(
"role", std::string());
380 if (
m.contains(
"loss")) {
381 if (!
m[
"loss"].
is_boolean())
throw Error(
"loss is a bool on assistant messages only");
384 if (
m.contains(
"tool_calls")) {
385 x.has_tool_calls =
true;
386 const auto&
tc =
m[
"tool_calls"];
388 if (!
tc.is_array() ||
tc.empty())
throw Error(
"tool_calls is a non-empty list");
389 for (
const auto&
c :
tc) {
390 if (!
c.is_object() || !
c.contains(
"function") || !
c[
"function"].is_object())
391 throw Error(
"a tool call is {id?, type?: function, function: {name, arguments}}");
392 if (
c.contains(
"type") && !(
c[
"type"].is_string() &&
c[
"type"].template
get<std::string>() ==
"function"))
393 throw Error(
"tool call type is not implemented (function)");
394 const auto&
f =
c[
"function"];
396 if (!
f.contains(
"name") || !
f[
"name"].is_string())
397 throw Error(
"a tool call's name is a non-empty string without a newline");
399 if (
k.name.empty() ||
k.name.find(
'\n') != std::string::npos)
400 throw Error(
"a tool call's name is a non-empty string without a newline");
401 if (
f.contains(
"arguments")) {
402 if (!
f[
"arguments"].
is_string())
throw Error(
"a tool call's arguments are a string (rendered verbatim)");
405 if (
c.contains(
"id")) {
410 x.tool_calls.push_back(std::move(
k));
414 if (
m.contains(
"tool_call_id")) {
415 x.has_tool_call_id =
true;
417 x.tool_call_id_null =
true;
419 throw Error(
"tool_call_id is a string or null");
423 out.push_back(std::move(
x));
429inline std::vector<Message>
from_pairs(
const std::vector<std::pair<std::string, std::string>>& turns,
430 const std::string* user =
nullptr,
const std::string* system =
nullptr) {
431 std::vector<Message>
m;
432 if (system)
m.push_back({
"system", *system});
433 for (
const auto&
t : turns) {
434 m.push_back({
"user",
t.first});
435 m.push_back({
"assistant",
t.second});
437 if (user)
m.push_back({
"user", *user});
444 if (
m.empty())
throw Error(
"a conversation needs a non-empty messages list");
445 for (
const auto&
x :
m) {
446 if (
x.role ==
"tool" && !
tools_ok)
throw Error(
"role 'tool' is reserved: no template defines it yet");
447 if (
x.role !=
"system" &&
x.role !=
"user" &&
x.role !=
"assistant" &&
x.role !=
"tool")
448 throw Error(
"unknown role '" +
x.role +
"'");
449 if (
x.has_tool_calls && (!
tools_ok ||
x.role !=
"assistant"))
450 throw Error(
"tool_calls belong on assistant messages of a template with tool turns");
451 if (
x.has_tool_call_id &&
x.role !=
"tool")
throw Error(
"tool_call_id belongs on tool messages");
453 const size_t first =
m[0].role ==
"system" ? 1 : 0;
454 if (
first ==
m.size())
throw Error(
"a system message needs a user message after it");
455 std::string
want =
"user";
458 for (
size_t i =
first;
i <
m.size(); ++
i) {
459 if (
m[
i].role ==
"system")
throw Error(
"a system message is allowed only as the first message");
462 throw Error(
"message " + std::to_string(
i) +
" is " +
m[
i].role +
", expected " +
want +
463 " (user and assistant alternate from the user)");
464 throw Error(
"message " + std::to_string(
i) +
" is " +
m[
i].role +
", expected " +
want +
465 " (user, then assistant; an assistant message with k tool calls, then k tool messages, then "
468 if (
m[
i].loss >= 0 &&
m[
i].role !=
"assistant")
throw Error(
"loss is a bool on assistant messages only");
469 if (
m[
i].role ==
"user") {
471 }
else if (
m[
i].role ==
"assistant") {
480 if (
m[
i].has_tool_call_id &&
m[
i].tool_call_id_null &&
c.has_id)
481 throw Error(
"tool_call_id null is not the call's id '" +
c.id +
"' (tool messages answer the calls in "
482 "order; null answers a call without an id)");
483 if (
m[
i].has_tool_call_id && !
m[
i].tool_call_id_null && (!
c.has_id ||
m[
i].tool_call_id !=
c.id))
484 throw Error(
"tool_call_id '" +
m[
i].tool_call_id +
"' is not the call's id (tool messages answer the "
489 const std::string&
last =
m.back().role;
491 throw Error(std::string(
"a prompt conversation ends on a user message") +
492 (
tools_ok ?
" or on the last tool message of a call" :
""));
493 if (!prompt &&
last !=
"assistant")
throw Error(
"a training conversation ends on an assistant message");
505 return (
c >= 0x09 &&
c <= 0x0d) || (
c >= 0x1c &&
c <= 0x20) ||
c == 0x85 ||
c == 0xa0 ||
c == 0x1680 ||
506 (
c >= 0x2000 &&
c <= 0x200a) ||
c == 0x2028 ||
c == 0x2029 ||
c == 0x202f ||
c == 0x205f ||
512 auto decode = [&](
size_t i,
uint32_t&
c) ->
size_t {
513 const unsigned char b =
static_cast<unsigned char>(
s[
i]);
514 size_t n =
b < 0x80 ? 1 : (
b >> 5) == 0x6 ? 2 : (
b >> 4) == 0xe ? 3 : (
b >> 3) == 0x1e ? 4 : 0;
515 if (
n == 0 ||
i +
n >
s.size())
return 0;
516 c =
n == 1 ?
b :
b & (0xff >> (
n + 1));
517 for (
size_t k = 1;
k <
n; ++
k) {
518 const unsigned char x =
static_cast<unsigned char>(
s[
i +
k]);
519 if ((
x >> 6) != 0x2)
return 0;
520 c = (
c << 6) | (
x & 0x3f);
525 while (
b <
s.size()) {
527 const size_t n = decode(
b,
c);
534 while (
st >
b && (
static_cast<unsigned char>(
s[
st]) >> 6) == 0x2 &&
e -
st < 4) --
st;
536 const size_t n = decode(
st,
c);
540 return s.substr(
b,
e -
b);
551 for (
auto it =
x.begin();
it !=
x.end(); ++
it) {
560 for (
size_t i = 0;
i <
x.size(); ++
i)
s += (
i ?
", " :
"") +
dump_py(
x[
i]);
563 if (
x.is_number_float())
564 throw Error(
"a tool definition holds a non-integer number (refused: its text is not portable)");
571 if (!d.is_object())
throw Error(
"a tool definition is a JSON object");
579 int thinking = -1,
const std::vector<json>& tools = {}) {
580 if (!tools.empty() && !
t.has_tools)
throw Error(
"this model's template has no tool definitions");
582 const bool trim =
t.content ==
"trim";
584 for (
const auto&
r :
t.reserved)
586 throw Error(
"content holds the template's reserved text '" +
r +
"'");
589 auto body = [&](std::string
s) {
594 std::vector<Piece>
out;
595 auto add = [&](
const std::string&
text,
const std::string& role,
const char*
kind,
int idx) {
598 add(
t.bos,
"",
"bos", -1);
600 std::string sys =
have_sys ?
m[0].content : std::string();
602 if (!
have_sys &&
t.has_default_system && !
t.default_system.empty()) {
604 sys =
t.default_system;
608 if (!tools.empty()) {
610 sys = (
head.empty() ? std::string() :
head +
t.tools_join) +
defs;
616 if (
t.system_mode ==
"none")
throw Error(
"this model's template has no system role");
617 if (
t.system_mode ==
"turn") {
619 add(tools.empty() ?
body(sys) : sys,
"system",
"content",
sys_idx);
628 for (
size_t i =
first;
i <
m.size(); ++
i)
630 for (
size_t i =
first;
i <
m.size(); ++
i) {
631 const std::string& role =
m[
i].role;
632 const bool user = role ==
"user",
tool = role ==
"tool";
638 if (role ==
"assistant" &&
t.past_assistant ==
"strip_think" &&
i <
last_user) {
639 const size_t p =
text.rfind(
"</think>");
640 if (
p != std::string::npos) {
642 text.erase(0,
text.find_first_not_of(
'\n') == std::string::npos ?
text.size()
646 const int idx =
static_cast<int>(
i);
647 add(user ?
t.user_open :
tool ?
t.tool_open :
t.assistant_open, role,
"open", idx);
648 add(
text, role,
"content", idx);
649 for (
const auto&
c :
m[
i].tool_calls) {
650 add(
t.tool_call_open, role,
"call_open", idx);
653 add(user ?
t.user_close :
tool ?
t.tool_close :
t.assistant_close, role,
"close", idx);
655 if (generation_prompt)
656 add(thinking == 0 &&
t.has_nothink ?
t.generation_prompt_nothink :
t.generation_prompt,
"",
"generation", -1);
657 else if (
t.past_assistant ==
"strip_think")
658 throw Error(
"a strip_think template renders prompt conversations only (HF re-wraps the reasoning of "
659 "replies after the last user message; prompts never have one)");
663inline std::string
render(
const std::vector<Message>&
m,
const Template&
t,
bool generation_prompt,
int thinking = -1,
664 const std::vector<json>& tools = {}) {
666 for (
const auto&
p : render_pieces(
m,
t, generation_prompt, thinking, tools))
s +=
p.
text;
673inline std::vector<std::pair<size_t, size_t>>
exchanges(
const std::vector<Message>&
m) {
674 std::vector<std::pair<size_t, size_t>>
out;
675 for (
size_t i = 0;
i <
m.size(); ++
i)
676 if (
m[
i].role ==
"user") {
677 if (!
out.empty())
out.back().second =
i;
678 out.push_back({
i,
m.size()});
694template <
class Decode>
696 std::vector<std::vector<int>>
parts(1);
699 else parts.back().push_back(
x);
703 for (
size_t k = 1;
k <
parts.size(); ++
k) {
705 const size_t nl =
text.find(
'\n');
706 if (
nl == std::string::npos ||
nl == 0)
return ParsedReply{
false, {}, {}};
709 c.id =
"call_" + std::to_string(
k - 1);
711 c.arguments =
text.substr(
nl + 1);
712 r.tool_calls.push_back(std::move(
c));
721 const std::vector<int32_t>&
types) {
722 if (!
t.has_tool)
return;
723 for (
const auto&
r :
t.reserved) {
725 for (
size_t i = 0;
i < tokens.size() && !ok; ++
i)
727 if (!ok)
throw Error(
"chat template " +
t.id() +
" needs '" +
r +
"' as a control token of the vocabulary");
733inline std::string
jstr(
const std::string&
s) {
736 if (
c ==
'\\')
o +=
"\\\\";
737 else if (
c ==
'\'')
o +=
"\\'";
738 else if (
c ==
'\n')
o +=
"\\n";
743inline std::string
go(
const std::string&
s) {
745 for (
size_t i = 0;
i <
s.size(); ++
i) {
746 if (
s.compare(
i, 2,
"{{") == 0) {
756 if (
t.name !=
"paperkiln")
throw Error(std::string(
what) +
" writes paperkiln records only");
766 if (!
t.bos.empty())
j +=
"{{- " +
jstr(
t.bos) +
" -}}";
767 j +=
"{%- if messages and messages[0]['role'] == 'system' -%}"
768 "{%- set sys = messages[0]['content'] -%}{%- set msgs = messages[1:] -%}"
769 "{%- else -%}{%- set sys = none -%}{%- set msgs = messages -%}{%- endif -%}";
771 j +=
"{%- if tools -%}{%- set ns = namespace(d='') -%}{%- for x in tools -%}"
772 "{%- if not loop.first -%}{%- set ns.d = ns.d + " +
jstr(
t.tools_sep) +
" -%}{%- endif -%}"
773 "{%- set ns.d = ns.d + (x | tojson) -%}{%- endfor -%}"
774 "{%- if sys -%}{%- set sys = sys + " +
jstr(
t.tools_join) +
" + ns.d -%}"
775 "{%- else -%}{%- set sys = ns.d -%}{%- endif -%}{%- endif -%}";
776 j +=
"{%- if sys is not none -%}{{- " +
jstr(
t.system_open) +
" + sys + " +
jstr(
t.system_close) +
778 j +=
"{%- for m in msgs -%}"
779 "{%- if m['role'] == 'user' -%}{{- " +
jstr(
t.user_open) +
" + m['content'] + " +
jstr(
t.user_close) +
" -}}"
780 "{%- elif m['role'] == 'assistant' -%}{{- " +
jstr(
t.assistant_open) +
" + m['content'] -}}"
781 "{%- for c in (m['tool_calls'] or []) -%}{{- " +
jstr(
t.tool_call_open) +
" + c['function']['name'] + " +
782 jstr(
t.tool_call_join) +
" -}}"
783 "{%- if c['function']['arguments'] is string -%}{{- c['function']['arguments'] -}}"
784 "{%- else -%}{{- c['function']['arguments'] | tojson -}}{%- endif -%}{%- endfor -%}"
785 "{{- " +
jstr(
t.assistant_close) +
" -}}"
786 "{%- elif m['role'] == 'tool' -%}{{- " +
jstr(
t.tool_open) +
" + m['content'] + " +
jstr(
t.tool_close) +
" -}}"
787 "{%- else -%}{{ raise_exception('roles: one leading system message, then user, assistant and tool') }}"
788 "{%- endif -%}{%- endfor -%}"
789 "{%- if add_generation_prompt -%}{{- " +
jstr(
t.generation_prompt) +
" -}}{%- endif -%}";
796 head +=
"{{ if or .System .Tools }}" +
go(
t.system_open) +
"{{ if .System }}{{ .System }}{{ end }}"
797 "{{ if .Tools }}{{ if .System }}" +
go(
t.tools_join) +
"{{ end }}"
798 "{{ range $i, $t := .Tools }}{{ if $i }}" +
go(
t.tools_sep) +
"{{ end }}{{ json $t }}{{ end }}"
799 "{{ end }}" +
go(
t.system_close) +
"{{ end }}";
800 const std::string
msgs =
801 "{{ range .Messages }}{{ if eq .Role \"user\" }}" +
go(
t.user_open) +
"{{ .Content }}" +
go(
t.user_close) +
802 "{{ else if eq .Role \"assistant\" }}" +
go(
t.assistant_open) +
"{{ .Content }}"
803 "{{ range .ToolCalls }}" +
go(
t.tool_call_open) +
"{{ .Function.Name }}" +
go(
t.tool_call_join) +
804 "{{ json .Function.Arguments }}{{ end }}" +
go(
t.assistant_close) +
805 "{{ else if eq .Role \"tool\" }}" +
go(
t.tool_open) +
"{{ .Content }}" +
go(
t.tool_close) +
806 "{{ end }}{{ end }}" +
go(
t.generation_prompt);
807 std::string
s =
"# Ollama Modelfile for " +
gguf_name +
": chat template " +
t.id() +
808 " (LOCAL_LLM_STACK.md section 3, point 3).\nFROM ./" +
gguf_name +
"\nTEMPLATE \"\"\"" +
head +
810 std::vector<std::string>
stops =
t.stop_eos;
811 stops.insert(
stops.end(),
t.stop_turn.begin(),
t.stop_turn.end());
812 for (
const auto&
x :
stops)
s +=
"PARAMETER stop " +
json(
x).dump() +
"\n";
819 detail::paperkiln_only(
t,
"jinja()");
820 if (
t.content !=
"raw" ||
t.past_assistant !=
"keep" ||
t.has_default_system)
821 throw Error(
"jinja() covers raw content, kept replies and no default system");
822 if (
t.has_tool)
return detail::jinja_tools(
t);
824 if (!
t.bos.empty())
j +=
"{{- " + jstr(
t.bos) +
" -}}";
825 j +=
"{%- if messages and messages[0]['role'] == 'system' -%}"
826 "{%- set sys = messages[0]['content'] -%}{%- set msgs = messages[1:] -%}"
827 "{%- else -%}{%- set sys = none -%}{%- set msgs = messages -%}{%- endif -%}";
828 if (
t.system_mode ==
"none")
829 j +=
"{%- if sys is not none -%}{{ raise_exception('this model\\'s template has no system role') }}"
831 else if (
t.system_mode ==
"turn")
832 j +=
"{%- if sys is not none -%}{{- " + jstr(
t.system_open) +
" + sys + " + jstr(
t.system_close) +
834 j +=
"{%- for m in msgs -%}"
835 "{%- if (m['role'] == 'user') != (loop.index0 % 2 == 0) -%}"
836 "{{ raise_exception('user and assistant messages alternate from the user') }}{%- endif -%}"
837 "{%- if m['role'] == 'user' -%}{{- " + jstr(
t.user_open) +
" -}}";
838 if (
t.system_mode ==
"fold")
839 j +=
"{%- if loop.first and sys is not none -%}{{- sys + " + jstr(
t.system_join) +
" -}}{%- endif -%}";
840 j +=
"{{- m['content'] + " + jstr(
t.user_close) +
" -}}"
841 "{%- else -%}{{- " + jstr(
t.assistant_open) +
" + m['content'] + " + jstr(
t.assistant_close) +
" -}}"
842 "{%- endif -%}{%- endfor -%}"
843 "{%- if add_generation_prompt -%}{{- " + jstr(
t.generation_prompt) +
" -}}{%- endif -%}";
849 detail::paperkiln_only(
t,
"modelfile()");
850 if (
t.has_tool)
return detail::modelfile_tools(
gguf_name,
t);
853 std::string
msgs =
"{{ $first := true }}{{ range .Messages }}{{ if eq .Role \"user\" }}" + go(
t.user_open);
854 if (
t.system_mode ==
"fold")
msgs +=
"{{ if and $first $.System }}{{ $.System }}" + go(
t.system_join) +
"{{ end }}";
855 msgs +=
"{{ $first = false }}{{ .Content }}" + go(
t.user_close) +
"{{ else if eq .Role \"assistant\" }}" +
856 go(
t.assistant_open) +
"{{ .Content }}" + go(
t.assistant_close) +
"{{ end }}{{ end }}" + go(
t.generation_prompt);
857 std::string
head = go(
t.bos);
858 if (
t.system_mode ==
"turn")
859 head +=
"{{ if .System }}" + go(
t.system_open) +
"{{ .System }}" + go(
t.system_close) +
"{{ end }}";
860 std::string
s =
"# Ollama Modelfile for " +
gguf_name +
": chat template " +
t.id() +
861 " (LOCAL_LLM_STACK.md section 3, point 3).\nFROM ./" +
gguf_name +
"\nTEMPLATE \"\"\"" +
head +
863 std::vector<std::string>
stops =
t.stop_eos;
864 stops.insert(
stops.end(),
t.stop_turn.begin(),
t.stop_turn.end());
865 for (
const auto&
x :
stops)
s +=
"PARAMETER stop " +
json(
x).dump() +
"\n";
873 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
874 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
875 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
876 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
877 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
878 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
879 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
880 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
881 uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
884 m +=
static_cast<char>(0x80);
885 while (
m.size() % 64 != 56)
m +=
'\0';
886 for (
int i = 7;
i >= 0; --
i)
m +=
static_cast<char>((
bits >> (8 *
i)) & 0xff);
888 for (
size_t off = 0;
off <
m.size();
off += 64) {
890 for (
int i = 0;
i < 16; ++
i)
893 for (
int i = 16;
i < 64; ++
i) {
899 for (
int i = 0;
i < 64; ++
i) {
911 h[0] +=
a;
h[1] +=
b;
h[2] +=
c;
h[3] += d;
h[4] +=
e;
h[5] +=
f;
h[6] += g;
h[7] +=
hh;
914 for (
int i = 0;
i < 8; ++
i) std::snprintf(
buf + 8 *
i, 9,
"%08x",
h[
i]);
915 return std::string(
buf, 64);
928inline Resolution resolve(
const std::string& declared,
const std::vector<std::string>& tokens,
const std::string& model,
929 const std::string& chat_template,
const std::string& arch) {
932 if (!declared.empty()) {
933 r.tmpl = Template::from_json(json::parse(declared));
941 r.why =
"paperkiln specials at ids 0-3";
944 if (model ==
"word") {
946 r.why =
"word-level vocabulary";
950 if (!chat_template.empty()) {
951 const std::string
h = detail::sha256_hex(chat_template);
952 for (
const auto&
t :
reg)
953 for (
const auto&
m :
t.match)
956 r.why =
"tokenizer.chat_template sha256 " +
h.substr(0, 12);
960 for (
const auto&
t :
reg) {
961 if (
t.markers.empty())
continue;
965 for (
const auto&
mk :
t.markers) {
972 r.why =
"markers and architecture " + arch;
977 r.why =
"no declared template, no paperkiln specials, and no known tokenizer.chat_template or markers: raw "
const float * p[kIrDepthSourcesMax]
std::string go(const std::string &s)
std::string jinja_tools(const Template &t)
std::string sha256_hex(const std::string &msg)
std::string dump_py(const json &x)
std::string modelfile_tools(const std::string &gguf_name, const Template &t)
std::string jstr(const std::string &s)
void paperkiln_only(const Template &t, const char *what)
std::vector< Message > messages_from_json(const J &a)
std::string tool_json(const json &d)
Template get(const std::string &id)
constexpr const char * kDefaultSftTurns
constexpr const char * kGGUFKey
std::vector< Piece > render_pieces(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
const std::array< const char *, 8 > kV2Control
std::vector< std::pair< size_t, size_t > > exchanges(const std::vector< Message > &m)
std::string render(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
constexpr const char * kV1
std::string jinja(const Template &t)
constexpr const char * kLegacyWord
Template chatml(const std::string &name, bool qwen25)
std::vector< Message > from_pairs(const std::vector< std::pair< std::string, std::string > > &turns, const std::string *user=nullptr, const std::string *system=nullptr)
bool py_space(uint32_t c)
nlohmann::ordered_json json
std::string strip_py_ws(const std::string &s)
std::vector< Template > registry()
void check_control_tokens(const Template &t, const std::vector< std::string > &tokens, const std::vector< int32_t > &types)
constexpr const char * kV2
std::string modelfile(const std::string &gguf_name, const Template &t)
constexpr const char * kSystemModeV1
size_t check_structure(const std::vector< Message > &m, bool prompt, bool tools_ok=false)
const std::array< const char *, 4 > kV1Specials
ParsedReply parse_reply(const std::vector< int > &ids, int call_id, Decode decode)
Resolution resolve(const std::string &declared, const std::vector< std::string > &tokens, const std::string &model, const std::string &chat_template, const std::string &arch)
Message(std::string r, std::string c, int l=-1)
std::vector< ToolCall > tool_calls
std::vector< ToolCall > tool_calls
std::string tool_call_join
std::vector< std::string > stop_eos
std::string tool_call_open
std::vector< std::string > reserved
std::string generation_prompt_nothink
std::vector< std::string > architectures
std::string generation_prompt
std::string past_assistant
static Template from_json(const J &r)
std::vector< std::string > match
std::vector< std::string > markers
std::vector< std::string > stop_turn
std::string assistant_open
std::string default_system
std::string assistant_close