gglib_core/ports/process_runner.rs
1//! Process runner trait definition.
2//!
3//! This port defines the interface for managing model server processes.
4//! Implementations handle all process lifecycle details internally.
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10use super::ProcessError;
11use crate::domain::InferenceConfig;
12
13/// Configuration for starting a model server.
14///
15/// This is an intent-based configuration — it expresses what the caller
16/// wants, not how the server should be started. All typed fields are
17/// handled by `build_and_spawn()`; `extra_args` is an escape hatch for
18/// flags not yet promoted to first-class fields.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ServerConfig {
21 /// Database ID of the model to serve.
22 pub model_id: i64,
23 /// Human-readable model name.
24 pub model_name: String,
25 /// Path to the model file.
26 pub model_path: PathBuf,
27 /// Port to listen on (if None, a free port will be assigned).
28 pub port: Option<u16>,
29 /// Base port for allocation when port is None.
30 pub base_port: u16,
31 /// Context size to use (if None, use model default).
32 pub context_size: Option<u64>,
33 /// Number of GPU layers to offload (if None, use default).
34 pub gpu_layers: Option<i32>,
35 /// Enable Jinja templating for chat formats.
36 pub jinja: bool,
37 /// Reasoning format override (e.g., `"deepseek"`, `"none"`).
38 pub reasoning_format: Option<String>,
39 /// Number of MTP draft tokens to speculate ahead (`--spec-draft-n-max`).
40 ///
41 /// `None` means MTP speculative decoding is disabled. When `Some(n)`,
42 /// `--spec-type draft-mtp` and `--spec-draft-n-max n` are passed to
43 /// llama-server. Recommended value: `2` (Unsloth default).
44 pub spec_draft_n_max: Option<u32>,
45 /// Minimum acceptance probability for MTP draft tokens (`--spec-draft-p-min`).
46 ///
47 /// Only meaningful when `spec_draft_n_max` is `Some`. Skipping low-confidence
48 /// draft tokens is especially important on Apple Silicon (Metal) to avoid
49 /// throughput regression. Recommended value: `0.75`.
50 pub spec_draft_p_min: Option<f32>,
51 /// Inference sampling parameters (temperature, `top_p`, etc.).
52 pub inference_config: Option<InferenceConfig>,
53 /// Additional server-specific options (escape hatch).
54 pub extra_args: Vec<String>,
55 /// Directory for llama-server KV cache slot persistence (`--slot-save-path`).
56 ///
57 /// `None` means the disk slot-persistence feature is disabled — no
58 /// `--slot-save-path` flag is passed. Independent of [`Self::cache_ram_mb`]
59 /// / [`Self::cache_reuse`]: llama-server's own host-RAM prompt cache can be
60 /// tuned (or left at its built-in default) regardless of whether disk
61 /// persistence is on.
62 pub slot_save_path: Option<PathBuf>,
63 /// RAM budget in MiB for llama-server's own host-RAM prompt cache
64 /// (`--cache-ram`).
65 ///
66 /// `None` means no explicit flag is passed — llama-server's own built-in
67 /// default (8192 MiB) applies. `Some(n)` passes `--cache-ram n` directly;
68 /// `Some(0)` disables the cache.
69 pub cache_ram_mb: Option<u64>,
70 /// Minimum chunk size in tokens for KV-shift cache reuse past the first
71 /// prefix divergence point (`--cache-reuse`).
72 ///
73 /// `None` means no flag is passed (`--cache-reuse` off, llama-server
74 /// default `0`). `Some(n)` passes `--cache-reuse n`, letting llama-server
75 /// salvage matching KV chunks after an edited/summarized earlier message
76 /// instead of only reusing an unbroken prefix from token 0.
77 pub cache_reuse: Option<u32>,
78 /// K cache element type (`--cache-type-k`). `None` means no flag is
79 /// passed — llama-server's own `f16` default applies.
80 pub cache_type_k: Option<crate::cache_config::KvCacheType>,
81 /// V cache element type (`--cache-type-v`). Same semantics as
82 /// [`Self::cache_type_k`].
83 pub cache_type_v: Option<crate::cache_config::KvCacheType>,
84 /// Whether to lock the model in RAM (`--mlock`). Default: `false`.
85 pub mlock: bool,
86}
87
88impl ServerConfig {
89 /// Create a new server configuration with required fields.
90 #[must_use]
91 pub const fn new(
92 model_id: i64,
93 model_name: String,
94 model_path: PathBuf,
95 base_port: u16,
96 ) -> Self {
97 Self {
98 model_id,
99 model_name,
100 model_path,
101 port: None,
102 base_port,
103 context_size: None,
104 gpu_layers: None,
105 jinja: false,
106 reasoning_format: None,
107 spec_draft_n_max: None,
108 spec_draft_p_min: None,
109 inference_config: None,
110 extra_args: Vec::new(),
111 slot_save_path: None,
112 cache_ram_mb: None,
113 cache_reuse: None,
114 cache_type_k: None,
115 cache_type_v: None,
116 mlock: false,
117 }
118 }
119
120 /// Set the port to listen on.
121 #[must_use]
122 pub const fn with_port(mut self, port: u16) -> Self {
123 self.port = Some(port);
124 self
125 }
126
127 /// Set the context size.
128 #[must_use]
129 pub const fn with_context_size(mut self, size: u64) -> Self {
130 self.context_size = Some(size);
131 self
132 }
133
134 /// Set the number of GPU layers.
135 #[must_use]
136 pub const fn with_gpu_layers(mut self, layers: i32) -> Self {
137 self.gpu_layers = Some(layers);
138 self
139 }
140
141 /// Enable Jinja templating.
142 #[must_use]
143 pub const fn with_jinja(mut self) -> Self {
144 self.jinja = true;
145 self
146 }
147
148 /// Set the reasoning format (e.g., `"deepseek"`, `"none"`).
149 #[must_use]
150 pub fn with_reasoning_format(mut self, format: String) -> Self {
151 self.reasoning_format = Some(format);
152 self
153 }
154
155 /// Enable MTP speculative decoding with the given draft token count.
156 ///
157 /// This causes `--spec-type draft-mtp` and `--spec-draft-n-max n` to be
158 /// passed to llama-server. Call [`Self::with_spec_draft_p_min`] to also
159 /// set the acceptance probability threshold (defaults to 0.75).
160 #[must_use]
161 pub const fn with_spec_draft_n_max(mut self, n: u32) -> Self {
162 self.spec_draft_n_max = Some(n);
163 self
164 }
165
166 /// Set the minimum acceptance probability for MTP draft tokens.
167 ///
168 /// Has no effect unless `spec_draft_n_max` is also set. Recommended
169 /// value is `0.75`; lower values trade quality for speed.
170 #[must_use]
171 pub const fn with_spec_draft_p_min(mut self, p: f32) -> Self {
172 self.spec_draft_p_min = Some(p);
173 self
174 }
175
176 /// Set inference sampling parameters.
177 #[must_use]
178 pub const fn with_inference_config(mut self, config: InferenceConfig) -> Self {
179 self.inference_config = Some(config);
180 self
181 }
182
183 /// Add extra arguments to pass to the server.
184 #[must_use]
185 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
186 self.extra_args = args;
187 self
188 }
189
190 /// Set the KV cache slot-save directory (`--slot-save-path`).
191 ///
192 /// `None` disables the disk slot-persistence feature (no
193 /// `--slot-save-path` flag emitted). Independent of
194 /// [`Self::with_cache_ram_mb`] / [`Self::with_cache_reuse`].
195 #[must_use]
196 pub fn with_slot_save_path(mut self, path: Option<PathBuf>) -> Self {
197 self.slot_save_path = path;
198 self
199 }
200
201 /// Set the RAM budget (in MiB) for llama-server's own host-RAM prompt
202 /// cache (`--cache-ram`). `None` leaves llama-server's built-in default.
203 #[must_use]
204 pub const fn with_cache_ram_mb(mut self, mb: u64) -> Self {
205 self.cache_ram_mb = Some(mb);
206 self
207 }
208
209 /// Set the minimum chunk size (in tokens) for KV-shift cache reuse
210 /// (`--cache-reuse`). `None` leaves the feature off.
211 #[must_use]
212 pub const fn with_cache_reuse(mut self, n: u32) -> Self {
213 self.cache_reuse = Some(n);
214 self
215 }
216
217 /// Set the K cache element type (`--cache-type-k`).
218 #[must_use]
219 pub const fn with_cache_type_k(mut self, t: crate::cache_config::KvCacheType) -> Self {
220 self.cache_type_k = Some(t);
221 self
222 }
223
224 /// Set the V cache element type (`--cache-type-v`).
225 #[must_use]
226 pub const fn with_cache_type_v(mut self, t: crate::cache_config::KvCacheType) -> Self {
227 self.cache_type_v = Some(t);
228 self
229 }
230
231 /// Enable memory lock (`--mlock`).
232 #[must_use]
233 pub const fn with_mlock(mut self) -> Self {
234 self.mlock = true;
235 self
236 }
237}
238
239/// Handle to a running server process.
240///
241/// This is an opaque handle that implementations use to track processes.
242/// It contains enough information to identify and manage the process.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ProcessHandle {
245 /// Database ID of the model being served.
246 pub model_id: i64,
247 /// Human-readable model name.
248 pub model_name: String,
249 /// Process ID (if running on local system).
250 pub pid: Option<u32>,
251 /// Port the server is listening on.
252 pub port: u16,
253 /// Unix timestamp (seconds) when the server was started.
254 pub started_at: u64,
255}
256
257impl ProcessHandle {
258 /// Create a new process handle.
259 #[must_use]
260 pub const fn new(
261 model_id: i64,
262 model_name: String,
263 pid: Option<u32>,
264 port: u16,
265 started_at: u64,
266 ) -> Self {
267 Self {
268 model_id,
269 model_name,
270 pid,
271 port,
272 started_at,
273 }
274 }
275}
276
277/// Process runner for managing model server processes.
278///
279/// This trait abstracts process management for testability and
280/// potential alternative backends (local, remote, containerized).
281///
282/// # Design Rules
283///
284/// - Express **intent**, not implementation detail
285/// - No CLI/Tauri/Axum concerns in signatures
286/// - Must support: mock runner, remote runner, alternative inference backends
287#[async_trait]
288pub trait ProcessRunner: Send + Sync {
289 /// Start a model server with the given configuration.
290 ///
291 /// Returns a handle that can be used to manage the process.
292 async fn start(&self, config: ServerConfig) -> Result<ProcessHandle, ProcessError>;
293
294 /// Stop a running server.
295 ///
296 /// Returns `Err(ProcessError::NotRunning)` if the process isn't running.
297 async fn stop(&self, handle: &ProcessHandle) -> Result<(), ProcessError>;
298}