use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareInfo {
pub os: String,
pub arch: String,
pub cpu_cores: usize,
pub total_ram_mb: u64,
pub gpu_backend: GpuBackend,
pub gpu_memory_mb: Option<u64>,
#[serde(default)]
pub gpu_devices: Vec<GpuDevice>,
pub recommended_model: String,
pub recommended_context: usize,
pub max_model_mb: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GpuBackend {
Metal,
Cuda,
Cpu,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "tier", rename_all = "snake_case")]
pub enum SupportedAcceleration {
Apple {
unified_memory_mb: u64,
},
Cuda { device_memory_mb: Option<u64> },
UnsupportedDiscreteGpu {
vendor: GpuVendor,
name: String,
memory_mb: Option<u64>,
},
Cpu,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GpuDevice {
pub vendor: GpuVendor,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mb: Option<u64>,
}
impl GpuDevice {
pub fn is_discrete(&self) -> bool {
is_discrete_gpu(&self.vendor, &self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GpuVendor {
Apple,
Nvidia,
Amd,
Intel,
Other(String),
}
impl GpuVendor {
pub fn from_pci_id(raw: &str) -> Self {
let id = raw.trim().trim_start_matches("0x").to_ascii_lowercase();
match id.as_str() {
"1002" => Self::Amd,
"10de" => Self::Nvidia,
"8086" => Self::Intel,
"106b" => Self::Apple,
other => Self::Other(other.to_string()),
}
}
pub fn from_name(name: &str) -> Self {
let lower = name.to_ascii_lowercase();
if lower.contains("nvidia")
|| lower.contains("geforce")
|| lower.contains("quadro")
|| lower.contains("tesla")
|| lower.contains("rtx")
|| lower.contains("gtx")
{
Self::Nvidia
} else if lower.contains("amd")
|| lower.contains("radeon")
|| lower.contains("rx ")
|| lower.contains("vega")
|| lower.contains("instinct")
{
Self::Amd
} else if lower.contains("intel")
|| lower.contains("arc ")
|| lower.contains("iris")
|| lower.contains("uhd graphics")
|| lower.contains("hd graphics")
{
Self::Intel
} else if lower.contains("apple")
|| lower.starts_with("m1")
|| lower.starts_with("m2")
|| lower.starts_with("m3")
|| lower.starts_with("m4")
{
Self::Apple
} else {
Self::Other(name.to_string())
}
}
}
impl HardwareInfo {
pub fn supported_acceleration(&self) -> SupportedAcceleration {
match self.gpu_backend {
GpuBackend::Metal => SupportedAcceleration::Apple {
unified_memory_mb: self.total_ram_mb,
},
GpuBackend::Cuda => SupportedAcceleration::Cuda {
device_memory_mb: self.gpu_memory_mb,
},
GpuBackend::Cpu => {
if let Some(dev) = self
.gpu_devices
.iter()
.find(|d| is_discrete_gpu(&d.vendor, &d.name))
{
SupportedAcceleration::UnsupportedDiscreteGpu {
vendor: dev.vendor.clone(),
name: dev.name.clone(),
memory_mb: dev.memory_mb,
}
} else {
SupportedAcceleration::Cpu
}
}
}
}
pub fn detect() -> Self {
let os = detect_os();
let arch = std::env::consts::ARCH.to_string();
let cpu_cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let total_ram_mb = detect_ram_mb();
let gpu_backend = detect_gpu_backend();
let gpu_memory_mb = detect_gpu_memory_mb(&gpu_backend, total_ram_mb);
let gpu_devices = detect_gpu_devices();
let embedding_model_mb: u64 = 700;
let ram_budget_mb = ((total_ram_mb as f64 * 0.6) as u64).saturating_sub(embedding_model_mb);
let recommend_budget_mb =
gpu_recommend_budget_mb(&gpu_backend, gpu_memory_mb, ram_budget_mb);
let max_model_mb = ram_budget_mb.max(recommend_budget_mb);
let available_mb = total_ram_mb.max(gpu_memory_mb.unwrap_or(0));
let recommended_model = recommend_model(recommend_budget_mb);
let recommended_context = recommend_context(available_mb, &recommended_model);
Self {
os,
arch,
cpu_cores,
total_ram_mb,
gpu_backend,
gpu_memory_mb,
gpu_devices,
recommended_model,
recommended_context,
max_model_mb,
}
}
pub fn tool_capable_hint(&self) -> Option<String> {
if !matches!(self.gpu_backend, GpuBackend::Cuda) {
return None;
}
let vram = self.gpu_memory_mb?;
if model_is_tool_capable(&self.recommended_model) {
return None;
}
let weights_fit = recommend_model((vram as f64 * 0.9) as u64);
if weights_fit != self.recommended_model && model_is_tool_capable(&weights_fit) {
Some(format!(
"{weights_fit} adds tool-use but is tight on {vram} MB VRAM - \
heavy generations may fall back"
))
} else {
None
}
}
}
fn model_is_tool_capable(name: &str) -> bool {
let l = name.to_ascii_lowercase();
l.contains("4b") || l.contains("8b") || l.contains("30b")
}
fn detect_os() -> String {
if cfg!(target_os = "macos") {
"macos".into()
} else if cfg!(target_os = "linux") {
"linux".into()
} else if cfg!(target_os = "windows") {
"windows".into()
} else {
std::env::consts::OS.into()
}
}
pub fn available_ram_mb() -> Option<u64> {
#[cfg(target_os = "macos")]
{
let mut count: libc::mach_msg_type_number_t = (std::mem::size_of::<libc::vm_statistics64>()
/ std::mem::size_of::<libc::integer_t>())
as libc::mach_msg_type_number_t;
let mut stats: libc::vm_statistics64 = unsafe { std::mem::zeroed() };
#[allow(deprecated)]
let rc = unsafe {
libc::host_statistics64(
libc::mach_host_self(),
libc::HOST_VM_INFO64,
&mut stats as *mut libc::vm_statistics64 as *mut libc::integer_t,
&mut count,
)
};
if rc == libc::KERN_SUCCESS {
let page = page_size_bytes();
let reclaimable = (stats.free_count as u64)
.saturating_sub(stats.speculative_count as u64)
+ stats.inactive_count as u64
+ stats.purgeable_count as u64;
return Some(reclaimable.saturating_mul(page) / (1024 * 1024));
}
return None;
}
#[cfg(target_os = "linux")]
{
let content = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("MemAvailable:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb / 1024);
}
}
return None;
}
#[cfg(target_os = "windows")]
{
return windows_available_ram_mb();
}
#[allow(unreachable_code)]
None
}
#[cfg(target_os = "macos")]
fn page_size_bytes() -> u64 {
let rc = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if rc > 0 {
rc as u64
} else {
4096
}
}
#[cfg(target_os = "windows")]
fn windows_available_ram_mb() -> Option<u64> {
use std::process::Command;
let out = Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory",
])
.output()
.ok()?;
let kb: u64 = String::from_utf8_lossy(&out.stdout).trim().parse().ok()?;
Some(kb / 1024)
}
fn detect_ram_mb() -> u64 {
#[cfg(target_os = "macos")]
{
let mut size: u64 = 0;
let mut len: libc::size_t = std::mem::size_of::<u64>();
let rc = unsafe {
libc::sysctlbyname(
c"hw.memsize".as_ptr(),
&mut size as *mut u64 as *mut libc::c_void,
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc == 0 && size > 0 {
return size / (1024 * 1024);
}
}
#[cfg(target_os = "linux")]
{
if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
for line in content.lines() {
if line.starts_with("MemTotal:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(kb) = parts[1].parse::<u64>() {
return kb / 1024;
}
}
}
}
}
}
#[cfg(target_os = "windows")]
{
if let Some(mb) = detect_ram_mb_windows() {
return mb;
}
}
8192
}
#[cfg(target_os = "windows")]
pub fn detect_ram_mb_windows() -> Option<u64> {
if let Ok(output) = std::process::Command::new("wmic")
.args([
"ComputerSystem",
"get",
"TotalPhysicalMemory",
"/format:list",
])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
if let Some(mb) = parse_total_physical_memory_bytes(&s) {
return Some(mb);
}
}
}
if let Ok(output) = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory",
])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
if let Some(mb) = parse_total_physical_memory_bytes(&s) {
return Some(mb);
}
}
}
None
}
#[allow(dead_code)] fn parse_total_physical_memory_bytes(stdout: &str) -> Option<u64> {
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let candidate = if let Some((key, val)) = line.split_once('=') {
if !key.trim().eq_ignore_ascii_case("TotalPhysicalMemory") {
continue;
}
val.trim()
} else {
line
};
if let Ok(bytes) = candidate.parse::<u64>() {
if bytes > 0 {
return Some(bytes / (1024 * 1024));
}
}
}
None
}
fn is_discrete_gpu(vendor: &GpuVendor, name: &str) -> bool {
let lower = name
.to_ascii_lowercase()
.replace("(tm)", "")
.replace("(r)", "")
.replace(['™', '®'], "");
if matches!(vendor, GpuVendor::Apple) {
return false;
}
if matches!(vendor, GpuVendor::Intel) {
return lower.contains("arc ");
}
if matches!(vendor, GpuVendor::Amd) {
let integrated_markers = [
"vega 6",
"vega 7",
"vega 8",
"vega 9",
"vega 10",
"vega 11",
"radeon graphics", ];
if integrated_markers.iter().any(|m| lower.contains(m)) {
return false;
}
return true;
}
if matches!(vendor, GpuVendor::Nvidia) {
return true;
}
matches!(vendor, GpuVendor::Other(_))
}
fn detect_gpu_backend() -> GpuBackend {
#[cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))]
{
GpuBackend::Metal
}
#[cfg(all(
feature = "metal",
not(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))
))]
{
return GpuBackend::Metal;
}
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda),
not(feature = "metal")
))]
{
return GpuBackend::Cuda;
}
#[cfg(all(
not(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
)),
not(feature = "metal"),
not(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))
))]
{
GpuBackend::Cpu
}
}
pub fn cuda_runtime_available() -> Option<bool> {
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
))]
{
use std::sync::OnceLock;
static CACHE: OnceLock<bool> = OnceLock::new();
Some(*CACHE.get_or_init(probe_cublas_symbol))
}
#[cfg(not(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
)))]
{
None
}
}
pub fn nvidia_driver_present() -> Option<bool> {
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
))]
{
use std::sync::OnceLock;
static CACHE: OnceLock<bool> = OnceLock::new();
Some(*CACHE.get_or_init(|| {
#[cfg(target_os = "windows")]
let name = "nvcuda.dll";
#[cfg(target_os = "linux")]
let name = "libcuda.so.1";
unsafe { libloading::Library::new(name).is_ok() }
}))
}
#[cfg(not(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
)))]
{
None
}
}
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
))]
fn probe_cublas_symbol() -> bool {
#[cfg(target_os = "windows")]
let lib_name = "cublas64_12.dll";
#[cfg(target_os = "linux")]
let lib_name = "libcublas.so.12";
unsafe {
match libloading::Library::new(lib_name) {
Ok(lib) => lib
.get::<unsafe extern "C" fn() -> i32>(b"cublasCreate_v2\0")
.is_ok(),
Err(_) => false,
}
}
}
fn detect_gpu_memory_mb(backend: &GpuBackend, total_ram_mb: u64) -> Option<u64> {
match backend {
GpuBackend::Metal => {
Some(total_ram_mb.saturating_sub(4096))
}
GpuBackend::Cuda => {
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
if let Ok(output) = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=memory.total", "--format=csv,noheader,nounits"])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
if let Some(first) = s.lines().next() {
if let Ok(mb) = first.trim().parse::<u64>() {
return Some(mb);
}
}
}
}
}
None
}
GpuBackend::Cpu => None,
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn nvidia_smi_gpus() -> Vec<GpuDevice> {
std::process::Command::new("nvidia-smi")
.args([
"--query-gpu=name,memory.total",
"--format=csv,noheader,nounits",
])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| parse_nvidia_smi_gpus(&s))
.unwrap_or_default()
}
fn detect_gpu_devices() -> Vec<GpuDevice> {
#[cfg(target_os = "linux")]
{
return detect_gpu_devices_linux();
}
#[cfg(target_os = "windows")]
{
return detect_gpu_devices_windows();
}
#[cfg(target_os = "macos")]
{
return detect_gpu_devices_macos();
}
#[allow(unreachable_code)]
Vec::new()
}
#[cfg(target_os = "linux")]
fn detect_gpu_devices_linux() -> Vec<GpuDevice> {
let mut out = Vec::new();
let drm = match std::fs::read_dir("/sys/class/drm") {
Ok(d) => d,
Err(_) => return out,
};
let mut seen = std::collections::HashSet::new();
for entry in drm.flatten() {
let name = entry.file_name();
let s = name.to_string_lossy();
if !s.starts_with("card") || s.contains('-') {
continue;
}
let device_path = entry.path().join("device");
let vendor_id = std::fs::read_to_string(device_path.join("vendor"))
.ok()
.map(|s| s.trim().to_string());
if let Some(vid) = &vendor_id {
if !seen.insert(format!("{}:{}", vid, entry.path().display())) {
continue;
}
}
let device_name = read_drm_device_name(&device_path).unwrap_or_else(|| s.to_string());
let vendor = vendor_id
.as_deref()
.map(GpuVendor::from_pci_id)
.unwrap_or_else(|| GpuVendor::Other("unknown".into()));
out.push(GpuDevice {
vendor,
name: device_name,
memory_mb: read_drm_memory_mb(&device_path),
});
}
reconcile_nvidia(out, nvidia_smi_gpus())
}
#[cfg(target_os = "linux")]
fn read_drm_device_name(device: &std::path::Path) -> Option<String> {
if let Ok(name) = std::fs::read_to_string(device.join("product_name")) {
let trimmed = name.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
std::fs::read_to_string(device.join("device"))
.ok()
.map(|s| format!("PCI device {}", s.trim()))
}
#[cfg(target_os = "linux")]
fn read_drm_memory_mb(device: &std::path::Path) -> Option<u64> {
if let Ok(s) = std::fs::read_to_string(device.join("mem_info_vram_total")) {
if let Ok(bytes) = s.trim().parse::<u64>() {
return Some(bytes / (1024 * 1024));
}
}
None
}
#[cfg(target_os = "windows")]
fn detect_gpu_devices_windows() -> Vec<GpuDevice> {
reconcile_nvidia(wmi_gpu_devices(), nvidia_smi_gpus())
}
#[cfg(target_os = "windows")]
fn wmi_gpu_devices() -> Vec<GpuDevice> {
if let Ok(output) = std::process::Command::new("wmic")
.args([
"path",
"Win32_VideoController",
"get",
"Name,AdapterRAM",
"/format:list",
])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
return parse_windows_wmic_list(&s);
}
}
if let Ok(output) = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM | ConvertTo-Csv -NoTypeInformation",
])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
return parse_windows_wmic_csv(&s);
}
}
Vec::new()
}
#[cfg(target_os = "macos")]
fn detect_gpu_devices_macos() -> Vec<GpuDevice> {
if let Ok(output) = std::process::Command::new("system_profiler")
.args(["SPDisplaysDataType", "-json"])
.output()
{
if let Ok(s) = String::from_utf8(output.stdout) {
return parse_macos_system_profiler_json(&s);
}
}
Vec::new()
}
#[allow(dead_code)] fn parse_windows_wmic_list(stdout: &str) -> Vec<GpuDevice> {
let mut out = Vec::new();
let mut name: Option<String> = None;
let mut adapter_ram_bytes: Option<u64> = None;
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
if let Some(n) = name.take() {
out.push(GpuDevice {
vendor: GpuVendor::from_name(&n),
name: n,
memory_mb: adapter_ram_bytes.map(|b| b / (1024 * 1024)),
});
}
adapter_ram_bytes = None;
continue;
}
if let Some(rest) = line.strip_prefix("Name=") {
name = Some(rest.to_string());
} else if let Some(rest) = line.strip_prefix("AdapterRAM=") {
adapter_ram_bytes = rest.parse::<u64>().ok();
}
}
if let Some(n) = name {
out.push(GpuDevice {
vendor: GpuVendor::from_name(&n),
name: n,
memory_mb: adapter_ram_bytes.map(|b| b / (1024 * 1024)),
});
}
out
}
#[allow(dead_code)]
fn parse_windows_wmic_csv(stdout: &str) -> Vec<GpuDevice> {
let mut out = Vec::new();
let mut headers: Option<Vec<String>> = None;
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let cols: Vec<String> = line
.split(',')
.map(|c| c.trim().trim_matches('"').to_string())
.collect();
if headers.is_none() {
headers = Some(cols);
continue;
}
let h = headers.as_ref().unwrap();
let mut name = String::new();
let mut bytes: Option<u64> = None;
for (i, col) in cols.iter().enumerate() {
match h.get(i).map(|s| s.as_str()) {
Some("Name") => name = col.clone(),
Some("AdapterRAM") => bytes = col.parse::<u64>().ok(),
_ => {}
}
}
if !name.is_empty() {
out.push(GpuDevice {
vendor: GpuVendor::from_name(&name),
name,
memory_mb: bytes.map(|b| b / (1024 * 1024)),
});
}
}
out
}
fn parse_nvidia_smi_gpus(stdout: &str) -> Vec<GpuDevice> {
let mut out = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (name, mem) = match line.rsplit_once(',') {
Some((n, m)) => (n.trim(), m.trim()),
None => (line, ""),
};
if name.is_empty() {
continue;
}
out.push(GpuDevice {
vendor: GpuVendor::Nvidia,
name: name.to_string(),
memory_mb: mem.parse::<u64>().ok(),
});
}
out
}
fn reconcile_nvidia(base: Vec<GpuDevice>, nvidia: Vec<GpuDevice>) -> Vec<GpuDevice> {
if nvidia.is_empty() {
return base;
}
let mut out = nvidia;
out.extend(base.into_iter().filter(|d| d.vendor != GpuVendor::Nvidia));
out
}
#[allow(dead_code)]
fn parse_macos_system_profiler_json(stdout: &str) -> Vec<GpuDevice> {
let value: serde_json::Value = match serde_json::from_str(stdout) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let displays = match value.get("SPDisplaysDataType").and_then(|v| v.as_array()) {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::new();
for d in displays {
let name = d
.get("_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if name.is_empty() {
continue;
}
let memory_mb = d
.get("spdisplays_vram")
.or_else(|| d.get("spdisplays_vram_shared"))
.and_then(|v| v.as_str())
.and_then(parse_human_memory_mb);
out.push(GpuDevice {
vendor: GpuVendor::from_name(&name),
name,
memory_mb,
});
}
out
}
fn parse_human_memory_mb(s: &str) -> Option<u64> {
let mut number_buf = String::new();
let mut after_number = false;
let mut unit_buf = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() && !after_number {
number_buf.push(ch);
} else if !number_buf.is_empty() && (ch.is_ascii_alphabetic()) {
after_number = true;
unit_buf.push(ch);
} else if after_number && unit_buf.len() >= 2 {
break;
}
}
let value: u64 = number_buf.parse().ok()?;
let unit = unit_buf.to_ascii_uppercase();
let mb = if unit.starts_with("GB") {
value * 1024
} else if unit.starts_with("MB") {
value
} else if unit.starts_with("KB") {
value / 1024
} else {
return None;
};
Some(mb)
}
const VRAM_RUNTIME_RESERVE_MB: u64 = 2048;
fn gpu_recommend_budget_mb(
gpu_backend: &GpuBackend,
gpu_memory_mb: Option<u64>,
ram_budget_mb: u64,
) -> u64 {
match gpu_backend {
GpuBackend::Cuda => gpu_memory_mb
.map(|vram| vram.saturating_sub(VRAM_RUNTIME_RESERVE_MB))
.unwrap_or(ram_budget_mb),
GpuBackend::Metal | GpuBackend::Cpu => ram_budget_mb,
}
}
fn recommend_model(max_model_mb: u64) -> String {
#[cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))]
{
if max_model_mb >= 17000 {
return "Qwen3-30B-A3B-MLX".into();
} else if max_model_mb >= 4900 {
return "Qwen3-8B-MLX".into();
} else if max_model_mb >= 2500 {
return "Qwen3-4B-MLX".into();
} else if max_model_mb >= 800 {
return "Qwen3-1.7B-MLX".into();
}
"Qwen3-0.6B-MLX".into()
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx))))]
if max_model_mb >= 17000 {
"Qwen3-30B-A3B".into()
} else if max_model_mb >= 4900 {
"Qwen3-8B".into()
} else if max_model_mb >= 2500 {
"Qwen3-4B".into()
} else if max_model_mb >= 1800 {
"Qwen3-1.7B".into()
} else {
"Qwen3-0.6B".into()
}
}
fn recommend_context(available_mb: u64, model_name: &str) -> usize {
let model_mb = match model_name {
"Qwen3-0.6B" => 650,
"Qwen3-1.7B" => 1800,
"Qwen3-4B" => 2500,
"Qwen3-4B-MLX" => 2400,
"Qwen3-8B" => 4900,
"Qwen3-8B-MLX" => 4800,
"Qwen3-30B-A3B" => 17000,
"Qwen3-30B-A3B-MLX" => 16500,
"Qwen3-1.7B-MLX" => 800,
"Qwen3-0.6B-MLX" => 500,
_ => 650,
};
let kv_cost_per_1k = match model_name {
"Qwen3-0.6B" => 0.1,
"Qwen3-1.7B" => 0.3,
"Qwen3-4B" => 0.5,
"Qwen3-4B-MLX" => 0.5,
"Qwen3-8B" => 1.0,
"Qwen3-8B-MLX" => 1.0,
"Qwen3-30B-A3B" => 1.5,
"Qwen3-30B-A3B-MLX" => 1.5,
"Qwen3-1.7B-MLX" => 0.3,
"Qwen3-0.6B-MLX" => 0.1,
_ => 0.1,
};
let kv_budget_mb = available_mb.saturating_sub(model_mb).saturating_sub(1024) as f64;
let max_context = (kv_budget_mb / kv_cost_per_1k * 1000.0) as usize;
max_context.clamp(2048, 131072)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cuda_runtime_available_matches_build_and_never_panics() {
let r = cuda_runtime_available();
#[cfg(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
))]
assert!(r.is_some(), "CUDA build must resolve the probe to Some");
#[cfg(not(all(
any(target_os = "linux", target_os = "windows"),
target_arch = "x86_64",
not(car_skip_cuda)
)))]
assert_eq!(r, None, "non-CUDA build must report the probe as N/A");
}
#[test]
fn available_ram_is_readable_and_bounded_by_total() {
let total = detect_ram_mb();
assert!(total > 0, "total RAM must be detectable");
let avail = available_ram_mb().expect("available RAM must be readable");
assert!(
avail <= total,
"available ({avail} MB) cannot exceed total ({total} MB)"
);
}
#[test]
fn available_ram_differs_from_total() {
let total = detect_ram_mb();
let avail = available_ram_mb().expect("available RAM must be readable");
assert!(
avail < total,
"available ({avail} MB) == total ({total} MB) — reporting capacity, not availability"
);
}
#[test]
fn tool_capable_hint_names_the_tighter_option() {
assert!(model_is_tool_capable("Qwen3-4B"));
assert!(model_is_tool_capable("Qwen3-8B-MLX"));
assert!(model_is_tool_capable("Qwen3-30B-A3B"));
assert!(!model_is_tool_capable("Qwen3-1.7B"));
assert!(!model_is_tool_capable("Qwen3-0.6B"));
let hw = |backend, vram, rec: &str| HardwareInfo {
os: "windows".into(),
arch: "x86_64".into(),
cpu_cores: 12,
total_ram_mb: 16000,
gpu_backend: backend,
gpu_memory_mb: vram,
gpu_devices: vec![],
recommended_model: rec.into(),
recommended_context: 131072,
max_model_mb: 8643,
};
let hint = hw(GpuBackend::Cuda, Some(4096), "Qwen3-1.7B").tool_capable_hint();
assert!(
hint.as_deref().unwrap_or("").contains("Qwen3-4B"),
"got {hint:?}"
);
assert!(hw(GpuBackend::Cuda, Some(24576), "Qwen3-30B-A3B")
.tool_capable_hint()
.is_none());
assert!(hw(GpuBackend::Cpu, None, "Qwen3-1.7B")
.tool_capable_hint()
.is_none());
}
#[test]
fn gpu_recommend_budget_reserves_working_headroom() {
use GpuBackend::*;
let ram = 8643; assert_eq!(gpu_recommend_budget_mb(&Cuda, Some(4096), ram), 4096 - 2048);
assert!(
gpu_recommend_budget_mb(&Cuda, Some(4096), ram) < 2500,
"4 GB must fall below the 4B band"
);
assert!(gpu_recommend_budget_mb(&Cuda, Some(8192), ram) >= 4900);
assert!(gpu_recommend_budget_mb(&Cuda, Some(24576), ram) >= 17000);
assert_eq!(gpu_recommend_budget_mb(&Cuda, Some(1024), ram), 0);
assert_eq!(gpu_recommend_budget_mb(&Cuda, None, ram), ram);
assert_eq!(gpu_recommend_budget_mb(&Metal, Some(16384), ram), ram);
assert_eq!(gpu_recommend_budget_mb(&Cpu, None, ram), ram);
}
#[test]
fn parse_total_physical_memory_wmic_list() {
let out = "\r\n\r\nTotalPhysicalMemory=34305015808\r\n\r\n";
assert_eq!(parse_total_physical_memory_bytes(out), Some(32715));
}
#[test]
fn parse_total_physical_memory_powershell_bare_integer() {
assert_eq!(
parse_total_physical_memory_bytes("34305015808\r\n"),
Some(32715)
);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_ram_detection_matches_sysctl() {
let got = detect_ram_mb();
assert!(
got >= 1024,
"RAM detection returned implausibly low {got} MB"
);
if let Ok(o) = std::process::Command::new("sysctl")
.args(["-n", "hw.memsize"])
.output()
{
if let Ok(s) = String::from_utf8(o.stdout) {
if let Ok(bytes) = s.trim().parse::<u64>() {
assert_eq!(
got,
bytes / (1024 * 1024),
"sysctlbyname read disagrees with `sysctl` subprocess"
);
}
}
}
}
#[test]
fn parse_total_physical_memory_rejects_garbage_and_zero() {
assert_eq!(parse_total_physical_memory_bytes(""), None);
assert_eq!(parse_total_physical_memory_bytes("not a number\r\n"), None);
assert_eq!(
parse_total_physical_memory_bytes("TotalPhysicalMemory=0"),
None
);
assert_eq!(
parse_total_physical_memory_bytes("TotalVisibleMemorySize=8388608"),
None
);
}
#[test]
fn pci_id_to_vendor_known() {
assert_eq!(GpuVendor::from_pci_id("0x1002"), GpuVendor::Amd);
assert_eq!(GpuVendor::from_pci_id("0x10de"), GpuVendor::Nvidia);
assert_eq!(GpuVendor::from_pci_id("0x8086"), GpuVendor::Intel);
assert_eq!(GpuVendor::from_pci_id("0x10DE"), GpuVendor::Nvidia);
}
#[test]
fn pci_id_to_vendor_unknown_falls_through() {
match GpuVendor::from_pci_id("0xabcd") {
GpuVendor::Other(s) => assert_eq!(s, "abcd"),
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn name_to_vendor_picks_amd_radeon() {
assert_eq!(
GpuVendor::from_name("AMD Radeon RX 7900 XTX"),
GpuVendor::Amd
);
assert_eq!(GpuVendor::from_name("Radeon Pro 580X"), GpuVendor::Amd);
}
#[test]
fn name_to_vendor_picks_nvidia() {
assert_eq!(
GpuVendor::from_name("NVIDIA GeForce RTX 4090"),
GpuVendor::Nvidia
);
assert_eq!(GpuVendor::from_name("Quadro P2200"), GpuVendor::Nvidia);
}
#[test]
fn name_to_vendor_picks_intel_arc_and_iris() {
assert_eq!(GpuVendor::from_name("Intel Arc A770"), GpuVendor::Intel);
assert_eq!(
GpuVendor::from_name("Intel Iris Xe Graphics"),
GpuVendor::Intel
);
}
#[test]
fn name_to_vendor_picks_apple_silicon() {
assert_eq!(GpuVendor::from_name("Apple M3 Max"), GpuVendor::Apple);
}
#[test]
fn parse_wmic_list_two_devices() {
let stdout = "\r\nName=NVIDIA GeForce RTX 4090\r\nAdapterRAM=25756221440\r\n\r\nName=Intel UHD Graphics 770\r\nAdapterRAM=1073741824\r\n\r\n";
let devices = parse_windows_wmic_list(stdout);
assert_eq!(devices.len(), 2);
assert_eq!(devices[0].vendor, GpuVendor::Nvidia);
assert_eq!(devices[0].name, "NVIDIA GeForce RTX 4090");
assert_eq!(devices[0].memory_mb, Some(25756221440 / (1024 * 1024)));
assert_eq!(devices[1].vendor, GpuVendor::Intel);
assert_eq!(devices[1].memory_mb, Some(1024));
}
#[test]
fn parse_wmic_list_handles_missing_ram() {
let stdout = "Name=Microsoft Basic Display Adapter\r\n\r\n";
let devices = parse_windows_wmic_list(stdout);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].memory_mb, None);
}
#[test]
fn parse_nvidia_smi_reads_name_and_vram() {
let stdout = "NVIDIA GeForce RTX 2050, 4096\r\nNVIDIA A100-SXM4-40GB, 40960\n";
let devices = parse_nvidia_smi_gpus(stdout);
assert_eq!(devices.len(), 2);
assert_eq!(devices[0].vendor, GpuVendor::Nvidia);
assert_eq!(devices[0].name, "NVIDIA GeForce RTX 2050");
assert_eq!(devices[0].memory_mb, Some(4096));
assert_eq!(devices[1].name, "NVIDIA A100-SXM4-40GB");
assert_eq!(devices[1].memory_mb, Some(40960));
}
#[test]
fn reconcile_leads_with_nvidia_and_drops_wmi_duplicate() {
let wmi = vec![GpuDevice {
vendor: GpuVendor::Amd,
name: "AMD Radeon(TM) Graphics".into(),
memory_mb: Some(512),
}];
let smi = vec![GpuDevice {
vendor: GpuVendor::Nvidia,
name: "NVIDIA GeForce RTX 2050".into(),
memory_mb: Some(4096),
}];
let merged = reconcile_nvidia(wmi, smi);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].vendor, GpuVendor::Nvidia);
assert_eq!(merged[0].memory_mb, Some(4096));
assert_eq!(merged[1].vendor, GpuVendor::Amd);
}
#[test]
fn reconcile_replaces_a_wmi_nvidia_entry_with_smi() {
let wmi = vec![GpuDevice {
vendor: GpuVendor::Nvidia,
name: "NVIDIA GeForce RTX 2050".into(),
memory_mb: Some(512), }];
let smi = vec![GpuDevice {
vendor: GpuVendor::Nvidia,
name: "NVIDIA GeForce RTX 2050".into(),
memory_mb: Some(4096),
}];
let merged = reconcile_nvidia(wmi, smi);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].memory_mb, Some(4096));
}
#[test]
fn reconcile_no_nvidia_leaves_list_untouched() {
let wmi = vec![GpuDevice {
vendor: GpuVendor::Amd,
name: "AMD Radeon".into(),
memory_mb: Some(8192),
}];
let merged = reconcile_nvidia(wmi.clone(), vec![]);
assert_eq!(merged, wmi);
}
#[test]
fn gpu_device_is_discrete_flags_integrated_apu_and_rtx() {
let apu = GpuDevice {
vendor: GpuVendor::Amd,
name: "AMD Radeon(TM) Graphics".into(),
memory_mb: Some(512),
};
assert!(!apu.is_discrete());
let rtx = GpuDevice {
vendor: GpuVendor::Nvidia,
name: "NVIDIA GeForce RTX 2050".into(),
memory_mb: Some(4096),
};
assert!(rtx.is_discrete());
}
#[test]
fn parse_macos_system_profiler_apple_silicon() {
let stdout = r#"{
"SPDisplaysDataType": [
{
"_name": "Apple M3 Max",
"spdisplays_vram_shared": "Up to 96 GB"
}
]
}"#;
let devices = parse_macos_system_profiler_json(stdout);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].vendor, GpuVendor::Apple);
assert_eq!(devices[0].memory_mb, Some(96 * 1024));
}
#[test]
fn parse_macos_system_profiler_discrete_amd() {
let stdout = r#"{
"SPDisplaysDataType": [
{
"_name": "AMD Radeon Pro 5500M",
"spdisplays_vram": "8 GB"
}
]
}"#;
let devices = parse_macos_system_profiler_json(stdout);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].vendor, GpuVendor::Amd);
assert_eq!(devices[0].memory_mb, Some(8 * 1024));
}
#[test]
fn parse_macos_system_profiler_handles_garbage() {
assert!(parse_macos_system_profiler_json("not json").is_empty());
assert!(parse_macos_system_profiler_json("{}").is_empty());
}
#[cfg(target_os = "macos")]
#[test]
fn live_macos_detection_returns_at_least_one_device() {
let devices = detect_gpu_devices_macos();
assert!(
!devices.is_empty(),
"expected at least one GPU device on macOS",
);
}
#[test]
fn human_memory_parser() {
assert_eq!(parse_human_memory_mb("8 GB"), Some(8 * 1024));
assert_eq!(parse_human_memory_mb("Up to 96 GB"), Some(96 * 1024));
assert_eq!(parse_human_memory_mb("1024 MB"), Some(1024));
assert_eq!(parse_human_memory_mb("nope"), None);
}
fn hw_with_devices(backend: GpuBackend, devices: Vec<GpuDevice>) -> HardwareInfo {
HardwareInfo {
os: "linux".into(),
arch: "x86_64".into(),
cpu_cores: 8,
total_ram_mb: 32_000,
gpu_backend: backend,
gpu_memory_mb: None,
gpu_devices: devices,
recommended_model: String::new(),
recommended_context: 4096,
max_model_mb: 0,
}
}
#[test]
fn discrete_gpu_heuristic() {
assert!(is_discrete_gpu(&GpuVendor::Nvidia, "GeForce RTX 4090"));
assert!(is_discrete_gpu(&GpuVendor::Amd, "Radeon RX 7900 XTX"));
assert!(is_discrete_gpu(&GpuVendor::Amd, "Radeon Pro W7900"));
assert!(!is_discrete_gpu(&GpuVendor::Amd, "Radeon Vega 8 Graphics"));
assert!(!is_discrete_gpu(&GpuVendor::Amd, "AMD Radeon(TM) Graphics"));
assert!(is_discrete_gpu(
&GpuVendor::Intel,
"Intel Arc A770 Graphics"
));
assert!(!is_discrete_gpu(
&GpuVendor::Intel,
"Intel(R) UHD Graphics 630"
));
assert!(!is_discrete_gpu(
&GpuVendor::Intel,
"Intel Iris Xe Graphics"
));
assert!(!is_discrete_gpu(&GpuVendor::Apple, "Apple M3 Max"));
}
#[test]
fn supported_acceleration_metal_apple_silicon() {
let hw = hw_with_devices(
GpuBackend::Metal,
vec![GpuDevice {
vendor: GpuVendor::Apple,
name: "Apple M3".into(),
memory_mb: None,
}],
);
match hw.supported_acceleration() {
SupportedAcceleration::Apple { unified_memory_mb } => {
assert_eq!(unified_memory_mb, 32_000);
}
other => panic!("expected Apple, got {:?}", other),
}
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64", not(car_skip_mlx)))]
fn detect_gpu_backend_reports_metal_on_apple_silicon() {
assert_eq!(detect_gpu_backend(), GpuBackend::Metal);
let hw = HardwareInfo::detect();
assert!(
matches!(
hw.supported_acceleration(),
SupportedAcceleration::Apple { .. }
),
"expected Apple tier on Apple Silicon, got {:?}",
hw.supported_acceleration()
);
}
#[test]
fn supported_acceleration_cuda_with_memory() {
let mut hw = hw_with_devices(
GpuBackend::Cuda,
vec![GpuDevice {
vendor: GpuVendor::Nvidia,
name: "GeForce RTX 4090".into(),
memory_mb: Some(24_000),
}],
);
hw.gpu_memory_mb = Some(24_000);
match hw.supported_acceleration() {
SupportedAcceleration::Cuda { device_memory_mb } => {
assert_eq!(device_memory_mb, Some(24_000));
}
other => panic!("expected Cuda, got {:?}", other),
}
}
#[test]
fn supported_acceleration_unsupported_amd_discrete() {
let hw = hw_with_devices(
GpuBackend::Cpu,
vec![GpuDevice {
vendor: GpuVendor::Amd,
name: "Radeon RX 7900 XTX".into(),
memory_mb: Some(24_000),
}],
);
match hw.supported_acceleration() {
SupportedAcceleration::UnsupportedDiscreteGpu {
vendor,
name,
memory_mb,
} => {
assert_eq!(vendor, GpuVendor::Amd);
assert!(name.contains("7900"));
assert_eq!(memory_mb, Some(24_000));
}
other => panic!("expected UnsupportedDiscreteGpu, got {:?}", other),
}
}
#[test]
fn supported_acceleration_integrated_falls_to_cpu() {
let hw = hw_with_devices(
GpuBackend::Cpu,
vec![GpuDevice {
vendor: GpuVendor::Intel,
name: "Intel(R) UHD Graphics 630".into(),
memory_mb: None,
}],
);
assert_eq!(hw.supported_acceleration(), SupportedAcceleration::Cpu);
}
#[test]
fn supported_acceleration_no_gpu_at_all() {
let hw = hw_with_devices(GpuBackend::Cpu, vec![]);
assert_eq!(hw.supported_acceleration(), SupportedAcceleration::Cpu);
}
}