1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct ModelSource {
17 pub kind: SourceKind,
19 pub path: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub repo: Option<String>,
24 #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
26 pub reference: Option<String>,
27}
28
29impl ModelSource {
30 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Resolution {
55 Auto,
57 User,
59 #[default]
61 Unresolved,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
66pub struct RuntimeRef {
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub id: Option<RuntimeId>,
70 #[serde(default)]
72 pub resolved: Resolution,
73 #[serde(default)]
75 pub tier: RunTier,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub alternatives: Vec<RuntimeId>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub confirmed_at: Option<i64>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87pub enum ParamType {
88 Int,
90 Float,
92 Bool,
94 String,
96 Enum,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct ParamSpec {
103 pub key: String,
105 #[serde(rename = "type")]
107 pub param_type: ParamType,
108 #[serde(rename = "default", default, skip_serializing_if = "Option::is_none")]
110 pub default_value: Option<JsonValue>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub range: Option<Vec<JsonValue>>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub values: Option<Vec<String>>,
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct ModelRecord {
123 pub id: String,
125 pub name: String,
127 pub modality: Modality,
129 pub capabilities: Vec<Capability>,
131 pub source: ModelSource,
133 #[serde(default)]
135 pub runtime: RuntimeRef,
136 #[serde(default)]
138 pub params: Vec<ParamSpec>,
139 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
141 pub param_values: BTreeMap<String, JsonValue>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub system_prompt: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub alias: Option<String>,
148 #[serde(default)]
150 pub execution: ExecutionMode,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub footprint_mb: Option<i64>,
154 #[serde(default)]
156 pub state: ModelState,
157 pub registered_at: i64,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub primary_weight_path: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub context_length: Option<i64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub has_chat_template: Option<bool>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub stop_tokens: Option<Vec<String>>,
171 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
173 pub downloading: bool,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub content_fingerprint: Option<String>,
177}
178
179impl ModelRecord {
180 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 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 pub fn can(&self, capability: &Capability) -> bool {
222 self.capabilities.contains(capability)
223 }
224}
225
226pub 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}