chat : stop qwen3-coder reasoning and content before template separators (#30557)

* chat : strip trailing whitespace from parsed reasoning and content

Assisted-by: Claude Fable 5.1

* chat : stop qwen3-coder reasoning and content before template separators

Assisted-by: Claude

* tests : remove the Qwen parser case from the chat session tests

Assisted-by: Claude

* common : handle partial prefix until match at end of generation

---------

Co-authored-by: Zeshawn Zahid <zzshawnn@gmail.com>
Co-authored-by: Alde Rojas <hello@alde.dev>
This commit is contained in:
authored and GitHub committed 2026-10-28 10:11:47 +02:00
1 parent f4d8af608a
commit a71c00f0bb
5 files changed
+28 -14

No files matched your search

+3
View File
@@ -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;
}
+11 -3
View File
@@ -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 </think>, so leave it out of the reasoning
reasoning = p.optional("<think>" + p.space() +
p.reasoning(p.until_one_of({ "</think>", "<tool_call>" })) +
(p.literal("</think>") | p.peek(p.literal("<tool_call>"))));
p.reasoning(p.until_one_of({ "\n</think>", "</think>", "<tool_call>" })) +
(p.literal("\n</think>") | p.literal("</think>") | p.peek(p.literal("<tool_call>"))));
}
// 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<std::string> 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
+3 -2
View File
@@ -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));
}
+5 -3
View File
@@ -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;
+6 -6
View File
@@ -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<think>\nI'm thinking\n</think>\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);