pub mod chat_completion;
pub mod completion;
pub mod embedding;
pub mod hf_tokenizer;
pub mod infill;
pub mod openai_compat;
pub mod rerank;
pub mod tokenizer;
#[cfg(feature = "hf-tokenizer")]
#[cfg_attr(docsrs, doc(cfg(feature = "hf-tokenizer")))]
pub use self::hf_tokenizer::HfTokenizer;
pub use self::tokenizer::{FimTokens, LlamaTokenizer, Tokenizer};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use crate::backend::LlamaBackend;
use crate::context::{LlamaContext, LlamaContextParams};
use crate::error::Result;
use crate::hf::downloader::HfDownloader;
use crate::hf::repo::HfRepo;
use crate::model::params::LlamaModelParams;
use crate::model::LlamaModel;
pub use self::chat_completion::{
create_chat_completion, create_chat_completion_stream, create_chat_completion_stream_with,
ChatMessage,
};
pub use self::completion::{
create_completion, create_completion_stream, create_completion_stream_with_sampler,
create_completion_with_options, create_completion_with_sampler, Completion, CompletionChunk,
CompletionLogprobs, CompletionOptions, SamplingOptions, StopReason, StreamControl,
TokenLogprob,
};
#[derive(Debug)]
pub struct Llama {
context: LlamaContext,
model: Box<LlamaModel>,
_backend: LlamaBackend,
_not_send_sync: std::marker::PhantomData<*mut ()>,
}
impl Llama {
pub fn load(params: LlamaParams) -> Result<Self> {
let backend = LlamaBackend::init()?;
let downloader: Arc<dyn HfDownloader> = match params.hf_downloader.clone() {
Some(d) => d,
None => crate::hf::downloader::default_downloader()?,
};
let resolved_path = crate::hf::source::resolve(
¶ms.model_path,
params.hf_filename.as_deref(),
params.hf_repo_override.as_ref(),
downloader.as_ref(),
)?;
let model = Box::new(LlamaModel::load_from_file(
&backend,
&resolved_path,
¶ms.model,
)?);
let ctx = model.new_context(&backend, params.context.clone())?;
Ok(Self {
context: ctx,
model,
_backend: backend,
_not_send_sync: std::marker::PhantomData,
})
}
#[must_use]
pub fn model(&self) -> &LlamaModel {
&self.model
}
#[must_use]
pub fn context(&mut self) -> &mut LlamaContext {
&mut self.context
}
pub fn create_completion(&mut self, prompt: &str, max_tokens: usize) -> Result<Completion> {
create_completion(self, prompt, max_tokens)
}
pub fn create_completion_with_options(
&mut self,
prompt: &str,
options: CompletionOptions,
) -> Result<Completion> {
create_completion_with_options(self, prompt, options)
}
pub fn create_completion_with_sampler(
&mut self,
prompt: &str,
options: CompletionOptions,
sampler: &mut crate::sampling::LlamaSampler,
) -> Result<Completion> {
create_completion_with_sampler(self, prompt, options, sampler)
}
pub fn create_completion_stream<F>(
&mut self,
prompt: &str,
options: CompletionOptions,
on_chunk: F,
) -> Result<Completion>
where
F: FnMut(CompletionChunk) -> StreamControl,
{
create_completion_stream(self, prompt, options, on_chunk)
}
pub fn create_completion_stream_with_sampler<F>(
&mut self,
prompt: &str,
options: CompletionOptions,
sampler: &mut crate::sampling::LlamaSampler,
on_chunk: F,
) -> Result<Completion>
where
F: FnMut(CompletionChunk) -> StreamControl,
{
create_completion_stream_with_sampler(self, prompt, options, sampler, on_chunk)
}
pub fn create_chat_completion(
&mut self,
messages: &[ChatMessage],
max_tokens: usize,
) -> Result<ChatMessage> {
create_chat_completion(self, messages, max_tokens)
}
pub fn create_chat_completion_stream<F>(
&mut self,
messages: &[ChatMessage],
max_tokens: usize,
on_chunk: F,
) -> Result<ChatMessage>
where
F: FnMut(CompletionChunk) -> StreamControl,
{
create_chat_completion_stream(self, messages, max_tokens, on_chunk)
}
pub fn create_chat_completion_stream_with<F>(
&mut self,
messages: &[ChatMessage],
template: crate::chat::BuiltinTemplate,
tools: &[crate::chat::ToolDefinition],
options: CompletionOptions,
on_chunk: F,
) -> Result<ChatMessage>
where
F: FnMut(CompletionChunk) -> StreamControl,
{
create_chat_completion_stream_with(self, messages, template, tools, options, on_chunk)
}
}
#[derive(Clone)]
pub struct LlamaParams {
pub model_path: PathBuf,
pub model: LlamaModelParams,
pub context: LlamaContextParams,
hf_filename: Option<String>,
hf_revision: Option<String>,
hf_token: Option<String>,
hf_cache_dir: Option<PathBuf>,
hf_endpoint: Option<String>,
hf_repo_override: Option<HfRepo>,
hf_downloader: Option<Arc<dyn HfDownloader>>,
}
impl std::fmt::Debug for LlamaParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LlamaParams")
.field("model_path", &self.model_path)
.field("model", &self.model)
.field("context", &self.context)
.field("hf_filename", &self.hf_filename)
.field("hf_revision", &self.hf_revision)
.field("hf_token", &self.hf_token)
.field("hf_cache_dir", &self.hf_cache_dir)
.field("hf_endpoint", &self.hf_endpoint)
.field("hf_repo_override", &self.hf_repo_override)
.field(
"hf_downloader",
&self.hf_downloader.as_ref().map(|_| "<HfDownloader>"),
)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MobilePreset {
LowRam,
Balanced,
GpuMax,
}
impl FromStr for MobilePreset {
type Err = String;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
match value {
"low-ram" | "low_ram" | "lowram" => Ok(Self::LowRam),
"balanced" => Ok(Self::Balanced),
"gpu-max" | "gpu_max" | "gpumax" => Ok(Self::GpuMax),
other => Err(format!(
"unknown mobile preset: {other} (expected low-ram, balanced, or gpu-max)"
)),
}
}
}
impl LlamaParams {
#[must_use]
pub fn new(model_path: impl AsRef<Path>) -> Self {
Self {
model_path: model_path.as_ref().to_path_buf(),
model: LlamaModelParams::default(),
context: LlamaContextParams::default(),
hf_filename: None,
hf_revision: None,
hf_token: None,
hf_cache_dir: None,
hf_endpoint: None,
hf_repo_override: None,
hf_downloader: None,
}
}
#[must_use]
pub fn with_model_path(mut self, p: impl AsRef<Path>) -> Self {
self.model_path = p.as_ref().to_path_buf();
self
}
#[must_use]
pub fn with_n_gpu_layers(mut self, n: i32) -> Self {
self.model = self.model.with_n_gpu_layers(n);
self
}
#[must_use]
pub fn with_use_mmap(mut self, yes: bool) -> Self {
self.model = self.model.with_use_mmap(yes);
self
}
#[must_use]
pub fn with_n_ctx(mut self, n: u32) -> Self {
self.context = self.context.with_n_ctx(n);
self
}
#[must_use]
pub fn with_n_batch(mut self, n: u32) -> Self {
self.context = self.context.with_n_batch(n);
self
}
#[must_use]
pub fn with_n_ubatch(mut self, n: u32) -> Self {
self.context = self.context.with_n_ubatch(n);
self
}
#[must_use]
pub fn with_embeddings(mut self, yes: bool) -> Self {
self.context = self.context.with_embeddings(yes);
self
}
#[must_use]
pub fn with_n_threads(mut self, n: i32) -> Self {
self.context = self.context.with_n_threads(n);
self
}
#[must_use]
pub fn with_n_threads_batch(mut self, n: i32) -> Self {
self.context = self.context.with_n_threads_batch(n);
self
}
#[must_use]
pub fn with_offload_kqv(mut self, yes: bool) -> Self {
self.context = self.context.with_offload_kqv(yes);
self
}
#[must_use]
pub fn with_flash_attn(mut self, yes: bool) -> Self {
self.context = self.context.with_flash_attn(yes);
self
}
#[must_use]
pub fn with_pooling_type(mut self, p: crate::context::params::PoolingType) -> Self {
self.context = self.context.with_pooling_type(p);
self
}
#[must_use]
pub fn with_hf_filename(mut self, filename: impl Into<String>) -> Self {
self.hf_filename = Some(filename.into());
if let Some(s) = self.model_path.to_str() {
if HfRepo::looks_like_repo_id(s) {
if let Ok(repo) = HfRepo::new(s) {
self.hf_repo_override = Some(repo);
}
}
}
self
}
#[must_use]
pub fn with_hf_revision(mut self, revision: impl Into<String>) -> Self {
self.hf_revision = Some(revision.into());
self
}
#[must_use]
pub fn with_hf_token(mut self, token: impl Into<String>) -> Self {
self.hf_token = Some(token.into());
self
}
#[must_use]
pub fn with_hf_cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.hf_cache_dir = Some(dir.into());
self
}
#[must_use]
pub fn with_hf_endpoint(mut self, ep: impl Into<String>) -> Self {
self.hf_endpoint = Some(ep.into());
self
}
#[must_use]
pub fn with_mobile_preset(self, preset: MobilePreset) -> Self {
match preset {
MobilePreset::LowRam => self
.with_n_ctx(2048)
.with_n_batch(128)
.with_n_ubatch(128)
.with_n_threads(4)
.with_n_threads_batch(4)
.with_n_gpu_layers(0)
.with_flash_attn(false)
.with_use_mmap(true),
MobilePreset::Balanced => self
.with_n_ctx(4096)
.with_n_batch(512)
.with_n_ubatch(256)
.with_n_threads(4)
.with_n_threads_batch(4)
.with_n_gpu_layers(32)
.with_flash_attn(true)
.with_use_mmap(true),
MobilePreset::GpuMax => self
.with_n_ctx(4096)
.with_n_batch(1024)
.with_n_ubatch(512)
.with_n_gpu_layers(99)
.with_flash_attn(true)
.with_offload_kqv(true)
.with_use_mmap(true),
}
}
}
impl Default for LlamaParams {
fn default() -> Self {
Self {
model_path: PathBuf::new(),
model: LlamaModelParams::default(),
context: LlamaContextParams::default(),
hf_filename: None,
hf_revision: None,
hf_token: None,
hf_cache_dir: None,
hf_endpoint: None,
hf_repo_override: None,
hf_downloader: None,
}
}
}
#[doc(inline)]
pub use StopReason as _StopReasonShim;
#[cfg(test)]
mod tests {
use super::{HfDownloader, Llama, LlamaParams, MobilePreset};
use crate::error::LlamaError;
use crate::hf::downloader::MockHfDownloader;
use std::path::PathBuf;
use std::sync::Arc;
#[test]
fn mobile_preset_can_be_overridden() {
let params = LlamaParams::new("model.gguf")
.with_mobile_preset(MobilePreset::Balanced)
.with_n_ctx(1024)
.with_n_gpu_layers(0);
assert_eq!(params.context.build().n_ctx, 1024);
assert_eq!(params.model.n_gpu_layers(), 0);
}
#[test]
fn mobile_preset_parse_accepts_cli_names() {
assert_eq!("low-ram".parse(), Ok(MobilePreset::LowRam));
assert_eq!("balanced".parse(), Ok(MobilePreset::Balanced));
assert_eq!("gpu-max".parse(), Ok(MobilePreset::GpuMax));
assert!("fast".parse::<MobilePreset>().is_err());
}
#[test]
fn with_hf_filename_sets_field() {
let p = LlamaParams::new("foo.gguf").with_hf_filename("model.Q4_K_M.gguf");
assert_eq!(p.hf_filename.as_deref(), Some("model.Q4_K_M.gguf"));
}
#[test]
fn with_hf_revision_sets_field() {
let p = LlamaParams::new("foo.gguf").with_hf_revision("refs/pr/42");
assert_eq!(p.hf_revision.as_deref(), Some("refs/pr/42"));
}
#[test]
fn with_hf_token_sets_field() {
let p = LlamaParams::new("foo.gguf").with_hf_token("hf_secret");
assert_eq!(p.hf_token.as_deref(), Some("hf_secret"));
}
#[test]
fn with_hf_cache_dir_sets_field() {
let p =
LlamaParams::new("foo.gguf").with_hf_cache_dir(std::path::PathBuf::from("/tmp/cache"));
assert_eq!(
p.hf_cache_dir.as_deref(),
Some(std::path::Path::new("/tmp/cache"))
);
}
#[test]
fn with_hf_endpoint_sets_field() {
let p = LlamaParams::new("foo.gguf").with_hf_endpoint("https://hf-mirror.com");
assert_eq!(p.hf_endpoint.as_deref(), Some("https://hf-mirror.com"));
}
#[test]
fn load_with_existing_local_path_does_not_invoke_downloader() {
let tmp = tempfile::NamedTempFile::new().expect("create temp file");
std::fs::write(tmp.path(), b"GGUF\x00\x00\x00\x03not-a-real-gguf").expect("write blob");
let local_path = tmp.path().to_path_buf();
let mock = MockHfDownloader::default()
.with_next_error(LlamaError::ModelDownload("DOWNLOADER_INVOKED".into()));
let mut params = LlamaParams::new(&local_path);
params.hf_downloader = Some(Arc::new(mock));
let err = Llama::load(params)
.err()
.expect("load must fail (blob is not a real GGUF)");
let msg = format!("{err}");
assert!(
!msg.contains("DOWNLOADER_INVOKED"),
"downloader must NOT have been invoked for an existing local file, got: {msg}"
);
}
#[test]
fn load_with_hf_repo_invokes_mock_downloader() {
let model_path = PathBuf::from("TheBloke/LoadTestRepo");
assert!(
!model_path.exists(),
"test fixture leaked: {} exists on disk",
model_path.display()
);
let mock = MockHfDownloader::default()
.with_next_error(LlamaError::ModelDownload("HF_DISPATCH_PROOF".into()));
let mut params = LlamaParams::new(&model_path).with_hf_filename("foo.gguf");
params.hf_downloader = Some(Arc::new(mock));
let err = Llama::load(params)
.err()
.expect("load must surface the downloader error");
let msg = format!("{err}");
assert!(
msg.contains("HF_DISPATCH_PROOF"),
"resolver must have dispatched to the mock downloader, got: {msg}"
);
}
#[cfg(not(feature = "hf-hub"))]
#[test]
fn load_with_hf_repo_and_feature_off_returns_runtime_error() {
let model_path = PathBuf::from("org/repo");
assert!(
!model_path.exists(),
"test fixture leaked: {} exists on disk",
model_path.display()
);
let params = LlamaParams::new(&model_path).with_hf_filename("foo.gguf");
let err = Llama::load(params)
.err()
.expect("load must fail under --no-default-features");
let msg = format!("{err}");
assert!(
msg.contains("hf-hub feature is disabled"),
"error must point at the build flag, got: {msg}"
);
}
}