use crate::AprenderError;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "hf-hub-integration")]
use dirs;
#[derive(Debug, Clone)]
pub struct ModelCache {
pub cache_dir: PathBuf,
pub auto_download: bool,
pub max_size_bytes: u64,
}
impl Default for ModelCache {
fn default() -> Self {
#[cfg(feature = "hf-hub-integration")]
let cache_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("apr")
.join("models");
#[cfg(not(feature = "hf-hub-integration"))]
let cache_dir = std::env::var("APR_CACHE_DIR")
.map_or_else(|_| PathBuf::from(".apr_cache"), PathBuf::from)
.join("models");
Self {
cache_dir,
auto_download: true,
max_size_bytes: 0, }
}
}
impl ModelCache {
#[must_use]
pub fn new(cache_dir: PathBuf) -> Self {
Self {
cache_dir,
auto_download: true,
max_size_bytes: 0,
}
}
#[must_use]
pub fn model_path(&self, model_name: &str) -> PathBuf {
self.cache_dir.join(model_name)
}
#[must_use]
pub fn has_model(&self, model_name: &str) -> bool {
self.model_path(model_name).exists()
}
pub fn ensure_dir(&self) -> Result<(), AprenderError> {
std::fs::create_dir_all(&self.cache_dir).map_err(|e| {
AprenderError::Io(io::Error::other(format!(
"Failed to create cache directory: {}",
e
)))
})
}
}
#[derive(Debug, Clone)]
pub enum ModelSource {
HuggingFace { repo_id: String, filename: String },
Url(String),
Local(PathBuf),
}
impl ModelSource {
#[must_use]
pub fn parse(source: &str) -> Self {
if source.starts_with("hf://") {
let path = source.strip_prefix("hf://").unwrap_or(source);
let parts: Vec<&str> = path.splitn(3, '/').collect();
if parts.len() >= 2 {
let repo_id = format!("{}/{}", parts[0], parts[1]);
let filename = (*parts.get(2).unwrap_or(&"model.safetensors")).to_string();
return Self::HuggingFace { repo_id, filename };
}
}
if source.starts_with("http://") || source.starts_with("https://") {
return Self::Url(source.to_string());
}
Self::Local(PathBuf::from(source))
}
#[must_use]
pub fn is_remote(&self) -> bool {
matches!(self, Self::HuggingFace { .. } | Self::Url(_))
}
}
#[derive(Debug, Clone)]
pub struct PrerequisiteCheck {
pub name: String,
pub satisfied: bool,
pub version: Option<String>,
pub install_hint: Option<String>,
}
impl PrerequisiteCheck {
#[must_use]
pub fn satisfied(name: &str) -> Self {
Self {
name: name.to_string(),
satisfied: true,
version: None,
install_hint: None,
}
}
#[must_use]
pub fn missing(name: &str, install_hint: &str) -> Self {
Self {
name: name.to_string(),
satisfied: false,
version: None,
install_hint: Some(install_hint.to_string()),
}
}
}
#[must_use]
pub fn check_command(command: &str) -> PrerequisiteCheck {
let exists = std::process::Command::new("which")
.arg(command)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if exists {
PrerequisiteCheck::satisfied(command)
} else {
PrerequisiteCheck::missing(
command,
&format!("Please install {}: check your package manager", command),
)
}
}
pub fn check_prerequisites(required: &[&str]) -> Vec<PrerequisiteCheck> {
required.iter().map(|cmd| check_command(cmd)).collect()
}
pub fn print_prerequisites(checks: &[PrerequisiteCheck]) {
for check in checks {
if check.satisfied {
eprintln!(" ✓ {} found", check.name);
} else {
eprintln!(" ✗ {} missing", check.name);
if let Some(hint) = &check.install_hint {
eprintln!(" → {}", hint);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionMode {
Interactive,
Batch,
}
impl ExecutionMode {
#[must_use]
pub fn detect() -> Self {
if io::stdin().is_terminal() && io::stdout().is_terminal() {
Self::Interactive
} else {
Self::Batch
}
}
#[must_use]
pub fn is_interactive(&self) -> bool {
*self == Self::Interactive
}
#[must_use]
pub fn is_batch(&self) -> bool {
*self == Self::Batch
}
}
#[derive(Debug)]
pub struct AdaptiveOutput {
mode: ExecutionMode,
json_output: bool,
}
impl Default for AdaptiveOutput {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveOutput {
#[must_use]
pub fn new() -> Self {
Self {
mode: ExecutionMode::detect(),
json_output: false,
}
}
#[must_use]
pub fn with_json(mut self) -> Self {
self.json_output = true;
self
}
#[must_use]
pub fn with_mode(mut self, mode: ExecutionMode) -> Self {
self.mode = mode;
self
}
pub fn status(&self, msg: &str) {
if self.mode.is_interactive() && !self.json_output {
eprintln!("{}", msg);
}
}
pub fn progress(&self, current: usize, total: usize, msg: &str) {
if self.mode.is_interactive() && !self.json_output {
eprint!("\r[{}/{}] {}", current, total, msg);
let _ = io::stderr().flush();
}
}
pub fn result(&self, data: &str) {
println!("{}", data);
}
pub fn error(&self, msg: &str) {
eprintln!("Error: {}", msg);
}
}
#[derive(Debug)]
pub struct RecoverableError {
pub message: String,
pub recovery: Option<String>,
pub auto_recoverable: bool,
}
impl RecoverableError {
#[must_use]
pub fn new(message: &str) -> Self {
Self {
message: message.to_string(),
recovery: None,
auto_recoverable: false,
}
}
#[must_use]
pub fn with_recovery(mut self, recovery: &str) -> Self {
self.recovery = Some(recovery.to_string());
self
}
#[must_use]
pub fn auto_recoverable(mut self) -> Self {
self.auto_recoverable = true;
self
}
#[must_use]
pub fn format(&self) -> String {
use std::fmt::Write;
let mut output = format!("Error: {}", self.message);
if let Some(recovery) = &self.recovery {
let _ = write!(output, "\n\nSuggested fix: {}", recovery);
}
output
}
}
pub mod recovery {
use super::{Path, RecoverableError};
#[must_use]
pub fn model_not_found(path: &Path) -> RecoverableError {
RecoverableError::new(&format!("Model file not found: {}", path.display()))
.with_recovery("Run 'apr download <model>' to fetch the model, or check the path")
}
#[must_use]
pub fn checksum_mismatch(expected: &str, actual: &str) -> RecoverableError {
RecoverableError::new(&format!(
"Model checksum mismatch\n Expected: {}\n Actual: {}",
expected, actual
))
.with_recovery("The model file may be corrupted. Delete it and re-download with 'apr download --force <model>'")
.auto_recoverable()
}
#[must_use]
pub fn gpu_not_available() -> RecoverableError {
RecoverableError::new("GPU acceleration requested but not available").with_recovery(
"Falling back to CPU. For GPU support, ensure CUDA/Metal drivers are installed",
)
}
#[must_use]
pub fn out_of_memory(required: usize, available: usize) -> RecoverableError {
RecoverableError::new(&format!(
"Insufficient memory: need {} MB, have {} MB",
required / 1_000_000,
available / 1_000_000
))
.with_recovery("Try a smaller model or enable quantization with '--quantize int4'")
}
}
#[derive(Debug, Clone, Default)]
pub struct PerformanceMetrics {
pub load_time: Duration,
pub time_to_first_token: Duration,
pub tokens_generated: usize,
pub generation_time: Duration,
pub peak_memory: usize,
pub backend: String,
}
mod performance;
pub use performance::*;