1use std::sync::Arc;
4
5use anyhow::Result;
6use serde::Serialize;
7
8use neurondump_gguf::GgufFile;
9
10use crate::discovery::ModelEntry;
11
12#[derive(Debug, Clone, Default, Serialize)]
13pub struct OllamaExtras {
14 pub template: Option<String>,
15 pub system: Option<String>,
16 pub params: Option<serde_json::Value>,
17 pub licenses: Vec<String>,
18 pub messages: Option<serde_json::Value>,
19 pub adapters: Vec<String>,
20}
21
22pub struct ModelHandle {
23 pub entry: ModelEntry,
24 pub gguf: Arc<GgufFile>,
25 pub ollama: OllamaExtras,
26}
27
28#[cfg(feature = "native")]
29fn read_small(path: &std::path::Path, limit: u64) -> Option<String> {
30 let md = std::fs::metadata(path).ok()?;
31 if md.len() > limit {
32 return None;
33 }
34 let bytes = std::fs::read(path).ok()?;
35 Some(String::from_utf8_lossy(&bytes).into_owned())
36}
37
38impl ModelHandle {
39 pub fn from_gguf(entry: ModelEntry, gguf: GgufFile) -> Self {
41 Self { entry, gguf: Arc::new(gguf), ollama: OllamaExtras::default() }
42 }
43
44 #[cfg(feature = "native")]
45 pub fn open(entry: ModelEntry) -> Result<Self> {
46 use anyhow::Context;
47 let gguf = GgufFile::open(&entry.path).with_context(|| format!("parsing {}", entry.path.display()))?;
48 let mut ollama = OllamaExtras::default();
49 if let Some(info) = &entry.ollama {
50 for l in &info.layers {
51 let Some(p) = &l.path else { continue };
52 match l.kind.as_str() {
53 "template" => ollama.template = read_small(p, 1 << 20),
54 "system" => ollama.system = read_small(p, 1 << 20),
55 "params" => ollama.params = read_small(p, 1 << 20).and_then(|s| serde_json::from_str(&s).ok()),
56 "messages" => ollama.messages = read_small(p, 4 << 20).and_then(|s| serde_json::from_str(&s).ok()),
57 "license" => ollama.licenses.extend(read_small(p, 4 << 20)),
58 "adapter" => ollama.adapters.push(p.to_string_lossy().to_string()),
59 _ => {}
60 }
61 }
62 }
63 Ok(Self { entry, gguf: Arc::new(gguf), ollama })
64 }
65
66 pub fn architecture(&self) -> &str {
67 self.gguf.architecture().unwrap_or("unknown")
68 }
69
70 pub fn open_projector(&self) -> Option<Result<GgufFile>> {
71 #[cfg(feature = "native")]
72 return self.entry.projector.as_ref().map(|p| GgufFile::open(p).map_err(Into::into));
73 #[cfg(not(feature = "native"))]
74 None
75 }
76}