1use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize)]
9pub struct HealthcheckDef {
10 pub method: String,
11 #[serde(default)]
12 pub path: Option<String>,
13 #[serde(default)]
14 pub url: Option<String>,
15 #[serde(default)]
16 pub body: Option<String>,
17}
18
19#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
20pub struct LocalRuntimeDef {
21 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub kind: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub command: Option<String>,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
31 pub prefix_args: Vec<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub model_source: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub model_source_env: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub default_port: Option<u16>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub model_arg: Option<String>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub served_model_arg: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub host_arg: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub port_arg: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub ctx_arg: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub parallel_arg: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub gpu_layers_arg: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub cache_type_k_arg: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub cache_type_v_arg: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub cache_ram_arg: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub enable_lora_arg: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub lora_modules_arg: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub lora_modules_value_format: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub max_lora_rank_arg: Option<String>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub default_args: Vec<String>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub stop: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub source_url: Option<String>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub last_verified: Option<String>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub notes: Option<String>,
90}
91
92#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
93pub struct LocalMemoryDef {
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub measured_resident_gib: Option<f64>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub measured_context_window: Option<u64>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub measured_cache_type: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub base_resident_gib: Option<f64>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub kv_cache_gib_per_1k_ctx: Option<f64>,
110 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
112 pub cache_type_multipliers: BTreeMap<String, f64>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub default_cache_type: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub safety_margin_gib: Option<f64>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub max_recommended_context: Option<u64>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub source_url: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub last_verified: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub notes: Option<String>,
131}
132
133impl LocalMemoryDef {
134 pub fn is_empty(&self) -> bool {
135 self.measured_resident_gib.is_none()
136 && self.measured_context_window.is_none()
137 && self.measured_cache_type.is_none()
138 && self.base_resident_gib.is_none()
139 && self.kv_cache_gib_per_1k_ctx.is_none()
140 && self.cache_type_multipliers.is_empty()
141 && self.default_cache_type.is_none()
142 && self.safety_margin_gib.is_none()
143 && self.max_recommended_context.is_none()
144 && self.source_url.is_none()
145 && self.last_verified.is_none()
146 && self.notes.is_none()
147 }
148}
149
150#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
151pub struct AliasDef {
152 pub id: String,
153 pub provider: String,
154 #[serde(default)]
159 pub tool_format: Option<String>,
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
163pub struct AliasToolCallingDef {
164 #[serde(default)]
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub native: Option<String>,
167 #[serde(default)]
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub text: Option<String>,
170 #[serde(default)]
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub streaming_native: Option<String>,
173 #[serde(default)]
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub fallback_mode: Option<String>,
176 #[serde(default)]
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub failure_reason: Option<String>,
179 #[serde(default)]
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub last_probe_at: Option<String>,
182}
183
184#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
185pub struct ModelPricing {
186 pub input_per_mtok: f64,
187 pub output_per_mtok: f64,
188 #[serde(default)]
189 pub cache_read_per_mtok: Option<f64>,
190 #[serde(default)]
191 pub cache_write_per_mtok: Option<f64>,
192}
193
194#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
197pub struct RateLimitsDef {
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub rpm: Option<u32>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub rph: Option<u32>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub rpd: Option<u32>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub tpm: Option<u64>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub tph: Option<u64>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub tpd: Option<u64>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub input_tpm: Option<u64>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub output_tpm: Option<u64>,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub concurrency: Option<u32>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub tier: Option<String>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub source_url: Option<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub last_verified: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub notes: Option<String>,
237}
238
239impl RateLimitsDef {
240 pub fn is_empty(&self) -> bool {
241 self.rpm.is_none()
242 && self.rph.is_none()
243 && self.rpd.is_none()
244 && self.tpm.is_none()
245 && self.tph.is_none()
246 && self.tpd.is_none()
247 && self.input_tpm.is_none()
248 && self.output_tpm.is_none()
249 && self.concurrency.is_none()
250 && self.tier.is_none()
251 && self.source_url.is_none()
252 && self.last_verified.is_none()
253 && self.notes.is_none()
254 }
255
256 pub fn with_rpm_fallback(mut self, rpm: Option<u32>) -> Option<Self> {
257 if self.rpm.is_none() {
258 self.rpm = rpm;
259 }
260 (!self.is_empty()).then_some(self)
261 }
262}
263
264#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
268pub struct ServingPerformanceDef {
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub observed_ttft_ms: Option<u64>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub output_tokens_per_sec: Option<f64>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub time_to_answer_s: Option<f64>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub source: Option<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub source_url: Option<String>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub last_verified: Option<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub sample_size: Option<u32>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub notes: Option<String>,
295}
296
297impl ServingPerformanceDef {
298 pub fn is_empty(&self) -> bool {
299 self.observed_ttft_ms.is_none()
300 && self.output_tokens_per_sec.is_none()
301 && self.time_to_answer_s.is_none()
302 && self.source.is_none()
303 && self.source_url.is_none()
304 && self.last_verified.is_none()
305 && self.sample_size.is_none()
306 && self.notes.is_none()
307 }
308}
309
310#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
314pub struct ModelArchitectureDef {
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub parameter_count_b: Option<f64>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub active_parameter_count_b: Option<f64>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub moe: Option<bool>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub quantization: Option<String>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub precision: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub license: Option<String>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub tokenizer: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub knowledge_cutoff: Option<String>,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub source_url: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub last_verified: Option<String>,
345}
346
347impl ModelArchitectureDef {
348 pub fn is_empty(&self) -> bool {
349 self.parameter_count_b.is_none()
350 && self.active_parameter_count_b.is_none()
351 && self.moe.is_none()
352 && self.quantization.is_none()
353 && self.precision.is_none()
354 && self.license.is_none()
355 && self.tokenizer.is_none()
356 && self.knowledge_cutoff.is_none()
357 && self.source_url.is_none()
358 && self.last_verified.is_none()
359 }
360}
361
362#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
364pub struct ServingTierRequestDef {
365 pub param: String,
368 pub value: String,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub beta_header: Option<String>,
373}
374
375#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
380#[serde(rename_all = "snake_case")]
381pub enum ServingTierMode {
382 Synchronous,
383}
384
385#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
387#[serde(rename_all = "snake_case")]
388pub enum ServingTierEconomics {
389 Discounted,
390 Standard,
391 Premium,
392}
393
394#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
400pub struct ServingTierDef {
401 pub id: String,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub label: Option<String>,
406 pub mode: ServingTierMode,
407 pub economics: ServingTierEconomics,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub request: Option<ServingTierRequestDef>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub otps_speedup: Option<f64>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub cost_multiplier: Option<f64>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub discount_percent: Option<u32>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub status: Option<String>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub pricing: Option<ModelPricing>,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub latency: Option<String>,
431 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub reliability: Option<String>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub quota: Option<String>,
437 #[serde(default, skip_serializing_if = "Vec::is_empty")]
439 pub suitable_workloads: Vec<String>,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 pub unsuitable_workloads: Vec<String>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub note: Option<String>,
446}
447
448#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
461pub struct ModelLadderDef {
462 #[serde(default)]
464 pub steps: Vec<ModelLadderStepDef>,
465 #[serde(default)]
467 pub label: Option<String>,
468}
469
470#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
485pub struct ModelLadderStepDef {
486 pub model: String,
487 #[serde(default)]
488 pub provider: Option<String>,
489 #[serde(default)]
490 pub label: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub when: Option<String>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub options: Option<BTreeMap<String, toml::Value>>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub family: Option<String>,
507 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub capabilities: Vec<String>,
512}
513
514#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
515pub struct ModelDef {
516 pub name: String,
517 pub provider: String,
518 pub context_window: u64,
519 #[serde(default)]
522 pub logical_model: Option<String>,
523 #[serde(default)]
527 pub equivalence_group: Option<String>,
528 #[serde(default)]
531 pub served_variant: Option<String>,
532 #[serde(default)]
536 pub wire_model: Option<String>,
537 #[serde(default)]
540 pub api_dialect: Option<String>,
541 #[serde(default)]
543 pub rate_limits: Option<RateLimitsDef>,
544 #[serde(default)]
546 pub performance: Option<ServingPerformanceDef>,
547 #[serde(default)]
549 pub architecture: Option<ModelArchitectureDef>,
550 #[serde(default)]
552 pub local_memory: Option<LocalMemoryDef>,
553 #[serde(default)]
554 pub runtime_context_window: Option<u64>,
555 #[serde(default)]
556 pub stream_timeout: Option<f64>,
557 #[serde(default)]
558 pub capabilities: Vec<String>,
559 #[serde(default)]
560 pub pricing: Option<ModelPricing>,
561 #[serde(default)]
562 pub deprecated: bool,
563 #[serde(default)]
564 pub deprecation_note: Option<String>,
565 #[serde(default)]
573 pub superseded_by: Option<String>,
574 #[serde(default, skip_serializing_if = "Vec::is_empty")]
579 pub serving_tiers: Vec<ServingTierDef>,
580 #[serde(default)]
581 pub quality_tags: Vec<String>,
582 #[serde(default)]
588 pub availability: ModelAvailability,
589 #[serde(default)]
596 pub tier: Option<String>,
597 #[serde(default)]
602 pub open_weight: Option<bool>,
603 #[serde(default)]
608 pub strengths: Vec<String>,
609 #[serde(default)]
615 pub benchmarks: BTreeMap<String, f64>,
616 #[serde(default)]
621 pub family: Option<String>,
622 #[serde(default)]
624 pub lineage: Option<String>,
625 #[serde(default)]
627 pub complementary_with: Vec<String>,
628 #[serde(default)]
631 pub avoid_as_reviewer_for: Vec<String>,
632}
633
634#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
635#[serde(rename_all = "snake_case")]
636pub enum ModelAvailability {
637 #[default]
641 Serverless,
642 Dedicated,
646 Unknown,
650}
651
652impl ModelAvailability {
653 pub fn as_str(self) -> &'static str {
654 match self {
655 Self::Serverless => "serverless",
656 Self::Dedicated => "dedicated",
657 Self::Unknown => "unknown",
658 }
659 }
660
661 pub fn parse(value: &str) -> Option<Self> {
662 match value {
663 "serverless" => Some(Self::Serverless),
664 "dedicated" => Some(Self::Dedicated),
665 "unknown" => Some(Self::Unknown),
666 _ => None,
667 }
668 }
669}
670
671#[cfg(test)]
672mod ladder_step_tests {
673 use super::{ModelLadderDef, ModelLadderStepDef};
674
675 #[test]
676 fn all_added_fields_round_trip() {
677 let mut options = std::collections::BTreeMap::new();
678 options.insert("temperature".to_string(), toml::Value::Float(0.2));
679 options.insert("max_tokens".to_string(), toml::Value::Integer(512));
680 let step = ModelLadderStepDef {
681 model: "claude-haiku-4-5".to_string(),
682 provider: Some("anthropic".to_string()),
683 label: Some("cheap".to_string()),
684 when: Some("transport_failure".to_string()),
685 options: Some(options),
686 family: Some("haiku".to_string()),
687 capabilities: vec!["vision".to_string(), "tools".to_string()],
688 };
689 let json = serde_json::to_string(&step).expect("serialize");
690 let back: ModelLadderStepDef = serde_json::from_str(&json).expect("deserialize");
691 assert_eq!(step, back);
692 assert!(json.contains("\"family\":\"haiku\""));
693 assert!(json.contains("\"capabilities\":[\"vision\",\"tools\"]"));
694 assert!(json.contains("\"when\":\"transport_failure\""));
695 assert!(json.contains("\"temperature\":0.2"));
696 }
697
698 #[test]
699 fn unset_added_fields_are_absent_from_serialized_output() {
700 let step = ModelLadderStepDef {
705 model: "mock-cheap".to_string(),
706 provider: Some("mock".to_string()),
707 label: None,
708 when: None,
709 options: None,
710 family: None,
711 capabilities: Vec::new(),
712 };
713 let json = serde_json::to_string(&step).expect("serialize");
714 assert_eq!(
715 json,
716 r#"{"model":"mock-cheap","provider":"mock","label":null}"#
717 );
718 for absent in ["family", "capabilities", "when", "options"] {
719 assert!(
720 !json.contains(absent),
721 "unexpected key {absent:?} in {json}"
722 );
723 }
724 }
725
726 #[test]
727 fn deserializes_without_added_fields() {
728 let step: ModelLadderStepDef =
731 serde_json::from_str(r#"{"model":"mock-cheap"}"#).expect("deserialize legacy");
732 assert_eq!(step.when, None);
733 assert_eq!(step.family, None);
734 assert!(step.options.is_none());
735 assert!(step.capabilities.is_empty());
736 }
737
738 #[test]
739 fn catalog_toml_row_retains_when_and_options() {
740 let toml_src = r#"
744label = "with overrides"
745steps = [
746 { model = "haiku", label = "cheap", when = "transport_failure", family = "haiku", capabilities = ["tools"], options = { temperature = 0.1, max_tokens = 256 } },
747 { model = "opus", label = "frontier", family = "opus" },
748]
749"#;
750 let def: ModelLadderDef = toml::from_str(toml_src).expect("parse ladder toml");
751 assert_eq!(def.steps.len(), 2);
752 let cheap = &def.steps[0];
753 assert_eq!(cheap.when.as_deref(), Some("transport_failure"));
754 assert_eq!(cheap.family.as_deref(), Some("haiku"));
755 assert_eq!(cheap.capabilities, vec!["tools".to_string()]);
756 let opts = cheap.options.as_ref().expect("options present");
757 assert_eq!(opts.get("temperature"), Some(&toml::Value::Float(0.1)));
758 assert_eq!(opts.get("max_tokens"), Some(&toml::Value::Integer(256)));
759 assert_eq!(def.steps[1].family.as_deref(), Some("opus"));
760 }
761}