1use crate::messages::{Message, Role};
2use crate::tools::ToolSchema;
3
4pub trait ChatTemplate: Send + Sync {
10 fn name(&self) -> &str;
12
13 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String;
15
16 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String;
18
19 fn format_message(&self, message: &Message) -> String;
22
23 fn assistant_prefix(&self) -> &str;
25}
26
27fn render_tools(tools: &[ToolSchema]) -> String {
33 if tools.is_empty() {
34 return String::new();
35 }
36 let mut s = String::from("\n\nYou have access to the following tools:\n\n");
37 for tool in tools {
38 s.push_str(&format!(
39 "### {}\n{}\nParameters: {}\n\n",
40 tool.name,
41 tool.description,
42 serde_json::to_string_pretty(&tool.parameters).unwrap_or_default()
43 ));
44 }
45 s.push_str(
46 "To use a tool, respond with a JSON block:\n\
47 ```tool_call\n\
48 {\"name\": \"tool_name\", \"arguments\": {...}}\n\
49 ```\n",
50 );
51 s
52}
53
54fn render_tool_result(message: &Message) -> String {
58 format!("[Tool result]\n{}", message.content)
59}
60
61pub struct ChatMLTemplate;
73
74impl ChatTemplate for ChatMLTemplate {
75 fn name(&self) -> &str {
76 "chatml"
77 }
78
79 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
80 let mut prompt = self.format_system(system_prompt, tools);
81
82 for msg in messages.iter().filter(|m| m.role != Role::System) {
83 prompt.push_str(&self.format_message(msg));
84 }
85
86 prompt.push_str(self.assistant_prefix());
87 prompt
88 }
89
90 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
91 let mut s = String::from("<|im_start|>system\n");
92 s.push_str(system_prompt);
93 s.push_str(&render_tools(tools));
94 s.push_str("<|im_end|>\n");
95 s
96 }
97
98 fn format_message(&self, message: &Message) -> String {
99 let role_str = match message.role {
100 Role::User => "user",
101 Role::Assistant => "assistant",
102 Role::ToolResult => "tool",
103 Role::ToolCall => "assistant",
104 Role::System => return String::new(),
105 };
106 format!("<|im_start|>{role_str}\n{}<|im_end|>\n", message.content)
107 }
108
109 fn assistant_prefix(&self) -> &str {
110 "<|im_start|>assistant\n"
111 }
112}
113
114pub struct Llama3Template;
127
128impl Llama3Template {
129 fn header(&self, role: &str, content: &str) -> String {
130 format!("<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>")
131 }
132}
133
134impl ChatTemplate for Llama3Template {
135 fn name(&self) -> &str {
136 "llama3"
137 }
138
139 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
140 let mut prompt = self.format_system(system_prompt, tools);
141
142 for msg in messages.iter().filter(|m| m.role != Role::System) {
143 prompt.push_str(&self.format_message(msg));
144 }
145
146 prompt.push_str(self.assistant_prefix());
147 prompt
148 }
149
150 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
151 let mut content = String::from(system_prompt);
152 content.push_str(&render_tools(tools));
153 format!("<|begin_of_text|>{}", self.header("system", &content))
154 }
155
156 fn format_message(&self, message: &Message) -> String {
157 match message.role {
158 Role::User => self.header("user", &message.content),
159 Role::Assistant | Role::ToolCall => self.header("assistant", &message.content),
160 Role::ToolResult => self.header("ipython", &message.content),
162 Role::System => String::new(),
163 }
164 }
165
166 fn assistant_prefix(&self) -> &str {
167 "<|start_header_id|>assistant<|end_header_id|>\n\n"
168 }
169}
170
171pub struct MistralTemplate;
186
187impl MistralTemplate {
188 fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
189 let mut s = String::from(system_prompt);
190 s.push_str(&render_tools(tools));
191 s
192 }
193}
194
195impl ChatTemplate for MistralTemplate {
196 fn name(&self) -> &str {
197 "mistral"
198 }
199
200 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
201 let system = self.system_text(system_prompt, tools);
202 let mut out = String::from("<s>");
203 let mut system_pending = !system.is_empty();
204
205 for msg in messages.iter().filter(|m| m.role != Role::System) {
206 match msg.role {
207 Role::User | Role::ToolResult => {
208 let body = if msg.role == Role::ToolResult {
209 render_tool_result(msg)
210 } else {
211 msg.content.clone()
212 };
213 let body = if system_pending {
214 system_pending = false;
215 format!("{system}\n\n{body}")
216 } else {
217 body
218 };
219 out.push_str(&format!("[INST] {body} [/INST]"));
220 }
221 Role::Assistant | Role::ToolCall => {
222 out.push_str(&format!(" {}</s>", msg.content));
223 }
224 Role::System => {}
225 }
226 }
227
228 out
229 }
230
231 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
242 let system = self.system_text(system_prompt, tools);
243 if system.is_empty() {
244 String::from("<s>")
245 } else {
246 format!("<s>[INST] {system}\n\n")
247 }
248 }
249
250 fn format_message(&self, message: &Message) -> String {
251 match message.role {
252 Role::User => format!("[INST] {} [/INST]", message.content),
253 Role::Assistant | Role::ToolCall => format!(" {}</s>", message.content),
254 Role::ToolResult => format!("[INST] {} [/INST]", render_tool_result(message)),
255 Role::System => String::new(),
256 }
257 }
258
259 fn assistant_prefix(&self) -> &str {
260 " "
261 }
262}
263
264pub struct AlpacaTemplate;
278
279const ALPACA_PREAMBLE: &str =
280 "Below is an instruction that describes a task. Write a response that appropriately completes the request.";
281
282impl ChatTemplate for AlpacaTemplate {
283 fn name(&self) -> &str {
284 "alpaca"
285 }
286
287 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
288 let mut prompt = self.format_system(system_prompt, tools);
289
290 for msg in messages.iter().filter(|m| m.role != Role::System) {
291 prompt.push_str(&self.format_message(msg));
292 }
293
294 prompt.push_str(self.assistant_prefix());
295 prompt
296 }
297
298 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
299 let preamble = if system_prompt.is_empty() {
300 ALPACA_PREAMBLE
301 } else {
302 system_prompt
303 };
304 format!("{preamble}{}\n\n", render_tools(tools))
305 }
306
307 fn format_message(&self, message: &Message) -> String {
308 match message.role {
309 Role::User => format!("### Instruction:\n{}\n\n", message.content),
310 Role::Assistant | Role::ToolCall => format!("### Response:\n{}\n\n", message.content),
311 Role::ToolResult => {
312 format!("### Instruction:\n{}\n\n", render_tool_result(message))
313 }
314 Role::System => String::new(),
315 }
316 }
317
318 fn assistant_prefix(&self) -> &str {
319 "### Response:\n"
320 }
321}
322
323pub struct VicunaTemplate;
331
332const VICUNA_PREAMBLE: &str = "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.";
333
334impl ChatTemplate for VicunaTemplate {
335 fn name(&self) -> &str {
336 "vicuna"
337 }
338
339 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
340 let mut prompt = self.format_system(system_prompt, tools);
341
342 for msg in messages.iter().filter(|m| m.role != Role::System) {
343 prompt.push_str(&self.format_message(msg));
344 }
345
346 prompt.push_str(self.assistant_prefix());
347 prompt
348 }
349
350 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
351 let preamble = if system_prompt.is_empty() {
352 VICUNA_PREAMBLE
353 } else {
354 system_prompt
355 };
356 format!("{preamble}{} ", render_tools(tools))
357 }
358
359 fn format_message(&self, message: &Message) -> String {
360 match message.role {
361 Role::User => format!("USER: {} ", message.content),
362 Role::Assistant | Role::ToolCall => format!("ASSISTANT: {}</s>", message.content),
363 Role::ToolResult => format!("USER: {} ", render_tool_result(message)),
364 Role::System => String::new(),
365 }
366 }
367
368 fn assistant_prefix(&self) -> &str {
369 "ASSISTANT: "
370 }
371}
372
373pub struct GemmaTemplate;
391
392impl GemmaTemplate {
393 fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
394 let mut s = String::from(system_prompt);
395 s.push_str(&render_tools(tools));
396 s
397 }
398}
399
400impl ChatTemplate for GemmaTemplate {
401 fn name(&self) -> &str {
402 "gemma"
403 }
404
405 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
406 let system = self.system_text(system_prompt, tools);
407 let mut out = String::from("<bos>");
408 let mut system_pending = !system.is_empty();
409
410 for msg in messages.iter().filter(|m| m.role != Role::System) {
411 match msg.role {
412 Role::User | Role::ToolResult => {
413 let body = if msg.role == Role::ToolResult {
414 render_tool_result(msg)
415 } else {
416 msg.content.clone()
417 };
418 let body = if system_pending {
419 system_pending = false;
420 format!("{system}\n\n{body}")
421 } else {
422 body
423 };
424 out.push_str(&format!("<start_of_turn>user\n{body}<end_of_turn>\n"));
425 }
426 Role::Assistant | Role::ToolCall => {
427 out.push_str(&format!(
428 "<start_of_turn>model\n{}<end_of_turn>\n",
429 msg.content
430 ));
431 }
432 Role::System => {}
433 }
434 }
435
436 out.push_str(self.assistant_prefix());
437 out
438 }
439
440 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
441 let system = self.system_text(system_prompt, tools);
442 if system.is_empty() {
443 String::from("<bos>")
444 } else {
445 format!("<bos><start_of_turn>user\n{system}\n\n")
446 }
447 }
448
449 fn format_message(&self, message: &Message) -> String {
450 match message.role {
451 Role::User => format!("<start_of_turn>user\n{}<end_of_turn>\n", message.content),
452 Role::Assistant | Role::ToolCall => {
453 format!("<start_of_turn>model\n{}<end_of_turn>\n", message.content)
454 }
455 Role::ToolResult => format!(
456 "<start_of_turn>user\n{}<end_of_turn>\n",
457 render_tool_result(message)
458 ),
459 Role::System => String::new(),
460 }
461 }
462
463 fn assistant_prefix(&self) -> &str {
464 "<start_of_turn>model\n"
465 }
466}
467
468pub struct Phi3Template;
485
486impl ChatTemplate for Phi3Template {
487 fn name(&self) -> &str {
488 "phi3"
489 }
490
491 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
492 let mut prompt = self.format_system(system_prompt, tools);
493
494 for msg in messages.iter().filter(|m| m.role != Role::System) {
495 prompt.push_str(&self.format_message(msg));
496 }
497
498 prompt.push_str(self.assistant_prefix());
499 prompt
500 }
501
502 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
503 let tools_block = render_tools(tools);
504 if system_prompt.is_empty() && tools_block.is_empty() {
505 return String::new();
506 }
507 format!("<|system|>\n{system_prompt}{tools_block}<|end|>\n")
508 }
509
510 fn format_message(&self, message: &Message) -> String {
511 match message.role {
512 Role::User => format!("<|user|>\n{}<|end|>\n", message.content),
513 Role::Assistant | Role::ToolCall => {
514 format!("<|assistant|>\n{}<|end|>\n", message.content)
515 }
516 Role::ToolResult => format!("<|user|>\n{}<|end|>\n", render_tool_result(message)),
517 Role::System => String::new(),
518 }
519 }
520
521 fn assistant_prefix(&self) -> &str {
522 "<|assistant|>\n"
523 }
524}
525
526const DEEPSEEK_BOS: &str = "<|begin▁of▁sentence|>";
530const DEEPSEEK_EOS: &str = "<|end▁of▁sentence|>";
532
533pub struct DeepSeekTemplate;
547
548impl ChatTemplate for DeepSeekTemplate {
549 fn name(&self) -> &str {
550 "deepseek"
551 }
552
553 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
554 let mut prompt = self.format_system(system_prompt, tools);
555
556 for msg in messages.iter().filter(|m| m.role != Role::System) {
557 prompt.push_str(&self.format_message(msg));
558 }
559
560 prompt.push_str(self.assistant_prefix());
561 prompt
562 }
563
564 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
565 let mut s = String::from(system_prompt);
566 s.push_str(&render_tools(tools));
567 if s.is_empty() {
568 String::from(DEEPSEEK_BOS)
569 } else {
570 format!("{DEEPSEEK_BOS}{s}\n\n")
571 }
572 }
573
574 fn format_message(&self, message: &Message) -> String {
575 match message.role {
576 Role::User => format!("User: {}\n\n", message.content),
577 Role::Assistant | Role::ToolCall => {
578 format!("Assistant: {}{DEEPSEEK_EOS}", message.content)
579 }
580 Role::ToolResult => format!("User: {}\n\n", render_tool_result(message)),
581 Role::System => String::new(),
582 }
583 }
584
585 fn assistant_prefix(&self) -> &str {
586 "Assistant:"
587 }
588}
589
590pub struct CommandRTemplate;
601
602impl ChatTemplate for CommandRTemplate {
603 fn name(&self) -> &str {
604 "command-r"
605 }
606
607 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
608 let mut prompt = self.format_system(system_prompt, tools);
609
610 for msg in messages.iter().filter(|m| m.role != Role::System) {
611 prompt.push_str(&self.format_message(msg));
612 }
613
614 prompt.push_str(self.assistant_prefix());
615 prompt
616 }
617
618 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
619 let mut s = String::from(system_prompt);
620 s.push_str(&render_tools(tools));
621 if s.is_empty() {
622 String::from("<BOS_TOKEN>")
623 } else {
624 format!("<BOS_TOKEN><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{s}<|END_OF_TURN_TOKEN|>")
625 }
626 }
627
628 fn format_message(&self, message: &Message) -> String {
629 match message.role {
630 Role::User => format!(
631 "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
632 message.content
633 ),
634 Role::Assistant | Role::ToolCall => format!(
635 "<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
636 message.content
637 ),
638 Role::ToolResult => format!(
639 "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
640 render_tool_result(message)
641 ),
642 Role::System => String::new(),
643 }
644 }
645
646 fn assistant_prefix(&self) -> &str {
647 "<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>"
648 }
649}
650
651pub struct Llama2Template;
667
668impl Llama2Template {
669 fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
670 let mut s = String::from(system_prompt);
671 s.push_str(&render_tools(tools));
672 s
673 }
674}
675
676impl ChatTemplate for Llama2Template {
677 fn name(&self) -> &str {
678 "llama2"
679 }
680
681 fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
682 let system = self.system_text(system_prompt, tools);
683 let mut out = String::new();
684 let mut system_pending = !system.is_empty();
685
686 for msg in messages.iter().filter(|m| m.role != Role::System) {
687 match msg.role {
688 Role::User | Role::ToolResult => {
689 let body = if msg.role == Role::ToolResult {
690 render_tool_result(msg)
691 } else {
692 msg.content.clone()
693 };
694 let inst = if system_pending {
695 system_pending = false;
696 format!("<<SYS>>\n{system}\n<</SYS>>\n\n{body}")
697 } else {
698 body
699 };
700 out.push_str(&format!("<s>[INST] {inst} [/INST]"));
701 }
702 Role::Assistant | Role::ToolCall => {
703 out.push_str(&format!(" {} </s>", msg.content));
704 }
705 Role::System => {}
706 }
707 }
708
709 out
710 }
711
712 fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
713 let system = self.system_text(system_prompt, tools);
714 if system.is_empty() {
715 String::from("<s>")
716 } else {
717 format!("<s>[INST] <<SYS>>\n{system}\n<</SYS>>\n\n")
718 }
719 }
720
721 fn format_message(&self, message: &Message) -> String {
722 match message.role {
723 Role::User => format!("<s>[INST] {} [/INST]", message.content),
724 Role::Assistant | Role::ToolCall => format!(" {} </s>", message.content),
725 Role::ToolResult => format!("<s>[INST] {} [/INST]", render_tool_result(message)),
726 Role::System => String::new(),
727 }
728 }
729
730 fn assistant_prefix(&self) -> &str {
731 " "
732 }
733}
734
735pub fn template_from_name(name: &str) -> Option<Box<dyn ChatTemplate>> {
742 match name.trim().to_ascii_lowercase().as_str() {
743 "chatml" => Some(Box::new(ChatMLTemplate)),
744 "llama3" | "llama-3" | "llama3.1" | "llama-3.1" => Some(Box::new(Llama3Template)),
745 "llama2" | "llama-2" | "llama 2" => Some(Box::new(Llama2Template)),
746 "mistral" | "mixtral" => Some(Box::new(MistralTemplate)),
747 "alpaca" => Some(Box::new(AlpacaTemplate)),
748 "vicuna" => Some(Box::new(VicunaTemplate)),
749 "gemma" | "gemma2" => Some(Box::new(GemmaTemplate)),
750 "phi3" | "phi-3" | "phi" => Some(Box::new(Phi3Template)),
751 "deepseek" | "deepseek-llm" => Some(Box::new(DeepSeekTemplate)),
752 "command-r" | "commandr" | "command_r" | "cohere" => Some(Box::new(CommandRTemplate)),
753 _ => None,
757 }
758}
759
760pub fn detect_template(gguf_template: Option<&str>) -> Box<dyn ChatTemplate> {
765 if let Some(tmpl) = gguf_template {
766 if tmpl.contains("<|start_header_id|>") || tmpl.contains("<|begin_of_text|>") {
770 return Box::new(Llama3Template);
771 }
772 if tmpl.contains("<|START_OF_TURN_TOKEN|>") || tmpl.contains("<|CHATBOT_TOKEN|>") {
773 return Box::new(CommandRTemplate);
774 }
775 if tmpl.contains("<|im_start|>") {
776 return Box::new(ChatMLTemplate);
777 }
778 if tmpl.contains("<|assistant|>")
779 && (tmpl.contains("<|user|>") || tmpl.contains("<|system|>"))
780 {
781 return Box::new(Phi3Template);
782 }
783 if tmpl.contains("<start_of_turn>") {
784 return Box::new(GemmaTemplate);
785 }
786 if tmpl.contains("<<SYS>>") {
788 return Box::new(Llama2Template);
789 }
790 if tmpl.contains("[INST]") {
791 return Box::new(MistralTemplate);
792 }
793 if tmpl.contains("### Instruction:") {
794 return Box::new(AlpacaTemplate);
795 }
796 if (tmpl.contains("User:") && tmpl.contains("Assistant:")) || tmpl.contains("▁of▁sentence")
799 {
800 return Box::new(DeepSeekTemplate);
801 }
802 if tmpl.contains("ASSISTANT:") && tmpl.contains("USER:") {
803 return Box::new(VicunaTemplate);
804 }
805 log::debug!(
806 "Unknown chat template format (len={}), falling back to ChatML",
807 tmpl.len()
808 );
809 }
810 Box::new(ChatMLTemplate)
811}