common/autoparser: fixes for newline handling / forced tool calls (#22654)

* chat/autoparser: the fixes

* Move optspace() to chat-peg-parser, comment out server tests invalidated due to content now allowed with forced tool calls.

* Trim whitespace on apply instead
This commit is contained in:
Piotr Wilkin (ilintar)
2026-05-04 13:18:11 +02:00
committed by GitHub
parent 994118a183
commit a4701c98f7
10 changed files with 392 additions and 97 deletions
+265 -12
View File
@@ -542,6 +542,36 @@ static common_chat_tool edit_tool{
})",
};
static common_chat_tool manage_todo_list_tool{
/* .name = */ "manage_todo_list",
/* .description = */ "Create or update the todo list",
/* .parameters = */ R"({
"type": "object",
"properties": {
"todos": {
"type": "array",
"description": "List of TODO list items"
}
},
"required": ["todos"]
})",
};
static common_chat_tool run_in_terminal_tool{
/* .name = */ "run_in_terminal",
/* .description = */ "Run a shell command.",
/* .parameters = */ R"({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to run"
}
},
"required": ["command"]
})",
};
static common_chat_tool magic_tool{
/* .name = */ "magic",
/* .description = */ "Magic tool that takes a hash",
@@ -1379,6 +1409,16 @@ class peg_test_builder {
return *this;
}
peg_test_builder & tool_choice(common_chat_tool_choice choice) {
tc_.params.tool_choice = choice;
return *this;
}
peg_test_builder & messages(std::vector<common_chat_msg> messages) {
tc_.params.messages = std::move(messages);
return *this;
}
// Execute the test
void run() {
// Check template filter
@@ -1755,23 +1795,23 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
"hello()\n"
"</parameter>\n"
"</function>\n"
"</tool_call>"
)
"</tool_call>")
.enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({
python_tool
})
.expect_reasoning("Let's call a tool: <tool_call>\n"
"<function=python>\n"
"<parameter=code>\n"
"def hello():\n"
" print(\"Not the real call!\")\n"
"\n"
"hello()\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.expect_reasoning(
"Let's call a tool: <tool_call>\n"
"<function=python>\n"
"<parameter=code>\n"
"def hello():\n"
" print(\"Not the real call!\")\n"
"\n"
"hello()\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.expect_tool_calls({
{ "python", "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}", {} },
})
@@ -1800,6 +1840,219 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.tools({ empty_args_tool_no_properties })
.expect(message_with_tool_calls("empty_args_no_props", "{}"))
.run();
// Edge cases when reasoning traces are not sent
tst.test(
"<think>\n\n</think>\n\n"
"<tool_call>\n"
"<function=special_function>\n"
"<parameter=arg1>\n1\n</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({
special_function_tool
})
.expect_reasoning("<think>\n\n")
.expect_tool_calls({ { "special_function", "{\"arg1\": 1}", "" } })
.run();
tst.test(
"</think>\n\n"
"<tool_call>\n"
"<function=special_function>\n"
"<parameter=arg1>\n1\n</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({
special_function_tool
})
.expect_reasoning("")
.expect_tool_calls({ { "special_function", "{\"arg1\": 1}", "" } })
.run();
tst.test(
"</think>\n\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
run_in_terminal_tool
})
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"</think>\n\n"
"Let me inspect the current directory.\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
run_in_terminal_tool
})
.expect_content("Let me inspect the current directory.\n")
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"</think>\n\n"
"Let me inspect the current directory.\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
run_in_terminal_tool
})
.tool_choice(COMMON_CHAT_TOOL_CHOICE_REQUIRED)
.expect_content("Let me inspect the current directory.\n")
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"I should inspect the directory.\n"
"</think>\n\n"
"Let me inspect it now.\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
run_in_terminal_tool
})
.expect_reasoning("I should inspect the directory.")
.expect_content("Let me inspect it now.\n")
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"I might call <tool_call> later, but I am still thinking.\n"
"</think>\n\n"
"Final answer without tools.")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({ run_in_terminal_tool })
.expect_reasoning("I might call <tool_call> later, but I am still thinking.")
.expect_content("Final answer without tools.")
.run();
{
common_chat_msg user_start;
user_start.role = "user";
user_start.content = "Create a todo list, then inspect the repository.";
common_chat_msg assistant_todos =
simple_assist_msg("", "", "manage_todo_list",
R"({"todos":[{"item":"Inspect repository","selected":false}]})", "call_todos");
common_chat_msg tool_result;
tool_result.role = "tool";
tool_result.content = "Successfully wrote todo list";
tool_result.tool_call_id = "call_todos";
common_chat_msg user_continue;
user_continue.role = "user";
user_continue.content = "Proceed.";
tst.test(
"I need to run a terminal command.\n"
"</think>\n\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
manage_todo_list_tool, run_in_terminal_tool
})
.messages({ user_start, assistant_todos, tool_result, user_continue })
.expect_reasoning("I need to run a terminal command.")
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"I need to run a terminal command.\n"
"</think>\n\n"
"Let me inspect the current directory.\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
manage_todo_list_tool, run_in_terminal_tool
})
.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_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
tst.test(
"</think>\n\n"
"<tool_call>\n"
"<function=run_in_terminal>\n"
"<parameter=command>\n"
"pwd\n"
"</parameter>\n"
"</function>\n"
"</tool_call>")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.enable_thinking(true)
.tools({
manage_todo_list_tool, run_in_terminal_tool
})
.messages({ user_start, assistant_todos, tool_result, user_continue })
.expect_tool_calls({
{ "run_in_terminal", R"({"command": "pwd"})", {} },
})
.run();
}
}
{
+7 -7
View File
@@ -70,20 +70,20 @@ static void test_reasoning_budget(
llama_sampler_apply(sampler, &cur_p);
// Check if forcing is active (all logits except one should be -INFINITY)
size_t finite_count = 0;
llama_token finite_token = -1;
size_t not_neg_inf = 0;
llama_token not_neg_inf_token = -1;
for (size_t j = 0; j < cur.size(); j++) {
if (std::isfinite(cur[j].logit)) {
finite_count++;
finite_token = cur[j].id;
if (std::isfinite(cur[j].logit) || cur[j].logit > 0) { // +INFINITY
not_neg_inf++;
not_neg_inf_token = cur[j].id;
}
}
llama_sampler_accept(sampler, sequence[i]);
fprintf(stderr, " i=%zu: token=%d, finite_count=%zu, finite_token=%d\n", i, (int)sequence[i], finite_count, (int)finite_token);
fprintf(stderr, " i=%zu: token=%d, not_neg_inf_count=%zu, not_neg_inf_token=%d\n", i, (int)sequence[i], not_neg_inf, (int)not_neg_inf_token);
if (finite_count == 1) {
if (not_neg_inf == 1) {
if (actual_force_start == SIZE_MAX) {
actual_force_start = i;
}