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-4M",
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 root = search_roots().first().cloned().unwrap_or_else(|| PathBuf::from("models"));
268 match art.source.fetch_repo() {
269 None => "this artifact ships with the crate; the build is incomplete".to_string(),
270 Some(repo) => {
271 let credit = match art.source.attribution() {
272 Some(up) => format!("\n {up} — {}", art.source.license().unwrap_or("see model card")),
273 None => String::new(),
274 };
275 format!(
276 "Fetch it ({} MB) with:\n \
277 huggingface-cli download {repo} --revision {} --include '{}/*' --local-dir {}\n\
278 or set {}=/path/to/{}{credit}",
279 art.approx_mb,
280 art.source.revision(),
281 art.dir,
282 root.display(),
283 art.env,
284 art.dir
285 )
286 }
287 }
288}
289
290pub fn status() -> Vec<(&'static str, Option<PathBuf>)> {
292 ARTIFACTS.iter().map(|a| (a.name, resolve(a.name).ok())).collect()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn third_party_weights_keep_their_licence_and_credit() {
301 for a in ARTIFACTS {
304 match a.source {
305 Source::Ours { repo, .. } => {
306 assert!(repo.starts_with("cp500/"), "{} claims ours but points at {repo}", a.name);
307 assert!(a.source.license().is_none(), "our own work needs no third-party licence");
308 }
309 Source::Upstream { license, .. } | Source::Mirrored { license, .. } => {
310 assert!(!license.is_empty(), "{} must record a licence", a.name);
311 let credit = a.source.attribution().expect("third-party work needs attribution");
312 assert!(
313 !credit.starts_with("cp500/"),
314 "{} credits us for someone else's work",
315 a.name
316 );
317 }
318 Source::Bundled => {}
319 }
320 }
321 }
322
323 #[test]
324 fn every_fetched_artifact_pins_a_revision() {
325 for a in ARTIFACTS {
328 if a.source.fetch_repo().is_some() {
329 assert!(!a.source.revision().is_empty(), "{} must pin a revision", a.name);
330 }
331 }
332 let n = artifact("needle3").unwrap();
334 assert_eq!(n.source.revision().len(), 40, "needle3 should pin an exact commit");
335 }
336
337 #[test]
338 fn the_fetch_hint_downloads_into_the_models_ROOT_not_the_artifact_directory() {
339 std::env::set_var("STEELDB_MODELS", "/models-root");
345 let hint = fetch_hint(artifact("spo-tagger").unwrap());
346 assert!(hint.contains("--include 'step0_bundle_ml/*'"), "must select the subdirectory: {hint}");
347 assert!(hint.contains("--local-dir /models-root"), "must target the ROOT: {hint}");
348 assert!(
349 !hint.contains("--local-dir /models-root/step0_bundle_ml"),
350 "must not nest the artifact directory inside itself: {hint}"
351 );
352 std::env::remove_var("STEELDB_MODELS");
353 }
354
355 #[test]
356 fn a_missing_model_explains_how_to_get_it() {
357 std::env::set_var("STEELDB_MODELS", "/nonexistent-steeldb-test-root");
359 let err = resolve("needle3").unwrap_err();
360 let msg = err.to_string();
361 assert!(msg.contains("not found"), "{msg}");
362 assert!(msg.contains("Cactus-Compute/needle3"), "must credit the author: {msg}");
363 assert!(msg.contains("Apache-2.0"), "must state the licence: {msg}");
364 assert!(msg.contains("--revision"), "must pin a revision: {msg}");
365 assert!(msg.contains("242 MB"), "must state the size: {msg}");
366 std::env::remove_var("STEELDB_MODELS");
367 }
368
369 #[test]
370 fn an_unknown_name_is_an_error_not_a_panic() {
371 assert!(matches!(resolve("no-such-model"), Err(ModelError::Unknown(_))));
372 }
373
374 #[test]
375 fn every_artifact_is_uniquely_named_and_documented() {
376 let mut seen = std::collections::HashSet::new();
377 for a in ARTIFACTS {
378 assert!(seen.insert(a.name), "duplicate artifact name {}", a.name);
379 assert!(!a.purpose.is_empty(), "{} needs a purpose", a.name);
380 assert!(a.env.starts_with("STEELDB_"), "{} env var should be namespaced", a.name);
381 }
382 }
383}