From 37e7235645f851c4525ed0ca53a4791ce85b8afb Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 15 Aug 2026 13:06:26 +0200 Subject: [PATCH] better supports_string_content cap detect --- common/jinja/caps.cpp | 14 +++++++++---- tests/test-jinja.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index fb97909edf..6e3a1e9b29 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -23,7 +23,7 @@ void caps_apply_preserve_reasoning(jinja::context & ctx, bool enabled) { ctx.set_val("preserve_thinking", mk_val(enabled)); ctx.set_val("clear_thinking", mk_val(!enabled)); ctx.set_val("truncate_history_thinking", mk_val(!enabled)); - ctx.set_val("drop_thinking", mk_val(!enabled)); + ctx.set_val("drop_thinking", mk_val(!enabled)); } void caps_apply_reasoning_effort(jinja::context & ctx, const std::string & effort) { @@ -117,6 +117,8 @@ caps caps_get(jinja::program & prog) { JJ_DEBUG("%s\n", ">>> Running capability check: typed content"); + static const std::string content_marker = "STRING_MARKER"; + // case: typed content support caps_try_execute( prog, @@ -125,22 +127,26 @@ caps caps_get(jinja::program & prog) { return json::array({ { {"role", "user"}, - {"content", "content"} + {"content", content_marker} } }); }, nullptr, // ctx_fn nullptr, // tools_fn - [&](context &, bool success, value & messages, value &, const std::string &) { + [&](context &, bool success, value & messages, value &, const std::string & rendered) { auto & content = messages->at(0)->at("content"); caps_print_stats(content, "messages[0].content"); - if (has_op(content, "selectattr") || has_op(content, "array_access")) { + bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access"); + if (used_as_array) { // accessed as an array result.supports_typed_content = true; } if (!success) { // failed to execute with content as string result.supports_string_content = false; + } else if (used_as_array && rendered.find(content_marker) == std::string::npos) { + // edge case: string may be accessed for checking, but does not appear in the output + result.supports_string_content = false; } } ); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index d410b48b51..9208ff4ac9 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -10,6 +10,7 @@ #include "jinja/parser.h" #include "jinja/lexer.h" #include "jinja/utils.h" +#include "jinja/caps.h" #include "testing.h" @@ -33,6 +34,7 @@ static void test_array_methods(testing & t); static void test_object_methods(testing & t); static void test_hasher(testing & t); static void test_stats(testing & t); +static void test_caps(testing & t); static void test_string_parts(testing & t); static void test_fuzzing(testing & t); @@ -73,6 +75,7 @@ int main(int argc, char *argv[]) { if (!g_python_mode) { t.test("hasher", test_hasher); t.test("stats", test_stats); + t.test("caps", test_caps); t.test("string parts", test_string_parts); t.test("fuzzing", test_fuzzing); } @@ -2059,6 +2062,52 @@ static void test_stats(testing & t) { }); } +static void test_caps(testing & t) { + static auto get_caps = [](const std::string & tmpl) -> jinja::caps { + jinja::lexer lexer; + auto lexer_res = lexer.tokenize(tmpl); + + jinja::program prog = jinja::parse_from_tokens(lexer_res); + + return jinja::caps_get(prog); + }; + + t.test("string content", [](testing & t) { + auto caps = get_caps( + "{% for message in messages %}" + "{{ message['role'] + ': ' + message['content'] }}" + "{% endfor %}" + ); + t.assert_true("supports string content", caps.supports_string_content); + t.assert_true("does not support typed content", !caps.supports_typed_content); + }); + + t.test("typed content, raises on string", [](testing & t) { + // rendering with string content throws: 'selectattr' is not a String filter + auto caps = get_caps( + "{% for message in messages %}" + "{% for content in message['content'] | selectattr('type', 'equalto', 'text') %}" + "{{ content['text'] }}" + "{% endfor %}" + "{% endfor %}" + ); + t.assert_true("does not support string content", !caps.supports_string_content); + t.assert_true("supports typed content", caps.supports_typed_content); + }); + + t.test("typed content, silently drops string", [](testing & t) { + // rendering with string content does not throw, but content[0]['text'] yields + // undefined for a string, so the message is silently dropped (MiniMax-M1 shape) + auto caps = get_caps( + "{% for message in messages %}" + "{{ message['content'][0]['text'] }}" + "{% endfor %}" + ); + t.assert_true("does not support string content", !caps.supports_string_content); + t.assert_true("supports typed content", caps.supports_typed_content); + }); +} + static void test_string_parts(testing & t) { static auto render = [](const std::string & tmpl, const json & vars) -> jinja::string { jinja::lexer lexer;