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)]
373pub struct FastModeDef {
374 pub param: String,
377 pub value: String,
379 #[serde(default)]
382 pub beta_header: Option<String>,
383 #[serde(default)]
385 pub otps_speedup: Option<f64>,
386 #[serde(default)]
389 pub status: Option<String>,
390 #[serde(default)]
393 pub pricing: Option<ModelPricing>,
394 #[serde(default)]
396 pub note: Option<String>,
397}
398
399#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
412pub struct ModelLadderDef {
413 #[serde(default)]
415 pub steps: Vec<ModelLadderStepDef>,
416 #[serde(default)]
418 pub label: Option<String>,
419}
420
421#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
436pub struct ModelLadderStepDef {
437 pub model: String,
438 #[serde(default)]
439 pub provider: Option<String>,
440 #[serde(default)]
441 pub label: Option<String>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub when: Option<String>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub options: Option<BTreeMap<String, toml::Value>>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub family: Option<String>,
458 #[serde(default, skip_serializing_if = "Vec::is_empty")]
462 pub capabilities: Vec<String>,
463}
464
465#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
466pub struct ModelDef {
467 pub name: String,
468 pub provider: String,
469 pub context_window: u64,
470 #[serde(default)]
473 pub logical_model: Option<String>,
474 #[serde(default)]
478 pub equivalence_group: Option<String>,
479 #[serde(default)]
482 pub served_variant: Option<String>,
483 #[serde(default)]
487 pub wire_model: Option<String>,
488 #[serde(default)]
491 pub api_dialect: Option<String>,
492 #[serde(default)]
494 pub rate_limits: Option<RateLimitsDef>,
495 #[serde(default)]
497 pub performance: Option<ServingPerformanceDef>,
498 #[serde(default)]
500 pub architecture: Option<ModelArchitectureDef>,
501 #[serde(default)]
503 pub local_memory: Option<LocalMemoryDef>,
504 #[serde(default)]
505 pub runtime_context_window: Option<u64>,
506 #[serde(default)]
507 pub stream_timeout: Option<f64>,
508 #[serde(default)]
509 pub capabilities: Vec<String>,
510 #[serde(default)]
511 pub pricing: Option<ModelPricing>,
512 #[serde(default)]
513 pub deprecated: bool,
514 #[serde(default)]
515 pub deprecation_note: Option<String>,
516 #[serde(default)]
524 pub superseded_by: Option<String>,
525 #[serde(default)]
529 pub fast_mode: Option<FastModeDef>,
530 #[serde(default)]
531 pub quality_tags: Vec<String>,
532 #[serde(default)]
538 pub availability: ModelAvailability,
539 #[serde(default)]
546 pub tier: Option<String>,
547 #[serde(default)]
552 pub open_weight: Option<bool>,
553 #[serde(default)]
558 pub strengths: Vec<String>,
559 #[serde(default)]
565 pub benchmarks: BTreeMap<String, f64>,
566 #[serde(default)]
571 pub family: Option<String>,
572 #[serde(default)]
574 pub lineage: Option<String>,
575 #[serde(default)]
577 pub complementary_with: Vec<String>,
578 #[serde(default)]
581 pub avoid_as_reviewer_for: Vec<String>,
582}
583
584#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
585#[serde(rename_all = "snake_case")]
586pub enum ModelAvailability {
587 #[default]
591 Serverless,
592 Dedicated,
596 Unknown,
600}
601
602impl ModelAvailability {
603 pub fn as_str(self) -> &'static str {
604 match self {
605 Self::Serverless => "serverless",
606 Self::Dedicated => "dedicated",
607 Self::Unknown => "unknown",
608 }
609 }
610
611 pub fn parse(value: &str) -> Option<Self> {
612 match value {
613 "serverless" => Some(Self::Serverless),
614 "dedicated" => Some(Self::Dedicated),
615 "unknown" => Some(Self::Unknown),
616 _ => None,
617 }
618 }
619}
620
621#[cfg(test)]
622mod ladder_step_tests {
623 use super::{ModelLadderDef, ModelLadderStepDef};
624
625 #[test]
626 fn all_added_fields_round_trip() {
627 let mut options = std::collections::BTreeMap::new();
628 options.insert("temperature".to_string(), toml::Value::Float(0.2));
629 options.insert("max_tokens".to_string(), toml::Value::Integer(512));
630 let step = ModelLadderStepDef {
631 model: "claude-haiku-4-5".to_string(),
632 provider: Some("anthropic".to_string()),
633 label: Some("cheap".to_string()),
634 when: Some("transport_failure".to_string()),
635 options: Some(options),
636 family: Some("haiku".to_string()),
637 capabilities: vec!["vision".to_string(), "tools".to_string()],
638 };
639 let json = serde_json::to_string(&step).expect("serialize");
640 let back: ModelLadderStepDef = serde_json::from_str(&json).expect("deserialize");
641 assert_eq!(step, back);
642 assert!(json.contains("\"family\":\"haiku\""));
643 assert!(json.contains("\"capabilities\":[\"vision\",\"tools\"]"));
644 assert!(json.contains("\"when\":\"transport_failure\""));
645 assert!(json.contains("\"temperature\":0.2"));
646 }
647
648 #[test]
649 fn unset_added_fields_are_absent_from_serialized_output() {
650 let step = ModelLadderStepDef {
655 model: "mock-cheap".to_string(),
656 provider: Some("mock".to_string()),
657 label: None,
658 when: None,
659 options: None,
660 family: None,
661 capabilities: Vec::new(),
662 };
663 let json = serde_json::to_string(&step).expect("serialize");
664 assert_eq!(
665 json,
666 r#"{"model":"mock-cheap","provider":"mock","label":null}"#
667 );
668 for absent in ["family", "capabilities", "when", "options"] {
669 assert!(
670 !json.contains(absent),
671 "unexpected key {absent:?} in {json}"
672 );
673 }
674 }
675
676 #[test]
677 fn deserializes_without_added_fields() {
678 let step: ModelLadderStepDef =
681 serde_json::from_str(r#"{"model":"mock-cheap"}"#).expect("deserialize legacy");
682 assert_eq!(step.when, None);
683 assert_eq!(step.family, None);
684 assert!(step.options.is_none());
685 assert!(step.capabilities.is_empty());
686 }
687
688 #[test]
689 fn catalog_toml_row_retains_when_and_options() {
690 let toml_src = r#"
694label = "with overrides"
695steps = [
696 { model = "haiku", label = "cheap", when = "transport_failure", family = "haiku", capabilities = ["tools"], options = { temperature = 0.1, max_tokens = 256 } },
697 { model = "opus", label = "frontier", family = "opus" },
698]
699"#;
700 let def: ModelLadderDef = toml::from_str(toml_src).expect("parse ladder toml");
701 assert_eq!(def.steps.len(), 2);
702 let cheap = &def.steps[0];
703 assert_eq!(cheap.when.as_deref(), Some("transport_failure"));
704 assert_eq!(cheap.family.as_deref(), Some("haiku"));
705 assert_eq!(cheap.capabilities, vec!["tools".to_string()]);
706 let opts = cheap.options.as_ref().expect("options present");
707 assert_eq!(opts.get("temperature"), Some(&toml::Value::Float(0.1)));
708 assert_eq!(opts.get("max_tokens"), Some(&toml::Value::Integer(256)));
709 assert_eq!(def.steps[1].family.as_deref(), Some("opus"));
710 }
711}