use crate::errors::{
ChatWorkerError, ContextSyncError, DecodingError, GenerateResponseError, InitWorkerError,
MultimodalError, RenderError, SayError, SelectTemplateError, SetToolsError, ShiftError,
WrappedResponseError,
};
use crate::llm;
use crate::llm::{GlobalInferenceLockToken, GLOBAL_INFERENCE_LOCK};
use crate::llm::{Worker, WorkerGuard, WriteOutput};
use crate::sampler_config::read_sampler_from_metadata;
use crate::sampler_config::{SamplerConfig, ShiftStep};
use crate::template::{select_template, ChatTemplate, ChatTemplateContext};
use crate::tokenizer::{
find_chunks_prefix_difference, ChunkId, Prompt, PromptPart, Promptable, TokenizerChunk,
TokenizerChunks,
};
use crate::tool_calling::{detect_tool_format, Tool, ToolCall, ToolFormat};
use ahash::AHasher;
use indexmap::IndexMap;
use llama_cpp_2::context::params::LlamaPoolingType;
use llama_cpp_2::mtmd::MtmdBitmap;
use llama_cpp_2::sampling::LlamaSampler;
use llama_cpp_2::token::LlamaToken;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use std::collections::HashSet;
use std::hash::Hasher;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, MutexGuard};
use tracing::{debug, error, info, trace, trace_span};
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
System,
Tool,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug, Hash)]
pub struct Asset {
pub id: String,
pub path: PathBuf,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(untagged, deny_unknown_fields)]
pub enum Message {
Message {
role: Role,
content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
assets: Vec<Asset>,
},
ToolCalls {
role: Role,
content: String,
tool_calls: Vec<ToolCall>,
},
ToolResp {
role: Role,
name: String,
content: String,
},
}
impl Message {
pub fn role(&self) -> &Role {
match self {
Message::Message { role, .. }
| Message::ToolCalls { role, .. }
| Message::ToolResp { role, .. } => role,
}
}
pub fn content(&self) -> &str {
match self {
Message::Message { content, .. }
| Message::ToolCalls { content, .. }
| Message::ToolResp { content, .. } => content,
}
}
pub fn assets(&self) -> Vec<Asset> {
match self {
Message::Message { assets, .. } => assets.clone(),
Message::ToolCalls { .. } => vec![],
Message::ToolResp { .. } => vec![],
}
}
pub fn new_user(content: String) -> Self {
Self::Message {
role: Role::User,
content,
assets: vec![],
}
}
pub fn new_assistant(content: String) -> Self {
Self::Message {
role: Role::Assistant,
content,
assets: vec![],
}
}
pub fn new_system(content: String) -> Self {
Self::Message {
role: Role::System,
content,
assets: vec![],
}
}
}
pub struct ChatConfig {
pub tools: Vec<Tool>,
pub n_ctx: u32,
pub system_prompt: Option<String>,
pub template_variables: std::collections::HashMap<String, bool>,
pub sampler_config: Option<SamplerConfig>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChatStats {
pub context_size: u32,
pub context_used: u32,
pub history_count: u32,
pub tool_count: u32,
pub template_variable_count: u32,
}
impl Default for ChatConfig {
fn default() -> Self {
Self {
n_ctx: 4096,
template_variables: std::collections::HashMap::new(),
system_prompt: None,
tools: Vec::new(),
sampler_config: None,
}
}
}
pub struct ChatBuilder {
model: Arc<llm::Model>,
config: ChatConfig,
}
impl ChatBuilder {
pub fn new(model: Arc<llm::Model>) -> Self {
Self {
model,
config: ChatConfig::default(),
}
}
pub fn with_context_size(mut self, n_ctx: u32) -> Self {
self.config.n_ctx = n_ctx;
self
}
pub fn with_system_prompt<S: Into<String>>(mut self, prompt: Option<S>) -> Self {
self.config.system_prompt = prompt.map(|s| s.into());
self
}
pub fn with_tool(mut self, tool: Tool) -> Self {
self.config.tools.push(tool);
self
}
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.config.tools.extend(tools);
self
}
#[deprecated(
since = "0.6.0",
note = "Use with_template_variable(\"enable_thinking\", value) instead"
)]
pub fn with_allow_thinking(mut self, allow_thinking: bool) -> Self {
self.config
.template_variables
.insert("enable_thinking".to_string(), allow_thinking);
self
}
pub fn with_template_variable(mut self, variable_name: String, value: bool) -> Self {
self.config.template_variables.insert(variable_name, value);
self
}
pub fn with_template_variables(
mut self,
variables: std::collections::HashMap<String, bool>,
) -> Self {
self.config.template_variables = variables;
self
}
pub fn with_sampler(mut self, sampler: SamplerConfig) -> Self {
self.config.sampler_config = Some(sampler);
self
}
pub fn build(self) -> ChatHandle {
ChatHandle::new(self.model, self.config)
}
pub fn build_async(self) -> ChatHandleAsync {
ChatHandleAsync::new(self.model, self.config)
}
}
pub struct ChatHandle {
guard: WorkerGuard<ChatMsg>,
}
impl ChatHandle {
pub fn new(model: Arc<llm::Model>, config: ChatConfig) -> Self {
let (msg_tx, msg_rx) = std::sync::mpsc::channel();
let should_stop = Arc::new(AtomicBool::new(false));
let should_stop_clone = Arc::clone(&should_stop);
let join_handle = std::thread::spawn(move || {
let worker = Worker::new_chat_worker(&model, config, should_stop_clone);
let mut worker_state = match worker {
Ok(worker_state) => worker_state,
Err(errmsg) => {
return error!("Could not set up the worker initial state: {errmsg}")
}
};
while let Ok(msg) = msg_rx.recv() {
if let Err(e) = process_worker_msg(&mut worker_state, msg) {
return error!("Worker crashed: {e}");
}
}
});
Self {
guard: WorkerGuard::new(msg_tx, join_handle, Some(should_stop)),
}
}
pub fn ask_channel(
&self,
prompt: Prompt,
) -> tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput> {
let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel();
self.guard.send(ChatMsg::Ask { prompt, output_tx });
output_rx
}
pub fn ask(&self, prompt: impl Promptable) -> TokenStream {
TokenStream::new(self.ask_channel(prompt.to_prompt()))
}
fn set_and_wait_blocking<F>(&self, make_msg: F) -> Option<()>
where
F: FnOnce(tokio::sync::mpsc::Sender<()>) -> ChatMsg,
{
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
let msg = make_msg(output_tx);
self.guard.send(msg);
output_rx.blocking_recv()
}
pub fn reset_chat(
&self,
system_prompt: Option<String>,
tools: Vec<Tool>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::ResetChat {
system_prompt,
tools,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError("reset_chat".into()))
}
pub fn reset_history(&self) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetChatHistory {
messages: vec![],
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"reset_history".into(),
))
}
pub fn set_tools(&self, tools: Vec<Tool>) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetTools { tools, output_tx })
.ok_or(crate::errors::SetterError::SetterError("set_tools".into()))
}
#[deprecated(note = "Use set_template_variable(\"enable_thinking\", value) instead")]
pub fn set_allow_thinking(
&self,
allow_thinking: bool,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetThinking {
allow_thinking,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_allow_thinking".into(),
))
}
pub fn set_template_variable(
&self,
name: String,
value: bool,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetTemplateVariable {
name,
value,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_template_variable".into(),
))
}
pub fn set_template_variables(
&self,
variables: std::collections::HashMap<String, bool>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetTemplateVariables {
variables,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_template_variables".into(),
))
}
pub fn get_template_variables(
&self,
) -> Result<std::collections::HashMap<String, bool>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetTemplateVariables { output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError(
"get_template_variables".into(),
))
}
pub fn set_sampler_config(
&self,
sampler_config: SamplerConfig,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetSamplerConfig {
sampler_config,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_sampler_config".into(),
))
}
pub fn stop_generation(&self) {
self.guard.stop();
}
pub fn get_chat_history(&self) -> Result<Vec<Message>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetChatHistory { output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError(
"get_chat_history".into(),
))
}
pub fn set_chat_history(
&self,
messages: Vec<Message>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetChatHistory {
messages,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_chat_history".into(),
))
}
pub fn get_sampler_config(&self) -> Result<SamplerConfig, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetSamplerConfig { output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError(
"get_sampler_config".into(),
))
}
pub fn set_system_prompt(
&self,
system_prompt: Option<String>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_blocking(|output_tx| ChatMsg::SetSystemPrompt {
system_prompt,
output_tx,
})
.ok_or(crate::errors::SetterError::SetterError(
"set_system_prompt".into(),
))
}
pub fn get_system_prompt(&self) -> Result<Option<String>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetSystemPrompt { output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError(
"get_system_prompt".into(),
))
}
pub fn get_stats(&self) -> Result<ChatStats, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetStats { output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError("get_stats".into()))
}
pub fn tokenize(
&self,
message: String,
) -> Result<Vec<Option<i32>>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::Tokenize { message, output_tx });
output_rx
.blocking_recv()
.ok_or(crate::errors::GetterError::GetterError("tokenize".into()))?
.map_err(crate::errors::GetterError::GetterError)
}
}
#[derive(Clone)]
pub struct ChatHandleAsync {
guard: Arc<WorkerGuard<ChatMsg>>,
}
impl ChatHandleAsync {
pub fn new(model: Arc<llm::Model>, config: ChatConfig) -> Self {
let (msg_tx, msg_rx) = std::sync::mpsc::channel();
let should_stop = Arc::new(AtomicBool::new(false));
let should_stop_clone = Arc::clone(&should_stop);
let join_handle = std::thread::spawn(move || {
let worker = Worker::new_chat_worker(&model, config, should_stop_clone);
let mut worker_state = match worker {
Ok(worker_state) => worker_state,
Err(errmsg) => {
return error!("Could not set up the worker initial state: {errmsg}")
}
};
while let Ok(msg) = msg_rx.recv() {
if let Err(e) = process_worker_msg(&mut worker_state, msg) {
return error!("Worker crashed: {e}");
}
}
});
Self {
guard: Arc::new(WorkerGuard::new(msg_tx, join_handle, Some(should_stop))),
}
}
pub fn ask_channel(
&self,
prompt: Prompt,
) -> tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput> {
let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel();
self.guard.send(ChatMsg::Ask { prompt, output_tx });
output_rx
}
pub fn ask(&self, prompt: impl Promptable) -> TokenStreamAsync {
TokenStreamAsync::new(self.ask_channel(prompt.to_prompt()))
}
async fn set_and_wait_async<F>(&self, make_msg: F) -> Option<()>
where
F: FnOnce(tokio::sync::mpsc::Sender<()>) -> ChatMsg,
{
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
let msg = make_msg(output_tx);
self.guard.send(msg);
output_rx.recv().await
}
pub async fn reset_chat(
&self,
system_prompt: Option<String>,
tools: Vec<Tool>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::ResetChat {
system_prompt,
tools,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError("reset_chat".into()))
}
pub async fn reset_history(&self) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetChatHistory {
messages: vec![],
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"reset_history".into(),
))
}
pub async fn set_tools(&self, tools: Vec<Tool>) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetTools { tools, output_tx })
.await
.ok_or(crate::errors::SetterError::SetterError("set_tools".into()))
}
#[deprecated(note = "Use set_template_variable(\"enable_thinking\", value) instead")]
pub async fn set_allow_thinking(
&self,
allow_thinking: bool,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetThinking {
allow_thinking,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_allow_thinking".into(),
))
}
pub async fn set_template_variable(
&self,
name: String,
value: bool,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetTemplateVariable {
name,
value,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_template_variable".into(),
))
}
pub async fn set_template_variables(
&self,
variables: std::collections::HashMap<String, bool>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetTemplateVariables {
variables,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_template_variables".into(),
))
}
pub async fn get_template_variables(
&self,
) -> Result<std::collections::HashMap<String, bool>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetTemplateVariables { output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError(
"get_template_variables".into(),
))
}
pub async fn set_sampler_config(
&self,
sampler_config: SamplerConfig,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetSamplerConfig {
sampler_config,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_sampler_config".into(),
))
}
pub fn stop_generation(&self) {
self.guard.stop();
}
pub async fn get_chat_history(&self) -> Result<Vec<Message>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetChatHistory { output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError(
"get_chat_history".into(),
))
}
pub async fn set_chat_history(
&self,
messages: Vec<Message>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetChatHistory {
messages,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_chat_history".into(),
))
}
pub async fn get_sampler_config(&self) -> Result<SamplerConfig, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetSamplerConfig { output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError(
"get_sampler_config".into(),
))
}
pub async fn set_system_prompt(
&self,
system_prompt: Option<String>,
) -> Result<(), crate::errors::SetterError> {
self.set_and_wait_async(|output_tx| ChatMsg::SetSystemPrompt {
system_prompt,
output_tx,
})
.await
.ok_or(crate::errors::SetterError::SetterError(
"set_system_prompt".into(),
))
}
pub async fn get_system_prompt(&self) -> Result<Option<String>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetSystemPrompt { output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError(
"get_system_prompt".into(),
))
}
pub async fn get_stats(&self) -> Result<ChatStats, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::GetStats { output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError("get_stats".into()))
}
pub async fn tokenize(
&self,
message: String,
) -> Result<Vec<Option<i32>>, crate::errors::GetterError> {
let (output_tx, mut output_rx) = tokio::sync::mpsc::channel(1);
self.guard.send(ChatMsg::Tokenize { message, output_tx });
output_rx
.recv()
.await
.ok_or(crate::errors::GetterError::GetterError("tokenize".into()))?
.map_err(crate::errors::GetterError::GetterError)
}
}
pub struct TokenStream {
rx: tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput>,
completed_response: Option<String>,
}
impl TokenStream {
fn new(rx: tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput>) -> Self {
Self {
rx,
completed_response: None,
}
}
pub fn next_token(&mut self) -> Option<String> {
if self.completed_response.is_some() {
return None;
}
if let Some(output) = self.rx.blocking_recv() {
match output {
llm::WriteOutput::Token(token) => return Some(token),
llm::WriteOutput::Done(completed_response) => {
self.completed_response = Some(completed_response);
return None;
}
}
}
None
}
pub fn completed(&mut self) -> Result<String, crate::errors::CompletionError> {
loop {
match self.next_token() {
Some(_) => {
continue;
}
None => {
return self
.completed_response
.clone()
.ok_or(crate::errors::CompletionError::WorkerCrashed);
}
}
}
}
}
pub struct TokenStreamAsync {
rx: tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput>,
completed_response: Option<String>,
}
impl TokenStreamAsync {
pub fn new(rx: tokio::sync::mpsc::UnboundedReceiver<llm::WriteOutput>) -> Self {
Self {
rx,
completed_response: None,
}
}
pub async fn next_token(&mut self) -> Option<String> {
if self.completed_response.is_some() {
return None;
}
if let Some(output) = self.rx.recv().await {
match output {
llm::WriteOutput::Token(token) => return Some(token),
llm::WriteOutput::Done(completed_response) => {
self.completed_response = Some(completed_response);
return None;
}
}
}
None
}
pub async fn completed(&mut self) -> Result<String, crate::errors::CompletionError> {
loop {
match self.next_token().await {
Some(_) => {
continue;
}
None => {
return self
.completed_response
.clone()
.ok_or(crate::errors::CompletionError::WorkerCrashed);
}
}
}
}
}
enum ChatMsg {
Ask {
prompt: Prompt,
output_tx: tokio::sync::mpsc::UnboundedSender<llm::WriteOutput>,
},
ResetChat {
system_prompt: Option<String>,
tools: Vec<Tool>,
output_tx: tokio::sync::mpsc::Sender<()>,
},
SetTools {
tools: Vec<Tool>,
output_tx: tokio::sync::mpsc::Sender<()>,
},
SetSystemPrompt {
system_prompt: Option<String>,
output_tx: tokio::sync::mpsc::Sender<()>,
},
GetSystemPrompt {
output_tx: tokio::sync::mpsc::Sender<Option<String>>,
},
SetThinking {
allow_thinking: bool,
output_tx: tokio::sync::mpsc::Sender<()>,
},
SetTemplateVariable {
name: String,
value: bool,
output_tx: tokio::sync::mpsc::Sender<()>,
},
SetTemplateVariables {
variables: std::collections::HashMap<String, bool>,
output_tx: tokio::sync::mpsc::Sender<()>,
},
GetTemplateVariables {
output_tx: tokio::sync::mpsc::Sender<std::collections::HashMap<String, bool>>,
},
SetSamplerConfig {
sampler_config: SamplerConfig,
output_tx: tokio::sync::mpsc::Sender<()>,
},
GetChatHistory {
output_tx: tokio::sync::mpsc::Sender<Vec<Message>>,
},
GetSamplerConfig {
output_tx: tokio::sync::mpsc::Sender<SamplerConfig>,
},
SetChatHistory {
messages: Vec<Message>,
output_tx: tokio::sync::mpsc::Sender<()>,
},
GetStats {
output_tx: tokio::sync::mpsc::Sender<ChatStats>,
},
Tokenize {
message: String,
output_tx: tokio::sync::mpsc::Sender<Result<Vec<Option<i32>>, String>>,
},
}
impl std::fmt::Debug for ChatMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChatMsg::Ask { prompt, .. } => f.debug_struct("Ask").field("text", prompt).finish(),
ChatMsg::ResetChat {
system_prompt,
tools,
..
} => f
.debug_struct("ResetChat")
.field("system_prompt", system_prompt)
.field("tools", &format!("[{} tools]", tools.len()))
.finish(),
ChatMsg::SetTools { tools, .. } => f
.debug_struct("SetTools")
.field("tools", &format!("[{} tools]", tools.len()))
.finish(),
ChatMsg::SetSystemPrompt { system_prompt, .. } => f
.debug_struct("SetSystemPrompt")
.field("system_prompt", system_prompt)
.finish(),
ChatMsg::GetSystemPrompt { .. } => f.debug_struct("GetSystemPrompt").finish(),
ChatMsg::SetThinking { allow_thinking, .. } => f
.debug_struct("SetThinking")
.field("allow_thinking", allow_thinking)
.finish(),
ChatMsg::SetTemplateVariable { name, value, .. } => f
.debug_struct("SetTemplateVariable")
.field("name", name)
.field("value", value)
.finish(),
ChatMsg::SetTemplateVariables { variables, .. } => f
.debug_struct("SetTemplateVariables")
.field("variables", &format!("[{} variables]", variables.len()))
.finish(),
ChatMsg::GetTemplateVariables { .. } => f.debug_struct("GetTemplateVariables").finish(),
ChatMsg::SetSamplerConfig { sampler_config, .. } => f
.debug_struct("SetSamplerConfig")
.field("sampler_config", sampler_config)
.finish(),
ChatMsg::GetChatHistory { .. } => f.debug_struct("GetChatHistory").finish(),
ChatMsg::SetChatHistory { messages, .. } => f
.debug_struct("SetChatHistory")
.field("messages", &format!("[{} messages]", messages.len()))
.finish(),
ChatMsg::GetSamplerConfig { .. } => f.debug_struct("GetSamplerConfig").finish(),
ChatMsg::GetStats { .. } => f.debug_struct("GetStats").finish(),
ChatMsg::Tokenize { message, .. } => f
.debug_struct("Tokenize")
.field("message", message)
.finish(),
}
}
}
fn process_worker_msg(
worker_state: &mut Worker<'_, ChatWorker>,
msg: ChatMsg,
) -> Result<(), ChatWorkerError> {
info!(?msg, "Worker processing:");
match msg {
ChatMsg::Ask { prompt, output_tx } => {
let should_stop = Arc::clone(&worker_state.extra.should_stop);
let callback = move |out| {
if output_tx.send(out).is_err() {
should_stop.store(true, std::sync::atomic::Ordering::Relaxed);
}
};
worker_state.ask(prompt, callback)?;
}
ChatMsg::ResetChat {
system_prompt,
tools,
output_tx,
} => {
worker_state.reset_chat(system_prompt, tools)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::SetTools { tools, output_tx } => {
worker_state.set_tools(tools)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::SetSystemPrompt {
system_prompt,
output_tx,
} => {
worker_state.set_system_prompt(system_prompt)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::GetSystemPrompt { output_tx } => {
let system_prompt = worker_state.get_system_prompt();
let _ = output_tx.blocking_send(system_prompt);
}
ChatMsg::SetThinking {
allow_thinking,
output_tx,
} => {
worker_state.set_template_variable("enable_thinking".to_string(), allow_thinking)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::SetTemplateVariable {
name,
value,
output_tx,
} => {
worker_state.set_template_variable(name, value)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::SetTemplateVariables {
variables,
output_tx,
} => {
worker_state.set_template_variables(variables)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::GetTemplateVariables { output_tx } => {
let vars = worker_state.get_template_variables();
let _ = output_tx.blocking_send(vars);
}
ChatMsg::SetSamplerConfig {
sampler_config,
output_tx,
} => {
worker_state.set_sampler_config(sampler_config);
let _ = output_tx.blocking_send(());
}
ChatMsg::GetChatHistory { output_tx } => {
let msgs = worker_state.get_chat_history();
let _ = output_tx.blocking_send(msgs);
}
ChatMsg::SetChatHistory {
messages,
output_tx,
} => {
worker_state.set_chat_history(messages)?;
let _ = output_tx.blocking_send(());
}
ChatMsg::GetSamplerConfig { output_tx } => {
let sampler_config = worker_state.get_sampler_config();
let _ = output_tx.blocking_send(sampler_config);
}
ChatMsg::GetStats { output_tx } => {
let stats = worker_state.get_stats();
let _ = output_tx.blocking_send(stats);
}
ChatMsg::Tokenize { message, output_tx } => {
let result = worker_state
.tokenize_message(message)
.map_err(|e| e.to_string());
let _ = output_tx.blocking_send(result);
}
};
Ok(())
}
struct ChatContext {
chunks: TokenizerChunks,
bitmaps: IndexMap<ChunkId, MtmdBitmap>,
}
impl ChatContext {
fn new() -> Self {
Self {
chunks: TokenizerChunks::new(),
bitmaps: IndexMap::new(),
}
}
pub fn add_bitmaps(
&mut self,
bitmaps: Vec<MtmdBitmap>,
) -> Result<Vec<String>, MultimodalError> {
let mut bitmap_ids = Vec::with_capacity(bitmaps.len());
for bitmap in bitmaps {
let id = self.create_bitmap_id(&bitmap);
bitmap.set_id(&id)?;
bitmap_ids.push(id.clone());
self.bitmaps.entry(id).or_insert(bitmap);
}
Ok(bitmap_ids)
}
pub fn garbage_collect_bitmaps(&mut self, messages: &[Message]) {
let referenced_bitmaps: HashSet<String> = messages
.iter()
.flat_map(|msg| msg.assets())
.map(|asset| asset.id)
.collect();
let unreferenced_bitmap_ids: Vec<_> = self
.bitmaps
.keys()
.filter(|id| !referenced_bitmaps.contains(id.as_str()))
.cloned()
.collect();
self.remove_bitmaps(unreferenced_bitmap_ids);
}
fn create_bitmap_id(&self, bitmap: &MtmdBitmap) -> String {
let mut hasher = AHasher::default();
hasher.write(bitmap.data());
hasher.finish().to_string()
}
fn remove_bitmaps(&mut self, bitmap_ids: Vec<String>) {
for id in bitmap_ids {
if let Some(bitmap) = self.bitmaps.shift_remove(&id) {
drop(bitmap);
}
}
}
}
struct ChatWorker {
should_stop: Arc<AtomicBool>,
tool_grammar: Option<gbnf::GbnfGrammar>,
tool_format: Option<ToolFormat>,
sampler_config: SamplerConfig,
messages: Vec<Message>,
template_variables: std::collections::HashMap<String, bool>,
tools: Vec<Tool>,
chat_template: ChatTemplate,
context: ChatContext,
}
impl llm::PoolingType for ChatWorker {
fn pooling_type(&self) -> LlamaPoolingType {
LlamaPoolingType::None
}
}
impl Worker<'_, ChatWorker> {
fn new_chat_worker(
model: &llm::Model,
config: ChatConfig,
should_stop: Arc<AtomicBool>,
) -> Result<Worker<'_, ChatWorker>, InitWorkerError> {
let template = select_template(&model.language_model, !config.tools.is_empty())?;
let (tool_format, grammar) = if !config.tools.is_empty() {
match detect_tool_format(&model.language_model) {
Ok(format) => {
debug!(format = ?format, "Detected tool calling format");
let grammar = match format.generate_grammar(&config.tools) {
Ok(g) => {
debug!(grammar = %g.as_str(), root = %g.root_name, "Generated tool calling grammar");
Some(g)
}
Err(e) => {
debug!(error = %e, "Failed to generate grammar from tools");
None
}
};
(Some(format), grammar)
}
Err(e) => {
debug!(error = %e, "Failed to detect tool format, tools will not work");
(None, None)
}
}
} else {
(None, None)
};
let sampler_config = match config.sampler_config {
Some(sc) => sc,
None => read_sampler_from_metadata(&model.language_model).unwrap_or_default(),
};
Worker::new_with_type(
model,
config.n_ctx,
false,
ChatWorker {
should_stop,
tool_grammar: grammar,
tool_format,
sampler_config,
messages: match config.system_prompt {
Some(msg) => vec![Message::Message {
role: Role::System,
content: msg,
assets: vec![],
}],
None => vec![],
},
chat_template: template,
template_variables: config.template_variables,
tools: config.tools,
context: ChatContext::new(),
},
)
}
fn should_stop(&self) -> bool {
self.extra
.should_stop
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn add_system_message(&mut self, content: String) {
self.add_message(Role::System, content, vec![])
}
pub fn add_assistant_message(&mut self, content: String) {
self.add_message(Role::Assistant, content, vec![])
}
pub fn add_user_message(&mut self, content: String, assets: Vec<Asset>) {
self.add_message(Role::User, content, assets)
}
fn add_message(&mut self, role: Role, content: String, assets: Vec<Asset>) {
self.extra.messages.push(Message::Message {
role,
content,
assets,
});
}
pub fn add_tool_calls(&mut self, tool_calls: Vec<ToolCall>) {
self.extra.messages.push(Message::ToolCalls {
role: Role::Assistant,
content: "".into(),
tool_calls,
});
}
pub fn add_tool_resp(&mut self, name: String, content: String) {
self.extra.messages.push(Message::ToolResp {
role: Role::Tool,
name,
content,
});
}
#[tracing::instrument(level = "debug", skip_all)]
fn sync_context_with_render(
&mut self,
inference_lock_token: &MutexGuard<'_, GlobalInferenceLockToken>,
) -> Result<(), ContextSyncError> {
let mut chunks = self.render_as_chunks(true)?;
if chunks.n_tokens() > self.ctx.n_ctx() as usize {
self.context_shift()?;
chunks = self.render_as_chunks(true)?;
}
let prefix_index = find_chunks_prefix_difference(&self.extra.context.chunks, &chunks);
debug_assert!(!chunks.is_empty());
let old_n_past = self.n_past;
self.remove_all_tokens_from_index_from_ctx(prefix_index)?;
let chunks_to_read = chunks.tail(self.n_past as usize);
if chunks_to_read.n_tokens() > 0 {
self.read_chunks(chunks_to_read, inference_lock_token)?;
} else if self.n_past < old_n_past {
self.remove_all_tokens_from_index_from_ctx(self.n_past as usize - 1)?;
let refresh_tokens = chunks.tail(self.n_past as usize);
self.read_chunks(refresh_tokens, inference_lock_token)?;
}
self.extra.context.chunks = chunks;
self.extra
.context
.garbage_collect_bitmaps(&self.extra.messages);
Ok(())
}
fn context_shift(&mut self) -> Result<(), ShiftError> {
info!("Context shift happens!");
let target_token_size = (self.ctx.n_ctx() / 2) as usize;
let mut messages = self.extra.messages.clone();
let system_end = if matches!(messages[0].role(), Role::System) {
1
} else {
0
};
let first_user_message_index =
self.find_next_user_message(&messages, system_end)
.ok_or(ShiftError::Message(
"No first user message in chat history".into(),
))?;
let first_deletable_index = self
.find_next_user_message(&messages, first_user_message_index + 1)
.ok_or(ShiftError::Message("No deletable messages".into()))?; let mut last_deletable_index = self
.find_start_of_last_n_user_messages(&messages, 2)
.ok_or(ShiftError::Message(
"Less than two user messages in chat history.".into(),
))?
- 1;
let mut messages_to_delete = 2;
loop {
if first_deletable_index > last_deletable_index {
break;
}
let chunks = self.render_as_chunks(false)?;
if chunks.n_tokens() <= target_token_size {
break;
}
let target_delete_index = min(
first_deletable_index + messages_to_delete - 1,
last_deletable_index,
);
let delete_index = min(
self.find_next_user_message(&messages, target_delete_index + 1)
.ok_or(ShiftError::Message(
"Could find user message supposed to be there".into(),
))?
- 1,
last_deletable_index,
); messages.drain(first_deletable_index..=delete_index);
messages_to_delete *= 2;
let messages_deleted = delete_index - first_deletable_index + 1;
last_deletable_index -= messages_deleted;
}
self.extra.messages = messages;
Ok(())
}
fn find_next_user_message(&self, messages: &[Message], start_index: usize) -> Option<usize> {
messages[start_index..]
.iter()
.position(|msg| msg.role() == &Role::User)
.map(|pos| pos + start_index)
}
fn find_start_of_last_n_user_messages(&self, messages: &[Message], n: usize) -> Option<usize> {
let user_indices: Vec<usize> = messages
.iter()
.enumerate()
.filter(|(_, msg)| msg.role() == &Role::User)
.map(|(idx, _)| idx)
.collect();
if user_indices.len() >= n {
Some(user_indices[user_indices.len() - n])
} else {
None
}
}
pub fn generate_response_until_done<F>(
&mut self,
sampler_config: SamplerConfig,
mut respond: F,
inference_lock_token: &MutexGuard<'_, GlobalInferenceLockToken>,
) -> Result<&mut Self, GenerateResponseError>
where
F: FnMut(WriteOutput),
{
info!("Worker writing until done");
let mut full_response: String = String::with_capacity(4096);
let mut tokens_written_until_now = TokenizerChunks::new();
let mut sampler = sampler_config.to_stateful(self.ctx.model)?;
let mut decoder = encoding_rs::UTF_8.new_decoder();
while !self.should_stop() {
if self.n_past as u32 == self.ctx.n_ctx() {
self.context_shift()?;
self.sync_context_with_render(inference_lock_token)?;
self.read_chunks(tokens_written_until_now.clone(), inference_lock_token)?;
}
let new_token = self.sample_and_decode_next_token(&mut sampler)?;
tokens_written_until_now.append(TokenizerChunk::new_text(vec![new_token]));
let token_bytes = match self
.ctx
.model
.token_to_piece_bytes(new_token, 8, true, None)
{
Err(llama_cpp_2::TokenToStringError::InsufficientBufferSpace(i)) => {
self.ctx.model.token_to_piece_bytes(
new_token,
(-i).try_into().expect("Error buffer size is positive"),
true,
None,
)
}
x => x,
}?;
let max_len = decoder
.max_utf8_buffer_length(token_bytes.len())
.unwrap_or(32);
let mut token_str = String::with_capacity(max_len);
let (_result, _bytes_read, _had_errors) =
decoder.decode_to_string(&token_bytes, &mut token_str, false);
let gemma4_eog_hotfix = token_str == "<eos>" && new_token == LlamaToken::new(1);
let has_eog = self.ctx.model.is_eog_token(new_token) || gemma4_eog_hotfix;
trace!(?new_token, ?token_str, ?has_eog);
if !has_eog {
full_response.push_str(&token_str);
trace!(?token_str, "Sending out token:");
respond(WriteOutput::Token(token_str.to_string()));
}
if has_eog {
break;
}
}
debug!(%full_response, "Sending out");
respond(WriteOutput::Done(full_response));
Ok(self)
}
fn sample_and_decode_next_token(
&mut self,
sampler: &mut LlamaSampler,
) -> Result<LlamaToken, DecodingError> {
trace!("Applying sampler");
let new_token: LlamaToken = sampler.sample(&self.ctx, -1);
self.small_batch.clear();
self.small_batch.add(new_token, self.n_past, &[0], true)?;
let decode_span = trace_span!("write decode", n_past = self.n_past);
let decode_guard = decode_span.enter();
self.ctx.decode(&mut self.small_batch)?;
drop(decode_guard);
self.n_past += 1;
Ok(new_token)
}
pub fn ask<F>(&mut self, prompt: Prompt, respond: F) -> Result<&mut Self, SayError>
where
F: Fn(llm::WriteOutput) + Clone,
{
self.extra
.should_stop
.store(false, std::sync::atomic::Ordering::Relaxed);
let tool_call_begin = self
.extra
.tool_format
.as_ref()
.map(|fmt| fmt.begin_token().to_string());
let media_assets = prompt.extract_media_assets();
let bitmaps = if let Some(projection_model) = self.projection_model.as_ref() {
media_assets
.iter()
.map(|part| match part {
PromptPart::Image(path) => projection_model.load_image(path),
PromptPart::Audio(path) => projection_model.load_audio(path),
PromptPart::Text(_) => unreachable!(),
})
.collect::<Result<Vec<MtmdBitmap>, MultimodalError>>()?
} else {
vec![]
};
debug!("Detected bitmaps: {:?}", bitmaps);
let bitmap_ids = self.extra.context.add_bitmaps(bitmaps)?;
let assets = bitmap_ids
.iter()
.zip(media_assets.iter())
.map(|(id, part)| Asset {
id: id.clone(),
path: match part {
PromptPart::Image(path) | PromptPart::Audio(path) => path.to_path_buf(),
PromptPart::Text(_) => unreachable!(),
},
})
.collect::<Vec<_>>();
self.add_user_message(prompt.to_string(), assets);
let sampler = self.extra.tool_grammar.as_ref().map_or(
self.extra.sampler_config.clone(),
|tool_grammar| {
self.extra
.sampler_config
.clone()
.prepend(ShiftStep::Grammar {
trigger_on: tool_call_begin.clone(),
root: tool_grammar.root_name.to_string(),
grammar: tool_grammar.as_str().into(),
})
},
);
let mut response: String = self.wrapped_update_context_and_generate_response(
sampler.clone(),
respond.clone(),
tool_call_begin.clone(),
)?;
if let Some(tool_format) = self.extra.tool_format.clone() {
while let Some(tool_calls) = tool_format.extract_tool_calls(&response) {
debug!(?tool_calls, "Got tool calls:");
self.add_tool_calls(tool_calls.clone());
for tool_call in tool_calls {
let Some(tool) = self.extra.tools.iter().find(|t| t.name == tool_call.name)
else {
error!(
tool_name = tool_call.name,
"Model triggered tool call for invalid tool name:",
);
let errmsg = format!("ERROR - Invalid tool name: {}", tool_call.name);
self.add_tool_resp(tool_call.name, errmsg);
continue;
};
debug!("Calling the tool now!");
let response = (tool.function)(tool_call.arguments);
debug!(%tool_call.name, %response, "Tool call result:");
self.add_tool_resp(tool_call.name, response);
}
response = self.wrapped_update_context_and_generate_response(
sampler.clone(),
respond.clone(),
tool_call_begin.clone(),
)?;
}
}
debug_assert!(tool_call_begin
.as_ref()
.is_none_or(|t| !response.contains(t.as_str())));
self.add_assistant_message(response);
self.extra.context.chunks = self.render_as_chunks(true)?;
Ok(self)
}
fn render_as_chunks(&mut self, handled: bool) -> Result<TokenizerChunks, RenderError> {
let messages = &self.extra.messages;
let template_context = ChatTemplateContext::new(
self.extra.template_variables.clone(),
if self.extra.tools.is_empty() {
None
} else {
Some(self.extra.tools.clone())
},
);
let rendered_chat = if handled {
self.extra
.chat_template
.render(messages, &template_context)?
} else {
self.extra
.chat_template
.render_unhandled(messages, &template_context)?
};
let bitmaps: Vec<&MtmdBitmap> = self
.extra
.messages
.iter()
.flat_map(|msg| msg.assets())
.filter_map(|asset| self.extra.context.bitmaps.get(&asset.id))
.collect();
Ok(self.tokenizer.tokenize(rendered_chat, bitmaps)?)
}
fn wrapped_update_context_and_generate_response<F>(
&mut self,
sampler: SamplerConfig,
respond: F,
tool_call_begin_token: Option<String>,
) -> Result<String, WrappedResponseError>
where
F: Fn(llm::WriteOutput) + Clone,
{
let _gil_guard = GLOBAL_INFERENCE_LOCK.lock();
let inference_lock_token = _gil_guard.unwrap();
self.sync_context_with_render(&inference_lock_token)?;
let (wrapped_respond, resp_receiver) = wrap_respond(respond.clone(), tool_call_begin_token);
self.generate_response_until_done(sampler, wrapped_respond, &inference_lock_token)?;
Ok(resp_receiver.recv()?)
}
pub fn reset_chat(
&mut self,
system_prompt: Option<String>,
tools: Vec<Tool>,
) -> Result<(), SelectTemplateError> {
self.reset_context();
if !tools.is_empty() && self.extra.tool_format.is_none() {
match detect_tool_format(self.ctx.model) {
Ok(format) => {
debug!(format = ?format, "Detected tool calling format");
self.extra.tool_format = Some(format);
}
Err(e) => {
debug!(error = %e, "Failed to detect tool format, tools will not work");
}
}
}
self.extra.tool_grammar = if !tools.is_empty() {
if let Some(ref format) = self.extra.tool_format {
match format.generate_grammar(&tools) {
Ok(g) => Some(g),
Err(e) => {
debug!(error = %e, "Failed to generate grammar from tools");
None
}
}
} else {
None
}
} else {
None
};
self.extra.tools = tools;
self.extra.messages = Vec::new();
self.extra.context = ChatContext::new();
if let Some(sys_msg) = system_prompt {
self.add_system_message(sys_msg);
}
Ok(())
}
pub fn set_template_variable(
&mut self,
name: String,
value: bool,
) -> Result<(), ChatWorkerError> {
self.extra.template_variables.insert(name, value);
Ok(())
}
pub fn set_template_variables(
&mut self,
variables: std::collections::HashMap<String, bool>,
) -> Result<(), ChatWorkerError> {
self.extra.template_variables = variables;
Ok(())
}
pub fn get_template_variables(&self) -> std::collections::HashMap<String, bool> {
self.extra.template_variables.clone()
}
pub fn set_sampler_config(&mut self, sampler_config: SamplerConfig) {
self.extra.sampler_config = sampler_config;
}
pub fn set_system_prompt(
&mut self,
system_prompt: Option<String>,
) -> Result<(), ContextSyncError> {
match system_prompt {
Some(sys_msg) => {
let system_message = Message::Message {
role: Role::System,
content: sys_msg,
assets: vec![],
};
if self.extra.messages.is_empty() {
self.extra.messages.push(system_message);
} else if *self.extra.messages[0].role() == Role::System {
self.extra.messages[0] = system_message;
} else {
self.extra.messages.insert(0, system_message);
}
}
None => {
if !self.extra.messages.is_empty() && *self.extra.messages[0].role() == Role::System
{
self.extra.messages.remove(0);
}
}
}
let _gil_guard = GLOBAL_INFERENCE_LOCK.lock();
let inference_lock_token = _gil_guard.unwrap();
self.sync_context_with_render(&inference_lock_token)?;
Ok(())
}
pub fn get_system_prompt(&self) -> Option<String> {
if self.extra.messages.is_empty() {
return None;
};
match &self.extra.messages[0] {
Message::Message {
role: Role::System,
content,
assets: _,
} => Some(content.clone()),
_ => None,
}
}
pub fn set_tools(&mut self, tools: Vec<Tool>) -> Result<(), SetToolsError> {
if !tools.is_empty() && self.extra.tool_format.is_none() {
match detect_tool_format(self.ctx.model) {
Ok(format) => {
debug!(format = ?format, "Detected tool calling format");
self.extra.tool_format = Some(format);
}
Err(e) => {
debug!(error = %e, "Failed to detect tool format, tools will not work");
}
}
}
self.extra.tool_grammar = if !tools.is_empty() {
if let Some(ref format) = self.extra.tool_format {
match format.generate_grammar(&tools) {
Ok(g) => Some(g),
Err(e) => {
debug!(error = %e, "Failed to generate grammar from tools");
None
}
}
} else {
None
}
} else {
None
};
self.extra.tools = tools;
self.extra.chat_template = select_template(self.ctx.model, !self.extra.tools.is_empty())?;
let _gil_guard = GLOBAL_INFERENCE_LOCK.lock();
let inference_lock_token = _gil_guard.unwrap();
self.sync_context_with_render(&inference_lock_token)?;
Ok(())
}
pub fn set_chat_history(&mut self, messages: Vec<Message>) -> Result<(), ContextSyncError> {
let system_msg: Option<Message> = match self.extra.messages.as_slice() {
[msg @ Message::Message {
role: Role::System, ..
}, ..] => Some(msg.clone()),
_ => None,
};
self.extra.messages = system_msg.into_iter().chain(messages).collect();
self.extra
.context
.garbage_collect_bitmaps(&self.extra.messages);
Ok(())
}
pub fn get_chat_history(&self) -> Vec<Message> {
match self.extra.messages.as_slice() {
[Message::Message {
role: Role::System, ..
}, rest @ ..] => rest.to_vec(),
_ => self.extra.messages.clone(),
}
}
pub fn get_sampler_config(&self) -> SamplerConfig {
self.extra.sampler_config.clone()
}
pub fn get_stats(&self) -> ChatStats {
ChatStats {
context_size: self.ctx.n_ctx(),
context_used: self.n_past.max(0) as u32,
history_count: self.get_chat_history().len() as u32,
tool_count: self.extra.tools.len() as u32,
template_variable_count: self.extra.template_variables.len() as u32,
}
}
pub fn tokenize_message(&mut self, message: String) -> Result<Vec<Option<i32>>, RenderError> {
let mut messages = self.extra.messages.clone();
messages.push(Message::new_user(message));
let template_context = ChatTemplateContext::new(
self.extra.template_variables.clone(),
if self.extra.tools.is_empty() {
None
} else {
Some(self.extra.tools.clone())
},
);
let rendered_chat = self
.extra
.chat_template
.render(&messages, &template_context)?;
let bitmaps: Vec<&MtmdBitmap> = messages
.iter()
.flat_map(|msg| msg.assets())
.filter_map(|asset| self.extra.context.bitmaps.get(&asset.id))
.collect();
Ok(self.tokenizer.tokenize(rendered_chat, bitmaps)?.token_ids())
}
}
fn wrap_respond<F>(
respond: F,
tool_call_begin_token: Option<String>,
) -> (
impl FnMut(llm::WriteOutput),
std::sync::mpsc::Receiver<String>,
)
where
F: Fn(llm::WriteOutput),
{
let (resp_sender, resp_receiver) = std::sync::mpsc::channel();
let mut emitting = true;
let wrapped_respond = move |x| {
match &x {
llm::WriteOutput::Token(tok) if tool_call_begin_token.as_ref() == Some(tok) => {
emitting = false;
}
llm::WriteOutput::Done(resp) => {
resp_sender
.send(resp.clone())
.expect("Failed sending response");
}
llm::WriteOutput::Token(_) => (),
}
if emitting {
respond(x)
}
};
(wrapped_respond, resp_receiver)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sampler_config::SamplerPresets;
use crate::test_utils;
fn assert_valid_message_structure(messages: &[Message]) {
for i in 1..messages.len() {
let prev_msg = &messages[i - 1];
let curr_msg = &messages[i];
let prev_role = prev_msg.role();
let curr_role = curr_msg.role();
if prev_role == &Role::System {
assert_eq!(curr_role, &Role::User, "After system should come user");
continue;
}
if prev_role == &Role::User {
assert_eq!(
curr_role,
&Role::Assistant,
"User message should be followed by assistant role"
);
}
if prev_role == &Role::Assistant {
if matches!(prev_msg, Message::ToolCalls { .. }) {
assert_eq!(
curr_role,
&Role::Tool,
"Tool calls should be followed by tool response"
);
} else {
assert_eq!(
curr_role,
&Role::User,
"Assistant message should be followed by user"
);
}
}
if prev_role == &Role::Tool {
assert!(
curr_role == &Role::Tool || curr_role == &Role::Assistant,
"Tool response should be followed by another tool response or assistant"
);
}
}
}
#[test]
fn test_chat_worker() -> Result<(), Box<dyn std::error::Error>> {
let model = test_utils::load_test_model();
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
n_ctx: 1024,
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)?;
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker.ask("What is the capital of Denmark?".into(), f.clone())?;
let resp = receiver.recv()?;
println!("{}", resp);
assert!(resp.contains("Copenhagen"));
worker.ask("What language do they speak there?".into(), f)?;
let resp = receiver.recv()?;
println!("{}", resp);
assert!(resp.contains("Danish"));
Ok(())
}
#[test]
fn test_reset_chat() -> Result<(), Box<dyn std::error::Error>> {
let model = test_utils::load_test_model();
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
system_prompt: Some("You're a dog. End all responses with 'woof'".into()),
..ChatConfig::default()
},
Arc::new(AtomicBool::new(false)),
)?;
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker.ask("What is the capital of Denmark?".into(), f.clone())?;
let resp1 = receiver.recv()?;
println!("{}", resp1);
assert!(resp1.to_lowercase().contains("woof"));
let _ = worker.reset_chat(
Some("You're a cat. End all responses with 'meow'".into()),
vec![],
);
worker.ask("What is the capital of Denmark?".into(), f.clone())?;
let resp2 = receiver.recv()?;
println!("{}", resp2);
assert!(resp2.to_lowercase().contains("meow"));
Ok(())
}
#[test]
fn test_stop_mid_write() -> Result<(), Box<dyn std::error::Error>> {
let model = test_utils::load_test_model();
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
system_prompt: Some("You are a counter, only outputting numbers".into()),
n_ctx: 1024,
..ChatConfig::default()
},
Arc::new(AtomicBool::new(false)),
)?;
let should_stop = worker.extra.should_stop.clone();
should_stop.store(true, std::sync::atomic::Ordering::Relaxed);
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| match x {
llm::WriteOutput::Token(resp) => {
if resp.contains("5") {
should_stop.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
llm::WriteOutput::Done(resp) => {
sender.send(resp).unwrap();
}
};
worker.ask("Count from 0 to 9".into(), f.clone())?;
let response = receiver.recv()?;
println!("{}", response);
assert!(response.contains("5"));
assert!(!response.contains("8"));
Ok(())
}
fn test_tool() -> Tool {
Tool {
name: "get_current_temperature".into(),
description: "Gets the temperature at a given location".into(),
json_schema: serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The location to get the temperature for."
}
},
"required": [
"location"
]
}),
function: Arc::new(|args: serde_json::Value| {
let Some(location) = args.get("location") else {
return "Bad arguments format. Location key was missing.".into();
};
if location.as_str() == Some("Copenhagen") {
return "13.37°C".into();
}
if location.as_str() == Some("Beijing") {
return "42.69°C".into();
}
"Unknown location.".into()
}),
}
}
fn dkk_exchange_rate() -> Tool {
Tool {
name: "dkk_exchange_rate".into(),
description: "Gets the exchange rate for DKK to a given currency.".into(),
json_schema: serde_json::json!({
"type": "object",
"properties": {
"to-currency": {
"type": "string",
"description": "The currency to convert to in a three letter code. (eg. \"USD\")"
}
},
"required": [
"to-currency"
]
}),
function: Arc::new(|args: serde_json::Value| {
let Some(to_currency) = args.get("to-currency") else {
return "Bad arguments format. To currency key was missing.".into();
};
if to_currency.as_str() == Some("USD") {
debug!("returning 1 DKK = 0.15 USD");
return "1 DKK = 0.15 USD".into();
}
"Exchange rate not available".into()
}),
}
}
#[test]
fn test_tool_chat() {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
system_prompt: Some("You're a helpful assistant.".into()),
n_ctx: 4096,
tools: vec![test_tool()],
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)
.expect("Failed making worker");
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker
.ask(
"I would like to know the temperature in two cities: Copenhagen and Beijing."
.into(),
f,
)
.expect("fuck");
let result = receiver.recv().unwrap();
println!("{}", result);
println!("{}", worker.extra.tool_grammar.unwrap().as_str());
assert!(result.contains("13.37"));
assert!(result.contains("42.69"));
}
#[test]
fn test_multi_tool_call() {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
tools: vec![test_tool(), dkk_exchange_rate()],
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)
.expect("Failed making worker");
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker.ask(
"I would like to know the temperature in Copenhagen and the DKK to USD exchange rate."
.into(),
f,
)
.expect("dammit");
let result = receiver.recv().unwrap();
println!("{}", result);
assert!(result.contains("13.37"));
assert!(result.contains("0.15"));
}
#[test]
fn test_set_system_prompt() {
let model = test_utils::load_test_model();
let chat = ChatBuilder::new(model)
.with_context_size(2048)
.with_system_prompt(Some("You are a dog. End all responses with woof."))
.build();
let dog_response = chat.ask("Hello!").completed().unwrap();
assert!(dog_response.to_lowercase().contains("woof"));
chat.set_system_prompt(Some("You are a cat. End all responses with meow.".into()))
.unwrap();
let cat_response = chat.ask("Hello again!").completed().unwrap();
assert!(cat_response.to_lowercase().contains("meow"));
}
#[test]
fn test_context_shift() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let n_ctx = 512;
let n_messages = 8;
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
n_ctx,
system_prompt: Some("You are a helpful assistant that provides informative and detailed responses. End every response with \"Do you have any further questions?\"".into()),
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)?;
for i in 1..=n_messages {
worker.add_user_message(
format!("This is user message number {}. What is {} * {}?", i, i, i),
vec![],
);
worker.add_assistant_message(format!(
"<think> </think> The answer is {}. Do you have any further questions?",
i * i
));
}
worker.add_user_message("Hello!".into(), vec![]);
let messages_before = worker.extra.messages.len();
assert!(
messages_before > 6,
"Should have more than 6 messages before shift"
);
worker.context_shift()?;
println!("{:?}", worker.extra.messages);
let messages_after = worker.extra.messages.clone();
assert_eq!(
messages_after[0].role(),
&Role::System,
"System message should remain"
);
if let Message::Message { content, .. } = &messages_after[0] {
assert!(
content.contains("helpful assistant"),
"System prompt should be preserved"
);
}
let first_user_idx = messages_after.iter().position(|m| m.role() == &Role::User);
assert!(
first_user_idx.is_some(),
"First user message should be preserved"
);
let user_count = messages_after
.iter()
.filter(|m| m.role() == &Role::User)
.count();
assert!(
user_count >= 3,
"Should preserve first user message and last 2 user messages"
);
let last_user = messages_after
.iter()
.rev()
.find(|m| m.role() == &Role::User);
if let Some(Message::Message { content, .. }) = last_user {
assert!(
content.contains("Hello!"),
"Last user message should be preserved"
);
}
let token_count = worker.render_as_chunks(true)?.len();
let target_size = (n_ctx / 2) as usize;
assert!(
token_count <= target_size,
"Token count {} should be <= target size {}",
token_count,
target_size
);
assert!(
messages_after.len() < messages_before,
"Should have fewer messages after shift"
);
assert_valid_message_structure(&messages_after);
println!("Messages before shift: {}", messages_before);
println!("Messages after shift: {}", messages_after.len());
println!("Token count after shift: {}", token_count);
println!("Target token size: {}", target_size);
Ok(())
}
#[test]
fn test_context_shift_with_tool_calls() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let n_ctx = 1024;
let n_messages = 10;
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
n_ctx,
system_prompt: Some("You are a helpful assistant.".into()),
tools: vec![test_tool()],
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)?;
for i in 1..=n_messages {
worker.add_user_message(
format!("User message {}. What is {} * {}?", i, i, i),
vec![],
);
if i % 2 == 0 {
worker.add_tool_calls(vec![ToolCall {
name: "get_current_temperature".into(),
arguments: serde_json::json!({"location": "Copenhagen"}),
}]);
worker.add_tool_resp("get_current_temperature".into(), "13.37°C".into());
worker.add_assistant_message(format!(
"The temperature is 13.37°C and {} * {} = {}.",
i,
i,
i * i
));
} else {
worker.add_assistant_message(format!("The answer is {}.", i * i));
}
}
worker.add_user_message("Final question!".into(), vec![]);
let messages_before = worker.extra.messages.len();
println!("Messages before shift: {}", messages_before);
worker.context_shift()?;
println!("{:?}", worker.extra.messages);
let messages_after = worker.extra.messages.clone();
assert_eq!(messages_after[0].role(), &Role::System);
let first_user_idx = messages_after.iter().position(|m| m.role() == &Role::User);
assert!(
first_user_idx.is_some(),
"First user message should be preserved"
);
let user_count = messages_after
.iter()
.filter(|m| m.role() == &Role::User)
.count();
assert!(
user_count >= 3,
"Should preserve first user message and last 2 user messages"
);
let last_user = messages_after
.iter()
.rev()
.find(|m| m.role() == &Role::User);
if let Some(Message::Message { content, .. }) = last_user {
assert!(
content.contains("Final question!"),
"Last user message should be preserved"
);
}
let token_count = worker.render_as_chunks(true)?.len();
let target_size = (n_ctx / 2) as usize;
assert!(
token_count <= target_size,
"Token count {} should be <= target size {}",
token_count,
target_size
);
assert!(
messages_after.len() < messages_before,
"Should have fewer messages after shift"
);
assert_valid_message_structure(&messages_after);
println!("Messages before shift: {}", messages_before);
println!("Messages after shift: {}", messages_after.len());
println!("Token count after shift: {}", token_count);
println!("Target token size: {}", target_size);
Ok(())
}
#[test]
fn test_context_shift_on_say() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let n_messages = 14;
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
system_prompt: Some("You are a helpful assistant.".into()),
n_ctx: 512, ..Default::default()
},
Arc::new(AtomicBool::new(false)),
)?;
for i in 1..=n_messages {
worker.add_user_message(
format!("This is user message number {}. What is {} * {}?", i, i, i),
vec![],
);
worker.add_assistant_message(format!("The answer is {}.", i * i));
}
let messages_before_shift = worker.extra.messages.len();
println!("Messages before shift: {}", messages_before_shift);
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker.ask(
"This is a new question that will not fit in the context! What is 10 * 10?".into(),
f,
)?;
let _response = receiver.recv()?;
let messages_after = worker.extra.messages.clone();
println!("Messages after operation: {}", messages_after.len());
assert!(
messages_after.len() < messages_before_shift,
"Context shift should have reduced message count"
);
assert_eq!(messages_after[0].role(), &Role::System);
let first_user_idx = messages_after.iter().position(|m| m.role() == &Role::User);
assert!(
first_user_idx.is_some(),
"First user message should be preserved"
);
let last_user = messages_after
.iter()
.rev()
.find(|m| m.role() == &Role::User);
if let Some(Message::Message { content, .. }) = last_user {
assert!(
content.contains("new question"),
"Last user message should be preserved"
);
}
assert_valid_message_structure(&messages_after);
Ok(())
}
#[test]
fn test_context_while_writing() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let n_messages = 19;
let mut worker = Worker::new_chat_worker(
&model,
ChatConfig {
n_ctx: 768, system_prompt: Some("You are a helpful assistant.".into()),
..Default::default()
},
Arc::new(AtomicBool::new(false)),
)?;
for i in 1..=n_messages {
worker.add_user_message(
format!("This is user message number {}. What is {} * {}?", i, i, i),
vec![],
);
worker.add_assistant_message(format!("The answer is {}.", i * i));
}
let messages_before_shift = worker.extra.messages.len();
println!("Messages before shift: {}", messages_before_shift);
let (sender, receiver) = std::sync::mpsc::channel();
let f = move |x| {
if let llm::WriteOutput::Done(resp) = x {
sender.send(resp).unwrap();
}
};
worker.ask("What is 10 * 10?".into(), f)?;
let _response = receiver.recv()?;
let messages_after = worker.extra.messages.clone();
println!("Messages after operation: {}", messages_after.len());
assert!(
messages_after.len() < messages_before_shift,
"Context shift should have reduced message count"
);
assert_eq!(messages_after[0].role(), &Role::System);
let first_user_idx = messages_after.iter().position(|m| m.role() == &Role::User);
assert!(
first_user_idx.is_some(),
"First user message should be preserved"
);
let last_user = messages_after
.iter()
.rev()
.find(|m| m.role() == &Role::User);
if let Some(Message::Message { content, .. }) = last_user {
assert!(
content.contains("What is"),
"Last user message should be preserved"
);
}
assert_valid_message_structure(&messages_after);
Ok(())
}
#[test]
fn test_chat_worker_multiple_contexts() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let model_clone = Arc::clone(&model);
let dk_handle = std::thread::spawn(move || {
let chat = ChatBuilder::new(model_clone)
.with_context_size(4096)
.with_template_variable("enable_thinking".to_string(), false)
.build();
chat.ask("What is the capital of Denmark?").completed()
});
let de_handle = std::thread::spawn(move || {
let chat = ChatBuilder::new(model)
.with_context_size(4096)
.with_template_variable("enable_thinking".to_string(), false)
.build();
chat.ask("What is the capital of Germany?").completed()
});
let dk_resp = dk_handle.join().unwrap()?;
let de_resp = de_handle.join().unwrap()?;
println!("Denmark response: {}", dk_resp);
println!("Germany response: {}", de_resp);
assert!(
dk_resp.to_lowercase().contains("copenhagen"),
"Expected completion to contain 'Copenhagen', got: {dk_resp}"
);
assert!(
de_resp.to_lowercase().contains("berlin"),
"Expected completion to contain 'Berlin', got: {de_resp}"
);
Ok(())
}
#[tokio::test]
async fn test_enable_thinking() -> Result<(), Box<dyn std::error::Error>> {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let chat = ChatBuilder::new(model).build_async();
let res1: String = chat
.ask("What is the capital of Denmark?".to_string())
.completed()
.await?;
assert!(
res1.contains("<think>"),
"Expected the model to initialize with thinking mode, but it did not"
);
chat.set_template_variable("enable_thinking".to_string(), false)
.await?;
let res2: String = chat
.ask("What is the capital of the Czech Republic?".to_string())
.completed()
.await?;
assert!(
!res2.contains("<think>"),
"Expected the model to not think, but it did"
);
Ok(())
}
#[test]
fn test_greedy_sampler_produces_deterministic_output() {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let chat = ChatBuilder::new(model)
.with_context_size(2048)
.with_template_variable("enable_thinking".to_string(), false)
.build();
chat.set_sampler_config(SamplerPresets::greedy()).unwrap();
chat.set_sampler_config(chat.get_sampler_config().unwrap())
.unwrap();
let response1 = chat.ask("Say exactly: 'Hello'").completed().unwrap();
chat.reset_history().unwrap();
let response2 = chat.ask("Say exactly: 'Hello'").completed().unwrap();
assert_eq!(
response1, response2,
"Greedy sampler should produce identical output for the same prompt"
);
}
#[test]
fn test_reset_chat_with_no_system_prompt() {
test_utils::init_test_tracing();
let model = test_utils::load_test_model();
let chat = ChatBuilder::new(model)
.with_context_size(2048)
.with_template_variable("enable_thinking".to_string(), false)
.build();
let _ = chat.reset_history();
let resp = chat
.ask("What is the capital of Denmark?")
.completed()
.unwrap();
assert!(
resp.contains("Copenhagen"),
"Model failed to answer after reset"
);
}
}