Skip to main content

aither_models/
registry.rs

1//! Model registry with indexed lookup.
2
3use std::collections::HashMap;
4use std::sync::OnceLock;
5
6use aither_core::llm::model::Ability;
7
8use crate::convert;
9use crate::tier::{ChatPriceBreakpoints, ModelTier, classify_entry};
10use crate::types::{ModelEntry, ModelMode, Pricing, Provider};
11
12// The registry table below is emitted by `build.rs` from the LiteLLM snapshot.
13// It is machine-written and never edited by hand; the generator applies its own
14// `#[allow]` attributes because style lints on generated text can only be
15// "fixed" by changing the generator's formatting.
16include!(concat!(env!("OUT_DIR"), "/generated.rs"));
17
18/// A searchable collection of model entries.
19#[derive(Debug)]
20pub struct ModelRegistry {
21    entries: Vec<ModelEntry>,
22    index: HashMap<String, usize>,
23    chat_price_breakpoints: Option<ChatPriceBreakpoints>,
24}
25
26impl ModelRegistry {
27    /// Returns the compile-time bundled registry.
28    #[must_use]
29    pub fn bundled() -> &'static Self {
30        static INSTANCE: OnceLock<ModelRegistry> = OnceLock::new();
31        INSTANCE.get_or_init(|| {
32            let entries: Vec<ModelEntry> = ENTRIES.to_vec();
33            Self::from_entries(entries)
34        })
35    }
36
37    /// Build a registry from parsed `LiteLLM` JSON bytes.
38    #[must_use]
39    pub fn from_litellm_json(json: &[u8]) -> Self {
40        let converted = convert::parse_litellm_json(json);
41        let entries = converted.into_iter().filter_map(convert_to_entry).collect();
42        Self::from_entries(entries)
43    }
44
45    fn from_entries(entries: Vec<ModelEntry>) -> Self {
46        let mut index = HashMap::with_capacity(entries.len() * 2);
47        for (i, entry) in entries.iter().enumerate() {
48            // Index by canonical ID (lowercase)
49            index.entry(entry.id().to_lowercase()).or_insert(i);
50            // Index by litellm ID (lowercase)
51            index.entry(entry.litellm_id().to_lowercase()).or_insert(i);
52        }
53        let chat_price_breakpoints = ChatPriceBreakpoints::compute(entries.iter());
54        Self {
55            entries,
56            index,
57            chat_price_breakpoints,
58        }
59    }
60
61    /// Capability/cost tier of a model, classified against this registry's
62    /// chat-model price distribution. Unknown models classify as
63    /// [`ModelTier::Balanced`].
64    #[must_use]
65    pub fn tier(&self, model_id: &str) -> ModelTier {
66        self.lookup(model_id)
67            .map_or(ModelTier::Balanced, |entry| self.tier_of(entry))
68    }
69
70    /// Capability/cost tier of a specific registry entry.
71    #[must_use]
72    pub fn tier_of(&self, entry: &ModelEntry) -> ModelTier {
73        classify_entry(entry, self.chat_price_breakpoints)
74    }
75
76    /// Exact match by canonical ID or `LiteLLM` ID, then prefix match.
77    #[must_use]
78    pub fn lookup(&self, model_id: &str) -> Option<&ModelEntry> {
79        let lower = model_id.to_lowercase();
80
81        // Exact match
82        if let Some(&idx) = self.index.get(&lower) {
83            return Some(&self.entries[idx]);
84        }
85
86        // Prefix match: find the longest canonical ID that is a prefix of the query
87        self.entries
88            .iter()
89            .filter(|e| lower.starts_with(&e.id().to_lowercase()))
90            .max_by_key(|e| e.id().len())
91    }
92
93    /// All models from a given provider.
94    pub fn models_for_provider(&self, provider: &Provider) -> impl Iterator<Item = &ModelEntry> {
95        let provider = provider.clone();
96        self.entries
97            .iter()
98            .filter(move |e| *e.provider() == provider)
99    }
100
101    /// All models matching a given mode.
102    pub fn models_by_mode(&self, mode: ModelMode) -> impl Iterator<Item = &ModelEntry> {
103        self.entries.iter().filter(move |e| e.mode() == mode)
104    }
105
106    /// All models with a specific ability.
107    pub fn models_with_ability(&self, ability: Ability) -> impl Iterator<Item = &ModelEntry> {
108        self.entries.iter().filter(move |e| e.has_ability(ability))
109    }
110
111    /// Iterate over all entries.
112    pub fn all(&self) -> impl Iterator<Item = &ModelEntry> {
113        self.entries.iter()
114    }
115
116    /// Total number of entries.
117    #[must_use]
118    pub const fn len(&self) -> usize {
119        self.entries.len()
120    }
121
122    /// Whether the registry is empty.
123    #[must_use]
124    pub const fn is_empty(&self) -> bool {
125        self.entries.is_empty()
126    }
127}
128
129/// Convert a `ConvertedEntry` from the shared convert module into a `ModelEntry`.
130fn convert_to_entry(c: convert::ConvertedEntry) -> Option<ModelEntry> {
131    let provider = parse_provider_enum(&c.provider);
132    let mode = parse_mode_enum(&c.mode)?;
133
134    let mut pricing = crate::types::Pricing::new(c.input_cost_per_token, c.output_cost_per_token);
135    if let Some(v) = c.cache_read_per_token {
136        pricing = pricing.with_cache_read(v);
137    }
138    if let Some(v) = c.cache_write_per_token {
139        pricing = pricing.with_cache_write(v);
140    }
141    if let Some(v) = c.reasoning_per_token {
142        pricing = pricing.with_reasoning(v);
143    }
144    if let Some(v) = c.image_per_token {
145        pricing = pricing.with_image(v);
146    }
147
148    let abilities: Vec<Ability> = c
149        .abilities
150        .iter()
151        .filter_map(|a| parse_ability(a))
152        .collect();
153
154    let mut entry = ModelEntry::new(c.litellm_id, c.canonical_id, provider, mode)
155        .with_pricing(pricing)
156        .with_abilities(abilities);
157
158    if let Some(max_in) = c.max_input_tokens {
159        entry = entry.with_max_input_tokens(max_in);
160    }
161    if let Some(max_out) = c.max_output_tokens {
162        entry = entry.with_max_output_tokens(max_out);
163    }
164    if let Some(date) = c.deprecation_date {
165        entry = entry.with_deprecation_date(date);
166    }
167
168    Some(entry)
169}
170
171fn parse_provider_enum(s: &str) -> Provider {
172    match s {
173        "openai" => Provider::OpenAI,
174        "anthropic" => Provider::Anthropic,
175        "gemini" => Provider::Google,
176        "deepseek" => Provider::DeepSeek,
177        "xai" => Provider::XAI,
178        "mistral" => Provider::Mistral,
179        "meta" => Provider::Meta,
180        "alibaba" => Provider::Alibaba,
181        "bedrock" => Provider::Bedrock,
182        "azure" => Provider::Azure,
183        "vertex_ai" => Provider::VertexAI,
184        "groq" => Provider::Groq,
185        "together_ai" => Provider::Together,
186        "fireworks_ai" => Provider::Fireworks,
187        "replicate" => Provider::Replicate,
188        "cohere" => Provider::Cohere,
189        "perplexity" => Provider::Perplexity,
190        "github_copilot" => Provider::Copilot,
191        other => Provider::Other(std::borrow::Cow::Owned(other.to_string())),
192    }
193}
194
195fn parse_mode_enum(s: &str) -> Option<ModelMode> {
196    match s {
197        "chat" => Some(ModelMode::Chat),
198        "embedding" => Some(ModelMode::Embedding),
199        "image_generation" => Some(ModelMode::ImageGeneration),
200        "audio_transcription" => Some(ModelMode::AudioTranscription),
201        "audio_speech" => Some(ModelMode::AudioSpeech),
202        "rerank" => Some(ModelMode::Rerank),
203        "moderation" => Some(ModelMode::Moderation),
204        "completion" => Some(ModelMode::Completion),
205        "video_generation" => Some(ModelMode::VideoGeneration),
206        "search" => Some(ModelMode::Search),
207        "ocr" => Some(ModelMode::Ocr),
208        _ => None,
209    }
210}
211
212fn parse_ability(s: &str) -> Option<Ability> {
213    match s {
214        "ToolUse" => Some(Ability::ToolUse),
215        "Vision" => Some(Ability::Vision),
216        "Audio" => Some(Ability::Audio),
217        "AudioOutput" => Some(Ability::AudioOutput),
218        "Video" => Some(Ability::Video),
219        "WebSearch" => Some(Ability::WebSearch),
220        "Pdf" => Some(Ability::Pdf),
221        "CodeExecution" => Some(Ability::CodeExecution),
222        "Reasoning" => Some(Ability::Reasoning),
223        "ImageGeneration" => Some(Ability::ImageGeneration),
224        "ComputerUse" => Some(Ability::ComputerUse),
225        "PromptCaching" => Some(Ability::PromptCaching),
226        "AssistantPrefill" => Some(Ability::AssistantPrefill),
227        _ => None,
228    }
229}