use crate::ChatMessage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatTemplate {
ChatMl,
GenericRoleMarkers,
Llama3,
Gemma,
Gemma4,
Plain,
}
impl ChatTemplate {
pub fn detect(chat_template: Option<&str>) -> Self {
match chat_template {
Some(t) if t.contains("<|im_start|>") => ChatTemplate::ChatMl,
Some(t) if t.contains("<|start_header_id|>") => ChatTemplate::Llama3,
Some(t) if t.contains("<|user|>") || t.contains("<|assistant|>") => {
ChatTemplate::GenericRoleMarkers
}
Some(t) if t.contains("<start_of_turn>") => ChatTemplate::Gemma,
Some(t) if t.contains("<|turn>") || t.contains("<turn|>") => ChatTemplate::Gemma4,
_ => ChatTemplate::Plain,
}
}
pub fn detect_for_gguf(
chat_template: Option<&str>,
arch: Option<&str>,
byte_tokenizer: bool,
) -> Self {
match chat_template.filter(|t| !t.is_empty()) {
Some(t) => Self::detect(Some(t)),
None if byte_tokenizer || arch.is_none() => Self::Plain,
None => Self::ChatMl,
}
}
pub fn render(&self, messages: &[ChatMessage]) -> String {
let mut out = String::new();
match self {
ChatTemplate::ChatMl => {
for m in messages {
out.push_str("<|im_start|>");
out.push_str(&m.role);
out.push('\n');
out.push_str(&m.rendered_content());
out.push_str("<|im_end|>\n");
}
out.push_str("<|im_start|>assistant\n");
}
ChatTemplate::GenericRoleMarkers => {
for m in messages {
out.push_str("<|");
out.push_str(&m.role);
out.push_str("|>\n");
out.push_str(&m.rendered_content());
out.push_str("</s>\n");
}
out.push_str("<|assistant|>\n");
}
ChatTemplate::Llama3 => {
for m in messages {
out.push_str("<|start_header_id|>");
out.push_str(&m.role);
out.push_str("<|end_header_id|>\n\n");
out.push_str(&m.rendered_content());
out.push_str("<|eot_id|>");
}
out.push_str("<|start_header_id|>assistant<|end_header_id|>\n\n");
}
ChatTemplate::Gemma => {
let mut system_prefix = String::new();
let mut turns: Vec<&ChatMessage> = Vec::new();
for m in messages {
match m.role.as_str() {
"system" if turns.is_empty() && system_prefix.is_empty() => {
system_prefix = m.rendered_content();
if !system_prefix.ends_with('\n') {
system_prefix.push_str("\n\n");
}
}
_ => turns.push(m),
}
}
for m in turns {
let role = match m.role.as_str() {
"assistant" | "model" => "model",
_ => "user",
};
out.push_str("<start_of_turn>");
out.push_str(role);
out.push('\n');
if role == "user" && !system_prefix.is_empty() {
out.push_str(&system_prefix);
system_prefix.clear();
}
out.push_str(&m.rendered_content());
out.push_str("<end_of_turn>\n");
}
out.push_str("<start_of_turn>model\n");
}
ChatTemplate::Gemma4 => {
for m in messages {
let role = match m.role.as_str() {
"assistant" | "model" => "model",
other => other,
};
out.push_str("<|turn>");
out.push_str(role);
out.push('\n');
out.push_str(&m.rendered_content());
out.push_str("<turn|>\n");
}
out.push_str("<|turn>model\n");
}
ChatTemplate::Plain => {
let lines: Vec<String> = messages
.iter()
.map(|m| format!("{}: {}", m.role, m.rendered_content()))
.collect();
out.push_str(&lines.join("\n"));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn msg(role: &str, content: &str) -> ChatMessage {
ChatMessage {
role: role.to_string(),
content: Some(crate::MessageContent::Text(content.to_string())),
tool_calls: None,
tool_call_id: None,
}
}
#[test]
fn detects_chatml_from_a_real_template_string() {
let template =
"{% for m in messages %}<|im_start|>{{m.role}}\n{{m.content}}<|im_end|>\n{% endfor %}";
assert_eq!(ChatTemplate::detect(Some(template)), ChatTemplate::ChatMl);
}
#[test]
fn detects_generic_role_markers_from_a_real_tinyllama_style_template() {
let template =
"{% for m in messages %}<|{{m.role}}|>\n{{m.content}}\n{% endfor %}<|assistant|>\n";
assert_eq!(
ChatTemplate::detect(Some(template)),
ChatTemplate::GenericRoleMarkers
);
}
#[test]
fn detects_llama3_from_the_real_template_string() {
let template = r#"{{- bos_token }}
{%- if messages[0]['role'] == 'system' %}
{%- set system_message = messages[0]['content']|trim %}
{%- endif %}
{{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
{%- for message in messages %}
{{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}"#;
assert_eq!(ChatTemplate::detect(Some(template)), ChatTemplate::Llama3);
}
#[test]
fn llama3_renders_real_markers_and_a_trailing_generation_prompt() {
let messages = vec![msg("system", "be helpful"), msg("user", "hi")];
let rendered = ChatTemplate::Llama3.render(&messages);
assert_eq!(
rendered,
"<|start_header_id|>system<|end_header_id|>\n\nbe helpful<|eot_id|><|start_header_id|>user<|end_header_id|>\n\nhi<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
);
}
#[test]
fn detects_gemma_from_start_of_turn_marker() {
let template = "{{ bos_token }}{% for message in messages %}<start_of_turn>{{ message['role'] }}\n{{ message['content'] }}<end_of_turn>\n{% endfor %}";
assert_eq!(ChatTemplate::detect(Some(template)), ChatTemplate::Gemma);
}
#[test]
fn detects_gemma4_from_turn_markers() {
let template = "{%- for message in messages -%}{{- '<|turn>' + message['role'] + '\\n' -}}{{- message['content'] -}}{{- '<turn|>\\n' -}}{%- endfor -%}{{- '<|turn>model\\n' -}}";
assert_eq!(ChatTemplate::detect(Some(template)), ChatTemplate::Gemma4);
let rendered = ChatTemplate::Gemma4.render(&[msg("user", "How are you?")]);
assert_eq!(rendered, "<|turn>user\nHow are you?<turn|>\n<|turn>model\n");
}
#[test]
fn gemma_renders_user_model_turns_and_generation_prompt() {
let messages = vec![msg("user", "Capital of France?")];
let rendered = ChatTemplate::Gemma.render(&messages);
assert_eq!(
rendered,
"<start_of_turn>user\nCapital of France?<end_of_turn>\n<start_of_turn>model\n"
);
}
#[test]
fn gemma_folds_system_into_first_user_turn() {
let messages = vec![msg("system", "be brief"), msg("user", "hi")];
let rendered = ChatTemplate::Gemma.render(&messages);
assert_eq!(
rendered,
"<start_of_turn>user\nbe brief\n\nhi<end_of_turn>\n<start_of_turn>model\n"
);
}
#[test]
fn falls_back_to_plain_when_no_template_or_unrecognized() {
assert_eq!(ChatTemplate::detect(None), ChatTemplate::Plain);
assert_eq!(
ChatTemplate::detect(Some("some unrecognized custom format")),
ChatTemplate::Plain
);
}
#[test]
fn gguf_without_template_defaults_to_chatml_for_real_architectures() {
assert_eq!(
ChatTemplate::detect_for_gguf(None, Some("olmoe"), false),
ChatTemplate::ChatMl
);
assert_eq!(
ChatTemplate::detect_for_gguf(Some(""), Some("olmoe"), false),
ChatTemplate::ChatMl
);
assert_eq!(
ChatTemplate::detect_for_gguf(None, Some("olmoe"), true),
ChatTemplate::Plain
);
assert_eq!(
ChatTemplate::detect_for_gguf(
Some("some unrecognized custom format"),
Some("olmoe"),
false
),
ChatTemplate::Plain
);
}
#[test]
fn chatml_renders_real_markers_and_a_trailing_generation_prompt() {
let messages = vec![msg("system", "be helpful"), msg("user", "hi")];
let rendered = ChatTemplate::ChatMl.render(&messages);
assert_eq!(
rendered,
"<|im_start|>system\nbe helpful<|im_end|>\n<|im_start|>user\nhi<|im_end|>\n<|im_start|>assistant\n"
);
}
#[test]
fn generic_role_markers_renders_real_markers_and_a_trailing_generation_prompt() {
let messages = vec![msg("user", "hello there")];
let rendered = ChatTemplate::GenericRoleMarkers.render(&messages);
assert_eq!(rendered, "<|user|>\nhello there</s>\n<|assistant|>\n");
}
#[test]
fn plain_renders_role_labeled_lines_with_no_special_tokens() {
let messages = vec![msg("user", "hello"), msg("assistant", "hi back")];
let rendered = ChatTemplate::Plain.render(&messages);
assert_eq!(rendered, "user: hello\nassistant: hi back");
}
}