use super::dropdown::DropdownItem;
#[derive(Clone, Debug)]
pub struct ModelItem {
pub label: String,
pub model_id: String,
pub provider: String,
pub is_current: bool,
pub is_local: bool,
}
impl DropdownItem for ModelItem {
fn label(&self) -> &str {
&self.label
}
fn description(&self) -> String {
let mut parts = Vec::new();
if self.label != self.model_id {
parts.push(self.model_id.clone());
}
parts.push(self.provider.clone());
if self.is_current {
parts.push("\u{25c0} current".to_string());
}
parts.join(" ")
}
fn matches_filter(&self, filter: &str) -> bool {
let f = filter.to_lowercase();
self.label.to_lowercase().contains(&f)
|| self.model_id.to_lowercase().contains(&f)
|| self.provider.to_lowercase().contains(&f)
}
}
pub fn build_items(current_model: &str, local_model: Option<&str>) -> Vec<ModelItem> {
let mut items: Vec<ModelItem> = koda_core::model_alias::all()
.iter()
.map(|a| {
let provider_name = a.provider.to_string();
ModelItem {
label: a.alias.to_string(),
model_id: a.model_id.to_string(),
provider: provider_name,
is_current: a.model_id == current_model || a.alias == current_model,
is_local: false,
}
})
.collect();
match local_model {
Some(id) => items.push(ModelItem {
label: "local".to_string(),
model_id: id.to_string(),
provider: "LM Studio".to_string(),
is_current: id == current_model,
is_local: true,
}),
None => items.push(ModelItem {
label: "local".to_string(),
model_id: "(not running)".to_string(),
provider: "LM Studio".to_string(),
is_current: false,
is_local: true,
}),
}
items
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_model_shows_marker() {
let item = ModelItem {
label: "claude-sonnet".into(),
model_id: "claude-sonnet-4-6".into(),
provider: "Anthropic".into(),
is_current: true,
is_local: false,
};
assert!(item.description().contains('\u{25c0}'));
}
#[test]
fn alias_shows_model_id_and_provider() {
let item = ModelItem {
label: "gemini-flash-lite".into(),
model_id: "gemini-flash-lite-latest".into(),
provider: "Gemini".into(),
is_current: false,
is_local: false,
};
let desc = item.description();
assert!(desc.contains("gemini-flash-lite-latest"));
assert!(desc.contains("Gemini"));
}
#[test]
fn filter_matches_alias_model_provider() {
let item = ModelItem {
label: "claude-sonnet".into(),
model_id: "claude-sonnet-4-6".into(),
provider: "Anthropic".into(),
is_current: false,
is_local: false,
};
assert!(item.matches_filter("sonnet"));
assert!(item.matches_filter("anthropic"));
assert!(item.matches_filter("4-6"));
assert!(!item.matches_filter("gemini"));
}
#[test]
fn build_items_includes_local() {
let items = build_items("gpt-4o", None);
let local = items.iter().find(|i| i.label == "local").unwrap();
assert!(local.is_local);
assert_eq!(local.model_id, "(not running)");
}
#[test]
fn build_items_with_local_model() {
let items = build_items("qwen3-8b", Some("qwen3-8b"));
let local = items.iter().find(|i| i.label == "local").unwrap();
assert!(local.is_current);
assert_eq!(local.model_id, "qwen3-8b");
}
#[test]
fn build_items_marks_current_by_alias() {
let items = build_items("claude-sonnet", None);
let current = items.iter().find(|i| i.is_current);
assert!(current.is_some());
assert_eq!(current.unwrap().label, "claude-sonnet");
}
#[test]
fn build_items_marks_current_by_model_id() {
let items = build_items("claude-sonnet-4-6", None);
let current = items.iter().find(|i| i.is_current);
assert!(current.is_some());
assert_eq!(current.unwrap().label, "claude-sonnet");
}
}