1use 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
15type Entries<'a> = indexmap::IndexMap<String, &'a RawValue>;
17
18const NEW_MODEL_NEEDS: [&str; 3] = ["reasoning", "tool_call", "modalities"];
22
23#[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#[non_exhaustive]
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct OverrideError {
44 pub path: String,
48 pub kind: OverrideErrorKind,
50}
51
52#[non_exhaustive]
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum OverrideErrorKind {
56 UnknownVendor {
59 suggestion: Option<&'static str>,
61 },
62 UnknownField {
64 suggestion: Option<&'static str>,
66 },
67 NewModel {
71 missing: Vec<&'static str>,
73 suggestion: Option<String>,
76 },
77 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#[non_exhaustive]
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct Skipped {
120 pub path: String,
122 pub reason: SkipReason,
124}
125
126#[non_exhaustive]
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum SkipReason {
130 UnknownVendor,
132 Invalid(String),
134}
135
136impl Catalog {
137 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 §ions {
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 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 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
349fn unknown_fields(
353 object: &Entries<'_>,
354 path: &[String],
355 report: &mut dyn FnMut(String, Option<&'static str>),
356) -> Result<(), OverrideError> {
357 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
390fn vendor_keys() -> impl Iterator<Item = &'static str> {
393 ProviderId::catalog_vendors().chain(KEYS.iter().map(|(models_dev, _)| *models_dev))
394}
395
396fn 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
409fn 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;