diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index b78789d8c..9e3b6d640 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -291,7 +291,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context common_peg_parser tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & func = tool.at("function"); std::string name = func.at("name"); const auto schema = common_chat_tool_parameters(func); @@ -308,7 +308,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context } have_call_id = true; } - auto args_parser = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)); + auto args_parser = p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", schema)); if (!arguments.start.empty()) { args_parser = p.literal(arguments.start) + args_parser; } @@ -318,7 +318,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context auto atomic_peek = !arguments.start.empty() ? std::optional(p.peek(p.literal(arguments.start))) : std::nullopt; auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_parser, atomic_peek); - tool_choice |= p.rule("tool-" + name, func_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), func_parser); }); auto require_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; @@ -364,14 +364,14 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte common_peg_parser tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & func = tool.at("function"); std::string name = func.at("name"); // Build parser for each argument, separating required and optional std::vector required_parsers; std::vector optional_parsers; - foreach_parameter(func, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + foreach_parameter(func, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { auto arg = p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param.name)) + arguments.name_suffix) + @@ -380,10 +380,10 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte p.ac(p.tool_arg_string_value(until_suffix) + p.tool_arg_close(p.literal(arguments.value_suffix)), arguments.value_suffix) : (p.tool_arg_json_value(p.schema( - p.json(), "tool-" + name + "-arg-" + param.name + "-schema", doc, *param.schema)) + + p.json(), "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index) + "-schema", doc, *param.schema)) + p.tool_arg_close(p.literal(arguments.value_suffix))))); - auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg); + auto named_arg = p.rule("tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index), arg); if (param.required) { required_parsers.push_back(named_arg); } else { @@ -434,7 +434,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte auto atomic_peek = (!arguments.name_prefix.empty() && !required_parsers.empty()) ? std::optional(p.peek(p.literal(arguments.name_prefix))) : std::nullopt; auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_seq, atomic_peek); - tool_choice |= p.rule("tool-" + name, func_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), func_parser); }); auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index ee9a05fe0..74b81dcdd 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -483,7 +483,9 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools( // Build tool choices for tagged format auto tool_choices = choice(); - for (const auto & tool_def : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool_def = tools[i]; + if (!tool_def.contains("function")) { continue; } @@ -513,7 +515,7 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools( auto tool_parser = tool(tool_open(literal(func_opener) + tool_name(literal(name)) + literal(func_name_suffix)) + space() + tool_args(args) + space() + tool_close(literal(func_closer))); - tool_choices |= rule("tool-" + name, tool_parser); + tool_choices |= rule("tool-" + std::to_string(i), tool_parser); } // Build the section with markers @@ -560,7 +562,8 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( auto tool_choices = choice(); - for (const auto & tool_def : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool_def = tools[i]; if (!tool_def.contains("function")) { continue; } @@ -607,7 +610,7 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( space() + tool_args(args) + space() + tool_close(literal(")")) ); - tool_choices |= rule("tool-" + name, tool_parser); + tool_choices |= rule("tool-" + std::to_string(i), tool_parser); } if (parallel_tool_calls) { @@ -635,7 +638,8 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( auto tool_choices = choice(); - for (const auto & tool_def : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool_def = tools[i]; if (!tool_def.contains("function")) { continue; } @@ -668,10 +672,10 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( // Arguments — either wrapped in args_key or parsed directly common_peg_parser args_parser = eps(); if (args_key.empty()) { - args_parser = tool_args(schema(json(), "tool-" + name + "-schema", params)); + args_parser = tool_args(schema(json(), "tool-" + std::to_string(i) + "-schema", params)); } else { args_parser = literal("\"" + effective_args_key + "\"") + space() + literal(":") + space() + - tool_args(schema(json(), "tool-" + name + "-schema", params)); + tool_args(schema(json(), "tool-" + std::to_string(i) + "-schema", params)); } inner_fields.push_back(args_parser); @@ -698,7 +702,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( space() + tool_close(literal("}")) ); - tool_choices |= rule("tool-" + name, tool_parser); + tool_choices |= rule("tool-" + std::to_string(i), tool_parser); } return tool_choices; @@ -721,7 +725,8 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( std::string nested_name_field = !name_spec.first.empty() ? name_spec.second : effective_name_key; std::string nested_args_field = !args_spec.first.empty() ? args_spec.second : effective_args_key; - for (const auto & tool_def : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool_def = tools[i]; if (!tool_def.contains("function")) { continue; } @@ -732,7 +737,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() + atomic(literal("\"") + tool_name(literal(name)) + literal("\"")); auto nested_args = literal("\"" + nested_args_field + "\"") + space() + literal(":") + space() + - tool_args(schema(json(), "tool-" + name + "-schema", params)); + tool_args(schema(json(), "tool-" + std::to_string(i) + "-schema", params)); auto nested_object = literal("{") + space() + nested_name + space() + literal(",") + space() + @@ -770,7 +775,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( auto nested_field = literal("\"" + nested_prefix + "\"") + space() + literal(":") + space() + nested_object; tool_parser_body = tool_parser_body + nested_field + space() + tool_close(literal("}")); - tool_choices |= rule("tool-" + name, tool(tool_parser_body)); + tool_choices |= rule("tool-" + std::to_string(i), tool(tool_parser_body)); } return tool_choices; @@ -790,7 +795,8 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( auto name_key_parser = literal("\"" + effective_name_key + "\""); auto args_key_parser = literal("\"" + effective_args_key + "\""); - for (const auto & tool_def : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool_def = tools[i]; if (!tool_def.contains("function")) { continue; } @@ -801,7 +807,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( auto tool_name_ = name_key_parser + space() + literal(":") + space() + atomic(literal("\"") + tool_name(literal(name)) + literal("\"")); auto tool_args_ = args_key_parser + space() + literal(":") + space() + - tool_args(schema(json(), "tool-" + name + "-schema", params)); + tool_args(schema(json(), "tool-" + std::to_string(i) + "-schema", params)); // Build ID parsers if keys are provided common_peg_parser id_parser = eps(); @@ -861,7 +867,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( } ordered_body = ordered_body + space() + tool_close(literal("}")); - tool_choices |= rule("tool-" + name, tool(ordered_body)); + tool_choices |= rule("tool-" + std::to_string(i), tool(ordered_body)); } return tool_choices; diff --git a/common/parsers/deepseek.cpp b/common/parsers/deepseek.cpp index 9ca4bb34c..88122eaab 100644 --- a/common/parsers/deepseek.cpp +++ b/common/parsers/deepseek.cpp @@ -152,13 +152,13 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ // build tool call section first since we might need it in reasoning auto tool_choice = p.choice(); if (has_tool_calls) { - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); std::vector required_parsers; std::vector optional_parsers; - foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { bool is_string = param.schema->may_be_string(); auto arg = p.tool_arg( @@ -166,11 +166,11 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ p.literal("\" string=\"" + std::string(is_string ? "true" : "false") + "\">")) + (is_string ? p.tool_arg_string_value(p.until(PARAM_END)) : - p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param.name + "-schema", + p.tool_arg_json_value(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index) + "-schema", doc, *param.schema))) + p.tool_arg_close(p.literal(PARAM_END))); - auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg); + auto named_arg = p.rule("tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index), arg); if (param.required) { required_parsers.push_back(named_arg); } else { @@ -199,7 +199,7 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ p.tool_name(p.literal(name)) + p.literal("\">\n")) + invoke_body + p.space() + p.tool_close(p.literal(INVOKE_END))); - tool_choice |= p.rule("tool-" + name, func_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), func_parser); }); } diff --git a/common/parsers/functionary-v3-2.cpp b/common/parsers/functionary-v3-2.cpp index 9d47f0a33..56f7643f2 100644 --- a/common/parsers/functionary-v3-2.cpp +++ b/common/parsers/functionary-v3-2.cpp @@ -42,7 +42,7 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te // Build tool call parsers for each available function auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const auto schema = common_chat_tool_parameters(function); @@ -50,10 +50,10 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te // Tool format: >>>function_name\n{json_args} auto tool_parser = p.tool( p.tool_open(p.tool_name(p.literal(name)) + p.literal("\n")) + - p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)) + p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", schema)) ); - tool_choice |= p.rule("tool-" + name, tool_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_parser); }); auto content_only = content_until_end; diff --git a/common/parsers/gemma4.cpp b/common/parsers/gemma4.cpp index f43c5ae46..2223089da 100644 --- a/common/parsers/gemma4.cpp +++ b/common/parsers/gemma4.cpp @@ -254,13 +254,13 @@ common_chat_params common_chat_params_init_gemma4(const common_chat_template & auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); // TODO @aldehir : need to extend json-schema-to-grammar to produce more than JSON rules // const auto & params = function.at("parameters"); - tool_choice |= p.rule("tool-" + name, p.tool(p.sequence({ + tool_choice |= p.rule("tool-" + std::to_string(tool_index), p.tool(p.sequence({ p.tool_open(p.tool_name(p.literal(name)) + p.peek(p.literal("{"))), p.tool_args(p.ref("gemma4-dict")), }))); diff --git a/common/parsers/gigachat-v3.cpp b/common/parsers/gigachat-v3.cpp index 48abcb3f3..949942547 100644 --- a/common/parsers/gigachat-v3.cpp +++ b/common/parsers/gigachat-v3.cpp @@ -30,17 +30,18 @@ common_chat_params common_chat_params_init_gigachat_v3( if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) { // Build a choice of all available tools auto tool_choice = p.choice(); - for (const auto & tool : inputs.tools) { + for (size_t i = 0; i < inputs.tools.size(); i++) { + const auto & tool = inputs.tools[i]; const auto & function = tool.at("function"); std::string name = function.at("name"); const auto schema = common_chat_tool_parameters(function); auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\""); - auto tool_args = p.json_member("arguments", p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema))); + auto tool_args = p.json_member("arguments", p.tool_args(p.schema(p.json(), "tool-" + std::to_string(i) + "-schema", schema))); auto tool_open = p.tool_open(p.literal("{") << tool_name); - tool_choice |= p.rule("tool-" + name, tool_open << "," << tool_args << "}"); + tool_choice |= p.rule("tool-" + std::to_string(i), tool_open << "," << tool_args << "}"); } // Define the tool call structure diff --git a/common/parsers/gpt-oss.cpp b/common/parsers/gpt-oss.cpp index 00beb41a4..ed50524a9 100644 --- a/common/parsers/gpt-oss.cpp +++ b/common/parsers/gpt-oss.cpp @@ -106,14 +106,14 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) { auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const auto params = common_chat_tool_parameters(function); auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name)); auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type); - auto args = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", params)); + auto args = p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", params)); // recipient in role header // <|start|>assistant to=functions.NAME<|channel|>(commentary|analysis)[constraint]<|message|>ARGS @@ -123,7 +123,7 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & // <|channel|>(commentary|analysis) to=functions.NAME[constraint]<|message|>ARGS auto tool_in_channel = p.tool(p.tool_open(channel + func_name + constraint + p.literal("<|message|>")) + args); - tool_choice |= p.rule("tool-" + name, tool_in_role | tool_in_channel); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_in_role | tool_in_channel); }); auto tool_call = p.trigger_rule("tool-call", tool_choice); diff --git a/common/parsers/k2-horizon.cpp b/common/parsers/k2-horizon.cpp index 687a893ce..bd64f6890 100644 --- a/common/parsers/k2-horizon.cpp +++ b/common/parsers/k2-horizon.cpp @@ -110,14 +110,14 @@ common_chat_params common_chat_params_init_k2_horizon(const common_chat_template // The models leave out even when asked for xml_typed auto arg_type = call_format == "xml_typed" ? p.optional(ARG_TYPE + p.until(ARG_TYPE_END) + ARG_TYPE_END + p.space()) : p.eps(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); std::vector required_args; std::vector optional_args; - foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { - auto rule_name = "tool-" + name + "-arg-" + param.name; + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + auto rule_name = "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index); auto types = param.schema->value_types(); auto arg_value = arg_string; if (!types.has(common_chat_schema::TYPE_STRING)) { @@ -149,12 +149,12 @@ common_chat_params common_chat_params_init_k2_horizon(const common_chat_template (param.required ? required_args : optional_args).push_back(p.rule(rule_name, arg)); }); - auto args = p.permute("tool-" + name + "-args", required_args); + auto args = p.permute("tool-" + std::to_string(tool_index) + "-args", required_args); if (!optional_args.empty()) { args = args + p.zero_or_more(p.choice(optional_args)); } - tool_choice |= p.rule("tool-" + name, p.tool( + tool_choice |= p.rule("tool-" + std::to_string(tool_index), p.tool( p.tool_open(CALL_START + p.tool_name(p.literal(name)) + "\n") + p.tool_args(args) << p.tool_close(p.literal(CALL_END)))); }); } diff --git a/common/parsers/kimi-k2.cpp b/common/parsers/kimi-k2.cpp index 5ee9121ab..9992968a5 100644 --- a/common/parsers/kimi-k2.cpp +++ b/common/parsers/kimi-k2.cpp @@ -79,7 +79,7 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & // The ID format is: functions.: // We need to match: functions.: auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const auto schema = common_chat_tool_parameters(function); @@ -89,11 +89,11 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & auto tool_id = p.tool_id(p.literal("functions.") + p.tool_name(p.literal(name)) + p.literal(":") + p.chars("[0-9]", 1, -1)); auto tool_parser = p.tool( p.tool_open(tool_id + p.literal(ARGS_BEGIN)) + - p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)) + + p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", schema)) + p.tool_close(p.optional((p.literal(CALL_END)))) ); - tool_choice |= p.rule("tool-" + name, tool_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_parser); }); // Tool calls section: <|tool_calls_section_begin|> tool_calls <|tool_calls_section_end|> diff --git a/common/parsers/kimi-k3.cpp b/common/parsers/kimi-k3.cpp index 989e39f95..cf153f6b5 100644 --- a/common/parsers/kimi-k3.cpp +++ b/common/parsers/kimi-k3.cpp @@ -95,7 +95,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & } auto tool_choices = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const json schema = common_chat_tool_parameters(function); @@ -106,6 +106,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & auto args = p.eps(); if (schema.contains("properties") && !schema.at("properties").empty()) { auto arg_choices = p.choice(); + size_t param_index = 0; for (const auto & prop : schema.at("properties").items()) { const std::string & key = prop.key(); @@ -119,7 +120,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & p.tool_arg_value(p.until(ARG_END)); // skip the trailing type="..." attribute: anything up to <|sep|> - arg_choices |= p.rule("kimi-k3-arg-" + name + "-" + key, + arg_choices |= p.rule("kimi-k3-arg-" + std::to_string(tool_index) + "-" + std::to_string(param_index++), p.tool_arg(p.tool_arg_open(p.literal(ARG_START)) + p.tool_arg_name(p.literal(key)) + p.literal("\"") + p.until(SEP) + p.literal(SEP) + value + @@ -133,7 +134,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & p.until(SEP) + p.literal(SEP)) + p.tool_args(args) + p.tool_close(p.literal(CALL_END))); - tool_choices |= p.rule("kimi-k3-tool-" + name, call); + tool_choices |= p.rule("kimi-k3-tool-" + std::to_string(tool_index), call); }); // all calls go inside one tools section, then the message is closed. the diff --git a/common/parsers/ling3.cpp b/common/parsers/ling3.cpp index 8b60e40f4..67d44d47a 100644 --- a/common/parsers/ling3.cpp +++ b/common/parsers/ling3.cpp @@ -118,7 +118,7 @@ common_chat_params common_chat_params_init_ling3(const common_chat_template & auto arg_string = p.rule("ling3-arg-string", p.tool_arg_string_value(p.until(ARG_VAL_END)) + arg_close); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); @@ -127,8 +127,8 @@ common_chat_params common_chat_params_init_ling3(const common_chat_template & // each argument may be preceded by whitespace: the model emits // newlines between arguments, the template history does not - foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { - auto rule_name = "ling3-arg-" + name + "-" + param.name; + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + auto rule_name = "ling3-arg-" + std::to_string(tool_index) + "-" + std::to_string(param_index); auto types = param.schema->value_types(); @@ -159,7 +159,7 @@ common_chat_params common_chat_params_init_ling3(const common_chat_template & // required arguments in any order (as Qwen3-Coder does), then // optional ones in any order and number - auto args = p.permute("ling3-" + name + "-args", required_args); + auto args = p.permute("ling3-" + std::to_string(tool_index) + "-args", required_args); if (!optional_args.empty()) { args = args + p.zero_or_more(p.choice(optional_args)); } @@ -169,7 +169,7 @@ common_chat_params common_chat_params_init_ling3(const common_chat_template & p.tool_args(args) + p.tool_close(p.optional(p.space()) + p.literal(CALL_END))); - tool_choices |= p.rule("ling3-tool-" + name, call); + tool_choices |= p.rule("ling3-tool-" + std::to_string(tool_index), call); }); auto calls = inputs.parallel_tool_calls ? diff --git a/common/parsers/llm-jp-harmony.cpp b/common/parsers/llm-jp-harmony.cpp index b012f9a05..cafb82c12 100644 --- a/common/parsers/llm-jp-harmony.cpp +++ b/common/parsers/llm-jp-harmony.cpp @@ -109,13 +109,13 @@ common_chat_params common_chat_params_init_llm_jp_harmony(const common_chat_temp if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) { auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const auto params = common_chat_tool_parameters(function); auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name)); - auto args = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", params)); + auto args = p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", params)); // recipient in role header // <|start|>assistant to=functions.NAME<|channel|>(commentary|analysis)[constraint]<|message|>ARGS @@ -125,7 +125,7 @@ common_chat_params common_chat_params_init_llm_jp_harmony(const common_chat_temp // <|channel|>(commentary|analysis) to=functions.NAME[constraint]<|message|>ARGS auto tool_in_channel = p.tool(p.tool_open(channel + func_name + constraint + message) + args); - tool_choice |= p.rule("tool-" + name, tool_in_role | tool_in_channel); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_in_role | tool_in_channel); }); // parallel calls are separated by <|end|>; inside the trigger rule so the lazy grammar covers all of them diff --git a/common/parsers/minicpm5.cpp b/common/parsers/minicpm5.cpp index 4d18d3d96..06f4cc8c8 100644 --- a/common/parsers/minicpm5.cpp +++ b/common/parsers/minicpm5.cpp @@ -68,18 +68,18 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template & }); auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); const std::string name = function.at("name"); std::vector arg_rules; - foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { auto value_parser = p.eps(); if (prop.schema->may_be_string()) { value_parser = string_value; } else { value_parser = p.tool_arg_json_value( - p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema) + p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index) + "-schema", doc, *prop.schema) ) + p.tool_arg_close(p.literal("")); } @@ -99,7 +99,7 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template & << p.tool_args(args) << p.tool_close(p.literal(""))); - tool_choice |= p.rule("tool-" + name, tool_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_parser); }); auto max_calls = inputs.parallel_tool_calls ? -1 : 1; diff --git a/common/parsers/minimax-m3.cpp b/common/parsers/minimax-m3.cpp index 7ea9bfe5a..c38ab3e23 100644 --- a/common/parsers/minimax-m3.cpp +++ b/common/parsers/minimax-m3.cpp @@ -85,7 +85,7 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template } auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); auto params = common_chat_tool_parameters(function); @@ -154,8 +154,9 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template members_of = [&](const common_chat_schema_object & object, const std::string & rule_prefix) -> common_peg_parser { std::vector required_elements; std::vector optional_elements; - for (const auto & prop : object.properties) { - auto element = element_of(prop.name, *prop.schema, rule_prefix + "-" + prop.name); + for (size_t i = 0; i < object.properties.size(); i++) { + const auto & prop = object.properties[i]; + auto element = element_of(prop.name, *prop.schema, rule_prefix + "-" + std::to_string(i)); (prop.required ? required_elements : optional_elements).push_back(element); } @@ -180,7 +181,7 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template common_peg_parser invoke_body = p.eps(); if (doc->root->kind() == common_chat_schema::KIND_OBJECT) { - invoke_body = members_of(static_cast(*doc->root), "tool-" + name + "-arg"); + invoke_body = members_of(static_cast(*doc->root), "tool-" + std::to_string(tool_index) + "-arg"); } auto func_parser = p.tool( @@ -189,7 +190,7 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template p.space() + invoke_body + p.space() + p.tool_close(p.literal(INVOKE_END))); - tool_choice |= p.rule("tool-" + name, func_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), func_parser); }); auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; diff --git a/common/parsers/ministral3.cpp b/common/parsers/ministral3.cpp index 8bcd204c6..9cc5248b5 100644 --- a/common/parsers/ministral3.cpp +++ b/common/parsers/ministral3.cpp @@ -86,14 +86,14 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat // Tool call parser if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) { auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); const auto schema = common_chat_tool_parameters(function); tool_choice |= - p.rule("tool-" + name, p.tool_open(p.tool_name(p.literal(name)) + "[ARGS]") + - p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema))); + p.rule("tool-" + std::to_string(tool_index), p.tool_open(p.tool_name(p.literal(name)) + "[ARGS]") + + p.tool_args(p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-schema", schema))); }); auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0; diff --git a/common/parsers/muse-glimmer.cpp b/common/parsers/muse-glimmer.cpp index 784928269..6c7303701 100644 --- a/common/parsers/muse-glimmer.cpp +++ b/common/parsers/muse-glimmer.cpp @@ -81,18 +81,18 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa ""); auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); const std::string name = function.at("name"); std::vector arg_rules; - foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { auto value_parser = p.eps(); if (prop.schema->may_be_string()) { value_parser = string_value; } else { value_parser = p.tool_arg_json_value( - p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema)) + p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index) + "-schema", doc, *prop.schema)) + p.tool_arg_close(p.literal("")); } @@ -113,7 +113,7 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa << p.tool_args(args) << p.tool_close(p.literal("") + p.space() + p.literal(""))); - tool_choice |= p.rule("tool-" + name, tool_parser); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_parser); }); auto tool_calls = inputs.parallel_tool_calls diff --git a/common/parsers/parsers.cpp b/common/parsers/parsers.cpp index 643186c34..2629fb3e9 100644 --- a/common/parsers/parsers.cpp +++ b/common/parsers/parsers.cpp @@ -2,24 +2,25 @@ #include "log.h" -void foreach_function(const json & tools, const std::function & fn) { - for (const auto & tool : tools) { +void foreach_function(const json & tools, const std::function & fn) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool = tools[i]; if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { LOG_INF("Skipping tool without function: %s", tool.dump(2).c_str()); continue; } - fn(tool); + fn(i, tool); } } -void foreach_parameter(const json & function, const std::function & fn) { +void foreach_parameter(const json & function, const std::function & fn) { auto params = common_chat_tool_parameters(function); auto doc = std::make_shared(common_chat_schema_from_json(params)); const auto * object = dynamic_cast(doc->root.get()); if (!object) { return; } - for (const auto & prop : object->properties) { - fn(prop, doc); + for (size_t i = 0; i < object->properties.size(); i++) { + fn(i, object->properties[i], doc); } } diff --git a/common/parsers/parsers.h b/common/parsers/parsers.h index 7786b82f4..1b385264e 100644 --- a/common/parsers/parsers.h +++ b/common/parsers/parsers.h @@ -17,11 +17,11 @@ using json = common_json; -// iterate over the function tools of an OpenAI-style tools array -void foreach_function(const json & tools, const std::function & fn); +// iterate over the function tools of an OpenAI-style tools array, passing each tool with its index in the array +void foreach_function(const json & tools, const std::function & fn); -// iterate over the parameters of a function tool, with the document that owns them -void foreach_parameter(const json & function, const std::function & fn); +// iterate over the parameters of a function tool, passing each parameter with its index and the document that owns it +void foreach_parameter(const json & function, const std::function & fn); // render a template; the override arguments let a parser feed in messages, tools or context it has rewritten std::string common_chat_template_direct_apply_impl( diff --git a/common/parsers/qwen3-coder.cpp b/common/parsers/qwen3-coder.cpp index 208f551f1..323b3da31 100644 --- a/common/parsers/qwen3-coder.cpp +++ b/common/parsers/qwen3-coder.cpp @@ -65,7 +65,7 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat // Match complete opener for Qwen3-Coder models that occasionally omit the // starting . The model may hallucinate a tool name, but it is preferable over // constraining on - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t, const json & tool) { const std::string name = tool.at("function").at("name"); tool_call_starts.push_back(""); }); @@ -93,15 +93,15 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat p.ac(p.tool_arg_string_value(p.until("\n\n")) + arg_close, "\n\n")); auto tool_choice = p.choice(); - foreach_function(inputs.tools, [&](const json & tool) { + foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); std::vector required_args; std::vector optional_args; - foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { - auto rule_name = "tool-" + name + "-arg-" + param.name; + foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + auto rule_name = "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index); auto arg_open = p.tool_arg_open("\n"); @@ -141,7 +141,7 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat // Accept required arguments in any order, as Qwen does not always adhere to the // order provided. - auto args = p.permute("tool-" + name + "-args", required_args); + auto args = p.permute("tool-" + std::to_string(tool_index) + "-args", required_args); if (!optional_args.empty()) { args = args + p.zero_or_more(p.choice(optional_args)); } @@ -150,7 +150,7 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat p.tool_args(args) + p.tool_close(p.literal("\n"))); - tool_choice |= p.rule("tool-" + name, func); + tool_choice |= p.rule("tool-" + std::to_string(tool_index), func); }); auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0; diff --git a/docs/development/parsing.md b/docs/development/parsing.md index 0cb372eca..ab0ccad80 100644 --- a/docs/development/parsing.md +++ b/docs/development/parsing.md @@ -25,15 +25,16 @@ output from a model that emits arguments as JSON. auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { // Build a choice of all available tools auto tool_choice = p.choice(); - for (const auto & tool : tools) { + for (size_t i = 0; i < tools.size(); i++) { + const auto & tool = tools[i]; const auto & function = tool.at("function"); std::string name = function.at("name"); const auto schema = common_chat_tool_parameters(function); auto tool_name = p.json_member("name", "\"" + p.literal(name) + "\""); - auto tool_args = p.json_member("arguments", p.schema(p.json(), "tool-" + name + "-schema", schema)); + auto tool_args = p.json_member("arguments", p.schema(p.json(), "tool-" + std::to_string(i) + "-schema", schema)); - tool_choice |= p.rule("tool-" + name, "{" << tool_name << "," << tool_args << "}"); + tool_choice |= p.rule("tool-" + std::to_string(i), "{" << tool_name << "," << tool_args << "}"); } // Define the tool call structure: [{tool}] diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index e31c093b0..f0aec5bbe 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -1087,6 +1087,7 @@ struct peg_test_case { common_chat_msg expect; bool is_partial = false; bool expect_reconstruction = false; + std::vector expect_rules; }; struct make_peg_parser { @@ -1166,6 +1167,14 @@ static void test_peg_parser(common_chat_templates * tmpls, LOG_DBG("Generation prompt: '%s'\n", parser.params_.generation_prompt.c_str()); } + for (const auto & rule : tc.expect_rules) { + if (!parser.arena_.has_rule(rule)) { + LOG_ERR("Missing rule: %s\n", rule.c_str()); + common_log_flush(common_log_main()); + throw std::runtime_error("Test failed"); + } + } + common_chat_msg msg_accum; common_chat_msg msg_prev; msg_accum.role = msg_prev.role = "assistant"; @@ -1573,6 +1582,11 @@ class peg_test_builder { return *this; } + peg_test_builder & expect_rules(std::vector rules) { + tc_.expect_rules = std::move(rules); + return *this; + } + peg_test_builder & tool_choice(common_chat_tool_choice choice) { tc_.params.tool_choice = choice; return *this; @@ -2174,6 +2188,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ special_function_tool }) .expect(message_assist_call) + .expect_rules({ "tool-0", "tool-0-arg-0" }) .run(); tst.test( @@ -2625,6 +2640,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .enable_thinking(true) .tools({ special_function_tool }) .expect(message_assist_call) + .expect_rules({ "tool-0" }) .run(); tst.test( @@ -3024,6 +3040,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { "<|tool_call>call:get_time{city:<|\"|>London<|\"|>}") .tools({ get_time_tool }) .expect(message_with_tool_calls("get_time", R"({"city": "London"})")) + .expect_rules({ "tool-0" }) .run(); // Tool call with string argument containing special chars @@ -3340,6 +3357,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { "") .tools({ special_function_tool }) .expect(message_assist_call) + .expect_rules({ "tool-0" }) .run(); tst.test( @@ -3624,6 +3642,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { { "special_function", R"({"arg1": 1})", {} }, { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", {} }, }) + .expect_rules({ "tool-0", "tool-0-arg-0", "tool-1", "tool-1-arg-0", "tool-1-arg-1" }) .run(); tst.test( @@ -4035,6 +4054,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) .tools({ get_time_tool }) .expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time")) + .expect_rules({ "tool-0", "tool-0-arg-0" }) .run(); // Tool call without reasoning (non-thinking mode), integer param (string="false") @@ -4673,6 +4693,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ get_time_tool }) .expect_reasoning("I need to check the time first.\n") .expect_tool_calls({ { "get_time", R"({"city": "Paris"})", "" } }) + .expect_rules({ "ling3-tool-0", "ling3-arg-0-0" }) .run(); // Closed think block, prose, then a tool call. @@ -4911,6 +4932,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(message_assist_call_thoughts) .expect_reconstruction() + .expect_rules({ "tool-0", "tool-0-arg-0" }) .run(); tst.test( @@ -5123,6 +5145,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .expect_tool_calls({ { "special_function", R"({"arg1":1})", "" }, }) + .expect_rules({ "kimi-k3-tool-0", "kimi-k3-arg-0-0" }) .run(); // Tool call preceded by reasoning (no opening think tag) and content. @@ -5411,6 +5434,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(kimi_id_special_func_tool_call) .expect_reconstruction() + .expect_rules({ "tool-0" }) .run(); // Kimi-K2-Instruct @@ -5465,6 +5489,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ special_function_tool }) .expect(message_assist_call_thoughts) + .expect_rules({ "tool-0" }) .run(); // Tool call with reasoning and content @@ -5742,6 +5767,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ get_time_tool }) .expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time")) .expect_reconstruction() + .expect_rules({ "tool-0", "tool-0-arg-0" }) .run(); // Tool call without reasoning, integer param @@ -6140,6 +6166,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(message_assist_call_id) .expect_reconstruction() + .expect_rules({ "tool-0" }) .run(); // Continuation tests @@ -6175,6 +6202,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(message_assist_call) .expect_reconstruction() + .expect_rules({ "tool-0" }) .run(); // Continuation tests @@ -6547,7 +6575,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { { // Llama 3.2 auto tst = peg_tester("models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja", detailed_debug); - tst.test("Hello, world!\nWhat's up?").tools({ special_function_tool }).expect(message_assist).expect_reconstruction().run(); + tst.test("Hello, world!\nWhat's up?").tools({ special_function_tool }).expect(message_assist).expect_reconstruction().expect_rules({ "tool-0" }).run(); // Continuation tests tst.test("world!\nWhat's up?") @@ -6595,6 +6623,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .expect(message_assist_call) + .expect_rules({ "tool-0" }) .run(); // "Inform then act": the model answers the user and calls a tool in ONE generation, @@ -6680,6 +6709,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ special_function_tool }) .expect(message_assist_call) + .expect_rules({ "tool-0" }) .run(); // Tool call with recipient in channel header: "<|channel|>analysis to=functions.NAME<|message|>JSON" @@ -6855,6 +6885,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ special_function_tool }) .expect(message_assist_call_thoughts) + .expect_rules({ "tool-0" }) .run(); // Tool call, recipient in channel header @@ -7111,6 +7142,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(message_assist_call) .expect_reconstruction() + .expect_rules({ "tool-0" }) .run(); tst.test( @@ -7139,6 +7171,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .tools({ special_function_tool }) .expect(message_assist_call) .expect_reconstruction() + .expect_rules({ "tool-0" }) .run(); tst.test( @@ -7174,6 +7207,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ python_tool }) .expect_tool_calls({ { "python", R"#({"code": "print('Hello, World!')"})#", {} } }) + .expect_rules({ "tool-0" }) .run(); tst.test(R"()")