char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
acecd5603
char/aliceai-t5-moe
1#include "parsers.h" 2 3namespace workaround { 4 5// Gemma4 uses a custom tool_responses field instead of role:tool messages. 6// 7// This will transform a sequence of messages: 8// assistant(tool_call+) -> tool+ -> assistant(content) 9// 10// Into a single assistant message containing a tool_responses field: 11// assistant(content + tool_call + tool_responses) 12// 13// This is necessary for the Gemma4 chat template to properly format the prompt. 14// See https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4 15struct gemma4_model_turn_builder { 16json & messages ; 17size_t pos ; 18json tool_calls = json::array (); 19json tool_responses = json::array (); 20json content ; 21json reasoning_content ; 22 23gemma4_model_turn_builder (json & msgs ,size_t pos ) :messages (msgs ),pos (pos ) {} 24 25void collect () { 26// Collect the first assistant message 27auto & msg = messages [pos ]; 28if (msg .contains ("reasoning_content" )&& msg .at ("reasoning_content" ).is_string ()) { 29// According to the prompt formatting guide, we need to preserve reasoning_content 30// between function calls. The current chat templates do not support this, but we will do it anyway. 31reasoning_content = msg .at ("reasoning_content" ); 32 } 33for (auto & tc :msg .at ("tool_calls" )) { 34tool_calls .push_back (tc ); 35 } 36pos ++ ; 37 38// Collect tool call results 39while (pos < messages .size ()&& messages [pos ].value ("role" ,"" )== "tool" ) { 40collect_result (messages [pos ]); 41pos ++ ; 42 } 43 44// Check if the next assistant message is the final message 45if (pos < messages .size ()&& messages [pos ].value ("role" ,"" )== "assistant" ) { 46auto & next = messages [pos ]; 47if (!has_tool_calls (next )&& has_content (next )) { 48content = next .at ("content" ); 49pos ++ ; 50 } 51 } 52 } 53 54void collect_result (const json & curr ) { 55json response ; 56if (curr .contains ("content" )) { 57const auto & content = curr .at ("content" ); 58if (content .is_string ()) { 59// Try to parse the content as JSON; fall back to raw string 60try { 61response = json::parse (content .get < std::string > ()); 62 }catch (...) { 63response = content ; 64 } 65 }else { 66response = content ; 67 } 68 } 69 70 std::string name ; 71 72// Match name with corresponding tool call 73size_t idx = tool_responses .size (); 74if (idx < tool_calls .size ()) { 75auto & tc = tool_calls [idx ]; 76if (tc .contains ("function" )) { 77name = tc .at ("function" ).value ("name" ,"" ); 78 } 79 } 80 81// Fallback to the tool call id 82if (name .empty ()) { 83name = curr .value ("tool_call_id" ,"" ); 84 } 85 86tool_responses .push_back ({{"name" ,name }, {"response" ,response }}); 87 } 88 89json build () { 90collect (); 91 92json msg = { 93 {"role" ,"assistant" }, 94 {"tool_calls" ,tool_calls }, 95 }; 96if (!tool_responses .empty ()) { 97msg ["tool_responses" ]= tool_responses ; 98 } 99if (!content .is_null ()) { 100msg ["content" ]= content ; 101 } 102if (!reasoning_content .is_null ()) { 103msg ["reasoning_content" ]= reasoning_content ; 104 } 105return msg ; 106 } 107 108static bool has_content (const json & msg ) { 109if (!msg .contains ("content" )|| msg .at ("content" ).is_null ()) { 110return false; 111 } 112const auto & content = msg .at ("content" ); 113if (content .is_string ()&& !content .get < std::string > ().empty ()) { 114return true; 115 } 116if (content .is_array ()&& !content .empty ()) { 117return true; 118 } 119return false; 120 } 121 122static bool has_tool_calls (const json & msg ) { 123return msg .contains ("tool_calls" )&& msg .at ("tool_calls" ).is_array ()&& !msg .at ("tool_calls" ).empty (); 124 } 125}; 126 127void convert_tool_responses_gemma4 (json & messages ) { 128json result = json::array (); 129size_t i = 0 ; 130 131while (i < messages .size ()) { 132auto & msg = messages [i ]; 133 134if (msg .value ("role" ,"" )!= "assistant" || !msg .contains ("tool_calls" )|| 135 !msg .at ("tool_calls" ).is_array ()|| msg .at ("tool_calls" ).empty ()) { 136result .push_back (msg ); 137i ++ ; 138continue ; 139 } 140 141gemma4_model_turn_builder builder (messages ,i ); 142result .push_back (builder .build ()); 143i = builder .pos ; 144 } 145 146messages = result ; 147} 148 149} 150 151common_chat_params common_chat_params_init_gemma4 (const common_chat_template & tmpl , 152const autoparser::generation_params & inputs ) { 153common_chat_params data ; 154 155data .prompt = common_chat_template_direct_apply_impl (tmpl ,inputs ); 156data .generation_prompt = common_chat_template_generation_prompt_impl (tmpl ,inputs ); 157 158if (inputs .add_generation_prompt && string_ends_with (data .prompt ,"<turn|>\n" )) { 159// This may happen if the model generates content + tool_call, the 160// template does not add the model's next turn and confuses the model 161// from emitting its proper reasoning token sequence. 162data .generation_prompt = "<|turn>model\n" ; 163data .prompt += data .generation_prompt ; 164 } 165 166data .message_delimiters = { 167 {COMMON_CHAT_ROLE_USER ,"<|turn>user" }, 168 {COMMON_CHAT_ROLE_ASSISTANT ,"<|turn>model" }, 169 }; 170 171data .format = COMMON_CHAT_FORMAT_PEG_GEMMA4 ; 172data .supports_thinking = true; 173data .thinking_start_tag = "<|channel>thought" ; 174data .thinking_end_tags = {"<channel|>" }; 175 176data .preserved_tokens = { 177"<|channel>" , 178"<channel|>" , 179"<|tool_call>" , 180"<tool_call|>" , 181"<|turn>" , 182 }; 183 184if (inputs .has_continuation ()) { 185const auto & msg = inputs .continue_msg ; 186 187data .generation_prompt = string_ends_with (data .prompt ,"<turn|>\n" ) ?"<|turn>model\n" :"" ; 188data .generation_prompt += "<|channel>thought\n" + msg .reasoning_content ; 189if (inputs .continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) { 190data .generation_prompt += "<channel|>" + msg .render_content (); 191 } 192 193data .prompt += data .generation_prompt ; 194 } 195 196auto has_tools = inputs .tools .is_array ()&& !inputs .tools .empty (); 197auto has_response_format = !inputs .json_schema .is_null ()&& inputs .json_schema .is_object (); 198auto include_grammar = has_response_format || (has_tools && inputs .tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ); 199auto extract_reasoning = inputs .reasoning_format != COMMON_REASONING_FORMAT_NONE ; 200 201auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 202auto start = p .rule ("start" ,p .optional (p .literal ("<|turn>model\n" ))); 203 204if (extract_reasoning ) { 205p .rule ("thought" ,p .literal ("<|channel>thought" )+ p .space ()+ p .reasoning (p .until ("<channel|>" ))+ p .literal ("<channel|>" )); 206 }else { 207p .rule ("thought" ,p .content (p .literal ("<|channel>thought" )+ p .space ()+ p .until ("<channel|>" )+ p .literal ("<channel|>" ))); 208 } 209 210auto consume_empty_channels = p .gbnf (p .zero_or_more (p .literal ("<|channel>" )+ p .negate (p .literal ("thought" ))),"" ); 211auto thought = (p .peek (p .literal ("<|channel>" ))+ consume_empty_channels + p .ref ("thought" )) |p .negate (p .literal ("<|channel>" )); 212 213if (has_response_format ) { 214auto response_format = p .literal ("```json" ) << 215p .content (p .schema (p .json (),"response-format-schema" ,inputs .json_schema )) << 216p .literal ("```" ); 217return start + p .optional (thought )+ response_format ; 218 } 219 220if (has_tools && inputs .tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) { 221// Gemma4 tool calling syntax 222// Rules should match traversal logic in gemma4_to_json() 223p .rule ("gemma4-string-content" ,p .until ("<|\"|>" )); 224p .rule ("gemma4-string" ,p .literal ("<|\"|>" )+ p .ref ("gemma4-string-content" )+ p .literal ("<|\"|>" )); 225p .rule ("gemma4-bool" ,p .json_bool ()); 226p .rule ("gemma4-null" ,p .json_null ()); 227p .rule ("gemma4-number" ,p .json_number ()); 228p .rule ("gemma4-dict-key" ,p .rule ("gemma4-dict-key-name" ,p .chars ("[^:}]" ,1 ,-1 ))+ p .literal (":" )); 229p .rule ("gemma4-dict-kv" ,p .ref ("gemma4-dict-key" )+ p .space ()+ p .ref ("gemma4-value" )); 230p .rule ("gemma4-dict" , [& ]() { 231auto ws = p .space (); 232auto member = p .ref ("gemma4-dict-kv" ); 233auto members = p .sequence ({member ,p .zero_or_more (p .sequence ({p .literal ("," ),ws ,member }))}); 234return p .sequence ({ 235p .literal ("{" ),ws , 236p .choice ({p .literal ("}" ),p .sequence ({members ,ws ,p .literal ("}" )})}) 237 }); 238 }); 239p .rule ("gemma4-array" , [& ]() { 240auto ws = p .space (); 241auto value = p .ref ("gemma4-value" ); 242auto elements = p .sequence ({value ,p .zero_or_more (p .sequence ({p .literal ("," ),ws ,value }))}); 243return p .sequence ({ 244p .literal ("[" ),ws , 245p .choice ({p .literal ("]" ),p .sequence ({elements ,ws ,p .literal ("]" )})}) 246 }); 247 }); 248p .rule ("gemma4-value" , [& ]() { 249return p .choice ({ 250p .ref ("gemma4-string" ),p .ref ("gemma4-dict" ),p .ref ("gemma4-array" ), 251p .ref ("gemma4-number" ),p .ref ("gemma4-bool" ),p .ref ("gemma4-null" ) 252 }); 253 }); 254 255auto tool_choice = p .choice (); 256 257foreach_function (inputs .tools , [& ](const json & tool ) { 258const auto & function = tool .at ("function" ); 259 std::string name = function .at ("name" ); 260// TODO @aldehir : need to extend json-schema-to-grammar to produce more than JSON rules 261// const auto & params = function.at("parameters"); 262 263tool_choice |=p .rule ("tool-" + name ,p .tool (p .sequence ({ 264p .tool_open (p .tool_name (p .literal (name ))+ p .peek (p .literal ("{" ))), 265p .tool_args (p .ref ("gemma4-dict" )), 266 }))); 267 }); 268 269auto tool_call = p .trigger_rule ("tool-call" ,p .repeat ( 270"<|tool_call>call:" + tool_choice + "<tool_call|>" , 271/* min = */ inputs .tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ?1 :0 , 272/* max = */ inputs .parallel_tool_calls ?-1 :1 273 )); 274 275auto scan_to_toolcall = p .rule ("scan-to-toolcall" ,p .until ("<|tool_call>" )); 276auto content = p .rule ("content" ,p .content (p .until_one_of ({"<|channel>" ,"<channel|>" ,"<|tool_call>" }))); 277auto message = p .rule ("message" ,thought + content ); 278return start + p .zero_or_more (message )+ scan_to_toolcall + tool_call ; 279 } 280 281// Gemma 4 may emit an extra <|channel>thought\n<channel|> at the end of the content. It may 282// also emit a single trailing <channel|> token. Consume all complete reasoning blocks and 283// then stop at the first unmatched <channel|> token. 284auto content = p .rule ("content" ,p .content (p .until_one_of ({"<|channel>" ,"<channel|>" }))); 285auto message = p .rule ("message" ,thought + content ); 286return start + p .one_or_more (message ); 287 }); 288 289data .parser = parser .save (); 290 291if (include_grammar ) { 292data .grammar_lazy = !(has_response_format || (has_tools && inputs .tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED )); 293data .grammar = build_grammar ([& ](const common_grammar_builder & builder ) { 294parser .build_grammar (builder ,data .grammar_lazy ); 295 }); 296 297data .grammar_triggers = { 298 {COMMON_GRAMMAR_TRIGGER_TYPE_WORD ,"<|tool_call>" }, 299 }; 300 } 301 302return data ; 303}