Skip to main content

kernel/records/
model_record.rs

1//! The central data model: a `ModelRecord` and the value types it composes.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use crate::records::identifiers::{
9    Capability, ExecutionMode, Modality, ModelState, RunTier, RuntimeId, SourceKind,
10};
11use crate::records::json_value::JsonValue;
12use crate::time::now_millis;
13
14/// Where a model's weights live and how it is identified.
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct ModelSource {
17    /// The kind of store the model came from.
18    pub kind: SourceKind,
19    /// The on-disk path (or endpoint identifier) of the model.
20    pub path: String,
21    /// The Hugging Face / Ollama repository, when applicable.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub repo: Option<String>,
24    /// The revision/ref within the repository, when applicable.
25    #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
26    pub reference: Option<String>,
27}
28
29impl ModelSource {
30    /// Create a source with just a kind and path.
31    pub fn new(kind: SourceKind, path: &str) -> Self {
32        Self {
33            kind,
34            path: path.to_owned(),
35            repo: None,
36            reference: None,
37        }
38    }
39
40    /// The identity string a stable id is derived from: `kind|path|repo`.
41    pub fn identity(&self) -> String {
42        format!(
43            "{}|{}|{}",
44            self.kind.as_str(),
45            self.path,
46            self.repo.as_deref().unwrap_or("")
47        )
48    }
49}
50
51/// How a model was resolved to its runtime.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Resolution {
55    /// Chosen automatically by the runtime auction.
56    Auto,
57    /// Pinned by the user.
58    User,
59    /// Not yet resolved.
60    #[default]
61    Unresolved,
62}
63
64/// The runtime a model resolves to, plus how that choice was made.
65#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
66pub struct RuntimeRef {
67    /// The winning runtime, if resolved.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub id: Option<RuntimeId>,
70    /// How the runtime was chosen.
71    #[serde(default)]
72    pub resolved: Resolution,
73    /// How much support the runtime needs.
74    #[serde(default)]
75    pub tier: RunTier,
76    /// Other runtimes that could also serve this model.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub alternatives: Vec<RuntimeId>,
79    /// When the user confirmed the runtime, epoch millis.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub confirmed_at: Option<i64>,
82}
83
84/// The type of a tunable parameter.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87pub enum ParamType {
88    /// An integer.
89    Int,
90    /// A floating-point number.
91    Float,
92    /// A boolean.
93    Bool,
94    /// A free string.
95    String,
96    /// One of a fixed set of string values.
97    Enum,
98}
99
100/// The schema of one tunable parameter a model exposes.
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct ParamSpec {
103    /// The parameter's key, e.g. `temperature`.
104    pub key: String,
105    /// The parameter's type.
106    #[serde(rename = "type")]
107    pub param_type: ParamType,
108    /// The default value, if any.
109    #[serde(rename = "default", default, skip_serializing_if = "Option::is_none")]
110    pub default_value: Option<JsonValue>,
111    /// The `[min, max]` range for numeric parameters, if any.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub range: Option<Vec<JsonValue>>,
114    /// The allowed values for an enum parameter, if any.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub values: Option<Vec<String>>,
117}
118
119/// A single model the kernel knows about: what it is, where it lives, how it runs,
120/// and how the user has configured it.
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct ModelRecord {
123    /// Stable identity derived from the source (see [`stable_id`]).
124    pub id: String,
125    /// The model's display name.
126    pub name: String,
127    /// Its primary modality.
128    pub modality: Modality,
129    /// Everything it can be asked to do.
130    pub capabilities: Vec<Capability>,
131    /// Where its weights live.
132    pub source: ModelSource,
133    /// The runtime it resolves to.
134    #[serde(default)]
135    pub runtime: RuntimeRef,
136    /// The parameter schema it exposes.
137    #[serde(default)]
138    pub params: Vec<ParamSpec>,
139    /// User-set parameter values.
140    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
141    pub param_values: BTreeMap<String, JsonValue>,
142    /// A user-set system prompt override.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub system_prompt: Option<String>,
145    /// A user-set display alias.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub alias: Option<String>,
148    /// How the runtime delivers output.
149    #[serde(default)]
150    pub execution: ExecutionMode,
151    /// Estimated memory footprint in megabytes.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub footprint_mb: Option<i64>,
154    /// The record's lifecycle state.
155    #[serde(default)]
156    pub state: ModelState,
157    /// When the record was first registered, epoch millis.
158    pub registered_at: i64,
159    /// The primary weight file, when one file dominates.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub primary_weight_path: Option<String>,
162    /// The model's context window, when known.
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub context_length: Option<i64>,
165    /// Whether the model ships a chat template.
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub has_chat_template: Option<bool>,
168    /// Whether the model's chat template supports tool calling, when it could be
169    /// read. `None` means undetermined — treated as capable, gated by a request.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub supports_tools: Option<bool>,
172    /// Stop tokens the model declares.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub stop_tokens: Option<Vec<String>>,
175    /// Whether the model is still downloading.
176    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177    pub downloading: bool,
178    /// A content fingerprint used to follow a model across moves.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub content_fingerprint: Option<String>,
181}
182
183impl ModelRecord {
184    /// Create a record for a model, deriving its stable id from `source` and
185    /// stamping `registered_at` with the current time.
186    pub fn new(
187        name: &str,
188        modality: Modality,
189        capabilities: Vec<Capability>,
190        source: ModelSource,
191    ) -> Self {
192        Self {
193            id: stable_id(&source),
194            name: name.to_owned(),
195            modality,
196            capabilities,
197            source,
198            runtime: RuntimeRef::default(),
199            params: Vec::new(),
200            param_values: BTreeMap::new(),
201            system_prompt: None,
202            alias: None,
203            execution: ExecutionMode::Sync,
204            footprint_mb: None,
205            state: ModelState::Unresolved,
206            registered_at: now_millis(),
207            primary_weight_path: None,
208            context_length: None,
209            has_chat_template: None,
210            supports_tools: None,
211            stop_tokens: None,
212            downloading: false,
213            content_fingerprint: None,
214        }
215    }
216
217    /// The name to show the user: the alias if set, otherwise the name.
218    pub fn display_name(&self) -> &str {
219        match &self.alias {
220            Some(alias) if !alias.is_empty() => alias,
221            _ => &self.name,
222        }
223    }
224
225    /// The id this model has on the wire: what the gateway's model listings
226    /// advertise and what clients send back. One definition, so a launcher that
227    /// configures a client and the listing it will call cannot disagree. Same
228    /// rule as [`display_name`](Self::display_name), including the empty-alias
229    /// guard.
230    pub fn wire_id(&self) -> &str {
231        self.display_name()
232    }
233
234    /// The recorded footprint in bytes, when one is recorded and positive.
235    pub fn footprint_bytes(&self) -> Option<i64> {
236        self.footprint_mb
237            .filter(|mb| *mb > 0)
238            .map(|mb| mb * crate::records::byte_format::BYTES_PER_MIB)
239    }
240
241    /// Whether the model can perform `capability`.
242    pub fn can(&self, capability: &Capability) -> bool {
243        self.capabilities.contains(capability)
244    }
245}
246
247/// The stable identity of a model: the first eight bytes of the SHA-256 of the
248/// source's `(kind, path, repo)` fields, hex-encoded. The fields are hashed with
249/// a length prefix so the mapping is injective — a `|` (or any byte) inside a
250/// path or repo cannot make two distinct sources collide. Two sources with the
251/// same kind, path, and repo map to the same id.
252pub fn stable_id(source: &ModelSource) -> String {
253    let mut hasher = Sha256::new();
254    for field in [
255        source.kind.as_str(),
256        source.path.as_str(),
257        source.repo.as_deref().unwrap_or(""),
258    ] {
259        hasher.update((field.len() as u64).to_le_bytes());
260        hasher.update(field.as_bytes());
261    }
262    let digest = hasher.finalize();
263    hex::encode(&digest[..8])
264}