返回 CodeWhale
reviewed.rs
根目录 / crates / config / src / catalog / reviewed.rs
1 //! Pure reviewed model facts in the existing catalog owner.
2 //!
3 //! The seed renderer copies the committed `catalog_corrections.json.reviewed`
4 //! data into the bundled Models.dev asset. Live/config/account rows never enter
5 //! this projection. An intrinsic fact or alias is metadata, not route authority;
6 //! executable requests still pass through the existing scoped RouteResolver.
7
8 use std::collections::{BTreeMap, BTreeSet};
9
10 use serde::{Deserialize, Serialize};
11
12 use crate::models_dev::{ModelsDevCatalog, ModelsDevModel};
13 use crate::route::{
14 CapabilityState, ModelId, ProviderId, ProviderModelOffering, RouteCapabilities, RouteLimits,
15 WireModelId,
16 };
17
18 /// Coarse model family for presentation only; never selects a provider.
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20 pub enum ModelFamily {
21 DeepSeek,
22 Anthropic,
23 OpenAi,
24 OpenAiCodex,
25 Moonshot,
26 Zai,
27 Minimax,
28 Stepfun,
29 Qwen,
30 Arcee,
31 Together,
32 XiaomiMimo,
33 Meta,
34 Xai,
35 Mistral,
36 Google,
37 Other,
38 }
39
40 /// Reviewed intrinsic facts. Missing fields remain unknown. The generation
41 /// default is separate from the route maximum (notably Kimi K3).
42 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
43 #[serde(deny_unknown_fields)]
44 pub struct IntrinsicModel {
45 #[serde(default)]
46 pub canonical_id: Option<String>,
47 #[serde(default)]
48 pub context_window: Option<u32>,
49 #[serde(default)]
50 pub max_output: Option<u32>,
51 #[serde(default)]
52 pub generation_default: Option<u32>,
53 #[serde(default)]
54 pub reasoning: Option<bool>,
55 #[serde(default)]
56 pub family: Option<ModelFamily>,
57 #[serde(default)]
58 pub display_name: Option<String>,
59 #[serde(default)]
60 pub aliases: Vec<String>,
61 #[serde(default)]
62 pub modalities: Vec<String>,
63 #[serde(default)]
64 pub supported_parameters: Vec<String>,
65 #[serde(default)]
66 pub capabilities: RouteCapabilities,
67 #[serde(default)]
68 pub architecture: Option<serde_json::Value>,
69 #[serde(default)]
70 pub latency: Option<serde_json::Value>,
71 pub source: String,
72 }
73
74 /// Exact provider-scoped selector aliases. Their order preserves the released
75 /// first-match contract. These rows do not assert live account availability.
76 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77 #[serde(deny_unknown_fields)]
78 pub struct ModelSelection {
79 pub provider: String,
80 pub id: String,
81 pub aliases: Vec<String>,
82 pub supports_tools: bool,
83 pub supports_reasoning: bool,
84 }
85
86 /// Pure transport facts required by model-aware providers, without a second
87 /// routing implementation or pricing authority.
88 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89 #[serde(deny_unknown_fields)]
90 pub struct ReviewedTransport {
91 pub provider: String,
92 pub id: String,
93 pub endpoint_key: String,
94 #[serde(default)]
95 pub canonical_model: Option<String>,
96 #[serde(default)]
97 pub default_for_provider: bool,
98 #[serde(default)]
99 pub limits: RouteLimits,
100 #[serde(default)]
101 pub capabilities: RouteCapabilities,
102 pub source: String,
103 }
104
105 impl ReviewedTransport {
106 #[must_use]
107 pub fn to_offering(&self) -> ProviderModelOffering {
108 ProviderModelOffering {
109 provider: ProviderId::from(self.provider.clone()),
110 canonical_model: self.canonical_model.clone().map(ModelId::from),
111 wire_model_id: WireModelId::from(self.id.clone()),
112 endpoint_key: self.endpoint_key.clone(),
113 default_for_provider: self.default_for_provider,
114 limits: self.limits,
115 capabilities: self.capabilities,
116 pricing: crate::route::PricingSku::UnknownOrStale,
117 }
118 }
119 }
120
121 /// Reviewed reference observations used by metadata-only presentation. They
122 /// never substitute for scoped live prices or authenticated billing facts.
123 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124 #[serde(deny_unknown_fields)]
125 pub struct ReferencePrice {
126 pub input: f64,
127 pub output: f64,
128 pub cache_read: f64,
129 #[serde(default)]
130 pub cache_write: Option<f64>,
131 pub source: String,
132 }
133
134 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135 #[serde(deny_unknown_fields)]
136 pub struct PublicModel {
137 pub id: String,
138 pub label: Option<String>,
139 pub added_at: Option<String>,
140 pub aliases: Vec<String>,
141 }
142
143 /// A retained compile-time contract name projected from an intrinsic field.
144 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145 #[serde(deny_unknown_fields)]
146 pub struct NumericRef {
147 pub model: String,
148 pub field: String,
149 }
150
151 /// Supplement to Models.dev's intrinsic/offering schema. The original tables
152 /// are retired into this one reviewed data owner, not reloaded from a global
153 /// provider cache. Runtime catalogs retain their existing scope and precedence.
154 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
155 #[serde(deny_unknown_fields)]
156 pub struct ReviewedCatalog {
157 #[serde(default)]
158 pub revision: String,
159 #[serde(default)]
160 pub intrinsic: BTreeMap<String, IntrinsicModel>,
161 #[serde(default)]
162 pub official_deepseek_aliases: BTreeMap<String, String>,
163 #[serde(default)]
164 pub openai_reasoning_ids: BTreeMap<String, String>,
165 #[serde(default)]
166 pub snapshot_prefixes: BTreeMap<String, String>,
167 #[serde(default)]
168 pub selections: Vec<ModelSelection>,
169 #[serde(default)]
170 pub public_models: Vec<PublicModel>,
171 #[serde(default)]
172 pub transports: Vec<ReviewedTransport>,
173 #[serde(default)]
174 pub constants: BTreeMap<String, String>,
175 #[serde(default)]
176 pub numeric_refs: BTreeMap<String, NumericRef>,
177 #[serde(default)]
178 pub groups: BTreeMap<String, Vec<String>>,
179 #[serde(default)]
180 pub compatibility_aliases: BTreeMap<String, BTreeMap<String, String>>,
181 #[serde(default)]
182 pub completion_rosters: BTreeMap<String, Vec<String>>,
183 #[serde(default)]
184 pub go_aliases: BTreeMap<String, String>,
185 #[serde(default)]
186 pub search_models: BTreeMap<String, Vec<String>>,
187 #[serde(default)]
188 pub route_model_sets: BTreeMap<String, Vec<String>>,
189 #[serde(default)]
190 pub reference_prices: BTreeMap<String, ReferencePrice>,
191 #[serde(default)]
192 pub reference_price_policies: BTreeMap<String, String>,
193 #[serde(default)]
194 pub source_receipts: BTreeMap<String, String>,
195 }
196
197 impl ReviewedCatalog {
198 /// Validate authored source data before it can become a bundled projection.
199 /// Empty is allowed only for external Models.dev documents with no supplement.
200 pub fn validate(&self) -> Result<(), String> {
201 if self.revision.is_empty() {
202 if self == &Self::default() {
203 return Ok(());
204 }
205 return Err("reviewed catalog revision missing".into());
206 }
207 let identifier = |s: &str| {
208 !s.is_empty() && s.len() <= 512 && s.trim() == s && !s.chars().any(char::is_control)
209 };
210 for (id, row) in &self.intrinsic {
211 if !identifier(id)
212 || row.source.trim().is_empty()
213 || row.context_window == Some(0)
214 || row.max_output == Some(0)
215 || row.generation_default == Some(0)
216 {
217 return Err(format!("malformed intrinsic model {id}"));
218 }
219 }
220 for row in &self.selections {
221 // Authored rows retain exact legacy table identities; the picker
222 // parser deliberately collapses those into vendor-primary names.
223 let provider = crate::ProviderKind::parse_config_identity(&row.provider);
224 if !identifier(&row.id)
225 || provider.is_none_or(|p| {
226 p.as_str() != row.provider || p == crate::ProviderKind::Antigravity
227 })
228 || row.aliases.iter().any(|s| !identifier(s))
229 {
230 return Err(format!(
231 "malformed provider selector {}/{}",
232 row.provider, row.id
233 ));
234 }
235 }
236 let mut routes = BTreeSet::new();
237 for row in &self.transports {
238 if !identifier(&row.id)
239 || row.source.trim().is_empty()
240 || !matches!(row.endpoint_key.as_str(), "chat" | "messages" | "responses")
241 || !routes.insert((&row.provider, &row.id))
242 || row.limits.context_tokens == Some(0)
243 || row.limits.output_tokens == Some(0)
244 || crate::ProviderKind::parse_config_identity(&row.provider).is_none_or(|p| {
245 p.as_str() != row.provider || p == crate::ProviderKind::Antigravity
246 })
247 {
248 return Err(format!("malformed transport {}/{}", row.provider, row.id));
249 }
250 }
251 for (id, row) in &self.reference_prices {
252 if !identifier(id)
253 || row.source.trim().is_empty()
254 || [
255 Some(row.input),
256 Some(row.output),
257 Some(row.cache_read),
258 row.cache_write,
259 ]
260 .into_iter()
261 .flatten()
262 .any(|v| !v.is_finite() || v < 0.0)
263 {
264 return Err(format!("malformed reference price {id}"));
265 }
266 }
267 for (alias, target) in self
268 .official_deepseek_aliases
269 .iter()
270 .chain(&self.go_aliases)
271 {
272 if !identifier(alias) || !identifier(target) {
273 return Err("malformed exact alias".into());
274 }
275 }
276 for (prefix, target) in &self.snapshot_prefixes {
277 if !identifier(prefix) || !self.intrinsic.contains_key(target) {
278 return Err("missing snapshot target".into());
279 }
280 }
281 for reference in self.numeric_refs.values() {
282 let row = self
283 .intrinsic
284 .get(&reference.model)
285 .ok_or("missing numeric model")?;
286 let value = match reference.field.as_str() {
287 "context_window" => row.context_window,
288 "max_output" => row.max_output,
289 "generation_default" => row.generation_default,
290 _ => return Err("unknown numeric model field".into()),
291 };
292 if value.is_none_or(|value| value == 0) {
293 return Err("missing numeric model fact".into());
294 }
295 }
296 for row in &self.public_models {
297 if !identifier(&row.id)
298 || !self.intrinsic.contains_key(&row.id.to_ascii_lowercase())
299 || row.label.as_deref().is_some_and(|label| !identifier(label))
300 || row.aliases.iter().any(|alias| !identifier(alias))
301 || row.added_at.as_deref().is_some_and(|date| {
302 chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").is_err()
303 })
304 {
305 return Err(format!("missing public model {}", row.id));
306 }
307 }
308 for (group, aliases) in &self.compatibility_aliases {
309 if !identifier(group)
310 || aliases
311 .iter()
312 .any(|(alias, target)| !identifier(alias) || !identifier(target))
313 {
314 return Err("malformed compatibility selector".into());
315 }
316 }
317 for (provider, entries) in &self.completion_rosters {
318 if provider != crate::descriptors::LEGACY_DEEPSEEK_CN.id
319 && crate::ProviderKind::parse_config_identity(provider)
320 .is_none_or(|kind| kind.as_str() != provider)
321 || entries.iter().any(|entry| !identifier(entry))
322 || matches!(provider.as_str(), "antigravity" | "custom") && !entries.is_empty()
323 {
324 return Err(format!("malformed completion roster {provider}"));
325 }
326 }
327 Ok(())
328 }
329 }
330
331 /// The same immutable parsed bundled catalog used by config's existing owner.
332 #[must_use]
333 pub fn bundled_reviewed() -> &'static ReviewedCatalog {
334 &super::bundled_models_dev_catalog().reviewed
335 }
336
337 /// Exact intrinsic lookup. Canonical facts win over provider enrichment; a
338 /// provider-scoped row is eligible only through an explicit, conflict-free join.
339 /// Never examine the process's live/custom/account catalog here.
340 #[must_use]
341 pub fn intrinsic_model(model: &str) -> Option<IntrinsicModel> {
342 intrinsic_model_in(super::bundled_models_dev_catalog(), model)
343 }
344
345 #[must_use]
346 pub fn intrinsic_model_in(catalog: &ModelsDevCatalog, model: &str) -> Option<IntrinsicModel> {
347 let id = model.trim().to_ascii_lowercase();
348 let reviewed = catalog.reviewed.intrinsic.get(&id);
349 let canonical = catalog
350 .models
351 .iter()
352 .find(|(key, _)| key.eq_ignore_ascii_case(&id))
353 .map(|(_, row)| row);
354 let mut result = reviewed.cloned().or_else(|| canonical.map(from_canonical));
355 if let Some(canonical) = canonical {
356 let canonical = from_canonical(canonical);
357 if let Some(row) = &mut result {
358 fill_missing(row, &canonical);
359 }
360 }
361 // A shared wire spelling does not establish a canonical model. Only
362 // explicit joins can supply missing intrinsic facts.
363 let joined = catalog
364 .providers
365 .values()
366 .flat_map(|provider| provider.models.iter())
367 .filter(|(_, row)| {
368 row.base_model
369 .as_deref()
370 .is_some_and(|base| base.eq_ignore_ascii_case(&id))
371 })
372 .collect::<Vec<_>>();
373 let joins = joined
374 .iter()
375 .filter_map(|(_, row)| row.base_model.as_deref())
376 .map(str::to_ascii_lowercase)
377 .collect::<BTreeSet<_>>();
378 if joins.len() == 1 {
379 // Enrich the named canonical model only. A provider-owned wire id is
380 // resolved through its scoped offering, never this model-only lookup.
381 if let Some(base) = joins.first().and_then(|base| {
382 catalog
383 .models
384 .iter()
385 .find(|(key, _)| key.eq_ignore_ascii_case(base))
386 .map(|(_, row)| row)
387 }) {
388 let canonical = from_canonical(base);
389 if let Some(row) = &mut result {
390 fill_missing(row, &canonical);
391 } else {
392 result = Some(canonical);
393 }
394 }
395 let consensus = IntrinsicModel {
396 context_window: consensus(joined.iter().filter_map(|(_, row)| {
397 row.limit
398 .as_ref()
399 .and_then(|v| v.context)
400 .and_then(|v| u32::try_from(v).ok())
401 })),
402 max_output: consensus(joined.iter().filter_map(|(_, row)| {
403 row.limit
404 .as_ref()
405 .and_then(|v| v.output)
406 .and_then(|v| u32::try_from(v).ok())
407 })),
408 reasoning: consensus(joined.iter().filter_map(|(_, row)| row.reasoning)),
409 source: "conflict-free explicitly joined bundled offerings".into(),
410 ..IntrinsicModel::default()
411 };
412 if let Some(row) = &mut result {
413 fill_missing(row, &consensus);
414 } else if consensus.context_window.is_some()
415 || consensus.max_output.is_some()
416 || consensus.reasoning.is_some()
417 {
418 result = Some(consensus);
419 }
420 }
421 result
422 }
423
424 fn consensus<T: Copy + PartialEq>(values: impl IntoIterator<Item = T>) -> Option<T> {
425 let mut values = values.into_iter();
426 let first = values.next()?;
427 values.all(|value| value == first).then_some(first)
428 }
429
430 fn from_canonical(row: &ModelsDevModel) -> IntrinsicModel {
431 IntrinsicModel {
432 canonical_id: (!row.id.is_empty()).then(|| row.id.clone()),
433 context_window: row
434 .limit
435 .as_ref()
436 .and_then(|v| v.context)
437 .and_then(|v| u32::try_from(v).ok()),
438 max_output: row
439 .limit
440 .as_ref()
441 .and_then(|v| v.output)
442 .and_then(|v| u32::try_from(v).ok()),
443 reasoning: row.reasoning,
444 display_name: row.name.clone(),
445 modalities: row
446 .modalities
447 .as_ref()
448 .map(|v| v.input.clone())
449 .unwrap_or_default(),
450 capabilities: RouteCapabilities {
451 reasoning: CapabilityState::from_optional_bool(row.reasoning),
452 native_tool_calls: CapabilityState::from_optional_bool(row.tool_call),
453 structured_output: CapabilityState::from_optional_bool(row.structured_output),
454 attachments: CapabilityState::from_optional_bool(row.attachment),
455 image_input: crate::models_dev::image_input_support(row.modalities.as_ref()),
456 ..RouteCapabilities::default()
457 },
458 source: "bundled canonical Models.dev row".into(),
459 ..IntrinsicModel::default()
460 }
461 }
462
463 fn fill_missing(row: &mut IntrinsicModel, other: &IntrinsicModel) {
464 row.context_window = row.context_window.or(other.context_window);
465 row.max_output = row.max_output.or(other.max_output);
466 row.reasoning = row.reasoning.or(other.reasoning);
467 row.family = row.family.or(other.family);
468 if row.display_name.is_none() {
469 row.display_name.clone_from(&other.display_name);
470 }
471 }
472
473 /// Exact public bundled identifiers only. Live/private/configured model IDs
474 /// can never enlarge a telemetry/UI public-label allowlist.
475 #[must_use]
476 pub fn public_model_identifier(model: &str) -> bool {
477 let catalog = super::bundled_models_dev_catalog();
478 catalog.reviewed.intrinsic.contains_key(model)
479 || catalog.models.contains_key(model)
480 || catalog.providers.values().any(|provider| {
481 provider
482 .models
483 .iter()
484 .any(|(id, row)| id == model || row.id == model)
485 })
486 }
487
488 #[must_use]
489 pub fn official_deepseek_model_id(model: &str) -> Option<&'static str> {
490 bundled_reviewed()
491 .official_deepseek_aliases
492 .get(&model.trim().to_ascii_lowercase())
493 .map(String::as_str)
494 }
495
496 /// Exact data-owned compatibility selector. Provider/endpoint policy remains
497 /// at the caller; this projection cannot choose or authorize a route.
498 #[must_use]
499 pub fn compatibility_alias(group: &str, model: &str) -> Option<&'static str> {
500 constants::compatibility_alias(group, model)
501 }
502
503 #[must_use]
504 pub fn route_model_set_contains(set: &str, model: &str) -> bool {
505 bundled_reviewed()
506 .route_model_sets
507 .get(set)
508 .is_some_and(|ids| ids.iter().any(|id| id.eq_ignore_ascii_case(model.trim())))
509 }
510
511 #[must_use]
512 pub fn reviewed_transport(provider: &str, model: &str) -> Option<&'static ReviewedTransport> {
513 bundled_reviewed()
514 .transports
515 .iter()
516 .find(|row| row.provider == provider && row.id == model)
517 }
518
519 /// Compile-time compatibility names are generated from the same JSON owner.
520 pub mod constants {
521 include!(concat!(env!("OUT_DIR"), "/catalog_constants.rs"));
522 }
523
524 /// Actual source fetch time of the existing generated seed, not an unrelated
525 /// former bundled asset date. Callers evaluate TTL against their current clock.
526 #[must_use]
527 pub fn bundled_source_fetched_at() -> Option<u64> {
528 let source = super::bundled_models_dev_catalog()
529 .meta
530 .get("upstream")?
531 .as_str()?;
532 let (_, source) = source.split_once(" fetched ")?;
533 let time = source.split_whitespace().next()?;
534 u64::try_from(chrono::DateTime::parse_from_rfc3339(time).ok()?.timestamp()).ok()
535 }
536
537 #[cfg(test)]
538 mod tests {
539 use super::*;
540
541 fn catalog(value: serde_json::Value) -> ModelsDevCatalog {
542 ModelsDevCatalog::parse_json(&value.to_string()).expect("fixture")
543 }
544
545 #[test]
546 fn identical_wire_names_do_not_merge_provider_private_facts() {
547 let data = catalog(serde_json::json!({ "providers": {
548 "one": { "models": { "private-name": { "id": "private-name", "reasoning": true, "limit": { "context": 900000 } } } },
549 "two": { "models": { "private-name": { "id": "private-name", "reasoning": false, "limit": { "context": 32000 } } } }
550 }}));
551 assert!(intrinsic_model_in(&data, "private-name").is_none());
552 assert_eq!(
553 data.provider_offering("one", "private-name")
554 .unwrap()
555 .limits
556 .context_tokens,
557 Some(900000)
558 );
559 assert_eq!(
560 data.provider_offering("two", "private-name")
561 .unwrap()
562 .limits
563 .context_tokens,
564 Some(32000)
565 );
566 assert!(!public_model_identifier("private-name"));
567 }
568
569 #[test]
570 fn proven_join_preserves_canonical_facts_and_wire_lookup_stays_scoped() {
571 let mut fixture = serde_json::json!({
572 "models": { "canonical": { "id": "canonical", "reasoning": false, "limit": { "context": 128000 } } },
573 "providers": { "one": { "models": { "wire": { "id": "wire", "base_model": "canonical", "reasoning": true, "limit": { "context": 900000 } } } } }
574 });
575 let data = catalog(fixture.clone());
576 assert!(intrinsic_model_in(&data, "wire").is_none());
577 assert_eq!(
578 data.provider_offering("one", "wire")
579 .expect("exact provider-owned offering")
580 .limits
581 .context_tokens,
582 Some(900000)
583 );
584 let joined = intrinsic_model_in(&data, "canonical").unwrap();
585 assert_eq!(joined.canonical_id.as_deref(), Some("canonical"));
586 assert_eq!(joined.context_window, Some(128000));
587 assert_eq!(joined.reasoning, Some(false));
588 fixture["providers"]["two"] = serde_json::json!({ "models": { "wire": { "id": "wire", "base_model": "different", "reasoning": true } } });
589 assert!(intrinsic_model_in(&catalog(fixture), "wire").is_none());
590 }
591
592 #[test]
593 fn reviewed_selection_and_transport_preserve_separate_scopes() {
594 let reviewed = bundled_reviewed();
595 reviewed.validate().expect("committed data");
596 assert_eq!(reviewed.selections.len(), 175);
597 assert_eq!(reviewed.transports.len(), 123);
598 let go = reviewed_transport("opencode-go", "gpt-5.6-luna").unwrap();
599 assert_eq!(go.endpoint_key, "responses");
600 assert_eq!(
601 go.limits,
602 RouteLimits::default(),
603 "Go metadata is enriched by existing resolver, not borrowed from another account"
604 );
605 assert_eq!(
606 reviewed_transport("opencode-zen", "claude-opus-5")
607 .unwrap()
608 .endpoint_key,
609 "messages"
610 );
611 assert!(reviewed_transport("custom", "gpt-5.6").is_none());
612 assert!(constants::completion_names("custom").is_empty());
613 assert!(constants::completion_names("antigravity").is_empty());
614 assert_eq!(
615 compatibility_alias("canonical_zai_model_id", "glm-5.2"),
616 Some("GLM-5.2")
617 );
618 assert_eq!(
619 compatibility_alias("canonical_zai_model_id", "glm-5.3"),
620 Some("GLM-5.3")
621 );
622 }
623
624 #[test]
625 fn generation_default_does_not_assert_route_maximum_or_unknown_capability() {
626 let kimi = intrinsic_model("kimi-k3").unwrap();
627 assert_eq!(kimi.generation_default, Some(131072));
628 assert_ne!(kimi.generation_default, Some(1_048_576));
629 assert_eq!(kimi.capabilities.streaming, CapabilityState::Unknown);
630 assert!(intrinsic_model("kimi-k3-private").is_none());
631 }
632
633 #[test]
634 fn exact_legacy_identities_validate_without_accepting_picker_aliases() {
635 let reviewed = ReviewedCatalog {
636 revision: "fixture".into(),
637 selections: vec![ModelSelection {
638 provider: "minimax-anthropic".into(),
639 id: "MiniMax-M3".into(),
640 aliases: vec![],
641 supports_tools: true,
642 supports_reasoning: true,
643 }],
644 transports: vec![ReviewedTransport {
645 provider: "modelstudio-coding-plan-anthropic".into(),
646 id: "qwen".into(),
647 endpoint_key: "messages".into(),
648 canonical_model: None,
649 default_for_provider: false,
650 limits: RouteLimits::default(),
651 capabilities: RouteCapabilities::default(),
652 source: "fixture".into(),
653 }],
654 ..ReviewedCatalog::default()
655 };
656 reviewed
657 .validate()
658 .expect("exact released config identities");
659 for bad in ["mini-max-anthropic", "unknown-provider", "antigravity"] {
660 let mut malformed = reviewed.clone();
661 malformed.selections[0].provider = bad.into();
662 assert!(malformed.validate().is_err(), "selector {bad}");
663 let mut malformed = reviewed.clone();
664 malformed.transports[0].provider = bad.into();
665 assert!(malformed.validate().is_err(), "transport {bad}");
666 }
667 }
668
669 #[test]
670 fn malformed_authored_rows_are_refused_before_projection() {
671 for bad in ["unknown-provider", "antigravity", "openai-compatible"] {
672 let mut reviewed = bundled_reviewed().clone();
673 reviewed.selections[0].provider = bad.into();
674 assert!(reviewed.validate().is_err());
675 }
676 let mut reviewed = bundled_reviewed().clone();
677 reviewed.transports[0].limits.output_tokens = Some(0);
678 assert!(reviewed.validate().is_err());
679 reviewed = bundled_reviewed().clone();
680 reviewed
681 .completion_rosters
682 .insert("custom".into(), vec!["private-name".into()]);
683 assert!(reviewed.validate().is_err());
684 }
685 }
686
686 lines RUST