#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelArch {
Qwen2,
Llama,
Gemma2,
}
#[derive(Debug, Clone, Copy)]
pub struct MiniModel {
pub name: &'static str,
pub params: &'static str,
pub quant: &'static str,
pub approx_ram_gb: f32,
pub hf_repo: &'static str,
pub gguf_file: &'static str,
pub tokenizer_repo: &'static str,
pub arch: ModelArch,
pub note: &'static str,
}
pub const PALETTE: &[MiniModel] = &[
MiniModel {
name: "qwen2.5-0.5b",
params: "0.5B",
quant: "Q4_K_M",
approx_ram_gb: 0.5,
hf_repo: "Qwen/Qwen2.5-0.5B-Instruct-GGUF",
gguf_file: "qwen2.5-0.5b-instruct-q4_k_m.gguf",
tokenizer_repo: "Qwen/Qwen2.5-0.5B-Instruct",
arch: ModelArch::Qwen2,
note: "tiniest; fast summarizer/classifier, lowest RAM",
},
MiniModel {
name: "llama-3.2-1b",
params: "1B",
quant: "Q4_K_M",
approx_ram_gb: 0.8,
hf_repo: "bartowski/Llama-3.2-1B-Instruct-GGUF",
gguf_file: "Llama-3.2-1B-Instruct-Q4_K_M.gguf",
tokenizer_repo: "unsloth/Llama-3.2-1B-Instruct",
arch: ModelArch::Llama,
note: "strong 1B; good summaries with tiny footprint",
},
MiniModel {
name: "smollm2-1.7b",
params: "1.7B",
quant: "Q4_K_M",
approx_ram_gb: 1.0,
hf_repo: "bartowski/SmolLM2-1.7B-Instruct-GGUF",
gguf_file: "SmolLM2-1.7B-Instruct-Q4_K_M.gguf",
tokenizer_repo: "HuggingFaceTB/SmolLM2-1.7B-Instruct",
arch: ModelArch::Llama,
note: "instruction-tuned small; concise summarizer",
},
MiniModel {
name: "qwen2.5-1.5b",
params: "1.5B",
quant: "Q4_K_M",
approx_ram_gb: 1.0,
hf_repo: "Qwen/Qwen2.5-1.5B-Instruct-GGUF",
gguf_file: "qwen2.5-1.5b-instruct-q4_k_m.gguf",
tokenizer_repo: "Qwen/Qwen2.5-1.5B-Instruct",
arch: ModelArch::Qwen2,
note: "balanced default summarizer pick on 16 GB",
},
MiniModel {
name: "gemma-2-2b",
params: "2B",
quant: "Q4_K_M",
approx_ram_gb: 1.7,
hf_repo: "bartowski/gemma-2-2b-it-GGUF",
gguf_file: "gemma-2-2b-it-Q4_K_M.gguf",
tokenizer_repo: "unsloth/gemma-2-2b-it",
arch: ModelArch::Gemma2,
note: "fluent 2B; nicer prose, more RAM",
},
MiniModel {
name: "qwen2.5-3b",
params: "3B",
quant: "Q4_K_M",
approx_ram_gb: 2.0,
hf_repo: "Qwen/Qwen2.5-3B-Instruct-GGUF",
gguf_file: "qwen2.5-3b-instruct-q4_k_m.gguf",
tokenizer_repo: "Qwen/Qwen2.5-3B-Instruct",
arch: ModelArch::Qwen2,
note: "strongest small; upper bound for a 16 GB box",
},
MiniModel {
name: "llama-3.2-3b",
params: "3B",
quant: "Q4_K_M",
approx_ram_gb: 2.0,
hf_repo: "bartowski/Llama-3.2-3B-Instruct-GGUF",
gguf_file: "Llama-3.2-3B-Instruct-Q4_K_M.gguf",
tokenizer_repo: "unsloth/Llama-3.2-3B-Instruct",
arch: ModelArch::Llama,
note: "capable 3B; upper bound on a 16 GB box",
},
];
#[must_use]
pub fn palette() -> &'static [MiniModel] {
PALETTE
}
#[must_use]
pub fn find(name: &str) -> Option<&'static MiniModel> {
PALETTE.iter().find(|m| m.name == name)
}
#[must_use]
pub fn fitting(ram_budget_gb: f32) -> Vec<&'static MiniModel> {
PALETTE
.iter()
.filter(|m| m.approx_ram_gb <= ram_budget_gb)
.collect()
}
#[must_use]
pub fn default_model() -> &'static MiniModel {
&PALETTE[0]
}
#[must_use]
pub fn models_dir() -> Option<std::path::PathBuf> {
newt_core::Config::user_config_dir().map(|d| d.join("models"))
}
#[must_use]
pub fn local_gguf_path(m: &MiniModel) -> Option<std::path::PathBuf> {
models_dir().map(|d| d.join(m.name).join(m.gguf_file))
}
pub const EMBED_HF_REPO: &str = "BAAI/bge-small-en-v1.5";
pub const EMBED_MODEL_DIR: &str = "bge-small-en-v1.5";
pub const EMBED_REQUIRED_FILES: &[&str] = &["config.json", "tokenizer.json", "model.safetensors"];
#[must_use]
pub fn embed_model_dir() -> Option<std::path::PathBuf> {
models_dir().map(|d| d.join(EMBED_MODEL_DIR))
}
#[must_use]
pub fn embed_model_present_in(dir: &std::path::Path) -> bool {
EMBED_REQUIRED_FILES.iter().all(|f| dir.join(f).is_file())
}
#[must_use]
pub fn embed_model_dir_if_present() -> Option<std::path::PathBuf> {
let dir = embed_model_dir()?;
embed_model_present_in(&dir).then_some(dir)
}
#[must_use]
pub fn local_tokenizer_path(m: &MiniModel) -> Option<std::path::PathBuf> {
models_dir().map(|d| d.join(m.name).join("tokenizer.json"))
}
#[must_use]
pub fn resolve_local(alias: &str) -> Option<std::path::PathBuf> {
let m = find(alias)?;
let gguf = local_gguf_path(m)?;
let tok = local_tokenizer_path(m)?;
(gguf.is_file() && tok.is_file()).then_some(gguf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn palette_is_nonempty_and_well_formed() {
assert!(!PALETTE.is_empty());
for m in PALETTE {
assert!(!m.name.is_empty());
assert!(!m.hf_repo.is_empty() && m.gguf_file.ends_with(".gguf"));
assert!(
!m.tokenizer_repo.is_empty(),
"{}: no tokenizer_repo",
m.name
);
assert!(
m.approx_ram_gb > 0.0 && m.approx_ram_gb <= 3.0,
"{} is too large for the mini palette: {} GB",
m.name,
m.approx_ram_gb
);
}
}
#[test]
fn palette_is_smallest_first_and_names_unique() {
let mut seen = std::collections::HashSet::new();
let mut last = 0.0f32;
for m in PALETTE {
assert!(seen.insert(m.name), "duplicate palette name: {}", m.name);
assert!(m.approx_ram_gb >= last, "palette must be smallest-first");
last = m.approx_ram_gb;
}
}
#[test]
fn tokenizer_json_sits_beside_the_gguf() {
for m in PALETTE {
let (Some(g), Some(t)) = (local_gguf_path(m), local_tokenizer_path(m)) else {
continue; };
assert_eq!(
g.parent(),
t.parent(),
"{}: tokenizer not beside gguf",
m.name
);
assert_eq!(
t.file_name().and_then(|s| s.to_str()),
Some("tokenizer.json"),
"{}: tokenizer must be tokenizer.json",
m.name
);
}
}
#[test]
fn find_resolves_by_alias() {
assert_eq!(find("qwen2.5-1.5b").unwrap().arch, ModelArch::Qwen2);
assert_eq!(find("llama-3.2-1b").unwrap().arch, ModelArch::Llama);
assert!(find("does-not-exist").is_none());
}
#[test]
fn fitting_respects_a_ram_budget() {
let small = fitting(1.0);
assert!(small.iter().all(|m| m.approx_ram_gb <= 1.0));
assert!(
small.len() < PALETTE.len(),
"a 1 GB budget should exclude the larger entries"
);
assert!(fitting(0.5).iter().any(|m| m.name == "qwen2.5-0.5b"));
}
}