1mod row;
26mod spec;
27
28use std::collections::BTreeMap;
29use std::sync::OnceLock;
30
31use serde::Deserialize;
32
33use crate::providers::registry::{Format, ProviderId};
34use row::Row;
35
36#[doc(hidden)]
37pub use spec::Compat;
38pub use spec::{CacheSupport, Modalities, ModelSpec, Pricing, ReasoningSupport, Sampling};
39
40const BUILTIN: &str = include_str!("models.json");
42
43#[derive(Clone, Debug, Default)]
45pub struct Catalog {
46 entries: Vec<Entry>,
48}
49
50#[derive(Clone, Debug)]
53struct Entry {
54 row: Row,
55 spec: ModelSpec,
56}
57
58#[non_exhaustive]
60#[derive(Debug, thiserror::Error)]
61pub enum CatalogError {
62 #[error("not a models.dev-style catalog: {0}")]
65 Json(#[from] serde_json::Error),
66}
67
68#[derive(Deserialize)]
71struct Section {
72 #[serde(default)]
73 models: BTreeMap<String, Row>,
74}
75
76const KEYS: [(&str, &str); 9] = [
79 ("azure", "azure.openai"),
80 ("google", "gcp.gemini"),
81 ("google-vertex", "vertexai"),
82 ("amazon-bedrock", "aws_bedrock"),
83 ("togetherai", "together"),
84 ("moonshotai", "moonshot"),
85 ("xiaomi", "xiaomimimo"),
86 ("ollama-cloud", "ollama"),
87 ("github-copilot", "copilot"),
88];
89
90impl Catalog {
91 pub fn builtin() -> &'static Catalog {
93 static BUILTIN_CATALOG: OnceLock<Catalog> = OnceLock::new();
94 BUILTIN_CATALOG.get_or_init(|| Catalog::from_json(BUILTIN).unwrap_or_default())
96 }
97
98 pub fn get(&self, provider: ProviderId, model: &str) -> Option<&ModelSpec> {
101 self.exact(provider.vendor(), model)
102 }
103
104 pub fn resolve(&self, reference: &str) -> Option<&ModelSpec> {
113 let (vendor, model) = split_reference(reference)?;
114 self.exact(vendor, model)
115 }
116
117 pub fn iter(&self) -> impl Iterator<Item = &ModelSpec> {
119 self.entries.iter().map(|entry| &entry.spec)
120 }
121
122 pub fn from_json(json: &str) -> Result<Catalog, CatalogError> {
129 let sections: BTreeMap<String, Section> = serde_json::from_str(json)?;
130 let mut catalog = Catalog::default();
131 for (key, section) in sections {
132 let Some(provider) = ProviderId::catalog(vendor_of(&key)) else {
133 continue;
134 };
135 for (id, row) in section.models {
136 catalog.put(provider, &id, row);
137 }
138 }
139 Ok(catalog)
140 }
141
142 pub fn merge(mut self, overrides: Catalog) -> Catalog {
146 for entry in overrides.entries {
147 self.put(entry.spec.provider, &entry.spec.id, entry.row);
148 }
149 self
150 }
151
152 pub fn find(&self, provider: ProviderId, model: &str) -> Option<&ModelSpec> {
157 self.find_vendor(provider.vendor(), model)
158 }
159
160 pub(crate) fn find_vendor(&self, vendor: &str, model: &str) -> Option<&ModelSpec> {
162 self.exact(vendor, model)
163 .or_else(|| self.exact(vendor, undated(model)?))
164 }
165
166 fn exact(&self, vendor: &str, model: &str) -> Option<&ModelSpec> {
167 self.position(vendor, model)
168 .ok()
169 .and_then(|index| self.entries.get(index))
170 .map(|entry| &entry.spec)
171 }
172
173 fn position(&self, vendor: &str, model: &str) -> Result<usize, usize> {
174 self.entries.binary_search_by(|entry| {
175 (entry.spec.provider.vendor(), entry.spec.id.as_str()).cmp(&(vendor, model))
176 })
177 }
178
179 fn put(&mut self, provider: ProviderId, id: &str, row: Row) {
181 match self.position(provider.vendor(), id) {
182 Ok(index) => {
183 if let Some(entry) = self.entries.get_mut(index) {
184 let row = std::mem::take(&mut entry.row).overlay(row);
185 entry.spec = row.spec(entry.spec.provider, id);
186 entry.row = row;
187 }
188 }
189 Err(index) => {
190 let spec = row.spec(provider, id);
191 self.entries.insert(index, Entry { row, spec });
192 }
193 }
194 }
195}
196
197pub(crate) fn lookup(vendor: &str, model: &str) -> Option<&'static ModelSpec> {
200 Catalog::builtin().find_vendor(vendor, model)
201}
202
203pub(crate) fn lookup_snapshot(vendor: &str, model: &str) -> Option<&'static ModelSpec> {
208 let catalog = Catalog::builtin();
209 catalog.exact(vendor, model).or_else(|| {
210 model
211 .match_indices("-20")
212 .filter_map(|(at, _)| catalog.exact(vendor, model.get(..at)?))
213 .last()
214 })
215}
216
217pub(crate) fn reads_images_or(vendor: &str, model: &str, rule: impl FnOnce(&str) -> bool) -> bool {
223 lookup(vendor, model).map_or_else(|| rule(model), |spec| spec.input.image)
224}
225
226fn vendor_of(key: &str) -> &str {
228 KEYS.iter()
229 .find_map(|(models_dev, vendor)| (*models_dev == key).then_some(*vendor))
230 .unwrap_or(key)
231}
232
233pub(crate) fn split_reference(reference: &str) -> Option<(&str, &str)> {
236 if let Some((selection, model)) = reference.split_once(':') {
237 let vendor = match selection.split_once('/') {
238 None => Some(selection),
239 Some((vendor, format)) => Format::named(format).map(|_| vendor),
240 };
241 if let Some(vendor) = vendor.filter(|vendor| !vendor.is_empty()) {
242 return (!model.is_empty()).then_some((vendor, model));
243 }
244 }
245 reference
246 .split_once('/')
247 .filter(|(vendor, model)| !vendor.is_empty() && !model.is_empty())
248}
249
250fn undated(model: &str) -> Option<&str> {
252 let digits = |text: &str| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
253 let (rest, last) = model.rsplit_once('-')?;
254 if last.len() == 8 && digits(last) {
255 return Some(rest);
256 }
257 let (rest, month) = rest.rsplit_once('-')?;
258 let (rest, year) = rest.rsplit_once('-')?;
259 (year.len() == 4 && month.len() == 2 && last.len() == 2)
260 .then_some(())
261 .filter(|()| digits(year) && digits(month) && digits(last))
262 .map(|()| rest)
263}
264
265#[cfg(test)]
266mod tests;