use serde_json::Value;
use crate::config::load::Environment;
pub const STORE_FILENAME: &str = "models-store.json";
#[derive(Debug, Clone, PartialEq)]
pub struct ModelInfo {
pub id: String,
pub base_url: Option<String>,
pub input: Vec<String>,
pub cost_in: f64,
}
pub fn agent_directories(env: &Environment) -> Vec<String> {
let override_dir = env
.get("PI_CODING_AGENT_DIR")
.map(|value| value.trim())
.filter(|value| !value.is_empty());
let home = env.get("HOME").map_or("", |value| value.trim());
let config_root = env
.get("XDG_CONFIG_HOME")
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map_or_else(|| format!("{home}/.config"), str::to_owned);
let mut directories = Vec::new();
if let Some(override_dir) = override_dir {
directories.push(override_dir.to_owned());
}
directories.push(format!("{home}/.pi/agent"));
directories.push(format!("{config_root}/pi"));
directories
}
pub fn agent_directory(env: &Environment) -> Option<String> {
for directory in agent_directories(env) {
let path = std::path::Path::new(&directory).join(STORE_FILENAME);
if std::fs::metadata(&path).is_ok() {
return Some(directory);
}
}
None
}
pub fn sees_images(model: Option<&ModelInfo>) -> bool {
model.is_some_and(|model| model.input.iter().any(|kind| kind == "image"))
}
pub fn read_models(directory: Option<&str>, provider: &str) -> Vec<ModelInfo> {
let Some(directory) = directory else {
return Vec::new();
};
let Ok(text) = std::fs::read_to_string(std::path::Path::new(directory).join(STORE_FILENAME))
else {
return Vec::new();
};
let parsed: Value = match serde_json::from_str(&text) {
Ok(parsed) => parsed,
Err(_) => return Vec::new(),
};
let models = parsed
.get(provider)
.and_then(|entry| entry.get("models"))
.and_then(Value::as_array);
let Some(models) = models else {
return Vec::new();
};
let mut found = Vec::new();
for raw in models {
let Some(model) = raw.as_object() else {
assert!(
!raw.is_null(),
"the model store holds a null entry where a model was expected"
);
continue;
};
let Some(Value::String(id)) = model.get("id") else {
continue;
};
let cost = model.get("cost");
found.push(ModelInfo {
id: id.clone(),
base_url: match model.get("baseUrl") {
Some(Value::String(base_url)) => Some(base_url.clone()),
_ => None,
},
input: match model.get("input") {
Some(Value::Array(kinds)) => kinds
.iter()
.filter_map(|kind| kind.as_str())
.map(str::to_owned)
.collect(),
_ => Vec::new(),
},
cost_in: cost
.and_then(|cost| cost.get("input"))
.and_then(Value::as_f64)
.unwrap_or(f64::INFINITY),
});
}
found
}
pub fn model_by_id<'a>(models: &'a [ModelInfo], id: Option<&str>) -> Option<&'a ModelInfo> {
let id = id?;
models.iter().find(|model| model.id == id)
}
pub fn vision_model(models: &[ModelInfo], preferred: Option<&str>) -> Option<ModelInfo> {
if let Some(preferred) = preferred {
let named = model_by_id(models, Some(preferred));
return match named {
Some(named) if sees_images(Some(named)) && named.base_url.is_some() => {
Some(named.clone())
}
_ => None,
};
}
let mut candidates: Vec<&ModelInfo> = models
.iter()
.filter(|model| sees_images(Some(model)) && model.base_url.is_some())
.collect();
candidates.sort_by(|left, right| {
left.cost_in
.partial_cmp(&right.cost_in)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.first().map(|model| (*model).clone())
}