Commit 0cc522e45 for llama.cpp
commit 0cc522e45d29dc7de9980d65c2caa85f2d133a25
Author: Tarek Dakhran <tarek@liquid.ai>
Date: Sat Nov 14 23:34:18 2026 +0100
chat : allow undeclared tool calls in LFM2 tool-call grammar (#32164)
* chat : honor OpenAI tool strict flag in LFM2 tool-call grammar
* chat : drop strict, always allow undeclared LFM2 tool calls
diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp
index 968cbfa98..56938d5ef 100644
--- a/common/chat-peg-parser.cpp
+++ b/common/chat-peg-parser.cpp
@@ -562,7 +562,8 @@ common_peg_parser common_chat_peg_builder::python_or_json_value() {
common_peg_parser common_chat_peg_builder::python_style_tool_calls(
const ordered_json & tools,
bool parallel_tool_calls,
- bool allow_json_literals) {
+ bool allow_json_literals,
+ bool allow_undeclared) {
if (!tools.is_array() || tools.empty()) {
return eps();
}
@@ -620,6 +621,18 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls(
tool_choices |= rule("tool-" + std::to_string(i), tool_parser);
}
+ // last, so declared tools win when parsing
+ if (allow_undeclared) {
+ auto ident = rule("tool-undeclared-ident", chars("[a-zA-Z_]", 1, 1) + chars("[a-zA-Z0-9_.]", 0, -1));
+ auto value = tool_arg_value(allow_json_literals ? python_or_json_value() : python_value());
+ auto arg = tool_arg(tool_arg_open(tool_arg_name(ident) + literal("=")) + value + tool_arg_close(eps()));
+ auto args = optional(arg + zero_or_more("," + space() + arg));
+
+ tool_choices |= rule("tool-undeclared", tool(tool_open(tool_name(ident) + literal("(")) +
+ space() + tool_args(args) + space() + tool_close(literal(")"))
+ ));
+ }
+
if (parallel_tool_calls) {
return "[" + space() + tool_choices + zero_or_more("," + space() + tool_choices) + space() + "]";
}
diff --git a/common/chat-peg-parser.h b/common/chat-peg-parser.h
index b827b60b0..c77ec117b 100644
--- a/common/chat-peg-parser.h
+++ b/common/chat-peg-parser.h
@@ -161,9 +161,11 @@ class common_chat_peg_builder : public common_peg_parser_builder {
// Helper for Python-style function call format: name(arg1="value1", arg2=123)
// Used by LFM2 and similar templates
+ // allow_undeclared: also accept calls to names not in tools (any identifier, any arguments)
common_peg_parser python_style_tool_calls(const common_json & tools,
bool parallel_tool_calls,
- bool allow_json_literals);
+ bool allow_json_literals,
+ bool allow_undeclared = false);
private:
// Python values plus JSON true/false/null.
diff --git a/common/parsers/lfm2.cpp b/common/parsers/lfm2.cpp
index 1bb1e6728..3ecd9573d 100644
--- a/common/parsers/lfm2.cpp
+++ b/common/parsers/lfm2.cpp
@@ -83,7 +83,8 @@ common_chat_params common_chat_params_init_lfm2(const common_chat_template &
auto tool_calls = p.rule("tool-calls",
p.trigger_rule("tool-call",
p.literal(TOOL_CALL_START) +
- p.python_style_tool_calls(inputs.tools, inputs.parallel_tool_calls, /* allow_json_literals = */ true) +
+ p.python_style_tool_calls(inputs.tools, inputs.parallel_tool_calls, /* allow_json_literals = */ true,
+ /* allow_undeclared = */ true) +
p.literal(TOOL_CALL_END)
)
);
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index 62a5117e7..53a1b6d1a 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -2214,6 +2214,21 @@ static void test_lfm2_parser(const std::string & template_path, bool detailed_de
.expect(simple_assist_msg("", "", "empty_args", "{}"))
.run();
+ // undeclared names stay tool calls instead of being forced onto a declared tool
+ tst.test("<|tool_call_start|>[reseat_sim_card()]<|tool_call_end|>")
+ .tools({ special_function_tool })
+ .expect(simple_assist_msg("", "", "reseat_sim_card", "{}"))
+ .run();
+
+ tst.test("<|tool_call_start|>[set_sim_lock(mode='pin', force=True)]<|tool_call_end|>")
+ .tools({ special_function_tool })
+ .expect_tool_calls({ { "set_sim_lock", R"({"mode": "pin", "force": true})", "" } })
+ .run();
+
+ tst.test("<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>")
+ .tools({ special_function_tool })
+ .expect(message_assist_call)
+ .run();
}
static void test_template_output_peg_parsers(bool detailed_debug) {