use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ModelKind {
EmbedderGraniteQ4KM,
RerankerBgeV2M3Q5KM,
}
impl ModelKind {
#[must_use]
pub fn filename(self) -> &'static str {
match self {
Self::EmbedderGraniteQ4KM => "granite-embedding-311m-multilingual-r2.Q4_K_M.gguf",
Self::RerankerBgeV2M3Q5KM => "bge-reranker-v2-m3.Q5_K_M.gguf",
}
}
#[must_use]
pub fn url(self) -> &'static str {
match self {
Self::EmbedderGraniteQ4KM => {
"https://huggingface.co/mykor/granite-embedding-311m-multilingual-r2-GGUF/resolve/main/granite-embedding-311M-multilingual-r2-Q4_K_M.gguf"
}
Self::RerankerBgeV2M3Q5KM => {
"https://huggingface.co/gpustack/bge-reranker-v2-m3-GGUF/resolve/main/bge-reranker-v2-m3-Q5_K_M.gguf"
}
}
}
#[must_use]
pub fn sha256(self) -> &'static str {
match self {
Self::EmbedderGraniteQ4KM => {
"58d27f63e69ccf7abce27bf6b35bb0edebc3a1c05ad4a3165acaba1cdca107c0"
}
Self::RerankerBgeV2M3Q5KM => {
"1a212007526c7083627eed92b39dd4472e90ff1374a03fb068733378220813ef"
}
}
}
#[must_use]
pub fn size_mb(self) -> u64 {
match self {
Self::EmbedderGraniteQ4KM => 253,
Self::RerankerBgeV2M3Q5KM => 468,
}
}
#[must_use]
pub fn display_name(self) -> &'static str {
match self {
Self::EmbedderGraniteQ4KM => "granite-embedding-311m Q4_K_M",
Self::RerankerBgeV2M3Q5KM => "bge-reranker-v2-m3 Q5_K_M",
}
}
#[must_use]
pub fn env_override(self) -> &'static str {
match self {
Self::EmbedderGraniteQ4KM => "LUNARIS_EMBEDDER_GGUF",
Self::RerankerBgeV2M3Q5KM => "LUNARIS_RERANKER_GGUF",
}
}
}
#[must_use]
pub fn models_dir() -> Option<PathBuf> {
for var in ["LUNARIS_MODELS_DIR", "LUNARIS_MCP_MODELS_DIR"] {
if let Some(dir) = std::env::var_os(var).filter(|v| !v.is_empty()) {
return Some(PathBuf::from(dir));
}
}
home_dir().map(|h| h.join(".lunaris").join("models"))
}
#[must_use]
pub fn staged_path(kind: ModelKind) -> Option<PathBuf> {
models_dir().map(|d| d.join(kind.filename()))
}
#[must_use]
pub fn staged_path_in(home: &Path, kind: ModelKind) -> PathBuf {
home.join(".lunaris").join("models").join(kind.filename())
}
#[must_use]
pub fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").filter(|v| !v.is_empty()).map(PathBuf::from).or_else(dirs::home_dir)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_digest_is_a_full_lowercase_sha256() {
for kind in [ModelKind::EmbedderGraniteQ4KM, ModelKind::RerankerBgeV2M3Q5KM] {
let d = kind.sha256();
assert_eq!(d.len(), 64, "{kind:?} digest is not 64 hex chars: {d}");
assert!(
d.chars().all(|c| c.is_ascii_digit() || c.is_ascii_lowercase()),
"{kind:?} digest must be lowercase hex: {d}"
);
}
}
#[test]
fn the_url_ends_in_a_gguf_and_the_two_kinds_never_collide() {
let e = ModelKind::EmbedderGraniteQ4KM;
let r = ModelKind::RerankerBgeV2M3Q5KM;
for kind in [e, r] {
assert!(kind.url().ends_with(".gguf"), "{kind:?} url does not end in .gguf");
assert!(kind.filename().ends_with(".gguf"));
}
assert_ne!(e.filename(), r.filename());
assert_ne!(e.sha256(), r.sha256());
assert_ne!(e.url(), r.url());
assert_ne!(e.env_override(), r.env_override());
}
#[test]
fn staged_path_in_is_home_relative_and_carries_the_locked_name() {
let p = staged_path_in(Path::new("/home/x"), ModelKind::EmbedderGraniteQ4KM);
assert!(p.starts_with("/home/x/.lunaris/models"));
assert_eq!(
p.file_name().unwrap(),
ModelKind::EmbedderGraniteQ4KM.filename(),
"the staged file must carry the locked name, not the mirror's"
);
}
}