test-chat-parser.cpp 27 KB

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