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    /// Stop tokens the model declares.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub stop_tokens: Option<Vec<String>>,
171    /// Whether the model is still downloading.
172    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
173    pub downloading: bool,
174    /// A content fingerprint used to follow a model across moves.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub content_fingerprint: Option<String>,
177}
178
179impl ModelRecord {
180    /// Create a record for a model, deriving its stable id from `source` and
181    /// stamping `registered_at` with the current time.
182    pub fn new(
183        name: &str,
184        modality: Modality,
185        capabilities: Vec<Capability>,
186        source: ModelSource,
187    ) -> Self {
188        Self {
189            id: stable_id(&source),
190            name: name.to_owned(),
191            modality,
192            capabilities,
193            source,
194            runtime: RuntimeRef::default(),
195            params: Vec::new(),
196            param_values: BTreeMap::new(),
197            system_prompt: None,
198            alias: None,
199            execution: ExecutionMode::Sync,
200            footprint_mb: None,
201            state: ModelState::Unresolved,
202            registered_at: now_millis(),
203            primary_weight_path: None,
204            context_length: None,
205            has_chat_template: None,
206            stop_tokens: None,
207            downloading: false,
208            content_fingerprint: None,
209        }
210    }
211
212    /// The name to show the user: the alias if set, otherwise the name.
213    pub fn display_name(&self) -> &str {
214        match &self.alias {
215            Some(alias) if !alias.is_empty() => alias,
216            _ => &self.name,
217        }
218    }
219
220    /// Whether the model can perform `capability`.
221    pub fn can(&self, capability: &Capability) -> bool {
222        self.capabilities.contains(capability)
223    }
224}
225
226/// The stable identity of a model: the first eight bytes of the SHA-256 of the
227/// source's `(kind, path, repo)` fields, hex-encoded. The fields are hashed with
228/// a length prefix so the mapping is injective — a `|` (or any byte) inside a
229/// path or repo cannot make two distinct sources collide. Two sources with the
230/// same kind, path, and repo map to the same id.
231pub fn stable_id(source: &ModelSource) -> String {
232    let mut hasher = Sha256::new();
233    for field in [
234        source.kind.as_str(),
235        source.path.as_str(),
236        source.repo.as_deref().unwrap_or(""),
237    ] {
238        hasher.update((field.len() as u64).to_le_bytes());
239        hasher.update(field.as_bytes());
240    }
241    let digest = hasher.finalize();
242    hex::encode(&digest[..8])
243}