#include "parsers.h" // Harmony with XML tool calls: <|start|>assistant to=NAME<|channel|>analysis <|constrain|>xml<|message|>\nVALUE\n common_chat_params common_chat_params_init_harmony_xml(const common_chat_template & tmpl, const autoparser::generation_params & inputs) { common_chat_params data; data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs); data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs); data.message_delimiters = { { COMMON_CHAT_ROLE_ASSISTANT, "<|start|>assistant" }, { COMMON_CHAT_ROLE_USER, "<|start|>user" }, { COMMON_CHAT_ROLE_SYSTEM, "<|start|>developer" }, { COMMON_CHAT_ROLE_SYSTEM, "<|start|>system" }, }; data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; data.supports_thinking = true; data.thinking_start_tag = "<|channel|>analysis<|message|>"; data.thinking_end_tags = {"<|end|>"}; // EOG text must reach the parser: an XML call has no closing tag of its own static const std::vector EOG = { "<|call|>", "<|return|>", "<|endoftext|>", "<|im_end|>", "<|fim_pad|>", "<|repo_name|>", "<|file_sep|>", }; data.preserved_tokens = { "<|channel|>", "<|constrain|>", "<|message|>", "<|start|>", "<|end|>", }; data.preserved_tokens.insert(data.preserved_tokens.end(), EOG.begin(), EOG.end()); if (inputs.has_continuation()) { const auto & msg = inputs.continue_msg; data.generation_prompt = "<|start|>assistant<|channel|>analysis<|message|>" + msg.reasoning_content; if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) { data.generation_prompt += "<|end|><|start|>assistant<|channel|>final<|message|>" + msg.render_content(); } data.prompt.append(data.generation_prompt); } auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object(); auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE); auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; static const std::string PARAM_OPEN = "assistant")); auto eog = p.choice(); for (const auto & tok : EOG) { eog |= p.literal(tok); } auto with_eog = [&](std::vector delims) { delims.insert(delims.end(), EOG.begin(), EOG.end()); return delims; }; auto analysis_open = p.literal("<|channel|>analysis<|message|>"); auto analysis_body = p.until_one_of(with_eog({ "<|end|>", "<|constrain|><|start|>assistant", "<|start|>assistant" })); auto analysis_close = p.optional(p.literal("<|end|>") | p.literal("<|constrain|>") | eog); if (extract_reasoning) { p.rule("analysis", analysis_open + p.reasoning(analysis_body) + analysis_close); } else { p.rule("analysis", p.atomic(p.content(analysis_open)) + p.content(analysis_body + analysis_close)); } auto analysis = p.ref("analysis"); auto final_msg = p.rule("final", p.literal("<|channel|>final<|message|>") + p.content(p.until_one_of(with_eog({ "<|end|>" }))) + p.optional(p.literal("<|end|>") | eog)); if (has_response_format) { auto constrain_type = p.chars("[A-Za-z0-9_-]", 1, -1); auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type); auto response_format = p.rule("response-format", p.literal("<|channel|>final") + constraint + p.literal("<|message|>") + p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) + p.optional(p.literal("<|end|>") | eog)); return start + p.zero_or_more(analysis + start) + response_format; } if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) { // tolerate a missing closing tag std::vector value_ends = with_eog({ PARAM_OPEN, "<|end|>" }); std::vector string_ends = { PARAM_CLOSE }; auto next_open = p.choice(); for (const auto & e : value_ends) { string_ends.push_back("\n" + e); next_open |= p.literal(e); } auto arg_close = p.tool_arg_close(p.gbnf( p.literal(PARAM_CLOSE) | (p.literal("\n") + p.peek(p.optional(p.literal("\n")) + next_open)), gbnf_format_literal("\n") + " " + gbnf_format_literal(PARAM_CLOSE.substr(1)) + "?")); auto arg_string = p.rule("rfl-arg-string", p.ac(p.tool_arg_string_value(p.until_one_of(string_ends)) + arg_close, PARAM_CLOSE)); auto arg_sep = p.optional(p.literal("\n")); std::string call_close_gbnf = gbnf_format_literal("<|end|>"); for (const auto & tok : EOG) { call_close_gbnf += " | " + gbnf_format_literal(tok); } auto call_close = p.gbnf(p.literal("<|end|>") | eog | p.peek(p.literal("<|start|>assistant")), "(" + call_close_gbnf + ")?"); auto tool_choice = p.choice(); 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 arg_rules; 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(p.literal(PARAM_OPEN) + p.tool_arg_name(p.literal(param.name)) + p.literal("\">\n")); auto types = param.schema->value_types(); auto arg_value = p.eps(); if (!types.has(common_chat_schema::TYPE_STRING) && (types.has(common_chat_schema::TYPE_OBJECT) || types.has(common_chat_schema::TYPE_ARRAY))) { // the schema grammar fixes the key order, and the model writes keys in its own order arg_value = p.tool_arg_json_value(p.json()) + arg_close; } else if (!types.has(common_chat_schema::TYPE_STRING)) { arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, *param.schema)) + arg_close; } else if (types.is_only(common_chat_schema::TYPE_STRING)) { arg_value = arg_string; } else { // grammar keeps only the string rule; the parser tries JSON first to type the value auto json_value = p.choice(); if (types.has(common_chat_schema::TYPE_OBJECT)) { json_value |= p.json_object(); } if (types.has(common_chat_schema::TYPE_ARRAY)) { json_value |= p.json_array(); } if (types.has(common_chat_schema::TYPE_NUMBER) || types.has(common_chat_schema::TYPE_INTEGER)) { json_value |= p.json_number(); } if (types.has(common_chat_schema::TYPE_BOOLEAN)) { json_value |= p.json_bool(); } if (types.has(common_chat_schema::TYPE_NULL)) { json_value |= p.json_null(); } arg_value = p.gbnf(p.atomic(p.tool_arg_json_value(json_value) + arg_close) | arg_string, "rfl-arg-string"); } arg_rules.push_back(p.rule(rule_name, p.tool_arg(arg_open + arg_value))); }); if (arg_rules.empty()) { auto any_name = p.chars("[^\"\\n<]", 1, -1); arg_rules.push_back(p.rule("tool-" + std::to_string(tool_index) + "-arg-any", p.tool_arg(p.tool_arg_open(p.literal(PARAM_OPEN) + p.tool_arg_name(any_name) + p.literal("\">\n")) + arg_string))); } auto args = p.zero_or_more(p.choice(arg_rules) + arg_sep); auto call = p.tool( p.tool_open(p.literal(" to=") + p.tool_name(p.literal(name)) + p.literal("<|channel|>analysis <|constrain|>xml<|message|>")) + p.tool_args(args) + p.tool_close(call_close)); tool_choice |= p.rule("tool-" + std::to_string(tool_index), call); }); auto tool_calls = inputs.parallel_tool_calls ? tool_choice + p.zero_or_more(start + tool_choice) : tool_choice; auto tool_call = p.trigger_rule("tool-call", tool_calls); if (inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED) { return start + p.zero_or_more(analysis + start) + tool_call; } return start + p.zero_or_more(analysis + start) + (tool_call | final_msg | p.content(p.rest())); } auto unsolicited = p.rule("unsolicited", p.atomic(p.literal(" to=") + p.rest())); // output without a channel header is returned as content instead of failing the request return start + p.zero_or_more(analysis + start) + (final_msg | unsolicited | p.content(p.rest())); }); if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { data.parser.build_grammar(builder, data.grammar_lazy); }); if (data.grammar_lazy) { data.grammar_triggers = { { COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "^\\s+to$" }, { COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "<\\|start\\|>assistant(\\s+to)" }, }; } } return data; }