use serde::{Deserialize, Serialize};
use crate::hardware::{HardwareInfo, SupportedAcceleration};
use crate::intent::{Privacy, QualityTier, UseCase, UseCaseRole};
use crate::resource_policy::{
estimate_model_memory, model_parameter_billions_active, model_parameter_billions_total,
ResourcePolicy, ResourceProfile, RECOMMENDATION_CONTEXT_TOKENS,
};
use crate::schema::{ModelSchema, TrustTier};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FitStatus {
Fits,
TooBig,
ServerProvided,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub model_id: String,
pub display_name: String,
pub role: UseCaseRole,
pub rationale: String,
pub download_mb: u64,
pub already_installed: bool,
pub fit: FitStatus,
pub acceleration: SupportedAcceleration,
pub is_local: bool,
pub requires_cloud_consent: bool,
pub trust_tier: TrustTier,
pub score: f32,
#[serde(default = "default_true")]
pub within_recommendation_target: bool,
}
const fn default_true() -> bool {
true
}
const OS_RESERVE_MB: u64 = 3072;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecommendationSet {
pub picks: Vec<Recommendation>,
pub not_enough_memory: Vec<Recommendation>,
pub note: Option<String>,
}
pub fn recommend(
models: &[&ModelSchema],
hw: &HardwareInfo,
use_case: UseCase,
tier: QualityTier,
privacy: Privacy,
) -> RecommendationSet {
recommend_inner(models, hw, use_case, tier, privacy, None)
}
pub fn recommend_with_policy(
models: &[&ModelSchema],
hw: &HardwareInfo,
policy: &ResourcePolicy,
use_case: UseCase,
tier: QualityTier,
privacy: Privacy,
) -> RecommendationSet {
recommend_inner(models, hw, use_case, tier, privacy, Some(policy))
}
fn recommend_inner(
models: &[&ModelSchema],
hw: &HardwareInfo,
use_case: UseCase,
tier: QualityTier,
privacy: Privacy,
policy: Option<&ResourcePolicy>,
) -> RecommendationSet {
let accel = hw.supported_acceleration();
let everyday_assistant_balanced = policy.is_some_and(|policy| {
policy.profile == ResourceProfile::Everyday
&& use_case == UseCase::Assistant
&& tier == QualityTier::Balanced
});
let memory_limits = RecommendationMemoryLimits {
legacy_budget_mb: memory_budget_mb(hw),
policy_host_budget_mb: policy.map(|policy| {
policy
.effective_budget(hw.total_ram_mb)
.configured_model_ceiling_mb
}),
recommendation_target_mb: policy
.map(|policy| policy.recommendation_target_mb(hw.total_ram_mb)),
};
let sort = |v: &mut Vec<RankedRecommendation>| {
v.sort_by(|a, b| {
if everyday_assistant_balanced {
let policy_class = |recommendation: &Recommendation| {
if recommendation.fit == FitStatus::Unknown {
3
} else if recommendation.is_local && recommendation.within_recommendation_target
{
0
} else if !recommendation.is_local
&& recommendation.within_recommendation_target
{
1
} else {
2
}
};
let a_recommendation = &a.recommendation;
let b_recommendation = &b.recommendation;
return policy_class(a_recommendation)
.cmp(&policy_class(b_recommendation))
.then(
b_recommendation
.already_installed
.cmp(&a_recommendation.already_installed),
)
.then(a.estimated_peak_mb.cmp(&b.estimated_peak_mb))
.then(a.latency_p50_ms.cmp(&b.latency_p50_ms))
.then_with(|| b_recommendation.score.total_cmp(&a_recommendation.score))
.then(a_recommendation.model_id.cmp(&b_recommendation.model_id));
}
b.recommendation
.score
.total_cmp(&a.recommendation.score)
.then(
b.recommendation
.already_installed
.cmp(&a.recommendation.already_installed),
)
.then(
a.recommendation
.download_mb
.cmp(&b.recommendation.download_mb),
)
.then(a.recommendation.model_id.cmp(&b.recommendation.model_id))
});
};
let (mut picks, mut not_enough_memory): (Vec<_>, Vec<_>) = models
.iter()
.filter(|m| {
passes_base_filter(m, hw, use_case, privacy)
&& (!everyday_assistant_balanced
|| m.has_capability(crate::schema::ModelCapability::ToolUse))
})
.map(|model| build_recommendation(model, hw, &accel, use_case, tier, &memory_limits))
.partition(|ranked| ranked.recommendation.fit != FitStatus::TooBig);
sort(&mut picks);
sort(&mut not_enough_memory);
let picks: Vec<_> = picks
.into_iter()
.map(|ranked| ranked.recommendation)
.collect();
let not_enough_memory: Vec<_> = not_enough_memory
.into_iter()
.map(|ranked| ranked.recommendation)
.collect();
let note = explain_if_needed(&picks, ¬_enough_memory, hw, use_case, tier, privacy);
RecommendationSet {
picks,
not_enough_memory,
note,
}
}
struct RankedRecommendation {
recommendation: Recommendation,
estimated_peak_mb: u64,
latency_p50_ms: u64,
}
#[derive(Clone, Copy)]
struct RecommendationMemoryLimits {
legacy_budget_mb: u64,
policy_host_budget_mb: Option<u64>,
recommendation_target_mb: Option<u64>,
}
fn passes_base_filter(
m: &ModelSchema,
hw: &HardwareInfo,
use_case: UseCase,
privacy: Privacy,
) -> bool {
if m.deprecated {
return false;
}
if !use_case
.required_capabilities()
.iter()
.all(|c| m.has_capability(*c))
{
return false;
}
if privacy == Privacy::OnDevice && !m.is_local() {
return false;
}
if m.requires_apple_silicon()
&& !matches!(
hw.supported_acceleration(),
SupportedAcceleration::Apple { .. }
)
{
return false;
}
true
}
fn explain_if_needed(
picks: &[Recommendation],
too_big: &[Recommendation],
hw: &HardwareInfo,
use_case: UseCase,
tier: QualityTier,
privacy: Privacy,
) -> Option<String> {
let purpose = use_case_purpose(use_case);
if picks.is_empty() {
let ram_gb = hw.total_ram_mb / 1024;
return Some(if !too_big.is_empty() {
match privacy {
Privacy::OnDevice => format!(
"No on-device model for {purpose} fits your {ram_gb} GB machine. \
Free up memory, pick a smaller tier, or allow cloud models."
),
Privacy::CloudOk => format!(
"No local model for {purpose} fits your {ram_gb} GB machine, and no \
cloud model is configured. Add an API key or free up memory."
),
}
} else {
format!("No model available for {purpose} on this machine.")
});
}
if picks[0].requires_cloud_consent {
return Some(format!(
"The best {purpose} pick runs in the cloud and needs your OK before first use. \
{} fits locally if you prefer on-device.",
picks
.iter()
.find(|p| p.is_local)
.map(|p| p.display_name.as_str())
.unwrap_or("No local model")
));
}
let _ = tier;
None
}
fn use_case_purpose(use_case: UseCase) -> &'static str {
match use_case {
UseCase::Assistant => "chat & general help",
UseCase::Coding => "coding",
UseCase::Summarize => "summarizing",
UseCase::Vision => "understanding images",
UseCase::Transcription => "transcription",
UseCase::Search => "semantic search",
}
}
fn build_recommendation(
m: &ModelSchema,
hw: &HardwareInfo,
accel: &SupportedAcceleration,
use_case: UseCase,
tier: QualityTier,
memory_limits: &RecommendationMemoryLimits,
) -> RankedRecommendation {
let estimate = estimate_model_memory(m, hw, RECOMMENDATION_CONTEXT_TOKENS);
let fit = fit_status(m, hw, &estimate, memory_limits);
let quality = quality_score(m);
let latency = latency_score(m, accel);
let pressure = memory_pressure(&estimate, memory_limits.legacy_budget_mb);
let w = tier.weights();
let mut score =
w.quality * quality + w.latency * latency + w.memory_pressure * (1.0 - pressure);
let pref_hits = use_case
.preferred_capabilities()
.iter()
.filter(|c| m.has_capability(**c))
.count();
score += 0.05 * pref_hits as f32;
let is_local = m.is_local();
let within_recommendation_target = match memory_limits.recommendation_target_mb {
None => true,
Some(_) if fit == FitStatus::Unknown => false,
Some(target_mb) if is_local => match hw.supported_acceleration() {
SupportedAcceleration::Cuda {
device_memory_mb: Some(device_memory_mb),
} => {
let host_required_mb = estimate
.estimated_peak_mb
.saturating_sub(estimate.weights_mb);
fit == FitStatus::Fits
&& estimate.weights_mb <= device_memory_mb
&& host_required_mb <= target_mb
}
SupportedAcceleration::Cuda {
device_memory_mb: None,
} => false,
_ => fit == FitStatus::Fits && estimate.estimated_peak_mb <= target_mb,
},
Some(_) => true,
};
RankedRecommendation {
estimated_peak_mb: if is_local {
estimate.estimated_peak_mb
} else {
0
},
latency_p50_ms: m.performance.latency_p50_ms.unwrap_or(u64::MAX),
recommendation: Recommendation {
model_id: m.id.clone(),
display_name: m.name.clone(),
role: use_case.role(),
rationale: rationale(m, hw, use_case, tier, fit, quality),
download_mb: if m.downloads_weights() {
m.size_mb()
} else {
0
},
already_installed: m.has_installed_weights(),
fit,
acceleration: accel.clone(),
is_local,
requires_cloud_consent: !is_local,
trust_tier: m.trust_tier,
score,
within_recommendation_target,
},
}
}
fn quality_score(m: &ModelSchema) -> f32 {
if !m.public_benchmarks.is_empty() {
let sum: f64 = m.public_benchmarks.iter().map(|b| b.score).sum();
return (sum / m.public_benchmarks.len() as f64).clamp(0.0, 1.0) as f32;
}
let b = param_billions_total(m).max(0.1);
(b / (b + 7.0)).clamp(0.0, 1.0)
}
fn latency_score(m: &ModelSchema, accel: &SupportedAcceleration) -> f32 {
let b = param_billions_active(m).max(0.1);
let size_term = 8.0 / (b + 8.0);
let accel_bonus = match accel {
SupportedAcceleration::Apple { .. } | SupportedAcceleration::Cuda { .. } => 0.1,
_ => 0.0,
};
(size_term + accel_bonus).clamp(0.0, 1.0)
}
fn memory_pressure(estimate: &crate::resource_policy::ModelMemoryEstimate, budget: u64) -> f32 {
if budget == 0 {
return 1.0;
}
(estimate.estimated_peak_mb as f32 / budget as f32).clamp(0.0, 1.5)
}
fn fit_status(
m: &ModelSchema,
hw: &HardwareInfo,
estimate: &crate::resource_policy::ModelMemoryEstimate,
memory_limits: &RecommendationMemoryLimits,
) -> FitStatus {
if m.is_remote() || m.is_delegated() {
return FitStatus::ServerProvided;
}
if m.is_foundation_models() {
return FitStatus::Fits;
}
if m.size_mb() == 0 && m.ram_mb() == 0 {
return FitStatus::Unknown;
}
let required_mb = estimate.estimated_peak_mb;
let fits = if let Some(host_budget) = memory_limits.policy_host_budget_mb {
match hw.supported_acceleration() {
SupportedAcceleration::Cuda {
device_memory_mb: Some(device_memory_mb),
} => {
let host_required_mb = estimate
.estimated_peak_mb
.saturating_sub(estimate.weights_mb);
estimate.weights_mb <= device_memory_mb && host_required_mb <= host_budget
}
SupportedAcceleration::Cuda {
device_memory_mb: None,
} => return FitStatus::Unknown,
_ => required_mb <= host_budget,
}
} else {
required_mb <= memory_limits.legacy_budget_mb
};
if fits {
FitStatus::Fits
} else {
FitStatus::TooBig
}
}
fn memory_budget_mb(hw: &HardwareInfo) -> u64 {
match hw.supported_acceleration() {
SupportedAcceleration::Apple { unified_memory_mb } => {
unified_memory_mb.saturating_sub(OS_RESERVE_MB)
}
SupportedAcceleration::Cuda { device_memory_mb } => {
device_memory_mb.unwrap_or(hw.total_ram_mb)
}
_ => hw.total_ram_mb.saturating_sub(OS_RESERVE_MB),
}
}
fn param_billions_total(m: &ModelSchema) -> f32 {
model_parameter_billions_total(m)
}
fn param_billions_active(m: &ModelSchema) -> f32 {
model_parameter_billions_active(m)
}
fn rationale(
m: &ModelSchema,
hw: &HardwareInfo,
use_case: UseCase,
tier: QualityTier,
fit: FitStatus,
quality: f32,
) -> String {
let purpose = use_case_purpose(use_case);
let machine = match hw.supported_acceleration() {
SupportedAcceleration::Apple { unified_memory_mb } => {
format!(
"your {} GB Apple Silicon Mac (Metal)",
unified_memory_mb / 1024
)
}
SupportedAcceleration::Cuda { device_memory_mb } => match device_memory_mb {
Some(mb) => format!("your {} GB NVIDIA GPU (CUDA)", mb / 1024),
None => "your NVIDIA GPU (CUDA)".to_string(),
},
SupportedAcceleration::UnsupportedDiscreteGpu { .. } | SupportedAcceleration::Cpu => {
format!("your {} GB machine (CPU)", hw.total_ram_mb / 1024)
}
};
match fit {
FitStatus::ServerProvided
if matches!(&m.source, crate::schema::ModelSource::VllmMlx { .. }) =>
{
format!(
"{}: external server for {} — its operator runs the model, nothing to download",
m.name, purpose
)
}
FitStatus::ServerProvided if m.is_remote() => format!(
"{}: cloud model for {} — runs on Parslee's servers, nothing to download",
m.name, purpose
),
FitStatus::ServerProvided => format!(
"{}: served externally for {} — no local memory needed",
m.name, purpose
),
_ => {
let tier_word = match tier {
QualityTier::Fastest => "fastest",
QualityTier::Balanced => "best-balanced",
QualityTier::MostCapable => "most capable",
};
let quality_note = if quality >= 0.7 { "high-quality " } else { "" };
let size = if m.size_mb() >= 1024 {
format!("{:.1} GB download", m.size_mb() as f64 / 1024.0)
} else {
format!("{} MB download", m.size_mb())
};
format!(
"{}: the {} {}{} model that fits {} ({})",
m.name, tier_word, quality_note, purpose, machine, size
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hardware::{GpuBackend, GpuDevice, GpuVendor};
use crate::schema::{CostModel, ModelCapability, ModelSource, PerformanceEnvelope};
pub(super) fn hw(accel_backend: GpuBackend, ram_mb: u64, gpu_mb: Option<u64>) -> HardwareInfo {
HardwareInfo {
os: "test".into(),
arch: "test".into(),
cpu_cores: 8,
total_ram_mb: ram_mb,
gpu_backend: accel_backend,
gpu_memory_mb: gpu_mb,
gpu_devices: vec![],
recommended_model: String::new(),
recommended_context: 4096,
max_model_mb: 0,
}
}
fn mac(ram_gb: u64) -> HardwareInfo {
hw(GpuBackend::Metal, ram_gb * 1024, None)
}
pub(super) fn local_model(id: &str, name: &str, params: &str, size_mb: u64) -> ModelSchema {
ModelSchema {
id: id.into(),
name: name.into(),
provider: "qwen".into(),
family: "qwen3".into(),
version: String::new(),
capabilities: vec![ModelCapability::Generate, ModelCapability::Code],
context_length: 32768,
max_output_tokens: None,
param_count: params.into(),
quantization: Some("Q4_K_M".into()),
performance: PerformanceEnvelope::default(),
cost: CostModel {
size_mb: Some(size_mb),
ram_mb: Some(size_mb),
..Default::default()
},
source: ModelSource::Local {
hf_repo: "x/y".into(),
hf_filename: "m.gguf".into(),
tokenizer_repo: "x/y".into(),
},
tags: vec![],
supported_params: vec![],
public_benchmarks: vec![],
trust_tier: TrustTier::Curated,
deprecated: false,
available: false,
weights_ready: false,
}
}
fn catalog() -> Vec<ModelSchema> {
vec![
local_model("qwen/qwen3-0.6b", "Qwen3-0.6B", "0.6B", 650),
local_model("qwen/qwen3-4b", "Qwen3-4B", "4B", 2500),
local_model("qwen/qwen3-8b", "Qwen3-8B", "8B", 4900),
local_model("qwen/qwen3-30b", "Qwen3-30B-A3B", "30B (3B active)", 17000),
]
}
fn qwen_mlx_policy_catalog() -> Vec<ModelSchema> {
let mut four = local_model("mlx/qwen3-4b:4bit", "Qwen3-4B-MLX", "4B", 2400);
four.source = ModelSource::Mlx {
hf_repo: "mlx-community/Qwen3-4B-4bit".into(),
hf_weight_file: None,
};
four.capabilities = vec![
ModelCapability::Generate,
ModelCapability::Code,
ModelCapability::ToolUse,
];
four.performance.latency_p50_ms = Some(294);
let mut eight = local_model("mlx/qwen3-8b:4bit", "Qwen3-8B-MLX", "8B", 4800);
eight.source = ModelSource::Mlx {
hf_repo: "mlx-community/Qwen3-8B-4bit".into(),
hf_weight_file: None,
};
eight.capabilities = vec![
ModelCapability::Generate,
ModelCapability::Code,
ModelCapability::ToolUse,
];
eight.performance.latency_p50_ms = Some(451);
vec![four, eight]
}
fn refs(v: &[ModelSchema]) -> Vec<&ModelSchema> {
v.iter().collect()
}
#[test]
fn fastest_prefers_the_small_model() {
let cat = catalog();
let recs = recommend(
&refs(&cat),
&mac(36),
UseCase::Coding,
QualityTier::Fastest,
Privacy::OnDevice,
)
.picks;
assert_eq!(recs[0].display_name, "Qwen3-0.6B");
}
#[test]
fn downloadable_catalog_entry_is_not_installed_until_weights_are_ready() {
let mut model = qwen_mlx_policy_catalog().remove(0);
model.available = true;
model.weights_ready = false;
let set = recommend(
&[&model],
&mac(32),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(set.picks.len(), 1);
assert!(!set.picks[0].already_installed);
}
#[test]
fn everyday_32gb_apple_assistant_balanced_prefers_four_b_and_keeps_eight_b() {
let mut catalog = qwen_mlx_policy_catalog();
let mut no_tools = local_model("mlx/qwen3-1.7b:3bit", "Qwen3-1.7B-MLX", "1.7B", 900);
no_tools.source = ModelSource::Mlx {
hf_repo: "mlx-community/Qwen3-1.7B-3bit".into(),
hf_weight_file: None,
};
no_tools.capabilities = vec![ModelCapability::Generate];
catalog.push(no_tools);
let set = recommend_with_policy(
&refs(&catalog),
&mac(32),
&crate::resource_policy::ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
let ids: Vec<&str> = set
.picks
.iter()
.map(|pick| pick.model_id.as_str())
.collect();
assert_eq!(ids, ["mlx/qwen3-4b:4bit", "mlx/qwen3-8b:4bit"]);
let budget = crate::resource_policy::ResourcePolicy::everyday().effective_budget(32 * 1024);
for model in &catalog[..2] {
assert!(
estimate_model_memory(model, &mac(32), RECOMMENDATION_CONTEXT_TOKENS)
.estimated_peak_mb
< budget.configured_model_ceiling_mb
);
}
}
#[test]
fn everyday_target_prefers_under_half_ceiling_but_keeps_heavier_fit_visible() {
let mut four = qwen_mlx_policy_catalog().remove(0);
four.cost.ram_mb = Some(3_500);
four.cost.size_mb = Some(3_500);
let mut nine = four.clone();
nine.id = "mlx/qwen3-9b:4bit".into();
nine.name = "Qwen3-9B-MLX".into();
nine.param_count = "9B".into();
nine.cost.ram_mb = Some(9_000);
nine.cost.size_mb = Some(9_000);
nine.weights_ready = true;
nine.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "quality".into(),
score: 0.99,
harness: None,
source_url: None,
measured_at: None,
}];
let catalog = vec![nine, four];
let set = recommend_with_policy(
&refs(&catalog),
&mac(32),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(set.picks[0].model_id, "mlx/qwen3-4b:4bit");
assert_eq!(set.picks[1].model_id, "mlx/qwen3-9b:4bit");
assert!(set.picks[0].within_recommendation_target);
assert!(!set.picks[1].within_recommendation_target);
let only_heavy = recommend_with_policy(
&[&catalog[0]],
&mac(32),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(only_heavy.picks[0].fit, FitStatus::Fits);
assert!(!only_heavy.picks[0].within_recommendation_target);
}
#[test]
fn everyday_ranking_is_permutation_stable_across_local_and_cloud_candidates() {
let mut four = qwen_mlx_policy_catalog().remove(0);
four.cost.ram_mb = Some(3_500);
four.cost.size_mb = Some(3_500);
let mut nine = four.clone();
nine.id = "mlx/qwen3-9b:4bit".into();
nine.name = "Qwen3-9B-MLX".into();
nine.param_count = "9B".into();
nine.cost.ram_mb = Some(9_000);
nine.cost.size_mb = Some(9_000);
nine.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "quality".into(),
score: 0.99,
harness: None,
source_url: None,
measured_at: None,
}];
let mut cloud = four.clone();
cloud.id = "remote/tool-use".into();
cloud.name = "ToolUse Cloud".into();
cloud.source = ModelSource::RemoteApi {
endpoint: "https://example.invalid".into(),
api_key_env: "TEST_KEY".into(),
api_key_envs: vec![],
api_version: None,
protocol: crate::schema::ApiProtocol::OpenAiCompat,
};
cloud.cost.ram_mb = None;
cloud.cost.size_mb = None;
cloud.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "quality".into(),
score: 1.0,
harness: None,
source_url: None,
measured_at: None,
}];
let candidates = [four, nine, cloud];
let permutations = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
let expected = ["mlx/qwen3-4b:4bit", "remote/tool-use", "mlx/qwen3-9b:4bit"];
for permutation in permutations {
let catalog: Vec<ModelSchema> = permutation
.into_iter()
.map(|index| candidates[index].clone())
.collect();
let set = recommend_with_policy(
&refs(&catalog),
&mac(32),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::CloudOk,
);
let actual: Vec<&str> = set
.picks
.iter()
.map(|pick| pick.model_id.as_str())
.collect();
assert_eq!(actual, expected, "permutation {permutation:?}");
}
}
#[test]
fn cuda_policy_checks_gpu_weights_and_host_overhead_as_separate_pools() {
let mut model = qwen_mlx_policy_catalog().remove(0);
model.source = ModelSource::Local {
hf_repo: "x/y".into(),
hf_filename: "m.gguf".into(),
tokenizer_repo: "x/y".into(),
};
model.cost.ram_mb = Some(1_000);
model.cost.size_mb = Some(1_000);
let vram_too_small = recommend_with_policy(
&[&model],
&hw(GpuBackend::Cuda, 64 * 1024, Some(900)),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(vram_too_small.not_enough_memory[0].fit, FitStatus::TooBig);
model.cost.ram_mb = Some(5_000);
model.cost.size_mb = Some(5_000);
let separate_pools_fit = recommend_with_policy(
&[&model],
&hw(GpuBackend::Cuda, 8 * 1024, Some(16 * 1024)),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(separate_pools_fit.picks[0].fit, FitStatus::Fits);
assert!(separate_pools_fit.picks[0].within_recommendation_target);
}
#[test]
fn custom_zero_blocks_automatic_and_explicit_local_fit() {
let model = qwen_mlx_policy_catalog().remove(0);
let set = recommend_with_policy(
&[&model],
&mac(32),
&ResourcePolicy::custom_gb(0.0).unwrap(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(set.picks.is_empty());
assert_eq!(set.not_enough_memory[0].fit, FitStatus::TooBig);
assert!(!set.not_enough_memory[0].within_recommendation_target);
}
#[test]
fn local_focused_uses_its_full_configured_ceiling() {
let mut model = qwen_mlx_policy_catalog().remove(0);
model.cost.ram_mb = Some(11_500);
model.cost.size_mb = Some(11_500);
let set = recommend_with_policy(
&[&model],
&mac(16),
&ResourcePolicy::local_focused(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(set.picks[0].fit, FitStatus::Fits);
assert!(set.picks[0].within_recommendation_target);
}
#[test]
fn unknown_memory_never_outranks_a_known_fit() {
let mut known = qwen_mlx_policy_catalog().remove(0);
known.public_benchmarks.clear();
let mut unknown = known.clone();
unknown.id = "local/unknown-memory".into();
unknown.name = "Unknown Memory".into();
unknown.param_count.clear();
unknown.cost.ram_mb = None;
unknown.cost.size_mb = None;
unknown.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "quality".into(),
score: 1.0,
harness: None,
source_url: None,
measured_at: None,
}];
let set = recommend_with_policy(
&refs(&[unknown, known]),
&mac(32),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert_eq!(set.picks[0].model_id, "mlx/qwen3-4b:4bit");
assert_eq!(set.picks[1].fit, FitStatus::Unknown);
assert!(!set.picks[1].within_recommendation_target);
}
#[test]
fn everyday_assistant_tool_floor_excludes_generate_only_cloud_models() {
let mut local = qwen_mlx_policy_catalog().remove(0);
local.public_benchmarks.clear();
let mut cloud = local.clone();
cloud.id = "remote/high-score-generate-only".into();
cloud.name = "Remote Generate Only".into();
cloud.source = ModelSource::RemoteApi {
endpoint: "https://api".into(),
api_key_env: "K".into(),
api_key_envs: vec![],
api_version: None,
protocol: crate::schema::ApiProtocol::OpenAiCompat,
};
cloud.capabilities = vec![ModelCapability::Generate];
cloud.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "quality".into(),
score: 1.0,
harness: None,
source_url: None,
measured_at: None,
}];
let set = recommend_with_policy(
&refs(&[cloud, local]),
&mac(32),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::CloudOk,
);
assert_eq!(set.picks.len(), 1);
assert_eq!(set.picks[0].model_id, "mlx/qwen3-4b:4bit");
}
#[test]
fn policy_entry_point_preserves_legacy_order_among_policy_eligible_candidates() {
let catalog = catalog();
for (policy, use_case, tier) in [
(
ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Fastest,
),
(
ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::MostCapable,
),
(
ResourcePolicy::everyday(),
UseCase::Coding,
QualityTier::Balanced,
),
(
ResourcePolicy::local_focused(),
UseCase::Assistant,
QualityTier::Balanced,
),
] {
let legacy = recommend(&refs(&catalog), &mac(36), use_case, tier, Privacy::OnDevice);
let policy_aware = recommend_with_policy(
&refs(&catalog),
&mac(36),
&policy,
use_case,
tier,
Privacy::OnDevice,
);
let legacy_common: Vec<&str> = legacy
.picks
.iter()
.filter(|pick| {
policy_aware
.picks
.iter()
.any(|candidate| candidate.model_id == pick.model_id)
})
.map(|pick| pick.model_id.as_str())
.collect();
let policy_common: Vec<&str> = policy_aware
.picks
.iter()
.filter(|pick| {
legacy
.picks
.iter()
.any(|candidate| candidate.model_id == pick.model_id)
})
.map(|pick| pick.model_id.as_str())
.collect();
assert_eq!(
policy_common, legacy_common,
"{policy:?} {use_case:?} {tier:?}"
);
}
}
#[test]
fn most_capable_prefers_the_big_model_when_it_fits() {
let cat = catalog();
let recs = recommend(
&refs(&cat),
&mac(36), UseCase::Coding,
QualityTier::MostCapable,
Privacy::OnDevice,
)
.picks;
assert_eq!(recs[0].display_name, "Qwen3-30B-A3B");
assert_eq!(recs[0].fit, FitStatus::Fits);
}
#[test]
fn too_big_models_are_excluded_on_small_machines() {
let cat = catalog();
let recs = recommend(
&refs(&cat),
&mac(8), UseCase::Coding,
QualityTier::MostCapable,
Privacy::OnDevice,
)
.picks;
let names: Vec<&str> = recs.iter().map(|r| r.display_name.as_str()).collect();
assert!(!names.contains(&"Qwen3-30B-A3B"), "30B must not fit 8GB");
assert!(recs.iter().all(|r| r.fit == FitStatus::Fits));
assert!(!recs.is_empty(), "the 0.6B model should still be offered");
}
#[test]
fn balanced_picks_a_capable_model_that_fits() {
let cat = catalog();
let recs = recommend(
&refs(&cat),
&mac(16),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
)
.picks;
assert!(matches!(
recs[0].display_name.as_str(),
"Qwen3-4B" | "Qwen3-8B"
));
}
#[test]
fn search_only_returns_embedding_models() {
let mut cat = catalog();
let mut embed = local_model("qwen/embed", "Qwen3-Embedding", "0.6B", 640);
embed.capabilities = vec![ModelCapability::Embed];
cat.push(embed);
let recs = recommend(
&refs(&cat),
&mac(16),
UseCase::Search,
QualityTier::Balanced,
Privacy::OnDevice,
)
.picks;
assert_eq!(recs.len(), 1, "only the embed model is in the Search lane");
assert_eq!(recs[0].display_name, "Qwen3-Embedding");
assert_eq!(recs[0].role, UseCaseRole::Retrieval);
}
#[test]
fn deprecated_models_are_never_recommended() {
let mut cat = catalog();
cat[1].deprecated = true; let recs = recommend(
&refs(&cat),
&mac(16),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
)
.picks;
assert!(recs.iter().all(|r| r.display_name != "Qwen3-4B"));
}
#[test]
fn on_device_excludes_cloud_but_cloud_ok_includes_it_with_consent() {
let mut cat = catalog();
let mut cloud = local_model("anthropic/sonnet", "Claude Sonnet", "", 0);
cloud.capabilities = vec![ModelCapability::Generate, ModelCapability::Code];
cloud.source = ModelSource::RemoteApi {
endpoint: "https://api".into(),
api_key_env: "K".into(),
api_key_envs: vec![],
api_version: None,
protocol: crate::schema::ApiProtocol::Anthropic,
};
cloud.public_benchmarks = vec![crate::schema::BenchmarkScore {
name: "SWE-bench".into(),
score: 0.7,
harness: None,
source_url: None,
measured_at: None,
}];
cat.push(cloud);
let on_device = recommend(
&refs(&cat),
&mac(16),
UseCase::Coding,
QualityTier::MostCapable,
Privacy::OnDevice,
)
.picks;
assert!(on_device.iter().all(|r| r.is_local));
let cloud_ok = recommend(
&refs(&cat),
&mac(16),
UseCase::Coding,
QualityTier::MostCapable,
Privacy::CloudOk,
)
.picks;
let claude = cloud_ok
.iter()
.find(|r| r.display_name == "Claude Sonnet")
.expect("cloud model eligible under CloudOk");
assert!(claude.requires_cloud_consent);
assert_eq!(claude.fit, FitStatus::ServerProvided);
}
#[test]
fn metal_only_model_excluded_on_cpu_host() {
let mut cat = catalog();
let mut mlx = local_model("mlx/qwen3-4b", "Qwen3-4B-MLX", "4B", 2400);
mlx.source = ModelSource::Mlx {
hf_repo: "mlx-community/x".into(),
hf_weight_file: None,
};
cat.push(mlx);
let recs = recommend(
&refs(&cat),
&hw(GpuBackend::Cpu, 32 * 1024, None),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
)
.picks;
assert!(recs.iter().all(|r| r.display_name != "Qwen3-4B-MLX"));
}
#[test]
fn ranking_is_deterministic() {
let cat = catalog();
let a = recommend(
&refs(&cat),
&mac(16),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
let b = recommend(
&refs(&cat),
&mac(16),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
let ids_a: Vec<&str> = a.picks.iter().map(|r| r.model_id.as_str()).collect();
let ids_b: Vec<&str> = b.picks.iter().map(|r| r.model_id.as_str()).collect();
assert_eq!(ids_a, ids_b);
}
#[test]
fn rationale_is_plain_language_no_jargon() {
let cat = catalog();
let recs = recommend(
&refs(&cat),
&mac(36),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
)
.picks;
let r = &recs[0].rationale;
assert!(!r.contains("Q4_K_M"), "no quantization jargon");
assert!(!r.contains("gguf") && !r.contains("hf_repo"));
assert!(r.contains("coding"), "states the purpose");
}
#[test]
fn all_too_big_surfaces_needs_more_ram_with_a_note() {
let cat = catalog();
let set = recommend(
&refs(&cat),
&hw(GpuBackend::Cpu, 2 * 1024, None),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(set.picks.is_empty(), "nothing should fit 2 GB");
assert!(
!set.not_enough_memory.is_empty(),
"too-big models surfaced, not dropped"
);
let note = set.note.expect("empty picks must carry a note");
assert!(note.contains("fits"), "note explains the no-fit: {note}");
assert_eq!(set.not_enough_memory[0].fit, FitStatus::TooBig);
}
#[test]
fn all_deprecated_gives_generic_note_not_a_memory_note() {
let mut cat = catalog();
for m in &mut cat {
m.deprecated = true;
}
let set = recommend(
&refs(&cat),
&mac(36), UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(set.picks.is_empty());
assert!(set.not_enough_memory.is_empty());
let note = set.note.expect("must explain");
assert!(
!note.contains("fits") && !note.contains("memory"),
"deprecated-only must not claim a memory problem: {note}"
);
}
#[test]
fn not_enough_memory_is_ordered_deterministically() {
let cat = catalog();
let mk = || {
recommend(
&refs(&cat),
&hw(GpuBackend::Cpu, 3 * 1024, None), UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
)
.not_enough_memory
.into_iter()
.map(|r| r.model_id)
.collect::<Vec<_>>()
};
assert!(mk().len() >= 2, "several models should be too big for 3 GB");
assert_eq!(mk(), mk(), "too-big ordering must be deterministic");
}
#[test]
fn empty_registry_returns_empty_with_a_note() {
let set = recommend(
&[],
&mac(16),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(set.picks.is_empty());
assert!(set.not_enough_memory.is_empty());
assert!(set.note.is_some(), "no-model case must explain itself");
}
#[test]
fn cuda_box_sizes_against_vram() {
let cat = catalog();
let h = hw(GpuBackend::Cuda, 64 * 1024, Some(24 * 1024));
let recs = recommend(
&refs(&cat),
&h,
UseCase::Coding,
QualityTier::MostCapable,
Privacy::OnDevice,
)
.picks;
assert_eq!(recs[0].display_name, "Qwen3-30B-A3B");
}
#[test]
fn unsupported_discrete_gpu_uses_system_ram_not_vram() {
let cat = catalog();
let mut h = hw(GpuBackend::Cpu, 16 * 1024, None);
h.gpu_devices = vec![GpuDevice {
vendor: GpuVendor::Nvidia,
name: "GeForce RTX 4090".into(),
memory_mb: Some(24_000),
}];
assert!(matches!(
h.supported_acceleration(),
crate::hardware::SupportedAcceleration::UnsupportedDiscreteGpu { .. }
));
let recs = recommend(
&refs(&cat),
&h,
UseCase::Coding,
QualityTier::MostCapable,
Privacy::OnDevice,
)
.picks;
assert!(
recs.iter().all(|r| r.display_name != "Qwen3-30B-A3B"),
"17 GB model must not fit a 16 GB-RAM CPU host"
);
assert!(!recs.is_empty(), "smaller models still fit");
}
#[test]
fn recommendation_set_wire_shape_is_snake_case_and_stable() {
let cat = catalog();
let set = recommend(
&refs(&cat),
&mac(36),
UseCase::Coding,
QualityTier::Balanced,
Privacy::OnDevice,
);
let json = serde_json::to_string(&set).unwrap();
assert!(json.contains("\"picks\""));
assert!(json.contains("\"not_enough_memory\""));
assert!(json.contains("\"model_id\""));
assert!(json.contains("\"already_installed\""));
assert!(json.contains("\"requires_cloud_consent\""));
assert!(json.contains("\"within_recommendation_target\""));
assert!(json.contains("\"fit\""));
let mut legacy = serde_json::to_value(&set.picks[0]).unwrap();
legacy
.as_object_mut()
.unwrap()
.remove("within_recommendation_target");
let decoded: Recommendation = serde_json::from_value(legacy).unwrap();
assert!(decoded.within_recommendation_target);
}
#[test]
fn blank_param_count_estimates_from_size_not_zero() {
let mut m = local_model("x/unknown", "Unknown-Model", "", 4900);
m.param_count = String::new();
assert!(
param_billions_total(&m) > 5.0,
"4.9 GB ⇒ roughly an 8B model, not 0B"
);
}
}
#[cfg(test)]
mod local_server_fit_tests {
use super::*;
use crate::schema::{ModelCapability, ModelSource};
fn managed_vllm_model(id: &str, size_mb: u64) -> ModelSchema {
let mut m = super::tests::local_model(id, id, "12B", size_mb);
m.capabilities.push(ModelCapability::ToolUse);
m.cost.ram_mb = Some(size_mb + size_mb / 4);
m.source = ModelSource::ManagedVllmMlx {
hf_repo: "mlx-community/whatever-4bit".into(),
hf_weight_file: None,
};
m
}
fn external_vllm_model(id: &str, endpoint: &str, size_mb: u64) -> ModelSchema {
let mut m = super::tests::local_model(id, id, "12B", size_mb);
m.capabilities.push(ModelCapability::ToolUse);
m.cost.ram_mb = Some(size_mb + size_mb / 4);
m.source = ModelSource::VllmMlx {
endpoint: endpoint.to_string(),
model_name: "externally-managed-model".into(),
};
m
}
fn small_mac() -> HardwareInfo {
super::tests::hw(crate::hardware::GpuBackend::Metal, 16384, Some(12288))
}
#[test]
fn managed_vllm_mlx_is_memory_checked_and_rejected_when_over_budget() {
let big = managed_vllm_model("vllm-mlx/huge:4bit", 20_000);
let set = recommend_with_policy(
&[&big],
&small_mac(),
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(set.picks.is_empty());
assert_eq!(set.not_enough_memory.len(), 1);
assert_eq!(set.not_enough_memory[0].fit, FitStatus::TooBig);
assert_eq!(
set.not_enough_memory[0].download_mb, 20_000,
"CAR-managed vllm weights must retain their declared download size"
);
}
#[test]
fn external_vllm_mlx_requires_cloud_consent_and_is_cross_platform() {
let machines = [
small_mac(),
super::tests::hw(crate::hardware::GpuBackend::Cpu, 16_384, None),
super::tests::hw(crate::hardware::GpuBackend::Cuda, 16_384, Some(12_288)),
];
for endpoint in [
"http://localhost:8000",
"http://127.0.0.1:8000",
"https://gpu-owner.example/v1",
] {
let external = external_vllm_model("external/vllm", endpoint, 20_000);
for machine in &machines {
let on_device = recommend_with_policy(
&[&external],
machine,
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::OnDevice,
);
assert!(
on_device.picks.is_empty(),
"external endpoint {endpoint} must require cloud consent on {:?}",
machine.gpu_backend
);
let cloud_ok = recommend_with_policy(
&[&external],
machine,
&ResourcePolicy::everyday(),
UseCase::Assistant,
QualityTier::Balanced,
Privacy::CloudOk,
);
assert_eq!(
cloud_ok.picks.len(),
1,
"external endpoint {endpoint} on {:?}",
machine.gpu_backend
);
assert_eq!(cloud_ok.picks[0].fit, FitStatus::ServerProvided);
assert_eq!(
cloud_ok.picks[0].download_mb, 0,
"external vllm owns its weights, so CAR has no download to report"
);
assert!(
cloud_ok.picks[0].rationale.contains("external server"),
"external vllm rationale must describe its actual owner: {}",
cloud_ok.picks[0].rationale
);
assert!(
!cloud_ok.picks[0].rationale.contains("Parslee's servers"),
"external vllm must not be attributed to Parslee: {}",
cloud_ok.picks[0].rationale
);
}
}
}
}