use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct HealthcheckDef {
pub method: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub body: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct LocalRuntimeDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prefix_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_source_env: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub served_model_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub port_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gpu_layers_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_type_k_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_type_v_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_ram_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_lora_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lora_modules_arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lora_modules_value_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_lora_rank_arg: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_verified: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct LocalMemoryDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measured_resident_gib: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measured_context_window: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measured_cache_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_resident_gib: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kv_cache_gib_per_1k_ctx: Option<f64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub cache_type_multipliers: BTreeMap<String, f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_cache_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub safety_margin_gib: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_recommended_context: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_verified: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
impl LocalMemoryDef {
pub fn is_empty(&self) -> bool {
self.measured_resident_gib.is_none()
&& self.measured_context_window.is_none()
&& self.measured_cache_type.is_none()
&& self.base_resident_gib.is_none()
&& self.kv_cache_gib_per_1k_ctx.is_none()
&& self.cache_type_multipliers.is_empty()
&& self.default_cache_type.is_none()
&& self.safety_margin_gib.is_none()
&& self.max_recommended_context.is_none()
&& self.source_url.is_none()
&& self.last_verified.is_none()
&& self.notes.is_none()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct AliasDef {
pub id: String,
pub provider: String,
#[serde(default)]
pub tool_format: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct AliasToolCallingDef {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub native: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub streaming_native: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_mode: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub last_probe_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ModelPricing {
pub input_per_mtok: f64,
pub output_per_mtok: f64,
#[serde(default)]
pub cache_read_per_mtok: Option<f64>,
#[serde(default)]
pub cache_write_per_mtok: Option<f64>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct RateLimitsDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rpm: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rph: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rpd: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tpm: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tph: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tpd: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tpm: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tpm: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub concurrency: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_verified: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
impl RateLimitsDef {
pub fn is_empty(&self) -> bool {
self.rpm.is_none()
&& self.rph.is_none()
&& self.rpd.is_none()
&& self.tpm.is_none()
&& self.tph.is_none()
&& self.tpd.is_none()
&& self.input_tpm.is_none()
&& self.output_tpm.is_none()
&& self.concurrency.is_none()
&& self.tier.is_none()
&& self.source_url.is_none()
&& self.last_verified.is_none()
&& self.notes.is_none()
}
pub fn with_rpm_fallback(mut self, rpm: Option<u32>) -> Option<Self> {
if self.rpm.is_none() {
self.rpm = rpm;
}
(!self.is_empty()).then_some(self)
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct ServingPerformanceDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_ttft_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens_per_sec: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_to_answer_s: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_verified: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sample_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
impl ServingPerformanceDef {
pub fn is_empty(&self) -> bool {
self.observed_ttft_ms.is_none()
&& self.output_tokens_per_sec.is_none()
&& self.time_to_answer_s.is_none()
&& self.source.is_none()
&& self.source_url.is_none()
&& self.last_verified.is_none()
&& self.sample_size.is_none()
&& self.notes.is_none()
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct ModelArchitectureDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameter_count_b: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_parameter_count_b: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub moe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quantization: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub precision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokenizer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub knowledge_cutoff: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_verified: Option<String>,
}
impl ModelArchitectureDef {
pub fn is_empty(&self) -> bool {
self.parameter_count_b.is_none()
&& self.active_parameter_count_b.is_none()
&& self.moe.is_none()
&& self.quantization.is_none()
&& self.precision.is_none()
&& self.license.is_none()
&& self.tokenizer.is_none()
&& self.knowledge_cutoff.is_none()
&& self.source_url.is_none()
&& self.last_verified.is_none()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct FastModeDef {
pub param: String,
pub value: String,
#[serde(default)]
pub beta_header: Option<String>,
#[serde(default)]
pub otps_speedup: Option<f64>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub pricing: Option<ModelPricing>,
#[serde(default)]
pub note: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct ModelLadderDef {
#[serde(default)]
pub steps: Vec<ModelLadderStepDef>,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct ModelLadderStepDef {
pub model: String,
#[serde(default)]
pub provider: Option<String>,
#[serde(default)]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<BTreeMap<String, toml::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ModelDef {
pub name: String,
pub provider: String,
pub context_window: u64,
#[serde(default)]
pub logical_model: Option<String>,
#[serde(default)]
pub equivalence_group: Option<String>,
#[serde(default)]
pub served_variant: Option<String>,
#[serde(default)]
pub wire_model: Option<String>,
#[serde(default)]
pub api_dialect: Option<String>,
#[serde(default)]
pub rate_limits: Option<RateLimitsDef>,
#[serde(default)]
pub performance: Option<ServingPerformanceDef>,
#[serde(default)]
pub architecture: Option<ModelArchitectureDef>,
#[serde(default)]
pub local_memory: Option<LocalMemoryDef>,
#[serde(default)]
pub runtime_context_window: Option<u64>,
#[serde(default)]
pub stream_timeout: Option<f64>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub pricing: Option<ModelPricing>,
#[serde(default)]
pub deprecated: bool,
#[serde(default)]
pub deprecation_note: Option<String>,
#[serde(default)]
pub superseded_by: Option<String>,
#[serde(default)]
pub fast_mode: Option<FastModeDef>,
#[serde(default)]
pub quality_tags: Vec<String>,
#[serde(default)]
pub availability: ModelAvailability,
#[serde(default)]
pub tier: Option<String>,
#[serde(default)]
pub open_weight: Option<bool>,
#[serde(default)]
pub strengths: Vec<String>,
#[serde(default)]
pub benchmarks: BTreeMap<String, f64>,
#[serde(default)]
pub family: Option<String>,
#[serde(default)]
pub lineage: Option<String>,
#[serde(default)]
pub complementary_with: Vec<String>,
#[serde(default)]
pub avoid_as_reviewer_for: Vec<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ModelAvailability {
#[default]
Serverless,
Dedicated,
Unknown,
}
impl ModelAvailability {
pub fn as_str(self) -> &'static str {
match self {
Self::Serverless => "serverless",
Self::Dedicated => "dedicated",
Self::Unknown => "unknown",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"serverless" => Some(Self::Serverless),
"dedicated" => Some(Self::Dedicated),
"unknown" => Some(Self::Unknown),
_ => None,
}
}
}
#[cfg(test)]
mod ladder_step_tests {
use super::{ModelLadderDef, ModelLadderStepDef};
#[test]
fn all_added_fields_round_trip() {
let mut options = std::collections::BTreeMap::new();
options.insert("temperature".to_string(), toml::Value::Float(0.2));
options.insert("max_tokens".to_string(), toml::Value::Integer(512));
let step = ModelLadderStepDef {
model: "claude-haiku-4-5".to_string(),
provider: Some("anthropic".to_string()),
label: Some("cheap".to_string()),
when: Some("transport_failure".to_string()),
options: Some(options),
family: Some("haiku".to_string()),
capabilities: vec!["vision".to_string(), "tools".to_string()],
};
let json = serde_json::to_string(&step).expect("serialize");
let back: ModelLadderStepDef = serde_json::from_str(&json).expect("deserialize");
assert_eq!(step, back);
assert!(json.contains("\"family\":\"haiku\""));
assert!(json.contains("\"capabilities\":[\"vision\",\"tools\"]"));
assert!(json.contains("\"when\":\"transport_failure\""));
assert!(json.contains("\"temperature\":0.2"));
}
#[test]
fn unset_added_fields_are_absent_from_serialized_output() {
let step = ModelLadderStepDef {
model: "mock-cheap".to_string(),
provider: Some("mock".to_string()),
label: None,
when: None,
options: None,
family: None,
capabilities: Vec::new(),
};
let json = serde_json::to_string(&step).expect("serialize");
assert_eq!(
json,
r#"{"model":"mock-cheap","provider":"mock","label":null}"#
);
for absent in ["family", "capabilities", "when", "options"] {
assert!(
!json.contains(absent),
"unexpected key {absent:?} in {json}"
);
}
}
#[test]
fn deserializes_without_added_fields() {
let step: ModelLadderStepDef =
serde_json::from_str(r#"{"model":"mock-cheap"}"#).expect("deserialize legacy");
assert_eq!(step.when, None);
assert_eq!(step.family, None);
assert!(step.options.is_none());
assert!(step.capabilities.is_empty());
}
#[test]
fn catalog_toml_row_retains_when_and_options() {
let toml_src = r#"
label = "with overrides"
steps = [
{ model = "haiku", label = "cheap", when = "transport_failure", family = "haiku", capabilities = ["tools"], options = { temperature = 0.1, max_tokens = 256 } },
{ model = "opus", label = "frontier", family = "opus" },
]
"#;
let def: ModelLadderDef = toml::from_str(toml_src).expect("parse ladder toml");
assert_eq!(def.steps.len(), 2);
let cheap = &def.steps[0];
assert_eq!(cheap.when.as_deref(), Some("transport_failure"));
assert_eq!(cheap.family.as_deref(), Some("haiku"));
assert_eq!(cheap.capabilities, vec!["tools".to_string()]);
let opts = cheap.options.as_ref().expect("options present");
assert_eq!(opts.get("temperature"), Some(&toml::Value::Float(0.1)));
assert_eq!(opts.get("max_tokens"), Some(&toml::Value::Integer(256)));
assert_eq!(def.steps[1].family.as_deref(), Some("opus"));
}
}