use infrastructure_llama_cpp::context::params::LlamaModelContextParams;
use infrastructure_llama_cpp::context::LlamaModelContext;
use infrastructure_llama_cpp::llama_backend::LlamaBackend;
use infrastructure_llama_cpp::llama_batch::LlamaBatch;
use infrastructure_llama_cpp::model::params::LlamaModelParams;
use infrastructure_llama_cpp::model::{
AddBos, LlamaChatMessage, LlamaChatTemplate, LlamaModel, Special,
};
use infrastructure_llama_cpp::sampling::LlamaSampler;
use infrastructure_llama_cpp::speculative::{LlamaMtp, LlamaMtpModel, MtpSampling};
use infrastructure_llama_cpp::token::LlamaToken;
use foundation_compact::SystemTime;
use std::fmt::Write;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use foundation_core::valtron::Stream;
use crate::backends::llamacpp_helpers::build_sampler_chain;
use crate::costing::{calculate_cost, CostAccumulator};
use crate::errors::{
GenerationError, GenerationResult, ModelErrors, ModelProviderErrors, ModelProviderResult,
};
use crate::types::base_types::{
CostStatus, KVCacheType, Messages, Model, ModelId, ModelInteraction, ModelOutput, ModelParams,
ModelProvider, ModelProviderDescriptor, ModelProviders, ModelSpec, ModelState, ModelStreamBox,
ModelUsageCosting, SplitMode, StopReason, TextBasedFormatter, TextContent, ToolFormatter,
ToolShed, UsageCosting, UsageReport, UserModelContent,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpeculativeKind {
Mtp,
}
#[derive(Debug, Clone)]
pub struct SpeculativeConfig {
pub kind: SpeculativeKind,
pub mtp_model: Option<PathBuf>,
pub n_max: u32,
}
impl SpeculativeConfig {
#[must_use]
pub fn mtp(mtp_model: Option<PathBuf>, n_max: u32) -> Self {
Self {
kind: SpeculativeKind::Mtp,
mtp_model,
n_max,
}
}
}
#[derive(Debug, Clone)]
pub struct LlamaBackendConfig {
pub n_gpu_layers: u32,
pub context_length: usize,
pub batch_size: usize,
pub n_threads: usize,
pub use_mmap: bool,
pub use_mlock: bool,
pub kv_cache_type: KVCacheType,
pub split_mode: SplitMode,
pub main_gpu: u32,
pub speculative: Option<SpeculativeConfig>,
}
impl Default for LlamaBackendConfig {
fn default() -> Self {
Self {
n_gpu_layers: 0, context_length: 4096, batch_size: 512, n_threads: num_cpus(), use_mmap: true, use_mlock: false, kv_cache_type: KVCacheType::F16,
split_mode: SplitMode::Layer,
main_gpu: 0,
speculative: None, }
}
}
#[must_use]
fn num_cpus() -> usize {
std::thread::available_parallelism().map_or(4, std::num::NonZeroUsize::get)
}
impl crate::types::base_types::AuthProvider for LlamaBackendConfig {
fn auth(&self) -> Option<&foundation_auth::AuthCredential> {
None
}
}
impl LlamaBackendConfig {
#[must_use]
pub fn builder() -> LlamaBackendConfigBuilder {
LlamaBackendConfigBuilder::new()
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn to_model_params(&self) -> LlamaModelParams {
let mut params = LlamaModelParams::default();
params = params.with_n_gpu_layers(self.n_gpu_layers);
params
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
pub fn to_context_params(&self) -> LlamaModelContextParams {
let mut params = LlamaModelContextParams::default();
params = params.with_n_ctx(NonZeroU32::new(self.context_length as u32));
params = params.with_n_batch(self.batch_size as u32);
params = params.with_n_threads(self.n_threads as i32);
params
}
}
#[derive(Debug, Clone)]
pub struct LlamaBackendConfigBuilder {
config: LlamaBackendConfig,
}
impl LlamaBackendConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self {
config: LlamaBackendConfig::default(),
}
}
#[must_use]
pub fn n_gpu_layers(mut self, n: u32) -> Self {
self.config.n_gpu_layers = n;
self
}
#[must_use]
pub fn offload_all_layers(mut self) -> Self {
self.config.n_gpu_layers = u32::MAX;
self
}
#[must_use]
pub fn context_length(mut self, n: usize) -> Self {
self.config.context_length = n;
self
}
#[must_use]
pub fn batch_size(mut self, n: usize) -> Self {
self.config.batch_size = n;
self
}
#[must_use]
pub fn n_threads(mut self, n: usize) -> Self {
self.config.n_threads = n;
self
}
#[must_use]
pub fn use_mmap(mut self, enabled: bool) -> Self {
self.config.use_mmap = enabled;
self
}
#[must_use]
pub fn use_mlock(mut self, enabled: bool) -> Self {
self.config.use_mlock = enabled;
self
}
#[must_use]
pub fn kv_cache_type(mut self, t: KVCacheType) -> Self {
self.config.kv_cache_type = t;
self
}
#[must_use]
pub fn split_mode(mut self, s: SplitMode) -> Self {
self.config.split_mode = s;
self
}
#[must_use]
pub fn main_gpu(mut self, gpu: u32) -> Self {
self.config.main_gpu = gpu;
self
}
#[must_use]
pub fn speculative(mut self, spec: SpeculativeConfig) -> Self {
self.config.speculative = Some(spec);
self
}
#[must_use]
pub fn mtp(mut self, mtp_model: Option<PathBuf>, n_max: u32) -> Self {
self.config.speculative = Some(SpeculativeConfig::mtp(mtp_model, n_max));
self
}
#[must_use]
pub fn build(self) -> LlamaBackendConfig {
self.config
}
}
impl Default for LlamaBackendConfigBuilder {
fn default() -> Self {
Self::new()
}
}
struct LlamaModelsInner {
model: Arc<LlamaModel>,
context: LlamaModelContextParams,
#[allow(dead_code)]
sampler: Option<LlamaSampler>,
spec: ModelSpec,
pricing: ModelUsageCosting,
cumulative_cost: CostAccumulator,
config: LlamaBackendConfig,
mtp_model: Arc<Mutex<Option<Arc<LlamaMtpModel>>>>,
}
pub struct LlamaModels {
inner: Arc<Mutex<LlamaModelsInner>>,
}
unsafe impl Send for LlamaModels {}
unsafe impl Sync for LlamaModels {}
impl Clone for LlamaModels {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl LlamaModels {
#[allow(clippy::arc_with_non_send_sync)]
fn new_with_config(
model: LlamaModel,
context: LlamaModelContextParams,
spec: ModelSpec,
config: LlamaBackendConfig,
) -> Self {
Self {
inner: Arc::new(Mutex::new(LlamaModelsInner {
model: Arc::new(model),
context,
sampler: None,
spec,
pricing: ModelUsageCosting::default(),
cumulative_cost: CostAccumulator::new(),
config,
mtp_model: Arc::new(Mutex::new(None)),
})),
}
}
fn share_weights(&self) -> Self {
let inner = self.inner.lock().expect("model inner poisoned");
Self {
inner: Arc::new(Mutex::new(LlamaModelsInner {
model: Arc::clone(&inner.model),
context: inner.context.clone(),
sampler: None,
spec: inner.spec.clone(),
pricing: inner.pricing.clone(),
cumulative_cost: CostAccumulator::new(),
config: inner.config.clone(),
mtp_model: Arc::clone(&inner.mtp_model),
})),
}
}
#[must_use]
pub fn spec(&self) -> ModelSpec {
self.inner.lock().unwrap().spec.clone()
}
}
impl Model for LlamaModels {
fn spec(&self) -> ModelSpec {
self.inner.lock().unwrap().spec.clone()
}
fn tool_formatter(&self) -> Box<dyn ToolFormatter> {
Box::new(TextBasedFormatter)
}
fn descriptor(&self) -> Option<ModelProviderDescriptor> {
let inner = self.inner.lock().unwrap();
Some(ModelProviderDescriptor {
id: "llamacpp",
name: "llama.cpp",
reasoning: false,
api: crate::types::base_types::ModelAPI::Custom("llamacpp".into()),
provider: ModelProviders::LLAMACPP,
base_url: None,
inputs: crate::types::base_types::MessageType::TextAndImages,
cost: inner.pricing,
context_window: 0,
max_tokens: 0,
})
}
fn costing(&self) -> GenerationResult<UsageReport> {
let inner = self.inner.lock().unwrap();
let cost = inner.cumulative_cost.result();
Ok(UsageReport {
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: cost.total_tokens,
cost,
})
}
fn generate(
&self,
interaction: ModelInteraction,
specs: Option<ModelParams>,
) -> GenerationResult<Vec<Messages>> {
let backend = LlamaBackend::init_or_get().map_err(Into::<GenerationError>::into)?;
let (model, spec, ctx_params, backend_config, mtp_model_slot) = {
let inner = self.inner.lock().unwrap();
(
Arc::clone(&inner.model),
inner.spec.clone(),
inner.context.clone(),
inner.config.clone(),
Arc::clone(&inner.mtp_model),
)
};
let params = specs.unwrap_or_default();
let is_embedding = is_embedding_request(&interaction.messages);
let (prompt, templated) = if interaction.messages.is_empty() {
(interaction.system_prompt.unwrap_or_default(), false)
} else {
(apply_chat_template(&model, &interaction)?, true)
};
if is_embedding {
let mut ctx = model
.new_context(&backend, ctx_params)
.map_err(Into::<GenerationError>::into)?;
return generate_embeddings(&model, &mut ctx, &prompt, &spec);
}
if let Some(spec_cfg) = &backend_config.speculative {
if let (SpeculativeKind::Mtp, Some(mtp_path)) = (spec_cfg.kind, &spec_cfg.mtp_model) {
match run_mtp_generation(
&mtp_model_slot,
&model,
&prompt,
¶ms,
&backend_config,
spec_cfg,
mtp_path,
) {
Ok(messages) => return Ok(messages),
Err(err) => {
tracing::warn!(
error = %err,
n_max = spec_cfg.n_max,
"MTP speculative generation failed — falling back to standard decoding"
);
}
}
}
}
let mut ctx = model
.new_context(&backend, ctx_params)
.map_err(Into::<GenerationError>::into)?;
let mut sampler = build_sampler_chain(¶ms);
generate_text(&model, &mut ctx, &mut sampler, &prompt, templated, ¶ms, &spec)
}
fn stream(
&self,
interaction: ModelInteraction,
specs: Option<ModelParams>,
) -> GenerationResult<ModelStreamBox> {
let stream = LlamaCppStream::new(self.clone(), &interaction, specs)?;
Ok(Box::new(stream))
}
}
pub struct LlamaCppStream {
inner: Arc<Mutex<LlamaCppStreamInner>>,
}
unsafe impl Send for LlamaCppStreamInner {}
struct MtpStreamState {
engine: LlamaMtp,
prompt: String,
sampling: MtpSampling,
n_predict: i32,
started: bool,
}
struct LlamaCppStreamInner {
model: LlamaModels,
mtp: Option<MtpStreamState>,
backend: Option<LlamaBackend>,
ctx: Option<LlamaModelContext<'static>>,
sampler: Option<LlamaSampler>,
current_pos: i32,
tokens_generated: i32,
max_tokens: i32,
input_tokens: usize,
prompt_evaluated: bool,
finished: bool,
prompt: Option<String>,
prompt_templated: bool,
}
impl LlamaCppStream {
pub fn new(
model: LlamaModels,
interaction: &ModelInteraction,
specs: Option<ModelParams>,
) -> GenerationResult<Self> {
let backend = LlamaBackend::init_or_get().map_err(|e| {
GenerationError::Generic(format!("Failed to initialize llama.cpp backend: {e}"))
})?;
let params = specs.unwrap_or_default();
let sampler = build_sampler_chain(¶ms);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)] let max_tokens = params.max_tokens as i32;
let model_arc = {
let model_inner = model.inner.lock().unwrap();
Arc::clone(&model_inner.model)
};
let (prompt, prompt_templated) = if interaction.messages.is_empty() {
(interaction.system_prompt.clone().unwrap_or_default(), false)
} else {
(apply_chat_template(&model_arc, interaction)?, true)
};
let mtp = build_mtp_stream_state(&model, interaction, ¶ms, max_tokens);
Ok(Self {
inner: Arc::new(Mutex::new(LlamaCppStreamInner {
model,
mtp,
backend: Some(backend),
ctx: None,
sampler: Some(sampler),
current_pos: 0,
tokens_generated: 0,
max_tokens,
input_tokens: 0,
prompt_evaluated: false,
finished: false,
prompt: Some(prompt),
prompt_templated,
})),
})
}
}
fn stream_error(msg: impl std::fmt::Display) -> Stream<Messages, ModelState> {
let msg = msg.to_string();
tracing::error!("llamacpp stream failed: {msg}");
Stream::Pending(ModelState::Error(msg))
}
impl Iterator for LlamaCppStream {
type Item = Stream<Messages, ModelState>;
#[allow(
clippy::too_many_lines,
clippy::single_match_else,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_lossless,
clippy::cast_sign_loss
)] fn next(&mut self) -> Option<Self::Item> {
let mut inner = self.inner.lock().unwrap();
if inner.finished {
return None;
}
if !inner.prompt_evaluated {
inner.prompt_evaluated = true;
return Some(Stream::Init);
}
if inner.mtp.is_some() {
tracing::trace!("mtp_stream_next: mtp is some, driving MTP engine one step");
return mtp_stream_next(&mut inner);
}
if inner.ctx.is_none() {
if inner.backend.is_none() {
let Ok(backend) = LlamaBackend::init_or_get() else {
inner.finished = true;
return Some(stream_error("failed to initialize llama.cpp backend"));
};
inner.backend = Some(backend);
}
let (model, ctx_params) = {
let model_inner = inner.model.inner.lock().unwrap();
(Arc::clone(&model_inner.model), model_inner.context.clone())
};
let backend = inner
.backend
.take()
.expect("backend is Some — ensured immediately above");
let ctx = match model.new_context(&backend, ctx_params) {
Ok(ctx) => {
unsafe {
std::mem::transmute::<LlamaModelContext<'_>, LlamaModelContext<'static>>(
ctx,
)
}
}
Err(err) => {
inner.backend = Some(backend);
inner.finished = true;
return Some(stream_error(format!(
"failed to create llama.cpp inference context: {err}"
)));
}
};
inner.backend = Some(backend);
inner.ctx = Some(ctx);
return Some(Stream::Pending(ModelState::GeneratingTokens(None)));
}
if inner.tokens_generated >= inner.max_tokens {
inner.finished = true;
return Some(Stream::Pending(ModelState::Finished));
}
let inner = &mut *inner;
let Some(ctx) = inner.ctx.as_mut() else {
inner.finished = true;
return Some(stream_error(
"inference context missing after init — cannot generate",
));
};
let model = {
let model_inner = inner.model.inner.lock().unwrap();
Arc::clone(&model_inner.model)
};
tracing::trace!("stream_next: get model for interaction");
if inner.sampler.is_none() {
inner.finished = true;
return Some(stream_error("sampler missing — cannot generate"));
}
if inner.tokens_generated == 0 {
tracing::trace!("stream_next: evaluating token generation count");
let prompt = inner.prompt.take().unwrap_or_default();
let Ok(tokens) = tokenize_prompt(&model, &prompt, inner.prompt_templated) else {
inner.finished = true;
return Some(stream_error("failed to tokenize prompt"));
};
inner.input_tokens = tokens.len();
let mut batch = LlamaBatch::new(tokens.len(), 1);
tracing::trace!("stream_next: evaluating prompt Batch: LlamaBatch");
if let Err(err) = batch.add_sequence(&tokens, 0, true) {
inner.finished = true;
return Some(stream_error(format!("failed to build prompt batch: {err}")));
}
tracing::trace!("stream_next: decoding Batch with ctx");
if let Err(err) = ctx.decode(&mut batch) {
inner.finished = true;
return Some(stream_error(format!("failed to decode prompt batch: {err}")));
}
inner.current_pos = tokens.len() as i32;
}
let Some(sampler) = inner.sampler.as_mut() else {
inner.finished = true;
return Some(stream_error("sampler missing — cannot sample next token"));
};
let next_token = sampler.sample(&ctx, 0);
tracing::trace!("stream_next: generated next token: {:?}", &next_token);
if model.is_eog_token(next_token) {
tracing::trace!("stream_next: is it eog token?: {:?}", &next_token);
inner.finished = true;
return Some(Stream::Pending(ModelState::Finished));
}
#[allow(clippy::manual_unwrap_or_default)] let token_str = match model.token_to_str(next_token, Special::Tokenize) {
Ok(s) => s,
Err(_) => String::new(),
};
tracing::trace!("stream_next: get token str?: {:?}", &token_str);
let mut next_batch = LlamaBatch::new(1, 1);
if let Err(err) = next_batch.add(next_token, inner.current_pos, &[0], true) {
inner.finished = true;
return Some(stream_error(format!(
"failed to add sampled token to batch: {err}"
)));
}
if let Err(err) = ctx.decode(&mut next_batch) {
inner.finished = true;
return Some(stream_error(format!(
"failed to decode sampled token: {err}"
)));
}
inner.tokens_generated += 1;
inner.current_pos += 1;
#[allow(clippy::cast_precision_loss)]
let stream_usage = UsageReport {
input: inner.input_tokens as f64,
output: inner.tokens_generated as f64,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: (inner.input_tokens + inner.tokens_generated as usize) as f64,
cost: UsageCosting {
currency: "USD".to_string(),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: 0.0,
status: CostStatus::Actual,
},
};
let zero_pricing = ModelUsageCosting::default();
let stream_cost = calculate_cost(&zero_pricing, &stream_usage, CostStatus::Actual);
Some(Stream::Next(Messages::Assistant {
id: foundation_compact::ids::new_scru128(),
model: ModelId::Name("llamacpp".to_string(), None),
timestamp: SystemTime::now(),
usage: UsageReport {
cost: stream_cost,
..stream_usage
},
content: ModelOutput::Text(TextContent {
content: token_str,
signature: None,
}),
stop_reason: StopReason::Stop,
provider: ModelProviders::LLAMACPP,
error_detail: None,
signature: None,
metadata: None,
}))
}
}
fn build_stream_assistant(input_tokens: usize, output_tokens: usize, piece: String) -> Messages {
#[allow(clippy::cast_precision_loss)]
let usage = UsageReport {
input: input_tokens as f64,
output: output_tokens as f64,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: (input_tokens + output_tokens) as f64,
cost: UsageCosting {
currency: "USD".to_string(),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: 0.0,
status: CostStatus::Actual,
},
};
let zero_pricing = ModelUsageCosting::default();
let cost = calculate_cost(&zero_pricing, &usage, CostStatus::Actual);
Messages::Assistant {
id: foundation_compact::ids::new_scru128(),
model: ModelId::Name("llamacpp".to_string(), None),
timestamp: SystemTime::now(),
usage: UsageReport { cost, ..usage },
content: ModelOutput::Text(TextContent {
content: piece,
signature: None,
}),
stop_reason: StopReason::Stop,
provider: ModelProviders::LLAMACPP,
error_detail: None,
signature: None,
metadata: None,
}
}
#[allow(clippy::cast_sign_loss)]
fn mtp_stream_next(inner: &mut LlamaCppStreamInner) -> Option<Stream<Messages, ModelState>> {
let need_begin = match inner.mtp.as_ref() {
Some(m) => !m.started,
None => return None,
};
tracing::trace!("mtp_stream_next: need_begin: {:?}", need_begin);
if need_begin {
let res = {
let mtp = inner.mtp.as_ref().unwrap();
mtp.engine.begin(&mtp.prompt, mtp.n_predict, mtp.sampling)
};
return match res {
Ok(n) => {
inner.mtp.as_mut().unwrap().started = true;
inner.input_tokens = n.max(0) as usize;
tracing::trace!("mtp_stream_next: get next token");
Some(Stream::Pending(ModelState::GeneratingTokens(None)))
}
Err(err) => {
inner.finished = true;
Some(stream_error(format!("MTP stream begin failed: {err}")))
}
};
}
tracing::trace!("mtp_stream_next: get next step");
let res = inner.mtp.as_ref().unwrap().engine.step();
match res {
Ok(step) => {
if step.piece.is_empty() {
if step.done {
inner.finished = true;
return Some(Stream::Pending(ModelState::Finished));
}
return Some(Stream::Pending(ModelState::GeneratingTokens(None)));
}
inner.tokens_generated += 1;
let msg = build_stream_assistant(
inner.input_tokens,
inner.tokens_generated as usize,
step.piece,
);
tracing::trace!("mtp_stream_next: get msg: {:?}", &msg);
if step.done {
inner.finished = true;
}
Some(Stream::Next(msg))
}
Err(err) => {
inner.finished = true;
Some(stream_error(format!("MTP stream step failed: {err}")))
}
}
}
fn is_embedding_request(messages: &[Messages]) -> bool {
messages.iter().any(|msg| {
if let Messages::Assistant { content, .. } = msg {
matches!(content, ModelOutput::Embedding { .. })
} else {
false
}
})
}
fn flatten_tools(shed: &ToolShed) -> Vec<crate::types::base_types::Tool> {
shed.all_tools()
}
fn tokenize_prompt(
model: &LlamaModel,
prompt: &str,
templated: bool,
) -> Result<Vec<LlamaToken>, crate::errors::GenerationError> {
if !templated {
return model
.str_to_token(prompt, AddBos::Always)
.map_err(Into::into);
}
let mut tokens = model
.str_to_token(prompt, AddBos::Never)
.map_err(Into::<crate::errors::GenerationError>::into)?;
let bos = model.token_bos();
if tokens.first() != Some(&bos) {
tokens.insert(0, bos);
}
Ok(tokens)
}
fn apply_chat_template(
model: &LlamaModel,
interaction: &ModelInteraction,
) -> GenerationResult<String> {
let mut system_content = String::new();
if let Some(sys) = &interaction.system_prompt {
system_content.push_str(sys);
}
if let Some(soul) = &interaction.soul {
if !system_content.is_empty() {
system_content.push_str("\n\n");
}
system_content.push_str(soul);
}
let shed = &interaction.tools_shed;
{
let all_tools = flatten_tools(shed);
if !all_tools.is_empty() {
if !system_content.is_empty() {
system_content.push_str("\n\n");
}
let formatter = TextBasedFormatter;
if let Some(instructions) = formatter.tool_calling_instructions() {
system_content.push_str(&instructions);
system_content.push_str("\n\nAvailable tools:\n");
} else {
system_content.push_str("Available tools:\n");
}
for tool in &all_tools {
let _ = writeln!(system_content, "- {}({})", tool.name(), tool.arg_summary());
}
}
}
let mut chat_messages: Vec<LlamaChatMessage> = Vec::new();
if !system_content.is_empty() {
if let Ok(msg) = LlamaChatMessage::new("system".to_string(), system_content) {
chat_messages.push(msg);
}
}
for msg in &interaction.messages {
let (role, content_str) = match msg {
Messages::User { content, .. } => {
let text = match content {
UserModelContent::Text(TextContent { content, .. }) => content.clone(),
UserModelContent::Image(_) => String::new(),
};
("user", text)
}
Messages::Assistant { content, .. } => {
let text = match content {
ModelOutput::Text(TextContent { content, .. }) => content.clone(),
ModelOutput::ToolCall {
name, arguments, ..
} => {
let args_str = arguments
.as_ref()
.map(|a| serde_json::to_string(a).unwrap_or_else(|_| "{}".to_string()))
.unwrap_or_default();
format!("[Tool call: {name}({args_str})]")
}
ModelOutput::ThinkingContent { thinking, .. } => thinking.clone(),
ModelOutput::Embedding { .. } | ModelOutput::Image(_) => String::new(),
};
("assistant", text)
}
Messages::ToolResult { content, .. } => {
let text = match content {
UserModelContent::Text(TextContent { content, .. }) => content.clone(),
UserModelContent::Image(_) => String::new(),
};
("tool", text)
}
};
if !content_str.is_empty() {
if let Ok(chat_msg) = LlamaChatMessage::new(role.to_string(), content_str) {
chat_messages.push(chat_msg);
}
}
}
if let Some(custom_template) = &interaction.chat_template {
let template = LlamaChatTemplate::new(custom_template).map_err(|e| {
GenerationError::Generic(format!("Failed to create chat template: {e}"))
})?;
return model
.apply_chat_template(&template, &chat_messages, true)
.map_err(Into::<GenerationError>::into);
}
match model.apply_jinja_chat_template(&chat_messages, true) {
Ok(prompt) => {
tracing::trace!("apply_chat_template (jinja) => {prompt:?}");
Ok(prompt)
}
Err(jinja_err) => {
tracing::debug!(
"Jinja chat template failed ({jinja_err}); falling back to legacy template path"
);
let template = model
.chat_template(None)
.map_err(Into::<GenerationError>::into)?;
model
.apply_chat_template(&template, &chat_messages, true)
.map_err(Into::<GenerationError>::into)
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn generate_embeddings(
model: &LlamaModel,
ctx: &mut LlamaModelContext,
prompt: &str,
_spec: &ModelSpec,
) -> GenerationResult<Vec<Messages>> {
let tokens = model
.str_to_token(prompt, AddBos::Always)
.map_err(Into::<GenerationError>::into)?;
let mut batch = LlamaBatch::new(tokens.len(), 1);
batch
.add_sequence(&tokens, 0, false)
.map_err(|e| GenerationError::Generic(format!("Failed to add tokens to batch: {e}")))?;
ctx.encode(&mut batch)
.map_err(Into::<GenerationError>::into)?;
let embeddings = ctx
.embeddings_seq_ith(0)
.map_err(Into::<GenerationError>::into)?;
let dimensions = embeddings.len();
#[allow(clippy::cast_precision_loss)]
let emb_usage = UsageReport {
input: tokens.len() as f64,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: tokens.len() as f64,
cost: UsageCosting {
currency: "USD".to_string(),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: tokens.len() as f64,
status: CostStatus::Actual,
},
};
let zero_pricing = ModelUsageCosting::default();
let emb_cost = calculate_cost(&zero_pricing, &emb_usage, CostStatus::Actual);
let emb_usage = UsageReport {
cost: emb_cost,
..emb_usage
};
Ok(vec![Messages::Assistant {
id: foundation_compact::ids::new_scru128(),
model: ModelId::Name("llamacpp".to_string(), None),
timestamp: SystemTime::now(),
usage: emb_usage,
content: ModelOutput::Embedding {
dimensions,
values: embeddings.to_vec(),
},
stop_reason: StopReason::Stop,
provider: ModelProviders::LLAMACPP,
error_detail: None,
signature: None,
metadata: None,
}])
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn mtp_sampling_from_params(params: &ModelParams) -> MtpSampling {
MtpSampling {
temperature: params.temperature,
top_k: params.top_k as i32,
top_p: params.top_p,
repeat_penalty: params.repeat_penalty,
seed: params.seed.unwrap_or(0xFFFF_FFFF),
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn build_mtp_stream_state(
model: &LlamaModels,
interaction: &ModelInteraction,
params: &ModelParams,
n_predict: i32,
) -> Option<MtpStreamState> {
let (model_arc, config, mtp_model_slot) = {
let inner = model.inner.lock().unwrap();
(
Arc::clone(&inner.model),
inner.config.clone(),
Arc::clone(&inner.mtp_model),
)
};
let spec_cfg = config.speculative.as_ref()?;
if spec_cfg.kind != SpeculativeKind::Mtp {
return None;
}
let mtp_path = spec_cfg.mtp_model.as_ref()?;
let prompt = if interaction.messages.is_empty() {
interaction.system_prompt.clone().unwrap_or_default()
} else {
match apply_chat_template(&model_arc, interaction) {
Ok(p) => p,
Err(err) => {
tracing::warn!(error = %err, "MTP stream: chat template failed — using standard streaming");
return None;
}
}
};
let draft_model = match get_or_load_mtp_model(&mtp_model_slot, mtp_path) {
Ok(m) => m,
Err(err) => {
tracing::warn!(error = %err, "MTP stream: draft model load failed — using standard streaming");
return None;
}
};
let engine = match build_mtp_engine(&model_arc, draft_model, &config, spec_cfg) {
Ok(engine) => engine,
Err(err) => {
tracing::warn!(error = %err, "MTP stream: engine init failed — using standard streaming");
return None;
}
};
Some(MtpStreamState {
engine,
prompt,
sampling: mtp_sampling_from_params(params),
n_predict,
started: false,
})
}
fn get_or_load_mtp_model(
slot: &Arc<Mutex<Option<Arc<LlamaMtpModel>>>>,
mtp_path: &std::path::Path,
) -> Result<Arc<LlamaMtpModel>, infrastructure_llama_cpp::speculative::MtpError> {
let mut guard = slot.lock().unwrap();
if let Some(model) = guard.as_ref() {
return Ok(Arc::clone(model));
}
let model = Arc::new(LlamaMtpModel::load(mtp_path)?);
*guard = Some(Arc::clone(&model));
Ok(model)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn build_mtp_engine(
model: &LlamaModel,
draft_model: Arc<LlamaMtpModel>,
backend_config: &LlamaBackendConfig,
spec_cfg: &SpeculativeConfig,
) -> Result<LlamaMtp, infrastructure_llama_cpp::speculative::MtpError> {
LlamaMtp::new(
model,
draft_model,
backend_config.context_length as u32,
backend_config.batch_size as u32,
backend_config.n_threads as i32,
spec_cfg.n_max as i32,
)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::cast_precision_loss
)]
fn run_mtp_generation(
mtp_model_slot: &Arc<Mutex<Option<Arc<LlamaMtpModel>>>>,
model: &LlamaModel,
prompt: &str,
params: &ModelParams,
backend_config: &LlamaBackendConfig,
spec_cfg: &SpeculativeConfig,
mtp_path: &std::path::Path,
) -> GenerationResult<Vec<Messages>> {
let sampling = mtp_sampling_from_params(params);
let n_predict = params.max_tokens as i32;
let draft_model = get_or_load_mtp_model(mtp_model_slot, mtp_path)
.map_err(|e| GenerationError::Generic(format!("MTP model load failed: {e}")))?;
let engine = build_mtp_engine(model, draft_model, backend_config, spec_cfg)
.map_err(|e| GenerationError::Generic(format!("MTP init failed: {e}")))?;
let generation = engine
.generate(prompt, n_predict, sampling)
.map_err(|e| GenerationError::Generic(format!("MTP generate failed: {e}")))?;
tracing::info!(
n_prompt_tokens = generation.stats.n_prompt_tokens,
n_generated = generation.stats.n_generated,
n_drafted = generation.stats.n_drafted,
n_accepted = generation.stats.n_accepted,
acceptance_rate = generation.acceptance_rate(),
"MTP speculative decode completed"
);
let input_tokens = f64::from(generation.stats.n_prompt_tokens.max(0));
let output_tokens = f64::from(generation.stats.n_generated.max(0));
let zero_pricing = ModelUsageCosting::default();
let mtp_usage = UsageReport {
input: input_tokens,
output: output_tokens,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: input_tokens + output_tokens,
cost: UsageCosting {
currency: "USD".to_string(),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: 0.0,
status: CostStatus::Actual,
},
};
let mtp_cost = calculate_cost(&zero_pricing, &mtp_usage, CostStatus::Actual);
let mtp_usage = UsageReport {
cost: mtp_cost,
..mtp_usage
};
Ok(vec![Messages::Assistant {
id: foundation_compact::ids::new_scru128(),
model: ModelId::Name("llamacpp".to_string(), None),
timestamp: SystemTime::now(),
usage: mtp_usage,
content: ModelOutput::Text(TextContent {
content: generation.text,
signature: None,
}),
stop_reason: StopReason::Stop,
provider: ModelProviders::LLAMACPP,
error_detail: None,
signature: None,
metadata: None,
}])
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::cast_precision_loss
)]
fn generate_text(
model: &LlamaModel,
ctx: &mut LlamaModelContext,
sampler: &mut LlamaSampler,
prompt: &str,
templated: bool,
params: &ModelParams,
_spec: &ModelSpec,
) -> GenerationResult<Vec<Messages>> {
let tokens = tokenize_prompt(model, prompt, templated)?;
tracing::trace!("generae: prompt={prompt}");
let mut batch = LlamaBatch::new(tokens.len(), 1);
batch
.add_sequence(&tokens, 0, true)
.map_err(|e| GenerationError::Generic(format!("Failed to add tokens to batch: {e}")))?;
ctx.decode(&mut batch)
.map_err(Into::<GenerationError>::into)?;
tracing::trace!("generate: decode prompt");
let max_tokens = params.max_tokens;
let mut generated_tokens: Vec<LlamaToken> = Vec::new();
let mut output_text = String::new();
let start_pos = tokens.len() as i32;
for current_pos in (start_pos..).take(max_tokens) {
tracing::trace!("generate: current pos: {current_pos}");
let next_token = sampler.sample(ctx, 0);
generated_tokens.push(next_token);
if model.is_eog_token(next_token) {
tracing::trace!("generate: eog stopping: {current_pos}");
break;
}
let token_str = model
.token_to_str(next_token, Special::Tokenize)
.map_err(Into::<GenerationError>::into)?;
tracing::trace!("generate: token: {token_str}: {current_pos}");
output_text.push_str(&token_str);
if params
.stop_tokens
.iter()
.any(|seq| output_text.contains(seq))
{
break;
}
let mut batch = LlamaBatch::new(1, 1);
batch
.add(next_token, current_pos, &[0], true)
.map_err(|e| GenerationError::Generic(format!("Failed to add token to batch: {e}")))?;
tracing::trace!("generate: token: adding to next batch");
ctx.decode(&mut batch)
.map_err(Into::<GenerationError>::into)?;
}
let input_tokens = tokens.len() as f64;
let output_tokens = generated_tokens.len() as f64;
tracing::trace!("generate: input tokens: {input_tokens}, output tokens: {output_tokens}");
let zero_pricing = ModelUsageCosting::default();
let txt_usage = UsageReport {
input: input_tokens,
output: output_tokens,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: input_tokens + output_tokens,
cost: UsageCosting {
currency: "USD".to_string(),
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
total_tokens: 0.0,
status: CostStatus::Actual,
},
};
let txt_cost = calculate_cost(&zero_pricing, &txt_usage, CostStatus::Actual);
let txt_usage = UsageReport {
cost: txt_cost,
..txt_usage
};
Ok(vec![Messages::Assistant {
id: foundation_compact::ids::new_scru128(),
model: ModelId::Name("llamacpp".to_string(), None),
timestamp: SystemTime::now(),
usage: txt_usage,
content: ModelOutput::Text(TextContent {
content: output_text,
signature: None,
}),
stop_reason: StopReason::Stop,
provider: ModelProviders::LLAMACPP,
error_detail: None,
signature: None,
metadata: None,
}])
}
#[derive(Debug, Clone, Copy)]
pub enum LlamaBackends {
LLamaCPU,
LLamaGPU,
LLamaMetal,
}
impl ModelProvider for LlamaBackends {
type Config = LlamaBackendConfig;
type Model = LlamaModels;
fn create(self, _config: Option<Self::Config>) -> ModelProviderResult<Self>
where
Self: Sized,
{
let _backend = LlamaBackend::init_or_get()
.map_err(|e| crate::errors::ModelProviderErrors::FailedFetching(Box::new(e)))?;
Ok(self)
}
fn describe(&self) -> ModelProviderResult<crate::types::base_types::ModelProviderDescriptor> {
Ok(crate::types::base_types::ModelProviderDescriptor {
id: "llamacpp",
name: "llama.cpp Local Inference",
reasoning: false,
api: crate::types::base_types::ModelAPI::Custom("llama-cpp".to_string()),
provider: ModelProviders::LLAMACPP,
base_url: None,
inputs: crate::types::base_types::MessageType::Text,
cost: crate::types::base_types::ModelUsageCosting {
input: 0.0,
output: 0.0,
cache_read: 0.0,
cache_write: 0.0,
},
context_window: 4096,
max_tokens: 2048,
})
}
fn get_model(&self, model_id: ModelId) -> ModelProviderResult<Self::Model> {
let model_spec = ModelSpec {
name: format!("{model_id:?}"),
id: model_id.clone(),
devices: None,
model_location: None,
lora_location: None,
};
self.get_model_by_spec(model_spec)
}
fn get_model_by_spec(&self, model_spec: ModelSpec) -> ModelProviderResult<Self::Model> {
self.load_model(model_spec, &LlamaBackendConfig::default())
}
fn get_one(
&self,
model_id: ModelId,
) -> ModelProviderResult<crate::types::base_types::ModelSpec> {
Err(ModelProviderErrors::NotFound(format!(
"Model {model_id:?} not found in registry"
)))
}
fn get_all(
&self,
_model_id: ModelId,
) -> ModelProviderResult<Vec<crate::types::base_types::ModelSpec>> {
Err(ModelProviderErrors::NotFound(
"Model registry not implemented".to_string(),
))
}
}
static LOADED_MODELS: OnceLock<Mutex<std::collections::HashMap<String, LlamaModels>>> =
OnceLock::new();
static LOAD_LOCKS: OnceLock<Mutex<std::collections::HashMap<String, Arc<Mutex<()>>>>> =
OnceLock::new();
impl LlamaBackends {
pub fn load_model(
&self,
model_spec: ModelSpec,
config: &LlamaBackendConfig,
) -> ModelProviderResult<LlamaModels> {
let model_path = model_spec.model_location.as_ref().ok_or_else(|| {
ModelProviderErrors::ModelErrors(ModelErrors::NotFound(
"No model location specified".to_string(),
))
})?;
let cache_key = format!(
"{}|{}|{config:?}",
model_path.display(),
model_spec.name
);
let cache = LOADED_MODELS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
if let Some(cached) = cache
.lock()
.expect("loaded-model cache poisoned")
.get(&cache_key)
{
tracing::debug!(model = %model_spec.name, "load_model: reusing cached model weights");
return Ok(cached.share_weights());
}
let load_guard = {
let locks = LOAD_LOCKS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
let mut locks = locks.lock().expect("load-locks poisoned");
Arc::clone(
locks
.entry(cache_key.clone())
.or_insert_with(|| Arc::new(Mutex::new(()))),
)
};
let _load = load_guard.lock().expect("per-key load lock poisoned");
if let Some(cached) = cache
.lock()
.expect("loaded-model cache poisoned")
.get(&cache_key)
{
tracing::debug!(model = %model_spec.name, "load_model: cached after waiting for concurrent load");
return Ok(cached.share_weights());
}
let backend = LlamaBackend::init_or_get().map_err(|e| {
ModelProviderErrors::ModelErrors(ModelErrors::FailedLoading(Box::new(e)))
})?;
let model_params = config.to_model_params();
tracing::debug!(model = %model_spec.name, "load_model: loading model weights from disk");
let model = LlamaModel::load_from_file(&backend, model_path, &model_params)
.map_err(|e| ModelProviderErrors::ModelErrors(e.into()))?;
if let Some(spec) = &config.speculative {
let has_head = model.supports_mtp() || spec.mtp_model.is_some();
if !has_head {
return Err(ModelProviderErrors::ModelErrors(ModelErrors::NotFound(
format!(
"speculative/MTP decoding requested but model '{}' has no embedded MTP \
head (n_layer_nextn == 0) and no separate mtp_model path was provided",
model_spec.name
),
)));
}
}
let context_params = config.to_context_params();
let loaded =
LlamaModels::new_with_config(model, context_params, model_spec, config.clone());
cache
.lock()
.expect("loaded-model cache poisoned")
.insert(cache_key, loaded.clone());
Ok(loaded)
}
}