use std::sync::Arc;
use mold_core::{build_model_catalog, ModelInfoExtended, ModelPaths};
use crate::model_cache::ModelResidency;
use crate::{routes::ApiError, state::AppState};
pub(crate) type EngineProgressCallback = Arc<dyn Fn(mold_inference::ProgressEvent) + Send + Sync>;
pub(crate) type DownloadProgressCallback =
Arc<dyn Fn(mold_core::download::DownloadProgressEvent) + Send + Sync>;
pub(crate) enum PullStatus {
AlreadyAvailable,
Pulled,
}
pub(crate) async fn refresh_config(state: &AppState) -> mold_core::Config {
let fresh = {
let current = state.config.read().await;
current.reload_from_disk_preserving_runtime()
};
let mut config = state.config.write().await;
*config = fresh.clone();
fresh
}
pub(crate) async fn list_models(state: &AppState) -> Vec<ModelInfoExtended> {
let snapshot = state.engine_snapshot.read().await.clone();
let config = refresh_config(state).await;
build_model_catalog(&config, snapshot.model_name.as_deref(), snapshot.is_loaded)
}
pub(crate) async fn check_model_available(
state: &AppState,
model_name: &str,
) -> Result<Option<ModelPaths>, ApiError> {
{
let cache = state.model_cache.lock().await;
if cache.contains(model_name) {
return Ok(None);
}
}
{
let snapshot = state.engine_snapshot.read().await;
if snapshot.model_name.as_deref() == Some(model_name) {
return Ok(None);
}
}
let paths = {
let config = state.config.read().await;
ModelPaths::resolve(model_name, &config)
};
if let Some(paths) = paths {
return Ok(Some(paths));
}
{
let current = state.config.read().await.clone();
let fresh_config = current.reload_from_disk_preserving_runtime();
if let Some(paths) = ModelPaths::resolve(model_name, &fresh_config) {
let mut config = state.config.write().await;
*config = fresh_config;
return Ok(Some(paths));
}
}
if mold_core::manifest::find_manifest(model_name).is_some() {
return Err(ApiError::not_found(format!(
"model '{model_name}' is not downloaded. Run: mold pull {model_name}"
)));
}
Err(ApiError::unknown_model(format!(
"unknown model '{model_name}'. Run 'mold list' to see available models."
)))
}
pub(crate) async fn ensure_model_ready(
state: &AppState,
model_name: &str,
progress: Option<EngineProgressCallback>,
) -> Result<(), ApiError> {
let _guard = state.model_load_lock.lock().await;
{
let mut cache = state.model_cache.lock().await;
if let Some(entry) = cache.get_mut(model_name) {
if entry.residency == ModelResidency::Gpu {
if let Some(callback) = progress.clone() {
entry.engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
entry.engine.clear_on_progress();
}
return Ok(());
}
if let Some(active_name) = cache.unload_active() {
tracing::info!(
from = %active_name,
to = %model_name,
"unloaded active model to reload cached model"
);
mold_inference::reclaim_gpu_memory();
}
let mut engine = cache.remove(model_name).unwrap();
drop(cache);
if let Some(callback) = progress.clone() {
engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
engine.clear_on_progress();
}
let model_log = model_name.to_string();
let result = tokio::task::spawn_blocking(move || {
tracing::info!(model = %model_log, "reloading cached engine...");
if let Err(e) = engine.load() {
tracing::error!("model reload failed: {e:#}");
return Err((
ApiError::internal(format!("model reload error: {e}")),
engine,
));
}
Ok(engine)
})
.await
.map_err(|e| ApiError::internal(format!("model reload task failed: {e}")))?;
match result {
Ok(loaded_engine) => {
let vram = mold_inference::device::vram_used_estimate();
let mut cache = state.model_cache.lock().await;
cache.insert(loaded_engine, vram);
update_snapshot(state, &cache).await;
}
Err((api_err, unloaded_engine)) => {
let mut cache = state.model_cache.lock().await;
cache.insert(unloaded_engine, 0);
return Err(api_err);
}
}
return Ok(());
}
}
match check_model_available(state, model_name).await? {
Some(paths) => create_and_load_engine(state, model_name, paths, progress).await,
None => Ok(()),
}
}
pub(crate) async fn pull_model(
state: &AppState,
model: &str,
progress: Option<DownloadProgressCallback>,
) -> Result<PullStatus, ApiError> {
if mold_core::manifest::find_manifest(&mold_core::manifest::resolve_model_name(model)).is_none()
{
return Err(ApiError::unknown_model(format!(
"unknown model '{model}'. Run 'mold list' to see available models."
)));
}
let _guard = state.pull_lock.lock().await;
{
let config = refresh_config(state).await;
if ModelPaths::resolve(model, &config).is_some() {
return Ok(PullStatus::AlreadyAvailable);
}
}
tracing::info!(model = %model, "pulling model via API");
let opts = mold_core::download::PullOptions::default();
let new_config = match progress {
Some(callback) => {
mold_core::download::pull_and_configure_with_callback(model, callback, &opts)
.await
.map(|(config, _)| config)
}
None => mold_core::download::pull_and_configure(model, &opts)
.await
.map(|(config, _)| config),
}
.map_err(|e| {
tracing::error!("pull failed for {}: {e}", model);
ApiError::internal(format!("failed to pull model '{}': {e}", model))
})?;
{
let mut config = state.config.write().await;
*config = new_config;
}
tracing::info!(model = %model, "pull complete");
Ok(PullStatus::Pulled)
}
pub(crate) async fn unload_model(state: &AppState) -> String {
let mut cache = state.model_cache.lock().await;
match cache.unload_active() {
Some(name) => {
update_snapshot(state, &cache).await;
drop(cache);
mold_inference::reclaim_gpu_memory();
tracing::info!(model = %name, "model unloaded via API");
format!("unloaded {name}")
}
None => "no model loaded".to_string(),
}
}
async fn create_and_load_engine(
state: &AppState,
model_name: &str,
paths: ModelPaths,
progress: Option<EngineProgressCallback>,
) -> Result<(), ApiError> {
let had_active = {
let mut cache = state.model_cache.lock().await;
let result = cache.unload_active();
if let Some(ref name) = result {
tracing::info!(
from = %name,
to = %model_name,
"unloading active model before loading new one"
);
}
update_snapshot(state, &cache).await;
result.is_some()
};
if had_active {
mold_inference::reclaim_gpu_memory();
}
let config = state.config.read().await;
let offload = std::env::var("MOLD_OFFLOAD").is_ok_and(|v| v == "1");
let mut new_engine = mold_inference::create_engine(
model_name.to_string(),
paths,
&config,
mold_inference::LoadStrategy::Eager,
offload,
)
.map_err(|e| ApiError::internal(format!("failed to create engine for '{model_name}': {e}")))?;
drop(config);
if let Some(callback) = progress {
new_engine.set_on_progress(Box::new(move |event| {
callback(event);
}));
} else {
new_engine.clear_on_progress();
}
let model_log = model_name.to_string();
new_engine = tokio::task::spawn_blocking(move || {
tracing::info!(model = %model_log, "loading model...");
new_engine.load().map_err(|e| {
tracing::error!("model load failed: {e:#}");
ApiError::internal(format!("model load error: {e}"))
})?;
Ok::<_, ApiError>(new_engine)
})
.await
.map_err(|e| ApiError::internal(format!("model load task failed: {e}")))??;
let vram = mold_inference::device::vram_used_estimate();
let mut cache = state.model_cache.lock().await;
let _evicted = cache.insert(new_engine, vram);
update_snapshot(state, &cache).await;
drop(cache);
Ok(())
}
async fn update_snapshot(state: &AppState, cache: &crate::model_cache::ModelCache) {
let mut snapshot = state.engine_snapshot.write().await;
snapshot.model_name = cache.active_model().map(|s| s.to_string());
snapshot.is_loaded = cache.active_model().is_some();
snapshot.cached_models = cache.cached_model_names();
}