diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp
index e1105c146a..aa03aea5a9 100644
--- a/common/chat-auto-parser-generator.cpp
+++ b/common/chat-auto-parser-generator.cpp
@@ -369,7 +369,9 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
func_parser = p.atomic(p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) +
call_id_section) + p.space() + args_seq;
matched_atomic = true;
- } else if (!arguments.name_prefix.empty() && properties.size() > 0) {
+ } else if (!arguments.name_prefix.empty() && !required_parsers.empty()) {
+ // Only peek for an arg tag when there are required args that must follow.
+ // When all args are optional, the model may emit no arg tags at all (#20650).
func_parser = p.atomic(p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) +
call_id_section + p.space() + p.peek(p.literal(arguments.name_prefix))) + args_seq;
matched_atomic = true;
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index 727cb35748..58fef8e99c 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -1934,6 +1934,24 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
{ "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", {} },
})
.run();
+
+ // #20650: tool with no required args, model emits name with no arg tags.
+ {
+ static common_chat_tool no_args_tool{
+ "read_file_diff_md", "Reads a file diff",
+ R"({"type":"object","properties":{"review_id":{"type":"string"},"file_id":{"type":"string"}}})",
+ };
+ tst.test(
+ "Let me read the diff content."
+ ""
+ "read_file_diff_md")
+ .enable_thinking(true)
+ .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
+ .tools({ no_args_tool })
+ .expect_reasoning("Let me read the diff content.")
+ .expect_tool_calls({{ "read_file_diff_md", "{}", {} }})
+ .run();
+ }
}
// Kimi-K2-Thinking tests - custom parser