char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
acecd5603
char/aliceai-t5-moe
1#include "parsers.h" 2 3// Kimi K3 - XTML tagged format, built by open_tag/close_tag macros: 4// open_tag(t, attrs) = <|open|>t k="v"...<|sep|> close_tag(t) = <|close|>t<|sep|> 5// assistant := [think] [response] [tools] close_tag(message) <|end_of_msg|> 6// the generation prompt already opens the think (or response) section, so the 7// section opener is optional here - same as Kimi K2 Thinking 8common_chat_params common_chat_params_init_kimi_k3 (const common_chat_template & tmpl , 9const autoparser::generation_params & inputs ) { 10common_chat_params data ; 11 12data .prompt = common_chat_template_direct_apply_impl (tmpl ,inputs ); 13data .generation_prompt = common_chat_template_generation_prompt_impl (tmpl ,inputs ); 14data .format = COMMON_CHAT_FORMAT_PEG_NATIVE ; 15data .supports_thinking = true; 16 17const std::string SEP = "<|sep|>" ; 18const std::string MSG_START = "<|open|>message role=\"assistant\"<|sep|>" ; 19const std::string THINK_START = "<|open|>think<|sep|>" ; 20const std::string THINK_END = "<|close|>think<|sep|>" ; 21const std::string RESP_START = "<|open|>response<|sep|>" ; 22const std::string RESP_END = "<|close|>response<|sep|>" ; 23const std::string TOOLS_START = "<|open|>tools<|sep|>" ; 24const std::string TOOLS_END = "<|close|>tools<|sep|>" ; 25const std::string CALL_START = "<|open|>call tool=\"" ; 26const std::string CALL_END = "<|close|>call<|sep|>" ; 27const std::string ARG_START = "<|open|>argument key=\"" ; 28const std::string ARG_END = "<|close|>argument<|sep|>" ; 29const std::string MSG_END = "<|close|>message<|sep|>" ; 30const std::string EOM_TOKEN = "<|end_of_msg|>" ; 31 32// only the markers are special tokens. tag names ("think", "response", ...) are 33// normal tokens and must not be preserved, or prose with those words is broken 34data .preserved_tokens = { 35"<|open|>" , 36"<|close|>" , 37"<|sep|>" , 38"<|end_of_msg|>" , 39 }; 40 41data .thinking_start_tag = THINK_START ; 42data .thinking_end_tags = {THINK_END }; 43 44// per-role message-start delimiters. user/assistant messages only have the role 45// attribute, so the full opener is used. system and tool messages have more 46// attributes, so those delimiters stop after the closing quote of the role 47data .message_delimiters = { 48 {COMMON_CHAT_ROLE_ASSISTANT ,"<|open|>message role=\"assistant\"<|sep|>" }, 49 {COMMON_CHAT_ROLE_USER ,"<|open|>message role=\"user\"<|sep|>" }, 50 {COMMON_CHAT_ROLE_TOOL ,"<|open|>message role=\"tool\"" }, 51 {COMMON_CHAT_ROLE_SYSTEM ,"<|open|>message role=\"system\"" }, 52 }; 53 54auto has_tools = inputs .tools .is_array ()&& !inputs .tools .empty (); 55auto extract_reasoning = inputs .reasoning_format != COMMON_REASONING_FORMAT_NONE ; 56auto include_grammar = has_tools && inputs .tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ; 57 58if (inputs .has_continuation ()) { 59const auto & msg = inputs .continue_msg ; 60 61data .generation_prompt = MSG_START + THINK_START + msg .reasoning_content ; 62if (inputs .continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) { 63data .generation_prompt += THINK_END + RESP_START + msg .render_content (); 64 } 65 66data .prompt += data .generation_prompt ; 67 } 68 69auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 70auto end = p .end (); 71 72auto start = p .optional (p .literal (MSG_START )); 73 74// the think section is always consumed, even with reasoning extraction off: 75// the generation prompt ends with open_tag('think'), so it is always present. 76// reasoning stops at its own closer, or at the response opener if the model 77// skips the closer 78auto think_body = extract_reasoning ?p .reasoning (p .until_one_of ({THINK_END ,RESP_START })) : 79p .content (p .until_one_of ({THINK_END ,RESP_START })); 80 81auto reasoning = p .optional (p .optional (p .literal (THINK_START ))+ think_body + 82p .optional (p .literal (THINK_END ))); 83 84// content runs to the response closer, or to the next section if truncated 85auto response = p .optional (p .literal (RESP_START ))+ 86p .content (p .until_one_of ({RESP_END ,TOOLS_START ,MSG_END }))+ 87p .optional (p .literal (RESP_END )); 88 89// the EOG token after the message closer reaches the parser as text, 90// so it must be consumed or the parse stays incomplete 91auto trailer = p .optional (p .literal (MSG_END ))+ p .optional (p .literal (EOM_TOKEN )); 92 93if (!has_tools || inputs .tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) { 94return start + reasoning + response + trailer + end ; 95 } 96 97auto tool_choices = p .choice (); 98foreach_function (inputs .tools , [& ](const json & tool ) { 99const auto & function = tool .at ("function" ); 100 std::string name = function .at ("name" ); 101const json schema = common_chat_tool_parameters (function ); 102 103// arguments come one tag per key, with the JSON type in a type="..." 104// attribute. the type is taken from the tool schema instead, as it tells 105// us if the value is JSON or a literal string 106auto args = p .eps (); 107if (schema .contains ("properties" )&& !schema .at ("properties" ).empty ()) { 108auto arg_choices = p .choice (); 109for (const auto & prop :schema .at ("properties" ).items ()) { 110const std::string & key = prop .key (); 111 112 std::string type = "string" ; 113if (prop .value ().is_object ()&& prop .value ().contains ("type" )&& 114prop .value ().at ("type" ).is_string ()) { 115type = prop .value ().at ("type" ).get < std::string > (); 116 } 117 118auto value = type == "string" ?p .tool_arg_string_value (p .until (ARG_END )) : 119p .tool_arg_value (p .until (ARG_END )); 120 121// skip the trailing type="..." attribute: anything up to <|sep|> 122arg_choices |=p .rule ("kimi-k3-arg-" + name + "-" + key , 123p .tool_arg (p .tool_arg_open (p .literal (ARG_START ))+ 124p .tool_arg_name (p .literal (key ))+ p .literal ("\"" )+ 125p .until (SEP )+ p .literal (SEP )+ value + 126p .tool_arg_close (p .literal (ARG_END )))); 127 } 128args = p .zero_or_more (arg_choices ); 129 } 130 131// skip the trailing index="N" attribute the same way 132auto call = p .tool (p .tool_open (p .literal (CALL_START )+ p .tool_name (p .literal (name ))+ p .literal ("\"" )+ 133p .until (SEP )+ p .literal (SEP ))+ 134p .tool_args (args )+ p .tool_close (p .literal (CALL_END ))); 135 136tool_choices |=p .rule ("kimi-k3-tool-" + name ,call ); 137 }); 138 139// all calls go inside one tools section, then the message is closed. the 140// message closer is part of the trigger rule, or else the lazy grammar 141// rejects it once tool calls have started 142auto tools_section = 143p .trigger_rule ("kimi-k3-tool-call" ,p .literal (TOOLS_START )+ p .one_or_more (tool_choices )+ 144p .literal (TOOLS_END )+ p .optional (p .literal (MSG_END ))+ 145p .optional (p .literal (EOM_TOKEN ))); 146 147auto tools = inputs .tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ?tools_section : 148p .optional (tools_section ); 149 150return start + reasoning + response + tools + trailer + end ; 151 }); 152 153data .parser = parser .save (); 154 155if (include_grammar ) { 156data .grammar_lazy = inputs .tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED ; 157data .grammar = build_grammar ([& ](const common_grammar_builder & builder ) { 158parser .build_grammar (builder ,data .grammar_lazy ); 159 }); 160 161data .grammar_triggers = { 162 {COMMON_GRAMMAR_TRIGGER_TYPE_WORD ,TOOLS_START }, 163 }; 164 } 165 166return data ; 167}