From 7dad2f1a17d65b5e2034c277125bc9f97573a779 Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Mon, 15 Jun 2026 22:10:09 +0200 Subject: [PATCH] chat : fix LFM2 tool-call parsing double-escaping (#24667) * Add escape test cases * chat : fix LFM2 tool-call parsing double-escaping --- common/chat-peg-parser.cpp | 9 +++++---- tests/test-chat.cpp | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index a3aa765d1c..a309f02765 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -540,10 +540,11 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( auto arg_name_parser = literal(prop_name); common_peg_parser arg_value_parser = eps(); - auto string_value_parser = choice({ - literal("\"") + tool_arg_string_value(string_content('"')) + literal("\""), - literal("'") + tool_arg_string_value(string_content('\'')) + literal("'") - }); + // Quoted literal as a value: normalize_quotes_to_json preserves escapes. + auto string_value_parser = tool_arg_value(choice({ + literal("\"") + string_content('"') + literal("\""), + literal("'") + string_content('\'') + literal("'") + })); if (is_string_type) { arg_value_parser = string_value_parser; diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 548071c906..902a4c135a 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -1882,11 +1882,29 @@ static void test_lfm2_parser(const std::string & template_path, bool detailed_de .expect(simple_assist_msg("Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org).")) .run(); - // Python tool with multiline code in string + // Python tool with multiline code in string: the \n in the literal decodes to a real + // newline, emitted as a JSON \n escape (not a doubled backslash). tst.test("<|tool_call_start|>[python(code=\"def hello():\\n print('hey')\")]<|tool_call_end|>") .tools({ python_tool }) .expect_tool_calls({ - { "python", R"#({"code": "def hello():\\n print('hey')"})#", "" } + { "python", R"#({"code": "def hello():\n print('hey')"})#", "" } + }) + .run(); + + // String escape sequences decode to their actual characters (newline + tab here), + // so a "write a two line file" style call produces real line breaks, not literal "\n". + tst.test("<|tool_call_start|>[python(code=\"First line\\nSecond line\\tindented\")]<|tool_call_end|>") + .tools({ python_tool }) + .expect_tool_calls({ + { "python", R"#({"code": "First line\nSecond line\tindented"})#", "" } + }) + .run(); + + // Escaped quotes inside a string argument survive the round-trip. + tst.test("<|tool_call_start|>[python(code=\"print(\\\"hi\\\")\")]<|tool_call_end|>") + .tools({ python_tool }) + .expect_tool_calls({ + { "python", R"#({"code": "print(\"hi\")"})#", "" } }) .run();