From 7c0c73d4bd90b034780be3e09964487b78e13211 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 7 Feb 2026 18:18:41 +0100 Subject: [PATCH 1/3] chat: fix case where template accepts type content only --- common/chat.cpp | 36 +++++++++++++++++++++++++++++++++++- common/chat.h | 2 ++ common/jinja/caps.cpp | 13 +++++++++---- common/jinja/caps.h | 4 +++- common/jinja/runtime.cpp | 6 ++++++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 2bf46326694..7d050c0190a 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -380,6 +380,7 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa return msgs; } +// DEPRECATED: only used in tests json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text) { json messages = json::array(); for (const auto & msg : msgs) { @@ -3009,6 +3010,39 @@ static void use_generic_schema(json & messages) { } // namespace workaround +static json render_message_to_json(const std::vector & msgs, const jinja::caps & c) { + if (!c.supports_string_content && !c.supports_typed_content) { + LOG_WRN("%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues.\n", __func__); + } + + bool only_string_accepted = c.supports_string_content && !c.supports_typed_content; + bool only_typed_accepted = !c.supports_string_content && c.supports_typed_content; + + json messages = json::array(); + for (const auto & msg : msgs) { + if (only_string_accepted) { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ true); + messages.push_back(jmsg); + } else if (only_typed_accepted) { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); + if (jmsg.at("content").is_string()) { + jmsg["content"] = json::array({ + json{ + {"type", "text"}, + {"text", jmsg.at("content").get()}, + } + }); + } + printf("%s\n", jmsg.dump().c_str()); + messages.push_back(jmsg); + } else { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); + messages.push_back(jmsg); + } + } + return messages; +} + static common_chat_params common_chat_templates_apply_jinja( const struct common_chat_templates * tmpls, const struct common_chat_templates_inputs & inputs) @@ -3020,7 +3054,7 @@ static common_chat_params common_chat_templates_apply_jinja( : *tmpls->template_default; const auto & src = tmpl.source(); const auto & caps = tmpl.original_caps(); - params.messages = common_chat_msgs_to_json_oaicompat(inputs.messages, /* concat_text= */ !tmpl.original_caps().requires_typed_content); + params.messages = render_message_to_json(inputs.messages, tmpl.original_caps()); params.add_generation_prompt = inputs.add_generation_prompt; params.tool_choice = inputs.tool_choice; params.reasoning_format = inputs.reasoning_format; diff --git a/common/chat.h b/common/chat.h index 24aa4aab5cd..1bf43f72617 100644 --- a/common/chat.h +++ b/common/chat.h @@ -240,6 +240,8 @@ bool common_chat_templates_support_enable_thinking(const common_chat_templates * // Parses a JSON array of messages in OpenAI's chat completion API format. std::vector common_chat_msgs_parse_oaicompat(const nlohmann::ordered_json & messages); + +// DEPRECATED: only used in tests nlohmann::ordered_json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text = false); std::vector common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools); diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index f27490f1fb7..dbaaed500a8 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -63,7 +63,8 @@ static void caps_print_stats(value & v, const std::string & path) { std::map caps::to_map() const { return { - {"requires_typed_content", requires_typed_content}, + {"supports_string_content", supports_string_content}, + {"supports_typed_content", supports_typed_content}, {"supports_tools", supports_tools}, {"supports_tool_calls", supports_tool_calls}, {"supports_parallel_tool_calls", supports_parallel_tool_calls}, @@ -89,7 +90,7 @@ caps caps_get(jinja::program & prog) { return v->stats.ops.find(op_name) != v->stats.ops.end(); }; - // case: typed content requirement + // case: typed content support caps_try_execute( prog, [&]() { @@ -105,12 +106,16 @@ caps caps_get(jinja::program & prog) { // tools return json{nullptr}; }, - [&](bool, value & messages, value &) { + [&](bool success, value & messages, value &) { auto & content = messages->at(0)->at("content"); caps_print_stats(content, "messages[0].content"); if (has_op(content, "selectattr") || has_op(content, "array_access")) { // accessed as an array - result.requires_typed_content = true; + result.supports_typed_content = true; + } + if (!success) { + // failed to execute with content as string + result.supports_string_content = false; } } ); diff --git a/common/jinja/caps.h b/common/jinja/caps.h index 77df117baa1..e694e7bfaa5 100644 --- a/common/jinja/caps.h +++ b/common/jinja/caps.h @@ -14,7 +14,9 @@ struct caps { bool supports_parallel_tool_calls = true; bool supports_preserve_reasoning = false; // support assistant message with reasoning_content - bool requires_typed_content = false; // default: use string content + // one of the 2 content capabilities must be true + bool supports_string_content = true; + bool supports_typed_content = false; // for reporting on server std::map to_map() const; diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index 4453d86e6d7..cc012c892fb 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -446,6 +446,12 @@ value for_statement::execute_impl(context & ctx) { value iterable_val = iter_expr->execute(scope); + // mark the variable being iterated as used for stats + if (ctx.is_get_stats) { + iterable_val->stats.used = true; + iterable_val->stats.ops.insert("array_access"); + } + if (iterable_val->is_undefined()) { JJ_DEBUG("%s", "For loop iterable is undefined, skipping loop"); iterable_val = mk_val(); From 0de61b59b597c55cbb21a5a8d168f1f85a688b3a Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Mon, 9 Feb 2026 19:42:39 +0100 Subject: [PATCH 2/3] rm stray log --- common/chat.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/common/chat.cpp b/common/chat.cpp index 7d050c0190a..5c6bbb8a6e4 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -3033,7 +3033,6 @@ static json render_message_to_json(const std::vector & msgs, co } }); } - printf("%s\n", jmsg.dump().c_str()); messages.push_back(jmsg); } else { json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); From 6edb330ffa4090d3e9c6b45d2a263356e12c6cdc Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Mon, 9 Feb 2026 19:49:48 +0100 Subject: [PATCH 3/3] reuse render_message_to_json --- common/chat.cpp | 70 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 5c6bbb8a6e4..47a34d58228 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -380,16 +380,46 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa return msgs; } -// DEPRECATED: only used in tests -json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text) { +static json render_message_to_json(const std::vector & msgs, const jinja::caps & c) { + if (!c.supports_string_content && !c.supports_typed_content) { + LOG_WRN("%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues.\n", __func__); + } + + bool only_string_accepted = c.supports_string_content && !c.supports_typed_content; + bool only_typed_accepted = !c.supports_string_content && c.supports_typed_content; + json messages = json::array(); for (const auto & msg : msgs) { - json jmsg = msg.to_json_oaicompat(concat_typed_text); - messages.push_back(jmsg); + if (only_string_accepted) { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ true); + messages.push_back(jmsg); + } else if (only_typed_accepted) { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); + if (jmsg.at("content").is_string()) { + jmsg["content"] = json::array({ + json{ + {"type", "text"}, + {"text", jmsg.at("content").get()}, + } + }); + } + messages.push_back(jmsg); + } else { + json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); + messages.push_back(jmsg); + } } return messages; } +// DEPRECATED: only used in tests +json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text) { + jinja::caps c; + c.supports_string_content = true; + c.supports_typed_content = !concat_typed_text; + return render_message_to_json(msgs, c); +} + std::vector common_chat_tools_parse_oaicompat(const json & tools) { std::vector result; @@ -3010,38 +3040,6 @@ static void use_generic_schema(json & messages) { } // namespace workaround -static json render_message_to_json(const std::vector & msgs, const jinja::caps & c) { - if (!c.supports_string_content && !c.supports_typed_content) { - LOG_WRN("%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues.\n", __func__); - } - - bool only_string_accepted = c.supports_string_content && !c.supports_typed_content; - bool only_typed_accepted = !c.supports_string_content && c.supports_typed_content; - - json messages = json::array(); - for (const auto & msg : msgs) { - if (only_string_accepted) { - json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ true); - messages.push_back(jmsg); - } else if (only_typed_accepted) { - json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); - if (jmsg.at("content").is_string()) { - jmsg["content"] = json::array({ - json{ - {"type", "text"}, - {"text", jmsg.at("content").get()}, - } - }); - } - messages.push_back(jmsg); - } else { - json jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false); - messages.push_back(jmsg); - } - } - return messages; -} - static common_chat_params common_chat_templates_apply_jinja( const struct common_chat_templates * tmpls, const struct common_chat_templates_inputs & inputs)