use serde::Serialize;
#[derive(Debug, Serialize, PartialEq)]
pub struct ModelInfo {
pub name: String,
pub size_bytes: u64,
pub reasoning: Option<bool>,
}
fn classify_reasoning(name: &str) -> Option<bool> {
let lower = name.to_lowercase();
if lower.contains("embed") {
return None;
}
const KNOWN_REASONING: &[&str] = &[
"deepseek-r1",
"qwq",
"qwen3", "phi4-reasoning",
"magistral",
"marco-o1",
];
const KNOWN_NON_REASONING: &[&str] = &[
"llama3",
"llama2",
"gemma",
"mistral",
"phi3",
"phi4-mini",
"codellama",
"starcoder",
];
if KNOWN_REASONING
.iter()
.any(|pattern| lower.contains(pattern))
{
Some(true)
} else if KNOWN_NON_REASONING
.iter()
.any(|pattern| lower.contains(pattern))
{
Some(false)
} else {
None
}
}
#[derive(serde::Deserialize)]
struct TagsResponse {
models: Vec<TagEntry>,
}
#[derive(serde::Deserialize)]
struct TagEntry {
name: String,
#[serde(default)]
size: u64,
}
pub async fn list_ollama_models(host: &str) -> anyhow::Result<Vec<ModelInfo>> {
let url = format!("{}/api/tags", host.trim_end_matches('/'));
let response = reqwest::get(&url)
.await
.map_err(|e| anyhow::anyhow!("connecting to Ollama at {host}: {e} (is it running?)"))?
.error_for_status()
.map_err(|e| anyhow::anyhow!("Ollama at {host} returned an error: {e}"))?
.json::<TagsResponse>()
.await
.map_err(|e| anyhow::anyhow!("parsing Ollama's response from {host}: {e}"))?;
Ok(response
.models
.into_iter()
.map(|entry| ModelInfo {
reasoning: classify_reasoning(&entry.name),
name: entry.name,
size_bytes: entry.size,
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_known_reasoning_family_is_classified_true() {
assert_eq!(classify_reasoning("qwen3.5:latest"), Some(true));
assert_eq!(classify_reasoning("deepseek-r1:8b"), Some(true));
}
#[test]
fn a_known_non_reasoning_family_is_classified_false() {
assert_eq!(classify_reasoning("llama3.2:3b"), Some(false));
assert_eq!(classify_reasoning("gemma4:latest"), Some(false));
}
#[test]
fn an_unrecognized_family_is_unknown_not_guessed_false() {
assert_eq!(classify_reasoning("some-brand-new-model:latest"), None);
}
#[test]
fn an_embedding_model_is_unknown_not_misclassified_via_its_base_family() {
assert_eq!(classify_reasoning("qwen3-embedding:4b"), None);
assert_eq!(classify_reasoning("mxbai-embed-large:latest"), None);
}
}