Skip to content

Commit 746aa39

Browse files
chat: add defensive IBM Granite Jinja compatibility (<tool_call> and <|tool_call|> support)
1 parent 58a6fa4 commit 746aa39

File tree

1 file changed

+65
-14
lines changed

1 file changed

+65
-14
lines changed

common/chat.cpp

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,6 +2324,16 @@ static common_chat_params common_chat_params_init_granite(const common_chat_temp
23242324
data.prompt = apply(tmpl, inputs, /* messages_override= */ std::nullopt, /* tools_override= */ std::nullopt, additional_context);
23252325
data.format = COMMON_CHAT_FORMAT_GRANITE;
23262326

2327+
const auto & src = tmpl.source();
2328+
const bool has_pipe_tool_call = src.find("<|tool_call|>") != std::string::npos;
2329+
const bool has_plain_tool_call = src.find("<tool_call>") != std::string::npos;
2330+
const bool has_plain_tool_call_close = src.find("</tool_call>") != std::string::npos;
2331+
const bool use_plain_tool_call = !has_pipe_tool_call && has_plain_tool_call;
2332+
2333+
const std::string tool_call_tag = use_plain_tool_call ? "<tool_call>" : "<|tool_call|>";
2334+
const std::string tool_call_close_tag =
2335+
use_plain_tool_call && has_plain_tool_call_close ? "</tool_call>" : "";
2336+
23272337
if (string_ends_with(data.prompt, "<think>\n") || string_ends_with(data.prompt, "<think>")) {
23282338
if (!inputs.enable_thinking) {
23292339
data.prompt += "</think>";
@@ -2333,9 +2343,23 @@ static common_chat_params common_chat_params_init_granite(const common_chat_temp
23332343
}
23342344

23352345
if (!inputs.tools.is_null()) {
2336-
// Granite uses <|tool_call|> followed by JSON list
2346+
// Granite uses a sentinel tag followed by a JSON list of tool calls
23372347
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
23382348
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
2349+
const auto quote_literal = [](const std::string & literal) {
2350+
std::string escaped;
2351+
escaped.reserve(literal.size() * 2 + 2);
2352+
escaped.push_back('"');
2353+
for (const char ch : literal) {
2354+
if (ch == '"' || ch == '\\') {
2355+
escaped.push_back('\\');
2356+
}
2357+
escaped.push_back(ch);
2358+
}
2359+
escaped.push_back('"');
2360+
return escaped;
2361+
};
2362+
23392363
std::vector<std::string> tool_rules;
23402364
foreach_function(inputs.tools, [&](const json & tool) {
23412365
const auto & function = tool.at("function");
@@ -2356,24 +2380,37 @@ static common_chat_params common_chat_params_init_granite(const common_chat_temp
23562380
auto tool_call = builder.add_rule("tool_call", string_join(tool_rules, " | "));
23572381
auto tool_list = builder.add_rule("tool_list", "\"[\" space " + tool_call + " (\",\" space " + tool_call + ")* space \"]\"");
23582382

2383+
const auto tool_call_literal = quote_literal(tool_call_tag);
2384+
const auto tool_call_close_literal =
2385+
tool_call_close_tag.empty() ? std::string{} : quote_literal(tool_call_close_tag);
2386+
const auto optional_close_segment = tool_call_close_literal.empty()
2387+
? std::string{}
2388+
: " (space " + tool_call_close_literal + ")?";
2389+
23592390
if (data.thinking_forced_open) {
2360-
builder.add_rule("root", "\"</think>\" space \"<response>\" space [^<]* \"</response>\" space \"<|tool_call|>\" space " + tool_list);
2391+
builder.add_rule(
2392+
"root",
2393+
"\"</think>\" space \"<response>\" space [^<]* \"</response>\" space " +
2394+
tool_call_literal + " space " + tool_list + optional_close_segment);
23612395
} else {
2362-
builder.add_rule("root", "\"<|tool_call|>\" space " + tool_list);
2396+
builder.add_rule("root", tool_call_literal + " space " + tool_list + optional_close_segment);
23632397
}
23642398

23652399
data.grammar_triggers.push_back({
23662400
COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
2367-
"<|tool_call|>"
2401+
tool_call_tag
23682402
});
23692403

23702404
data.preserved_tokens = {
23712405
"<think>",
23722406
"</think>",
23732407
"<response>",
23742408
"</response>",
2375-
"<|tool_call|>",
2409+
tool_call_tag,
23762410
};
2411+
if (!tool_call_close_tag.empty()) {
2412+
data.preserved_tokens.push_back(tool_call_close_tag);
2413+
}
23772414
});
23782415
} else {
23792416
// Handle thinking tags for non-tool responses
@@ -2426,17 +2463,30 @@ static void common_chat_parse_granite(common_chat_msg_parser & builder) {
24262463
}
24272464

24282465
// Look for tool calls
2429-
static const common_regex tool_call_regex(regex_escape("<|tool_call|>"));
2430-
if (auto res = builder.try_find_regex(tool_call_regex)) {
2431-
builder.move_to(res->groups[0].end);
2466+
static const common_regex tool_call_regex_legacy(regex_escape("<|tool_call|>"));
2467+
static const common_regex tool_call_regex_plain(regex_escape("<tool_call>"));
24322468

2433-
// Expect JSON array of tool calls
2434-
if (auto tool_call = builder.try_consume_json_with_dumped_args({{{"arguments"}}})) {
2435-
if (!builder.add_tool_calls(tool_call->value) || tool_call->is_partial) {
2436-
throw common_chat_msg_partial_exception("incomplete tool call");
2469+
const auto try_parse_tool_calls = [&](const common_regex & regex, const std::string & close_tag) {
2470+
if (auto res = builder.try_find_regex(regex)) {
2471+
builder.move_to(res->groups[0].end);
2472+
2473+
// Expect JSON array of tool calls
2474+
if (auto tool_call = builder.try_consume_json_with_dumped_args({{{"arguments"}}})) {
2475+
if (!builder.add_tool_calls(tool_call->value) || tool_call->is_partial) {
2476+
throw common_chat_msg_partial_exception("incomplete tool call");
2477+
}
2478+
builder.consume_spaces();
2479+
if (!close_tag.empty()) {
2480+
builder.try_consume_literal(close_tag);
2481+
}
24372482
}
2483+
return true;
24382484
}
2439-
} else {
2485+
return false;
2486+
};
2487+
2488+
if (!try_parse_tool_calls(tool_call_regex_legacy, "") &&
2489+
!try_parse_tool_calls(tool_call_regex_plain, "</tool_call>")) {
24402490
builder.add_content(builder.consume_rest());
24412491
}
24422492
}
@@ -2719,7 +2769,8 @@ static common_chat_params common_chat_templates_apply_jinja(
27192769
}
27202770

27212771
// Granite (IBM) - detects thinking / tools support
2722-
if (src.find("elif thinking") != std::string::npos && src.find("<|tool_call|>") != std::string::npos) {
2772+
if ((src.find("elif thinking") != std::string::npos || src.find("tools_system_message_prefix") != std::string::npos) &&
2773+
(src.find("<|tool_call|>") != std::string::npos || src.find("<tool_call>") != std::string::npos)) {
27232774
return common_chat_params_init_granite(tmpl, params);
27242775
}
27252776

0 commit comments

Comments
 (0)