test-chat-parser.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  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 <exception>
  9. #include <iostream>
  10. #include <string>
  11. #include "chat-parser.h"
  12. #include "common.h"
  13. #include "log.h"
  14. #include "regex-partial.h"
  15. template <class T>
  16. static void assert_equals(const std::string_view label, const T & expected, const T & actual) {
  17. if (expected != actual) {
  18. std::cerr << label << std::endl;
  19. std::cerr << "Expected: " << expected << std::endl;
  20. std::cerr << "Actual: " << actual << std::endl;
  21. std::cerr << std::flush;
  22. throw std::runtime_error("Test failed");
  23. }
  24. }
  25. template <class T>
  26. static void assert_equals(const T & expected, const T & actual) {
  27. assert_equals("", expected, actual);
  28. }
  29. static void assert_equals(const char * expected, const std::string & actual) {
  30. return assert_equals<std::string>(expected, actual);
  31. }
  32. static void assert_throws(const std::function<void()> & fn, const std::string & expected_exception_pattern = "") {
  33. try {
  34. fn();
  35. } catch (const std::exception & e) {
  36. if (expected_exception_pattern.empty()) {
  37. return;
  38. }
  39. std::regex expected_exception_regex(expected_exception_pattern);
  40. std::string actual_message = e.what();
  41. if (std::regex_search(actual_message, expected_exception_regex)) {
  42. return;
  43. }
  44. throw std::runtime_error("Exception doesn't match expected pattern: " + actual_message + " (pattern: " + expected_exception_pattern + ")");
  45. throw std::runtime_error("Exception of unexpected type: " + std::string(e.what()));
  46. }
  47. throw std::runtime_error("Exception was expected but not thrown");
  48. }
  49. static void test_reasoning() {
  50. //common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
  51. {
  52. common_chat_parser_params params;
  53. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  54. params.reasoning_format = COMMON_REASONING_FORMAT_NONE;
  55. params.reasoning_in_content = false;
  56. params.thinking_forced_open = false;
  57. common_chat_msg_parser builder("<tnk>Cogito</tnk>Ergo sum", /* is_partial= */ false, params);
  58. assert_equals(false, builder.try_parse_reasoning("<tnk>", "</tnk>"));
  59. assert_equals("<tnk>Cogito</tnk>Ergo sum", builder.consume_rest());
  60. }
  61. {
  62. common_chat_parser_params params;
  63. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  64. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  65. params.reasoning_in_content = false;
  66. params.thinking_forced_open = false;
  67. common_chat_msg_parser builder("<tnk>Cogito</tnk>Ergo sum", /* is_partial= */ false, params);
  68. assert_equals(true, builder.try_parse_reasoning("<tnk>", "</tnk>"));
  69. assert_equals(std::string("Cogito"), builder.result().reasoning_content);
  70. assert_equals("Ergo sum", builder.consume_rest());
  71. }
  72. {
  73. common_chat_parser_params params;
  74. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  75. params.reasoning_format = COMMON_REASONING_FORMAT_NONE;
  76. params.reasoning_in_content = false;
  77. params.thinking_forced_open = false;
  78. common_chat_msg_parser builder("Cogito</tnk>Ergo sum", /* is_partial= */ false, params);
  79. assert_equals(false, builder.try_parse_reasoning("<tnk>", "</tnk>"));
  80. assert_equals("Cogito</tnk>Ergo sum", builder.consume_rest());
  81. }
  82. {
  83. common_chat_parser_params params;
  84. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  85. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  86. params.reasoning_in_content = false;
  87. params.thinking_forced_open = true;
  88. common_chat_msg_parser builder("Cogito</tnk>Ergo sum", /* is_partial= */ false, params);
  89. assert_equals(true, builder.try_parse_reasoning("<tnk>", "</tnk>"));
  90. assert_equals(std::string("Cogito"), builder.result().reasoning_content);
  91. assert_equals("Ergo sum", builder.consume_rest());
  92. }
  93. {
  94. common_chat_parser_params params;
  95. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  96. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  97. params.reasoning_in_content = true;
  98. params.thinking_forced_open = true;
  99. common_chat_msg_parser builder("Cogito</tnk>Ergo sum", /* is_partial= */ false, params);
  100. assert_equals(true, builder.try_parse_reasoning("<tnk>", "</tnk>"));
  101. assert_equals("<think>Cogito</think>", builder.result().content);
  102. assert_equals("Ergo sum", builder.consume_rest());
  103. }
  104. {
  105. const std::string variant("content_only_inline_think");
  106. common_chat_parser_params params;
  107. params.format = COMMON_CHAT_FORMAT_CONTENT_ONLY;
  108. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  109. params.reasoning_in_content = false;
  110. params.thinking_forced_open = false;
  111. params.parse_tool_calls = false;
  112. const std::string input = "<think>Pense</think>Bonjour";
  113. auto msg = common_chat_parse(input, false, params);
  114. assert_equals(variant, std::string("Pense"), msg.reasoning_content);
  115. assert_equals(variant, std::string("Bonjour"), msg.content);
  116. }
  117. {
  118. const std::string variant("llama_3_inline_think");
  119. common_chat_parser_params params;
  120. params.format = COMMON_CHAT_FORMAT_LLAMA_3_X;
  121. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  122. params.reasoning_in_content = false;
  123. params.thinking_forced_open = false;
  124. params.parse_tool_calls = false;
  125. const std::string input = "<think>Plan</think>Réponse";
  126. auto msg = common_chat_parse(input, false, params);
  127. assert_equals(variant, std::string("Plan"), msg.reasoning_content);
  128. assert_equals(variant, std::string("Réponse"), msg.content);
  129. }
  130. // Test DeepSeek V3.1 parsing - reasoning content followed by "</think>" and then regular content
  131. {
  132. common_chat_parser_params params;
  133. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  134. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  135. params.reasoning_in_content = false;
  136. params.thinking_forced_open = true;
  137. params.parse_tool_calls = true;
  138. const std::string variant("deepseek_v3_1_reasoning_format_deepseek");
  139. common_chat_msg_parser builder("REASONING</think>ok", /* is_partial= */ false, params);
  140. assert_equals(variant, true, builder.try_parse_reasoning("<think>", "</think>"));
  141. assert_equals(variant, std::string("REASONING"), builder.result().reasoning_content);
  142. assert_equals(variant, std::string("ok"), builder.consume_rest());
  143. }
  144. // Test DeepSeek V3.1 parsing - reasoning_format none - reasoning content followed by "</think>" and then regular content
  145. {
  146. common_chat_parser_params params;
  147. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  148. params.reasoning_format = COMMON_REASONING_FORMAT_NONE;
  149. params.reasoning_in_content = false;
  150. params.thinking_forced_open = true;
  151. params.parse_tool_calls = true;
  152. const std::string variant("deepseek_v3_1_reasoning_format_none");
  153. const std::string input = "REASONING</think>ok";
  154. auto msg = common_chat_parse(input, false, params);
  155. assert_equals(variant, std::string("REASONING</think>ok"), msg.content);
  156. assert_equals(variant, std::string(""), msg.reasoning_content);
  157. }
  158. }
  159. static void test_regex() {
  160. auto test_throws = [](const std::string & input, const std::string & regex, const std::string & expected_exception_pattern = "") {
  161. common_chat_msg_parser builder(input, /* is_partial= */ false, {});
  162. assert_throws([&]() { builder.consume_regex(common_regex(regex)); }, expected_exception_pattern);
  163. };
  164. test_throws("Hello, world!", "abc", "^abc$");
  165. test_throws("Hello, world!", "e", "^e$");
  166. {
  167. common_chat_msg_parser builder("Hello, world!", /* is_partial= */ false, {});
  168. builder.consume_regex(common_regex("Hello"));
  169. assert_equals(", world!", builder.consume_rest());
  170. }
  171. {
  172. // When in non partial mode, we can say whether the regex was consumed or not.
  173. common_chat_msg_parser builder("Hello,", /* is_partial= */ false, {});
  174. assert_equals(false, builder.try_consume_regex(common_regex("Hello, world!")).has_value());
  175. }
  176. {
  177. common_chat_msg_parser builder("Hello,", /* is_partial= */ false, {});
  178. auto res = builder.try_consume_regex(common_regex("H(el)l(?:o, world!)?"));
  179. assert_equals(true, res.has_value());
  180. // Verify captures
  181. assert_equals<size_t>(2, res->groups.size());
  182. assert_equals("Hell", builder.str(res->groups[0]));
  183. assert_equals("el", builder.str(res->groups[1]));
  184. // Verify position is after the match
  185. assert_equals<size_t>(4, builder.pos());
  186. assert_equals("o,", builder.consume_rest());
  187. }
  188. {
  189. // But in partial mode, we have a partial final match / can't decide, so we throw a partial exception.
  190. common_chat_msg_parser builder("Hello,", /* is_partial= */ true, {});
  191. assert_throws([&]() {
  192. builder.try_consume_regex(common_regex("Hello, world!"));
  193. }, "^Hello, world!$");
  194. }
  195. // Now regardless of the mode, we can tell these aren't a match.
  196. for (const auto is_partial : {false, true}) {
  197. common_chat_msg_parser builder("Hello,", is_partial, {});
  198. assert_equals(false, builder.try_consume_regex(common_regex("a(b|c)(d|e)f")).has_value());
  199. }
  200. for (const auto is_partial : {false, true}) {
  201. common_chat_msg_parser builder("Hello,", is_partial, {});
  202. assert_equals(false, builder.try_consume_literal("Oh"));
  203. }
  204. }
  205. const std::vector<std::string> barely_healable_jsons = {
  206. "{",
  207. "{\"",
  208. "{\"\\",
  209. "{\"n",
  210. "{\"name\"",
  211. "{\"name\":",
  212. "{\"name\":\"",
  213. "{\"name\":\"\\",
  214. "{\"name\":\"python",
  215. "{\"name\":\"python\\",
  216. "{\",",
  217. "{\":",
  218. "{\"[",
  219. "{\"]",
  220. "{\"{",
  221. "{\"}",
  222. "{\"1",
  223. "{\"name\":\",",
  224. "{\"name\":\":",
  225. "{\"name\":\"[",
  226. "{\"name\":\"]",
  227. "{\"name\":\"{",
  228. "{\"name\":\"}",
  229. "{\"name\":\"1",
  230. };
  231. static void test(const std::string & input, bool is_partial, const std::vector<std::vector<std::string>> & args_paths, const std::vector<std::vector<std::string>> & content_paths, const std::string & expected) {
  232. common_chat_msg_parser builder(input, is_partial, {});
  233. auto js = builder.try_consume_json_with_dumped_args(args_paths, content_paths);
  234. assert_equals(true, js.has_value());
  235. assert_equals(is_partial, js->is_partial);
  236. assert_equals(expected, args_paths.size() == 1 && args_paths[0].empty() ? js->value.get<std::string>() : js->value.dump());
  237. }
  238. static void test_deepseek_v3_1_tool_calls() {
  239. //common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
  240. // variant: happy path for when it works as the model card says it should
  241. const std::string variant("simple");
  242. common_chat_parser_params params;
  243. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  244. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  245. params.reasoning_in_content = false;
  246. params.thinking_forced_open = false;
  247. params.parse_tool_calls = true;
  248. const std::string input = "<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>";
  249. auto msg = common_chat_parse(input, false, params);
  250. assert_equals<std::size_t>(variant, 1, msg.tool_calls.size());
  251. assert_equals(variant, std::string("get_time"), msg.tool_calls[0].name);
  252. // JSON arguments are dumped without spaces
  253. assert_equals(variant, std::string("{\"city\":\"Tokyo\"}"), msg.tool_calls[0].arguments);
  254. assert_equals(variant, std::string(""), msg.content);
  255. assert_equals(variant, std::string(""), msg.reasoning_content);
  256. // variant: simple + thinking open
  257. {
  258. common_chat_parser_params params;
  259. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  260. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  261. params.reasoning_in_content = false;
  262. params.thinking_forced_open = true;
  263. params.parse_tool_calls = true;
  264. const std::string variant("simple_thinking");
  265. const std::string in = "REASONING</think><|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>";
  266. auto m = common_chat_parse(in, false, params);
  267. assert_equals<std::size_t>(variant, 1, m.tool_calls.size());
  268. assert_equals(variant, std::string("get_time"), m.tool_calls[0].name);
  269. assert_equals(variant, std::string("{\"city\":\"Tokyo\"}"), m.tool_calls[0].arguments);
  270. assert_equals(variant, std::string(""), m.content);
  271. assert_equals(variant, std::string("REASONING"), m.reasoning_content);
  272. }
  273. // variant: simple + multiple tool calls
  274. {
  275. common_chat_parser_params params;
  276. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  277. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  278. params.reasoning_in_content = false;
  279. params.thinking_forced_open = false;
  280. params.parse_tool_calls = true;
  281. const std::string variant("simple_multiple_tool_calls");
  282. const std::string in = "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|>";
  283. auto m = common_chat_parse(in, false, params);
  284. assert_equals<std::size_t>(variant, 2, m.tool_calls.size());
  285. assert_equals(variant, std::string("get_time"), m.tool_calls[0].name);
  286. assert_equals(variant, std::string("{\"city\":\"Paris\"}"), m.tool_calls[0].arguments);
  287. assert_equals(variant, std::string("get_weather"), m.tool_calls[1].name);
  288. assert_equals(variant, std::string("{\"city\":\"Paris\"}"), m.tool_calls[1].arguments);
  289. assert_equals(variant, std::string("CONTENT"), m.content);
  290. assert_equals(variant, std::string(""), m.reasoning_content);
  291. }
  292. // variant: thinking forced open + tool call in reasoning content
  293. {
  294. common_chat_parser_params params;
  295. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  296. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  297. params.reasoning_in_content = false;
  298. params.thinking_forced_open = true;
  299. params.parse_tool_calls = true;
  300. const std::string variant("thinking_forced_open_tool_call_in_reasoning");
  301. const std::string in = "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|>";
  302. auto m = common_chat_parse(in, false, params);
  303. assert_equals<std::size_t>(variant, 1, m.tool_calls.size());
  304. assert_equals(variant, std::string("get_time"), m.tool_calls[0].name);
  305. assert_equals(variant, std::string("{\"city\":\"Tokyo\"}"), m.tool_calls[0].arguments);
  306. assert_equals(variant, std::string(""), m.content);
  307. assert_equals(variant, std::string("REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time2<|tool▁sep|>{\"city\": \"Tokyo2\"}<|tool▁call▁end|><|tool▁calls▁end|>REASONING"), m.reasoning_content);
  308. }
  309. // variant: thinking forced open + tool call in reasoning content + no closing think + not partial
  310. // This is a bit of a fine tuning issue on the model's part IMO. It really should not be attempting
  311. // to make tool calls in reasoning content according to the model card, but it does sometimes, so
  312. // add the reasoning content as regular content and parse the tool calls.
  313. {
  314. common_chat_parser_params params;
  315. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  316. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  317. params.reasoning_in_content = false;
  318. params.thinking_forced_open = true;
  319. params.parse_tool_calls = true;
  320. const std::string variant("thinking_forced_open_tool_call_in_reasoning_no_closing_think_not_partial");
  321. const std::string in = "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>";
  322. auto m = common_chat_parse(in, false, params);
  323. assert_equals(variant, std::string("REASONING"), m.content);
  324. assert_equals(variant, std::string(""), m.reasoning_content);
  325. assert_equals<std::size_t>(variant, 1, m.tool_calls.size());
  326. assert_equals(variant, std::string("get_time"), m.tool_calls[0].name);
  327. assert_equals(variant, std::string("{\"city\":\"Tokyo\"}"), m.tool_calls[0].arguments);
  328. }
  329. // variant: thinking forced open + tool call in reasoning content + no closing think + partial
  330. {
  331. common_chat_parser_params params;
  332. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  333. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  334. params.reasoning_in_content = false;
  335. params.thinking_forced_open = true;
  336. params.parse_tool_calls = true;
  337. const std::string variant("thinking_forced_open_tool_call_in_reasoning_no_closing_think_partial");
  338. const std::string in = "REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>";
  339. auto m = common_chat_parse(in, /* is_partial= */ true, params);
  340. assert_equals(variant, std::string("REASONING<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>"), m.reasoning_content);
  341. assert_equals(variant, std::string(""), m.content);
  342. assert_equals<std::size_t>(variant, 0, m.tool_calls.size());
  343. }
  344. // variant: thinking not forced open + reasoning + regular content + no tool calls
  345. {
  346. common_chat_parser_params params;
  347. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  348. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  349. params.reasoning_in_content = false;
  350. params.thinking_forced_open = true;
  351. params.parse_tool_calls = true;
  352. const std::string variant("thinking_forced_open_reasoning_regular_content_no_tool_calls");
  353. const std::string in = "REASONING</think>CONTENT";
  354. auto m = common_chat_parse(in, false, params);
  355. assert_equals<std::size_t>(variant, 0, m.tool_calls.size());
  356. assert_equals(variant, std::string("CONTENT"), m.content);
  357. assert_equals(variant, std::string("REASONING"), m.reasoning_content);
  358. }
  359. // variant: thinking not forced open + missing reasoning + no tool calls
  360. {
  361. common_chat_parser_params params;
  362. params.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_1;
  363. params.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
  364. params.reasoning_in_content = false;
  365. params.thinking_forced_open = false;
  366. params.parse_tool_calls = true;
  367. const std::string variant("thinking_not_forced_open_missing_reasoning_no_tool_calls");
  368. const std::string in = "CONTENT";
  369. auto m = common_chat_parse(in, false, params);
  370. assert_equals<std::size_t>(variant, 0, m.tool_calls.size());
  371. assert_equals(variant, std::string("CONTENT"), m.content);
  372. assert_equals(variant, std::string(""), m.reasoning_content);
  373. }
  374. }
  375. static void test_with_args(const std::string & input, const std::string & expected, bool parse_as_partial = true, bool is_partial = true) {
  376. common_chat_msg_parser builder(input, parse_as_partial, {});
  377. auto js = builder.try_consume_json_with_dumped_args({{"args"}}, {});
  378. assert_equals(true, js.has_value());
  379. assert_equals(is_partial, js->is_partial);
  380. assert_equals(expected, js->value.dump());
  381. }
  382. static void test_json_with_dumped_args_no_args() {
  383. // Normal JSON, nothing to heal, nothing to dump
  384. test("{\"name\": \"python\"}", false, {}, {}, "{\"name\":\"python\"}");
  385. // Full json is args
  386. test("{\"name\": \"python\"}", false, {{}}, {}, "{\"name\":\"python\"}");
  387. // If the arguments are further down, don't heal partial content.
  388. for (const auto & src : barely_healable_jsons) {
  389. test(src, true, {{"arguments"}}, {}, "{}");
  390. }
  391. // But heal content that isn't partial.
  392. test("{\"name\": \"python\"", true, {{"arguments"}}, {}, "{\"name\":\"python\"}");
  393. }
  394. static void test_json_with_dumped_args() {
  395. // Partial content.
  396. test("{\"content\": \"t", true, {}, {{"content"}}, "{\"content\":\"t\"}");
  397. test("{\"content\": \"", true, {}, {{"content"}}, "{\"content\":\"\"}");
  398. test("{\"content\": ", true, {}, {{"content"}}, "{}");
  399. // If the entire JSON is the arguments, healing it them dumping it produces the same output as the input (just reformatted).
  400. test("{\"name\": \"python", true, {{}}, {}, "{\"name\":\"python");
  401. for (const auto & src : barely_healable_jsons) {
  402. test(src, true, {{}}, {}, src);
  403. }
  404. // Full JSON w/ args
  405. for (auto parse_as_partial : {true, false}) {
  406. test_with_args(
  407. R"({"name": "python", "args": {"arg1": 1}})",
  408. R"({"name":"python","args":"{\"arg1\":1}"})",
  409. parse_as_partial,
  410. /* is_partial= */ false
  411. );
  412. }
  413. // Partial JSON w/ partial args
  414. test_with_args(
  415. R"({"foo": "bar", "args": {")",
  416. R"({"foo":"bar","args":"{\""})"
  417. );
  418. // Partial args broken in object key
  419. test_with_args(
  420. R"({"foo": "bar", "args": {"ar)",
  421. R"({"foo":"bar","args":"{\"ar"})"
  422. );
  423. // Partial args broken after object key
  424. test_with_args(
  425. R"({"foo": "bar", "args": {"arg1")",
  426. R"({"foo":"bar","args":"{\"arg1\""})"
  427. );
  428. // Partial args broken before object value
  429. test_with_args(
  430. R"({"foo": "bar", "args": {"arg1":)",
  431. R"({"foo":"bar","args":"{\"arg1\":"})"
  432. );
  433. // Partial args broken before object value (space)
  434. test_with_args(
  435. R"({"foo": "bar", "args": {"arg1": )",
  436. R"({"foo":"bar","args":"{\"arg1\":"})"
  437. );
  438. // Partial args broken in object value that may not be complete (int)
  439. test_with_args(
  440. R"({"foo": "bar", "args": {"arg1": 1)",
  441. R"({"foo":"bar","args":"{\"arg1\":"})"
  442. );
  443. // Partial args broken in object value that is complete (int)
  444. test_with_args(
  445. R"({"foo": "bar", "args": {"arg1": 1 )",
  446. R"({"foo":"bar","args":"{\"arg1\":1"})"
  447. );
  448. // Partial args broken in object value that is incomplete (string)
  449. test_with_args(
  450. R"({"foo": "bar", "args": {"arg1": ")",
  451. R"({"foo":"bar","args":"{\"arg1\":\""})"
  452. );
  453. // Partial args broken in object value that is complete (string)
  454. test_with_args(
  455. R"({"foo": "bar", "args": {"arg1": "1")",
  456. R"({"foo":"bar","args":"{\"arg1\":\"1\""})"
  457. );
  458. // Partial args broken on array opening
  459. test_with_args(
  460. R"({"foo": "bar", "args": [)",
  461. R"({"foo":"bar","args":"["})"
  462. );
  463. // Partial args broken on array value that is incomplete (int)
  464. test_with_args(
  465. R"({"foo": "bar", "args": [1)",
  466. R"({"foo":"bar","args":"["})"
  467. );
  468. // Partial args broken on array value that is complete (int)
  469. test_with_args(
  470. R"({"foo": "bar", "args": [1 )",
  471. R"({"foo":"bar","args":"[1"})"
  472. );
  473. // Partial args broken on array value that is complete (string)
  474. test_with_args(
  475. R"({"foo": "bar", "args": ["1")",
  476. R"({"foo":"bar","args":"[\"1\""})"
  477. );
  478. // Partial args broken after array value
  479. test_with_args(
  480. R"({"foo": "bar", "args": [1,)",
  481. R"({"foo":"bar","args":"[1,"})"
  482. );
  483. // Partial args broken on nested array
  484. test_with_args(
  485. R"({"foo": "bar", "args": {"arg1": [)",
  486. R"({"foo":"bar","args":"{\"arg1\":["})"
  487. );
  488. // Unicode tests
  489. test_with_args(
  490. R"({"foo": "bar", "args": {"arg1": "\u)",
  491. R"({"foo":"bar","args":"{\"arg1\":\"\\u"})"
  492. );
  493. test_with_args(
  494. R"({"foo": "bar", "args": {"arg1": "\u0)",
  495. R"({"foo":"bar","args":"{\"arg1\":\"\\u0"})"
  496. );
  497. test_with_args(
  498. R"({"foo": "bar", "args": {"arg1": "\u00)",
  499. R"({"foo":"bar","args":"{\"arg1\":\"\\u00"})"
  500. );
  501. test_with_args(
  502. R"({"foo": "bar", "args": {"arg1": "\u000)",
  503. R"({"foo":"bar","args":"{\"arg1\":\"\\u000"})"
  504. );
  505. test_with_args(
  506. R"({"foo": "bar", "args": {"arg1": "\u0000)",
  507. R"({"foo":"bar","args":"{\"arg1\":\"\\u0000"})"
  508. );
  509. test_with_args(
  510. R"({"foo": "bar", "args": {"arg1": "\ud8)",
  511. R"({"foo":"bar","args":"{\"arg1\":\"\\ud8"})"
  512. );
  513. test_with_args(
  514. R"({"foo": "bar", "args": {"arg1": "\ud80)",
  515. R"({"foo":"bar","args":"{\"arg1\":\"\\ud80"})"
  516. );
  517. test_with_args(
  518. R"({"foo": "bar", "args": {"arg1": "\ud800)",
  519. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800"})"
  520. );
  521. test_with_args(
  522. R"({"foo": "bar", "args": {"arg1": "\ud800\)",
  523. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\"})"
  524. );
  525. test_with_args(
  526. R"({"foo": "bar", "args": {"arg1": "\ud800\u)",
  527. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\u"})"
  528. );
  529. test_with_args(
  530. R"({"foo": "bar", "args": {"arg1": "\ud800\ud)",
  531. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\ud"})"
  532. );
  533. test_with_args(
  534. R"({"foo": "bar", "args": {"arg1": "\ud800\udc)",
  535. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\udc"})"
  536. );
  537. test_with_args(
  538. R"({"foo": "bar", "args": {"arg1": "\ud800\udc0)",
  539. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\udc0"})"
  540. );
  541. test_with_args(
  542. R"({"foo": "bar", "args": {"arg1": "\ud800\udc00)",
  543. R"({"foo":"bar","args":"{\"arg1\":\"\\ud800\\udc00"})"
  544. );
  545. }
  546. static void test_positions() {
  547. {
  548. common_chat_msg_parser builder("Hello, world!", /* is_partial= */ false, {});
  549. assert_equals<size_t>(0, builder.pos());
  550. assert_throws([&]() { builder.move_to(100); });
  551. assert_equals<size_t>(0, builder.pos());
  552. assert_throws([&]() { builder.move_back(1); });
  553. assert_equals<size_t>(0, builder.pos());
  554. builder.move_to(8);
  555. assert_equals<size_t>(8, builder.pos());
  556. builder.move_back(1);
  557. assert_equals<size_t>(7, builder.pos());
  558. assert_equals("world!", builder.consume_rest());
  559. builder.move_to(0);
  560. assert_equals<size_t>(0, builder.pos());
  561. assert_throws([&]() { builder.finish(); });
  562. assert_equals<size_t>(0, builder.pos());
  563. builder.move_to(builder.input().size());
  564. builder.finish();
  565. }
  566. {
  567. common_chat_msg_parser builder("Hello, world!", /* is_partial= */ true, {});
  568. builder.move_to(builder.input().size());
  569. assert_equals<size_t>(builder.input().size(), builder.pos());
  570. builder.finish();
  571. }
  572. }
  573. int main() {
  574. test_positions();
  575. test_json_with_dumped_args_no_args();
  576. test_json_with_dumped_args();
  577. test_reasoning();
  578. test_regex();
  579. test_deepseek_v3_1_tool_calls();
  580. std::cout << "All tests passed!\n";
  581. return 0;
  582. }