diff --git a/common/chat.cpp b/common/chat.cpp index 96019cc38..5f956e64e 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1803,6 +1803,9 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars //LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.text.c_str()); common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT; + if (!is_partial) { + flags |= COMMON_PEG_PARSE_FLAG_COMPLETE; + } if (params.debug) { flags |= COMMON_PEG_PARSE_FLAG_DEBUG; } diff --git a/common/parsers/qwen3-coder.cpp b/common/parsers/qwen3-coder.cpp index abed16974..7cb9cce90 100644 --- a/common/parsers/qwen3-coder.cpp +++ b/common/parsers/qwen3-coder.cpp @@ -76,9 +76,10 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat auto reasoning = p.eps(); if (supports_reasoning && extract_reasoning) { + // The template strips the newline before , so leave it out of the reasoning reasoning = p.optional("" + p.space() + - p.reasoning(p.until_one_of({ "", "" })) + - (p.literal("") | p.peek(p.literal("")))); + p.reasoning(p.until_one_of({ "\n", "", "" })) + + (p.literal("\n") | p.literal("") | p.peek(p.literal("")))); } // Response format parser @@ -166,8 +167,15 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat auto calls = inputs.parallel_tool_calls ? tool_call_first + p.zero_or_more(tool_call) : tool_call_first; auto tool_calls = p.trigger_rule("tool-call-root", p.repeat(calls, min_calls, 1)); + // The templates separate content from the tool calls with one or two newlines, + // stop the content before them + std::vector content_ends; + for (const auto & start : tool_call_starts) { + content_ends.insert(content_ends.end(), { "\n\n" + start, "\n" + start, start }); + } + return generation_prompt + - (reasoning << p.content(p.until_one_of(tool_call_starts)) << tool_calls); + (reasoning << p.content(p.until_one_of(content_ends)) << tool_calls); } // Content only parser diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 167a889e3..e2ccb77bb 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -721,8 +721,9 @@ struct parser_executor { return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_SUCCESS, start_pos, pos, {}, std::move(invalid_utf8)); } - if (match == common_trie::PARTIAL_MATCH) { - // Found a partial match extending to end of input, return everything before it + if (match == common_trie::PARTIAL_MATCH && !ctx.is_complete()) { + // Found a partial match extending to end of input, return everything before it. + // On a complete input the delimiter can no longer form, so the bytes belong to the text return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_SUCCESS, start_pos, pos, {}, std::move(invalid_utf8)); } diff --git a/common/peg-parser.h b/common/peg-parser.h index 6b348fab0..46df99875 100644 --- a/common/peg-parser.h +++ b/common/peg-parser.h @@ -162,9 +162,10 @@ struct common_peg_parse_result { }; enum common_peg_parse_flags { - COMMON_PEG_PARSE_FLAG_NONE = 0, - COMMON_PEG_PARSE_FLAG_LENIENT = 1 << 0, - COMMON_PEG_PARSE_FLAG_DEBUG = 1 << 1, + COMMON_PEG_PARSE_FLAG_NONE = 0, + COMMON_PEG_PARSE_FLAG_LENIENT = 1 << 0, + COMMON_PEG_PARSE_FLAG_DEBUG = 1 << 1, + COMMON_PEG_PARSE_FLAG_COMPLETE = 1 << 2, }; inline common_peg_parse_flags operator|(common_peg_parse_flags a, common_peg_parse_flags b) { @@ -209,6 +210,7 @@ struct common_peg_parse_context { bool is_lenient() const { return flags & COMMON_PEG_PARSE_FLAG_LENIENT; } bool is_debug() const { return flags & COMMON_PEG_PARSE_FLAG_DEBUG; } + bool is_complete() const { return flags & COMMON_PEG_PARSE_FLAG_COMPLETE; } }; class common_peg_arena; diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 38a3fbe2e..0ab8e65ab 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -2473,7 +2473,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ run_in_terminal_tool }) - .expect_content("Let me inspect the current directory.\n") + .expect_content("Let me inspect the current directory.") .expect_tool_calls({ { "run_in_terminal", R"({"command": "pwd"})", {} }, }) @@ -2495,7 +2495,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { run_in_terminal_tool }) .tool_choice(COMMON_CHAT_TOOL_CHOICE_REQUIRED) - .expect_content("Let me inspect the current directory.\n") + .expect_content("Let me inspect the current directory.") .expect_tool_calls({ { "run_in_terminal", R"({"command": "pwd"})", {} }, }) @@ -2604,7 +2604,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tool_choice(COMMON_CHAT_TOOL_CHOICE_REQUIRED) .messages({ user_start, assistant_todos, tool_result, user_continue }) .expect_reasoning("I need to run a terminal command.") - .expect_content("Let me inspect the current directory.\n") + .expect_content("Let me inspect the current directory.") .expect_tool_calls({ { "run_in_terminal", R"({"command": "pwd"})", {} }, }) @@ -8146,12 +8146,12 @@ static void test_chat_session() { for (size_t i = 0; i < thinking.size(); i += 3) { session.feed(common_chat_input(thinking.substr(i, 3))); } - assert_msg_equals(simple_assist_msg("", "I'm\nthinking\n"), session.msg()); + assert_msg_equals(simple_assist_msg("", "I'm\nthinking"), session.msg()); for (size_t i = thinking.size(); i < output.size(); i += 3) { session.feed(common_chat_input(output.substr(i, 3))); } - assert_msg_equals(simple_assist_msg("", "I'm\nthinking\n", "special_function", "{\"arg1\":1}"), session.finish()); + assert_msg_equals(simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1\":1}"), session.finish()); } // a copy does not see what is fed to the original @@ -8176,7 +8176,7 @@ static void test_chat_session() { common_chat_session session(tmpls.get(), nullptr, cont); assert_equals(std::string("<|im_start|>assistant\n\nI'm thinking\n\n\nHello, "), session.generation_prompt()); - assert_msg_equals(simple_assist_msg("Hello, ", "I'm thinking\n"), session.msg()); + assert_msg_equals(simple_assist_msg("Hello, ", "I'm thinking"), session.msg()); session.feed(common_chat_input("world!")); assert_equals(std::string("Hello, world!"), session.msg().content);