Skip to main content

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}