chat: fix multi-argument tool calls for tagged templates (#2351)

This commit is contained in:
Joel Farthing 2026-08-25 02:38:39 -05:00 committed by GitHub
parent d180050f89
commit 97370e3f27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 2 deletions

View File

@ -361,7 +361,13 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
auto & p = ctx.p;
const auto & inputs = ctx.inputs;
auto until_suffix = p.rule("until-suffix", p.until(arguments.value_suffix));
// stop at the trimmed closer only before an arg tag, so one inside a value does not match
std::vector<std::string> value_end = { arguments.value_suffix };
auto trimmed_suffix = trim_whitespace(arguments.value_suffix);
if (trimmed_suffix != arguments.value_suffix && !arguments.name_prefix.empty()) {
value_end.push_back(trimmed_suffix + arguments.name_prefix);
}
auto until_suffix = p.rule("until-suffix", p.until_one_of(value_end));
common_peg_parser tool_choice = p.choice();
@ -394,7 +400,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
p.tool_arg_json_value(p.schema(
p.json(), "tool-" + name + "-arg-" + param_name + "-schema", param_schema, false)) +
p.space()) +
p.tool_arg_close(p.literal(arguments.value_suffix)));
p.tool_arg_close(p.optspace(arguments.value_suffix)));
auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg);
if (is_required) {

View File

@ -86,6 +86,7 @@ static void test_normalize_quotes_with_embedded_quotes(testing & t);
// TAG_WITH_TAGGED argument parsing tests
static void test_tagged_args_with_embedded_quotes(testing & t);
static void test_tagged_args_closer_newline_optional(testing & t);
int main(int argc, char * argv[]) {
testing t(std::cout);
@ -111,6 +112,7 @@ int main(int argc, char * argv[]) {
t.test("standard_json_tools", test_standard_json_tools_formats);
t.test("normalize_quotes_to_json", test_normalize_quotes_to_json);
t.test("tagged_args_embedded_quotes", test_tagged_args_with_embedded_quotes);
t.test("tagged_args_closer_newline_optional", test_tagged_args_closer_newline_optional);
return t.summary();
}
@ -2314,3 +2316,60 @@ static void test_cohere2moe_parser(testing & t) {
t.assert_equal("parallel : tool 1 id", std::string("1"), parallel_msg.tool_calls[1].id);
}
}
// the input drops the newline the inferred closer carries
static void test_tagged_args_closer_newline_optional(testing & t) {
struct autoparser a;
a.analysis_complete = true;
a.jinja_caps.supports_tool_calls = true;
a.tools.format.mode = tool_format::TAG_WITH_TAGGED;
a.tools.format.per_call_start = "<tool_call>";
a.tools.format.per_call_end = "</tool_call>";
a.tools.function.name_suffix = "\n";
a.tools.arguments.name_prefix = "<arg_key>";
a.tools.arguments.name_suffix = "</arg_key>\n";
a.tools.arguments.value_prefix = "<arg_value>";
a.tools.arguments.value_suffix = "</arg_value>\n";
a.content.mode = content_mode::PLAIN;
a.reasoning.mode = reasoning_mode::NONE;
generation_params inputs;
inputs.tools = json::parse(R"([{"type":"function","function":{"name":"search","parameters":{
"type":"object","properties":{"file":{"type":"string"},"pattern":{"type":"string"}},
"required":["file","pattern"]}}}])");
inputs.tool_choice = COMMON_CHAT_TOOL_CHOICE_AUTO;
inputs.reasoning_format = COMMON_REASONING_FORMAT_NONE;
inputs.enable_thinking = false;
inputs.parallel_tool_calls = false;
auto arena = a.build_parser(inputs);
common_chat_parser_params pp;
pp.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
pp.reasoning_format = COMMON_REASONING_FORMAT_NONE;
pp.parse_tool_calls = true;
auto check = [&](const char * label, const std::string & gen) {
common_chat_msg msg;
try {
msg = common_chat_peg_parse(arena, gen, /* is_partial = */ false, pp);
} catch (const std::exception & ex) {
t.assert_true(std::string(label) + " : parsed (" + ex.what() + ")", false);
return;
}
if (!t.assert_equal(std::string(label) + " : tool calls", 1u, msg.tool_calls.size())) {
return;
}
const std::string & args = msg.tool_calls[0].arguments;
t.assert_true(std::string(label) + " : no delimiter absorbed", args.find("</arg_value>") == std::string::npos);
t.assert_true(std::string(label) + " : file value", args.find("\"file\":\"app.log\"") != std::string::npos);
t.assert_true(std::string(label) + " : pattern value", args.find("\"pattern\":\"error\"") != std::string::npos);
};
check("closer with newline",
"<tool_call>search\n<arg_key>file</arg_key>\n<arg_value>app.log</arg_value>\n"
"<arg_key>pattern</arg_key>\n<arg_value>error</arg_value>\n</tool_call>");
check("closer without newline",
"<tool_call>search\n<arg_key>file</arg_key>\n<arg_value>app.log</arg_value>"
"<arg_key>pattern</arg_key>\n<arg_value>error</arg_value>\n</tool_call>");
}