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