1// Tests chat handling, including grammar generation and parsing for tool calling, for various templates.
2//
3// Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates,
4// e.g. given Minja (http://github.com/google/minja) checked out in parent dir:
5//
6// cmake -B build && cmake --build build --parallel && ./build/bin/test-chat ../minja/build/tests/*.jinja 2>/dev/null
7//
8#include "chat.h"
9
10#include "log.h"
11
12#include "../src/unicode.h"
13#include "../src/llama-grammar.h"
14
15#include <nlohmann/json.hpp>
16
17#include <fstream>
18#include <iostream>
19#include <functional>
20#include <string>
21
22using json = nlohmann::ordered_json;
23
24static std::ostream & operator<<(std::ostream & os, const common_chat_msg_diff & diff) {
25 os << "{ content_delta: " << diff.content_delta << "; ";
26 os << "reasoning_content_delta: " << diff.reasoning_content_delta << "; ";
27 if (diff.tool_call_index != std::string::npos) {
28 os << "tool_call_index: " << diff.tool_call_index << "; ";
29 os << "tool_call_delta.name: " << diff.tool_call_delta.name << "; ";
30 os << "tool_call_delta.id: " << diff.tool_call_delta.id << "; ";
31 os << "tool_call_delta.arguments: " << diff.tool_call_delta.arguments << "; ";
32 }
33 os << "}";
34 return os;
35}
36// operator<< for vector<common_chat_msg_diff>:
37static std::ostream & operator<<(std::ostream & os, const std::vector<common_chat_msg_diff> & diffs) {
38 os << "[\n";
39 for (const auto & diff : diffs) {
40 os << " " << diff << ",\n";
41 }
42 os << "]";
43 return os;
44}
45static std::ostream & operator<<(std::ostream & os, const common_chat_msg & msg) {
46 os << "{ role: " << msg.role << "; ";
47 os << "content: " << msg.content << "; ";
48 os << "content_parts: [\n";
49 for (const auto & part : msg.content_parts) {
50 os << " { type: " << part.type << "; text: " << part.text << " },\n";
51 }
52 os << "]; ";
53 os << "reasoning_content: " << msg.reasoning_content << "; ";
54 os << "tool_calls: [\n";
55 for (const auto & tool_call : msg.tool_calls) {
56 os << " { name: " << tool_call.name << "; arguments: " << tool_call.arguments << "; id: " << tool_call.id << " },\n";
57 }
58 os << "]";
59 os << "}";
60 return os;
61}
62
63template <class T> static bool equals(const T & expected, const T & actual) {
64 return expected == actual;
65}
66
67static common_chat_msg normalize(const common_chat_msg & msg) {
68 common_chat_msg normalized = msg;
69 for (auto & tool_call : normalized.tool_calls) {
70 try {
71 tool_call.arguments = json::parse(i&: tool_call.arguments).dump();
72 } catch (const std::exception &) {
73 // Do nothing
74 }
75 }
76 return normalized;
77}
78template <>
79bool equals(const common_chat_msg & expected, const common_chat_msg & actual) {
80 return normalize(msg: expected) == normalize(msg: actual);
81}
82
83template <class T> static void assert_equals(const T & expected, const T & actual) {
84 if (!equals(expected, actual)) {
85 std::cerr << "Expected: " << expected << std::endl;
86 std::cerr << "Actual: " << actual << std::endl;
87 std::cerr << std::flush;
88 throw std::runtime_error("Test failed");
89 }
90}
91
92static std::string read_file(const std::string & path) {
93 std::cerr << "# Reading: " << path << '\n' << std::flush;
94 std::ifstream fs(path, std::ios_base::binary);
95 if (!fs.is_open()) {
96 fs = std::ifstream("../" + path, std::ios_base::binary);
97 if (!fs.is_open()) {
98 throw std::runtime_error("Failed to open file: " + path);
99 }
100 }
101 fs.seekg(0, std::ios_base::end);
102 auto size = fs.tellg();
103 fs.seekg(0);
104 std::string out;
105 out.resize(n: static_cast<size_t>(size));
106 fs.read(s: out.data(), n: static_cast<std::streamsize>(size));
107 return out;
108}
109
110static common_chat_templates_ptr read_templates(const std::string & path) {
111 return common_chat_templates_ptr(common_chat_templates_init(/* model= */ nullptr, chat_template_override: read_file(path)));
112}
113
114static std::unique_ptr<llama_grammar> build_grammar(const std::string & grammar_str) {
115 return std::unique_ptr<llama_grammar>(
116 llama_grammar_init_impl(vocab: nullptr, grammar_str: grammar_str.c_str(), grammar_root: "root", lazy: false, trigger_patterns: nullptr, num_trigger_patterns: 0, trigger_tokens: nullptr, num_trigger_tokens: 0));
117}
118
119// TODO: extract to common helper (copied from test-grammar-integration.cpp)
120static bool match_string(const std::string & input, llama_grammar * grammar) {
121 const auto cpts = unicode_cpts_from_utf8(utf8: input);
122
123 auto & stacks_cur = llama_grammar_get_stacks(grammar);
124
125 for (const auto & cpt : cpts) {
126 llama_grammar_accept(grammar, chr: cpt);
127
128 if (stacks_cur.empty()) {
129 // no stacks means that the grammar failed to match at this point
130 return false;
131 }
132 }
133
134 if (std::any_of(first: stacks_cur.begin(), last: stacks_cur.end(), pred: [](const auto & stack) { return stack.empty(); })) {
135 // An empty stack means that the grammar has been completed
136 return true;
137 }
138
139 return false;
140}
141
142static std::string renormalize_json(const std::string & json_str) {
143 try {
144 auto json_obj = json::parse(i: json_str);
145 return json_obj.dump();
146 } catch (const std::exception & e) {
147 std::cerr << "Failed to parse JSON: " << e.what() << '\n';
148 return json_str;
149 }
150}
151static void assert_msg_equals(const common_chat_msg & expected, const common_chat_msg & actual) {
152 assert_equals(expected: expected.role, actual: actual.role);
153 assert_equals(expected: expected.content, actual: actual.content);
154 assert_equals(expected: expected.content_parts.size(), actual: actual.content_parts.size());
155 for (size_t i = 0; i < expected.content_parts.size(); i++) {
156 const auto & expected_part = expected.content_parts[i];
157 const auto & actual_part = actual.content_parts[i];
158 assert_equals(expected: expected_part.type, actual: actual_part.type);
159 assert_equals(expected: expected_part.text, actual: actual_part.text);
160 }
161 assert_equals(expected: expected.reasoning_content, actual: actual.reasoning_content);
162 assert_equals(expected: expected.tool_calls.size(), actual: actual.tool_calls.size());
163 for (size_t i = 0; i < expected.tool_calls.size(); i++) {
164 const auto & expected_tool_call = expected.tool_calls[i];
165 const auto & actual_tool_call = actual.tool_calls[i];
166 assert_equals(expected: expected_tool_call.name, actual: actual_tool_call.name);
167 assert_equals(expected: renormalize_json(json_str: expected_tool_call.arguments), actual: renormalize_json(json_str: actual_tool_call.arguments));
168 assert_equals(expected: expected_tool_call.id, actual: actual_tool_call.id);
169 }
170}
171
172common_chat_tool special_function_tool {
173 /* .name = */ "special_function",
174 /* .description = */ "I'm special",
175 /* .parameters = */ R"({
176 "type": "object",
177 "properties": {
178 "arg1": {
179 "type": "integer",
180 "description": "The arg."
181 }
182 },
183 "required": ["arg1"]
184 })",
185};
186common_chat_tool python_tool {
187 /* .name = */ "python",
188 /* .description = */ "an ipython interpreter",
189 /* .parameters = */ R"({
190 "type": "object",
191 "properties": {
192 "code": {
193 "type": "string",
194 "description": "Python code to execute."
195 }
196 },
197 "required": ["code"]
198 })",
199};
200common_chat_tool code_interpreter_tool {
201 /* .name = */ "code_interpreter",
202 /* .description = */ "an ipython interpreter",
203 /* .parameters = */ R"({
204 "type": "object",
205 "properties": {
206 "code": {
207 "type": "string",
208 "description": "Python code to execute."
209 }
210 },
211 "required": ["code"]
212 })",
213};
214std::vector<common_chat_tool> tools { special_function_tool, python_tool };
215std::vector<common_chat_tool> llama_3_1_tools { special_function_tool, code_interpreter_tool };
216
217struct delta_data {
218 std::string delta;
219 common_chat_params params;
220};
221
222static delta_data init_delta(const struct common_chat_templates * tmpls, const std::vector<std::string> & end_tokens,
223 const common_chat_msg & user_message,
224 const common_chat_msg & delta_message,
225 const std::vector<common_chat_tool> & tools,
226 const common_chat_tool_choice & tool_choice) {
227 common_chat_templates_inputs inputs;
228 inputs.parallel_tool_calls = true;
229 inputs.messages.push_back(x: user_message);
230 inputs.tools = tools;
231 inputs.tool_choice = tool_choice;
232 auto params_prefix = common_chat_templates_apply(tmpls, inputs);
233
234 inputs.messages.push_back(x: delta_message);
235 inputs.add_generation_prompt = false;
236 auto params_full = common_chat_templates_apply(tmpls, inputs);
237
238 std::string prefix = params_prefix.prompt;
239 std::string full = params_full.prompt;
240
241 if (full == prefix) {
242 throw std::runtime_error("Full message is the same as the prefix");
243 }
244
245 size_t common_prefix_length = 0;
246 for (size_t i = 0; i < prefix.size() && i < full.size(); ++i) {
247 if (prefix[i] != full[i]) {
248 break;
249 }
250 if (prefix[i] == '<') {
251 // DeepSeek R1's template (as of 20250209) adds a trailing <think> if add_generation_prompt,
252 // but it removes thinking tags for past messages.
253 // The prefix and full strings diverge at <think> vs. <|tool▁calls▁begin|>, we avoid consuming the leading <.
254 continue;
255 }
256 common_prefix_length = i + 1;
257 }
258 auto delta = full.substr(pos: common_prefix_length);
259
260 // Strip end tokens
261 for (const auto & end_token : end_tokens) {
262 // rfind to find the last occurrence
263 auto pos = delta.rfind(str: end_token);
264 if (pos != std::string::npos) {
265 delta = delta.substr(pos: 0, n: pos);
266 break;
267 }
268 }
269 return { .delta: delta, .params: params_full };
270}
271
272/*
273 Applies the template to 1 user message w/ add_generation_prompt=true, then w/ the test message w/ add_generation_prompt=false,
274 gets the diff, removes any end tokens and parses the result w/ the grammar, checking that
275 the parsed message is the same as the test_message
276*/
277static void test_templates(const struct common_chat_templates * tmpls, const std::vector<std::string> & end_tokens,
278 const common_chat_msg & test_message,
279 const std::vector<common_chat_tool> & tools = {},
280 const std::string & expected_delta = "",
281 bool expect_grammar_triggered = true,
282 bool test_grammar_if_triggered = true,
283 common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_NONE) {
284 common_chat_msg user_message;
285 user_message.role = "user";
286 user_message.content = "Hello, world!";
287
288 for (const auto & tool_choice : std::vector<common_chat_tool_choice> {COMMON_CHAT_TOOL_CHOICE_AUTO, COMMON_CHAT_TOOL_CHOICE_REQUIRED}) {
289 auto data = init_delta(tmpls, end_tokens, user_message, delta_message: test_message, tools, tool_choice);
290 if (!expected_delta.empty()) {
291 assert_equals(expected: expected_delta, actual: data.delta);
292 }
293
294 if (expect_grammar_triggered) {
295 common_chat_syntax syntax;
296 syntax.format = data.params.format;
297 syntax.reasoning_format = reasoning_format;
298 const auto msg = common_chat_parse(input: data.delta, /* is_partial= */ false, syntax);
299 assert_msg_equals(expected: test_message, actual: msg);
300 }
301
302 if (!test_message.tool_calls.empty()) {
303 GGML_ASSERT(!data.params.grammar.empty());
304 }
305 if (!data.params.grammar.empty()) {
306 auto grammar = build_grammar(grammar_str: data.params.grammar);
307 if (!grammar) {
308 throw std::runtime_error("Failed to build grammar");
309 }
310 auto earliest_trigger_pos = std::string::npos;
311 auto constrained = data.delta;
312 for (const auto & trigger : data.params.grammar_triggers) {
313 size_t pos = std::string::npos;
314 std::smatch match;
315 switch (trigger.type) {
316 case COMMON_GRAMMAR_TRIGGER_TYPE_WORD:
317 {
318 const auto & word = trigger.value;
319 pos = constrained.find(str: word);
320 break;
321 }
322 case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN:
323 {
324 const auto & pattern = trigger.value;
325 if (std::regex_search(s: constrained, m&: match, e: std::regex(pattern))) {
326 pos = match.position(sub: 1);
327 }
328 break;
329 }
330 case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL:
331 {
332 const auto & pattern = trigger.value;
333 if (std::regex_match(s: constrained, m&: match, re: std::regex(pattern))) {
334 auto mpos = std::string::npos;
335 for (size_t i = 1; i < match.size(); ++i) {
336 if (match[i].length() > 0) {
337 mpos = match.position(sub: i);
338 break;
339 }
340 }
341 if (mpos == std::string::npos) {
342 mpos = match.position(sub: 0);
343 }
344 pos = mpos;
345 }
346 break;
347 }
348 default:
349 throw std::runtime_error("Unknown trigger type");
350 }
351 if (pos == std::string::npos) {
352 continue;
353 }
354 if (earliest_trigger_pos == std::string::npos || pos < earliest_trigger_pos) {
355 earliest_trigger_pos = pos;
356 }
357 }
358 auto grammar_triggered = false;
359 if (earliest_trigger_pos != std::string::npos) {
360 constrained = constrained.substr(pos: earliest_trigger_pos);
361 grammar_triggered = true;
362 }
363 if (data.params.grammar_lazy) {
364 assert_equals(expected: expect_grammar_triggered, actual: grammar_triggered);
365 }
366
367 if (grammar_triggered && test_grammar_if_triggered && !match_string(input: constrained, grammar: grammar.get())) {
368 throw std::runtime_error("Failed to match delta against grammar:\n\n" + data.delta +
369 "\n\nConstrained: " + constrained +
370 "\n\nGrammar: " + data.params.grammar);
371 }
372 }
373 }
374}
375
376const common_chat_msg message_user {
377 .role: "user",
378 .content: "Hey there!",
379 /* .content_parts = */ {},
380 /* .tool_calls = */ {},
381 /* .reasoning_content = */ "",
382 /* .tool_name = */ "",
383 /* .tool_call_id = */ "",
384};
385
386const common_chat_msg message_user_parts {
387 .role: "user",
388 /* .content = */ "",
389 /* .content_parts = */ {
390 { .type: "text", .text: "Hey" },
391 { .type: "text", .text: "there" },
392 },
393 /* .tool_calls = */ {},
394 /* .reasoning_content = */ "",
395 /* .tool_name = */ "",
396 /* .tool_call_id = */ "",
397};
398static common_chat_msg simple_assist_msg(const std::string & content, const std::string & reasoning_content = "", const std::string & tool_name = "", const std::string & arguments = "", const std::string & id = "") {
399 common_chat_msg msg;
400 msg.role = "assistant";
401 msg.content = content;
402 msg.reasoning_content = reasoning_content;
403 if (!tool_name.empty()) {
404 msg.tool_calls.push_back(x: { .name: tool_name, .arguments: arguments, .id: id });
405 }
406 return msg;
407}
408const common_chat_msg message_assist = simple_assist_msg(content: "Hello, world!\nWhat's up?");
409const common_chat_msg message_assist_empty = simple_assist_msg(content: "");
410const common_chat_msg message_assist_thoughts_unparsed_deepseek = simple_assist_msg(content: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?");
411const common_chat_msg message_assist_thoughts_unparsed_md = simple_assist_msg(content: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```");
412const common_chat_msg message_assist_thoughts_unparsed_md_partial = simple_assist_msg(content: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}");
413
414const common_chat_msg message_assist_thoughts_unparsed_r7b = simple_assist_msg(content: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>Hello, world!\nWhat's up?");
415const common_chat_msg message_assist_thoughts_unparsed_magistral = simple_assist_msg(content: "[THINK]raisonnement[/THINK]Réponse");
416const common_chat_msg message_assist_thoughts = simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking");
417const common_chat_msg message_assist_thoughts_unopened_unparsed = simple_assist_msg(content: "I'm\nthinking</think>Hello, world!\nWhat's up?");
418const common_chat_msg message_assist_thoughts_no_content = simple_assist_msg(content: "", reasoning_content: "I'm\nthinking");
419const common_chat_msg message_assist_call = simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\": 1}");
420const common_chat_msg message_assist_call_content = simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\":1}");
421const common_chat_msg message_assist_call_empty_args = simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function");
422const common_chat_msg message_assist_call_cutoff_args = simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg");
423const common_chat_msg message_assist_call_thoughts = simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\":1}");
424const common_chat_msg message_assist_call_thoughts_unparsed = simple_assist_msg(content: "<think>I'm\nthinking</think>\n\n", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\": 1}");
425const common_chat_msg message_assist_call_thoughts_content = simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}");
426const common_chat_msg message_assist_call_id = simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\":1}", /* .id = */ "123456789");
427const common_chat_msg message_assist_call_idx = simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\":1}", /* .id = */ "0");
428const common_chat_msg message_assist_thoughts_call_idx = simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}", /* id = */ "0");
429const common_chat_msg message_assist_call_python = simple_assist_msg(content: "", reasoning_content: "", tool_name: "python", arguments: "{\"code\":\"print('hey')\"}");
430const common_chat_msg message_assist_call_python_lines = simple_assist_msg(content: "", reasoning_content: "", tool_name: "python", arguments: "{\"code\":\"# This is a program:\\nprint('hey')\"}");
431const common_chat_msg message_assist_call_python_lines_unclosed = simple_assist_msg(content: "", reasoning_content: "", tool_name: "python", arguments: "{\"code\":\"# This is a program:\\nprint('hey')");
432const common_chat_msg message_assist_call_code_interpreter = simple_assist_msg(content: "", reasoning_content: "", tool_name: "code_interpreter", arguments: "{\"code\":\"print('hey')\"}");
433
434static void test_msgs_oaicompat_json_conversion() {
435 printf(format: "[%s]\n", __func__);
436 std::vector<common_chat_msg> msgs{
437 message_user,
438 message_user_parts,
439 message_assist_call,
440 message_assist_call_thoughts,
441 message_assist_call_thoughts_unparsed,
442 message_assist_call_thoughts_content,
443 message_assist_call_id,
444 message_assist_call_idx,
445 message_assist_call_python,
446 message_assist_call_code_interpreter,
447 };
448 for (const auto & msg : msgs) {
449 auto oai_json = common_chat_msgs_to_json_oaicompat<json>(msgs: {msg});
450 auto msgs2 = common_chat_msgs_parse_oaicompat(messages: oai_json);
451 assert_equals(expected: (size_t) 1, actual: msgs2.size());
452 auto msg2 = msgs2[0];
453 assert_msg_equals(expected: msg, actual: msg2);
454 }
455 assert_equals(
456 expected: std::string(
457 "[\n"
458 " {\n"
459 " \"role\": \"user\",\n"
460 " \"content\": [\n"
461 " {\n"
462 " \"type\": \"text\",\n"
463 " \"text\": \"Hey\"\n"
464 " },\n"
465 " {\n"
466 " \"type\": \"text\",\n"
467 " \"text\": \"there\"\n"
468 " }\n"
469 " ]\n"
470 " }\n"
471 "]"
472 ),
473 actual: common_chat_msgs_to_json_oaicompat<json>(msgs: {message_user_parts}).dump(indent: 2));
474
475 assert_equals(
476 expected: std::string(
477 "[\n"
478 " {\n"
479 " \"role\": \"assistant\",\n"
480 " \"content\": null,\n"
481 " \"tool_calls\": [\n"
482 " {\n"
483 " \"type\": \"function\",\n"
484 " \"function\": {\n"
485 " \"name\": \"python\",\n"
486 " \"arguments\": \"{\\\"code\\\":\\\"print('hey')\\\"}\"\n"
487 " }\n"
488 " }\n"
489 " ]\n"
490 " }\n"
491 "]"
492 ),
493 actual: common_chat_msgs_to_json_oaicompat<json>(msgs: {message_assist_call_python}).dump(indent: 2));
494
495 auto res = common_chat_msgs_parse_oaicompat(messages: json::parse(i: "[{\"role\": \"assistant\", \"tool_calls\": []}]"));
496 assert_equals<size_t>(expected: 1, actual: res.size());
497 assert_equals<std::string>(expected: res[0].role, actual: "assistant");
498 assert_equals(expected: true, actual: res[0].content.empty());
499 assert_equals(expected: true, actual: res[0].tool_calls.empty());
500
501 try {
502 common_chat_msgs_parse_oaicompat(messages: json::parse(i: "[{\"role\": \"assistant\"}]"));
503 throw std::runtime_error("Expected exception");
504 } catch (const std::exception & e) {
505 if (std::string(e.what()).find(s: "'content'") == std::string::npos) {
506 throw std::runtime_error("Expected exception about missing 'content'");
507 }
508 }
509}
510
511static void test_tools_oaicompat_json_conversion() {
512 printf(format: "[%s]\n", __func__);
513 std::vector<common_chat_tool> tools{
514 special_function_tool,
515 python_tool,
516 code_interpreter_tool,
517 };
518
519 for (const auto & tool : tools) {
520 auto oai_json = common_chat_tools_to_json_oaicompat<json>(tools: {tool});
521 auto tools2 = common_chat_tools_parse_oaicompat(tools: oai_json);
522 assert_equals(expected: (size_t) 1, actual: tools2.size());
523 auto tool2 = tools2[0];
524 assert_equals(expected: tool.name, actual: tool2.name);
525 assert_equals(expected: tool.description, actual: tool2.description);
526 assert_equals(expected: json::parse(i: tool.parameters).dump(indent: 2), actual: json::parse(i&: tool2.parameters).dump(indent: 2));
527 }
528
529 assert_equals(
530 expected: std::string(
531 "[\n"
532 " {\n"
533 " \"type\": \"function\",\n"
534 " \"function\": {\n"
535 " \"name\": \"special_function\",\n"
536 " \"description\": \"I'm special\",\n"
537 " \"parameters\": {\n"
538 " \"type\": \"object\",\n"
539 " \"properties\": {\n"
540 " \"arg1\": {\n"
541 " \"type\": \"integer\",\n"
542 " \"description\": \"The arg.\"\n"
543 " }\n"
544 " },\n"
545 " \"required\": [\n"
546 " \"arg1\"\n"
547 " ]\n"
548 " }\n"
549 " }\n"
550 " }\n"
551 "]"
552 ),
553 actual: common_chat_tools_to_json_oaicompat<json>(tools: {special_function_tool}).dump(indent: 2));
554}
555
556static void test_template_output_parsers() {
557 printf(format: "[%s]\n", __func__);
558
559 common_chat_templates_inputs inputs_no_tools;
560 inputs_no_tools.messages = {message_user};
561
562 common_chat_templates_inputs inputs_tools;
563 inputs_tools.messages = {message_user};
564 inputs_tools.tools = {special_function_tool};
565
566 common_chat_templates_inputs inputs_tools_builtin;
567 inputs_tools_builtin.messages = {message_user};
568 inputs_tools_builtin.tools = {python_tool};
569
570 {
571 // Not supported yet
572 auto tmpls = read_templates(path: "models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja");
573 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
574 assert_equals(expected: COMMON_CHAT_FORMAT_GENERIC, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
575 }
576 {
577 auto tmpls = read_templates(path: "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja");
578 std::vector<std::string> end_tokens{ "<|END_OF_TURN_TOKEN|>" };
579
580 for (const auto & inputs : { inputs_no_tools, inputs_tools }) {
581 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs);
582 assert_equals(expected: COMMON_CHAT_FORMAT_COMMAND_R7B, actual: params.format);
583 assert_equals(expected: false, actual: params.thinking_forced_open);
584 }
585
586 assert_msg_equals(expected: message_assist,
587 actual: common_chat_parse(
588 input: "Hello, world!\nWhat's up?",
589 /* is_partial= */ false,
590 syntax: {.format: COMMON_CHAT_FORMAT_COMMAND_R7B}));
591 assert_msg_equals(expected: message_assist,
592 actual: common_chat_parse(
593 input: "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>",
594 /* is_partial= */ false,
595 syntax: {.format: COMMON_CHAT_FORMAT_COMMAND_R7B}));
596 assert_msg_equals(expected: message_assist_thoughts,
597 actual: common_chat_parse(
598 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
599 "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>",
600 /* is_partial= */ false,
601 syntax: {
602 /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B,
603 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
604 }));
605 assert_msg_equals(expected: message_assist_thoughts_unparsed_deepseek,
606 actual: common_chat_parse(
607 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
608 "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>",
609 /* is_partial= */ false,
610 syntax: {
611 /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B,
612 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
613 /* .reasoning_in_content = */ true,
614 /* .thinking_forced_open = */ false,
615 }));
616 assert_msg_equals(expected: message_assist_thoughts_unparsed_r7b,
617 actual: common_chat_parse(
618 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
619 "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>",
620 /* is_partial= */ false,
621 syntax: {.format: COMMON_CHAT_FORMAT_COMMAND_R7B}));
622 assert_msg_equals(expected: message_assist_thoughts,
623 actual: common_chat_parse(
624 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
625 "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>",
626 /* is_partial= */ false,
627 syntax: {
628 /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B,
629 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
630 }));
631 assert_msg_equals(expected: message_assist_thoughts_call_idx,
632 actual: common_chat_parse(
633 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
634 "<|START_ACTION|>[\n"
635 " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n"
636 "]<|END_ACTION|>",
637 /* is_partial= */ false,
638 syntax: {
639 /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B,
640 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
641 }));
642 assert_msg_equals(expected: message_assist_thoughts_no_content,
643 actual: common_chat_parse(
644 input: "<|START_THINKING|>I'm\nthinking<|END_THINKING|>"
645 "<|START_ACTION|>[\n"
646 " {\"tool_call_id\": \"0\", \"tool_name\": \"special",
647 /* is_partial= */ true,
648 syntax: {
649 /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B,
650 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
651 }));
652
653 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_idx, tools,
654 expected_delta: "<|START_THINKING|><|END_THINKING|>"
655 "<|START_ACTION|>[\n"
656 " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n"
657 "]<|END_ACTION|>",
658 /* expect_grammar_triggered= */ true,
659 /* test_grammar_if_triggered= */ true,
660 reasoning_format: COMMON_REASONING_FORMAT_DEEPSEEK);
661 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools,
662 expected_delta: "<|START_RESPONSE|>Hello, world!\n"
663 "What's up?<|END_RESPONSE|>",
664 /* expect_grammar_triggered= */ false);
665 }
666 {
667 auto tmpls = read_templates(path: "models/templates/google-gemma-2-2b-it.jinja");
668 std::vector<std::string> end_tokens{ "<end_of_turn>" };
669
670 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
671 assert_equals(expected: COMMON_CHAT_FORMAT_GENERIC, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
672 assert_equals(expected: COMMON_CHAT_FORMAT_GENERIC,
673 actual: common_chat_templates_apply(
674 tmpls: read_templates(path: "models/templates/microsoft-Phi-3.5-mini-instruct.jinja").get(),
675 inputs: inputs_tools)
676 .format);
677
678 // Generic tool calls doesn't generate / parse content-only messages symmetrically.
679
680 assert_equals(
681 expected: simple_assist_msg(content: "{ \"tool_call\" : { \"name\" : \"t"),
682 actual: common_chat_parse(
683 input: "{ \"tool_call\" : { \"name\" : \"t",
684 /* is_partial= */ true,
685 syntax: {
686 /* .format = */ COMMON_CHAT_FORMAT_GENERIC,
687 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
688 /* .reasoning_in_content = */ false,
689 /* .thinking_forced_open = */ true,
690 /* .parse_tool_calls = */ false,
691 }));
692 assert_equals(
693 expected: message_assist_empty,
694 actual: common_chat_parse(
695 input: "{ \"tool_call\" : { \"name\" : \"t",
696 /* is_partial= */ true,
697 syntax: {.format: COMMON_CHAT_FORMAT_GENERIC}));
698
699 assert_equals(
700 expected: simple_assist_msg(content: "", reasoning_content: "", tool_name: "puppeteer_screenshot", arguments: "{\"name\":\"servethehome_homepage\","),
701 actual: common_chat_parse(
702 input: R"({"tool_call": {"name": "puppeteer_screenshot", "arguments": {"name": "servethehome_homepage",)",
703 /* is_partial= */ true,
704 syntax: {.format: COMMON_CHAT_FORMAT_GENERIC}));
705
706 assert_equals(
707 expected: message_assist_call_empty_args,
708 actual: common_chat_parse(
709 input: "{ \"tool_call\" : { \"name\" : \"special_function\"",
710 /* is_partial= */ true,
711 syntax: {.format: COMMON_CHAT_FORMAT_GENERIC}));
712 assert_equals(
713 expected: message_assist_call_cutoff_args,
714 actual: common_chat_parse(
715 input: "{ \"tool_call\" : { \"name\" : \"special_function\", \"arguments\" : { \"arg",
716 /* is_partial= */ true,
717 syntax: {.format: COMMON_CHAT_FORMAT_GENERIC}));
718
719 assert_msg_equals(expected: message_assist,
720 actual: common_chat_parse(
721 input: "{\n"
722 " \"response\": \"Hello, world!\\nWhat's up?\"\n"
723 "}",
724 /* is_partial= */ false,
725 syntax: {.format: COMMON_CHAT_FORMAT_GENERIC}));
726 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_id, tools,
727 expected_delta: "{\n"
728 " \"tool_calls\": [\n"
729 " {\n"
730 " \"name\": \"special_function\",\n"
731 " \"arguments\": {\n"
732 " \"arg1\": 1\n"
733 " },\n"
734 " \"id\": \"123456789\"\n"
735 " }\n"
736 " ]\n"
737 "}");
738 }
739 {
740 auto tmpls = read_templates(path: "models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja");
741 std::vector<std::string> end_tokens{ "</s>" };
742
743 assert_equals(expected: COMMON_CHAT_FORMAT_MISTRAL_NEMO, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
744
745 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
746 test_templates(
747 tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_id, tools,
748 expected_delta: "[TOOL_CALLS][{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}, \"id\": \"123456789\"}]");
749 }
750 {
751 assert_msg_equals(
752 expected: simple_assist_msg(content: "Réponse", reasoning_content: "raisonnement"),
753 actual: common_chat_parse(
754 input: message_assist_thoughts_unparsed_magistral.content,
755 /* is_partial= */ false,
756 syntax: {
757 /* .format = */ COMMON_CHAT_FORMAT_MAGISTRAL,
758 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
759 }));
760 }
761 {
762 auto tmpls = read_templates(path: "models/templates/Qwen-QwQ-32B.jinja");
763 std::vector<std::string> end_tokens{ "<|im_end|>" };
764
765 assert_equals(expected: COMMON_CHAT_FORMAT_HERMES_2_PRO, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
766 assert_equals(expected: COMMON_CHAT_FORMAT_HERMES_2_PRO, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
767 }
768 {
769 auto tmpls = read_templates(path: "models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja");
770 std::vector<std::string> end_tokens{ "<|im_end|>" };
771
772 assert_equals(expected: COMMON_CHAT_FORMAT_HERMES_2_PRO, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
773 assert_equals(expected: COMMON_CHAT_FORMAT_HERMES_2_PRO, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
774 assert_equals(
775 expected: COMMON_CHAT_FORMAT_HERMES_2_PRO,
776 actual: common_chat_templates_apply(
777 tmpls: read_templates(path: "models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja").get(),
778 inputs: inputs_tools)
779 .format);
780 assert_equals(
781 expected: COMMON_CHAT_FORMAT_HERMES_2_PRO,
782 actual: common_chat_templates_apply(
783 tmpls: read_templates(path: "models/templates/Qwen-Qwen2.5-7B-Instruct.jinja").get(),
784 inputs: inputs_tools)
785 .format);
786
787 // Test parsing
788 assert_msg_equals(
789 expected: simple_assist_msg(content: "", reasoning_content: "", tool_name: "python", arguments: ""),
790 actual: common_chat_parse(
791 input: "```json\n"
792 "<function_call> { \"name\" : \"python\"",
793 /* is_partial= */ true,
794 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
795 assert_msg_equals(
796 expected: simple_assist_msg(content: "Let's call something\n"),
797 actual: common_chat_parse(
798 input: "Let's call something\n"
799 "<tool_call>{\"name\"",
800 /* is_partial= */ true,
801 syntax: {
802 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
803 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
804 }));
805 assert_msg_equals(
806 expected: simple_assist_msg(content: "Let's call something\n"),
807 actual: common_chat_parse(
808 input: "Let's call something\n"
809 "<tool_call>{\"name",
810 /* is_partial= */ true,
811 syntax: {
812 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
813 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
814 }));
815 assert_msg_equals(expected: message_assist_call_thoughts,
816 actual: common_chat_parse(
817 // QwQ-32B's template adds a trailing <think> if add_generation_prompt
818 input: "I'm\nthinking</think>\n"
819 "<tool_call>{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>",
820 /* is_partial= */ false,
821 syntax: {
822 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
823 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
824 /* .reasoning_in_content = */ false,
825 /* .thinking_forced_open = */ true,
826 }));
827 assert_msg_equals(
828 expected: message_assist_call,
829 actual: common_chat_parse(
830 input: "<tool_call>\n"
831 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
832 "</tool_call>",
833 /* is_partial= */ false,
834 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
835 assert_msg_equals(expected: message_assist_call_content,
836 actual: common_chat_parse(
837 input: "Hello, world!\nWhat's up?<tool_call>\n"
838 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
839 "</tool_call>",
840 /* is_partial= */ false,
841 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
842 assert_msg_equals(
843 expected: message_assist_call,
844 actual: common_chat_parse(
845 input: "<function=special_function>{\"arg1\": 1}</function>",
846 /* is_partial= */ false,
847 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
848 assert_msg_equals(
849 expected: message_assist_call,
850 actual: common_chat_parse(
851 input: "<function name=\"special_function\">\n"
852 "{\"arg1\": 1}\n"
853 "</function>",
854 /* is_partial= */ false,
855 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
856 assert_msg_equals(
857 expected: message_assist_call,
858 actual: common_chat_parse(
859 input: "<tool>\n"
860 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
861 "</tool>",
862 /* is_partial= */ false,
863 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
864 assert_msg_equals(
865 expected: message_assist_call,
866 actual: common_chat_parse(
867 input: "<tools>\n"
868 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
869 "</tools>",
870 /* is_partial= */ false,
871 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
872 assert_msg_equals(
873 expected: message_assist_call,
874 actual: common_chat_parse(
875 input: "<response>\n"
876 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
877 "</response>",
878 /* is_partial= */ false,
879 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
880 assert_msg_equals(
881 expected: message_assist_call,
882 actual: common_chat_parse(
883 input: "```xml\n"
884 "<response>\n"
885 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
886 "</response>\n"
887 "```",
888 /* is_partial= */ false,
889 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
890 assert_msg_equals(
891 expected: message_assist_call,
892 actual: common_chat_parse(
893 input: "```xml\n"
894 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
895 "```",
896 /* is_partial= */ false,
897 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
898 assert_msg_equals(
899 expected: message_assist_call,
900 actual: common_chat_parse(
901 input: "```\n"
902 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
903 "```",
904 /* is_partial= */ false,
905 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
906 assert_msg_equals(
907 expected: message_assist_call,
908 actual: common_chat_parse(
909 input: "```\n"
910 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
911 "```",
912 /* is_partial= */ false,
913 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
914 assert_msg_equals(
915 expected: message_assist_call,
916 actual: common_chat_parse(
917 input: "```json\n"
918 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
919 "```",
920 /* is_partial= */ false,
921 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
922 assert_msg_equals(
923 expected: message_assist_call,
924 actual: common_chat_parse(
925 input: "```json\n"
926 "\n"
927 " <function_call> {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}} \n"
928 " </function_call> \n"
929 "``` ",
930 /* is_partial= */ false,
931 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
932 assert_msg_equals(
933 expected: message_assist_call,
934 actual: common_chat_parse(
935 input: "<json>\n"
936 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
937 "</json>",
938 /* is_partial= */ false,
939 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
940 assert_msg_equals(
941 expected: message_assist_call,
942 actual: common_chat_parse(
943 input: "<xml>\n"
944 " {\n"
945 " \"name\": \"special_function\", \"arguments\": {\"arg1\": 1}\n"
946 " }\n"
947 "</xml>",
948 /* is_partial= */ false,
949 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
950 assert_msg_equals(
951 expected: message_assist_call,
952 actual: common_chat_parse(
953 input: "<JSON>\n"
954 " {\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
955 "</JSON>",
956 /* is_partial= */ false,
957 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
958 assert_msg_equals(
959 expected: message_assist_call,
960 actual: common_chat_parse(
961 input: "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
962 /* is_partial= */ false,
963 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
964 assert_msg_equals(
965 expected: message_assist_call,
966 actual: common_chat_parse(
967 input: "{\n \"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
968 /* is_partial= */ false,
969 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
970
971 // Test multiple tool calls
972 common_chat_msg message_assist_multiple_calls;
973 message_assist_multiple_calls.role = "assistant";
974 message_assist_multiple_calls.content = "";
975 message_assist_multiple_calls.tool_calls.push_back(x: {.name: "special_function", .arguments: "{\"arg1\": 1}", .id: ""});
976 message_assist_multiple_calls.tool_calls.push_back(x: {.name: "python", .arguments: "{\"code\":\"print('hello')\"}", .id: ""});
977
978 assert_msg_equals(
979 expected: message_assist_multiple_calls,
980 actual: common_chat_parse(
981 input: "<tool_call>\n"
982 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
983 "</tool_call>\n"
984 "<tool_call>\n"
985 "{\"name\": \"python\", \"arguments\": {\"code\":\"print('hello')\"}}\n"
986 "</tool_call>",
987 /* is_partial= */ false,
988 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
989
990 assert_msg_equals(
991 expected: message_assist_multiple_calls,
992 actual: common_chat_parse(
993 input: "<function=special_function>{\"arg1\": 1}</function>\n"
994 "<function=python>{\"code\":\"print('hello')\"}</function>",
995 /* is_partial= */ false,
996 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
997
998 assert_msg_equals(
999 expected: simple_assist_msg(
1000 content: "This is not a tool call:",
1001 reasoning_content: "",
1002 tool_name: "special_function",
1003 arguments: "{\"arg1\": 1}"),
1004 actual: common_chat_parse(
1005 input: "This is not a tool call:\n"
1006 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
1007 /* is_partial= */ false,
1008 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
1009 assert_msg_equals(expected: message_assist,
1010 actual: common_chat_parse(
1011 input: "Hello, world!\nWhat's up?",
1012 /* is_partial= */ false,
1013 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
1014 assert_msg_equals(expected: message_assist_thoughts_unparsed_deepseek,
1015 actual: common_chat_parse(
1016 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1017 /* is_partial= */ false,
1018 syntax: {.format: COMMON_CHAT_FORMAT_HERMES_2_PRO}));
1019 // assert_msg_equals(message_assist_thoughts_unparsed_deepseek,
1020 // common_chat_parse(
1021 // "I'm\nthinking</think>Hello, world!\nWhat's up?",
1022 // COMMON_CHAT_FORMAT_HERMES_2_PRO));
1023 assert_msg_equals(expected: message_assist_thoughts,
1024 actual: common_chat_parse(
1025 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1026 /* is_partial= */ false,
1027 syntax: {
1028 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1029 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1030 }));
1031 assert_msg_equals(expected: message_assist_thoughts,
1032 actual: common_chat_parse(
1033 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1034 /* is_partial= */ true,
1035 syntax: {
1036 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1037 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1038 }));
1039 assert_msg_equals(expected: message_assist_thoughts_unparsed_md,
1040 actual: common_chat_parse(
1041 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```",
1042 /* is_partial= */ false,
1043 syntax: {
1044 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1045 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1046 /* .reasoning_in_content = */ true,
1047 /* .thinking_forced_open = */ false,
1048 /* .parse_tool_calls = */ false,
1049 }));
1050 assert_msg_equals(expected: message_assist_thoughts_unparsed_md_partial,
1051 actual: common_chat_parse(
1052 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```",
1053 /* is_partial= */ true,
1054 syntax: {
1055 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1056 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1057 /* .reasoning_in_content = */ true,
1058 /* .thinking_forced_open = */ false,
1059 }));
1060 assert_msg_equals(expected: message_assist_thoughts_unopened_unparsed,
1061 actual: common_chat_parse(
1062 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1063 /* is_partial= */ false,
1064 syntax: {
1065 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1066 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1067 }));
1068 assert_msg_equals(expected: message_assist_thoughts,
1069 actual: common_chat_parse(
1070 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1071 /* is_partial= */ false,
1072 syntax: {
1073 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1074 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1075 /* .reasoning_in_content = */ false,
1076 /* .thinking_forced_open = */ true,
1077 }));
1078
1079 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1080 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1081 expected_delta: "<tool_call>\n"
1082 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
1083 "</tool_call>");
1084
1085 // Test multiple tool calls with template
1086 common_chat_msg message_assist_multiple_calls_template;
1087 message_assist_multiple_calls_template.role = "assistant";
1088 message_assist_multiple_calls_template.content = "";
1089 message_assist_multiple_calls_template.tool_calls.push_back(x: {.name: "special_function", .arguments: "{\"arg1\": 1}", .id: ""});
1090 message_assist_multiple_calls_template.tool_calls.push_back(x: {.name: "python", .arguments: "{\"code\":\"print('test')\"}", .id: ""});
1091
1092 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_multiple_calls_template, tools,
1093 expected_delta: "<tool_call>\n"
1094 "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
1095 "</tool_call>\n"
1096 "<tool_call>\n"
1097 "{\"name\": \"python\", \"arguments\": {\"code\":\"print('test')\"}}\n"
1098 "</tool_call>");
1099
1100 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_python_lines, tools,
1101 expected_delta: "<tool_call>\n"
1102 "{\"name\": \"python\", \"arguments\": {\"code\":\"# This is a program:\\nprint('hey')\"}}\n"
1103 "</tool_call>");
1104 assert_msg_equals(
1105 expected: simple_assist_msg(content: "", /* reasoning_content= */ "<tool_call>nah uhg</tool_call>"),
1106 actual: common_chat_parse(
1107 input: "<think><tool_call>nah uhg</tool_call>",
1108 /* is_partial= */ false,
1109 syntax: {
1110 /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO,
1111 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1112 }));
1113 }
1114 {
1115 auto tmpls = read_templates(path: "models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja");
1116 std::vector<std::string> end_tokens{ "<|eom_id|>", "<|eot_id|>" };
1117
1118 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1119 assert_equals(expected: COMMON_CHAT_FORMAT_LLAMA_3_X, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1120 assert_equals(expected: COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS,
1121 actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools_builtin).format);
1122 assert_equals(expected: COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS,
1123 actual: common_chat_templates_apply(
1124 tmpls: read_templates(path: "models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja").get(),
1125 inputs: inputs_tools_builtin)
1126 .format);
1127
1128 assert_equals(
1129 expected: message_assist_call,
1130 actual: common_chat_parse(
1131 input: "{\"name\": \"special_function\", \"parameters\": {\"arg1\": 1}}",
1132 /* is_partial= */ false,
1133 syntax: {.format: COMMON_CHAT_FORMAT_LLAMA_3_X}));
1134
1135 // test_templates(tmpls.get(), end_tokens, message_assist, tools, R"(?)", /* expect_grammar_triggered= */ false);
1136 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_code_interpreter, tools: llama_3_1_tools,
1137 expected_delta: "<|python_tag|>code_interpreter.call(code=\"print('hey')\")");
1138 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_python, tools,
1139 expected_delta: "<|python_tag|>python.call(code=\"print('hey')\")");
1140 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1141 expected_delta: "{\"name\": \"special_function\", \"parameters\": {\"arg1\": 1}}");
1142 }
1143 {
1144 auto tmpls = read_templates(path: "models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja");
1145 std::vector<std::string> end_tokens{ "<|eom_id|>", "<|eot_id|>" };
1146
1147 assert_equals(expected: COMMON_CHAT_FORMAT_LLAMA_3_X, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1148 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1149
1150 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1151 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1152 expected_delta: "{\"name\": \"special_function\", \"parameters\": {\"arg1\": 1}}");
1153 }
1154 {
1155 auto tmpls = read_templates(path: "models/templates/meetkai-functionary-medium-v3.1.jinja");
1156 std::vector<std::string> end_tokens{ "<|eom_id|>", "<|eot_id|>" };
1157
1158 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY,
1159 actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1160 assert_equals(expected: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1,
1161 actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1162 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY,
1163 actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1164
1165 for (auto is_partial : { false, true }) {
1166 assert_equals(
1167 expected: message_assist_call,
1168 actual: common_chat_parse(
1169 input: "<function=special_function>{\"arg1\": 1}</function>",
1170 is_partial,
1171 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1}));
1172 }
1173
1174 assert_equals(
1175 expected: message_assist_call,
1176 actual: common_chat_parse(
1177 input: "<function=special_function>{\"arg1\": 1}<",
1178 /* is_partial= */ true,
1179 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1}));
1180
1181 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1182 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1183 expected_delta: "<function=special_function>{\"arg1\": 1}</function>");
1184 }
1185 {
1186 auto tmpls = read_templates(path: "models/templates/meetkai-functionary-medium-v3.2.jinja");
1187 std::vector<std::string> end_tokens{ "<|eom_id|>", "<|eot_id|>" };
1188
1189 assert_equals(expected: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1190 assert_equals(expected: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1191
1192 assert_msg_equals(
1193 expected: simple_assist_msg(
1194 content: "Hello, world!\nnono\nWhat's up?",
1195 reasoning_content: "",
1196 tool_name: "special_function",
1197 arguments: "{\"arg1\": 1}"),
1198 actual: common_chat_parse(
1199 input: "all\n"
1200 "Hello, world!\n"
1201 "nono\n"
1202 "What's up?>>>special_function\n"
1203 "{\"arg1\": 1}\n",
1204 /* is_partial= */ false,
1205 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2}));
1206 assert_msg_equals(expected: message_assist_call_python_lines,
1207 actual: common_chat_parse(
1208 input: "python\n"
1209 "# This is a program:\n"
1210 "print('hey')",
1211 /* is_partial= */ false,
1212 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2}));
1213 assert_msg_equals(expected: message_assist_call_python_lines_unclosed,
1214 actual: common_chat_parse(
1215 input: "python\n"
1216 "# This is a program:\n"
1217 "print('hey')",
1218 /* is_partial= */ true,
1219 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2}));
1220 assert_msg_equals(expected: message_assist_call,
1221 actual: common_chat_parse(
1222 input: "special_function\n"
1223 "{\"arg1\": 1} \n ",
1224 /* is_partial= */ false,
1225 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2}));
1226 assert_msg_equals(expected: message_assist,
1227 actual: common_chat_parse(
1228 input: "all\n"
1229 "Hello, world!\nWhat's up?",
1230 /* is_partial= */ false,
1231 syntax: {.format: COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2}));
1232
1233 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools: {},
1234 expected_delta: "all\n"
1235 "Hello, world!\n"
1236 "What's up?",
1237 /* expect_grammar_triggered= */ false);
1238 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1239 expected_delta: "special_function\n"
1240 "{\"arg1\": 1}");
1241 }
1242 {
1243 auto tmpls = read_templates(path: "models/templates/fireworks-ai-llama-3-firefunction-v2.jinja");
1244 std::vector<std::string> end_tokens{ "<|eot_id|>" };
1245
1246 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1247 assert_equals(expected: COMMON_CHAT_FORMAT_FIREFUNCTION_V2, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1248
1249 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1250 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1251 expected_delta: " functools[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]");
1252 }
1253 {
1254 // Original DeepSeek R1 template. Leaves <|tool▁calls▁begin|> and others unclosed. Our logic fixes the prompt.
1255 auto tmpls = read_templates(path: "models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja");
1256 std::vector<std::string> end_tokens{ "<|end▁of▁sentence|>" };
1257
1258 for (const auto & inputs : { inputs_no_tools, inputs_tools }) {
1259 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs);
1260 assert_equals(expected: COMMON_CHAT_FORMAT_DEEPSEEK_R1, actual: params.format);
1261 assert_equals(expected: true, actual: params.thinking_forced_open);
1262 }
1263
1264 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1265 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_thoughts, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1266 assert_msg_equals(
1267 expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "<think>I'm\nthinking"),
1268 actual: common_chat_parse(
1269 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1270 /* is_partial= */ false,
1271 syntax: {
1272 .format: COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1273 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1274 /* .reasoning_in_content = */ false,
1275 /* .thinking_forced_open = */ true,
1276 }));
1277 assert_msg_equals(
1278 expected: simple_assist_msg(content: "", reasoning_content: "I need to remember the correct syntax. It starts with <|tool▁calls▁begin|> and ends with"),
1279 actual: common_chat_parse(
1280 input: "I need to remember the correct syntax. It starts with <|tool▁calls▁begin|> and ends with",
1281 /* is_partial= */ true,
1282 syntax: {
1283 .format: COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1284 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1285 /* .reasoning_in_content = */ false,
1286 /* .thinking_forced_open = */ true,
1287 }));
1288 assert_msg_equals(expected: message_assist_thoughts,
1289 actual: common_chat_parse(
1290 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1291 /* is_partial= */ false,
1292 syntax: {
1293 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1294 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1295 }));
1296 assert_msg_equals(expected: message_assist_thoughts_unopened_unparsed,
1297 actual: common_chat_parse(
1298 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1299 /* is_partial= */ false,
1300 syntax: {
1301 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1302 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1303 }));
1304 assert_msg_equals(expected: message_assist_thoughts,
1305 actual: common_chat_parse(
1306 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1307 /* is_partial= */ false,
1308 syntax: {
1309 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1310 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1311 /* .reasoning_in_content = */ false,
1312 /* .thinking_forced_open = */ true,
1313 }));
1314 assert_msg_equals(expected: message_assist_thoughts,
1315 // Latest template update (ast of 20250209) adds a trailing <think>\n if add_generation_prompt is true.
1316 actual: common_chat_parse(
1317 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1318 /* is_partial= */ false,
1319 syntax: {
1320 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1321 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1322 /* .reasoning_in_content = */ false,
1323 /* .thinking_forced_open = */ true,
1324 }));
1325 // test_templates(tmpls.get(), end_tokens, message_assist_call, tools,
1326 // "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n"
1327 // "```json\n"
1328 // "{\"arg1\": 1}\n"
1329 // // Look what's not here: <|tool▁calls▁end|> (also missing the <|end▁of▁sentence|>, but that is removed lazily by the test's delta logic)
1330 // "```<|tool▁call▁end|>",
1331 // /* expect_grammar_triggered= */ true,
1332 // /* test_grammar_if_triggered= */ false);
1333 }
1334 {
1335 // Replacement DeepSeek R1 template. Makes the Distill Qwen 7B/32B models happy to call tools and all.
1336 auto tmpls = read_templates(path: "models/templates/llama-cpp-deepseek-r1.jinja");
1337 std::vector<std::string> end_tokens{ "<|end▁of▁sentence|>" };
1338
1339 assert_equals(expected: COMMON_CHAT_FORMAT_DEEPSEEK_R1, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1340 assert_equals(expected: COMMON_CHAT_FORMAT_DEEPSEEK_R1, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1341
1342 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1343 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_thoughts, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1344 assert_msg_equals(expected: message_assist_thoughts_unparsed_deepseek,
1345 actual: common_chat_parse(
1346 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1347 /* is_partial= */ false,
1348 syntax: {.format: COMMON_CHAT_FORMAT_DEEPSEEK_R1}));
1349 assert_msg_equals(expected: message_assist_thoughts,
1350 actual: common_chat_parse(
1351 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1352 /* is_partial= */ false,
1353 syntax: {
1354 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1355 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1356 }));
1357 assert_msg_equals(expected: message_assist_thoughts,
1358 actual: common_chat_parse(
1359 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1360 /* is_partial= */ false,
1361 syntax: {
1362 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1363 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1364 /* .reasoning_in_content = */ false,
1365 /* .thinking_forced_open = */ true,
1366 }));
1367
1368 assert_msg_equals(expected: message_assist_call_thoughts_unparsed,
1369 actual: common_chat_parse(
1370 input: "<think>I'm\nthinking</think>\n\n"
1371 "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n"
1372 "```json\n"
1373 "{\"arg1\": 1}\n"
1374 "```<|tool▁call▁end|><|tool▁calls▁end|>",
1375 /* is_partial= */ false,
1376 syntax: {.format: COMMON_CHAT_FORMAT_DEEPSEEK_R1}));
1377 assert_msg_equals(expected: message_assist_call,
1378 actual: common_chat_parse(
1379 input: "<|tool▁calls|>function<|tool▁sep|>special_function\n"
1380 "```json\n"
1381 "{\"arg1\": 1}\n"
1382 "```<|tool▁call▁end|><|tool▁calls▁end|>",
1383 /* is_partial= */ false,
1384 syntax: {.format: COMMON_CHAT_FORMAT_DEEPSEEK_R1}));
1385
1386 assert_msg_equals(expected: message_assist_call_thoughts,
1387 actual: common_chat_parse(
1388 input: "<think>I'm\nthinking</think>\n\n"
1389 "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n"
1390 "```json\n"
1391 "{\"arg1\": 1}\n"
1392 "```<|tool▁call▁end|><|tool▁calls▁end|>",
1393 /* is_partial= */ false,
1394 syntax: {
1395 /* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_R1,
1396 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1397 }));
1398 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1399 expected_delta: "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n"
1400 "```json\n"
1401 "{\"arg1\": 1}\n"
1402 "```<|tool▁call▁end|><|tool▁calls▁end|>");
1403 }
1404 {
1405 auto tmpls = read_templates(path: "models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja");
1406 std::vector<std::string> end_tokens{ "<|end_of_text|>" };
1407
1408 assert_equals(expected: COMMON_CHAT_FORMAT_GRANITE, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1409
1410 assert_equals(expected: COMMON_CHAT_FORMAT_GRANITE, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1411
1412 // Test parsing regular content
1413 assert_msg_equals(expected: message_assist,
1414 actual: common_chat_parse(
1415 input: "Hello, world!\nWhat's up?",
1416 /* is_partial= */ false,
1417 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1418 assert_msg_equals(
1419 expected: message_assist,
1420 actual: common_chat_parse(
1421 input: "Hello, world!\nWhat's up?",
1422 /* is_partial= */ true,
1423 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1424
1425 // Test parsing content with thinking
1426 assert_msg_equals(expected: message_assist_thoughts,
1427 actual: common_chat_parse(
1428 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1429 /* is_partial= */ false,
1430 syntax: {
1431 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1432 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1433 }));
1434 assert_msg_equals(expected: message_assist_thoughts_unparsed_deepseek,
1435 actual: common_chat_parse(
1436 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1437 /* is_partial= */ false,
1438 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1439 assert_msg_equals(expected: message_assist_thoughts,
1440 actual: common_chat_parse(
1441 input: "<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?",
1442 /* is_partial= */ true,
1443 syntax: {
1444 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1445 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1446 }));
1447 assert_msg_equals(expected: message_assist_thoughts,
1448 actual: common_chat_parse(
1449 input: "<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>",
1450 /* is_partial= */ false,
1451 syntax: {
1452 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1453 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1454 }));
1455 assert_msg_equals(expected: simple_assist_msg(content: "<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>"),
1456 actual: common_chat_parse(
1457 input: "<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>",
1458 /* is_partial= */ false,
1459 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1460 assert_msg_equals(expected: message_assist_empty,
1461 actual: common_chat_parse(
1462 input: "<think",
1463 /* is_partial= */ true,
1464 syntax: {
1465 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1466 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1467 }));
1468 assert_msg_equals(expected: message_assist_empty,
1469 actual: common_chat_parse(
1470 input: "<think",
1471 /* is_partial= */ true,
1472 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1473 assert_msg_equals(expected: message_assist_thoughts_no_content,
1474 actual: common_chat_parse(
1475 input: "<think>I'm\nthinking",
1476 /* is_partial= */ true,
1477 syntax: {
1478 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1479 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1480 }));
1481 assert_msg_equals(
1482 expected: message_assist_empty,
1483 actual: common_chat_parse(
1484 input: "<think>I'm\nthinking</think><response",
1485 /* is_partial= */ true,
1486 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1487
1488 // Test parsing tool calls
1489 assert_msg_equals(expected: message_assist_call,
1490 actual: common_chat_parse(
1491 input: "<|tool_call|>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]",
1492 /* is_partial= */ false,
1493 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1494 assert_msg_equals(
1495 expected: message_assist_call_empty_args,
1496 actual: common_chat_parse(
1497 input: "<|tool_call|>[{\"name\": \"special_function\"",
1498 /* is_partial= */ true,
1499 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1500 assert_msg_equals(
1501 expected: message_assist_call_cutoff_args,
1502 actual: common_chat_parse(
1503 input: "<|tool_call|>[{\"name\": \"special_function\", \"arguments\": {\"arg",
1504 /* is_partial= */ true,
1505 syntax: {.format: COMMON_CHAT_FORMAT_GRANITE}));
1506 assert_msg_equals(
1507 expected: message_assist_call_cutoff_args,
1508 actual: common_chat_parse(
1509 input: "<|tool_call|>[{\"name\": \"special_function\", \"arguments\": {\"arg",
1510 /* is_partial= */ true,
1511 syntax: {
1512 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1513 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1514 }));
1515
1516 // Test parsing tool calls with thinking
1517 assert_msg_equals(
1518 expected: message_assist_call_thoughts,
1519 actual: common_chat_parse(
1520 input: "<think>I'm\nthinking</think><|tool_call|>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}, {",
1521 /* is_partial= */ true,
1522 syntax: {
1523 /* .format = */ COMMON_CHAT_FORMAT_GRANITE,
1524 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1525 }));
1526
1527 // Test template generation for regular content
1528 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools,
1529 expected_delta: "Hello, world!\nWhat's up?",
1530 /* expect_grammar_triggered= */ false);
1531
1532 // Test template generation for tool calls
1533 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call_id, tools,
1534 expected_delta: "{\n"
1535 " \"tool_calls\": [\n"
1536 " {\n"
1537 " \"name\": \"special_function\",\n"
1538 " \"arguments\": {\n"
1539 " \"arg1\": 1\n"
1540 " },\n"
1541 " \"id\": \"123456789\"\n"
1542 " }\n"
1543 " ]\n"
1544 "}",
1545 /* expect_grammar_triggered= */ false
1546 );
1547 }
1548 {
1549 auto tmpls = read_templates(path: "models/templates/openai-gpt-oss-120b.jinja");
1550 std::vector<std::string> end_tokens{ "<|return|>", "<|call|>" };
1551
1552 assert_equals(expected: COMMON_CHAT_FORMAT_GPT_OSS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1553 assert_equals(expected: COMMON_CHAT_FORMAT_GPT_OSS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1554
1555 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthink"),
1556 actual: common_chat_parse(
1557 input: "<|channel|>analysis<|message|>I'm\nthink",
1558 /* is_partial= */ true,
1559 syntax: {
1560 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1561 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1562 }));
1563 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking"),
1564 actual: common_chat_parse(
1565 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>",
1566 /* is_partial= */ true,
1567 syntax: {
1568 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1569 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1570 }));
1571 assert_msg_equals(expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking"),
1572 actual: common_chat_parse(
1573 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1574 "<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1575 /* is_partial= */ false,
1576 syntax: {
1577 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1578 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1579 }));
1580 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1"),
1581 actual: common_chat_parse(
1582 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1583 "<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1",
1584 /* is_partial= */ true,
1585 syntax: {
1586 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1587 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1588 }));
1589 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1"),
1590 actual: common_chat_parse(
1591 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1592 "<|start|>assistant<|channel|>commentary to=functions.special_function<|message|>{\"arg1",
1593 /* is_partial= */ true,
1594 syntax: {
1595 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1596 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1597 }));
1598 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1599 actual: common_chat_parse(
1600 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1601 "<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",
1602 /* is_partial= */ false,
1603 syntax: {
1604 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1605 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1606 }));
1607 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1608 actual: common_chat_parse(
1609 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1610 "<|start|>assistant<|channel|>analysis to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",
1611 /* is_partial= */ false,
1612 syntax: {
1613 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1614 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1615 }));
1616 assert_msg_equals(expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking"),
1617 actual: common_chat_parse(
1618 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1619 "<|start|>assistant<|channel|>commentary<|message|>Hello, world!\nWhat's up?",
1620 /* is_partial= */ true,
1621 syntax: {
1622 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1623 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1624 }));
1625 assert_msg_equals(expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1626 actual: common_chat_parse(
1627 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1628 "<|start|>assistant<|channel|>commentary<|message|>Hello, world!\nWhat's up?<|end|>"
1629 "<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",
1630 /* is_partial= */ true,
1631 syntax: {
1632 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1633 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1634 }));
1635
1636 // Test parse_tool_calls == false
1637 assert_msg_equals(
1638 expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking"),
1639 actual: common_chat_parse(
1640 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1641 "<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1642 /* is_partial= */ true,
1643 syntax: {
1644 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1645 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1646 /* .reasoning_in_content = */ false,
1647 /* .thinking_forced_open = */ false,
1648 /* .parse_tool_calls = */ false,
1649 }));
1650 assert_msg_equals(
1651 expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking"),
1652 actual: common_chat_parse(
1653 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1654 "<|start|>assistant<|channel|>commentary to=functions.special_function<|message|>{\"arg1",
1655 /* is_partial= */ true,
1656 syntax: {
1657 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1658 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1659 /* .reasoning_in_content = */ false,
1660 /* .thinking_forced_open = */ false,
1661 /* .parse_tool_calls = */ false,
1662 }));
1663 assert_msg_equals(
1664 expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking"),
1665 actual: common_chat_parse(
1666 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1667 "<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",
1668 /* is_partial= */ false,
1669 syntax: {
1670 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1671 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1672 /* .reasoning_in_content = */ false,
1673 /* .thinking_forced_open = */ false,
1674 /* .parse_tool_calls = */ false,
1675 }));
1676
1677 // Test reasoning formats
1678 assert_msg_equals(
1679 expected: simple_assist_msg(
1680 content: "<|channel|>analysis<|message|>I'm\nthinking<|end|>Hello, world!\nWhat's up?"),
1681 actual: common_chat_parse(
1682 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1683 "<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1684 /* is_partial= */ false,
1685 syntax: {
1686 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1687 /* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE,
1688 }));
1689
1690 assert_msg_equals(
1691 expected: simple_assist_msg(
1692 content: "<|channel|>analysis<|message|>I'm\nthinking<|end|>Hello, world!\nWhat's up?"),
1693 actual: common_chat_parse(
1694 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1695 "<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1696 /* is_partial= */ false,
1697 syntax: {
1698 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1699 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1700 /* .reasoning_in_content = */ true,
1701 }));
1702
1703 // Test tool calling in role header
1704 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1705 actual: common_chat_parse(
1706 input: " to=functions.special_function<|channel|>commentary <|constrain|>json<|message|>{\"arg1\": 1}",
1707 /* is_partial= */ false,
1708 syntax: {
1709 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1710 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1711 }));
1712 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1713 actual: common_chat_parse(
1714 input: " to=functions.special_function<|channel|>analysis <|constrain|>json<|message|>{\"arg1\": 1}",
1715 /* is_partial= */ false,
1716 syntax: {
1717 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1718 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1719 }));
1720 assert_msg_equals(expected: simple_assist_msg(content: "", reasoning_content: "I'm\nthinking", tool_name: "special_function", arguments: "{\"arg1\": 1}"),
1721 actual: common_chat_parse(
1722 input: "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1723 "<|start|>assistant to=functions.special_function<|channel|>analysis <|constrain|>json<|message|>{\"arg1\": 1}",
1724 /* is_partial= */ false,
1725 syntax: {
1726 /* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1727 /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1728 }));
1729 }
1730 {
1731 // Seed-OSS format tests
1732 auto tmpls = read_templates(path: "models/templates/ByteDance-Seed-OSS.jinja");
1733 std::vector<std::string> end_tokens{ "<seed:eos>" };
1734
1735 assert_equals(expected: COMMON_CHAT_FORMAT_SEED_OSS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1736 assert_equals(expected: COMMON_CHAT_FORMAT_SEED_OSS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1737
1738 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1739
1740 // Test simple reasoning content
1741 assert_msg_equals(
1742 expected: simple_assist_msg(content: "Hello, world!", reasoning_content: "I'm thinking about the answer"),
1743 actual: common_chat_parse(
1744 input: "<seed:think>I'm thinking about the answer</seed:think>Hello, world!",
1745 /* is_partial= */ false,
1746 syntax: {
1747 /* .format = */ COMMON_CHAT_FORMAT_SEED_OSS,
1748 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1749 }));
1750
1751 // Test budget reflection tags
1752 common_chat_msg msg_budget_reflect;
1753 msg_budget_reflect.role = "assistant";
1754 msg_budget_reflect.content = "<seed:cot_budget_reflect>Token usage: 45/1000\nI should continue thinking to find the best solution.</seed:cot_budget_reflect>I need to calculate this step by step.";
1755 msg_budget_reflect.reasoning_content = "Token usage: 45/1000\nI should continue thinking to find the best solution.";
1756 assert_msg_equals(
1757 expected: msg_budget_reflect,
1758 actual: common_chat_parse(
1759 input: "<seed:think>Token usage: 45/1000\nI should continue thinking to find the best solution.</seed:think>"
1760 "<seed:cot_budget_reflect>Token usage: 45/1000\nI should continue thinking to find the best solution.</seed:cot_budget_reflect>"
1761 "I need to calculate this step by step.",
1762 /* is_partial= */ false,
1763 syntax: {
1764 /* .format = */ COMMON_CHAT_FORMAT_SEED_OSS,
1765 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1766 }));
1767
1768 // Test tool calls with Seed-OSS format
1769 common_chat_msg msg_tool_call;
1770 msg_tool_call.role = "assistant";
1771 msg_tool_call.tool_calls.push_back(x: {.name: "calculate_sum", .arguments: "{\"numbers\": [1, 2, 3]}", .id: ""});
1772 assert_msg_equals(
1773 expected: msg_tool_call,
1774 actual: common_chat_parse(
1775 input: "<seed:tool_call>\n"
1776 "<function=calculate_sum>\n"
1777 "<parameter=numbers>[1, 2, 3]</parameter>\n"
1778 "</function>\n"
1779 "</seed:tool_call>",
1780 /* is_partial= */ false,
1781 syntax: {.format: COMMON_CHAT_FORMAT_SEED_OSS}));
1782
1783 // Test reasoning + tool call combination
1784 common_chat_msg msg_reasoning_tool;
1785 msg_reasoning_tool.role = "assistant";
1786 msg_reasoning_tool.content = "";
1787 msg_reasoning_tool.reasoning_content = "I need to calculate the sum of these numbers";
1788 msg_reasoning_tool.tool_calls.push_back(x: {.name: "calculate_sum", .arguments: "{\"numbers\": [1, 2, 3]}", .id: ""});
1789 assert_msg_equals(
1790 expected: msg_reasoning_tool,
1791 actual: common_chat_parse(
1792 input: "<seed:think>I need to calculate the sum of these numbers</seed:think>"
1793 "<seed:tool_call>\n"
1794 "<function=calculate_sum>\n"
1795 "<parameter=numbers>[1, 2, 3]</parameter>\n"
1796 "</function>\n"
1797 "</seed:tool_call>",
1798 /* is_partial= */ false,
1799 syntax: {
1800 /* .format = */ COMMON_CHAT_FORMAT_SEED_OSS,
1801 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1802 }));
1803
1804 // Test deltas: the number of tool calls in partial parses should never decrease
1805 std::string tool_msg = "<seed:tool_call>\n"
1806 "<function=fun>\n"
1807 "<parameter=smth>[1, 2, 3]</parameter>\n"
1808 "</function>";
1809 std::size_t previousToolCalls = 0;
1810 for (std::size_t i = std::string("<seed:tool_call>").length(); i < tool_msg.length() - 1; i++) {
1811 auto partial = tool_msg.substr(pos: 0, n: i);
1812 auto partial_res = common_chat_parse(input: partial, is_partial: true, syntax: { .format: COMMON_CHAT_FORMAT_SEED_OSS, .reasoning_format: COMMON_REASONING_FORMAT_DEEPSEEK });
1813 if (partial_res.tool_calls.size() < previousToolCalls) {
1814 throw std::runtime_error("Tool call size decreased on partial: " + partial + " from " + std::to_string(val: previousToolCalls) + " to " + std::to_string(val: partial_res.tool_calls.size()));
1815 }
1816 previousToolCalls = partial_res.tool_calls.size();
1817 }
1818
1819 // Test multiple parameters in tool call
1820 common_chat_msg msg_multi_param;
1821 msg_multi_param.role = "assistant";
1822 msg_multi_param.tool_calls.push_back(x: {.name: "process_data", .arguments: "{\"input\": \"test\", \"format\": \"json\"}", .id: ""});
1823 assert_msg_equals(
1824 expected: msg_multi_param,
1825 actual: common_chat_parse(
1826 input: "<seed:tool_call>\n"
1827 "<function=process_data>\n"
1828 "<parameter=input>test</parameter>\n"
1829 "<parameter=format>json</parameter>\n"
1830 "</function>\n"
1831 "</seed:tool_call>",
1832 /* is_partial= */ false,
1833 syntax: {.format: COMMON_CHAT_FORMAT_SEED_OSS}));
1834
1835 // Test partial parsing for incomplete tool call - don't actually add the call until parsing parameters is done
1836 assert_msg_equals(
1837 expected: simple_assist_msg(content: "", reasoning_content: ""),
1838 actual: common_chat_parse(
1839 input: "<seed:tool_call>\n"
1840 "<function=calculate_sum>\n"
1841 "<parameter=numbers>[1,\n",
1842 /* is_partial= */ true,
1843 syntax: {.format: COMMON_CHAT_FORMAT_SEED_OSS}));
1844
1845 // Test incomplete reasoning tag
1846 assert_msg_equals(
1847 expected: simple_assist_msg(content: "", reasoning_content: "I was thinking"),
1848 actual: common_chat_parse(
1849 input: "<seed:think>I was thinking",
1850 /* is_partial= */ true,
1851 syntax: {
1852 /* .format = */ COMMON_CHAT_FORMAT_SEED_OSS,
1853 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1854 }));
1855
1856 // Test content without reasoning
1857 assert_msg_equals(
1858 expected: simple_assist_msg(content: "This is a simple response without reasoning."),
1859 actual: common_chat_parse(
1860 input: "This is a simple response without reasoning.",
1861 /* is_partial= */ false,
1862 syntax: {.format: COMMON_CHAT_FORMAT_SEED_OSS}));
1863 }
1864 {
1865 auto tmpls = read_templates(path: "models/templates/NVIDIA-Nemotron-Nano-v2.jinja");
1866 std::vector<std::string> end_tokens{ "<SPECIAL_12>" };
1867
1868 assert_equals(expected: COMMON_CHAT_FORMAT_NEMOTRON_V2, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
1869 assert_equals(expected: COMMON_CHAT_FORMAT_NEMOTRON_V2, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
1870
1871 // Test parsing regular content
1872 assert_msg_equals(expected: message_assist,
1873 actual: common_chat_parse(
1874 input: "Hello, world!\nWhat's up?",
1875 /* is_partial= */ false,
1876 syntax: {.format: COMMON_CHAT_FORMAT_NEMOTRON_V2}));
1877
1878 // Test parsing content with thinking
1879 assert_msg_equals(expected: message_assist_thoughts,
1880 actual: common_chat_parse(
1881 input: "<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
1882 /* is_partial= */ false,
1883 syntax: {
1884 /* .format = */ COMMON_CHAT_FORMAT_NEMOTRON_V2,
1885 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1886 }));
1887
1888 // Test parsing tool calls
1889 assert_msg_equals(expected: message_assist_call,
1890 actual: common_chat_parse(
1891 input: "<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>",
1892 /* is_partial= */ false,
1893 syntax: {.format: COMMON_CHAT_FORMAT_NEMOTRON_V2}));
1894
1895 // Test parsing tool calls with thinking
1896 assert_msg_equals(expected: message_assist_call_thoughts,
1897 actual: common_chat_parse(
1898 input: "<think>I'm\nthinking</think><TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>",
1899 /* is_partial= */ false,
1900 syntax: {
1901 /* .format = */ COMMON_CHAT_FORMAT_NEMOTRON_V2,
1902 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
1903 }));
1904
1905 // Test tool calls with extra content
1906 assert_msg_equals(expected: message_assist_call_content,
1907 actual: common_chat_parse(
1908 input: "<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>Hello, world!\nWhat's up?",
1909 /* is_partial= */ false,
1910 syntax: {.format: COMMON_CHAT_FORMAT_NEMOTRON_V2}
1911 ));
1912
1913 // Test tool calls with extra content AND thinking
1914 assert_msg_equals(expected: message_assist_call_thoughts_content,
1915 actual: common_chat_parse(
1916 input: "<think>I'm\nthinking</think><TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>Hello, world!\nWhat's up?",
1917 /* is_partial= */ false,
1918 syntax: {
1919 /* .format = */ COMMON_CHAT_FORMAT_NEMOTRON_V2,
1920 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
1921 }));
1922
1923 // Test template generation for regular content
1924 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools,
1925 expected_delta: "Hello, world!\nWhat's up?\n",
1926 /* expect_grammar_triggered= */ false);
1927
1928 // Test template generation for tool calls
1929 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
1930 expected_delta: "<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>",
1931 /* expect_grammar_triggered= */ true
1932 );
1933 }
1934 {
1935 auto tmpls = read_templates(path: "models/templates/deepseek-ai-DeepSeek-V3.1.jinja");
1936 std::vector<std::string> end_tokens{ "<|end▁of▁sentence|>" };
1937
1938 for (const auto & inputs : { inputs_no_tools, inputs_tools }) {
1939 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs);
1940 assert_equals(expected: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1, actual: params.format);
1941 assert_equals(expected: true, actual: params.thinking_forced_open);
1942 }
1943
1944 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools, expected_delta: "</think>Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1945 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_thoughts, tools, expected_delta: "</think>Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false);
1946 assert_msg_equals(
1947 expected: simple_assist_msg(content: "Hello, world!\nWhat's up?", reasoning_content: "I'm\nthinking"),
1948 actual: common_chat_parse(
1949 input: "I'm\nthinking</think>Hello, world!\nWhat's up?",
1950 /* is_partial= */ false,
1951 syntax: {
1952 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
1953 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1954 /* .reasoning_in_content = */ false,
1955 /* .thinking_forced_open = */ true,
1956 }));
1957 // variant: thinking forced open, reasoning_format none
1958 assert_msg_equals(
1959 expected: simple_assist_msg(content: "REASONING</think>ok", reasoning_content: ""),
1960 actual: common_chat_parse(
1961 input: "REASONING</think>ok",
1962 /* is_partial= */ false,
1963 syntax: {
1964 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
1965 /* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE,
1966 /* .reasoning_in_content = */ false,
1967 /* .thinking_forced_open = */ true,
1968 /* .parse_tool_calls = */ true,
1969 }));
1970 // variant: happy path for when it works as the model card says it should
1971 assert_msg_equals(
1972 expected: simple_assist_msg(content: "", reasoning_content: "", tool_name: "get_time", arguments: "{\"city\":\"Tokyo\"}"),
1973 actual: common_chat_parse(
1974 input: "<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>",
1975 /* is_partial= */ false,
1976 syntax: {
1977 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
1978 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1979 /* .reasoning_in_content = */ false,
1980 /* .thinking_forced_open = */ false,
1981 /* .parse_tool_calls = */ true,
1982 }));
1983 // variant: simple + thinking open
1984 assert_msg_equals(
1985 expected: simple_assist_msg(content: "", reasoning_content: "REASONING", tool_name: "get_time", arguments: "{\"city\":\"Tokyo\"}"),
1986 actual: common_chat_parse(
1987 input: "REASONING</think><|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>",
1988 /* is_partial= */ false,
1989 syntax: {
1990 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
1991 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1992 /* .reasoning_in_content = */ false,
1993 /* .thinking_forced_open = */ true,
1994 /* .parse_tool_calls = */ true,
1995 }));
1996 // variant: simple + multiple tool calls
1997 common_chat_msg message_assist_multiple_calls;
1998 message_assist_multiple_calls.role = "assistant";
1999 message_assist_multiple_calls.content = "CONTENT";
2000 message_assist_multiple_calls.tool_calls.push_back(x: {.name: "get_time", .arguments: "{\"city\":\"Paris\"}", .id: ""});
2001 message_assist_multiple_calls.tool_calls.push_back(x: {.name: "get_weather", .arguments: "{\"city\":\"Paris\"}", .id: ""});
2002 assert_msg_equals(
2003 expected: message_assist_multiple_calls,
2004 actual: common_chat_parse(
2005 input: "CONTENT<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Paris\"}<|tool▁call▁end|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"city\": \"Paris\"}<|tool▁call▁end|><|tool▁calls▁end|>",
2006 /* is_partial= */ false,
2007 syntax: {
2008 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
2009 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2010 /* .reasoning_in_content = */ false,
2011 /* .thinking_forced_open = */ false,
2012 /* .parse_tool_calls = */ true,
2013 }));
2014 // variant: thinking forced open + tool call in reasoning content
2015 assert_msg_equals(
2016 expected: simple_assist_msg(content: "", reasoning_content: "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time2<|tool▁sep|>{\"city\": \"Tokyo2\"}<|tool▁call▁end|><|tool▁calls▁end|>REASONING", tool_name: "get_time", arguments: "{\"city\":\"Tokyo\"}"),
2017 actual: common_chat_parse(
2018 input: "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time2<|tool▁sep|>{\"city\": \"Tokyo2\"}<|tool▁call▁end|><|tool▁calls▁end|>REASONING</think><|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>",
2019 /* is_partial= */ false,
2020 syntax: {
2021 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
2022 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2023 /* .reasoning_in_content = */ false,
2024 /* .thinking_forced_open = */ true,
2025 /* .parse_tool_calls = */ true,
2026 }));
2027 // variant: thinking forced open + tool call in reasoning content + no closing think + not partial
2028 // This is a bit of a fine tuning issue on the model's part IMO. It really should not be attempting
2029 // to make tool calls in reasoning content according to the model card, but it does sometimes, so
2030 // add the reasoning content as regular content and parse the tool calls.
2031 assert_msg_equals(
2032 expected: simple_assist_msg(content: "REASONING", reasoning_content: "", tool_name: "get_time", arguments: "{\"city\":\"Tokyo\"}"),
2033 actual: common_chat_parse(
2034 input: "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>",
2035 /* is_partial= */ false,
2036 syntax: {
2037 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
2038 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2039 /* .reasoning_in_content = */ false,
2040 /* .thinking_forced_open = */ true,
2041 /* .parse_tool_calls = */ true,
2042 }));
2043 // variant: thinking forced open + tool call in reasoning content + no closing think + partial
2044 assert_msg_equals(
2045 expected: simple_assist_msg(content: "", reasoning_content: "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>", tool_name: "", arguments: ""),
2046 actual: common_chat_parse(
2047 input: "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>",
2048 /* is_partial= */ true,
2049 syntax: {
2050 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
2051 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2052 /* .reasoning_in_content = */ false,
2053 /* .thinking_forced_open = */ true,
2054 /* .parse_tool_calls = */ true,
2055 }));
2056 // variant: thinking not forced open + missing reasoning + no tool calls
2057 assert_msg_equals(
2058 expected: simple_assist_msg(content: "CONTENT", reasoning_content: ""),
2059 actual: common_chat_parse(
2060 input: "CONTENT",
2061 /* is_partial= */ false,
2062 syntax: {
2063 .format: COMMON_CHAT_FORMAT_DEEPSEEK_V3_1,
2064 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2065 /* .reasoning_in_content = */ false,
2066 /* .thinking_forced_open = */ false,
2067 /* .parse_tool_calls = */ true,
2068 }));
2069 }
2070 {
2071 auto tmpls = read_templates(path: "models/templates/Apertus-8B-Instruct.jinja");
2072 std::vector<std::string> end_tokens{ "<|assistant_end|>" };
2073
2074 assert_equals(expected: COMMON_CHAT_FORMAT_APERTUS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools).format);
2075 assert_equals(expected: COMMON_CHAT_FORMAT_APERTUS, actual: common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools).format);
2076
2077 // Test parsing regular content
2078 assert_msg_equals(expected: message_assist,
2079 actual: common_chat_parse(
2080 input: "Hello, world!\nWhat's up?",
2081 /* is_partial= */ false,
2082 syntax: {.format: COMMON_CHAT_FORMAT_APERTUS}));
2083
2084 // Test parsing content with thinking
2085 assert_msg_equals(expected: message_assist_thoughts,
2086 actual: common_chat_parse(
2087 input: "<|inner_prefix|>I'm\nthinking<|inner_suffix|>Hello, world!\nWhat's up?",
2088 /* is_partial= */ false,
2089 syntax: {
2090 /* .format = */ COMMON_CHAT_FORMAT_APERTUS,
2091 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
2092 }));
2093
2094 // Test parsing tool calls
2095 assert_msg_equals(expected: message_assist_call,
2096 actual: common_chat_parse(
2097 input: "<|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>",
2098 /* is_partial= */ false,
2099 syntax: {.format: COMMON_CHAT_FORMAT_APERTUS}));
2100
2101 // Test parsing tool calls with thinking
2102 assert_msg_equals(expected: message_assist_call_thoughts,
2103 actual: common_chat_parse(
2104 input: "<|inner_prefix|>I'm\nthinking<|inner_suffix|><|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>",
2105 /* is_partial= */ false,
2106 syntax: {
2107 /* .format = */ COMMON_CHAT_FORMAT_APERTUS,
2108 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
2109 }));
2110
2111 // Test tool calls with extra content
2112 assert_msg_equals(expected: message_assist_call_content,
2113 actual: common_chat_parse(
2114 input: "<|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>Hello, world!\nWhat's up?",
2115 /* is_partial= */ false,
2116 syntax: {.format: COMMON_CHAT_FORMAT_APERTUS}
2117 ));
2118
2119 // Test tool calls with extra content AND thinking
2120 assert_msg_equals(expected: message_assist_call_thoughts_content,
2121 actual: common_chat_parse(
2122 input: "<|inner_prefix|>I'm\nthinking<|inner_suffix|><|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>Hello, world!\nWhat's up?",
2123 /* is_partial= */ false,
2124 syntax: {
2125 /* .format = */ COMMON_CHAT_FORMAT_APERTUS,
2126 /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
2127 }));
2128
2129 // Test template generation for regular content
2130 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist, tools,
2131 expected_delta: "Hello, world!\nWhat's up?",
2132 /* expect_grammar_triggered= */ false);
2133
2134 // Test template generation for tool calls
2135 test_templates(tmpls: tmpls.get(), end_tokens, test_message: message_assist_call, tools,
2136 expected_delta: "<|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>",
2137 /* expect_grammar_triggered= */ true
2138 );
2139
2140 assert_equals(expected: true, actual: common_chat_templates_support_enable_thinking(chat_templates: tmpls.get()));
2141 }
2142 {
2143 // LFM2 format tests
2144 auto tmpls = read_templates(path: "models/templates/llama-cpp-lfm2.jinja");
2145 std::vector<std::string> end_tokens{ "<|im_end|>" };
2146
2147 auto inputs_tools_forced_json_schema = std::invoke(fn: [&]() -> common_chat_templates_inputs {
2148 common_chat_templates_inputs inputs;
2149 inputs.messages = {
2150 std::invoke(fn: [&]() -> common_chat_msg {
2151 common_chat_msg msg;
2152 msg.role = "system";
2153 msg.content = "force json schema.\n";
2154 return msg;
2155 }),
2156 message_user,
2157 };
2158 inputs.tools = {special_function_tool};
2159 return inputs;
2160 });
2161
2162 {
2163 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_no_tools);
2164 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: params.format);
2165 assert_equals(expected: false, actual: params.grammar_lazy);
2166 assert_equals(expected: std::string(R"(<|im_start|>user
2167Hey there!<|im_end|>
2168<|im_start|>assistant
2169)"), actual: params.prompt);
2170 }
2171
2172 {
2173 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools);
2174 assert_equals(expected: COMMON_CHAT_FORMAT_CONTENT_ONLY, actual: params.format);
2175 assert_equals(expected: false, actual: params.grammar_lazy);
2176 assert_equals(expected: std::string(R"(<|im_start|>system
2177List of tools: <|tool_list_start|>[{"type": "function", "function": {"name": "special_function", "description": "I'm special", "parameters": {"type": "object", "properties": {"arg1": {"type": "integer", "description": "The arg."}}, "required": ["arg1"]}}}]<|tool_list_end|><|im_end|>
2178<|im_start|>user
2179Hey there!<|im_end|>
2180<|im_start|>assistant
2181)"), actual: params.prompt);
2182 assert_equals(expected: true, actual: params.grammar.empty());
2183 }
2184
2185 {
2186 auto params = common_chat_templates_apply(tmpls: tmpls.get(), inputs: inputs_tools_forced_json_schema);
2187 assert_equals(expected: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS, actual: params.format);
2188 assert_equals(expected: true, actual: params.grammar_lazy);
2189 assert_equals(expected: std::string(R"(<|im_start|>system
2190List of tools: <|tool_list_start|>[{"type": "function", "function": {"name": "special_function", "description": "I'm special", "parameters": {"type": "object", "properties": {"arg1": {"type": "integer", "description": "The arg."}}, "required": ["arg1"]}}}]<|tool_list_end|><|im_end|>
2191<|im_start|>user
2192Hey there!<|im_end|>
2193<|im_start|>assistant
2194)"), actual: params.prompt);
2195 assert_equals(expected: false, actual: params.grammar.empty());
2196 }
2197
2198 // Test parsing regular content
2199 assert_msg_equals(expected: message_assist,
2200 actual: common_chat_parse(
2201 input: "Hello, world!\nWhat's up?",
2202 /* is_partial= */ false,
2203 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2204
2205 // Test single tool call with JSON format
2206 common_chat_msg msg_single_tool_call;
2207 msg_single_tool_call.role = "assistant";
2208 msg_single_tool_call.tool_calls.push_back(x: {.name: "special_function", .arguments: "{\"arg1\":1}", .id: ""});
2209 assert_msg_equals(
2210 expected: msg_single_tool_call,
2211 actual: common_chat_parse(
2212 input: "<|tool_call_start|>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]<|tool_call_end|>",
2213 /* is_partial= */ false,
2214 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2215
2216 // Test tool call with string argument
2217 common_chat_msg msg_tool_call_string;
2218 msg_tool_call_string.role = "assistant";
2219 msg_tool_call_string.tool_calls.push_back(x: {.name: "get_weather", .arguments: "{\"location\":\"Paris\"}", .id: ""});
2220 assert_msg_equals(
2221 expected: msg_tool_call_string,
2222 actual: common_chat_parse(
2223 input: "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>",
2224 /* is_partial= */ false,
2225 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2226
2227 // Test tool call with multiple arguments
2228 common_chat_msg msg_multi_args;
2229 msg_multi_args.role = "assistant";
2230 msg_multi_args.tool_calls.push_back(x: {.name: "calculate", .arguments: "{\"x\":10,\"y\":20,\"operation\":\"add\"}", .id: ""});
2231 assert_msg_equals(
2232 expected: msg_multi_args,
2233 actual: common_chat_parse(
2234 input: "<|tool_call_start|>[{\"name\": \"calculate\", \"arguments\": {\"x\": 10, \"y\": 20, \"operation\": \"add\"}}]<|tool_call_end|>",
2235 /* is_partial= */ false,
2236 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2237
2238 // Test multiple tool calls in single array
2239 common_chat_msg msg_multiple_tools;
2240 msg_multiple_tools.role = "assistant";
2241 msg_multiple_tools.tool_calls.push_back(x: {.name: "get_weather", .arguments: "{\"location\":\"Paris\"}", .id: ""});
2242 msg_multiple_tools.tool_calls.push_back(x: {.name: "get_time", .arguments: "{\"timezone\":\"UTC\"}", .id: ""});
2243 assert_msg_equals(
2244 expected: msg_multiple_tools,
2245 actual: common_chat_parse(
2246 input: "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}, {\"name\": \"get_time\", \"arguments\": {\"timezone\": \"UTC\"}}]<|tool_call_end|>",
2247 /* is_partial= */ false,
2248 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2249
2250 // Test tool call with content before
2251 common_chat_msg msg_content_before_tool;
2252 msg_content_before_tool.role = "assistant";
2253 msg_content_before_tool.content = "Let me check the weather for you.";
2254 msg_content_before_tool.tool_calls.push_back(x: {.name: "get_weather", .arguments: "{\"location\":\"Paris\"}", .id: ""});
2255 assert_msg_equals(
2256 expected: msg_content_before_tool,
2257 actual: common_chat_parse(
2258 input: "Let me check the weather for you.<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>",
2259 /* is_partial= */ false,
2260 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2261
2262 // Test tool call with content after
2263 common_chat_msg msg_content_after_tool;
2264 msg_content_after_tool.role = "assistant";
2265 msg_content_after_tool.content = "Here's the result.";
2266 msg_content_after_tool.tool_calls.push_back(x: {.name: "get_weather", .arguments: "{\"location\":\"Paris\"}", .id: ""});
2267 assert_msg_equals(
2268 expected: msg_content_after_tool,
2269 actual: common_chat_parse(
2270 input: "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>Here's the result.",
2271 /* is_partial= */ false,
2272 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2273
2274 // Test tool call with newlines (common in LLM output)
2275 common_chat_msg msg_tool_call_newlines;
2276 msg_tool_call_newlines.role = "assistant";
2277 msg_tool_call_newlines.tool_calls.push_back(x: {.name: "get_current_time", .arguments: "{\"location\":\"Paris\"}", .id: ""});
2278 assert_msg_equals(
2279 expected: msg_tool_call_newlines,
2280 actual: common_chat_parse(
2281 input: "<|tool_call_start|>[{\n \"name\": \"get_current_time\",\n \"arguments\": {\n \"location\": \"Paris\"\n }\n}]<|tool_call_end|>",
2282 /* is_partial= */ false,
2283 syntax: {.format: COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS}));
2284
2285 // Note: LFM2 uses JSON format for tool calls: [{"name": "...", "arguments": {...}}]
2286 // Unlike other formats, LFM2 template does not render tool calls in conversation history,
2287 // so we don't use test_templates() for tool call generation. Instead, the parsing tests
2288 // above verify edge cases and format variations for the tool call output format.
2289 }
2290
2291}
2292
2293static void test_msg_diffs_compute() {
2294 printf(format: "[%s]\n", __func__);
2295 {
2296 common_chat_msg msg1;
2297
2298 common_chat_msg msg2;
2299 msg2.content = "Hello, world!";
2300
2301 common_chat_msg_diff diff;
2302 diff.content_delta = "Hello, world!";
2303
2304 assert_equals(
2305 expected: {diff},
2306 actual: common_chat_msg_diff::compute_diffs(previous_msg: msg1, new_msg: msg2));
2307 }
2308 {
2309 common_chat_msg msg1;
2310 msg1.content = "Hello,";
2311
2312 common_chat_msg msg2;
2313 msg2.content = "Hello, world!";
2314
2315 common_chat_msg_diff diff;
2316 diff.content_delta = " world!";
2317
2318 assert_equals(
2319 expected: {diff},
2320 actual: common_chat_msg_diff::compute_diffs(previous_msg: msg1, new_msg: msg2));
2321 }
2322 {
2323 common_chat_msg msg0;
2324
2325 common_chat_msg msg1;
2326 msg1.tool_calls = { { .name: "special_function", .arguments: "{\"ar", /* .id = */ "123" } };
2327
2328 common_chat_msg msg2;
2329 msg2.tool_calls = { { .name: "special_function", .arguments: "{\"arg1\": 1}", /* .id = */ "123" } };
2330
2331 common_chat_msg_diff diff01;
2332 diff01.tool_call_index = 0;
2333 diff01.tool_call_delta.name = "special_function";
2334 diff01.tool_call_delta.id = "123";
2335 diff01.tool_call_delta.arguments = "{\"ar";
2336
2337 assert_equals(
2338 expected: {diff01},
2339 actual: common_chat_msg_diff::compute_diffs(previous_msg: msg0, new_msg: msg1));
2340
2341 common_chat_msg_diff diff12;
2342 diff12.tool_call_index = 0;
2343 // Note: neither id nor name change here.
2344 diff12.tool_call_delta.arguments = "g1\": 1}";
2345
2346 assert_equals(
2347 expected: {diff12},
2348 actual: common_chat_msg_diff::compute_diffs(previous_msg: msg1, new_msg: msg2));
2349 }
2350 {
2351 common_chat_msg msg0;
2352
2353 common_chat_msg msg2;
2354 msg2.tool_calls = {
2355 { .name: "f1", .arguments: "{\"arg1\": 1}", /* .id = */ "123" },
2356 { .name: "f2", .arguments: "{\"arg2\": 2}", /* .id = */ "222" },
2357 };
2358
2359 common_chat_msg_diff diff1;
2360 diff1.tool_call_index = 0;
2361 diff1.tool_call_delta.name = "f1";
2362 diff1.tool_call_delta.id = "123";
2363 diff1.tool_call_delta.arguments = "{\"arg1\": 1}";
2364
2365 common_chat_msg_diff diff2;
2366 diff2.tool_call_index = 1;
2367 diff2.tool_call_delta.name = "f2";
2368 diff2.tool_call_delta.id = "222";
2369 diff2.tool_call_delta.arguments = "{\"arg2\": 2}";
2370
2371 assert_equals(
2372 expected: {diff1, diff2},
2373 actual: common_chat_msg_diff::compute_diffs(previous_msg: msg0, new_msg: msg2));
2374 }
2375}
2376
2377int main(int argc, char ** argv) {
2378 common_log_set_verbosity_thold(verbosity: 999);
2379
2380 // try {
2381#ifndef _WIN32
2382 if (argc > 1) {
2383 common_chat_templates_inputs inputs;
2384 common_chat_msg msg;
2385 msg.role = "user";
2386 msg.content = "Hey";
2387 inputs.messages = {msg};
2388 inputs.tools = { special_function_tool };
2389
2390 std::cout << "| Template | Format |\n";
2391 std::cout << "|----------|--------|\n";
2392
2393 for (int i = 1; i < argc; i++) {
2394 try {
2395 std::string path = argv[i];
2396 if (path.rfind(s: ".jinja") != path.size() - 6) {
2397 std::cerr << "Skipping non-jinja file: " << path << '\n';
2398 continue;
2399 }
2400 auto tmpls = read_templates(path);
2401 auto parts = string_split(str: path, delimiter: "/");
2402 auto name = parts[parts.size() - 1];
2403 auto format = common_chat_format_name(format: common_chat_templates_apply(tmpls: tmpls.get(), inputs).format);
2404 std::cout << "| " << name << " | " << format << " |\n";
2405 } catch (const std::exception & e) {
2406 std::cerr << "Failed to process " << argv[i] << ": " << e.what() << '\n';
2407 }
2408 }
2409 } else
2410#endif
2411 {
2412 test_msg_diffs_compute();
2413 test_msgs_oaicompat_json_conversion();
2414 test_tools_oaicompat_json_conversion();
2415 test_template_output_parsers();
2416 std::cout << "\n[chat] All tests passed!" << '\n';
2417 }
2418 return 0;
2419 // } catch (const std::exception & e) {
2420 // std::cerr << "Error: " << e.what() << '\n';
2421 // return 1;
2422 // }
2423}
2424