Skip to main content

rig_llama_cpp/
lib.rs

1//! # rig-llama-cpp
2//!
3//! A [Rig](https://docs.rs/rig-core) provider that runs GGUF models locally
4//! via [llama.cpp](https://github.com/ggml-org/llama.cpp), with optional Vulkan GPU acceleration.
5//!
6//! This crate implements Rig's [`rig_core::completion::CompletionModel`] and [`rig_core::embeddings::EmbeddingModel`] traits
7//! so that any GGUF model can be used as a drop-in replacement for cloud-based providers. It supports:
8//!
9//! - **Completion and streaming** — both one-shot and token-by-token responses.
10//! - **Tool calling** — models with OpenAI-compatible chat templates can invoke tools.
11//! - **Reasoning / thinking** — extended thinking output is forwarded when the model supports it.
12//! - **Configurable sampling** — top-p, top-k, min-p, temperature, presence and repetition penalties.
13//! - **Embeddings** — generate text embeddings using GGUF embedding models.
14//!
15//! # Feature flags
16//!
17//! There is **no default GPU backend** — pick exactly the one that matches
18//! your hardware. With no feature enabled the build is CPU-only.
19//!
20//! GPU backends (forwarded to `llama-cpp-2`):
21//!
22//! - `vulkan` — cross-vendor GPU (recommended on Linux/Windows when CUDA/ROCm aren't set up).
23//! - `cuda` — NVIDIA GPUs with the CUDA toolkit installed.
24//! - `metal` — Apple Silicon / macOS.
25//! - `rocm` — AMD GPUs on Linux with the ROCm toolchain.
26//!
27//! Other:
28//!
29//! - `openmp` — OpenMP CPU threading; orthogonal to the GPU backends and may be combined with any of them.
30//! - `mtmd` — multimodal (vision) inference; required for `Client::from_gguf_with_mmproj` and `ClientBuilder::mmproj`.
31//!
32//! Examples:
33//!
34//! ```text
35//! cargo build --features vulkan
36//! cargo build --features cuda
37//! cargo build --features "vulkan,mtmd"
38//! ```
39//!
40//! Backend support depends on the corresponding `llama-cpp-2` feature and any required
41//! native toolchain or system libraries being available on the host machine.
42//!
43//! # Quick start
44//!
45//! ```rust,no_run
46//! use rig_core::client::CompletionClient;
47//! use rig_core::completion::Prompt;
48//!
49//! # #[tokio::main]
50//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
51//! let client = rig_llama_cpp::Client::builder("path/to/model.gguf")
52//!     .n_ctx(8192)
53//!     .build()?;
54//!
55//! let agent = client
56//!     .agent("local")
57//!     .preamble("You are a helpful assistant.")
58//!     .max_tokens(512)
59//!     .build();
60//!
61//! let response = agent.prompt("Hello!").await?;
62//! println!("{response}");
63//! # Ok(())
64//! # }
65//! ```
66
67mod checkpoint;
68mod client;
69mod embedding;
70mod error;
71#[cfg(feature = "mtmd")]
72mod image;
73mod jinja;
74mod loader;
75mod parsing;
76mod prompt;
77mod request;
78mod sampling;
79mod slot;
80mod types;
81mod worker;
82
83pub use client::{Client, ClientBuilder, Model};
84pub use embedding::{EmbeddingClient, EmbeddingModelHandle};
85pub use error::LoadError;
86pub use types::{
87    CheckpointParams, FitParams, KvCacheParams, KvCacheType, RawResponse, SamplingParams,
88    StreamChunk,
89};
90
91fn env_flag_enabled(name: &str) -> bool {
92    match std::env::var(name) {
93        Ok(value) => matches!(
94            value.trim().to_ascii_lowercase().as_str(),
95            "1" | "true" | "yes" | "on"
96        ),
97        Err(_) => false,
98    }
99}
100
101/// Whether to forward llama.cpp's *C-side* logging to stderr.
102///
103/// This only controls log lines that originate inside the `llama-cpp-2` /
104/// `llama-cpp-sys-2` C++ code (via `printf`-style writes that bypass Rust's
105/// `log` facade). Library-level diagnostics from `rig-llama-cpp` itself go
106/// through the [`log`] crate and are controlled by the consumer's logger
107/// configuration (e.g. `RUST_LOG=rig_llama_cpp=debug`), not this env var.
108fn llama_logs_enabled() -> bool {
109    env_flag_enabled("RIG_LLAMA_CPP_LOGS")
110}
111
112/// Process-wide [`LlamaBackend`] initialised on first use and shared by every
113/// worker (chat + embedding). The underlying llama.cpp backend is a global
114/// singleton — calling `LlamaBackend::init()` twice in the same process
115/// returns `BackendAlreadyInitialized`. Routing all callers through this
116/// helper means a chat client and an embedding client can coexist without
117/// racing on the C-side init flag.
118///
119/// Returns `Ok(&'static LlamaBackend)` once the backend is up; subsequent
120/// calls are cheap (single `OnceLock::get`). On platforms where init can
121/// fail (e.g. no Vulkan device) the error is sticky for the lifetime of
122/// the process — there's no recovering anyway.
123pub(crate) fn shared_backend() -> Result<&'static llama_cpp_2::llama_backend::LlamaBackend, String>
124{
125    use llama_cpp_2::llama_backend::LlamaBackend;
126    use std::sync::{Mutex, OnceLock};
127
128    static BACKEND: OnceLock<LlamaBackend> = OnceLock::new();
129    static INIT_LOCK: Mutex<()> = Mutex::new(());
130
131    if let Some(b) = BACKEND.get() {
132        return Ok(b);
133    }
134    // Serialise concurrent first-time initialisations. The C-side init flag
135    // is process-global so multiple threads racing on `LlamaBackend::init`
136    // will produce `BackendAlreadyInitialized` for the loser even though
137    // they all want the same handle.
138    let _guard = INIT_LOCK.lock().map_err(|e| e.to_string())?;
139    if let Some(b) = BACKEND.get() {
140        return Ok(b);
141    }
142
143    let mut backend = LlamaBackend::init().map_err(|e| format!("Backend init failed: {e}"))?;
144    if !llama_logs_enabled() {
145        backend.void_logs();
146        // NOTE: upstream llama-cpp-2 0.1.146 does not yet expose a way to
147        // silence mtmd's own log stream — when the `mtmd` feature is on,
148        // mmproj init may print to stderr. Track upstream for an mtmd
149        // log-silencing API and re-enable suppression here.
150    }
151    let _ = BACKEND.set(backend);
152    // INVARIANT: we hold `INIT_LOCK` for the duration of this function and
153    // just called `BACKEND.set(backend)`. Any concurrent caller that
154    // reached the second `BACKEND.get().is_some()` check above already
155    // returned, so reaching this line means we are the unique writer and
156    // `BACKEND` is now `Some`. Even if `set()` raced (`Err`-returning),
157    // the "loser" still observes the state filled by the winner — `get()`
158    // is guaranteed to return `Some`.
159    Ok(BACKEND.get().expect("BACKEND set above under INIT_LOCK"))
160}