1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::sync::OnceLock;
5
6use crate::expand::ExpandSettings;
7use crate::manifest::resolve_model_name;
8use crate::types::Scheduler;
9
10static RUNTIME_MODELS_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
11
12const BOOTSTRAP_ONLY_BANNER: &str = "\
15# mold config — bootstrap-only surface.
16#
17# User preferences (expand.*, generate.default_*, per-model generation
18# defaults, lora, scheduler) live in SQLite at <MOLD_HOME>/mold.db.
19# Edit them via:
20# mold config set <key> <value>
21# mold config get <key>
22# mold config reset <key> # drop the DB row, fall back to this file
23#
24# This file retains only identifiers, paths, ports, credentials, logging,
25# and per-model file-path entries. Adding generation defaults here is
26# silently overridden by the DB on load.
27
28";
29
30const STRIPPED_GLOBAL_KEYS: &[&str] = &[
33 "default_width",
34 "default_height",
35 "default_steps",
36 "embed_metadata",
37 "default_negative_prompt",
38 "t5_variant",
39 "qwen3_variant",
40 "expand",
41];
42
43const STRIPPED_MODEL_KEYS: &[&str] = &[
44 "default_steps",
45 "default_guidance",
46 "default_width",
47 "default_height",
48 "scheduler",
49 "negative_prompt",
50 "lora",
51 "lora_scale",
52 "default_frames",
53 "default_fps",
54];
55
56fn strip_user_pref_fields(doc: &mut toml::Value) {
57 let Some(table) = doc.as_table_mut() else {
58 return;
59 };
60 for key in STRIPPED_GLOBAL_KEYS {
61 table.remove(*key);
62 }
63 if let Some(toml::Value::Table(models)) = table.get_mut("models") {
64 for (_, mc) in models.iter_mut() {
65 if let Some(mc_table) = mc.as_table_mut() {
66 for key in STRIPPED_MODEL_KEYS {
67 mc_table.remove(*key);
68 }
69 }
70 }
71 }
72}
73
74pub type ConfigPostLoadHook = fn(&mut Config);
84static POST_LOAD_HOOK: OnceLock<ConfigPostLoadHook> = OnceLock::new();
85
86pub fn install_post_load_hook(hook: ConfigPostLoadHook) {
89 let _ = POST_LOAD_HOOK.set(hook);
90}
91
92pub type ReadLastModelHook = fn() -> Option<String>;
96static READ_LAST_MODEL_HOOK: OnceLock<ReadLastModelHook> = OnceLock::new();
97
98pub fn install_read_last_model_hook(hook: ReadLastModelHook) {
100 let _ = READ_LAST_MODEL_HOOK.set(hook);
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum DefaultModelSource {
106 EnvVar,
108 ConfigCustomEntry,
110 Config,
112 LastUsed,
114 OnlyDownloaded,
116 ConfigDefault,
118}
119
120#[derive(Debug, Clone)]
122pub struct DefaultModelResolution {
123 pub model: String,
124 pub source: DefaultModelSource,
125}
126
127#[derive(Debug, Clone, Deserialize, Serialize, Default)]
129pub struct ModelConfig {
130 pub transformer: Option<String>,
132 pub transformer_shards: Option<Vec<String>>,
134 pub vae: Option<String>,
135 pub spatial_upscaler: Option<String>,
137 pub temporal_upscaler: Option<String>,
139 pub distilled_lora: Option<String>,
141 pub t5_encoder: Option<String>,
142 pub clip_encoder: Option<String>,
143 pub t5_tokenizer: Option<String>,
144 pub clip_tokenizer: Option<String>,
145 pub clip_encoder_2: Option<String>,
147 pub clip_tokenizer_2: Option<String>,
149 pub text_encoder_files: Option<Vec<String>>,
151 pub text_tokenizer: Option<String>,
153 pub decoder: Option<String>,
155
156 pub default_steps: Option<u32>,
159 pub default_guidance: Option<f64>,
161 pub default_width: Option<u32>,
163 pub default_height: Option<u32>,
165 pub is_schnell: Option<bool>,
168 pub is_turbo: Option<bool>,
171 pub scheduler: Option<Scheduler>,
173 pub negative_prompt: Option<String>,
175 pub lora: Option<String>,
177 pub lora_scale: Option<f64>,
179 pub default_frames: Option<u32>,
181 pub default_fps: Option<u32>,
183
184 pub description: Option<String>,
186 pub family: Option<String>,
187
188 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub placement: Option<crate::types::DevicePlacement>,
192}
193
194impl ModelConfig {
195 pub fn all_file_paths(&self) -> Vec<String> {
198 let mut paths = Vec::new();
199 let singles = [
200 &self.transformer,
201 &self.vae,
202 &self.spatial_upscaler,
203 &self.temporal_upscaler,
204 &self.distilled_lora,
205 &self.t5_encoder,
206 &self.clip_encoder,
207 &self.t5_tokenizer,
208 &self.clip_tokenizer,
209 &self.clip_encoder_2,
210 &self.clip_tokenizer_2,
211 &self.text_tokenizer,
212 &self.decoder,
213 ];
214 for p in singles.into_iter().flatten() {
215 paths.push(p.clone());
216 }
217 if let Some(ref shards) = self.transformer_shards {
218 paths.extend(shards.iter().cloned());
219 }
220 if let Some(ref files) = self.text_encoder_files {
221 paths.extend(files.iter().cloned());
222 }
223 paths
224 }
225
226 pub fn disk_usage(&self) -> (u64, f64) {
231 let total: u64 = self
232 .all_file_paths()
233 .iter()
234 .filter_map(|p| std::fs::metadata(p).ok())
235 .map(|m| m.len())
236 .sum();
237 (total, total as f64 / 1_073_741_824.0)
238 }
239
240 pub fn effective_steps(&self, global_cfg: &Config) -> u32 {
242 self.default_steps.unwrap_or(global_cfg.default_steps)
243 }
244
245 pub fn effective_guidance(&self) -> f64 {
247 self.default_guidance.unwrap_or(3.5)
248 }
249
250 pub fn effective_width(&self, global_cfg: &Config) -> u32 {
252 self.default_width.unwrap_or(global_cfg.default_width)
253 }
254
255 pub fn effective_height(&self, global_cfg: &Config) -> u32 {
257 self.default_height.unwrap_or(global_cfg.default_height)
258 }
259
260 pub fn effective_negative_prompt(&self, global_cfg: &Config) -> Option<String> {
262 self.negative_prompt
263 .clone()
264 .or_else(|| global_cfg.default_negative_prompt.clone())
265 }
266
267 pub fn effective_lora(&self) -> Option<(String, f64)> {
269 self.lora
270 .as_ref()
271 .map(|path| (path.clone(), self.lora_scale.unwrap_or(1.0)))
272 }
273
274 pub fn effective_frames(&self) -> Option<u32> {
276 self.default_frames
277 }
278
279 pub fn effective_fps(&self) -> Option<u32> {
281 self.default_fps
282 }
283}
284
285#[derive(Debug, Clone)]
291pub struct ModelPaths {
292 pub transformer: PathBuf,
293 pub transformer_shards: Vec<PathBuf>,
295 pub vae: PathBuf,
296 pub spatial_upscaler: Option<PathBuf>,
297 pub temporal_upscaler: Option<PathBuf>,
298 pub distilled_lora: Option<PathBuf>,
299 pub t5_encoder: Option<PathBuf>,
300 pub clip_encoder: Option<PathBuf>,
301 pub t5_tokenizer: Option<PathBuf>,
302 pub clip_tokenizer: Option<PathBuf>,
303 pub clip_encoder_2: Option<PathBuf>,
305 pub clip_tokenizer_2: Option<PathBuf>,
307 pub text_encoder_files: Vec<PathBuf>,
309 pub text_tokenizer: Option<PathBuf>,
311 pub decoder: Option<PathBuf>,
313}
314
315impl ModelPaths {
316 pub fn resolve(model_name: &str, config: &Config) -> Option<Self> {
320 if let Some(model_cfg) = config.discovered_manifest_model_config(model_name) {
321 return Self::resolve_from_model_config(Some(&model_cfg));
322 }
323
324 if crate::manifest::find_manifest(model_name).is_some() && config.has_models_dir_override()
325 {
326 return Self::resolve_from_model_config(None);
327 }
328
329 let model_cfg = config.lookup_model_config(model_name);
330 Self::resolve_from_model_config(model_cfg.as_ref())
331 }
332
333 fn resolve_from_model_config(model_cfg: Option<&ModelConfig>) -> Option<Self> {
334 let transformer = Self::resolve_path(
335 model_cfg.and_then(|m| m.transformer.as_deref()),
336 "MOLD_TRANSFORMER_PATH",
337 )?;
338 let transformer_shards = model_cfg
339 .and_then(|m| m.transformer_shards.as_ref())
340 .map(|shards| shards.iter().map(PathBuf::from).collect())
341 .unwrap_or_default();
342 let vae = Self::resolve_path(model_cfg.and_then(|m| m.vae.as_deref()), "MOLD_VAE_PATH")?;
343 let spatial_upscaler = Self::resolve_path(
344 model_cfg.and_then(|m| m.spatial_upscaler.as_deref()),
345 "MOLD_SPATIAL_UPSCALER_PATH",
346 );
347 let temporal_upscaler = Self::resolve_path(
348 model_cfg.and_then(|m| m.temporal_upscaler.as_deref()),
349 "MOLD_TEMPORAL_UPSCALER_PATH",
350 );
351 let distilled_lora = Self::resolve_path(
352 model_cfg.and_then(|m| m.distilled_lora.as_deref()),
353 "MOLD_DISTILLED_LORA_PATH",
354 );
355 let t5_encoder = Self::resolve_path(
356 model_cfg.and_then(|m| m.t5_encoder.as_deref()),
357 "MOLD_T5_PATH",
358 );
359 let clip_encoder = Self::resolve_path(
360 model_cfg.and_then(|m| m.clip_encoder.as_deref()),
361 "MOLD_CLIP_PATH",
362 );
363 let t5_tokenizer = Self::resolve_path(
364 model_cfg.and_then(|m| m.t5_tokenizer.as_deref()),
365 "MOLD_T5_TOKENIZER_PATH",
366 );
367 let clip_tokenizer = Self::resolve_path(
368 model_cfg.and_then(|m| m.clip_tokenizer.as_deref()),
369 "MOLD_CLIP_TOKENIZER_PATH",
370 );
371 let clip_encoder_2 = Self::resolve_path(
372 model_cfg.and_then(|m| m.clip_encoder_2.as_deref()),
373 "MOLD_CLIP2_PATH",
374 );
375 let clip_tokenizer_2 = Self::resolve_path(
376 model_cfg.and_then(|m| m.clip_tokenizer_2.as_deref()),
377 "MOLD_CLIP2_TOKENIZER_PATH",
378 );
379 let text_encoder_files = model_cfg
380 .and_then(|m| m.text_encoder_files.as_ref())
381 .map(|files| files.iter().map(PathBuf::from).collect())
382 .unwrap_or_default();
383 let text_tokenizer = Self::resolve_path(
384 model_cfg.and_then(|m| m.text_tokenizer.as_deref()),
385 "MOLD_TEXT_TOKENIZER_PATH",
386 );
387 let decoder = Self::resolve_path(
388 model_cfg.and_then(|m| m.decoder.as_deref()),
389 "MOLD_DECODER_PATH",
390 );
391
392 Some(Self {
393 transformer,
394 transformer_shards,
395 vae,
396 spatial_upscaler,
397 temporal_upscaler,
398 distilled_lora,
399 t5_encoder,
400 clip_encoder,
401 t5_tokenizer,
402 clip_tokenizer,
403 clip_encoder_2,
404 clip_tokenizer_2,
405 text_encoder_files,
406 text_tokenizer,
407 decoder,
408 })
409 }
410
411 fn resolve_path(config_val: Option<&str>, env_var: &str) -> Option<PathBuf> {
412 if let Ok(path) = std::env::var(env_var) {
413 return Some(PathBuf::from(path));
414 }
415 if let Some(path) = config_val {
416 return Some(PathBuf::from(path));
417 }
418 None
419 }
420}
421
422const CURRENT_CONFIG_VERSION: u32 = 1;
424
425#[derive(Debug, Clone, Deserialize, Serialize)]
426pub struct Config {
427 #[serde(default)]
430 pub config_version: u32,
431
432 #[serde(default = "default_model")]
433 pub default_model: String,
434
435 #[serde(default = "default_models_dir")]
436 pub models_dir: String,
437
438 #[serde(default = "default_port")]
439 pub server_port: u16,
440
441 #[serde(default = "default_dimension")]
442 pub default_width: u32,
443
444 #[serde(default = "default_dimension")]
445 pub default_height: u32,
446
447 #[serde(default = "default_steps")]
448 pub default_steps: u32,
449
450 #[serde(default = "default_embed_metadata")]
451 pub embed_metadata: bool,
452
453 #[serde(default)]
457 pub t5_variant: Option<String>,
458
459 #[serde(default)]
462 pub qwen3_variant: Option<String>,
463
464 #[serde(default)]
468 pub output_dir: Option<String>,
469
470 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub media_roots: Option<Vec<String>>,
474
475 #[serde(default)]
478 pub default_negative_prompt: Option<String>,
479
480 #[serde(default)]
482 pub expand: ExpandSettings,
483
484 #[serde(default)]
486 pub logging: LoggingConfig,
487
488 #[serde(default)]
490 pub runpod: crate::runpod::RunPodSettings,
491
492 #[serde(default)]
494 pub lambda: crate::lambda::LambdaSettings,
495
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub gpus: Option<Vec<usize>>,
499
500 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub queue_size: Option<usize>,
503
504 #[serde(default)]
506 pub models: HashMap<String, ModelConfig>,
507}
508
509#[derive(Debug, Clone, Deserialize, Serialize)]
511pub struct LoggingConfig {
512 #[serde(default = "default_log_level")]
514 pub level: String,
515
516 #[serde(default)]
518 pub file: bool,
519
520 #[serde(default)]
522 pub dir: Option<String>,
523
524 #[serde(default = "default_log_max_days")]
526 pub max_days: u32,
527}
528
529fn default_log_level() -> String {
530 "info".to_string()
531}
532fn default_log_max_days() -> u32 {
533 7
534}
535
536impl Default for LoggingConfig {
537 fn default() -> Self {
538 Self {
539 level: default_log_level(),
540 file: false,
541 dir: None,
542 max_days: default_log_max_days(),
543 }
544 }
545}
546
547fn default_model() -> String {
548 "flux2-klein:q8".to_string()
549}
550
551fn default_models_dir() -> String {
552 if let Ok(home) = std::env::var("MOLD_HOME") {
553 format!("{home}/models")
554 } else {
555 "~/.mold/models".to_string()
556 }
557}
558
559fn default_port() -> u16 {
560 7680
561}
562
563fn default_dimension() -> u32 {
564 768
565}
566
567fn default_steps() -> u32 {
568 4
569}
570
571fn default_embed_metadata() -> bool {
572 true
573}
574
575impl Default for Config {
576 fn default() -> Self {
577 Self {
578 config_version: CURRENT_CONFIG_VERSION,
579 default_model: default_model(),
580 models_dir: default_models_dir(),
581 server_port: default_port(),
582 default_width: default_dimension(),
583 default_height: default_dimension(),
584 default_steps: default_steps(),
585 embed_metadata: default_embed_metadata(),
586 t5_variant: None,
587 qwen3_variant: None,
588 output_dir: None,
589 media_roots: None,
590 default_negative_prompt: None,
591 expand: ExpandSettings::default(),
592 logging: LoggingConfig::default(),
593 runpod: crate::runpod::RunPodSettings::default(),
594 lambda: crate::lambda::LambdaSettings::default(),
595 gpus: None,
596 queue_size: None,
597 models: HashMap::new(),
598 }
599 }
600}
601
602impl Config {
603 pub fn gpu_selection(&self) -> crate::types::GpuSelection {
605 match &self.gpus {
606 Some(ordinals) if !ordinals.is_empty() => {
607 crate::types::GpuSelection::Specific(ordinals.clone())
608 }
609 _ => crate::types::GpuSelection::All,
610 }
611 }
612
613 pub fn queue_size(&self) -> usize {
615 self.queue_size.unwrap_or(200)
616 }
617
618 pub fn install_runtime_models_dir_override(models_dir: PathBuf) {
619 let _ = RUNTIME_MODELS_DIR_OVERRIDE.get_or_init(|| models_dir);
620 }
621
622 pub fn load_or_default() -> Self {
623 let Some(config_path) = Self::config_path() else {
624 eprintln!("warning: could not determine home directory — using default config");
625 return Config::default();
626 };
627 let mut cfg = if config_path.exists() {
628 match std::fs::read_to_string(&config_path) {
629 Ok(contents) => match toml::from_str(&contents) {
630 Ok(cfg) => cfg,
631 Err(e) => {
632 eprintln!(
633 "warning: failed to parse config at {}: {e} — using defaults",
634 config_path.display()
635 );
636 Config::default()
637 }
638 },
639 Err(e) => {
640 eprintln!(
641 "warning: failed to read config at {}: {e} — using defaults",
642 config_path.display()
643 );
644 Config::default()
645 }
646 }
647 } else {
648 Config::default()
649 };
650
651 if cfg.config_version < CURRENT_CONFIG_VERSION {
653 Self::run_migrations(&mut cfg);
654 cfg.config_version = CURRENT_CONFIG_VERSION;
655 if let Err(e) = cfg.save() {
656 eprintln!("warning: failed to save migrated config: {e}");
657 }
658 }
659
660 if let Some(hook) = POST_LOAD_HOOK.get() {
662 hook(&mut cfg);
663 }
664
665 cfg
666 }
667
668 pub(crate) fn run_migrations(cfg: &mut Config) {
670 if cfg.config_version < 1 {
671 Self::migrate_v0_to_v1(cfg);
672 }
673 }
676
677 fn migrate_v0_to_v1(cfg: &mut Config) {
684 let model_names: Vec<String> = cfg.models.keys().cloned().collect();
685 for name in model_names {
686 if crate::manifest::find_manifest(&name).is_some() {
687 if let Some(mc) = cfg.models.get_mut(&name) {
688 mc.default_steps = None;
689 mc.default_guidance = None;
690 mc.default_width = None;
691 mc.default_height = None;
692 mc.is_schnell = None;
693 mc.is_turbo = None;
694 mc.scheduler = None;
695 mc.negative_prompt = None;
696 mc.default_frames = None;
697 mc.default_fps = None;
698 mc.description = None;
699 mc.family = None;
700 }
701 }
702 }
703 eprintln!("config: migrated v0 → v1 (cleared stale manifest defaults)");
704 }
705
706 pub fn reload_from_disk_preserving_runtime(&self) -> Self {
708 let mut fresh = Self::load_or_default();
709 fresh.models_dir = self.models_dir.clone();
710 fresh
711 }
712
713 pub fn mold_dir() -> Option<PathBuf> {
716 if let Ok(home) = std::env::var("MOLD_HOME") {
717 return Some(PathBuf::from(home));
718 }
719 Some(
720 dirs::home_dir()
721 .unwrap_or_else(|| PathBuf::from("."))
722 .join(".mold"),
723 )
724 }
725
726 pub fn config_path() -> Option<PathBuf> {
727 Self::mold_dir().map(|d| d.join("config.toml"))
728 }
729
730 pub fn data_dir() -> Option<PathBuf> {
731 Self::mold_dir()
732 }
733
734 pub fn resolved_models_dir(&self) -> PathBuf {
735 if let Some(models_dir) = RUNTIME_MODELS_DIR_OVERRIDE.get() {
736 return models_dir.clone();
737 }
738 if let Ok(env_dir) = std::env::var("MOLD_MODELS_DIR") {
739 PathBuf::from(env_dir)
740 } else {
741 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
742 let expanded = self.models_dir.replace("~", &home.to_string_lossy());
743 PathBuf::from(expanded)
744 }
745 }
746
747 pub fn has_models_dir_override(&self) -> bool {
748 RUNTIME_MODELS_DIR_OVERRIDE.get().is_some() || std::env::var_os("MOLD_MODELS_DIR").is_some()
749 }
750
751 pub fn resolved_default_model(&self) -> String {
759 self.resolve_default_model().model
760 }
761
762 pub fn resolve_default_model(&self) -> DefaultModelResolution {
764 if let Ok(m) = std::env::var("MOLD_DEFAULT_MODEL") {
766 if !m.is_empty() {
767 return DefaultModelResolution {
768 model: m,
769 source: DefaultModelSource::EnvVar,
770 };
771 }
772 }
773 let configured = &self.default_model;
775 if self.lookup_model_config(configured).is_some() {
776 return DefaultModelResolution {
777 model: configured.clone(),
778 source: DefaultModelSource::ConfigCustomEntry,
779 };
780 }
781 if self.manifest_model_is_downloaded(configured) {
783 return DefaultModelResolution {
784 model: configured.clone(),
785 source: DefaultModelSource::Config,
786 };
787 }
788 if let Some(last) = Self::read_last_model() {
790 if self.manifest_model_is_downloaded(&last) {
791 return DefaultModelResolution {
792 model: last,
793 source: DefaultModelSource::LastUsed,
794 };
795 }
796 }
797 let downloaded: Vec<String> = crate::manifest::known_manifests()
799 .iter()
800 .filter(|m| {
801 !m.is_utility() && !m.is_upscaler() && self.manifest_model_is_downloaded(&m.name)
802 })
803 .map(|m| m.name.clone())
804 .collect();
805 if downloaded.len() == 1 {
806 return DefaultModelResolution {
807 model: downloaded.into_iter().next().unwrap(),
808 source: DefaultModelSource::OnlyDownloaded,
809 };
810 }
811 DefaultModelResolution {
814 model: crate::manifest::resolve_model_name(configured),
815 source: DefaultModelSource::ConfigDefault,
816 }
817 }
818
819 fn last_model_path() -> Option<PathBuf> {
821 Self::mold_dir().map(|d| d.join("last-model"))
822 }
823
824 pub fn read_last_model() -> Option<String> {
830 if let Some(hook) = READ_LAST_MODEL_HOOK.get() {
831 return hook();
832 }
833 Self::read_last_model_from_sidecar()
834 }
835
836 pub fn read_last_model_from_sidecar() -> Option<String> {
839 let path = Self::last_model_path()?;
840 std::fs::read_to_string(path).ok().and_then(|s| {
841 let trimmed = s.trim().to_string();
842 if trimmed.is_empty() {
843 None
844 } else {
845 Some(trimmed)
846 }
847 })
848 }
849
850 pub fn resolved_output_dir(&self) -> Option<PathBuf> {
854 let raw = if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
855 if env_dir.is_empty() {
856 None
857 } else {
858 Some(env_dir)
859 }
860 } else {
861 self.output_dir.clone().filter(|s| !s.is_empty())
862 };
863 raw.map(|dir| {
864 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
865 if dir == "~" {
866 home
867 } else if let Some(rest) = dir.strip_prefix("~/") {
868 home.join(rest)
869 } else {
870 PathBuf::from(dir)
871 }
872 })
873 }
874
875 pub fn is_output_disabled(&self) -> bool {
878 if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
879 return env_dir.is_empty();
880 }
881 matches!(self.output_dir.as_deref(), Some(""))
882 }
883
884 pub fn effective_output_dir(&self) -> PathBuf {
887 self.resolved_output_dir().unwrap_or_else(|| {
888 Self::mold_dir()
889 .unwrap_or_else(|| PathBuf::from(".mold"))
890 .join("output")
891 })
892 }
893
894 pub fn resolved_media_roots(&self) -> Vec<PathBuf> {
895 if let Ok(roots) = std::env::var("MOLD_MEDIA_ROOTS") {
896 return crate::parse_media_roots_env(&roots);
897 }
898 self.media_roots
899 .as_deref()
900 .map(crate::configured_media_roots)
901 .unwrap_or_default()
902 }
903
904 pub fn resolved_log_dir(&self) -> PathBuf {
906 if let Some(ref dir) = self.logging.dir {
907 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
908 if dir == "~" {
909 home
910 } else if let Some(rest) = dir.strip_prefix("~/") {
911 home.join(rest)
912 } else {
913 PathBuf::from(dir)
914 }
915 } else {
916 Self::mold_dir()
917 .unwrap_or_else(|| PathBuf::from(".mold"))
918 .join("logs")
919 }
920 }
921
922 pub fn effective_embed_metadata(&self, override_value: Option<bool>) -> bool {
923 if let Some(value) = override_value {
924 return value;
925 }
926
927 match std::env::var("MOLD_EMBED_METADATA") {
928 Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
929 "1" | "true" | "yes" | "on" => true,
930 "0" | "false" | "no" | "off" => false,
931 _ => {
932 eprintln!(
933 "warning: invalid MOLD_EMBED_METADATA value '{value}' — using config/default"
934 );
935 self.embed_metadata
936 }
937 },
938 Err(_) => self.embed_metadata,
939 }
940 }
941
942 pub fn discovered_manifest_paths(&self, name: &str) -> Option<ModelPaths> {
943 let manifest = crate::manifest::find_manifest(name)?;
944 if self.incomplete_pull_blocks_manifest(manifest) {
945 return None;
946 }
947 let models_dir = self.resolved_models_dir();
948 let downloads = manifest
949 .files
950 .iter()
951 .map(|file| {
952 let local = crate::manifest::storage_path_candidates(manifest, file)
955 .into_iter()
956 .map(|path| models_dir.join(path))
957 .find(|path| Self::file_is_complete(path, file.size_bytes));
962 if let Some(path) = local {
963 return Some((file.component, path));
964 }
965 if manifest.family != "companion" {
979 return None;
980 }
981 crate::download::cached_file_path_in_mold_cache(&file.hf_repo, &file.hf_filename)
982 .map(|path| (file.component, path))
983 })
984 .collect::<Option<Vec<_>>>()?;
985 crate::manifest::paths_from_downloads(&downloads, &manifest.family)
986 }
987
988 pub fn manifest_model_is_downloaded(&self, name: &str) -> bool {
989 let manifest = match crate::manifest::find_manifest(name) {
990 Some(m) => m,
991 None => return false,
992 };
993 if self.incomplete_pull_blocks_manifest(manifest) {
994 return false;
995 }
996 if manifest.is_upscaler() || manifest.is_utility() {
999 return self.manifest_files_exist(manifest);
1000 }
1001 self.resolved_local_manifest_model_config(name).is_some()
1002 }
1003
1004 pub fn manifest_model_needs_download(&self, name: &str) -> bool {
1007 let canonical = crate::manifest::resolve_model_name(name);
1008 crate::manifest::find_manifest(&canonical).is_some()
1009 && !self.manifest_model_is_downloaded(&canonical)
1010 }
1011
1012 fn manifest_files_exist(&self, manifest: &crate::manifest::ModelManifest) -> bool {
1029 let models_dir = self.resolved_models_dir();
1030 manifest.files.iter().all(|file| {
1031 crate::manifest::storage_path_candidates(manifest, file)
1032 .into_iter()
1033 .map(|path| models_dir.join(path))
1034 .any(|path| Self::file_is_complete(&path, file.size_bytes))
1035 })
1036 }
1037
1038 fn file_is_complete(path: &std::path::Path, expected_size: u64) -> bool {
1042 if !path.exists() {
1043 return false;
1044 }
1045 if crate::download::has_sha256_marker(path) {
1046 return true;
1047 }
1048 match path.metadata() {
1051 Ok(meta) => meta.len() == expected_size,
1052 Err(_) => false,
1053 }
1054 }
1055
1056 fn incomplete_pull_blocks_manifest(&self, manifest: &crate::manifest::ModelManifest) -> bool {
1063 let marker_path =
1064 crate::download::pulling_marker_path_in(&self.resolved_models_dir(), &manifest.name);
1065 if !marker_path.exists() {
1066 return false;
1067 }
1068
1069 if self.manifest_files_exist(manifest) {
1070 let _ = std::fs::remove_file(&marker_path);
1071 return false;
1072 }
1073
1074 true
1075 }
1076
1077 pub fn model_config(&self, name: &str) -> ModelConfig {
1080 let mut cfg = self.lookup_model_config(name).unwrap_or_default();
1081
1082 if let Some(discovered) = self.resolved_local_manifest_model_config(name) {
1083 overlay_model_paths(&mut cfg, &discovered);
1084 if cfg.description.is_none() {
1085 cfg.description = discovered.description;
1086 }
1087 if cfg.family.is_none() {
1088 cfg.family = discovered.family;
1089 }
1090 }
1091
1092 cfg
1093 }
1094
1095 pub fn resolved_model_config(&self, name: &str) -> ModelConfig {
1097 let mut cfg = self.model_config(name);
1098
1099 if let Some(manifest) = crate::manifest::find_manifest(name) {
1100 if cfg.default_steps.is_none() {
1105 cfg.default_steps = Some(manifest.defaults.steps);
1106 }
1107 if cfg.default_guidance.is_none() {
1108 cfg.default_guidance = Some(manifest.defaults.guidance);
1109 }
1110 if cfg.default_width.is_none() {
1111 cfg.default_width = Some(manifest.defaults.width);
1112 }
1113 if cfg.default_height.is_none() {
1114 cfg.default_height = Some(manifest.defaults.height);
1115 }
1116 if cfg.is_schnell.is_none() {
1117 cfg.is_schnell = Some(manifest.defaults.is_schnell);
1118 }
1119 if cfg.scheduler.is_none() {
1120 cfg.scheduler = manifest.defaults.scheduler;
1121 }
1122 if cfg.negative_prompt.is_none() {
1123 cfg.negative_prompt = manifest.defaults.negative_prompt.clone();
1124 }
1125 if cfg.default_frames.is_none() {
1126 cfg.default_frames = manifest.defaults.frames;
1127 }
1128 if cfg.default_fps.is_none() {
1129 cfg.default_fps = manifest.defaults.fps;
1130 }
1131 cfg.description = Some(manifest.description.clone());
1134 cfg.family = Some(manifest.family.clone());
1135 }
1136
1137 cfg
1138 }
1139
1140 pub fn upsert_model(&mut self, name: String, config: ModelConfig) {
1142 self.models.insert(name, config);
1143 }
1144
1145 pub fn remove_model(&mut self, name: &str) -> Option<ModelConfig> {
1147 self.models.remove(name)
1148 }
1149
1150 pub fn resolved_placement(&self, model_name: &str) -> Option<crate::types::DevicePlacement> {
1165 use crate::types::DevicePlacement;
1166
1167 let mut placement = self
1168 .lookup_model_config(model_name)
1169 .and_then(|mc| mc.placement);
1170
1171 let env_tier1 = parse_device_ref_env("MOLD_PLACE_TEXT_ENCODERS");
1172 let env_transformer = parse_device_ref_env("MOLD_PLACE_TRANSFORMER");
1173 let env_vae = parse_device_ref_env("MOLD_PLACE_VAE");
1174 let env_t5 = parse_device_ref_env("MOLD_PLACE_T5");
1175 let env_clip_l = parse_device_ref_env("MOLD_PLACE_CLIP_L");
1176 let env_clip_g = parse_device_ref_env("MOLD_PLACE_CLIP_G");
1177 let env_qwen = parse_device_ref_env("MOLD_PLACE_QWEN");
1178
1179 let any_env = env_tier1.is_some()
1180 || env_transformer.is_some()
1181 || env_vae.is_some()
1182 || env_t5.is_some()
1183 || env_clip_l.is_some()
1184 || env_clip_g.is_some()
1185 || env_qwen.is_some();
1186
1187 if !any_env {
1188 return placement;
1189 }
1190
1191 let mut effective: DevicePlacement = placement.unwrap_or_default();
1192 if let Some(r) = env_tier1 {
1193 effective.text_encoders = r;
1194 }
1195 let any_advanced = env_transformer.is_some()
1196 || env_vae.is_some()
1197 || env_t5.is_some()
1198 || env_clip_l.is_some()
1199 || env_clip_g.is_some()
1200 || env_qwen.is_some();
1201 if any_advanced {
1202 let mut adv = effective.advanced.unwrap_or_default();
1203 if let Some(r) = env_transformer {
1204 adv.transformer = r;
1205 }
1206 if let Some(r) = env_vae {
1207 adv.vae = r;
1208 }
1209 if let Some(r) = env_t5 {
1210 adv.t5 = Some(r);
1211 }
1212 if let Some(r) = env_clip_l {
1213 adv.clip_l = Some(r);
1214 }
1215 if let Some(r) = env_clip_g {
1216 adv.clip_g = Some(r);
1217 }
1218 if let Some(r) = env_qwen {
1219 adv.qwen = Some(r);
1220 }
1221 effective.advanced = Some(adv);
1222 }
1223 placement = Some(effective);
1224 placement
1225 }
1226
1227 pub fn set_model_placement(
1231 &mut self,
1232 model_name: &str,
1233 placement: Option<crate::types::DevicePlacement>,
1234 ) {
1235 let entry = self.models.entry(model_name.to_string()).or_default();
1236 entry.placement = placement;
1237 }
1238
1239 pub fn save(&self) -> anyhow::Result<()> {
1244 let path = Self::config_path()
1245 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory for config path"))?;
1246
1247 let path_str = path.to_string_lossy();
1252 let is_temp_config = path_str.contains("/tmp/") || path_str.contains("/mold-config-test-");
1253 let has_temp_models_dir = self.models_dir.contains("/tmp/mold-")
1254 || self.models_dir.contains("/mold-config-test-");
1255 if has_temp_models_dir && !is_temp_config {
1256 eprintln!(
1257 "warning: refusing to save config with test models_dir ({}) to real config ({})",
1258 self.models_dir,
1259 path.display()
1260 );
1261 return Ok(());
1262 }
1263
1264 if let Some(parent) = path.parent() {
1265 std::fs::create_dir_all(parent)?;
1266 }
1267 let contents = toml::to_string_pretty(self)?;
1268 std::fs::write(&path, contents)?;
1269 Ok(())
1270 }
1271
1272 pub fn save_bootstrap_only_to(&self, path: &std::path::Path) -> anyhow::Result<()> {
1281 if let Some(parent) = path.parent() {
1282 std::fs::create_dir_all(parent)?;
1283 }
1284 let mut doc = toml::Value::try_from(self)?;
1285 strip_user_pref_fields(&mut doc);
1286 let body = toml::to_string_pretty(&doc)?;
1287 let contents = format!("{BOOTSTRAP_ONLY_BANNER}{body}");
1288 std::fs::write(path, contents)?;
1289 Ok(())
1290 }
1291
1292 pub fn save_bootstrap_only(&self) -> anyhow::Result<()> {
1294 let path = Self::config_path()
1295 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory for config path"))?;
1296 self.save_bootstrap_only_to(&path)
1297 }
1298
1299 pub fn exists_on_disk() -> bool {
1301 Self::config_path().is_some_and(|p| p.exists())
1302 }
1303
1304 pub fn lookup_model_config(&self, name: &str) -> Option<ModelConfig> {
1307 if let Some(cfg) = self.models.get(name) {
1308 return Some(cfg.clone());
1309 }
1310 let canonical = resolve_model_name(name);
1311 if canonical != name {
1312 return self.models.get(&canonical).cloned();
1313 }
1314 None
1315 }
1316
1317 fn discovered_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
1318 let manifest = crate::manifest::find_manifest(name)?;
1319 let paths = self.discovered_manifest_paths(name)?;
1320 Some(manifest.to_model_config(&paths))
1321 }
1322
1323 fn resolved_local_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
1324 let manifest = crate::manifest::find_manifest(name)?;
1325 let paths = if let Some(paths) = self.discovered_manifest_paths(name) {
1326 paths
1327 } else {
1328 let paths = ModelPaths::resolve(name, self)?;
1329 if !resolved_manifest_paths_exist(manifest, &paths) {
1330 return None;
1331 }
1332 paths
1333 };
1334 Some(manifest.to_model_config(&paths))
1335 }
1336}
1337
1338fn overlay_model_paths(target: &mut ModelConfig, source: &ModelConfig) {
1339 target.transformer = source.transformer.clone();
1340 target.transformer_shards = source.transformer_shards.clone();
1341 target.vae = source.vae.clone();
1342 if source.spatial_upscaler.is_some() {
1343 target.spatial_upscaler = source.spatial_upscaler.clone();
1344 }
1345 if source.temporal_upscaler.is_some() {
1346 target.temporal_upscaler = source.temporal_upscaler.clone();
1347 }
1348 if source.distilled_lora.is_some() {
1349 target.distilled_lora = source.distilled_lora.clone();
1350 }
1351
1352 if source.t5_encoder.is_some() {
1353 target.t5_encoder = source.t5_encoder.clone();
1354 }
1355 if source.clip_encoder.is_some() {
1356 target.clip_encoder = source.clip_encoder.clone();
1357 }
1358 if source.t5_tokenizer.is_some() {
1359 target.t5_tokenizer = source.t5_tokenizer.clone();
1360 }
1361 if source.clip_tokenizer.is_some() {
1362 target.clip_tokenizer = source.clip_tokenizer.clone();
1363 }
1364 if source.clip_encoder_2.is_some() {
1365 target.clip_encoder_2 = source.clip_encoder_2.clone();
1366 }
1367 if source.clip_tokenizer_2.is_some() {
1368 target.clip_tokenizer_2 = source.clip_tokenizer_2.clone();
1369 }
1370 if source.text_encoder_files.is_some() {
1371 target.text_encoder_files = source.text_encoder_files.clone();
1372 }
1373 if source.text_tokenizer.is_some() {
1374 target.text_tokenizer = source.text_tokenizer.clone();
1375 }
1376 if source.decoder.is_some() {
1377 target.decoder = source.decoder.clone();
1378 }
1379}
1380
1381fn resolved_manifest_paths_exist(
1382 manifest: &crate::manifest::ModelManifest,
1383 paths: &ModelPaths,
1384) -> bool {
1385 use crate::manifest::ModelComponent;
1386
1387 let mut transformer_shard_idx = 0usize;
1388 let mut text_encoder_idx = 0usize;
1389
1390 manifest.files.iter().all(|file| match file.component {
1391 ModelComponent::Transformer => paths.transformer.exists(),
1392 ModelComponent::TransformerShard => {
1393 let path = paths.transformer_shards.get(transformer_shard_idx);
1394 transformer_shard_idx += 1;
1395 path.is_some_and(|path| path.exists())
1396 }
1397 ModelComponent::Vae => paths.vae.exists(),
1398 ModelComponent::SpatialUpscaler => paths
1399 .spatial_upscaler
1400 .as_ref()
1401 .is_some_and(|path| path.exists()),
1402 ModelComponent::TemporalUpscaler => paths
1403 .temporal_upscaler
1404 .as_ref()
1405 .is_some_and(|path| path.exists()),
1406 ModelComponent::DistilledLora => paths
1407 .distilled_lora
1408 .as_ref()
1409 .is_some_and(|path| path.exists()),
1410 ModelComponent::T5Encoder => paths.t5_encoder.as_ref().is_some_and(|path| path.exists()),
1411 ModelComponent::ClipEncoder => paths
1412 .clip_encoder
1413 .as_ref()
1414 .is_some_and(|path| path.exists()),
1415 ModelComponent::T5Tokenizer => paths
1416 .t5_tokenizer
1417 .as_ref()
1418 .is_some_and(|path| path.exists()),
1419 ModelComponent::ClipTokenizer => paths
1420 .clip_tokenizer
1421 .as_ref()
1422 .is_some_and(|path| path.exists()),
1423 ModelComponent::ClipEncoder2 => paths
1424 .clip_encoder_2
1425 .as_ref()
1426 .is_some_and(|path| path.exists()),
1427 ModelComponent::ClipTokenizer2 => paths
1428 .clip_tokenizer_2
1429 .as_ref()
1430 .is_some_and(|path| path.exists()),
1431 ModelComponent::TextEncoder => {
1432 let path = paths.text_encoder_files.get(text_encoder_idx);
1433 text_encoder_idx += 1;
1434 path.is_some_and(|path| path.exists())
1435 }
1436 ModelComponent::TextTokenizer => paths
1437 .text_tokenizer
1438 .as_ref()
1439 .is_some_and(|path| path.exists()),
1440 ModelComponent::Decoder => paths.decoder.as_ref().is_some_and(|path| path.exists()),
1441 ModelComponent::Upscaler => paths.transformer.exists(),
1442 })
1443}
1444
1445pub fn parse_device_ref_str(raw: &str) -> Result<crate::types::DeviceRef, String> {
1450 use crate::types::DeviceRef;
1451 let raw = raw.trim().to_lowercase();
1452 if raw == "auto" {
1453 Ok(DeviceRef::Auto)
1454 } else if raw == "cpu" {
1455 Ok(DeviceRef::Cpu)
1456 } else if raw == "gpu" {
1457 Ok(DeviceRef::gpu(0))
1458 } else if let Some(rest) = raw.strip_prefix("gpu:") {
1459 rest.parse::<usize>()
1460 .map(DeviceRef::gpu)
1461 .map_err(|_| format!("invalid device '{raw}' (expected auto|cpu|gpu[:N])"))
1462 } else {
1463 Err(format!(
1464 "invalid device '{raw}' (expected auto|cpu|gpu[:N])"
1465 ))
1466 }
1467}
1468
1469fn parse_device_ref_env(key: &str) -> Option<crate::types::DeviceRef> {
1470 let raw = std::env::var(key).ok()?;
1471 match parse_device_ref_str(&raw) {
1472 Ok(dr) => Some(dr),
1473 Err(msg) => {
1474 eprintln!("mold: ignoring {key}={raw}: {msg}");
1475 None
1476 }
1477 }
1478}