From e75dff5bc3f294067b0f24809cfa553a3799af46 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 01:49:06 +0000 Subject: [PATCH 01/20] build & test w/ sanitizers --- .github/workflows/build.yml | 12 +++++++++--- CMakeLists.txt | 9 +++++++++ README.md | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f7fbdb..fdbbdc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,8 +44,14 @@ jobs: Release, Debug, ] + sanitizer: [ + none, + address, + thread, + undefined, + ] runs-on: ${{ matrix.setup.os }} - name: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }} + name: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }} timeout-minutes: 30 steps: @@ -58,7 +64,7 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1.2.11 with: - key: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }} + key: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }}-ccache - name: Set up CMake uses: lukka/get-cmake@latest @@ -75,7 +81,7 @@ jobs: - name: Configure CMake env: HF_TOKEN: ${{ secrets.HF_TOKEN }} - run: cmake -B ${{github.workspace}}/build ${{ matrix.setup.defines }} -DCMAKE_BUILD_TYPE=${{ matrix.type }} + run: cmake -B ${{github.workspace}}/build ${{ matrix.setup.defines }} -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DMINJA_SANITIZER=${{ matrix.sanitizer }} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{ matrix.type }} --parallel diff --git a/CMakeLists.txt b/CMakeLists.txt index 95dabe7..b92ae02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,15 @@ option(MINJA_EXAMPLE_ENABLED "minja: Build with example" option(MINJA_FUZZTEST_ENABLED "minja: fuzztests enabled" MINJA_FUZZTEST_ENABLED_DEFAULT) option(MINJA_FUZZTEST_FUZZING_MODE "minja: run fuzztests (if enabled) in fuzzing mode" OFF) option(MINJA_USE_VENV "minja: use Python venv for build" MINJA_USE_VENV_DEFAULT) +set(MINJA_SANITIZERS thread address undefined none) +set(MINJA_SANITIZER none CACHE STRING "minja: sanitizer to use") +set_property(CACHE MINJA_SANITIZER PROPERTY STRINGS ${MINJA_SANITIZERS}) + +if (NOT MSVC AND NOT MINJA_SANITIZER STREQUAL "none") + message(STATUS "Using -fsanitize=${MINJA_SANITIZER}") + add_compile_options("-fsanitize=${MINJA_SANITIZER}") + link_libraries ("-fsanitize=${MINJA_SANITIZER}") +endif() set(CMAKE_CXX_STANDARD 17) diff --git a/README.md b/README.md index 5981079..36a3291 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,26 @@ Main limitations (non-exhaustive list): ./scripts/fuzzing_tests.sh ``` +- Sanitizer tests: + + ```bash + for sanitizer in ADDRESS THREAD UNDEFINED ; do + docker run --rm \ + -v "$PWD":/src:ro \ + -v "$PWD/build-sanitizer-${sanitizer}":/src/build \ + -w /src \ + "$(echo " + FROM ghcr.io/astral-sh/uv:debian-slim + RUN apt-get update && apt-get install -y build-essential libcurl4-openssl-dev cmake clang-tidy + " | docker build . -q -f - )" \ + bash -c " + cmake -B build -DCMAKE_BUILD_TYPE=Debug -DMINJA_SANITIZER=${sanitizer} && \ + cmake --build build -j --config Debug && \ + ctest --test-dir build -j -C Debug --output-on-failure + " + done + ``` + - If your model's template doesn't run fine, please consider the following before [opening a bug](https://github.com/googlestaging/minja/issues/new): - Is the template using any unsupported filter / test / method / global function, and which one(s)? From a40e7e4908807b81bb3d9f81acc686beb6d31be2 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 01:51:50 +0000 Subject: [PATCH 02/20] Fix circular context reference docker run --rm \ -v "$PWD":/src:ro \ -v "$PWD/build-docker":/src/build \ -w /src \ "$(echo " FROM ghcr.io/astral-sh/uv:debian-slim RUN apt-get update && apt-get install -y build-essential libcurl4-openssl-dev cmake clang-tidy " | docker build . -q -f - )" \ bash -c " cmake -B build -DCMAKE_BUILD_TYPE=Debug -DMINJA_SANITIZER=address && \ cmake --build build -j --config Debug && \ ctest --test-dir build -j -C Debug --output-on-failure " --- include/minja/minja.hpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 5ed0556..4295e73 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -1060,11 +1060,18 @@ class MacroNode : public TemplateNode { } } } - void do_render(std::ostringstream &, const std::shared_ptr & macro_context) const override { + void do_render(std::ostringstream &, const std::shared_ptr & context) const override { if (!name) throw std::runtime_error("MacroNode.name is null"); if (!body) throw std::runtime_error("MacroNode.body is null"); - auto callable = Value::callable([this, macro_context](const std::shared_ptr & call_context, ArgumentsValue & args) { - auto execution_context = Context::make(Value::object(), macro_context); + + // Use init-capture to avoid dangling 'this' pointer and circular references + auto callable = Value::callable([weak_context = std::weak_ptr(context), + name = name, params = params, body = body, + named_param_positions = named_param_positions] + (const std::shared_ptr & call_context, ArgumentsValue & args) { + auto context_locked = weak_context.lock(); + if (!context_locked) throw std::runtime_error("Macro context no longer valid"); + auto execution_context = Context::make(Value::object(), context_locked); if (call_context->contains("caller")) { execution_context->set("caller", call_context->get("caller")); @@ -1640,13 +1647,17 @@ class CallNode : public TemplateNode { void do_render(std::ostringstream & out, const std::shared_ptr & context) const override { if (!expr) throw std::runtime_error("CallNode.expr is null"); if (!body) throw std::runtime_error("CallNode.body is null"); - - auto caller = Value::callable([this, context](const std::shared_ptr &, ArgumentsValue &) -> Value { - return Value(body->render(context)); + + // Use init-capture to avoid dangling 'this' pointer and circular references + auto caller = Value::callable([weak_context = std::weak_ptr(context), body=body] + (const std::shared_ptr &, ArgumentsValue &) -> Value { + auto context_locked = weak_context.lock(); + if (!context_locked) throw std::runtime_error("Caller context no longer valid"); + return Value(body->render(context_locked)); }); - + context->set("caller", caller); - + auto call_expr = dynamic_cast(expr.get()); if (!call_expr) { throw std::runtime_error("Invalid call block syntax - expected function call"); @@ -1657,7 +1668,7 @@ class CallNode : public TemplateNode { throw std::runtime_error("Call target must be callable: " + function.dump()); } ArgumentsValue args = call_expr->args.evaluate(context); - + Value result = function.call(context, args); out << result.to_str(); } @@ -2215,7 +2226,7 @@ class Parser { } } } - + if ((has_first_colon || has_second_colon)) { index = std::make_shared(slice_loc, std::move(start), std::move(end), std::move(step)); } else { From 03a6c98f951affe78afbe640d7ff2a9405e3e562 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 01:56:13 +0000 Subject: [PATCH 03/20] fix bad patch --- include/minja/minja.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 4295e73..e861406 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -1101,7 +1101,7 @@ class MacroNode : public TemplateNode { } return body->render(execution_context); }); - macro_context->set(name->get_name(), callable); + context->set(name->get_name(), callable); } }; @@ -1271,7 +1271,7 @@ class SubscriptExpr : public Expression { } return result; - } else if (target_value.is_array()) { + } else if (target_value.is_array()) { auto result = Value::array(); for (int64_t i = start; step > 0 ? i < end : i > end; i += step) { result.push_back(target_value.at(i)); @@ -1320,7 +1320,7 @@ static bool in(const Value & value, const Value & container) { return (((container.is_array() || container.is_object()) && container.contains(value)) || (value.is_string() && container.is_string() && container.to_str().find(value.to_str()) != std::string::npos)); -}; +} class BinaryOpExpr : public Expression { public: From 2a42ba889ea52b984e340824c3884f9aeb902f19 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 02:00:11 +0000 Subject: [PATCH 04/20] Add tiny reserves in value ctor (+ use emplace to avoid some copies) --- include/minja/minja.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index e861406..5eedf63 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -158,12 +158,14 @@ class Value : public std::enable_shared_from_this { Value(const json & v) { if (v.is_object()) { auto object = std::make_shared(); + object->reserve(v.size()); for (auto it = v.begin(); it != v.end(); ++it) { - (*object)[it.key()] = it.value(); + object->emplace_back(it.key(), Value(it.value())); } object_ = std::move(object); } else if (v.is_array()) { auto array = std::make_shared(); + array->reserve(v.size()); for (const auto& item : v) { array->push_back(Value(item)); } From 844eae8b5d68eed100f2bfde7a4ef2a7c40ce39b Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 02:00:21 +0000 Subject: [PATCH 05/20] drop unused enable_shared_from_this --- include/minja/minja.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 5eedf63..57b138a 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -55,7 +55,7 @@ inline std::string normalize_newlines(const std::string & s) { } /* Values that behave roughly like in Python. */ -class Value : public std::enable_shared_from_this { +class Value { public: using CallableType = std::function &, ArgumentsValue &)>; using FilterType = std::function &, ArgumentsValue &)>; @@ -612,7 +612,7 @@ static std::string error_location_suffix(const std::string & source, size_t pos) return out.str(); } -class Context : public std::enable_shared_from_this { +class Context { protected: Value values_; std::shared_ptr parent_; @@ -852,12 +852,12 @@ struct LoopControlTemplateToken : public TemplateToken { struct CallTemplateToken : public TemplateToken { std::shared_ptr expr; - CallTemplateToken(const Location & loc, SpaceHandling pre, SpaceHandling post, std::shared_ptr && e) + CallTemplateToken(const Location & loc, SpaceHandling pre, SpaceHandling post, std::shared_ptr && e) : TemplateToken(Type::Call, loc, pre, post), expr(std::move(e)) {} }; struct EndCallTemplateToken : public TemplateToken { - EndCallTemplateToken(const Location & loc, SpaceHandling pre, SpaceHandling post) + EndCallTemplateToken(const Location & loc, SpaceHandling pre, SpaceHandling post) : TemplateToken(Type::EndCall, loc, pre, post) {} }; @@ -1084,7 +1084,7 @@ class MacroNode : public TemplateNode { auto & arg = args.args[i]; if (i >= params.size()) throw std::runtime_error("Too many positional arguments for macro " + name->get_name()); param_set[i] = true; - auto & param_name = params[i].first; + const auto & param_name = params[i].first; execution_context->set(param_name, arg); } for (auto & [arg_name, value] : args.kwargs) { From dc245f51724682304dc35deb22bf84e8ddb57cd8 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 02:10:13 +0000 Subject: [PATCH 06/20] Update minja.hpp --- include/minja/minja.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 57b138a..e82ff0b 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -33,6 +33,7 @@ using json = nlohmann::ordered_json; + namespace minja { class Context; From 66412572b6d30ebd94267ab141480dbc6b370a19 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 02:18:01 +0000 Subject: [PATCH 07/20] Update minja.hpp --- include/minja/minja.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index e82ff0b..57b138a 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -33,7 +33,6 @@ using json = nlohmann::ordered_json; - namespace minja { class Context; From bd30364969f1d56a8f797320331f88f0ad51cdc0 Mon Sep 17 00:00:00 2001 From: ochafik Date: Wed, 29 Oct 2025 02:19:03 +0000 Subject: [PATCH 08/20] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fdbbdc4..d3503e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1.2.11 with: - key: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }}-ccache + key: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }} - name: Set up CMake uses: lukka/get-cmake@latest From 3ca32fed1ef5ce329413cfa92b3d4f349f0977eb Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 16:28:04 +0000 Subject: [PATCH 09/20] Support GLM 4.6 template (#5) Fixes https://github.com/ochafik/minja/issues/4 - Fix parsing of values (nested method calls on function calls, e.g. `foo(x).bar(y)`) - Fix tool call capability detection - Tolerate `ensure_ascii` arg in `tojson` with support in Python jinja2 testing harness (supersedes https://github.com/google/minja/pull/84 - thanks @cnaples79 - & https://github.com/google/minja/pull/69 - thanks @rouseabout ), --- .github/workflows/build.yml | 10 +++++++++- CMakeLists.txt | 2 +- include/minja/chat-template.hpp | 4 ++-- include/minja/minja.hpp | 12 +++++------- scripts/fetch_templates_and_goldens.py | 11 ++++++++--- tests/CMakeLists.txt | 1 + tests/test-capabilities.cpp | 12 ++++++++++++ tests/test-syntax.cpp | 2 +- 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3503e2..0119181 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,8 +50,16 @@ jobs: thread, undefined, ] + exclude: + # Sanitizers are not supported on Windows with LLVM targeting MSVC + - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + sanitizer: address + - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + sanitizer: thread + - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + sanitizer: undefined runs-on: ${{ matrix.setup.os }} - name: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }} + name: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }} timeout-minutes: 30 steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index b92ae02..656d469 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,7 @@ set(MINJA_SANITIZERS thread address undefined none) set(MINJA_SANITIZER none CACHE STRING "minja: sanitizer to use") set_property(CACHE MINJA_SANITIZER PROPERTY STRINGS ${MINJA_SANITIZERS}) -if (NOT MSVC AND NOT MINJA_SANITIZER STREQUAL "none") +if (NOT MSVC AND NOT CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" AND NOT MINJA_SANITIZER STREQUAL "none") message(STATUS "Using -fsanitize=${MINJA_SANITIZER}") add_compile_options("-fsanitize=${MINJA_SANITIZER}") link_libraries ("-fsanitize=${MINJA_SANITIZER}") diff --git a/include/minja/chat-template.hpp b/include/minja/chat-template.hpp index d31fb90..f9580df 100644 --- a/include/minja/chat-template.hpp +++ b/include/minja/chat-template.hpp @@ -198,12 +198,12 @@ class chat_template { dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj.dump())})), }), {}, false); - auto tool_call_renders_str_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':"); + auto tool_call_renders_str_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':") || contains(out, ">argument_needle<"); out = try_raw_render(json::array({ dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj)})), }), {}, false); - auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':"); + auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':") || contains(out, ">argument_needle<"); caps_.supports_tool_calls = tool_call_renders_str_arguments || tool_call_renders_obj_arguments; caps_.requires_object_arguments = !tool_call_renders_str_arguments && tool_call_renders_obj_arguments; diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 57b138a..873ece8 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -2205,7 +2205,7 @@ class Parser { auto value = parseValue(); - while (it != end && consumeSpaces() && peekSymbols({ "[", "." })) { + while (it != end && consumeSpaces() && peekSymbols({ "[", ".", "(" })) { if (!consumeToken("[").empty()) { std::shared_ptr index; auto slice_loc = get_location(); @@ -2250,15 +2250,13 @@ class Parser { auto key = std::make_shared(identifier->location, Value(identifier->get_name())); value = std::make_shared(identifier->location, std::move(value), std::move(key)); } + } else if (peekSymbols({ "(" })) { + auto callParams = parseCallArgs(); + value = std::make_shared(get_location(), std::move(value), std::move(callParams)); } consumeSpaces(); } - if (peekSymbols({ "(" })) { - auto location = get_location(); - auto callParams = parseCallArgs(); - value = std::make_shared(location, std::move(value), std::move(callParams)); - } return value; } @@ -2738,7 +2736,7 @@ inline std::shared_ptr Context::builtins() { globals.set("raise_exception", simple_function("raise_exception", { "message" }, [](const std::shared_ptr &, Value & args) -> Value { throw std::runtime_error(args.at("message").get()); })); - globals.set("tojson", simple_function("tojson", { "value", "indent" }, [](const std::shared_ptr &, Value & args) { + globals.set("tojson", simple_function("tojson", { "value", "indent", "ensure_ascii" }, [](const std::shared_ptr &, Value & args) { return Value(args.at("value").dump(args.get("indent", -1), /* to_json= */ true)); })); globals.set("items", simple_function("items", { "object" }, [](const std::shared_ptr &, Value & args) { diff --git a/scripts/fetch_templates_and_goldens.py b/scripts/fetch_templates_and_goldens.py index acaf969..a9656d9 100644 --- a/scripts/fetch_templates_and_goldens.py +++ b/scripts/fetch_templates_and_goldens.py @@ -50,6 +50,8 @@ def strftime_now(format): now = datetime.datetime.strptime(TEST_DATE, "%Y-%m-%d") return now.strftime(format) +def tojson(value, indent=None, ensure_ascii=False, sort_keys=False): + return json.dumps(value, indent=indent, ensure_ascii=ensure_ascii, sort_keys=sort_keys) def join_cmake_path(parent, child): ''' @@ -119,8 +121,11 @@ def __init__(self, template, env=None, filters=None, global_functions=None): env = jinja2.Environment( trim_blocks=True, lstrip_blocks=True, - extensions=[jinja2.ext.loopcontrols] + extensions=[jinja2.ext.loopcontrols], ) + # https://jinja.palletsprojects.com/en/stable/api/#policies + env.policies["json.dumps_function"] = tojson + env.filters['tojson'] = tojson if filters: for name, func in filters.items(): env.filters[name] = func @@ -192,12 +197,12 @@ def make_tool_call(tool_name, arguments): dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", json.dumps(dummy_args_obj))]), ]) - tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out + tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out or ">argument_needle<" in out out = self.try_raw_render([ dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", dummy_args_obj)]), ]) - tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out + tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out or ">argument_needle<" in out caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments caps.requires_object_arguments = not tool_call_renders_str_arguments and tool_call_renders_obj_arguments diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index db82c2d..4a446ac 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -324,6 +324,7 @@ set(MODEL_IDS Qwen/Qwen3-235B-A22B-Thinking-2507 Qwen/Qwen3-Coder-30B-A3B-Instruct Qwen/QwQ-32B + zai-org/GLM-4.6 # Broken, TODO: # ai21labs/AI21-Jamba-1.5-Large # https://github.com/google/minja/issues/8 diff --git a/tests/test-capabilities.cpp b/tests/test-capabilities.cpp index 458f9b9..90b137a 100644 --- a/tests/test-capabilities.cpp +++ b/tests/test-capabilities.cpp @@ -257,3 +257,15 @@ TEST(CapabilitiesTest, CommandRPlusToolUse) { // EXPECT_TRUE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } + +TEST(CapabilitiesTest, GLM46) { + auto caps = get_caps("tests/zai-org-GLM-4.6.jinja"); + EXPECT_TRUE(caps.supports_system_role); + EXPECT_TRUE(caps.supports_tools); + EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_TRUE(caps.supports_tool_responses); + EXPECT_TRUE(caps.supports_parallel_tool_calls); + EXPECT_TRUE(caps.requires_object_arguments); + // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_typed_content); +} diff --git a/tests/test-syntax.cpp b/tests/test-syntax.cpp index 36bdaa3..e445f10 100644 --- a/tests/test-syntax.cpp +++ b/tests/test-syntax.cpp @@ -11,7 +11,6 @@ #include #include -#include #include static std::string render_python(const std::string & template_str, const json & bindings, const minja::Options & options) { @@ -373,6 +372,7 @@ TEST(SyntaxTest, SimpleCases) { {}, {} ) ); + EXPECT_EQ("False", render("{{ trim(' a ').endswith(' ') }}", {} , {})); // Test parsing of expression (chaining of identifier, function call, method call) } EXPECT_EQ( "[0, 1, 2][0, 2]", From 0c55c3673228772d470a378955291d65c24c1450 Mon Sep 17 00:00:00 2001 From: ochafik Date: Sun, 2 Nov 2025 16:43:28 +0000 Subject: [PATCH 10/20] Add missing capabilities tests (tool call id & requires non null content) --- tests/test-capabilities.cpp | 58 ++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/tests/test-capabilities.cpp b/tests/test-capabilities.cpp index 90b137a..5a1d0d0 100644 --- a/tests/test-capabilities.cpp +++ b/tests/test-capabilities.cpp @@ -53,9 +53,11 @@ static minja::chat_template_caps get_caps(const std::string &path) print("supports_system_role", caps.supports_system_role); print("supports_tools", caps.supports_tools); print("supports_tool_calls", caps.supports_tool_calls); + print("supports_tool_call_id", caps.supports_tool_call_id); print("supports_tool_responses", caps.supports_tool_responses); print("supports_parallel_tool_calls", caps.supports_parallel_tool_calls); print("requires_object_arguments", caps.requires_object_arguments); + print("requires_non_null_content", caps.requires_non_null_content); // print("requires_non_null_content", caps.requires_non_null_content); print("requires_typed_content", caps.requires_typed_content); std::cout << "}\n" << std::endl; @@ -68,10 +70,11 @@ TEST(CapabilitiesTest, Gemma7b) { EXPECT_FALSE(caps.supports_system_role); EXPECT_FALSE(caps.supports_tools); EXPECT_FALSE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_FALSE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -80,10 +83,11 @@ TEST(CapabilitiesTest, QwQ32B) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_TRUE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -92,37 +96,39 @@ TEST(CapabilitiesTest, Qwen3Coder) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } #ifndef _WIN32 -TEST(CapabilitiesTest, DeepSeekR1Distill) -{ +TEST(CapabilitiesTest, DeepSeekR1Distill) { auto caps = get_caps("tests/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja"); EXPECT_TRUE(caps.supports_system_role); EXPECT_FALSE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_FALSE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } -#endif +#endif // _WIN32 TEST(CapabilitiesTest, FunctionaryMediumV3_2) { auto caps = get_caps("tests/meetkai-functionary-medium-v3.2.jinja"); EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_FALSE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -131,10 +137,11 @@ TEST(CapabilitiesTest, MetaLlama3_1_8BInstruct) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -143,10 +150,11 @@ TEST(CapabilitiesTest, MetaLlama3_2_3BInstruct) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -155,10 +163,11 @@ TEST(CapabilitiesTest, MetaLlama3_3_70BInstruct) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -167,10 +176,11 @@ TEST(CapabilitiesTest, MiniMaxAIText01) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_FALSE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_FALSE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_FALSE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_TRUE(caps.requires_typed_content); } @@ -179,10 +189,11 @@ TEST(CapabilitiesTest, Mistral7BInstruct) { EXPECT_TRUE(caps.supports_system_role); EXPECT_FALSE(caps.supports_tools); EXPECT_FALSE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_FALSE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -191,10 +202,11 @@ TEST(CapabilitiesTest, MistralNemoInstruct) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_TRUE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -203,10 +215,11 @@ TEST(CapabilitiesTest, NousResearchHermes3Llama3_1_70BToolUse) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -215,10 +228,11 @@ TEST(CapabilitiesTest, NousResearchHermes2ProLlama3_8BToolUse) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -227,10 +241,11 @@ TEST(CapabilitiesTest, CommandRPlusDefault) { EXPECT_TRUE(caps.supports_system_role); EXPECT_FALSE(caps.supports_tools); EXPECT_FALSE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_FALSE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_TRUE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -239,10 +254,11 @@ TEST(CapabilitiesTest, CommandRPlusRag) { EXPECT_TRUE(caps.supports_system_role); EXPECT_FALSE(caps.supports_tools); EXPECT_FALSE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_FALSE(caps.supports_tool_responses); EXPECT_FALSE(caps.supports_parallel_tool_calls); EXPECT_FALSE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_TRUE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -251,10 +267,11 @@ TEST(CapabilitiesTest, CommandRPlusToolUse) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } @@ -263,9 +280,10 @@ TEST(CapabilitiesTest, GLM46) { EXPECT_TRUE(caps.supports_system_role); EXPECT_TRUE(caps.supports_tools); EXPECT_TRUE(caps.supports_tool_calls); + EXPECT_FALSE(caps.supports_tool_call_id); EXPECT_TRUE(caps.supports_tool_responses); EXPECT_TRUE(caps.supports_parallel_tool_calls); EXPECT_TRUE(caps.requires_object_arguments); - // EXPECT_TRUE(caps.requires_non_null_content); + EXPECT_FALSE(caps.requires_non_null_content); EXPECT_FALSE(caps.requires_typed_content); } From 41f90225096193b56c34416c246ee1cbd9905963 Mon Sep 17 00:00:00 2001 From: ochafik Date: Sun, 2 Nov 2025 16:55:36 +0000 Subject: [PATCH 11/20] fix sanitizer exclusion in github workflow matrix --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0119181..b039757 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,12 +51,12 @@ jobs: undefined, ] exclude: - # Sanitizers are not supported on Windows with LLVM targeting MSVC - - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + # Sanitizers not supported on Clang targeting MSVC (llvm-arm64) + - setup: { build: 'llvm-arm64' } sanitizer: address - - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + - setup: { build: 'llvm-arm64' } sanitizer: thread - - setup: { os: windows-latest, build: 'llvm-arm64', defines: '-G "Ninja Multi-Config" -D CMAKE_TOOLCHAIN_FILE=cmake/arm64-windows-llvm.cmake', test: false } + - setup: { build: 'llvm-arm64' } sanitizer: undefined runs-on: ${{ matrix.setup.os }} name: ${{ matrix.setup.os }}-${{ matrix.setup.build }}-${{ matrix.type }}-sanitizer-${{ matrix.sanitizer }} From 48916765b5d3a89eb482293e3cc4748dbd68bee0 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 18:03:53 +0000 Subject: [PATCH 12/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36a3291..dcfbfb0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # minja.hpp - A minimalistic C++ Jinja templating engine for LLM chat templates -_**This is not an official Google product**_ +_**Used to be at https://github.com/google/minja, but I've left Google and I'll only maintain my fork from now on**_ Minja is a minimalistic reimplementation of the [Jinja](https://github.com/pallets/jinja/) templating engine to integrate in/with C++ LLM projects (it's used in [llama.cpp](https://github.com/ggerganov/llama.cpp/pull/11016), [Jan](https://jan.ai/) (through [cortex.cpp](https://github.com/menloresearch/cortex.cpp/pull/1814)), [GPT4All](https://github.com/nomic-ai/gpt4all/pull/3433) and [Docker Model Runner](https://github.com/docker/model-runner)). From 2bd8894b35c0ca8b0736b3fee78dd4f6021efc9d Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Sat, 1 Nov 2025 21:56:47 +0100 Subject: [PATCH 13/20] Support MiniMax tool call format --- include/minja/chat-template.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/minja/chat-template.hpp b/include/minja/chat-template.hpp index f9580df..c482a4b 100644 --- a/include/minja/chat-template.hpp +++ b/include/minja/chat-template.hpp @@ -198,12 +198,14 @@ class chat_template { dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj.dump())})), }), {}, false); - auto tool_call_renders_str_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':") || contains(out, ">argument_needle<"); + auto tool_call_renders_str_arguments = contains(out, "") || contains(out, "\"argument_needle\":") + || contains(out, "'argument_needle':") || contains(out, "") || contains(out, ">argument_needle<"); out = try_raw_render(json::array({ dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj)})), }), {}, false); - auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':") || contains(out, ">argument_needle<"); + auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") + || contains(out, "'argument_needle':") || contains(out, ""); caps_.supports_tool_calls = tool_call_renders_str_arguments || tool_call_renders_obj_arguments; caps_.requires_object_arguments = !tool_call_renders_str_arguments && tool_call_renders_obj_arguments; From 646f5f6208086271cbf28a3856ba503bfa3f9bfc Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Sun, 2 Nov 2025 21:23:12 +0100 Subject: [PATCH 14/20] Add Minimax support to fetch script --- scripts/fetch_templates_and_goldens.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/fetch_templates_and_goldens.py b/scripts/fetch_templates_and_goldens.py index a9656d9..d19814a 100644 --- a/scripts/fetch_templates_and_goldens.py +++ b/scripts/fetch_templates_and_goldens.py @@ -197,12 +197,14 @@ def make_tool_call(tool_name, arguments): dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", json.dumps(dummy_args_obj))]), ]) - tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out or ">argument_needle<" in out + tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out + or "'argument_needle':" in out or ">argument_needle<" in out or "" in out out = self.try_raw_render([ dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", dummy_args_obj)]), ]) - tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out or "'argument_needle':" in out or ">argument_needle<" in out + tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out + or "'argument_needle':" in out or ">argument_needle<" in out or "" in out caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments caps.requires_object_arguments = not tool_call_renders_str_arguments and tool_call_renders_obj_arguments From 748699b54f858a87a820791d12d3ecca2d725f29 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 21:25:59 +0000 Subject: [PATCH 15/20] fix line continuation --- scripts/fetch_templates_and_goldens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/fetch_templates_and_goldens.py b/scripts/fetch_templates_and_goldens.py index d19814a..d451c7c 100644 --- a/scripts/fetch_templates_and_goldens.py +++ b/scripts/fetch_templates_and_goldens.py @@ -197,13 +197,13 @@ def make_tool_call(tool_name, arguments): dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", json.dumps(dummy_args_obj))]), ]) - tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out + tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out \ or "'argument_needle':" in out or ">argument_needle<" in out or "" in out out = self.try_raw_render([ dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", dummy_args_obj)]), ]) - tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out + tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out \ or "'argument_needle':" in out or ">argument_needle<" in out or "" in out caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments From 3d132cad11d4d59aaf29a7f0fc74739e7ff80b13 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin Date: Sun, 2 Nov 2025 22:37:47 +0100 Subject: [PATCH 16/20] Fix minor bugs --- include/minja/chat-template.hpp | 2 +- scripts/fetch_templates_and_goldens.py | 1 - tests/CMakeLists.txt | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/minja/chat-template.hpp b/include/minja/chat-template.hpp index c482a4b..1e8d044 100644 --- a/include/minja/chat-template.hpp +++ b/include/minja/chat-template.hpp @@ -205,7 +205,7 @@ class chat_template { make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj)})), }), {}, false); auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") - || contains(out, "'argument_needle':") || contains(out, ""); + || contains(out, "'argument_needle':") || contains(out, "") || contains(out, ">argument_needle<"); caps_.supports_tool_calls = tool_call_renders_str_arguments || tool_call_renders_obj_arguments; caps_.requires_object_arguments = !tool_call_renders_str_arguments && tool_call_renders_obj_arguments; diff --git a/scripts/fetch_templates_and_goldens.py b/scripts/fetch_templates_and_goldens.py index d451c7c..af840c2 100644 --- a/scripts/fetch_templates_and_goldens.py +++ b/scripts/fetch_templates_and_goldens.py @@ -205,7 +205,6 @@ def make_tool_call(tool_name, arguments): ]) tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out \ or "'argument_needle':" in out or ">argument_needle<" in out or "" in out - caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments caps.requires_object_arguments = not tool_call_renders_str_arguments and tool_call_renders_obj_arguments diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a446ac..4b44d84 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -315,6 +315,7 @@ set(MODEL_IDS unsloth/DeepSeek-R1-Distill-Llama-8B unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit unsloth/Mistral-Small-24B-Instruct-2501-unsloth-bnb-4bit + unsloth/MiniMax-M2 upstage/solar-pro-preview-instruct ValiantLabs/Llama3.1-8B-Enigma xwen-team/Xwen-72B-Chat From bd6f0bd623c887556826b7c9879bf826ec6c3e09 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 22:09:45 +0000 Subject: [PATCH 17/20] fix arg needle testing --- include/minja/chat-template.hpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/include/minja/chat-template.hpp b/include/minja/chat-template.hpp index 1e8d044..95b1fe0 100644 --- a/include/minja/chat-template.hpp +++ b/include/minja/chat-template.hpp @@ -96,10 +96,10 @@ class chat_template { opts.apply_polyfills = false; auto prompt = apply(inputs, opts); - // fprintf(stderr, "try_raw_render: %s\n", prompt.c_str()); + fprintf(stderr, "try_raw_render: %s\n", prompt.c_str()); return prompt; } catch (const std::exception & e) { - // fprintf(stderr, "try_raw_render error: %s\n", e.what()); + fprintf(stderr, "try_raw_render error: %s\n", e.what()); return ""; } } @@ -192,20 +192,25 @@ class chat_template { }; }; const json dummy_args_obj {{"argument_needle", "print('Hello, World!')"}}; + const auto contains_arg_needle = [&](const std::string & out_str) { + return contains(out_str, "") + || contains(out_str, "\"argument_needle\":") + || contains(out_str, "'argument_needle':") + || contains(out_str, ">argument_needle<") + || contains(out_str, ""); + }; // Note: the arguments are rendered in both cases, but may be double-escaped, which we don't want. out = try_raw_render(json::array({ dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj.dump())})), }), {}, false); - auto tool_call_renders_str_arguments = contains(out, "") || contains(out, "\"argument_needle\":") - || contains(out, "'argument_needle':") || contains(out, "") || contains(out, ">argument_needle<"); + auto tool_call_renders_str_arguments = contains_arg_needle(out); out = try_raw_render(json::array({ dummy_user_msg, make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj)})), }), {}, false); - auto tool_call_renders_obj_arguments = contains(out, "") || contains(out, "\"argument_needle\":") - || contains(out, "'argument_needle':") || contains(out, "") || contains(out, ">argument_needle<"); + auto tool_call_renders_obj_arguments = contains_arg_needle(out); caps_.supports_tool_calls = tool_call_renders_str_arguments || tool_call_renders_obj_arguments; caps_.requires_object_arguments = !tool_call_renders_str_arguments && tool_call_renders_obj_arguments; From 338e7725fdb50c6f07c2959dd96fc8ca0711ab68 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 22:10:06 +0000 Subject: [PATCH 18/20] fix arg needle testing --- scripts/fetch_templates_and_goldens.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/fetch_templates_and_goldens.py b/scripts/fetch_templates_and_goldens.py index af840c2..3eb7e17 100644 --- a/scripts/fetch_templates_and_goldens.py +++ b/scripts/fetch_templates_and_goldens.py @@ -192,19 +192,25 @@ def make_tool_call(tool_name, arguments): } dummy_args_obj = {"argument_needle": "print('Hello, World!')"} + contains_arg_needle = lambda out_str: ( + "" in out_str + or '"argument_needle":' in out_str + or "'argument_needle':" in out_str + or ">argument_needle<" in out_str + or "" in out_str + ) out = self.try_raw_render([ dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", json.dumps(dummy_args_obj))]), ]) - tool_call_renders_str_arguments = "" in out or '"argument_needle":' in out \ - or "'argument_needle':" in out or ">argument_needle<" in out or "" in out + tool_call_renders_str_arguments = contains_arg_needle(out) out = self.try_raw_render([ dummy_user_msg, make_tool_calls_msg([make_tool_call("ipython", dummy_args_obj)]), ]) - tool_call_renders_obj_arguments = "" in out or '"argument_needle":' in out \ - or "'argument_needle':" in out or ">argument_needle<" in out or "" in out + tool_call_renders_obj_arguments = contains_arg_needle(out) + caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments caps.requires_object_arguments = not tool_call_renders_str_arguments and tool_call_renders_obj_arguments From 1857778f3da861e4d50e6cf492d1ad313bea224e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 22:15:10 +0000 Subject: [PATCH 19/20] comment debug code --- include/minja/chat-template.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/minja/chat-template.hpp b/include/minja/chat-template.hpp index 95b1fe0..b53e08f 100644 --- a/include/minja/chat-template.hpp +++ b/include/minja/chat-template.hpp @@ -96,10 +96,10 @@ class chat_template { opts.apply_polyfills = false; auto prompt = apply(inputs, opts); - fprintf(stderr, "try_raw_render: %s\n", prompt.c_str()); + // fprintf(stderr, "try_raw_render: %s\n", prompt.c_str()); return prompt; } catch (const std::exception & e) { - fprintf(stderr, "try_raw_render error: %s\n", e.what()); + // fprintf(stderr, "try_raw_render error: %s\n", e.what()); return ""; } } From b44bfcc1f85165a705548f518e9a9873e8e98395 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sun, 2 Nov 2025 23:04:32 +0000 Subject: [PATCH 20/20] comment out unsloth/MiniMax-M2 in test as template buggy --- tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4b44d84..fe5040b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -315,7 +315,6 @@ set(MODEL_IDS unsloth/DeepSeek-R1-Distill-Llama-8B unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit unsloth/Mistral-Small-24B-Instruct-2501-unsloth-bnb-4bit - unsloth/MiniMax-M2 upstage/solar-pro-preview-instruct ValiantLabs/Llama3.1-8B-Enigma xwen-team/Xwen-72B-Chat @@ -336,6 +335,7 @@ set(MODEL_IDS # HuggingFaceTB/SmolVLM-256M-Instruct # HuggingFaceTB/SmolVLM-500M-Instruct # HuggingFaceTB/SmolVLM-Instruct + # unsloth/MiniMax-M2 # https://github.com/ochafik/minja/pull/7#issuecomment-3478459580 # meta-llama/Llama-3.2-11B-Vision-Instruct # unsloth/DeepSeek-R1 )