1use std::path::PathBuf;
25
26pub mod bundled {
31 pub const HEADS: &[u8] = include_bytes!("../assets/heads.safetensors");
36
37 pub fn have_heads() -> bool {
39 HEADS.len() > 1024
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum Source {
54 Bundled,
56 Ours { repo: &'static str, rev: &'static str },
58 Upstream { repo: &'static str, license: &'static str, rev: &'static str },
60 Mirrored {
63 ours: &'static str,
64 upstream: &'static str,
65 license: &'static str,
66 rev: &'static str,
67 },
68}
69
70impl Source {
71 pub fn license(&self) -> Option<&'static str> {
73 match self {
74 Source::Bundled | Source::Ours { .. } => None,
75 Source::Upstream { license, .. } | Source::Mirrored { license, .. } => Some(license),
76 }
77 }
78 pub fn attribution(&self) -> Option<&'static str> {
80 match self {
81 Source::Bundled | Source::Ours { .. } => None,
82 Source::Upstream { repo, .. } => Some(repo),
83 Source::Mirrored { upstream, .. } => Some(upstream),
84 }
85 }
86 pub fn revision(&self) -> &'static str {
88 match self {
89 Source::Bundled => "bundled",
90 Source::Ours { rev, .. } | Source::Upstream { rev, .. } | Source::Mirrored { rev, .. } => rev,
91 }
92 }
93 pub fn fetch_repo(&self) -> Option<&'static str> {
95 match self {
96 Source::Bundled => None,
97 Source::Ours { repo, .. } => Some(repo),
98 Source::Upstream { repo, .. } => Some(repo),
99 Source::Mirrored { ours, .. } => Some(ours),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct Artifact {
107 pub name: &'static str,
109 pub env: &'static str,
111 pub dir: &'static str,
113 pub marker: &'static str,
115 pub source: Source,
116 pub approx_mb: u32,
117 pub purpose: &'static str,
118}
119
120pub const ARTIFACTS: &[Artifact] = &[
122 Artifact {
123 name: "embeddings",
124 env: "STEELDB_EMBEDDINGS",
125 dir: "bert-tiny",
126 marker: "model.safetensors",
127 source: Source::Mirrored {
128 ours: "cp500/steeldb-models",
129 upstream: "google/bert_uncased_L-2_H-128_A-2",
130 license: "Apache-2.0",
131 rev: "main",
132 },
133 approx_mb: 16,
134 purpose: "the embedding table the bundled heads sit on; 15.2 MB of it is the vocabulary alone",
135 },
136 Artifact {
137 name: "spo-tagger",
138 env: "STEELDB_ML_BUNDLE",
139 dir: "step0_bundle_ml",
140 marker: "spo.onnx",
141 source: Source::Ours { repo: "cp500/steeldb-models", rev: "main" },
142 approx_mb: 168,
143 purpose: "typed span tagger: text becomes ENT/REL/GEO/TIME/QTY spans",
144 },
145 Artifact {
146 name: "splade",
147 env: "STEELDB_SPLADE_DIR",
148 dir: "splade",
149 marker: "splade.onnx",
150 source: Source::Ours { repo: "cp500/steeldb-models", rev: "main" },
151 approx_mb: 107,
152 purpose: "learned facet heads: the projection that builds the bitmap",
153 },
154 Artifact {
155 name: "model2vec",
156 env: "STEELDB_MODEL2VEC",
157 dir: "model2vec",
158 marker: "potion.f32",
159 source: Source::Mirrored {
160 ours: "cp500/steeldb-models",
161 upstream: "minishlab/potion-base-8M",
162 license: "MIT",
163 rev: "main",
164 },
165 approx_mb: 15,
166 purpose: "static embeddings for optimal-transport ontology discovery",
167 },
168 Artifact {
169 name: "needle3",
170 env: "STEELDB_NEEDLE_DIR",
171 dir: "needle3",
172 marker: "needle3.cact",
173 source: Source::Mirrored {
176 ours: "cp500/steeldb-models",
177 upstream: "Cactus-Compute/needle3",
178 license: "Apache-2.0",
179 rev: "b274efcb211a9eef48c9a88da4b43bd569696a39",
180 },
181 approx_mb: 242,
182 purpose: "121M tool-calling model used as the query planner",
183 },
184];
185
186#[derive(Debug, Clone)]
188pub enum ModelError {
189 Unknown(String),
191 NotFound { name: String, searched: Vec<PathBuf>, hint: String },
193}
194
195impl std::fmt::Display for ModelError {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 ModelError::Unknown(n) => write!(f, "unknown model '{n}'"),
199 ModelError::NotFound { name, searched, hint } => {
200 writeln!(f, "model '{name}' not found. Looked in:")?;
201 for p in searched {
202 writeln!(f, " {}", p.display())?;
203 }
204 write!(f, "{hint}")
205 }
206 }
207 }
208}
209
210impl std::error::Error for ModelError {}
211
212pub fn artifact(name: &str) -> Option<&'static Artifact> {
214 ARTIFACTS.iter().find(|a| a.name == name)
215}
216
217pub fn search_roots() -> Vec<PathBuf> {
222 let mut roots = Vec::new();
223 if let Ok(home) = std::env::var("HOME") {
224 roots.push(PathBuf::from(home).join(".steeldb").join("models"));
225 }
226 if let Ok(dir) = std::env::var("STEELDB_MODELS") {
227 roots.insert(0, PathBuf::from(dir));
228 }
229 roots.push(PathBuf::from("models"));
230 roots
231}
232
233pub fn resolve(name: &str) -> Result<PathBuf, ModelError> {
238 let art = artifact(name).ok_or_else(|| ModelError::Unknown(name.to_string()))?;
239
240 if let Ok(dir) = std::env::var(art.env) {
241 let p = PathBuf::from(dir);
242 if p.join(art.marker).exists() {
243 return Ok(p);
244 }
245 }
246
247 let mut searched = Vec::new();
248 for root in search_roots() {
249 let cand = root.join(art.dir);
250 if cand.join(art.marker).exists() {
251 return Ok(cand);
252 }
253 searched.push(cand);
254 }
255
256 Err(ModelError::NotFound { name: name.to_string(), searched, hint: fetch_hint(art) })
257}
258
259pub fn fetch_hint(art: &Artifact) -> String {
261 let dest = search_roots()
262 .first()
263 .map(|r| r.join(art.dir))
264 .unwrap_or_else(|| PathBuf::from("models").join(art.dir));
265 match art.source.fetch_repo() {
266 None => "this artifact ships with the crate; the build is incomplete".to_string(),
267 Some(repo) => {
268 let credit = match art.source.attribution() {
269 Some(up) => format!("\n {up} — {}", art.source.license().unwrap_or("see model card")),
270 None => String::new(),
271 };
272 format!(
273 "Fetch it ({} MB) with:\n \
274 huggingface-cli download {repo} --revision {} --local-dir {}\n\
275 or set {}=/path/to/{}{credit}",
276 art.approx_mb,
277 art.source.revision(),
278 dest.display(),
279 art.env,
280 art.dir
281 )
282 }
283 }
284}
285
286pub fn status() -> Vec<(&'static str, Option<PathBuf>)> {
288 ARTIFACTS.iter().map(|a| (a.name, resolve(a.name).ok())).collect()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn third_party_weights_keep_their_licence_and_credit() {
297 for a in ARTIFACTS {
300 match a.source {
301 Source::Ours { repo, .. } => {
302 assert!(repo.starts_with("cp500/"), "{} claims ours but points at {repo}", a.name);
303 assert!(a.source.license().is_none(), "our own work needs no third-party licence");
304 }
305 Source::Upstream { license, .. } | Source::Mirrored { license, .. } => {
306 assert!(!license.is_empty(), "{} must record a licence", a.name);
307 let credit = a.source.attribution().expect("third-party work needs attribution");
308 assert!(
309 !credit.starts_with("cp500/"),
310 "{} credits us for someone else's work",
311 a.name
312 );
313 }
314 Source::Bundled => {}
315 }
316 }
317 }
318
319 #[test]
320 fn every_fetched_artifact_pins_a_revision() {
321 for a in ARTIFACTS {
324 if a.source.fetch_repo().is_some() {
325 assert!(!a.source.revision().is_empty(), "{} must pin a revision", a.name);
326 }
327 }
328 let n = artifact("needle3").unwrap();
330 assert_eq!(n.source.revision().len(), 40, "needle3 should pin an exact commit");
331 }
332
333 #[test]
334 fn a_missing_model_explains_how_to_get_it() {
335 std::env::set_var("STEELDB_MODELS", "/nonexistent-steeldb-test-root");
337 let err = resolve("needle3").unwrap_err();
338 let msg = err.to_string();
339 assert!(msg.contains("not found"), "{msg}");
340 assert!(msg.contains("Cactus-Compute/needle3"), "must credit the author: {msg}");
341 assert!(msg.contains("Apache-2.0"), "must state the licence: {msg}");
342 assert!(msg.contains("--revision"), "must pin a revision: {msg}");
343 assert!(msg.contains("242 MB"), "must state the size: {msg}");
344 std::env::remove_var("STEELDB_MODELS");
345 }
346
347 #[test]
348 fn an_unknown_name_is_an_error_not_a_panic() {
349 assert!(matches!(resolve("no-such-model"), Err(ModelError::Unknown(_))));
350 }
351
352 #[test]
353 fn every_artifact_is_uniquely_named_and_documented() {
354 let mut seen = std::collections::HashSet::new();
355 for a in ARTIFACTS {
356 assert!(seen.insert(a.name), "duplicate artifact name {}", a.name);
357 assert!(!a.purpose.is_empty(), "{} needs a purpose", a.name);
358 assert!(a.env.starts_with("STEELDB_"), "{} env var should be namespaced", a.name);
359 }
360 }
361}