mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-28 17:56:13 +08:00
server: expose the reasoning effort levels of the chat template (#31271)
* server: expose the reasoning effort levels of the chat template The capability probe renders a user turn for each candidate level (minimal, low, medium, high, xhigh, max) and for an unknown level. A level is honored when it renders without raising and differently from the unknown level, which shows the template fallback. Levels sharing a render are aliases and the highest one is kept. The caps serialize to JSON, and chat_template_caps in /props reports the list as reasoning_effort_levels, from lowest to highest. * jinja: keep the caps free of the json dependency The caps keep reporting their flags as a map, and the effort levels stay a plain member. common_chat_templates_get_caps() builds the JSON the server reports from both, so jinja depends on json only for the probe inputs.
This commit is contained in:
1 parent
a919179942
commit
1b4554eff1
9 files changed
+90
-13
No files matched your search
+7
-6
@@ -1940,12 +1940,13 @@ const common_chat_msg & common_chat_session::finish(const common_chat_input & ch
|
||||
return result;
|
||||
}
|
||||
|
||||
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates) {
|
||||
common_json common_chat_templates_get_caps(const common_chat_templates * chat_templates) {
|
||||
GGML_ASSERT(chat_templates != nullptr);
|
||||
GGML_ASSERT(chat_templates->template_default != nullptr);
|
||||
if (chat_templates->template_tool_use != nullptr) {
|
||||
// take the more expressive template when available
|
||||
return chat_templates->template_tool_use->caps.to_map();
|
||||
}
|
||||
return chat_templates->template_default->caps.to_map();
|
||||
// take the more expressive template when available
|
||||
const auto & caps = chat_templates->template_tool_use != nullptr ? chat_templates->template_tool_use->caps
|
||||
: chat_templates->template_default->caps;
|
||||
common_json result = caps.to_map();
|
||||
result["reasoning_effort_levels"] = caps.reasoning_effort_levels;
|
||||
return result;
|
||||
}
|
||||
+1
-1
@@ -472,7 +472,7 @@ common_json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_to
|
||||
common_json common_chat_tool_parameters(const common_json & function);
|
||||
|
||||
// get template caps, useful for reporting to server /props endpoint
|
||||
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates);
|
||||
common_json common_chat_templates_get_caps(const common_chat_templates * chat_templates);
|
||||
|
||||
common_chat_prompt common_chat_template_direct_apply(
|
||||
const common_chat_template & tmpl,
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// we can remove it in the future when we figure out a better way to define inputs using jinja::value
|
||||
#include "json.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#define FILENAME "jinja-caps"
|
||||
@@ -588,6 +590,58 @@ caps caps_get(jinja::program & prog) {
|
||||
}
|
||||
);
|
||||
|
||||
if (result.supports_reasoning_effort) {
|
||||
JJ_DEBUG("%s\n", ">>> Running capability check: reasoning effort levels");
|
||||
|
||||
static const std::vector<std::string> candidates = { "minimal", "low", "medium", "high", "xhigh", "max" };
|
||||
|
||||
// renders a single user turn with the given effort, nothing when the template raises
|
||||
const auto render = [&](const std::string & effort) {
|
||||
std::optional<std::string> rendered;
|
||||
caps_try_execute(
|
||||
prog,
|
||||
[&]() {
|
||||
// messages
|
||||
return json::array({
|
||||
{
|
||||
{"role", "user"},
|
||||
{"content", "User message"}
|
||||
},
|
||||
});
|
||||
},
|
||||
[&](context & ctx) {
|
||||
ctx.set_val("enable_thinking", mk_val<value_bool>(true));
|
||||
caps_apply_reasoning_effort(ctx, effort);
|
||||
},
|
||||
nullptr, // tools_fn
|
||||
[&](context &, bool success, value &, value &, const std::string & output) {
|
||||
if (success) {
|
||||
rendered = output;
|
||||
}
|
||||
}
|
||||
);
|
||||
return rendered;
|
||||
};
|
||||
|
||||
// an unknown level shows the render of the template fallback, a level that renders the same is not honored
|
||||
// levels sharing a render are aliases and the highest one is kept
|
||||
const auto unknown = render("REASONING_EFFORT_UNKNOWN");
|
||||
std::vector<std::string> renders;
|
||||
for (const auto & level : candidates) {
|
||||
const auto rendered = render(level);
|
||||
if (!rendered || rendered == unknown) {
|
||||
continue;
|
||||
}
|
||||
const auto it = std::find(renders.begin(), renders.end(), *rendered);
|
||||
if (it != renders.end()) {
|
||||
result.reasoning_effort_levels[it - renders.begin()] = level;
|
||||
} else {
|
||||
renders.push_back(*rendered);
|
||||
result.reasoning_effort_levels.push_back(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JJ_DEBUG("%s\n", result.to_string().c_str());
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace jinja {
|
||||
|
||||
@@ -19,6 +20,9 @@ struct caps {
|
||||
// supports reasoning effort levels
|
||||
bool supports_reasoning_effort = false;
|
||||
|
||||
// reasoning effort levels the template renders distinctly, from lowest to highest
|
||||
std::vector<std::string> reasoning_effort_levels;
|
||||
|
||||
// one of the 2 content capabilities must be true
|
||||
bool supports_string_content = true;
|
||||
bool supports_typed_content = false;
|
||||
|
||||
+20
-2
@@ -7738,7 +7738,7 @@ static void test_template_generation_prompt() {
|
||||
preserve_reasoning_inputs.chat_template_kwargs["preserve_reasoning"] = "true";
|
||||
auto preserve_reasoning_params = common_chat_templates_apply(tmpls.get(), preserve_reasoning_inputs);
|
||||
assert_contains(preserve_reasoning_params.prompt.str(), "<|Assistant|><think>Previous reasoning</think>Previous answer");
|
||||
assert_equals(true, common_chat_templates_get_caps(tmpls.get()).at("supports_preserve_reasoning"));
|
||||
assert_equals(true, common_chat_templates_get_caps(tmpls.get()).at("supports_preserve_reasoning").get<bool>());
|
||||
|
||||
auto no_preserve_reasoning_inputs = default_history_inputs;
|
||||
no_preserve_reasoning_inputs.chat_template_kwargs["preserve_reasoning"] = "false";
|
||||
@@ -8311,7 +8311,7 @@ static void test_reasoning_effort_caps() {
|
||||
|
||||
auto assert_supports_effort = [](const std::string & path, bool expected) {
|
||||
auto tmpls = read_templates(path);
|
||||
assert_equals(expected, common_chat_templates_get_caps(tmpls.get()).at("supports_reasoning_effort"));
|
||||
assert_equals(expected, common_chat_templates_get_caps(tmpls.get()).at("supports_reasoning_effort").get<bool>());
|
||||
};
|
||||
|
||||
assert_supports_effort("models/templates/deepseek-ai-DeepSeek-V4.jinja", true);
|
||||
@@ -8322,6 +8322,24 @@ static void test_reasoning_effort_caps() {
|
||||
assert_supports_effort("models/templates/Cohere2MoE.jinja", true);
|
||||
assert_supports_effort("models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja", false);
|
||||
assert_supports_effort("models/templates/Qwen-Qwen3-0.6B.jinja", false);
|
||||
|
||||
auto assert_effort_levels = [](const std::string & path, const std::vector<std::string> & expected) {
|
||||
auto tmpls = read_templates(path);
|
||||
assert_equals(json(expected).dump(), common_chat_templates_get_caps(tmpls.get()).at("reasoning_effort_levels").dump());
|
||||
};
|
||||
|
||||
const std::vector<std::string> all_levels = { "minimal", "low", "medium", "high", "xhigh", "max" };
|
||||
|
||||
assert_effort_levels("models/templates/deepseek-ai-DeepSeek-V4.jinja", { "max" });
|
||||
assert_effort_levels("models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja", { "high", "max" });
|
||||
assert_effort_levels("models/templates/IFM-K2-Horizon.jinja", { "low", "medium", "high" });
|
||||
assert_effort_levels("models/templates/tencent-Hy3.jinja", { "low", "high" });
|
||||
assert_effort_levels("models/templates/muse-spark.jinja", { "minimal", "low", "medium", "high" });
|
||||
assert_effort_levels("models/templates/upstage-Solar-Open-100B.jinja", { "low" });
|
||||
assert_effort_levels("models/templates/muse-glimmer.jinja", all_levels);
|
||||
assert_effort_levels("models/templates/openai-gpt-oss-120b.jinja", all_levels);
|
||||
assert_effort_levels("models/templates/Cohere2MoE.jinja", {});
|
||||
assert_effort_levels("models/templates/Qwen-Qwen3-0.6B.jinja", {});
|
||||
}
|
||||
|
||||
static void test_msg_diffs_compute() {
|
||||
|
||||
@@ -1019,7 +1019,7 @@ By default, it is read-only. To make POST request to change global properties, y
|
||||
- `total_slots` - the total number of slots for process requests (defined by `--parallel` option)
|
||||
- `model_path` - the path to model file (same with `-m` argument)
|
||||
- `chat_template` - the model's original Jinja2 prompt template
|
||||
- `chat_template_caps` - capabilities of the chat template (see `common/jinja/caps.h` for more info)
|
||||
- `chat_template_caps` - capabilities of the chat template (see `common/jinja/caps.h` for more info), `reasoning_effort_levels` lists the `reasoning_effort` values the template renders distinctly, from lowest to highest
|
||||
- `modalities` - the list of supported modalities
|
||||
- `is_sleeping` - sleeping status, see [Sleeping on idle](#sleeping-on-idle)
|
||||
|
||||
|
||||
@@ -1442,7 +1442,7 @@ json oaicompat_chat_params_parse(
|
||||
inputs.json_schema = json_schema.is_null() ? "" : json_schema.dump();
|
||||
inputs.grammar = grammar;
|
||||
inputs.use_jinja = opt.use_jinja;
|
||||
inputs.parallel_tool_calls = json_value(body, "parallel_tool_calls", caps["supports_parallel_tool_calls"]);
|
||||
inputs.parallel_tool_calls = json_value(body, "parallel_tool_calls", caps.at("supports_parallel_tool_calls").get<bool>());
|
||||
inputs.add_generation_prompt = json_value(body, "add_generation_prompt", true);
|
||||
inputs.continue_final_message = body.contains("continue_final_message") ?
|
||||
common_chat_continuation_parse(body.at("continue_final_message")) :
|
||||
|
||||
@@ -1721,7 +1721,7 @@ private:
|
||||
{
|
||||
auto caps = common_chat_templates_get_caps(chat_params.tmpls.get());
|
||||
auto it = params_base.default_template_kwargs.find("preserve_reasoning");
|
||||
bool supported = caps.at("supports_preserve_reasoning");
|
||||
bool supported = caps.at("supports_preserve_reasoning").get<bool>();
|
||||
bool specified = params_base.preserve_reasoning_specified;
|
||||
// note: the kwarg is enabled by default if not specified explicitly, so check the value
|
||||
bool enabled = it != params_base.default_template_kwargs.end() && it->second == "true";
|
||||
|
||||
@@ -33,7 +33,7 @@ struct server_context_meta {
|
||||
|
||||
// chat params
|
||||
server_chat_params & chat_params;
|
||||
std::map<std::string, bool> chat_template_caps;
|
||||
json chat_template_caps;
|
||||
|
||||
// tokens
|
||||
std::string bos_token_str;
|
||||
|
||||
Reference in new issue
Block a user