use crate::tools::{ToolCall, ToolResult, ToolSpec};
pub const EOT: &str = "<turn|>";
pub fn conversation_start(system: Option<&str>, think: bool) -> String {
let mut s = String::from("<bos>");
if think || system.is_some() {
s.push_str("<|turn>system\n");
if think {
s.push_str("<|think|>\n");
}
if let Some(sys) = system {
s.push_str(sys.trim());
}
s.push_str("<turn|>\n");
}
s
}
pub fn user_turn(content: &str) -> String {
format!("<|turn>user\n{}<turn|>\n<|turn>model\n", content.trim())
}
pub fn end_of_model_turn() -> String {
format!("{EOT}\n")
}
pub trait ChatFormat {
fn conversation_start(&self, system: Option<&str>, think: bool) -> String;
fn user_turn(&self, content: &str, think: bool) -> String;
fn end_of_model_turn(&self) -> String;
fn end_of_turn_token(&self) -> &'static str;
fn think_ids(&self) -> (u32, u32);
fn starts_in_thinking(&self, think: bool) -> bool;
fn conversation_start_tools(&self, system: Option<&str>, think: bool, tools: &[ToolSpec]) -> String;
fn tool_call_marker(&self) -> &'static str;
fn parse_tool_calls(&self, raw: &str) -> Vec<ToolCall>;
fn tool_results(&self, results: &[ToolResult], think: bool) -> String;
}
pub struct GemmaChat;
impl ChatFormat for GemmaChat {
fn conversation_start(&self, system: Option<&str>, think: bool) -> String {
conversation_start(system, think)
}
fn user_turn(&self, content: &str, _think: bool) -> String {
user_turn(content)
}
fn end_of_model_turn(&self) -> String {
end_of_model_turn()
}
fn end_of_turn_token(&self) -> &'static str {
EOT
}
fn think_ids(&self) -> (u32, u32) {
(100, 101)
}
fn starts_in_thinking(&self, _think: bool) -> bool {
false
}
fn conversation_start_tools(&self, system: Option<&str>, think: bool, tools: &[ToolSpec]) -> String {
if tools.is_empty() {
return conversation_start(system, think);
}
let mut s = String::from("<bos><|turn>system\n");
if think {
s.push_str("<|think|>\n");
}
if let Some(sys) = system {
s.push_str(sys.trim());
}
for t in tools {
s.push_str(&crate::tools::gemma::declaration(t));
}
s.push_str("<turn|>\n");
s
}
fn tool_call_marker(&self) -> &'static str {
"<|tool_call>"
}
fn parse_tool_calls(&self, raw: &str) -> Vec<ToolCall> {
crate::tools::gemma::parse_calls(raw)
}
fn tool_results(&self, results: &[ToolResult], think: bool) -> String {
let mut s: String = results.iter().map(crate::tools::gemma::response).collect();
if think {
s.push_str("<|channel>thought\n");
}
s
}
}
pub struct QwenChat {
pub reasoning_effort: bool,
}
pub static QWEN: QwenChat = QwenChat { reasoning_effort: false };
pub static QWEN_EFFORT: QwenChat = QwenChat { reasoning_effort: true };
impl QwenChat {
pub fn for_model(dir: &std::path::Path) -> &'static QwenChat {
let jinja = crate::fsx::read(&dir.join("chat_template.jinja"))
.ok()
.map(|b| String::from_utf8_lossy(&b).into_owned());
let template = jinja.or_else(|| {
let cfg: serde_json::Value = serde_json::from_slice(&crate::fsx::read(&dir.join("tokenizer_config.json")).ok()?).ok()?;
cfg["chat_template"].as_str().map(str::to_string)
});
if template.is_some_and(|t| t.contains("reasoning_effort")) {
&QWEN_EFFORT
} else {
&QWEN
}
}
fn effort(&self, think: bool) -> Option<&'static str> {
(think && self.reasoning_effort).then_some(QWEN_REASONING_XHIGH)
}
}
const QWEN_REASONING_XHIGH: &str = "Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.";
impl ChatFormat for QwenChat {
fn conversation_start(&self, system: Option<&str>, think: bool) -> String {
let mut s = String::new();
match (self.effort(think), system) {
(Some(e), Some(sys)) => s.push_str(&format!("<|im_start|>system\n{e}\n\n{}<|im_end|>\n", sys.trim())),
(Some(e), None) => s.push_str(&format!("<|im_start|>system\n{e}<|im_end|>\n")),
(None, Some(sys)) => s.push_str(&format!("<|im_start|>system\n{}<|im_end|>\n", sys.trim())),
(None, None) => {}
}
s
}
fn user_turn(&self, content: &str, think: bool) -> String {
let tail = if think { "<think>\n" } else { "<think>\n\n</think>\n\n" };
format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n{tail}", content.trim())
}
fn end_of_model_turn(&self) -> String {
"<|im_end|>\n".to_string()
}
fn end_of_turn_token(&self) -> &'static str {
"<|im_end|>"
}
fn think_ids(&self) -> (u32, u32) {
(248068, 248069)
}
fn starts_in_thinking(&self, think: bool) -> bool {
think
}
fn conversation_start_tools(&self, system: Option<&str>, think: bool, tools: &[ToolSpec]) -> String {
if tools.is_empty() {
return self.conversation_start(system, think);
}
let mut s = String::from("<|im_start|>system\n");
if let Some(e) = self.effort(think) {
s.push_str(e);
s.push_str("\n\n");
}
s.push_str(&crate::tools::qwen::tools_block(tools));
if let Some(sys) = system.map(str::trim).filter(|s| !s.is_empty()) {
s.push_str("\n\n");
s.push_str(sys);
}
s.push_str("<|im_end|>\n");
s
}
fn tool_call_marker(&self) -> &'static str {
"<tool_call>"
}
fn parse_tool_calls(&self, raw: &str) -> Vec<ToolCall> {
crate::tools::qwen::parse_calls(raw)
}
fn tool_results(&self, results: &[ToolResult], think: bool) -> String {
let mut s = String::from("<|im_end|>\n<|im_start|>user");
for r in results {
s.push_str("\n<tool_response>\n");
s.push_str(&r.content);
s.push_str("\n</tool_response>");
}
s.push_str("<|im_end|>\n<|im_start|>assistant\n");
s.push_str(if think { "<think>\n" } else { "<think>\n\n</think>\n\n" });
s
}
}
pub fn with_turn_end(fmt: &dyn ChatFormat, tok: &tokenizers::Tokenizer, mut eos: Vec<u32>) -> Vec<u32> {
if let Some(id) = tok.token_to_id(fmt.end_of_turn_token())
&& !eos.contains(&id)
{
eos.push(id);
}
eos
}