Skip to main content

zeph_config/providers/
candle.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Candle local-inference backend configuration.
5//!
6//! Covers the `[llm.candle]` section ([`CandleConfig`]) and the inline per-provider
7//! variant ([`CandleInlineConfig`]) used inside `[[llm.providers]]`, plus the shared
8//! sampling parameters ([`GenerationParams`]) and device/source selectors.
9
10use serde::{Deserialize, Serialize};
11
12fn default_chat_template() -> String {
13    "chatml".into()
14}
15
16fn default_temperature() -> f64 {
17    0.7
18}
19
20fn default_max_tokens() -> usize {
21    2048
22}
23
24fn default_seed() -> u64 {
25    42
26}
27
28fn default_repeat_penalty() -> f32 {
29    1.1
30}
31
32fn default_repeat_last_n() -> usize {
33    64
34}
35/// Model source for the Candle local-inference backend.
36///
37/// Controls whether the model is downloaded from Hugging Face Hub or loaded
38/// from a local filesystem path specified in `local_path`.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
40#[serde(rename_all = "lowercase")]
41pub enum CandleSource {
42    /// Download model weights from Hugging Face Hub using the `repo_id` from
43    /// the provider's `model` field.
44    #[default]
45    Huggingface,
46    /// Load model weights from the local filesystem path in `local_path`.
47    Local,
48}
49
50/// Compute device for the Candle local-inference backend.
51///
52/// Determines which hardware accelerator is used for inference. Feature flags
53/// `candle/metal` and `candle/cuda` must be enabled at compile time for the
54/// corresponding variants to succeed at runtime.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
56#[serde(rename_all = "lowercase")]
57pub enum CandleDevice {
58    /// Run inference on the CPU.
59    #[default]
60    Cpu,
61    /// Run inference on an NVIDIA CUDA GPU (requires `cuda` feature).
62    Cuda,
63    /// Run inference on Apple Silicon via Metal (requires `metal` feature).
64    Metal,
65    /// Auto-detect the best available device: Metal → CUDA → CPU.
66    ///
67    /// Requires the corresponding `metal` or `cuda` feature to be enabled at compile time.
68    /// Falls back to CPU when no GPU features are compiled in.
69    Auto,
70}
71
72/// Configuration for the Candle local-inference backend.
73///
74/// Corresponds to the `[llm.candle]` section in `config.toml`. Used when the
75/// agent runs inference locally via `HuggingFace` Candle rather than a remote API.
76/// For inline provider definitions inside `[[llm.providers]]`, use
77/// [`CandleInlineConfig`] instead.
78#[derive(Debug, Deserialize, Serialize)]
79pub struct CandleConfig {
80    #[serde(default)]
81    pub source: CandleSource,
82    #[serde(default)]
83    pub local_path: String,
84    #[serde(default)]
85    pub filename: Option<String>,
86    #[serde(default = "default_chat_template")]
87    pub chat_template: String,
88    #[serde(default)]
89    pub device: CandleDevice,
90    #[serde(default)]
91    pub embedding_repo: Option<String>,
92    /// Resolved `HuggingFace` Hub API token for authenticated model downloads.
93    ///
94    /// Must be the **token value** — resolved by the caller before constructing this config.
95    #[serde(default)]
96    pub hf_token: Option<String>,
97    #[serde(default)]
98    pub generation: GenerationParams,
99    /// Maximum seconds to wait for each half of a single inference request.
100    ///
101    /// The timeout is applied **twice** per `chat()` call: once for the channel send
102    /// (waiting for a free slot) and once for the oneshot reply (waiting for the worker
103    /// to finish). The effective maximum wall-clock wait per request is therefore
104    /// `2 × inference_timeout_secs`. CPU inference can be slow; 120s is a conservative
105    /// default for large models, giving up to 240s total before an error is returned.
106    /// Values of 0 are silently promoted to 1 at bootstrap.
107    #[serde(default = "default_inference_timeout_secs")]
108    pub inference_timeout_secs: u64,
109}
110
111fn default_inference_timeout_secs() -> u64 {
112    120
113}
114
115/// Sampling / generation parameters for Candle local inference.
116///
117/// Used inside `[llm.candle.generation]` or a `[[llm.providers]]` Candle entry.
118#[derive(Debug, Clone, Deserialize, Serialize)]
119pub struct GenerationParams {
120    /// Sampling temperature. Higher values produce more creative outputs. Default: `0.7`.
121    #[serde(default = "default_temperature")]
122    pub temperature: f64,
123    /// Nucleus sampling threshold. When set, tokens with cumulative probability above
124    /// this value are excluded. Default: `None` (disabled).
125    #[serde(default)]
126    pub top_p: Option<f64>,
127    /// Top-k sampling. When set, only the top-k most probable tokens are considered.
128    /// Default: `None` (disabled).
129    #[serde(default)]
130    pub top_k: Option<usize>,
131    /// Maximum number of tokens to generate per response. Capped at [`MAX_TOKENS_CAP`].
132    /// Default: `2048`.
133    #[serde(default = "default_max_tokens")]
134    pub max_tokens: usize,
135    /// Random seed for reproducible outputs. Default: `42`.
136    #[serde(default = "default_seed")]
137    pub seed: u64,
138    /// Repetition penalty applied during sampling. Default: `1.1`.
139    #[serde(default = "default_repeat_penalty")]
140    pub repeat_penalty: f32,
141    /// Number of last tokens to consider for the repetition penalty window. Default: `64`.
142    #[serde(default = "default_repeat_last_n")]
143    pub repeat_last_n: usize,
144}
145
146/// Hard upper bound on `GenerationParams::max_tokens` to prevent unbounded generation.
147pub const MAX_TOKENS_CAP: usize = 32768;
148
149impl GenerationParams {
150    /// Returns `max_tokens` clamped to [`MAX_TOKENS_CAP`].
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use zeph_config::GenerationParams;
156    ///
157    /// let params = GenerationParams::default();
158    /// assert!(params.capped_max_tokens() <= 32768);
159    /// ```
160    #[must_use]
161    pub fn capped_max_tokens(&self) -> usize {
162        self.max_tokens.min(MAX_TOKENS_CAP)
163    }
164}
165
166impl Default for GenerationParams {
167    fn default() -> Self {
168        Self {
169            temperature: default_temperature(),
170            top_p: None,
171            top_k: None,
172            max_tokens: default_max_tokens(),
173            seed: default_seed(),
174            repeat_penalty: default_repeat_penalty(),
175            repeat_last_n: default_repeat_last_n(),
176        }
177    }
178}
179/// Inline candle config for use inside `ProviderEntry`.
180/// Re-uses the generation params from `CandleConfig`.
181#[derive(Debug, Clone, Deserialize, Serialize)]
182pub struct CandleInlineConfig {
183    #[serde(default)]
184    pub source: CandleSource,
185    #[serde(default)]
186    pub local_path: String,
187    #[serde(default)]
188    pub filename: Option<String>,
189    /// Optional SHA-256 hex digest of the chat model file (GGUF).
190    ///
191    /// When set, the file is verified before loading. Mismatch aborts startup with an error.
192    /// Useful for security-sensitive deployments to detect corruption or tampering.
193    #[serde(default)]
194    pub chat_model_sha256: Option<String>,
195    #[serde(default = "default_chat_template")]
196    pub chat_template: String,
197    #[serde(default)]
198    pub device: CandleDevice,
199    #[serde(default)]
200    pub embedding_repo: Option<String>,
201    /// Optional SHA-256 hex digest of the embedding model safetensors file.
202    ///
203    /// When set, the file is verified before loading. Mismatch aborts startup with an error.
204    #[serde(default)]
205    pub embedding_model_sha256: Option<String>,
206    /// Resolved `HuggingFace` Hub API token for authenticated model downloads.
207    #[serde(default)]
208    pub hf_token: Option<String>,
209    #[serde(default)]
210    pub generation: GenerationParams,
211    /// Maximum wall-clock seconds to wait for a single inference request.
212    ///
213    /// Effective timeout is `2 × inference_timeout_secs` (send + recv each have this budget).
214    /// CPU inference can be slow; 120s is a conservative default. Floored at 1s.
215    #[serde(default = "default_inference_timeout_secs")]
216    pub inference_timeout_secs: u64,
217}
218
219impl Default for CandleInlineConfig {
220    fn default() -> Self {
221        Self {
222            source: CandleSource::default(),
223            local_path: String::new(),
224            filename: None,
225            chat_model_sha256: None,
226            chat_template: default_chat_template(),
227            device: CandleDevice::default(),
228            embedding_repo: None,
229            embedding_model_sha256: None,
230            hf_token: None,
231            generation: GenerationParams::default(),
232            inference_timeout_secs: default_inference_timeout_secs(),
233        }
234    }
235}