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")]
171 pub supports_tools: Option<bool>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub stop_tokens: Option<Vec<String>>,
175 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177 pub downloading: bool,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub content_fingerprint: Option<String>,
181}
182
183impl ModelRecord {
184 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 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 pub fn wire_id(&self) -> &str {
231 self.display_name()
232 }
233
234 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 pub fn can(&self, capability: &Capability) -> bool {
243 self.capabilities.contains(capability)
244 }
245}
246
247pub 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}