@@ -90,41 +90,93 @@ std::string common_chat_msg::render_content(const std::string & delimiter) const
9090 return text;
9191}
9292
93- std::vector<common_chat_msg_span> common_chat_split_by_role (const std::string & prompt, const std::vector<common_chat_msg_delimiter> & delims) {
94- if (delims.empty () || prompt.empty ()) {
95- return {};
93+ common_chat_role common_chat_role_from_string (const std::string & role) {
94+ if (role == " system" ) { return COMMON_CHAT_ROLE_SYSTEM ; }
95+ if (role == " assistant" ) { return COMMON_CHAT_ROLE_ASSISTANT ; }
96+ if (role == " user" ) { return COMMON_CHAT_ROLE_USER ; }
97+ if (role == " tool" ) { return COMMON_CHAT_ROLE_TOOL ; }
98+ return COMMON_CHAT_ROLE_UNKNOWN ;
99+ }
100+
101+ const char * common_chat_role_to_string (common_chat_role role) {
102+ switch (role) {
103+ case COMMON_CHAT_ROLE_SYSTEM : return " system" ;
104+ case COMMON_CHAT_ROLE_ASSISTANT : return " assistant" ;
105+ case COMMON_CHAT_ROLE_USER : return " user" ;
106+ case COMMON_CHAT_ROLE_TOOL : return " tool" ;
107+ case COMMON_CHAT_ROLE_UNKNOWN : return " " ;
96108 }
109+ return " " ;
110+ }
97111
98- auto parser = build_peg_parser ([&](common_peg_parser_builder & p) {
99- std::vector<std::string> all_delims;
100- std::vector<common_peg_parser> tagged_messages;
112+ json common_chat_msg_delimiters::to_json () const {
113+ json result = json::array ();
114+ for (const auto & d : delimiters) {
115+ result.push_back ({
116+ { " role" , common_chat_role_to_string (d.role ) },
117+ { " delimiter" , d.delimiter },
118+ });
119+ }
120+ return result;
121+ }
101122
102- all_delims.reserve (delims.size ());
103- tagged_messages.reserve (delims.size ());
104- for (const auto & d : delims) {
105- all_delims.push_back (d.delimiter );
106- }
123+ common_chat_msg_delimiters common_chat_msg_delimiters_parse (const json & delimiters) {
124+ common_chat_msg_delimiters result;
107125
108- auto any_delim = p.until_one_of (all_delims);
109- for (const auto & d : delims) {
110- tagged_messages.push_back (p.tag (d.role , p.literal (d.delimiter ) + any_delim));
126+ if (!delimiters.is_array ()) {
127+ return result;
128+ }
129+
130+ result.delimiters .reserve (delimiters.size ());
131+ for (const auto & d : delimiters) {
132+ if (!d.is_object ()) {
133+ continue ;
111134 }
135+ result.delimiters .push_back ({
136+ common_chat_role_from_string (d.value (" role" , std::string ())),
137+ d.value (" delimiter" , std::string ()),
138+ });
139+ }
112140
113- return any_delim + p. zero_or_more (p. choice (tagged_messages)) + p. end () ;
114- });
141+ return result ;
142+ }
115143
116- common_peg_parse_context ctx (prompt);
117- const auto result = parser.parse (ctx);
118- if (!result.success ()) {
119- return {};
144+ void common_chat_msg_delimiters::tokenize (const llama_vocab * vocab) {
145+ for (auto & d : delimiters) {
146+ d.tokens = common_tokenize (vocab, d.delimiter , false , true );
120147 }
148+ }
121149
122- std::vector<common_chat_msg_span> spans;
123- ctx.ast .visit (result, [&](const common_peg_ast_node & node) {
124- if (!node.tag .empty ()) {
125- spans.push_back ({ node.tag , node.start , node.end - node.start });
150+ common_chat_msg_spans common_chat_msg_delimiters::split (const llama_tokens & tokens, const std::map<size_t , size_t > & skips) const {
151+ std::vector<std::pair<common_chat_role, size_t >> matches;
152+
153+ auto skip = skips.begin ();
154+ for (size_t i = 0 ; i < tokens.size ();) {
155+ if (skip != skips.end () && i == skip->first ) {
156+ i += skip->second ;
157+ ++skip;
158+ continue ;
126159 }
127- });
160+ for (const auto & d : delimiters) {
161+ if (i + d.tokens .size () > tokens.size ()) {
162+ continue ;
163+ }
164+ if (std::equal (d.tokens .begin (), d.tokens .end (), tokens.begin () + i)) {
165+ matches.emplace_back (d.role , i);
166+ break ;
167+ }
168+ }
169+ i++;
170+ }
171+
172+ matches.emplace_back (COMMON_CHAT_ROLE_UNKNOWN , tokens.size ());
173+
174+ common_chat_msg_spans spans;
175+ for (size_t i = 0 ; i + 1 < matches.size (); i++) {
176+ const auto & curr = matches[i];
177+ const auto & next = matches[i + 1 ];
178+ spans.add (curr.first , curr.second , next.second - curr.second );
179+ }
128180
129181 return spans;
130182}
@@ -1081,13 +1133,13 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp
10811133
10821134 data.prompt = prompt;
10831135 data.generation_prompt = common_chat_template_generation_prompt_impl (tmpl, inputs, /* messages_override= */ adjusted_messages);
1084- data.message_spans = common_chat_split_by_role (prompt, {
1085- { " assistant " , " <|start|>assistant" },
1086- { " user " , " <|start|>user" },
1087- { " system " , " <|start|>developer" },
1088- { " system " , " <|start|>system" },
1089- { " tool " , " <|start|>functions" },
1090- }) ;
1136+ data.message_delimiters = {
1137+ { COMMON_CHAT_ROLE_ASSISTANT , " <|start|>assistant" },
1138+ { COMMON_CHAT_ROLE_USER , " <|start|>user" },
1139+ { COMMON_CHAT_ROLE_SYSTEM , " <|start|>developer" },
1140+ { COMMON_CHAT_ROLE_SYSTEM , " <|start|>system" },
1141+ { COMMON_CHAT_ROLE_TOOL , " <|start|>functions" },
1142+ };
10911143
10921144 data.format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
10931145 data.supports_thinking = true ;
@@ -1228,10 +1280,10 @@ static common_chat_params common_chat_params_init_gemma4(const common_chat_templ
12281280 data.prompt += data.generation_prompt ;
12291281 }
12301282
1231- data.message_spans = common_chat_split_by_role (data. prompt , {
1232- { " user " , " <|turn>user\n " },
1233- { " assistant " , " <|turn>model\n " },
1234- }) ;
1283+ data.message_delimiters = {
1284+ { COMMON_CHAT_ROLE_USER , " <|turn>user" },
1285+ { COMMON_CHAT_ROLE_ASSISTANT , " <|turn>model" },
1286+ };
12351287
12361288 data.format = COMMON_CHAT_FORMAT_PEG_GEMMA4 ;
12371289 data.supports_thinking = true ;
@@ -2030,15 +2082,15 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t
20302082 RESULT_START , RESULT_END ,
20312083 };
20322084
2033- // Split the rendered prompt into per-role message spans . Tool results are rendered with the
2085+ // Declare per-role message delimiters . Tool results are rendered with the
20342086 // system token followed by <|START_TOOL_RESULT|>, so the "tool" delimiter must be listed before
20352087 // the plain "system" one (it is a strict superset, and the role split tries delimiters in order).
2036- data.message_spans = common_chat_split_by_role (data. prompt , {
2037- { " assistant " , GEN_PREFIX },
2038- { " user " , TURN_START + USER },
2039- { " tool " , TURN_START + SYSTEM + RESULT_START },
2040- { " system " , TURN_START + SYSTEM },
2041- }) ;
2088+ data.message_delimiters = {
2089+ { COMMON_CHAT_ROLE_ASSISTANT , GEN_PREFIX },
2090+ { COMMON_CHAT_ROLE_USER , TURN_START + USER },
2091+ { COMMON_CHAT_ROLE_TOOL , TURN_START + SYSTEM + RESULT_START },
2092+ { COMMON_CHAT_ROLE_SYSTEM , TURN_START + SYSTEM },
2093+ };
20422094
20432095 auto has_tools = inputs.tools .is_array () && !inputs.tools .empty ();
20442096 auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE ;
@@ -2526,17 +2578,15 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_
25262578 autoparser.analyze_template (tmpl);
25272579 auto auto_params = autoparser::peg_generator::generate_parser (tmpl, params, autoparser);
25282580
2529- std::vector<common_chat_msg_delimiter> delimiters;
2581+ common_chat_msg_delimiters delimiters;
25302582 if (!autoparser.assistant_start .empty ()) {
2531- delimiters.push_back ({ " assistant " , autoparser.assistant_start } );
2583+ delimiters.add ( COMMON_CHAT_ROLE_ASSISTANT , autoparser.assistant_start );
25322584 }
25332585 if (!autoparser.user_start .empty ()) {
2534- delimiters.push_back ({ " user " , autoparser.user_start } );
2586+ delimiters.add ( COMMON_CHAT_ROLE_USER , autoparser.user_start );
25352587 }
25362588
2537- if (!delimiters.empty ()) {
2538- auto_params.message_spans = common_chat_split_by_role (auto_params.prompt , delimiters);
2539- }
2589+ auto_params.message_delimiters = std::move (delimiters);
25402590
25412591 auto_params.supports_thinking = autoparser.reasoning .mode != autoparser::reasoning_mode::NONE ;
25422592 if (auto_params.supports_thinking ) {
0 commit comments