aither_models/
registry.rs1use 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
12include!(concat!(env!("OUT_DIR"), "/generated.rs"));
17
18#[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 #[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 #[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.entry(entry.id().to_lowercase()).or_insert(i);
50 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 #[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 #[must_use]
72 pub fn tier_of(&self, entry: &ModelEntry) -> ModelTier {
73 classify_entry(entry, self.chat_price_breakpoints)
74 }
75
76 #[must_use]
78 pub fn lookup(&self, model_id: &str) -> Option<&ModelEntry> {
79 let lower = model_id.to_lowercase();
80
81 if let Some(&idx) = self.index.get(&lower) {
83 return Some(&self.entries[idx]);
84 }
85
86 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 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 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 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 pub fn all(&self) -> impl Iterator<Item = &ModelEntry> {
113 self.entries.iter()
114 }
115
116 #[must_use]
118 pub const fn len(&self) -> usize {
119 self.entries.len()
120 }
121
122 #[must_use]
124 pub const fn is_empty(&self) -> bool {
125 self.entries.is_empty()
126 }
127}
128
129fn 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}