@@ -169,6 +169,8 @@ common_peg_parser analyze_tools::build_parser(parser_build_context & ctx) const
169169 return build_tool_parser_tag_json (ctx);
170170 case tool_format::TAG_WITH_TAGGED:
171171 return build_tool_parser_tag_tagged (ctx);
172+ case tool_format::TAG_WITH_GEMMA4_DICT:
173+ return build_tool_parser_tag_gemma4_dict (ctx);
172174 default :
173175 LOG_ERR (" [ERROR] Template seems to support tool calls, but failed to determine tool format. Tool calling will not work properly. "
174176 " Check for a fixed template for your model in the models/templates directory of your llama.cpp installation or "
@@ -433,4 +435,113 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
433435 p.end ();
434436}
435437
438+ common_peg_parser analyze_tools::build_tool_parser_tag_gemma4_dict (parser_build_context & ctx) const {
439+ auto & p = ctx.p ;
440+ const auto & inputs = ctx.inputs ;
441+ bool force_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
442+
443+ // The Gemma4 string quote token used in place of JSON "
444+ static const std::string QUOTE = " <|\" |>" ;
445+
446+ common_peg_parser tool_choice = p.choice ();
447+
448+ foreach_function (inputs.tools , [&](const json & tool) {
449+ const auto & func = tool.at (" function" );
450+ std::string name = func.at (" name" );
451+ const auto & params = func.at (" parameters" );
452+
453+ if (!params.contains (" properties" ) || !params.at (" properties" ).is_object ()) {
454+ // No arguments - just match the function name with empty braces
455+ auto func_parser = p.atomic (
456+ p.tool_open (p.literal (function.name_prefix ) + p.tool_name (p.literal (name)) + p.literal (" {" )) +
457+ p.tool_args (p.eps ()) +
458+ p.tool_close (p.literal (" }" )));
459+ tool_choice |= p.rule (" tool-" + name, func_parser);
460+ return ;
461+ }
462+
463+ const auto & properties = params.at (" properties" );
464+ std::set<std::string> required;
465+ if (params.contains (" required" ) && params.at (" required" ).is_array ()) {
466+ params.at (" required" ).get_to (required);
467+ }
468+
469+ // Build per-argument parsers, sorted alphabetically (matching template's dictsort)
470+ struct arg_entry {
471+ std::string param_name;
472+ common_peg_parser parser;
473+ };
474+ std::vector<arg_entry> arg_entries;
475+
476+ for (const auto & [param_name, param_schema] : properties.items ()) {
477+ std::string type = " object" ;
478+ auto type_v = param_schema.contains (" type" ) ? param_schema.at (" type" ) : json::object ();
479+ if (type_v.is_string ()) type_v.get_to (type);
480+
481+ common_peg_parser value_parser = p.eps ();
482+ if (type == " string" ) {
483+ // String values are delimited by <|"|>...<|"|>
484+ value_parser =
485+ p.literal (QUOTE) +
486+ p.tool_arg_string_value (p.schema (p.until (QUOTE),
487+ " tool-" + name + " -arg-" + param_name + " -schema" , param_schema, true )) +
488+ p.literal (QUOTE);
489+ } else {
490+ // Numbers, booleans: raw text up to the next comma or closing brace
491+ value_parser = p.tool_arg_value (p.until_one_of ({" ," , " }" }));
492+ }
493+
494+ auto arg = p.tool_arg (
495+ p.tool_arg_open (p.tool_arg_name (p.literal (param_name)) + p.literal (" :" )) +
496+ value_parser +
497+ p.tool_arg_close (p.eps ()));
498+
499+ arg_entries.push_back ({param_name, p.rule (" tool-" + name + " -arg-" + param_name, arg)});
500+ }
501+
502+ // Sort alphabetically to match Jinja's dictsort
503+ std::sort (arg_entries.begin (), arg_entries.end (), [](const auto & a, const auto & b) {
504+ return a.param_name < b.param_name ;
505+ });
506+
507+ // Build arg sequence: any arg, then zero-or-more comma-separated additional args
508+ common_peg_parser args_seq = p.eps ();
509+ if (!arg_entries.empty ()) {
510+ common_peg_parser any_arg = p.choice ();
511+ for (auto & entry : arg_entries) {
512+ any_arg |= entry.parser ;
513+ }
514+ args_seq = p.optional (
515+ any_arg + p.repeat (p.literal (" ," ) + any_arg, 0 , (int ) arg_entries.size () - 1 ));
516+ }
517+
518+ // Full parser: call:name{args}
519+ auto func_parser = p.atomic (
520+ p.tool_open (p.literal (function.name_prefix ) + p.tool_name (p.literal (name)) + p.literal (" {" )) +
521+ p.tool_args (args_seq) +
522+ p.tool_close (p.literal (" }" )));
523+
524+ tool_choice |= p.rule (" tool-" + name, func_parser);
525+ });
526+
527+ // Wrap each call in <|tool_call>...</tool_call|>
528+ auto wrapped_call = p.literal (format.per_call_start ) + tool_choice + p.literal (format.per_call_end );
529+
530+ common_peg_parser tool_calls = p.eps ();
531+ if (inputs.parallel_tool_calls ) {
532+ tool_calls = p.trigger_rule (" tool-call" , wrapped_call + p.zero_or_more (p.space () + wrapped_call));
533+ } else {
534+ tool_calls = p.trigger_rule (" tool-call" , wrapped_call);
535+ }
536+
537+ if (!force_tools) {
538+ tool_calls = p.optional (tool_calls);
539+ }
540+
541+ auto content_before_tools = p.until (format.per_call_start );
542+ return ctx.reasoning_parser +
543+ (force_tools ? p.eps () : p.optional (p.content (content_before_tools))) +
544+ tool_calls + p.end ();
545+ }
546+
436547} // namespace autoparser
0 commit comments