use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct PortfolioPolicy {
pub now: u64,
pub tracking_since: Option<u64>,
pub idle_after_secs: u64,
pub superseded_after_secs: u64,
pub cannot_load_failures: u64,
}
impl PortfolioPolicy {
pub const DEFAULT_IDLE_AFTER_SECS: u64 = 30 * 24 * 60 * 60;
pub const DEFAULT_SUPERSEDED_AFTER_SECS: u64 = 7 * 24 * 60 * 60;
pub const DEFAULT_CANNOT_LOAD_FAILURES: u64 = 3;
pub fn new(now: u64, tracking_since: Option<u64>) -> Self {
Self {
now,
tracking_since,
idle_after_secs: Self::DEFAULT_IDLE_AFTER_SECS,
superseded_after_secs: Self::DEFAULT_SUPERSEDED_AFTER_SECS,
cannot_load_failures: Self::DEFAULT_CANNOT_LOAD_FAILURES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RetireSummary {
pub freed_bytes: u64,
pub digest: String,
pub refusals: Vec<String>,
pub also_retires: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ModelFacts {
pub model_id: String,
pub name: String,
pub generates_text: bool,
pub platform_compatible: bool,
pub never_fits: bool,
pub estimated_peak_mb: Option<u64>,
pub hardware_budget_mb: u64,
pub protected: Option<String>,
pub kept: bool,
pub last_used: Option<u64>,
pub ledger_last_used: Option<u64>,
pub success_count: u64,
pub fail_count: u64,
pub superseded_by: Option<String>,
pub retire: Option<RetireSummary>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Protected,
NeverRunsHere,
CannotLoad,
Superseded,
Idle,
Active,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortfolioAction {
Keep,
Retire,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Assessment {
pub model_id: String,
pub name: String,
pub verdict: Verdict,
pub action: PortfolioAction,
pub evidence: Vec<String>,
pub kept: bool,
pub last_used: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retire: Option<RetireSummary>,
}
fn days(secs: u64) -> u64 {
secs / (24 * 60 * 60)
}
fn silent_since(facts: &ModelFacts, policy: &PortfolioPolicy) -> Option<u64> {
let since = policy.tracking_since?;
let ledger = facts.ledger_last_used.filter(|_| facts.generates_text);
Some(
facts
.last_used
.max(ledger)
.map_or(since, |last| last.max(since)),
)
}
pub fn assess(facts: &ModelFacts, policy: &PortfolioPolicy) -> Assessment {
let last_used = facts
.last_used
.max(facts.ledger_last_used.filter(|_| facts.generates_text));
let mut evidence = Vec::new();
if let Some(last) = last_used {
evidence.push(format!(
"last used {} days ago",
days(policy.now.saturating_sub(last))
));
}
let retire_freed = facts.retire.as_ref().map_or(0, |r| r.freed_bytes);
let verdict = 'verdict: {
if let Some(why) = &facts.protected {
evidence.push(format!("protected: {why}"));
break 'verdict Verdict::Protected;
}
let observed_running = facts.success_count > 0
|| facts
.last_used
.zip(policy.tracking_since)
.is_some_and(|(used, since)| used >= since);
if observed_running && (!facts.platform_compatible || facts.never_fits) {
evidence.push("estimated not to run here, but it has".into());
} else if !facts.platform_compatible {
evidence.push("this machine's platform cannot run it".into());
break 'verdict Verdict::NeverRunsHere;
} else if facts.never_fits {
evidence.push(match facts.estimated_peak_mb {
Some(peak) => format!(
"needs ~{peak} MB to load; this machine has {} MB for models",
facts.hardware_budget_mb
),
None => "too large for this machine's memory".into(),
});
break 'verdict Verdict::NeverRunsHere;
}
if facts.success_count == 0 && facts.fail_count >= policy.cannot_load_failures {
evidence.push(format!(
"failed {} times since it was installed and never succeeded here (threshold {})",
facts.fail_count, policy.cannot_load_failures
));
break 'verdict Verdict::CannotLoad;
}
let silent = silent_since(facts, policy).map(|since| policy.now.saturating_sub(since));
if let (Some(by), Some(silent)) = (&facts.superseded_by, silent) {
if silent >= policy.superseded_after_secs {
evidence.push(format!(
"replaced by {by}; unused {} days (threshold {})",
days(silent),
days(policy.superseded_after_secs)
));
break 'verdict Verdict::Superseded;
}
}
match silent {
Some(silent) if silent >= policy.idle_after_secs => {
evidence.push(format!(
"unused {} days (threshold {})",
days(silent),
days(policy.idle_after_secs)
));
Verdict::Idle
}
Some(_) if last_used.is_some() => Verdict::Active,
_ => {
evidence.push(match policy.tracking_since {
Some(since) => format!(
"usage tracked for {} days; {} needed before silence counts",
days(policy.now.saturating_sub(since)),
days(policy.idle_after_secs)
),
None => "usage is not tracked yet".into(),
});
Verdict::Unknown
}
}
};
let retirable = matches!(
verdict,
Verdict::NeverRunsHere | Verdict::CannotLoad | Verdict::Superseded | Verdict::Idle
);
let permanently_refused = facts.retire.as_ref().is_some_and(|r| {
r.refusals
.iter()
.any(|reason| !matches!(reason.as_str(), "in_use" | "downloading"))
});
if retirable && permanently_refused {
evidence.push(format!(
"retirement refused: {}",
facts
.retire
.as_ref()
.map(|r| r.refusals.join(", "))
.unwrap_or_default()
));
}
if retirable && facts.kept {
evidence.push("you asked to keep it".into());
}
let action = if retirable && retire_freed > 0 && !permanently_refused && !facts.kept {
PortfolioAction::Retire
} else {
if retirable && retire_freed == 0 && facts.retire.is_some() && !facts.kept {
evidence.push("retiring it would free nothing CAR owns".into());
}
PortfolioAction::Keep
};
Assessment {
model_id: facts.model_id.clone(),
name: facts.name.clone(),
verdict,
action,
evidence,
kept: facts.kept,
last_used,
retire: facts.retire.clone(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Portfolio {
pub policy: PortfolioPolicy,
pub models: Vec<Assessment>,
pub reclaimable_bytes: u64,
pub orphans: Vec<crate::retire::HubOrphan>,
pub recent_actions: Vec<RecentAction>,
pub revoked_pins: Vec<RevokedPin>,
pub revoked_copies: Vec<crate::retire::RevokedCopy>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RevokedPin {
pub pin: String,
pub model_id: String,
pub agents: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RecentAction {
pub at: u64,
pub kind: String,
pub subject: String,
pub detail: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub undo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_case: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prior: Option<String>,
pub interrupted: bool,
}
pub fn assess_all(facts: &[ModelFacts], policy: &PortfolioPolicy) -> Portfolio {
let mut models: Vec<Assessment> = facts.iter().map(|f| assess(f, policy)).collect();
models.sort_by(|a, b| a.model_id.cmp(&b.model_id));
let mut covered = std::collections::BTreeSet::new();
let mut reclaimable_bytes = 0;
for assessment in &mut models {
if covered.contains(&assessment.model_id) {
if assessment.action == PortfolioAction::Retire {
assessment.action = PortfolioAction::Keep;
assessment
.evidence
.push("retires with the row that shares its files".into());
}
continue;
}
if assessment.action != PortfolioAction::Retire {
continue;
}
let Some(retire) = &assessment.retire else {
continue;
};
let twin_kept = retire.also_retires.iter().any(|twin| {
facts
.iter()
.find(|f| &f.model_id == twin)
.is_some_and(|f| !retirable_alone(f, policy))
});
if twin_kept {
assessment.action = PortfolioAction::Keep;
assessment
.evidence
.push("a row sharing its files is still needed".into());
continue;
}
covered.extend(retire.also_retires.iter().cloned());
reclaimable_bytes += retire.freed_bytes;
}
Portfolio {
policy: *policy,
models,
reclaimable_bytes,
orphans: Vec::new(),
recent_actions: Vec::new(),
revoked_pins: Vec::new(),
revoked_copies: Vec::new(),
}
}
fn retirable_alone(facts: &ModelFacts, policy: &PortfolioPolicy) -> bool {
matches!(
assess(facts, policy).verdict,
Verdict::NeverRunsHere | Verdict::CannotLoad | Verdict::Superseded | Verdict::Idle
)
}
#[cfg(test)]
mod tests {
use super::*;
const DAY: u64 = 24 * 60 * 60;
const NOW: u64 = 1_000 * DAY;
fn policy(tracking_days: u64) -> PortfolioPolicy {
PortfolioPolicy::new(NOW, Some(NOW - tracking_days * DAY))
}
fn installed(id: &str) -> ModelFacts {
ModelFacts {
model_id: id.into(),
name: id.into(),
platform_compatible: true,
hardware_budget_mb: 60_000,
retire: Some(RetireSummary {
freed_bytes: 1_000,
digest: "d".into(),
refusals: Vec::new(),
also_retires: Vec::new(),
}),
..ModelFacts::default()
}
}
#[test]
fn silence_is_not_evidence_until_tracking_has_seen_a_full_period() {
let facts = installed("mlx/a");
let fresh = assess(&facts, &policy(2));
assert_eq!(fresh.verdict, Verdict::Unknown, "{fresh:?}");
assert_eq!(fresh.action, PortfolioAction::Keep);
let untracked = assess(&facts, &PortfolioPolicy::new(NOW, None));
assert_eq!(untracked.verdict, Verdict::Unknown);
let aged = assess(&facts, &policy(31));
assert_eq!(aged.verdict, Verdict::Idle, "{aged:?}");
assert_eq!(aged.action, PortfolioAction::Retire);
}
#[test]
fn an_old_stamp_counts_from_when_tracking_began() {
let mut facts = installed("mlx/a");
facts.last_used = Some(NOW - 100 * DAY);
assert_eq!(assess(&facts, &policy(5)).verdict, Verdict::Active);
facts.last_used = Some(NOW - 2 * DAY);
assert_eq!(assess(&facts, &policy(100)).verdict, Verdict::Active);
}
#[test]
fn the_ledger_proves_text_use_but_never_shortens_the_wait() {
let mut facts = installed("mlx/a");
facts.ledger_last_used = Some(NOW - 45 * DAY);
facts.generates_text = true;
let early = assess(&facts, &policy(8));
assert_eq!(early.verdict, Verdict::Active, "{early:?}");
let text = assess(&facts, &policy(31));
assert_eq!(text.verdict, Verdict::Idle, "{text:?}");
assert!(text.evidence.iter().any(|e| e.contains("threshold 30")));
facts.ledger_last_used = Some(NOW - DAY);
assert_eq!(assess(&facts, &policy(60)).verdict, Verdict::Active);
facts.generates_text = false;
facts.ledger_last_used = Some(NOW - 45 * DAY);
assert_eq!(assess(&facts, &policy(8)).verdict, Verdict::Unknown);
}
#[test]
fn a_permanently_refused_plan_is_not_an_action() {
let mut facts = installed("mlx/a");
facts.retire.as_mut().unwrap().refusals = vec!["unsafe_path".into()];
let refused = assess(&facts, &policy(60));
assert_eq!(refused.verdict, Verdict::Idle);
assert_eq!(refused.action, PortfolioAction::Keep, "{refused:?}");
facts.retire.as_mut().unwrap().refusals = vec!["in_use".into()];
assert_eq!(assess(&facts, &policy(60)).action, PortfolioAction::Retire);
}
#[test]
fn running_here_outranks_the_estimate_that_it_cannot() {
let mut facts = installed("mlx/moe");
facts.never_fits = true;
facts.success_count = 4;
facts.last_used = Some(NOW - DAY);
let assessment = assess(&facts, &policy(60));
assert_eq!(assessment.verdict, Verdict::Active, "{assessment:?}");
assert_eq!(assessment.action, PortfolioAction::Keep);
let mut stamped = installed("mlx/vlm");
stamped.platform_compatible = false;
stamped.last_used = Some(NOW - 2 * DAY);
assert_eq!(assess(&stamped, &policy(10)).verdict, Verdict::Active);
stamped.last_used = Some(NOW - 20 * DAY);
assert_eq!(
assess(&stamped, &policy(10)).verdict,
Verdict::NeverRunsHere
);
}
#[test]
fn a_protected_model_stays_whatever_else_is_true() {
let mut facts = installed("mlx/a");
facts.protected = Some("lane default for assistant".into());
facts.platform_compatible = false;
facts.never_fits = true;
facts.fail_count = 50;
facts.superseded_by = Some("mlx/b".into());
let assessment = assess(&facts, &policy(365));
assert_eq!(assessment.verdict, Verdict::Protected);
assert_eq!(assessment.action, PortfolioAction::Keep);
}
#[test]
fn never_runs_here_and_cannot_load_need_no_history() {
let mut facts = installed("qwen/gguf");
facts.platform_compatible = false;
assert_eq!(assess(&facts, &policy(0)).verdict, Verdict::NeverRunsHere);
let mut big = installed("mlx/huge");
big.never_fits = true;
big.estimated_peak_mb = Some(120_000);
let verdict = assess(&big, &policy(0));
assert_eq!(verdict.verdict, Verdict::NeverRunsHere);
assert!(verdict.evidence[0].contains("120000"));
let mut broken = installed("mlx/broken");
broken.fail_count = 3;
assert_eq!(assess(&broken, &policy(0)).verdict, Verdict::CannotLoad);
broken.success_count = 1;
broken.last_used = Some(NOW);
assert_eq!(assess(&broken, &policy(0)).verdict, Verdict::Active);
}
#[test]
fn a_replaced_model_goes_after_a_week_not_a_month() {
let mut facts = installed("mlx/old");
facts.superseded_by = Some("mlx/new".into());
facts.last_used = Some(NOW - 10 * DAY);
let assessment = assess(&facts, &policy(60));
assert_eq!(assessment.verdict, Verdict::Superseded, "{assessment:?}");
facts.last_used = Some(NOW - 2 * DAY);
assert_eq!(assess(&facts, &policy(60)).verdict, Verdict::Active);
}
#[test]
fn a_kept_model_is_never_recommended_for_retirement() {
let mut facts = installed("mlx/a");
facts.kept = true;
let assessment = assess(&facts, &policy(60));
assert_eq!(
assessment.verdict,
Verdict::Idle,
"the verdict stays honest"
);
assert_eq!(assessment.action, PortfolioAction::Keep);
assert!(assessment.kept);
let portfolio = assess_all(&[facts], &policy(60));
assert_eq!(portfolio.reclaimable_bytes, 0);
}
#[test]
fn nothing_to_free_is_not_an_action() {
let mut facts = installed("mlx/a");
facts.retire.as_mut().unwrap().freed_bytes = 0;
let assessment = assess(&facts, &policy(60));
assert_eq!(assessment.verdict, Verdict::Idle);
assert_eq!(assessment.action, PortfolioAction::Keep);
}
#[test]
fn twins_are_recommended_once_and_kept_when_either_is_needed() {
let mut a = installed("mlx/gemma");
let mut b = installed("vllm-mlx/gemma");
a.retire.as_mut().unwrap().also_retires = vec![b.model_id.clone()];
b.retire.as_mut().unwrap().also_retires = vec![a.model_id.clone()];
let portfolio = assess_all(&[a.clone(), b.clone()], &policy(60));
let retiring: Vec<_> = portfolio
.models
.iter()
.filter(|m| m.action == PortfolioAction::Retire)
.collect();
assert_eq!(retiring.len(), 1, "{portfolio:?}");
assert_eq!(
portfolio.reclaimable_bytes, 1_000,
"shared repo counted once"
);
b.last_used = Some(NOW);
let portfolio = assess_all(&[a, b], &policy(60));
assert!(
portfolio
.models
.iter()
.all(|m| m.action == PortfolioAction::Keep),
"{portfolio:?}"
);
assert_eq!(portfolio.reclaimable_bytes, 0);
}
}