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) {