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);