1use std::path::{Path, PathBuf};
8
9use crate::capability::{resolve_profile, ArchScope};
10
11pub fn find_mmproj_beside(main_gguf: &Path) -> Option<PathBuf> {
14 let dir = main_gguf.parent()?;
15 let mut matches: Vec<PathBuf> = std::fs::read_dir(dir)
16 .ok()?
17 .filter_map(|e| e.ok())
18 .map(|e| e.path())
19 .filter(|p| {
20 p.extension()
21 .and_then(|e| e.to_str())
22 .map(|e| e.eq_ignore_ascii_case("gguf"))
23 .unwrap_or(false)
24 && p.file_name()
25 .and_then(|n| n.to_str())
26 .map(|n| {
27 let lower = n.to_ascii_lowercase();
28 lower.starts_with("mmproj")
29 })
30 .unwrap_or(false)
31 })
32 .collect();
33 matches.sort();
34 matches.into_iter().next()
35}
36
37pub fn warn_mmproj_if_present(main_gguf: &Path, arch: Option<&str>) {
40 let Some(mmproj) = find_mmproj_beside(main_gguf) else {
41 return;
42 };
43 let vl_arch = arch.is_some_and(|a| {
44 resolve_profile(a).is_some_and(|p| p.scope == ArchScope::DeferredMultimodal)
45 });
46 let arch_note = arch.map(|a| format!(" (arch={a})")).unwrap_or_default();
47 if vl_arch {
48 eprintln!(
49 "ferrox: warning: mmproj {} beside {}{} — VL architecture but multimodal \
50 generation not implemented (see docs/MODELS.md P7)",
51 mmproj.display(),
52 main_gguf.display(),
53 arch_note
54 );
55 } else {
56 eprintln!(
57 "ferrox: warning: mmproj companion {} found beside {}{} — vision projector not \
58 loaded; text-only path continues (see docs/MODELS.md P7)",
59 mmproj.display(),
60 main_gguf.display(),
61 arch_note
62 );
63 }
64}
65
66pub fn eprint_mmproj_if_present(main_gguf: &Path, arch: Option<&str>) {
68 let Some(mmproj) = find_mmproj_beside(main_gguf) else {
69 return;
70 };
71 let arch_note = arch.map(|a| format!(" (arch={a})")).unwrap_or_default();
72 eprintln!(
73 "ferrox: warning: mmproj companion {} found beside {}{} — VL/projector not \
74 implemented; text-only load continues (see docs/MODELS.md)",
75 mmproj.display(),
76 main_gguf.display(),
77 arch_note
78 );
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::fs;
85 use std::io::Write;
86
87 #[test]
88 fn finds_mmproj_next_to_main() {
89 let dir = std::env::temp_dir().join(format!("ferrox_mmproj_{}", std::process::id()));
90 let _ = fs::remove_dir_all(&dir);
91 fs::create_dir_all(&dir).unwrap();
92 let main = dir.join("model-Q4_K_M.gguf");
93 let mm = dir.join("mmproj-f16.gguf");
94 fs::File::create(&main).unwrap().write_all(b"x").unwrap();
95 fs::File::create(&mm).unwrap().write_all(b"y").unwrap();
96 let found = find_mmproj_beside(&main).expect("mmproj");
97 assert_eq!(found, mm);
98 let _ = fs::remove_dir_all(&dir);
99 }
100
101 #[test]
102 fn none_when_no_mmproj() {
103 let dir = std::env::temp_dir().join(format!("ferrox_nomm_{}", std::process::id()));
104 let _ = fs::remove_dir_all(&dir);
105 fs::create_dir_all(&dir).unwrap();
106 let main = dir.join("only.gguf");
107 fs::File::create(&main).unwrap().write_all(b"x").unwrap();
108 assert!(find_mmproj_beside(&main).is_none());
109 let _ = fs::remove_dir_all(&dir);
110 }
111}