Skip to main content

koda_core/
model_alias.rs

1//! Hardcoded model aliases for stable, cross-provider model selection.
2//!
3//! Aliases map short, memorable names to exact model IDs + providers.
4//! Updated per release — we only alias models we've tested with Koda.
5//!
6//! ## Usage
7//!
8//! - `/model` picker shows aliases (user-facing)
9//! - Sub-agent definitions reference aliases: `model: Some("gemini-flash-lite")`
10//! - `resolve("claude-sonnet")` → `Some(ResolvedAlias { model_id, provider })`
11//! - `resolve("local")` → `Some(ResolvedAlias { model_id: "auto-detect", provider: LMStudio })`
12
13use crate::config::ProviderType;
14
15/// A hardcoded model alias.
16#[derive(Debug, Clone, Copy)]
17pub struct ModelAlias {
18    /// Short stable name (e.g. `"gemini-flash-lite"`).
19    pub alias: &'static str,
20    /// Exact model ID sent to the provider API.
21    pub model_id: &'static str,
22    /// Which provider serves this model.
23    pub provider: ProviderType,
24}
25
26/// The "local" alias uses auto-detection — model ID is discovered at runtime.
27pub const LOCAL_ALIAS: &str = "local";
28
29/// All known aliases. Order determines display order in `/model` picker.
30static ALIASES: &[ModelAlias] = &[
31    // ── Gemini (version-less → auto-resolves to latest) ─
32    ModelAlias {
33        alias: "gemini-flash-lite",
34        model_id: "gemini-flash-lite-latest",
35        provider: ProviderType::Gemini,
36    },
37    ModelAlias {
38        alias: "gemini-flash",
39        model_id: "gemini-flash-latest",
40        provider: ProviderType::Gemini,
41    },
42    ModelAlias {
43        alias: "gemini-pro",
44        model_id: "gemini-pro-latest",
45        provider: ProviderType::Gemini,
46    },
47    // ── Anthropic ────────────────────────────────────────
48    ModelAlias {
49        alias: "claude-haiku",
50        model_id: "claude-haiku-4-5-20251001",
51        provider: ProviderType::Anthropic,
52    },
53    ModelAlias {
54        alias: "claude-sonnet",
55        model_id: "claude-sonnet-4-6",
56        provider: ProviderType::Anthropic,
57    },
58    ModelAlias {
59        alias: "claude-opus",
60        model_id: "claude-opus-4-6",
61        provider: ProviderType::Anthropic,
62    },
63];
64
65/// Resolve an alias to its model ID and provider.
66///
67/// Returns `None` if `name` is not a known alias (treat as literal model ID).
68/// For `"local"`, returns a sentinel — caller must auto-detect via LMStudio API.
69///
70/// ```
71/// use koda_core::model_alias::resolve;
72/// use koda_core::config::ProviderType;
73///
74/// let r = resolve("gemini-flash").unwrap();
75/// assert_eq!(r.provider, ProviderType::Gemini);
76///
77/// assert!(resolve("not-an-alias").is_none());
78/// ```
79pub fn resolve(name: &str) -> Option<ResolvedAlias> {
80    if name == LOCAL_ALIAS {
81        return Some(ResolvedAlias {
82            alias: LOCAL_ALIAS,
83            model_id: "auto-detect",
84            provider: ProviderType::LMStudio,
85        });
86    }
87    ALIASES
88        .iter()
89        .find(|a| a.alias == name)
90        .map(|a| ResolvedAlias {
91            alias: a.alias,
92            model_id: a.model_id,
93            provider: a.provider,
94        })
95}
96
97/// Result of resolving an alias.
98#[derive(Debug, Clone)]
99pub struct ResolvedAlias {
100    /// The alias that was resolved.
101    pub alias: &'static str,
102    /// Exact model ID for the provider API (or `"auto-detect"` for local).
103    pub model_id: &'static str,
104    /// Provider that serves this model.
105    pub provider: ProviderType,
106}
107
108impl ResolvedAlias {
109    /// True if this alias requires runtime auto-detection (LMStudio/Ollama).
110    pub fn needs_auto_detect(&self) -> bool {
111        self.model_id == "auto-detect"
112    }
113}
114
115/// All aliases in display order (for the `/model` picker).
116pub fn all() -> &'static [ModelAlias] {
117    ALIASES
118}
119
120/// All alias names (for tab completion).
121pub fn alias_names() -> Vec<&'static str> {
122    let mut names: Vec<&str> = ALIASES.iter().map(|a| a.alias).collect();
123    names.push(LOCAL_ALIAS);
124    names
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn resolve_known_alias() {
133        let r = resolve("claude-sonnet").unwrap();
134        assert_eq!(r.model_id, "claude-sonnet-4-6");
135        assert_eq!(r.provider, ProviderType::Anthropic);
136        assert!(!r.needs_auto_detect());
137    }
138
139    #[test]
140    fn resolve_local_alias() {
141        let r = resolve("local").unwrap();
142        assert!(r.needs_auto_detect());
143        assert_eq!(r.provider, ProviderType::LMStudio);
144    }
145
146    #[test]
147    fn resolve_unknown_returns_none() {
148        assert!(resolve("not-a-real-model").is_none());
149    }
150
151    #[test]
152    fn resolve_literal_model_id_returns_none() {
153        // Literal model IDs should NOT resolve as aliases
154        assert!(resolve("claude-sonnet-4-6").is_none());
155        assert!(resolve("gemini-2.5-pro").is_none());
156    }
157
158    #[test]
159    fn all_aliases_non_empty() {
160        assert!(!all().is_empty());
161    }
162
163    #[test]
164    fn alias_names_includes_local() {
165        let names = alias_names();
166        assert!(names.contains(&"local"));
167        assert!(names.contains(&"gemini-flash-lite"));
168    }
169
170    #[test]
171    fn no_duplicate_aliases() {
172        let names = alias_names();
173        let mut seen = std::collections::HashSet::new();
174        for name in &names {
175            assert!(seen.insert(name), "duplicate alias: {name}");
176        }
177    }
178
179    #[test]
180    fn no_duplicate_model_ids() {
181        let mut seen = std::collections::HashSet::new();
182        for a in all() {
183            assert!(
184                seen.insert(a.model_id),
185                "duplicate model_id: {}",
186                a.model_id
187            );
188        }
189    }
190}