From 65811e0f1df0c0480c4ab9a77b70e65391fe9cd3 Mon Sep 17 00:00:00 2001 From: Alde Rojas Date: Sun, 15 Mar 2026 20:52:38 -0500 Subject: [PATCH] jinja : add capability check for object args --- common/chat.cpp | 5 +- common/jinja/caps.cpp | 120 +++++++++++++++++++++++++++++++++++++----- common/jinja/caps.h | 2 + 3 files changed, 114 insertions(+), 13 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index cfd5df30a7..056feb9681 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1519,7 +1519,6 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ // map developer to system for all models except for GPT-OSS workaround::map_developer_role_to_system(params.messages); } - workaround::func_args_not_string(params.messages); if (!tmpl.original_caps().supports_system_role) { workaround::system_message_not_supported(params.messages); @@ -1532,6 +1531,10 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ workaround::requires_non_null_content(params.messages); } + if (tmpl.original_caps().supports_object_arguments) { + workaround::func_args_not_string(params.messages); + } + params.extra_context = common_chat_extra_context(); for (auto el : inputs.chat_template_kwargs) { params.extra_context[el.first] = json::parse(el.second); diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 1158d5e5d6..ec207a53e8 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -75,6 +75,7 @@ std::map caps::to_map() const { {"supports_parallel_tool_calls", supports_parallel_tool_calls}, {"supports_system_role", supports_system_role}, {"supports_preserve_reasoning", supports_preserve_reasoning}, + {"supports_object_arguments", supports_object_arguments}, }; } @@ -158,9 +159,9 @@ caps caps_get(jinja::program & prog) { } ); - JJ_DEBUG("%s\n", ">>> Running capability check: single tool support"); + JJ_DEBUG("%s\n", ">>> Running capability check: single tool with object arguments support"); - // case: tools support: single call + // case: tools support: single call with object arguments caps_try_execute( prog, [&]() { @@ -226,9 +227,7 @@ caps caps_get(jinja::program & prog) { }, [&](bool success, value & messages, value & tools) { if (!success) { - result.supports_tool_calls = false; - result.supports_tools = false; - return; + return; // Nothing can be inferred } auto & tool_name = tools->at(0)->at("function")->at("name"); @@ -242,16 +241,117 @@ caps caps_get(jinja::program & prog) { caps_print_stats(tool_calls, "messages[1].tool_calls"); if (!tool_calls->stats.used) { result.supports_tool_calls = false; + return; + } + + auto & tool_arg = tool_calls->at(0)->at("function")->at("arguments")->at("arg"); + caps_print_stats(tool_arg, "messages[1].tool_calls[0].function.arguments.arg"); + if (tool_arg->stats.used) { + result.supports_object_arguments = true; } } ); + if (!result.supports_object_arguments) { + JJ_DEBUG("%s\n", ">>> Running capability check: single tool with string arguments support"); + + // case: tools support: single call with string arguments + caps_try_execute( + prog, + [&]() { + // messages + return json::array({ + { + {"role", "user"}, + {"content", "User message"}, + }, + { + {"role", "assistant"}, + {"content", ""}, // Some templates expect content to be empty with tool calls + {"tool_calls", json::array({ + { + {"id", "call00001"}, + {"type", "function"}, + {"function", { + {"name", "tool1"}, + {"arguments", R"({"arg": "value"})"} + }} + } + })} + }, + { + {"role", "tool"}, + {"content", "Tool response"}, + {"tool_call_id", "call00001"} + }, + { + {"role", "assistant"}, + {"content", "The tool response was 'tool response'"} + }, + { + {"role", "user"}, + {"content", "User message"}, + }, + }); + }, + [&]() { + // tools + return json::array({ + { + {"name", "tool"}, + {"type", "function"}, + {"function", { + {"name", "tool1"}, + {"description", "Tool description"}, + {"parameters", { + {"type", "object"}, + {"properties", { + {"arg", { + {"type", "string"}, + {"description", "Arg description"}, + }}, + }}, + {"required", json::array({ "arg" })}, + }}, + }}, + }, + }); + }, + [&](bool success, value & messages, value & tools) { + if (!success) { + result.supports_tool_calls = false; + result.supports_tools = false; + return; + } + + auto & tool_name = tools->at(0)->at("function")->at("name"); + caps_print_stats(tool_name, "tools[0].function.name"); + caps_print_stats(tools, "tools"); + if (!tool_name->stats.used) { + result.supports_tools = false; + } + + auto & tool_calls = messages->at(1)->at("tool_calls"); + caps_print_stats(tool_calls, "messages[1].tool_calls"); + if (!tool_calls->stats.used) { + result.supports_tool_calls = false; + return; + } + } + ); + } + JJ_DEBUG("%s\n", ">>> Running capability check: parallel tool support"); // case: tools support: parallel calls caps_try_execute( prog, [&]() { + json args = json(R"({"arg": "value"})"); + if (result.supports_object_arguments) { + args = json{{"arg", "value"}}; + } + // messages return json::array({ { @@ -267,9 +367,7 @@ caps caps_get(jinja::program & prog) { {"type", "function"}, {"function", { {"name", "tool1"}, - {"arguments", { - {"arg", "value"} - }} + {"arguments", args} }} }, { @@ -277,9 +375,7 @@ caps caps_get(jinja::program & prog) { {"type", "function"}, {"function", { {"name", "tool1"}, - {"arguments", { - {"arg", "value"} - }} + {"arguments", args} }} } })} @@ -328,7 +424,7 @@ caps caps_get(jinja::program & prog) { return; } - auto & tool_calls = messages->at(1)->at("tool_calls");; + auto & tool_calls = messages->at(1)->at("tool_calls"); caps_print_stats(tool_calls, "messages[1].tool_calls"); // check for second tool call usage diff --git a/common/jinja/caps.h b/common/jinja/caps.h index e694e7bfaa..93a7fe0926 100644 --- a/common/jinja/caps.h +++ b/common/jinja/caps.h @@ -18,6 +18,8 @@ struct caps { bool supports_string_content = true; bool supports_typed_content = false; + bool supports_object_arguments = false; + // for reporting on server std::map to_map() const;