1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::OnceLock;
6
7use crate::expand::ExpandSettings;
8use crate::manifest::resolve_model_name;
9use crate::types::Scheduler;
10
11static RUNTIME_MODELS_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
12
13const BOOTSTRAP_ONLY_BANNER: &str = "\
16# mold config — bootstrap-only surface.
17#
18# User preferences (expand.*, scheduler.*, generate.default_*, per-model generation
19# defaults, LoRA, and generation scheduler) live in SQLite at <MOLD_HOME>/mold.db.
20# Edit them via:
21# mold config set <key> <value>
22# mold config get <key>
23# mold config reset <key> # drop the DB row, fall back to this file
24#
25# This file retains only identifiers, paths, ports, credentials, logging,
26# and per-model file-path entries. Adding generation defaults here is
27# silently overridden by the DB on load.
28
29";
30
31const STRIPPED_GLOBAL_KEYS: &[&str] = &[
34 "default_width",
35 "default_height",
36 "default_steps",
37 "embed_metadata",
38 "default_negative_prompt",
39 "t5_variant",
40 "umt5_variant",
41 "qwen3_variant",
42 "expand",
43 "scheduler",
44];
45
46const STRIPPED_MODEL_KEYS: &[&str] = &[
47 "default_steps",
48 "default_guidance",
49 "default_width",
50 "default_height",
51 "scheduler",
52 "negative_prompt",
53 "lora",
54 "lora_scale",
55 "default_frames",
56 "default_fps",
57];
58
59fn strip_user_pref_fields(doc: &mut toml::Value) {
60 let Some(table) = doc.as_table_mut() else {
61 return;
62 };
63 for key in STRIPPED_GLOBAL_KEYS {
64 table.remove(*key);
65 }
66 if let Some(toml::Value::Table(models)) = table.get_mut("models") {
67 for (_, mc) in models.iter_mut() {
68 if let Some(mc_table) = mc.as_table_mut() {
69 for key in STRIPPED_MODEL_KEYS {
70 mc_table.remove(*key);
71 }
72 }
73 }
74 }
75}
76
77pub type ConfigPostLoadHook = fn(&mut Config);
87static POST_LOAD_HOOK: OnceLock<ConfigPostLoadHook> = OnceLock::new();
88
89pub fn install_post_load_hook(hook: ConfigPostLoadHook) {
92 let _ = POST_LOAD_HOOK.set(hook);
93}
94
95pub type ReadLastModelHook = fn() -> Option<String>;
99static READ_LAST_MODEL_HOOK: OnceLock<ReadLastModelHook> = OnceLock::new();
100
101pub fn install_read_last_model_hook(hook: ReadLastModelHook) {
103 let _ = READ_LAST_MODEL_HOOK.set(hook);
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum DefaultModelSource {
109 EnvVar,
111 ConfigCustomEntry,
113 Config,
115 LastUsed,
117 OnlyDownloaded,
119 ConfigDefault,
121}
122
123#[derive(Debug, Clone)]
125pub struct DefaultModelResolution {
126 pub model: String,
127 pub source: DefaultModelSource,
128}
129
130#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
132pub struct ModelConfig {
133 pub transformer: Option<String>,
135 pub transformer_shards: Option<Vec<String>>,
137 pub low_noise_transformer: Option<String>,
140 pub vae: Option<String>,
141 pub spatial_upscaler: Option<String>,
143 pub temporal_upscaler: Option<String>,
145 pub distilled_lora: Option<String>,
147 pub low_noise_distilled_lora: Option<String>,
149 pub t5_encoder: Option<String>,
150 pub clip_encoder: Option<String>,
151 pub t5_tokenizer: Option<String>,
152 pub clip_tokenizer: Option<String>,
153 pub clip_encoder_2: Option<String>,
155 pub clip_tokenizer_2: Option<String>,
157 pub text_encoder_files: Option<Vec<String>>,
159 pub text_tokenizer: Option<String>,
161 pub decoder: Option<String>,
163
164 pub default_steps: Option<u32>,
167 pub default_guidance: Option<f64>,
169 pub default_width: Option<u32>,
171 pub default_height: Option<u32>,
173 pub is_schnell: Option<bool>,
176 pub is_turbo: Option<bool>,
179 pub scheduler: Option<Scheduler>,
181 pub negative_prompt: Option<String>,
183 pub lora: Option<String>,
185 pub lora_scale: Option<f64>,
187 pub default_frames: Option<u32>,
189 pub default_fps: Option<u32>,
191
192 pub description: Option<String>,
194 pub family: Option<String>,
195
196 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub placement: Option<crate::types::DevicePlacement>,
200}
201
202impl ModelConfig {
203 pub fn all_file_paths(&self) -> Vec<String> {
206 let mut paths = Vec::new();
207 let singles = [
208 &self.transformer,
209 &self.low_noise_transformer,
210 &self.vae,
211 &self.spatial_upscaler,
212 &self.temporal_upscaler,
213 &self.distilled_lora,
214 &self.low_noise_distilled_lora,
215 &self.t5_encoder,
216 &self.clip_encoder,
217 &self.t5_tokenizer,
218 &self.clip_tokenizer,
219 &self.clip_encoder_2,
220 &self.clip_tokenizer_2,
221 &self.text_tokenizer,
222 &self.decoder,
223 &self.lora,
224 ];
225 for p in singles.into_iter().flatten() {
226 paths.push(p.clone());
227 }
228 if let Some(ref shards) = self.transformer_shards {
229 paths.extend(shards.iter().cloned());
230 }
231 if let Some(ref files) = self.text_encoder_files {
232 paths.extend(files.iter().cloned());
233 }
234 paths
235 }
236
237 pub fn disk_usage(&self) -> (u64, f64) {
242 let total: u64 = self
243 .all_file_paths()
244 .iter()
245 .filter_map(|p| std::fs::metadata(p).ok())
246 .map(|m| m.len())
247 .sum();
248 (total, total as f64 / 1_073_741_824.0)
249 }
250
251 pub fn effective_steps(&self, global_cfg: &Config) -> u32 {
253 self.default_steps.unwrap_or(global_cfg.default_steps)
254 }
255
256 pub fn effective_guidance(&self) -> f64 {
258 self.default_guidance.unwrap_or(3.5)
259 }
260
261 pub fn effective_width(&self, global_cfg: &Config) -> u32 {
263 self.default_width.unwrap_or(global_cfg.default_width)
264 }
265
266 pub fn effective_height(&self, global_cfg: &Config) -> u32 {
268 self.default_height.unwrap_or(global_cfg.default_height)
269 }
270
271 pub fn effective_negative_prompt(&self, global_cfg: &Config) -> Option<String> {
273 self.negative_prompt
274 .clone()
275 .or_else(|| global_cfg.default_negative_prompt.clone())
276 }
277
278 pub fn effective_lora(&self) -> Option<(String, f64)> {
280 self.lora
281 .as_ref()
282 .map(|path| (path.clone(), self.lora_scale.unwrap_or(1.0)))
283 }
284
285 pub fn effective_frames(&self) -> Option<u32> {
287 self.default_frames
288 }
289
290 pub fn effective_fps(&self) -> Option<u32> {
292 self.default_fps
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct ModelPaths {
303 pub transformer: PathBuf,
304 pub transformer_shards: Vec<PathBuf>,
306 pub low_noise_transformer: Option<PathBuf>,
313 pub vae: PathBuf,
314 pub spatial_upscaler: Option<PathBuf>,
315 pub temporal_upscaler: Option<PathBuf>,
316 pub distilled_lora: Option<PathBuf>,
317 pub low_noise_distilled_lora: Option<PathBuf>,
321 pub t5_encoder: Option<PathBuf>,
322 pub clip_encoder: Option<PathBuf>,
323 pub t5_tokenizer: Option<PathBuf>,
324 pub clip_tokenizer: Option<PathBuf>,
325 pub clip_encoder_2: Option<PathBuf>,
327 pub clip_tokenizer_2: Option<PathBuf>,
329 pub text_encoder_files: Vec<PathBuf>,
331 pub text_tokenizer: Option<PathBuf>,
333 pub decoder: Option<PathBuf>,
335}
336
337impl ModelPaths {
338 const FROZEN_MODEL_PREFIX: &'static str = "\0mold-frozen-chain:";
339
340 pub fn all_file_paths(&self) -> Vec<&Path> {
343 let mut paths = vec![self.transformer.as_path(), self.vae.as_path()];
344 paths.extend(self.transformer_shards.iter().map(PathBuf::as_path));
345 paths.extend(self.low_noise_transformer.as_deref());
346 paths.extend(self.spatial_upscaler.as_deref());
347 paths.extend(self.temporal_upscaler.as_deref());
348 paths.extend(self.distilled_lora.as_deref());
349 paths.extend(self.low_noise_distilled_lora.as_deref());
350 paths.extend(self.t5_encoder.as_deref());
351 paths.extend(self.clip_encoder.as_deref());
352 paths.extend(self.t5_tokenizer.as_deref());
353 paths.extend(self.clip_tokenizer.as_deref());
354 paths.extend(self.clip_encoder_2.as_deref());
355 paths.extend(self.clip_tokenizer_2.as_deref());
356 paths.extend(self.text_encoder_files.iter().map(PathBuf::as_path));
357 paths.extend(self.text_tokenizer.as_deref());
358 paths.extend(self.decoder.as_deref());
359 paths
360 }
361
362 pub fn resolve(model_name: &str, config: &Config) -> Option<Self> {
366 if let Some(model_cfg) = config
367 .models
368 .get(&format!("{}{model_name}", Self::FROZEN_MODEL_PREFIX))
369 {
370 return Self::resolve_from_model_config_exact(model_cfg);
371 }
372 if let Some(model_cfg) = config.discovered_manifest_model_config(model_name) {
373 return Self::resolve_from_model_config(Some(&model_cfg));
374 }
375
376 if crate::manifest::find_manifest(model_name).is_some() && config.has_models_dir_override()
377 {
378 return Self::resolve_from_model_config(None);
379 }
380
381 let model_cfg = config.lookup_model_config(model_name);
382 Self::resolve_from_model_config(model_cfg.as_ref())
383 }
384
385 pub fn resolve_from_model_config_exact(model_cfg: &ModelConfig) -> Option<Self> {
391 let path = |value: Option<&str>| value.map(PathBuf::from);
392 let vae = match model_cfg.vae.as_deref().filter(|path| !path.is_empty()) {
393 Some(path) => PathBuf::from(path),
394 None if model_cfg.family.as_deref().is_some_and(|family| {
395 family == "ltx2"
396 || crate::manifest::UTILITY_FAMILIES.contains(&family)
397 || crate::manifest::UPSCALER_FAMILIES.contains(&family)
398 }) =>
399 {
400 PathBuf::new()
401 }
402 None => return None,
403 };
404 Some(Self {
405 transformer: PathBuf::from(model_cfg.transformer.as_deref()?),
406 transformer_shards: model_cfg
407 .transformer_shards
408 .as_ref()
409 .map(|paths| paths.iter().map(PathBuf::from).collect())
410 .unwrap_or_default(),
411 low_noise_transformer: path(model_cfg.low_noise_transformer.as_deref()),
412 vae,
413 spatial_upscaler: path(model_cfg.spatial_upscaler.as_deref()),
414 temporal_upscaler: path(model_cfg.temporal_upscaler.as_deref()),
415 distilled_lora: path(model_cfg.distilled_lora.as_deref()),
416 low_noise_distilled_lora: path(model_cfg.low_noise_distilled_lora.as_deref()),
417 t5_encoder: path(model_cfg.t5_encoder.as_deref()),
418 clip_encoder: path(model_cfg.clip_encoder.as_deref()),
419 t5_tokenizer: path(model_cfg.t5_tokenizer.as_deref()),
420 clip_tokenizer: path(model_cfg.clip_tokenizer.as_deref()),
421 clip_encoder_2: path(model_cfg.clip_encoder_2.as_deref()),
422 clip_tokenizer_2: path(model_cfg.clip_tokenizer_2.as_deref()),
423 text_encoder_files: model_cfg
424 .text_encoder_files
425 .as_ref()
426 .map(|paths| paths.iter().map(PathBuf::from).collect())
427 .unwrap_or_default(),
428 text_tokenizer: path(model_cfg.text_tokenizer.as_deref()),
429 decoder: path(model_cfg.decoder.as_deref()),
430 })
431 }
432
433 fn resolve_from_model_config(model_cfg: Option<&ModelConfig>) -> Option<Self> {
434 let transformer = Self::resolve_path(
435 model_cfg.and_then(|m| m.transformer.as_deref()),
436 "MOLD_TRANSFORMER_PATH",
437 )?;
438 let transformer_shards = model_cfg
439 .and_then(|m| m.transformer_shards.as_ref())
440 .map(|shards| shards.iter().map(PathBuf::from).collect())
441 .unwrap_or_default();
442 let low_noise_transformer = Self::resolve_path(
443 model_cfg.and_then(|m| m.low_noise_transformer.as_deref()),
444 "MOLD_LOW_NOISE_TRANSFORMER_PATH",
445 );
446 let vae = Self::resolve_path(model_cfg.and_then(|m| m.vae.as_deref()), "MOLD_VAE_PATH")?;
447 let spatial_upscaler = Self::resolve_path(
448 model_cfg.and_then(|m| m.spatial_upscaler.as_deref()),
449 "MOLD_SPATIAL_UPSCALER_PATH",
450 );
451 let temporal_upscaler = Self::resolve_path(
452 model_cfg.and_then(|m| m.temporal_upscaler.as_deref()),
453 "MOLD_TEMPORAL_UPSCALER_PATH",
454 );
455 let distilled_lora = Self::resolve_path(
456 model_cfg.and_then(|m| m.distilled_lora.as_deref()),
457 "MOLD_DISTILLED_LORA_PATH",
458 );
459 let low_noise_distilled_lora = Self::resolve_path(
460 model_cfg.and_then(|m| m.low_noise_distilled_lora.as_deref()),
461 "MOLD_LOW_NOISE_DISTILLED_LORA_PATH",
462 );
463 let t5_encoder = Self::resolve_path(
464 model_cfg.and_then(|m| m.t5_encoder.as_deref()),
465 "MOLD_T5_PATH",
466 );
467 let clip_encoder = Self::resolve_path(
468 model_cfg.and_then(|m| m.clip_encoder.as_deref()),
469 "MOLD_CLIP_PATH",
470 );
471 let t5_tokenizer = Self::resolve_path(
472 model_cfg.and_then(|m| m.t5_tokenizer.as_deref()),
473 "MOLD_T5_TOKENIZER_PATH",
474 );
475 let clip_tokenizer = Self::resolve_path(
476 model_cfg.and_then(|m| m.clip_tokenizer.as_deref()),
477 "MOLD_CLIP_TOKENIZER_PATH",
478 );
479 let clip_encoder_2 = Self::resolve_path(
480 model_cfg.and_then(|m| m.clip_encoder_2.as_deref()),
481 "MOLD_CLIP2_PATH",
482 );
483 let clip_tokenizer_2 = Self::resolve_path(
484 model_cfg.and_then(|m| m.clip_tokenizer_2.as_deref()),
485 "MOLD_CLIP2_TOKENIZER_PATH",
486 );
487 let text_encoder_files = model_cfg
488 .and_then(|m| m.text_encoder_files.as_ref())
489 .map(|files| files.iter().map(PathBuf::from).collect())
490 .unwrap_or_default();
491 let text_tokenizer = Self::resolve_path(
492 model_cfg.and_then(|m| m.text_tokenizer.as_deref()),
493 "MOLD_TEXT_TOKENIZER_PATH",
494 );
495 let decoder = Self::resolve_path(
496 model_cfg.and_then(|m| m.decoder.as_deref()),
497 "MOLD_DECODER_PATH",
498 );
499
500 Some(Self {
501 transformer,
502 transformer_shards,
503 low_noise_transformer,
504 vae,
505 spatial_upscaler,
506 temporal_upscaler,
507 distilled_lora,
508 low_noise_distilled_lora,
509 t5_encoder,
510 clip_encoder,
511 t5_tokenizer,
512 clip_tokenizer,
513 clip_encoder_2,
514 clip_tokenizer_2,
515 text_encoder_files,
516 text_tokenizer,
517 decoder,
518 })
519 }
520
521 fn resolve_path(config_val: Option<&str>, env_var: &str) -> Option<PathBuf> {
522 if let Ok(path) = std::env::var(env_var) {
523 return Some(PathBuf::from(path));
524 }
525 if let Some(path) = config_val {
526 return Some(PathBuf::from(path));
527 }
528 None
529 }
530}
531
532const CURRENT_CONFIG_VERSION: u32 = 1;
534
535#[derive(Debug, Clone, Deserialize, Serialize)]
536pub struct Config {
537 #[serde(default)]
540 pub config_version: u32,
541
542 #[serde(default = "default_model")]
543 pub default_model: String,
544
545 #[serde(default = "default_models_dir")]
546 pub models_dir: String,
547
548 #[serde(default = "default_port")]
549 pub server_port: u16,
550
551 #[serde(default = "default_dimension")]
552 pub default_width: u32,
553
554 #[serde(default = "default_dimension")]
555 pub default_height: u32,
556
557 #[serde(default = "default_steps")]
558 pub default_steps: u32,
559
560 #[serde(default = "default_embed_metadata")]
561 pub embed_metadata: bool,
562
563 #[serde(default)]
567 pub t5_variant: Option<String>,
568 pub umt5_variant: Option<String>,
572
573 #[serde(default)]
576 pub qwen3_variant: Option<String>,
577
578 #[serde(default)]
582 pub output_dir: Option<String>,
583
584 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub media_roots: Option<Vec<String>>,
588
589 #[serde(default)]
592 pub default_negative_prompt: Option<String>,
593
594 #[serde(default)]
596 pub expand: ExpandSettings,
597
598 #[serde(default)]
601 pub scheduler: SchedulerSettings,
602
603 #[serde(default)]
605 pub logging: LoggingConfig,
606
607 #[serde(default)]
609 pub runpod: crate::runpod::RunPodSettings,
610
611 #[serde(default)]
613 pub lambda: crate::lambda::LambdaSettings,
614
615 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub gpus: Option<crate::types::GpuSelection>,
621
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub queue_size: Option<usize>,
625
626 #[serde(default)]
628 pub models: HashMap<String, ModelConfig>,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize)]
633pub struct LoggingConfig {
634 #[serde(default = "default_log_level")]
636 pub level: String,
637
638 #[serde(default)]
640 pub file: bool,
641
642 #[serde(default)]
644 pub dir: Option<String>,
645
646 #[serde(default = "default_log_max_days")]
648 pub max_days: u32,
649}
650
651pub const SCHEDULER_TIMING_MAX_MS: u32 = 30_000;
652
653#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
654pub struct SchedulerSettings {
655 #[serde(default = "default_replan_debounce_ms")]
656 pub replan_debounce_ms: u32,
657 #[serde(default = "default_replan_max_delay_ms")]
658 pub replan_max_delay_ms: u32,
659 #[serde(default = "default_warm_wait_max_ms")]
660 pub warm_wait_max_ms: u32,
661}
662
663impl<'de> Deserialize<'de> for SchedulerSettings {
664 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
665 where
666 D: serde::Deserializer<'de>,
667 {
668 #[derive(Deserialize)]
669 struct Wire {
670 #[serde(default = "default_replan_debounce_ms")]
671 replan_debounce_ms: u32,
672 #[serde(default = "default_replan_max_delay_ms")]
673 replan_max_delay_ms: u32,
674 #[serde(default = "default_warm_wait_max_ms")]
675 warm_wait_max_ms: u32,
676 }
677
678 let wire = Wire::deserialize(deserializer)?;
679 SchedulerSettings {
680 replan_debounce_ms: wire.replan_debounce_ms,
681 replan_max_delay_ms: wire.replan_max_delay_ms,
682 warm_wait_max_ms: wire.warm_wait_max_ms,
683 }
684 .validate()
685 .map_err(serde::de::Error::custom)
686 }
687}
688
689const fn default_replan_debounce_ms() -> u32 {
690 2_000
691}
692
693const fn default_replan_max_delay_ms() -> u32 {
694 5_000
695}
696
697const fn default_warm_wait_max_ms() -> u32 {
698 2_000
699}
700
701impl SchedulerSettings {
702 pub fn validate(self) -> anyhow::Result<Self> {
703 for (key, value) in [
704 ("scheduler.replan_debounce_ms", self.replan_debounce_ms),
705 ("scheduler.replan_max_delay_ms", self.replan_max_delay_ms),
706 ("scheduler.warm_wait_max_ms", self.warm_wait_max_ms),
707 ] {
708 anyhow::ensure!(
709 value <= SCHEDULER_TIMING_MAX_MS,
710 "{key} must be between 0 and {SCHEDULER_TIMING_MAX_MS}"
711 );
712 }
713 anyhow::ensure!(
714 self.replan_max_delay_ms >= self.replan_debounce_ms,
715 "scheduler.replan_max_delay_ms must be greater than or equal to \
716 scheduler.replan_debounce_ms"
717 );
718 Ok(self)
719 }
720}
721
722impl Default for SchedulerSettings {
723 fn default() -> Self {
724 Self {
725 replan_debounce_ms: default_replan_debounce_ms(),
726 replan_max_delay_ms: default_replan_max_delay_ms(),
727 warm_wait_max_ms: default_warm_wait_max_ms(),
728 }
729 }
730}
731
732fn default_log_level() -> String {
733 "info".to_string()
734}
735fn default_log_max_days() -> u32 {
736 7
737}
738
739impl Default for LoggingConfig {
740 fn default() -> Self {
741 Self {
742 level: default_log_level(),
743 file: false,
744 dir: None,
745 max_days: default_log_max_days(),
746 }
747 }
748}
749
750fn default_model() -> String {
751 "flux2-klein:q8".to_string()
752}
753
754fn default_models_dir() -> String {
755 if let Ok(home) = std::env::var("MOLD_HOME") {
756 format!("{home}/models")
757 } else if let Some(home) = Config::saved_mold_dir() {
758 home.join("models").to_string_lossy().into_owned()
759 } else {
760 "~/.mold/models".to_string()
761 }
762}
763
764fn default_port() -> u16 {
765 7680
766}
767
768fn default_dimension() -> u32 {
769 768
770}
771
772fn default_steps() -> u32 {
773 4
774}
775
776fn default_embed_metadata() -> bool {
777 true
778}
779
780impl Default for Config {
781 fn default() -> Self {
782 Self {
783 config_version: CURRENT_CONFIG_VERSION,
784 default_model: default_model(),
785 models_dir: default_models_dir(),
786 server_port: default_port(),
787 default_width: default_dimension(),
788 default_height: default_dimension(),
789 default_steps: default_steps(),
790 embed_metadata: default_embed_metadata(),
791 t5_variant: None,
792 umt5_variant: None,
793 qwen3_variant: None,
794 output_dir: None,
795 media_roots: None,
796 default_negative_prompt: None,
797 expand: ExpandSettings::default(),
798 scheduler: SchedulerSettings::default(),
799 logging: LoggingConfig::default(),
800 runpod: crate::runpod::RunPodSettings::default(),
801 lambda: crate::lambda::LambdaSettings::default(),
802 gpus: None,
803 queue_size: None,
804 models: HashMap::new(),
805 }
806 }
807}
808
809impl Config {
810 pub fn install_frozen_model_config(&mut self, model_name: &str, model: ModelConfig) {
818 self.models.insert(
819 format!("{}{model_name}", ModelPaths::FROZEN_MODEL_PREFIX),
820 model.clone(),
821 );
822 self.models.insert(model_name.to_string(), model);
823 }
824
825 pub fn has_frozen_model_config(&self, model_name: &str) -> bool {
826 self.models
827 .contains_key(&format!("{}{model_name}", ModelPaths::FROZEN_MODEL_PREFIX))
828 }
829
830 pub fn gpu_selection(&self) -> crate::types::GpuSelection {
832 self.gpus.clone().unwrap_or_default()
833 }
834
835 pub fn queue_size(&self) -> usize {
837 self.queue_size.unwrap_or(200)
838 }
839
840 pub fn install_runtime_models_dir_override(models_dir: PathBuf) {
841 let _ = RUNTIME_MODELS_DIR_OVERRIDE.get_or_init(|| models_dir);
842 }
843
844 pub fn load_or_default() -> Self {
845 let Some(config_path) = Self::config_path() else {
846 eprintln!("warning: could not determine home directory — using default config");
847 return Config::default();
848 };
849 let mut cfg = if config_path.exists() {
850 match std::fs::read_to_string(&config_path) {
851 Ok(contents) => match toml::from_str(&contents) {
852 Ok(cfg) => cfg,
853 Err(e) => {
854 eprintln!(
855 "warning: failed to parse config at {}: {e} — using defaults",
856 config_path.display()
857 );
858 Config::default()
859 }
860 },
861 Err(e) => {
862 eprintln!(
863 "warning: failed to read config at {}: {e} — using defaults",
864 config_path.display()
865 );
866 Config::default()
867 }
868 }
869 } else {
870 Config::default()
871 };
872
873 if cfg.config_version < CURRENT_CONFIG_VERSION {
875 Self::run_migrations(&mut cfg);
876 cfg.config_version = CURRENT_CONFIG_VERSION;
877 if let Err(e) = cfg.save() {
878 eprintln!("warning: failed to save migrated config: {e}");
879 }
880 }
881
882 if let Some(hook) = POST_LOAD_HOOK.get() {
884 hook(&mut cfg);
885 }
886
887 cfg
888 }
889
890 pub(crate) fn run_migrations(cfg: &mut Config) {
892 if cfg.config_version < 1 {
893 Self::migrate_v0_to_v1(cfg);
894 }
895 }
898
899 fn migrate_v0_to_v1(cfg: &mut Config) {
906 let model_names: Vec<String> = cfg.models.keys().cloned().collect();
907 for name in model_names {
908 if crate::manifest::find_manifest(&name).is_some() {
909 if let Some(mc) = cfg.models.get_mut(&name) {
910 mc.default_steps = None;
911 mc.default_guidance = None;
912 mc.default_width = None;
913 mc.default_height = None;
914 mc.is_schnell = None;
915 mc.is_turbo = None;
916 mc.scheduler = None;
917 mc.negative_prompt = None;
918 mc.default_frames = None;
919 mc.default_fps = None;
920 mc.description = None;
921 mc.family = None;
922 }
923 }
924 }
925 eprintln!("config: migrated v0 → v1 (cleared stale manifest defaults)");
926 }
927
928 pub fn reload_from_disk_preserving_runtime(&self) -> Self {
930 let mut fresh = Self::load_or_default();
931 fresh.models_dir = self.models_dir.clone();
932 fresh
933 }
934
935 pub fn mold_dir() -> Option<PathBuf> {
939 if let Ok(home) = std::env::var("MOLD_HOME") {
940 return Some(PathBuf::from(home));
941 }
942 if let Some(home) = Self::saved_mold_dir() {
943 return Some(home);
944 }
945 Some(
946 dirs::home_dir()
947 .unwrap_or_else(|| PathBuf::from("."))
948 .join(".mold"),
949 )
950 }
951
952 pub fn mold_home_pointer_path() -> Option<PathBuf> {
955 if let Some(path) = std::env::var_os("MOLD_HOME_POINTER_PATH") {
956 return Some(PathBuf::from(path));
957 }
958 dirs::config_dir().map(|dir| dir.join("mold").join("home"))
959 }
960
961 pub fn saved_mold_dir() -> Option<PathBuf> {
962 Self::read_saved_mold_dir().ok().flatten()
963 }
964
965 pub fn read_saved_mold_dir() -> std::io::Result<Option<PathBuf>> {
969 let Some(pointer) = Self::mold_home_pointer_path() else {
970 return Ok(None);
971 };
972 let raw = match std::fs::read_to_string(&pointer) {
973 Ok(raw) => raw,
974 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
975 Err(error) => return Err(error),
976 };
977 let value = raw.trim();
978 if value.is_empty() {
979 return Err(std::io::Error::new(
980 std::io::ErrorKind::InvalidData,
981 format!("{} is empty", pointer.display()),
982 ));
983 }
984 let path = PathBuf::from(value);
985 if !path.is_absolute() {
986 return Err(std::io::Error::new(
987 std::io::ErrorKind::InvalidData,
988 format!("{} does not contain an absolute path", pointer.display()),
989 ));
990 }
991 Ok(Some(path))
992 }
993
994 pub fn ensure_saved_mold_dir_available() -> anyhow::Result<()> {
1000 if std::env::var_os("MOLD_HOME").is_some() {
1001 return Ok(());
1002 }
1003 let saved = Self::read_saved_mold_dir().with_context(|| {
1004 "the saved Mold home selection is unreadable or invalid; repair it in Mold Desktop Settings"
1005 })?;
1006 if let Some(path) = saved.filter(|path| !path.is_dir()) {
1007 anyhow::bail!(
1008 "the saved Mold home at {} is unavailable; reconnect its drive or change the location in Mold Desktop Settings",
1009 path.display()
1010 );
1011 }
1012 Ok(())
1013 }
1014
1015 pub fn save_mold_dir(path: &Path) -> std::io::Result<()> {
1018 let pointer = Self::mold_home_pointer_path().ok_or_else(|| {
1019 std::io::Error::new(
1020 std::io::ErrorKind::NotFound,
1021 "could not resolve the Mold home bootstrap location",
1022 )
1023 })?;
1024 if let Some(parent) = pointer.parent() {
1025 std::fs::create_dir_all(parent)?;
1026 }
1027 let temp = pointer.with_extension("tmp");
1028 std::fs::write(&temp, path.to_string_lossy().as_bytes())?;
1029 match std::fs::rename(&temp, &pointer) {
1030 Ok(()) => Ok(()),
1031 Err(_error) if pointer.exists() => {
1032 std::fs::remove_file(&pointer)?;
1036 std::fs::rename(temp, pointer)
1037 }
1038 Err(error) => Err(error),
1039 }
1040 }
1041
1042 pub fn config_path() -> Option<PathBuf> {
1043 Self::mold_dir().map(|d| d.join("config.toml"))
1044 }
1045
1046 pub fn data_dir() -> Option<PathBuf> {
1047 Self::mold_dir()
1048 }
1049
1050 pub fn resolved_models_dir(&self) -> PathBuf {
1051 if let Some(models_dir) = RUNTIME_MODELS_DIR_OVERRIDE.get() {
1052 return models_dir.clone();
1053 }
1054 if let Ok(env_dir) = std::env::var("MOLD_MODELS_DIR") {
1055 PathBuf::from(env_dir)
1056 } else if self.models_dir == "~/.mold/models" {
1057 Self::mold_dir()
1058 .unwrap_or_else(|| PathBuf::from(".mold"))
1059 .join("models")
1060 } else {
1061 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1062 let expanded = self.models_dir.replace("~", &home.to_string_lossy());
1063 PathBuf::from(expanded)
1064 }
1065 }
1066
1067 pub fn has_models_dir_override(&self) -> bool {
1068 RUNTIME_MODELS_DIR_OVERRIDE.get().is_some() || std::env::var_os("MOLD_MODELS_DIR").is_some()
1069 }
1070
1071 pub fn resolved_default_model(&self) -> String {
1079 self.resolve_default_model().model
1080 }
1081
1082 pub fn resolve_default_model(&self) -> DefaultModelResolution {
1084 if let Ok(m) = std::env::var("MOLD_DEFAULT_MODEL") {
1086 if !m.is_empty() {
1087 return DefaultModelResolution {
1088 model: m,
1089 source: DefaultModelSource::EnvVar,
1090 };
1091 }
1092 }
1093 let configured = &self.default_model;
1095 if self.lookup_model_config(configured).is_some() {
1096 return DefaultModelResolution {
1097 model: configured.clone(),
1098 source: DefaultModelSource::ConfigCustomEntry,
1099 };
1100 }
1101 if self.manifest_model_is_downloaded(configured) {
1103 return DefaultModelResolution {
1104 model: configured.clone(),
1105 source: DefaultModelSource::Config,
1106 };
1107 }
1108 if let Some(last) = Self::read_last_model() {
1110 if self.manifest_model_is_downloaded(&last) {
1111 return DefaultModelResolution {
1112 model: last,
1113 source: DefaultModelSource::LastUsed,
1114 };
1115 }
1116 }
1117 let downloaded: Vec<String> = crate::manifest::known_manifests()
1119 .iter()
1120 .filter(|m| {
1121 !m.is_utility() && !m.is_upscaler() && self.manifest_model_is_downloaded(&m.name)
1122 })
1123 .map(|m| m.name.clone())
1124 .collect();
1125 if downloaded.len() == 1 {
1126 return DefaultModelResolution {
1127 model: downloaded.into_iter().next().unwrap(),
1128 source: DefaultModelSource::OnlyDownloaded,
1129 };
1130 }
1131 DefaultModelResolution {
1134 model: crate::manifest::resolve_model_name(configured),
1135 source: DefaultModelSource::ConfigDefault,
1136 }
1137 }
1138
1139 fn last_model_path() -> Option<PathBuf> {
1141 Self::mold_dir().map(|d| d.join("last-model"))
1142 }
1143
1144 pub fn read_last_model() -> Option<String> {
1150 if let Some(hook) = READ_LAST_MODEL_HOOK.get() {
1151 return hook();
1152 }
1153 Self::read_last_model_from_sidecar()
1154 }
1155
1156 pub fn read_last_model_from_sidecar() -> Option<String> {
1159 let path = Self::last_model_path()?;
1160 std::fs::read_to_string(path).ok().and_then(|s| {
1161 let trimmed = s.trim().to_string();
1162 if trimmed.is_empty() {
1163 None
1164 } else {
1165 Some(trimmed)
1166 }
1167 })
1168 }
1169
1170 pub fn resolved_output_dir(&self) -> Option<PathBuf> {
1174 let raw = if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
1175 if env_dir.is_empty() {
1176 None
1177 } else {
1178 Some(env_dir)
1179 }
1180 } else {
1181 self.output_dir.clone().filter(|s| !s.is_empty())
1182 };
1183 raw.map(|dir| {
1184 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1185 if dir == "~" {
1186 home
1187 } else if let Some(rest) = dir.strip_prefix("~/") {
1188 home.join(rest)
1189 } else {
1190 PathBuf::from(dir)
1191 }
1192 })
1193 }
1194
1195 pub fn is_output_disabled(&self) -> bool {
1198 if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
1199 return env_dir.is_empty();
1200 }
1201 matches!(self.output_dir.as_deref(), Some(""))
1202 }
1203
1204 pub fn effective_output_dir(&self) -> PathBuf {
1207 self.resolved_output_dir().unwrap_or_else(|| {
1208 Self::mold_dir()
1209 .unwrap_or_else(|| PathBuf::from(".mold"))
1210 .join("output")
1211 })
1212 }
1213
1214 pub fn resolved_media_roots(&self) -> Vec<PathBuf> {
1215 if let Ok(roots) = std::env::var("MOLD_MEDIA_ROOTS") {
1216 return crate::parse_media_roots_env(&roots);
1217 }
1218 self.media_roots
1219 .as_deref()
1220 .map(crate::configured_media_roots)
1221 .unwrap_or_default()
1222 }
1223
1224 pub fn resolved_log_dir(&self) -> PathBuf {
1226 if let Some(ref dir) = self.logging.dir {
1227 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1228 if dir == "~" {
1229 home
1230 } else if let Some(rest) = dir.strip_prefix("~/") {
1231 home.join(rest)
1232 } else {
1233 PathBuf::from(dir)
1234 }
1235 } else {
1236 Self::mold_dir()
1237 .unwrap_or_else(|| PathBuf::from(".mold"))
1238 .join("logs")
1239 }
1240 }
1241
1242 pub fn effective_embed_metadata(&self, override_value: Option<bool>) -> bool {
1243 if let Some(value) = override_value {
1244 return value;
1245 }
1246
1247 match std::env::var("MOLD_EMBED_METADATA") {
1248 Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
1249 "1" | "true" | "yes" | "on" => true,
1250 "0" | "false" | "no" | "off" => false,
1251 _ => {
1252 eprintln!(
1253 "warning: invalid MOLD_EMBED_METADATA value '{value}' — using config/default"
1254 );
1255 self.embed_metadata
1256 }
1257 },
1258 Err(_) => self.embed_metadata,
1259 }
1260 }
1261
1262 pub fn discovered_manifest_paths(&self, name: &str) -> Option<ModelPaths> {
1263 let manifest = crate::manifest::find_manifest(name)?;
1264 if self.incomplete_pull_blocks_manifest(manifest) {
1265 return None;
1266 }
1267 let models_dir = self.resolved_models_dir();
1268 let downloads = manifest
1269 .files
1270 .iter()
1271 .map(|file| {
1272 let local = crate::manifest::storage_path_candidates(manifest, file)
1275 .into_iter()
1276 .map(|path| models_dir.join(path))
1277 .find(|path| Self::file_is_complete(path, file.size_bytes));
1282 if let Some(path) = local {
1283 return Some((file.component, path));
1284 }
1285 if manifest.family != "companion" {
1299 return None;
1300 }
1301 crate::download::cached_file_path_in_mold_cache(&file.hf_repo, &file.hf_filename)
1302 .map(|path| (file.component, path))
1303 })
1304 .collect::<Option<Vec<_>>>()?;
1305 crate::manifest::paths_from_downloads(&downloads, &manifest.family)
1306 }
1307
1308 pub fn manifest_model_is_downloaded(&self, name: &str) -> bool {
1309 let manifest = match crate::manifest::find_manifest(name) {
1310 Some(m) => m,
1311 None => return false,
1312 };
1313 if self.incomplete_pull_blocks_manifest(manifest) {
1314 return false;
1315 }
1316 if manifest.is_upscaler() || manifest.is_utility() {
1319 return self.manifest_files_exist(manifest);
1320 }
1321 self.resolved_local_manifest_model_config(name).is_some()
1322 }
1323
1324 pub fn manifest_model_needs_download(&self, name: &str) -> bool {
1327 let canonical = crate::manifest::resolve_model_name(name);
1328 crate::manifest::find_manifest(&canonical).is_some()
1329 && !self.manifest_model_is_downloaded(&canonical)
1330 }
1331
1332 fn manifest_files_exist(&self, manifest: &crate::manifest::ModelManifest) -> bool {
1349 let models_dir = self.resolved_models_dir();
1350 manifest.files.iter().all(|file| {
1351 crate::manifest::storage_path_candidates(manifest, file)
1352 .into_iter()
1353 .map(|path| models_dir.join(path))
1354 .any(|path| Self::file_is_complete(&path, file.size_bytes))
1355 })
1356 }
1357
1358 fn file_is_complete(path: &std::path::Path, expected_size: u64) -> bool {
1362 if !path.exists() {
1363 return false;
1364 }
1365 if crate::download::has_sha256_marker(path) {
1366 return true;
1367 }
1368 match path.metadata() {
1371 Ok(meta) => meta.len() == expected_size,
1372 Err(_) => false,
1373 }
1374 }
1375
1376 fn incomplete_pull_blocks_manifest(&self, manifest: &crate::manifest::ModelManifest) -> bool {
1383 let marker_path =
1384 crate::download::pulling_marker_path_in(&self.resolved_models_dir(), &manifest.name);
1385 if !marker_path.exists() {
1386 return false;
1387 }
1388
1389 if self.manifest_files_exist(manifest) {
1390 let _ = std::fs::remove_file(&marker_path);
1391 return false;
1392 }
1393
1394 true
1395 }
1396
1397 pub fn model_config(&self, name: &str) -> ModelConfig {
1400 let mut cfg = self.lookup_model_config(name).unwrap_or_default();
1401
1402 if let Some(discovered) = self.resolved_local_manifest_model_config(name) {
1403 overlay_model_paths(&mut cfg, &discovered);
1404 if cfg.description.is_none() {
1405 cfg.description = discovered.description;
1406 }
1407 if cfg.family.is_none() {
1408 cfg.family = discovered.family;
1409 }
1410 }
1411
1412 cfg
1413 }
1414
1415 pub fn resolved_model_config(&self, name: &str) -> ModelConfig {
1417 let mut cfg = self.model_config(name);
1418
1419 if let Some(manifest) = crate::manifest::find_manifest(name) {
1420 if cfg.default_steps.is_none() {
1425 cfg.default_steps = Some(manifest.defaults.steps);
1426 }
1427 if cfg.default_guidance.is_none() {
1428 cfg.default_guidance = Some(manifest.defaults.guidance);
1429 }
1430 if cfg.default_width.is_none() {
1431 cfg.default_width = Some(manifest.defaults.width);
1432 }
1433 if cfg.default_height.is_none() {
1434 cfg.default_height = Some(manifest.defaults.height);
1435 }
1436 if cfg.is_schnell.is_none() {
1437 cfg.is_schnell = Some(manifest.defaults.is_schnell);
1438 }
1439 if cfg.scheduler.is_none() {
1440 cfg.scheduler = manifest.defaults.scheduler;
1441 }
1442 if cfg.negative_prompt.is_none() {
1443 cfg.negative_prompt = manifest.defaults.negative_prompt.clone();
1444 }
1445 if cfg.default_frames.is_none() {
1446 cfg.default_frames = manifest.defaults.frames;
1447 }
1448 if cfg.default_fps.is_none() {
1449 cfg.default_fps = manifest.defaults.fps;
1450 }
1451 cfg.description = Some(manifest.description.clone());
1454 cfg.family = Some(manifest.family.clone());
1455 }
1456
1457 cfg
1458 }
1459
1460 pub fn upsert_model(&mut self, name: String, config: ModelConfig) {
1462 self.models.insert(name, config);
1463 }
1464
1465 pub fn remove_model(&mut self, name: &str) -> Option<ModelConfig> {
1467 self.models.remove(name)
1468 }
1469
1470 pub fn resolved_placement(&self, model_name: &str) -> Option<crate::types::DevicePlacement> {
1485 use crate::types::DevicePlacement;
1486
1487 let mut placement = self
1488 .lookup_model_config(model_name)
1489 .and_then(|mc| mc.placement);
1490
1491 let env_tier1 = parse_device_ref_env("MOLD_PLACE_TEXT_ENCODERS");
1492 let env_transformer = parse_device_ref_env("MOLD_PLACE_TRANSFORMER");
1493 let env_vae = parse_device_ref_env("MOLD_PLACE_VAE");
1494 let env_t5 = parse_device_ref_env("MOLD_PLACE_T5");
1495 let env_clip_l = parse_device_ref_env("MOLD_PLACE_CLIP_L");
1496 let env_clip_g = parse_device_ref_env("MOLD_PLACE_CLIP_G");
1497 let env_qwen = parse_device_ref_env("MOLD_PLACE_QWEN");
1498
1499 let any_env = env_tier1.is_some()
1500 || env_transformer.is_some()
1501 || env_vae.is_some()
1502 || env_t5.is_some()
1503 || env_clip_l.is_some()
1504 || env_clip_g.is_some()
1505 || env_qwen.is_some();
1506
1507 if !any_env {
1508 return placement;
1509 }
1510
1511 let mut effective: DevicePlacement = placement.unwrap_or_default();
1512 if let Some(r) = env_tier1 {
1513 effective.text_encoders = r;
1514 }
1515 let any_advanced = env_transformer.is_some()
1516 || env_vae.is_some()
1517 || env_t5.is_some()
1518 || env_clip_l.is_some()
1519 || env_clip_g.is_some()
1520 || env_qwen.is_some();
1521 if any_advanced {
1522 let mut adv = effective.advanced.unwrap_or_default();
1523 if let Some(r) = env_transformer {
1524 adv.transformer = r;
1525 }
1526 if let Some(r) = env_vae {
1527 adv.vae = r;
1528 }
1529 if let Some(r) = env_t5 {
1530 adv.t5 = Some(r);
1531 }
1532 if let Some(r) = env_clip_l {
1533 adv.clip_l = Some(r);
1534 }
1535 if let Some(r) = env_clip_g {
1536 adv.clip_g = Some(r);
1537 }
1538 if let Some(r) = env_qwen {
1539 adv.qwen = Some(r);
1540 }
1541 effective.advanced = Some(adv);
1542 }
1543 placement = Some(effective);
1544 placement
1545 }
1546
1547 pub fn effective_placement(
1555 &self,
1556 model_name: &str,
1557 request: Option<&crate::types::DevicePlacement>,
1558 ) -> crate::types::DevicePlacement {
1559 request
1560 .cloned()
1561 .or_else(|| self.resolved_placement(model_name))
1562 .unwrap_or_default()
1563 }
1564
1565 pub fn set_model_placement(
1569 &mut self,
1570 model_name: &str,
1571 placement: Option<crate::types::DevicePlacement>,
1572 ) {
1573 let entry = self.models.entry(model_name.to_string()).or_default();
1574 entry.placement = placement;
1575 }
1576
1577 pub fn save(&self) -> anyhow::Result<()> {
1582 let path = Self::config_path()
1583 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory for config path"))?;
1584
1585 let path_str = path.to_string_lossy();
1590 let is_temp_config = path_str.contains("/tmp/") || path_str.contains("/mold-config-test-");
1591 let has_temp_models_dir = self.models_dir.contains("/tmp/mold-")
1592 || self.models_dir.contains("/mold-config-test-");
1593 if has_temp_models_dir && !is_temp_config {
1594 eprintln!(
1595 "warning: refusing to save config with test models_dir ({}) to real config ({})",
1596 self.models_dir,
1597 path.display()
1598 );
1599 return Ok(());
1600 }
1601
1602 if let Some(parent) = path.parent() {
1603 std::fs::create_dir_all(parent)?;
1604 }
1605 let contents = toml::to_string_pretty(self)?;
1606 std::fs::write(&path, contents)?;
1607 Ok(())
1608 }
1609
1610 pub fn save_bootstrap_only_to(&self, path: &std::path::Path) -> anyhow::Result<()> {
1619 if let Some(parent) = path.parent() {
1620 std::fs::create_dir_all(parent)?;
1621 }
1622 let mut doc = toml::Value::try_from(self)?;
1623 strip_user_pref_fields(&mut doc);
1624 let body = toml::to_string_pretty(&doc)?;
1625 let contents = format!("{BOOTSTRAP_ONLY_BANNER}{body}");
1626 std::fs::write(path, contents)?;
1627 Ok(())
1628 }
1629
1630 pub fn save_bootstrap_only(&self) -> anyhow::Result<()> {
1632 let path = Self::config_path()
1633 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory for config path"))?;
1634 self.save_bootstrap_only_to(&path)
1635 }
1636
1637 pub fn exists_on_disk() -> bool {
1639 Self::config_path().is_some_and(|p| p.exists())
1640 }
1641
1642 pub fn lookup_model_config(&self, name: &str) -> Option<ModelConfig> {
1645 if let Some(cfg) = self.models.get(name) {
1646 return Some(cfg.clone());
1647 }
1648 let canonical = resolve_model_name(name);
1649 if canonical != name {
1650 return self.models.get(&canonical).cloned();
1651 }
1652 None
1653 }
1654
1655 fn discovered_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
1656 let manifest = crate::manifest::find_manifest(name)?;
1657 let paths = self.discovered_manifest_paths(name)?;
1658 Some(manifest.to_model_config(&paths))
1659 }
1660
1661 fn resolved_local_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
1662 let manifest = crate::manifest::find_manifest(name)?;
1663 let paths = if let Some(paths) = self.discovered_manifest_paths(name) {
1664 paths
1665 } else {
1666 let paths = ModelPaths::resolve(name, self)?;
1667 if !resolved_manifest_paths_exist(manifest, &paths) {
1668 return None;
1669 }
1670 paths
1671 };
1672 Some(manifest.to_model_config(&paths))
1673 }
1674}
1675
1676fn overlay_model_paths(target: &mut ModelConfig, source: &ModelConfig) {
1677 target.transformer = source.transformer.clone();
1678 target.transformer_shards = source.transformer_shards.clone();
1679 target.vae = source.vae.clone();
1680 if source.spatial_upscaler.is_some() {
1681 target.spatial_upscaler = source.spatial_upscaler.clone();
1682 }
1683 if source.temporal_upscaler.is_some() {
1684 target.temporal_upscaler = source.temporal_upscaler.clone();
1685 }
1686 if source.distilled_lora.is_some() {
1687 target.distilled_lora = source.distilled_lora.clone();
1688 }
1689
1690 if source.t5_encoder.is_some() {
1691 target.t5_encoder = source.t5_encoder.clone();
1692 }
1693 if source.clip_encoder.is_some() {
1694 target.clip_encoder = source.clip_encoder.clone();
1695 }
1696 if source.t5_tokenizer.is_some() {
1697 target.t5_tokenizer = source.t5_tokenizer.clone();
1698 }
1699 if source.clip_tokenizer.is_some() {
1700 target.clip_tokenizer = source.clip_tokenizer.clone();
1701 }
1702 if source.clip_encoder_2.is_some() {
1703 target.clip_encoder_2 = source.clip_encoder_2.clone();
1704 }
1705 if source.clip_tokenizer_2.is_some() {
1706 target.clip_tokenizer_2 = source.clip_tokenizer_2.clone();
1707 }
1708 if source.text_encoder_files.is_some() {
1709 target.text_encoder_files = source.text_encoder_files.clone();
1710 }
1711 if source.text_tokenizer.is_some() {
1712 target.text_tokenizer = source.text_tokenizer.clone();
1713 }
1714 if source.decoder.is_some() {
1715 target.decoder = source.decoder.clone();
1716 }
1717}
1718
1719fn resolved_manifest_paths_exist(
1720 manifest: &crate::manifest::ModelManifest,
1721 paths: &ModelPaths,
1722) -> bool {
1723 use crate::manifest::ModelComponent;
1724
1725 let mut transformer_shard_idx = 0usize;
1726 let mut text_encoder_idx = 0usize;
1727
1728 manifest.files.iter().all(|file| match file.component {
1729 ModelComponent::Transformer => paths.transformer.exists(),
1730 ModelComponent::TransformerShard => {
1731 let path = paths.transformer_shards.get(transformer_shard_idx);
1732 transformer_shard_idx += 1;
1733 path.is_some_and(|path| path.exists())
1734 }
1735 ModelComponent::Vae => paths.vae.exists(),
1736 ModelComponent::SpatialUpscaler => paths
1737 .spatial_upscaler
1738 .as_ref()
1739 .is_some_and(|path| path.exists()),
1740 ModelComponent::TemporalUpscaler => paths
1741 .temporal_upscaler
1742 .as_ref()
1743 .is_some_and(|path| path.exists()),
1744 ModelComponent::LowNoiseTransformer => paths
1745 .low_noise_transformer
1746 .as_ref()
1747 .is_some_and(|path| path.exists()),
1748 ModelComponent::DistilledLora => paths
1749 .distilled_lora
1750 .as_ref()
1751 .is_some_and(|path| path.exists()),
1752 ModelComponent::LowNoiseDistilledLora => paths
1753 .low_noise_distilled_lora
1754 .as_ref()
1755 .is_some_and(|path| path.exists()),
1756 ModelComponent::T5Encoder => paths.t5_encoder.as_ref().is_some_and(|path| path.exists()),
1757 ModelComponent::ClipEncoder => paths
1758 .clip_encoder
1759 .as_ref()
1760 .is_some_and(|path| path.exists()),
1761 ModelComponent::T5Tokenizer => paths
1762 .t5_tokenizer
1763 .as_ref()
1764 .is_some_and(|path| path.exists()),
1765 ModelComponent::ClipTokenizer => paths
1766 .clip_tokenizer
1767 .as_ref()
1768 .is_some_and(|path| path.exists()),
1769 ModelComponent::ClipEncoder2 => paths
1770 .clip_encoder_2
1771 .as_ref()
1772 .is_some_and(|path| path.exists()),
1773 ModelComponent::ClipTokenizer2 => paths
1774 .clip_tokenizer_2
1775 .as_ref()
1776 .is_some_and(|path| path.exists()),
1777 ModelComponent::TextEncoder => {
1778 let path = paths.text_encoder_files.get(text_encoder_idx);
1779 text_encoder_idx += 1;
1780 path.is_some_and(|path| path.exists())
1781 }
1782 ModelComponent::TextTokenizer => paths
1783 .text_tokenizer
1784 .as_ref()
1785 .is_some_and(|path| path.exists()),
1786 ModelComponent::AudioVae
1790 | ModelComponent::Processor
1791 | ModelComponent::VideoScheduler
1792 | ModelComponent::AudioScheduler
1793 | ModelComponent::ModelConfig
1794 | ModelComponent::TaskConfig => false,
1795 ModelComponent::Decoder => paths.decoder.as_ref().is_some_and(|path| path.exists()),
1796 ModelComponent::Upscaler => paths.transformer.exists(),
1797 })
1798}
1799
1800pub fn parse_device_ref_str(raw: &str) -> Result<crate::types::DeviceRef, String> {
1808 use crate::types::DeviceRef;
1809 let raw = raw.trim();
1810 let normalized = raw.to_ascii_lowercase();
1811 if normalized == "auto" {
1812 Ok(DeviceRef::Auto)
1813 } else if normalized == "cpu" {
1814 Ok(DeviceRef::Cpu)
1815 } else if normalized == "gpu" {
1816 Ok(DeviceRef::gpu(0))
1817 } else if let Some(rest) = normalized.strip_prefix("gpu:") {
1818 rest.parse::<usize>()
1819 .map(DeviceRef::gpu)
1820 .map_err(|_| invalid_device_ref(raw))
1821 } else if normalized.starts_with("device:") {
1822 parse_stable_device_id(&raw["device:".len()..])
1823 } else if is_stable_device_id(raw) {
1824 Ok(DeviceRef::device(raw))
1825 } else {
1826 Err(invalid_device_ref(raw))
1827 }
1828}
1829
1830fn parse_stable_device_id(raw: &str) -> Result<crate::types::DeviceRef, String> {
1831 if is_stable_device_id(raw) {
1832 Ok(crate::types::DeviceRef::device(raw))
1833 } else {
1834 Err(invalid_device_ref(raw))
1835 }
1836}
1837
1838fn is_stable_device_id(raw: &str) -> bool {
1839 let lower = raw.to_ascii_lowercase();
1840 (lower.starts_with("cuda:") && raw.len() > "cuda:".len())
1841 || (lower.starts_with("metal:") && raw.len() > "metal:".len())
1842}
1843
1844fn invalid_device_ref(raw: &str) -> String {
1845 format!(
1846 "invalid device '{raw}' (expected auto|cpu|gpu[:N]|device:<stable-id>|cuda:<id>|metal:<id>)"
1847 )
1848}
1849
1850fn parse_device_ref_env(key: &str) -> Option<crate::types::DeviceRef> {
1851 let raw = std::env::var(key).ok()?;
1852 match parse_device_ref_str(&raw) {
1853 Ok(dr) => Some(dr),
1854 Err(msg) => {
1855 eprintln!("mold: ignoring {key}={raw}: {msg}");
1856 None
1857 }
1858 }
1859}
1860
1861#[cfg(test)]
1862mod scheduler_settings_tests {
1863 use super::SchedulerSettings;
1864
1865 #[test]
1866 fn scheduler_settings_defaults_and_rejects_invalid_toml() {
1867 let defaults: SchedulerSettings = toml::from_str("").unwrap();
1868 assert_eq!(defaults, SchedulerSettings::default());
1869
1870 let error = toml::from_str::<SchedulerSettings>(
1871 "replan_debounce_ms = 5000\nreplan_max_delay_ms = 4999",
1872 )
1873 .unwrap_err();
1874 assert!(error
1875 .to_string()
1876 .contains("replan_max_delay_ms must be greater than or equal"));
1877 assert!(toml::from_str::<SchedulerSettings>("warm_wait_max_ms = 30001").is_err());
1878 }
1879}