Skip to main content

rig_core/catalog/
read.rs

1//! The two readers of catalog data: a strict one for a user's override file,
2//! which reports every mistake with its path, and a lenient one for
3//! models.dev's `api.json`, which skips what it cannot use and says so.
4
5use std::fmt;
6
7use serde::Deserialize;
8use serde_json::{Map, Value};
9
10use super::lookup::{self, edit_distance};
11use super::row::{Row, known_fields};
12use super::{Catalog, CatalogError, KEYS, vendor_of};
13use crate::providers::registry::ProviderId;
14
15/// The row keys a model the base catalog does not list must set, since an
16/// absent one reads as "the model does not": reason, call tools, or read
17/// any input.
18const NEW_MODEL_NEEDS: [&str; 3] = ["reasoning", "tool_call", "modalities"];
19
20/// Every mistake [`Catalog::from_overrides`] found, in file order.
21#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
22pub struct OverrideErrors(pub Vec<OverrideError>);
23
24impl fmt::Display for OverrideErrors {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(f, "the catalog override file has ")?;
27        match self.0.as_slice() {
28            [one] => write!(f, "an error: {one}"),
29            errors => {
30                write!(f, "{} errors:", errors.len())?;
31                errors.iter().try_for_each(|error| write!(f, "\n  {error}"))
32            }
33        }
34    }
35}
36
37/// One mistake in an override file.
38#[non_exhaustive]
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct OverrideError {
41    /// Where it is: the keys leading to it, joined by `.`
42    /// (`antropic`, `anthropic.models.claude-opus-5-5.limits`). Empty for
43    /// the file as a whole.
44    pub path: String,
45    /// What is wrong there.
46    pub kind: OverrideErrorKind,
47}
48
49/// What is wrong at an [`OverrideError`]'s path.
50#[non_exhaustive]
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum OverrideErrorKind {
53    /// A provider key that is neither a rig vendor nor a models.dev key for
54    /// one, with the closest that is.
55    UnknownVendor {
56        /// The closest known key, if any is close.
57        suggestion: Option<&'static str>,
58    },
59    /// A key rig does not read at that place, with the closest that it does.
60    UnknownField {
61        /// The closest known key, if any is close.
62        suggestion: Option<&'static str>,
63    },
64    /// A model the catalog the file is laid over does not list, whose row
65    /// leaves out keys that would otherwise read as "the model does not".
66    /// Most often the id is misspelt.
67    NewModel {
68        /// The keys the row must set to add the model.
69        missing: Vec<&'static str>,
70        /// The closest id the catalog lists for that vendor, if any is
71        /// close: for a dated snapshot, the model it is a snapshot of.
72        suggestion: Option<String>,
73    },
74    /// A value of the wrong type or form, or text that is not JSON.
75    InvalidValue(String),
76}
77
78impl fmt::Display for OverrideError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let at = if self.path.is_empty() {
81            String::new()
82        } else {
83            format!("`{}`: ", self.path)
84        };
85        let hint = |suggestion: Option<&str>| {
86            suggestion.map_or_else(String::new, |close| format!("; did you mean `{close}`?"))
87        };
88        match &self.kind {
89            OverrideErrorKind::UnknownVendor { suggestion } => {
90                write!(f, "{at}unknown vendor{}", hint(*suggestion))
91            }
92            OverrideErrorKind::UnknownField { suggestion } => {
93                write!(f, "{at}unknown field{}", hint(*suggestion))
94            }
95            OverrideErrorKind::NewModel {
96                missing,
97                suggestion,
98            } => write!(
99                f,
100                "{at}not a listed model{}; to add it, set {}",
101                hint(suggestion.as_deref()),
102                missing
103                    .iter()
104                    .map(|key| format!("`{key}`"))
105                    .collect::<Vec<_>>()
106                    .join(", ")
107            ),
108            OverrideErrorKind::InvalidValue(message) => write!(f, "{at}{message}"),
109        }
110    }
111}
112
113/// A part of a models.dev file [`Catalog::from_models_dev`] did not read.
114#[non_exhaustive]
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct Skipped {
117    /// The provider key, or `vendor.models.id` for one row.
118    pub path: String,
119    /// Why it was skipped.
120    pub reason: SkipReason,
121}
122
123/// Why [`Catalog::from_models_dev`] skipped part of its input.
124#[non_exhaustive]
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub enum SkipReason {
127    /// The provider is not one rig knows.
128    UnknownVendor,
129    /// The provider's section, or one model's row, does not read.
130    Invalid(String),
131}
132
133impl Catalog {
134    /// Read a user's override file, laid over `base` afterwards with
135    /// [`Self::with_overrides`]. The file is in the shape of
136    /// [`Self::from_models_dev`]'s, with each section holding only
137    /// `models`, and each row only the keys rig reads plus its hand-entered
138    /// facts under `rig`.
139    ///
140    /// `base` is the catalog the file will be laid over. A row for a model
141    /// `base` does not list exactly adds that model, so it must set
142    /// `reasoning`, `tool_call` and `modalities`, which an absent key would
143    /// otherwise turn into "the model does not". That catches a misspelt
144    /// model id, and a dated snapshot id whose row would hide its base
145    /// model's facts.
146    ///
147    /// # Errors
148    ///
149    /// [`OverrideErrors`] holding every mistake found, each with its path:
150    /// an unknown vendor key or field, with the closest known one; a new
151    /// model that leaves out a needed key, with the closest listed id; and
152    /// a value of the wrong type. Text that is not a JSON object is one
153    /// error with an empty path.
154    pub fn from_overrides(json: &str, base: &Catalog) -> Result<Catalog, OverrideErrors> {
155        let whole = |message: String| {
156            OverrideErrors(vec![OverrideError {
157                path: String::new(),
158                kind: OverrideErrorKind::InvalidValue(message),
159            }])
160        };
161        let sections: Map<String, Value> =
162            serde_json::from_str(json).map_err(|error| whole(error.to_string()))?;
163        let mut errors = Vec::new();
164        let mut error = |path: String, kind| errors.push(OverrideError { path, kind });
165        let mut catalog = Catalog::default();
166        for (key, section) in &sections {
167            let Some(provider) = ProviderId::catalog(vendor_of(key)) else {
168                let suggestion = closest(key, vendor_keys());
169                error(key.clone(), OverrideErrorKind::UnknownVendor { suggestion });
170                continue;
171            };
172            let Some(section) = section.as_object() else {
173                error(key.clone(), invalid("expected an object with `models`"));
174                continue;
175            };
176            for field in section.keys().filter(|field| *field != "models") {
177                let suggestion = closest(field, ["models"]);
178                error(
179                    format!("{key}.{field}"),
180                    OverrideErrorKind::UnknownField { suggestion },
181                );
182            }
183            let Some(models) = section.get("models") else {
184                continue;
185            };
186            let Some(models) = models.as_object() else {
187                error(
188                    format!("{key}.models"),
189                    invalid("expected an object of model ids"),
190                );
191                continue;
192            };
193            for (id, value) in models {
194                let path = format!("{key}.models.{id}");
195                let Some(fields) = value.as_object() else {
196                    error(path, invalid("expected an object"));
197                    continue;
198                };
199                let mut problems = Vec::new();
200                unknown_fields(fields, &[], &mut |at, suggestion| {
201                    problems.push((
202                        format!("{path}.{at}"),
203                        OverrideErrorKind::UnknownField { suggestion },
204                    ));
205                });
206                let row = match serde_path_to_error::deserialize::<_, Row>(value) {
207                    Ok(row) => Some(row),
208                    Err(failed) => {
209                        let at = failed.path().to_string();
210                        let at = if at == "." {
211                            path.clone()
212                        } else {
213                            format!("{path}.{at}")
214                        };
215                        problems.push((at, invalid(&failed.into_inner().to_string())));
216                        None
217                    }
218                };
219                if let Some(format) = row.as_ref().and_then(Row::format)
220                    && ProviderId::new(provider.vendor(), format).is_none()
221                {
222                    problems.push((
223                        format!("{path}.rig.format"),
224                        invalid(&format!(
225                            "`{}` speaks no {format} endpoint in this build",
226                            provider.vendor()
227                        )),
228                    ));
229                }
230                if base.exact(provider.vendor(), id).is_none() {
231                    let missing: Vec<&'static str> = NEW_MODEL_NEEDS
232                        .into_iter()
233                        .filter(|needed| !fields.contains_key(*needed))
234                        .collect();
235                    if !missing.is_empty() {
236                        let suggestion = closest_model(base, provider, id);
237                        problems.push((
238                            path.clone(),
239                            OverrideErrorKind::NewModel {
240                                missing,
241                                suggestion,
242                            },
243                        ));
244                    }
245                }
246                match (row, problems.is_empty()) {
247                    (Some(row), true) => catalog.put(provider, id, row),
248                    _ => problems
249                        .into_iter()
250                        .for_each(|(path, kind)| error(path, kind)),
251                }
252            }
253        }
254        match errors.is_empty() {
255            true => Ok(catalog),
256            false => Err(OverrideErrors(errors)),
257        }
258    }
259
260    /// Read models.dev's `api.json`, or a file in its shape: an object of
261    /// provider keys, each with a `models` object keyed by model id. A
262    /// provider key is a models.dev key (`google`, `amazon-bedrock`) or a
263    /// rig vendor name (`gcp.gemini`). Keys rig does not read are ignored,
264    /// and a provider rig does not know, or a section or row that does not
265    /// read, is skipped and listed, so the rest of the file still loads.
266    ///
267    /// A row may carry rig's hand-entered facts under `rig`, among them
268    /// `format`, the protocol family the model is reached by when that is
269    /// not its vendor's own (see [`ProviderId::catalog`]).
270    ///
271    /// # Errors
272    ///
273    /// [`CatalogError::Json`] when the text is not a JSON object.
274    pub fn from_models_dev(json: &str) -> Result<(Catalog, Vec<Skipped>), CatalogError> {
275        /// What the file holds, in its order, each row left as its text
276        /// until it is read.
277        type Entries<'a> = indexmap::IndexMap<String, &'a serde_json::value::RawValue>;
278        #[derive(Deserialize)]
279        struct Section<'a> {
280            #[serde(borrow)]
281            models: Option<Entries<'a>>,
282        }
283        let sections: Entries<'_> = serde_json::from_str(json)?;
284        let mut skipped = Vec::new();
285        let mut skip = |path: String, reason| skipped.push(Skipped { path, reason });
286        let mut catalog = Catalog::default();
287        for (key, section) in sections {
288            let Some(provider) = ProviderId::catalog(vendor_of(&key)) else {
289                skip(key, SkipReason::UnknownVendor);
290                continue;
291            };
292            let section = serde_json::from_str::<Section<'_>>(section.get());
293            let Some(models) = section.ok().and_then(|section| section.models) else {
294                skip(key, SkipReason::Invalid("no `models` object".to_owned()));
295                continue;
296            };
297            for (id, text) in models {
298                let path = format!("{key}.models.{id}");
299                let row = match serde_json::from_str::<Row>(text.get()) {
300                    Ok(row) => row,
301                    Err(error) => {
302                        skip(path, SkipReason::Invalid(error.to_string()));
303                        continue;
304                    }
305                };
306                if let Some(format) = row.format()
307                    && ProviderId::new(provider.vendor(), format).is_none()
308                {
309                    let reason = format!(
310                        "`rig.format`: `{}` speaks no {format} endpoint in this build",
311                        provider.vendor()
312                    );
313                    skip(path, SkipReason::Invalid(reason));
314                    continue;
315                }
316                catalog.put(provider, &id, row);
317            }
318        }
319        Ok((catalog, skipped))
320    }
321}
322
323fn invalid(message: &str) -> OverrideErrorKind {
324    OverrideErrorKind::InvalidValue(message.to_owned())
325}
326
327/// Report each key of `object`, the object at `path` inside a row, that rig
328/// does not read there, then the same for each object below it.
329fn unknown_fields(
330    object: &Map<String, Value>,
331    path: &[String],
332    report: &mut dyn FnMut(String, Option<&'static str>),
333) {
334    let keys: Vec<&str> = path.iter().map(String::as_str).collect();
335    let Some(known) = known_fields(&keys) else {
336        return;
337    };
338    for (key, value) in object {
339        let mut at = path.to_vec();
340        at.push(key.clone());
341        if !known.contains(&key.as_str()) {
342            report(at.join("."), closest(key, known.iter().copied()));
343            continue;
344        }
345        match value {
346            Value::Object(inner) => unknown_fields(inner, &at, report),
347            Value::Array(items) => {
348                for (index, item) in items.iter().enumerate() {
349                    if let Value::Object(inner) = item {
350                        let mut at = at.clone();
351                        at.push(index.to_string());
352                        unknown_fields(inner, &at, report);
353                    }
354                }
355            }
356            _ => {}
357        }
358    }
359}
360
361/// Every provider key a catalog file may use: rig's vendor names and the
362/// models.dev keys that name one.
363fn vendor_keys() -> impl Iterator<Item = &'static str> {
364    ProviderId::catalog_vendors().chain(KEYS.iter().map(|(models_dev, _)| *models_dev))
365}
366
367/// The candidate closest to `word` by edit distance, if within a quarter of
368/// its length (at least one edit), so a typo finds its key and a different
369/// word (`output` for `input`) finds none.
370fn closest(word: &str, candidates: impl IntoIterator<Item = &'static str>) -> Option<&'static str> {
371    let limit = (word.chars().count() / 4).max(1);
372    candidates
373        .into_iter()
374        .map(|candidate| (edit_distance(word, candidate), candidate))
375        .filter(|(distance, _)| *distance <= limit)
376        .min()
377        .map(|(_, candidate)| candidate)
378}
379
380/// The id `base` lists for `provider`'s vendor that `id` most likely means:
381/// the model a dated snapshot id extends, else the closest listed id.
382fn closest_model(base: &Catalog, provider: ProviderId, id: &str) -> Option<String> {
383    if let Some(found) = base.get_vendor(provider.vendor(), id) {
384        return Some(found.spec.id.clone());
385    }
386    let vendor = provider.vendor();
387    let reference = format!("{vendor}/{id}");
388    lookup::suggestions(
389        &reference,
390        base.iter().filter(|spec| spec.provider.vendor() == vendor),
391    )
392    .into_iter()
393    .next()
394    .and_then(|close| close.strip_prefix(&format!("{vendor}/")).map(str::to_owned))
395}
396
397#[cfg(test)]
398mod tests;