chat: fix handling of space in reasoning markers (#22353)

* chat: fix handling of space in reasoning markers

* fix tests

* whitespace
This commit is contained in:
Piotr Wilkin (ilintar)
2026-04-25 21:24:13 +02:00
committed by GitHub
parent 98dc1418ea
commit dcad77cc3b
3 changed files with 23 additions and 28 deletions
+4 -4
View File
@@ -296,7 +296,7 @@ void analyze_reasoning::compare_reasoning_presence() {
return p.literal(reasoning_content) + p.space() + p.optional(p.tag("post", (p.marker() + p.space())) + p.rest()); return p.literal(reasoning_content) + p.space() + p.optional(p.tag("post", (p.marker() + p.space())) + p.rest());
}); });
auto parser_wrapped = build_tagged_peg_parser([&](common_peg_parser_builder &p) { auto parser_wrapped = build_tagged_peg_parser([&](common_peg_parser_builder &p) {
return p.tag("pre", p.marker() + p.space()) + p.literal(reasoning_content) + p.space() + p.tag("post", (p.marker() + p.space())) + p.rest(); return p.tag("pre", p.marker() + p.space()) + p.literal(reasoning_content) + p.tag("post", (p.space() + p.marker() + p.space())) + p.rest();
}); });
// try the more aggressive parse first, if it fails, fall back to the delimiter one // try the more aggressive parse first, if it fails, fall back to the delimiter one
auto result = parser_wrapped.parse_anywhere_and_extract(comparison->output_B); auto result = parser_wrapped.parse_anywhere_and_extract(comparison->output_B);
@@ -306,11 +306,11 @@ void analyze_reasoning::compare_reasoning_presence() {
if (result.result.success()) { if (result.result.success()) {
if (!result.tags["pre"].empty() && !result.tags["post"].empty()) { if (!result.tags["pre"].empty() && !result.tags["post"].empty()) {
mode = reasoning_mode::TAG_BASED; mode = reasoning_mode::TAG_BASED;
start = trim_leading_whitespace(result.tags["pre"]); start = result.tags["pre"];
end = trim_trailing_whitespace(result.tags["post"]); end = result.tags["post"];
} else if (!result.tags["post"].empty()) { } else if (!result.tags["post"].empty()) {
mode = reasoning_mode::TAG_BASED; mode = reasoning_mode::TAG_BASED;
end = trim_trailing_whitespace(result.tags["post"]); end = result.tags["post"];
} }
} }
} }
+1 -1
View File
@@ -1331,7 +1331,7 @@ static void test_nemotron_reasoning_detection(testing & t) {
// Check reasoning markers // Check reasoning markers
t.assert_equal("reasoning_start should be '<think>\\n'", "<think>\n", analysis.reasoning.start); t.assert_equal("reasoning_start should be '<think>\\n'", "<think>\n", analysis.reasoning.start);
t.assert_equal("reasoning_end should be '</think>'", "</think>", analysis.reasoning.end); t.assert_equal("reasoning_end should be '\\n</think>\\n'", "\n</think>\n", analysis.reasoning.end);
// Check reasoning mode detection // Check reasoning mode detection
// Nemotron uses tag-based reasoning; prefill handles the template's forced markers // Nemotron uses tag-based reasoning; prefill handles the template's forced markers
+18 -23
View File
@@ -1642,22 +1642,16 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
// Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case) // Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case)
auto tst = peg_tester("models/templates/Qwen3.5-4B.jinja", detailed_debug); auto tst = peg_tester("models/templates/Qwen3.5-4B.jinja", detailed_debug);
tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?") tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO) .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true) .enable_thinking(true)
.expect(message_assist_thoughts) .expect(message_assist_thoughts)
.run(); .run();
tst.test("I'm\nthinking\n</think>\nHello, world!\nWhat's up?") tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_NONE) .reasoning_format(COMMON_REASONING_FORMAT_NONE)
.expect_content("<think>\nI'm\nthinking\n</think>\nHello, world!\nWhat's up?") .expect_content("<think>\nI'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
.run();
tst.test("I'm\nthinking\n</think>\nHello, world!\nWhat's up?")
.enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.expect(message_assist_thoughts)
.run(); .run();
tst.test( tst.test(
@@ -1673,7 +1667,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.run(); .run();
tst.test( tst.test(
"I'm\nthinking\n</think>\n" "I'm\nthinking\n</think>\n\n"
"<tool_call>\n" "<tool_call>\n"
"<function=special_function>\n" "<function=special_function>\n"
"<parameter=arg1>\n1\n</parameter>\n" "<parameter=arg1>\n1\n</parameter>\n"
@@ -1731,7 +1725,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
tst.test( tst.test(
"I need to output the invoice details in JSON\n" "I need to output the invoice details in JSON\n"
"</think>\n" "</think>\n\n"
R"({"amount": 123.45, "date": "2025-12-03"})") R"({"amount": 123.45, "date": "2025-12-03"})")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO) .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true) .enable_thinking(true)
@@ -1751,7 +1745,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
"hello()\n" "hello()\n"
"</parameter>\n" "</parameter>\n"
"</function>\n" "</function>\n"
"</tool_call></think>\n" "</tool_call>\n</think>\n\n"
"<tool_call>\n" "<tool_call>\n"
"<function=python>\n" "<function=python>\n"
"<parameter=code>\n" "<parameter=code>\n"
@@ -1994,7 +1988,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
"hello()\n" "hello()\n"
"</parameter>\n" "</parameter>\n"
"</function>\n" "</function>\n"
"</tool_call></think>\n" "</tool_call>\n</think>\n"
"<tool_call>\n" "<tool_call>\n"
"<function=python>\n" "<function=python>\n"
"<parameter=code>\n" "<parameter=code>\n"
@@ -3463,7 +3457,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.run(); .run();
// Tool call with reasoning (enable_thinking=true) // Tool call with reasoning (enable_thinking=true)
tst.test("I'm\nthinking</think><tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>") tst.test("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO) .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({ special_function_tool }) .tools({ special_function_tool })
@@ -3487,7 +3481,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.run(); .run();
// Tool call with reasoning and content // Tool call with reasoning and content
tst.test("I need to call a function</think>" tst.test("I need to call a function\n</think>\n\n"
"Let me check the time.<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>") "Let me check the time.<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO) .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
@@ -3514,7 +3508,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
// fake tool call marker in reasoning // fake tool call marker in reasoning
tst.test( tst.test(
"Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm</think>" "Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm\n</think>\n\n"
"<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>") "<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO) .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
@@ -3542,11 +3536,11 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
// Format: <minimax:tool_call><invoke name="func"><parameter name="key">value</parameter></invoke></minimax:tool_call> // Format: <minimax:tool_call><invoke name="func"><parameter name="key">value</parameter></invoke></minimax:tool_call>
{ {
auto tst = peg_tester("models/templates/MiniMax-M2.jinja", detailed_debug); auto tst = peg_tester("models/templates/MiniMax-M2.jinja", detailed_debug);
tst.test("</think>Hello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist).run(); tst.test("\n</think>\n\nHello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist).run();
tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist_thoughts).run(); tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist_thoughts).run();
tst.test("Let's call a tool:</think><minimax:tool_call>\n<invoke name=\"empty_args\">\n</invoke>\n</minimax:tool_call>"). tst.test("Let's call a tool:\n</think>\n\n<minimax:tool_call>\n<invoke name=\"empty_args\">\n</invoke>\n</minimax:tool_call>").
enable_thinking(true). enable_thinking(true).
reasoning_format(COMMON_REASONING_FORMAT_AUTO). reasoning_format(COMMON_REASONING_FORMAT_AUTO).
tools({ empty_args_tool }). tools({ empty_args_tool }).
@@ -3554,7 +3548,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
run(); run();
tst.test( tst.test(
"</think><minimax:tool_call>\n<invoke name=\"special_function\">\n<parameter " "\n</think>\n\n<minimax:tool_call>\n<invoke name=\"special_function\">\n<parameter "
"name=\"arg1\">1</parameter>\n</invoke>\n</minimax:tool_call>") "name=\"arg1\">1</parameter>\n</invoke>\n</minimax:tool_call>")
.tools({ special_function_tool }) .tools({ special_function_tool })
.expect(message_assist_call) .expect(message_assist_call)
@@ -3714,7 +3708,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.enable_thinking(false) .enable_thinking(false)
.expect(message_assist) .expect(message_assist)
.run(); .run();
tst.test("I'm\nthinking</think>\n\nHello, world!\nWhat's up?") tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_assist_thoughts) .expect(message_assist_thoughts)
@@ -3729,7 +3723,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.tools({ special_function_tool }) .tools({ special_function_tool })
.expect(message_assist_call_content) .expect(message_assist_call_content)
.run(); .run();
tst.test("I'm\nthinking</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>") tst.test("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>")
.enable_thinking(true) .enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.tools({ special_function_tool }) .tools({ special_function_tool })
@@ -4006,7 +4000,8 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
{ {
auto tst = peg_tester("models/templates/StepFun3.5-Flash.jinja", detailed_debug); auto tst = peg_tester("models/templates/StepFun3.5-Flash.jinja", detailed_debug);
tst.test("I was thinking</think>\nNow I'm not.").
tst.test("I was thinking\n</think>\nNow I'm not.").
enable_thinking(true). enable_thinking(true).
reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK). reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK).
expect_reasoning("I was thinking"). expect_reasoning("I was thinking").