Skip to main content

ferrox_models/
vl_engine.rs

1//! Multimodal / VL serve stub (Qwen2-VL / CogVLM / Gemma4-VL / …).
2//!
3//! Vision primitives for Kimi MoonViT live in [`crate::vision`]; GGUF VL
4//! architectures remain `DeferredMultimodal` in the capability registry
5//! until a projection + chat-template path is wired.
6//!
7//! Expected pairing (P7): main text GGUF + companion `mmproj*.gguf` beside
8//! it — discovered via [`crate::mmproj::find_mmproj_beside`]. A future
9//! [`VlProjectorPair`] will hold both paths; generation stays fail-closed
10//! until projector weights + image tokenization land.
11
12use std::path::{Path, PathBuf};
13
14use thiserror::Error;
15
16#[derive(Debug, Error)]
17#[error("multimodal/VL engine not implemented for architecture {arch}")]
18pub struct VlUnavailable {
19    pub arch: String,
20}
21
22/// Main checkpoint + mmproj companion (not loaded yet).
23#[derive(Debug, Clone)]
24pub struct VlProjectorPair {
25    pub arch: String,
26    pub main_gguf: PathBuf,
27    pub mmproj_gguf: PathBuf,
28}
29
30impl VlProjectorPair {
31    /// Document expected pairing when mmproj is found beside a main GGUF.
32    pub fn from_paths(arch: &str, main_gguf: &Path, mmproj_gguf: PathBuf) -> Self {
33        Self {
34            arch: arch.to_string(),
35            main_gguf: main_gguf.to_path_buf(),
36            mmproj_gguf,
37        }
38    }
39}
40
41pub struct VlEngine {
42    pub arch: String,
43    /// When set, records the mmproj path we expect to wire later.
44    pub projector: Option<VlProjectorPair>,
45}
46
47impl VlEngine {
48    pub fn reject(arch: &str) -> Result<(), VlUnavailable> {
49        Err(VlUnavailable {
50            arch: arch.to_string(),
51        })
52    }
53
54    /// Fail-closed generate entry — projector pairing may be recorded for logs.
55    pub fn reject_with_mmproj(arch: &str, pair: VlProjectorPair) -> Result<(), VlUnavailable> {
56        let _ = pair;
57        Self::reject(arch)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn reject_is_fail_closed() {
67        assert!(VlEngine::reject("qwen2vl").is_err());
68    }
69
70    #[test]
71    fn projector_pair_records_paths() {
72        let main = Path::new("/tmp/model.gguf");
73        let mm = PathBuf::from("/tmp/mmproj-f16.gguf");
74        let pair = VlProjectorPair::from_paths("qwen2vl", main, mm.clone());
75        assert_eq!(pair.mmproj_gguf, mm);
76        assert!(VlEngine::reject_with_mmproj("qwen2vl", pair).is_err());
77    }
78}