1pub mod auth;
2pub mod logging;
3#[cfg(feature = "metrics")]
4pub mod metrics;
5pub mod model_cache;
6pub mod model_manager;
7pub mod queue;
8pub mod rate_limit;
9pub mod request_id;
10pub mod routes;
11pub mod state;
12
13#[cfg(all(test, feature = "metrics"))]
14mod metrics_test;
15#[cfg(test)]
16mod routes_test;
17
18use anyhow::Result;
19use axum::{extract::DefaultBodyLimit, middleware};
20use mold_core::{Config, ModelPaths};
21use std::net::SocketAddr;
22use std::path::PathBuf;
23use tokio::net::TcpListener;
24use tower_http::cors::CorsLayer;
25use tower_http::trace::TraceLayer;
26use tracing::info;
27
28use state::QueueHandle;
29
30const MAX_REQUEST_BODY_BYTES: usize = 64 * 1024 * 1024;
31
32pub async fn run_server(bind: &str, port: u16, models_dir: PathBuf) -> Result<()> {
33 Config::install_runtime_models_dir_override(models_dir.clone());
34
35 let mut config = Config::load_or_default();
36 config.models_dir = models_dir.to_string_lossy().into_owned();
37 let model_name = config.resolved_default_model();
38
39 let (job_tx, job_rx) = tokio::sync::mpsc::channel(16);
41 let queue_handle = QueueHandle::new(job_tx);
42
43 let state = match ModelPaths::resolve(&model_name, &config) {
44 Some(paths) => {
45 info!(model = %model_name, "configured model");
46 info!(transformer = %paths.transformer.display());
47 info!(vae = %paths.vae.display());
48 if let Some(spatial_upscaler) = &paths.spatial_upscaler {
49 info!(spatial_upscaler = %spatial_upscaler.display());
50 }
51 if let Some(t5) = &paths.t5_encoder {
52 info!(t5 = %t5.display());
53 }
54 if let Some(clip) = &paths.clip_encoder {
55 info!(clip = %clip.display());
56 }
57 if let Some(t5_tok) = &paths.t5_tokenizer {
58 info!(t5_tok = %t5_tok.display());
59 }
60 if let Some(clip_tok) = &paths.clip_tokenizer {
61 info!(clip_tok = %clip_tok.display());
62 }
63 if let Some(clip2) = &paths.clip_encoder_2 {
64 info!(clip2 = %clip2.display());
65 }
66 if let Some(clip2_tok) = &paths.clip_tokenizer_2 {
67 info!(clip2_tok = %clip2_tok.display());
68 }
69 for (i, te) in paths.text_encoder_files.iter().enumerate() {
70 info!(text_encoder_shard = i, path = %te.display());
71 }
72 if let Some(text_tok) = &paths.text_tokenizer {
73 info!(text_tok = %text_tok.display());
74 }
75
76 let offload = std::env::var("MOLD_OFFLOAD").is_ok_and(|v| v == "1");
77 let shared_pool = std::sync::Arc::new(std::sync::Mutex::new(
79 mold_inference::shared_pool::SharedPool::new(),
80 ));
81 let engine = mold_inference::create_engine_with_pool(
82 model_name,
83 paths,
84 &config,
85 mold_inference::LoadStrategy::Eager,
86 offload,
87 Some(shared_pool.clone()),
88 )?;
89 let mut state = state::AppState::new(engine, config, queue_handle);
90 state.shared_pool = shared_pool;
91 state
92 }
93 None => {
94 info!("no default model configured — models will be pulled on first request");
95 state::AppState::empty(config, queue_handle)
96 }
97 };
98
99 let worker_state = state.clone();
101 tokio::spawn(queue::run_queue_worker(job_rx, worker_state));
102
103 {
105 let config = state.config.read().await;
106 if config.is_output_disabled() {
107 tracing::warn!(
108 "image output is disabled (output_dir is empty) — \
109 generated images will not be saved and the TUI gallery will be empty"
110 );
111 } else {
112 let output_dir = config.effective_output_dir();
113 let _ = std::fs::create_dir_all(&output_dir);
114 info!(output_dir = %output_dir.display(), "gallery output directory");
115 routes::spawn_thumbnail_warmup(&config);
116 }
117 }
118
119 let auth_state = auth::load_api_keys()?;
121 let rl_config = rate_limit::load_rate_limit_config()?;
122
123 let cors = build_cors_layer()?;
124
125 #[cfg(feature = "metrics")]
128 let prometheus_handle = metrics::install_recorder();
129
130 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
136 *state.shutdown_tx.lock().await = Some(shutdown_tx);
137
138 #[cfg(unix)]
139 {
140 let sigterm_state = state.clone();
141 tokio::spawn(async move {
142 if let Ok(mut sig) =
143 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
144 {
145 sig.recv().await;
146 tracing::info!("received SIGTERM, initiating graceful shutdown");
147 if let Some(tx) = sigterm_state.shutdown_tx.lock().await.take() {
148 let _ = tx.send(());
149 }
150 }
151 });
152 }
153
154 #[cfg(feature = "metrics")]
156 let server_start_time = state.start_time;
157
158 #[allow(unused_mut)]
161 let mut app = routes::create_router(state)
162 .layer(DefaultBodyLimit::max(MAX_REQUEST_BODY_BYTES))
163 .layer(middleware::from_fn(rate_limit::rate_limit_middleware))
164 .layer(middleware::from_fn_with_state(
165 rl_config,
166 rate_limit::inject_rate_limit_state,
167 ))
168 .layer(middleware::from_fn(auth::require_api_key))
169 .layer(middleware::from_fn_with_state(
170 auth_state,
171 auth::inject_auth_state,
172 ));
173
174 #[cfg(feature = "metrics")]
177 {
178 app = app.layer(middleware::from_fn(metrics::http_metrics_middleware));
179 }
180
181 #[cfg(feature = "metrics")]
182 {
183 let metrics_state = metrics::MetricsState {
184 handle: prometheus_handle,
185 start_time: server_start_time,
186 };
187 app = app.route(
188 "/metrics",
189 axum::routing::get(metrics::metrics_endpoint).with_state(metrics_state),
190 );
191 }
192
193 let app = app
194 .layer(middleware::from_fn(request_id::request_id_middleware))
195 .layer(TraceLayer::new_for_http())
196 .layer(cors);
197
198 let addr: SocketAddr = format!("{bind}:{port}").parse()?;
199 let version = mold_core::build_info::version_string();
200 info!(%addr, %version, "starting mold server");
201
202 let listener = TcpListener::bind(addr).await?;
203 axum::serve(
204 listener,
205 app.into_make_service_with_connect_info::<SocketAddr>(),
206 )
207 .with_graceful_shutdown(async {
208 let _ = shutdown_rx.await;
209 tracing::info!("shutting down");
210 })
211 .await?;
212
213 Ok(())
214}
215
216fn build_cors_layer() -> Result<CorsLayer> {
217 let cors = match std::env::var("MOLD_CORS_ORIGIN") {
218 Ok(origin) if !origin.is_empty() => {
219 let origin = origin
220 .parse::<axum::http::HeaderValue>()
221 .map_err(|_| anyhow::anyhow!("invalid MOLD_CORS_ORIGIN value: {origin}"))?;
222 CorsLayer::new()
223 .allow_origin(origin)
224 .allow_methods([
225 axum::http::Method::GET,
226 axum::http::Method::POST,
227 axum::http::Method::DELETE,
228 ])
229 .allow_headers(tower_http::cors::Any)
230 .expose_headers([
231 axum::http::header::HeaderName::from_static("x-mold-seed-used"),
232 axum::http::header::HeaderName::from_static("x-request-id"),
233 axum::http::header::HeaderName::from_static("retry-after"),
234 ])
235 }
236 _ => CorsLayer::permissive(),
237 };
238 Ok(cors)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::build_cors_layer;
244 use std::sync::Mutex;
245
246 static ENV_LOCK: Mutex<()> = Mutex::new(());
247
248 #[test]
249 fn invalid_cors_origin_returns_error() {
250 let _lock = ENV_LOCK.lock().unwrap();
251 std::env::set_var("MOLD_CORS_ORIGIN", "\nnot-a-header");
252 let result = build_cors_layer();
253 std::env::remove_var("MOLD_CORS_ORIGIN");
254 assert!(result.is_err());
255 }
256
257 #[test]
258 fn valid_cors_origin_builds_layer() {
259 let _lock = ENV_LOCK.lock().unwrap();
260 std::env::set_var("MOLD_CORS_ORIGIN", "https://example.com");
261 let result = build_cors_layer();
262 std::env::remove_var("MOLD_CORS_ORIGIN");
263 assert!(result.is_ok());
264 }
265}