1use 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
15const NEW_MODEL_NEEDS: [&str; 3] = ["reasoning", "tool_call", "modalities"];
19
20#[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#[non_exhaustive]
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct OverrideError {
41 pub path: String,
45 pub kind: OverrideErrorKind,
47}
48
49#[non_exhaustive]
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum OverrideErrorKind {
53 UnknownVendor {
56 suggestion: Option<&'static str>,
58 },
59 UnknownField {
61 suggestion: Option<&'static str>,
63 },
64 NewModel {
68 missing: Vec<&'static str>,
70 suggestion: Option<String>,
73 },
74 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#[non_exhaustive]
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct Skipped {
117 pub path: String,
119 pub reason: SkipReason,
121}
122
123#[non_exhaustive]
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub enum SkipReason {
127 UnknownVendor,
129 Invalid(String),
131}
132
133impl Catalog {
134 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 §ions {
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 pub fn from_models_dev(json: &str) -> Result<(Catalog, Vec<Skipped>), CatalogError> {
275 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
327fn 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
361fn vendor_keys() -> impl Iterator<Item = &'static str> {
364 ProviderId::catalog_vendors().chain(KEYS.iter().map(|(models_dev, _)| *models_dev))
365}
366
367fn 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
380fn 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;