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}