Skip to main content

mold_core/
ltx2_control.rs

1use serde::{Deserialize, Serialize};
2
3use crate::config::ModelConfig;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum Ltx2ControlProfile {
7    Ltx2_19bDistilled,
8    Ltx2_23_22bDistilled,
9}
10
11impl Ltx2ControlProfile {
12    pub fn label(self) -> &'static str {
13        match self {
14            Self::Ltx2_19bDistilled => "LTX-2 19B distilled",
15            Self::Ltx2_23_22bDistilled => "LTX-2.3 22B distilled",
16        }
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Ltx2ControlAdapter {
22    pub id: &'static str,
23    pub label: &'static str,
24    pub guide: &'static str,
25    pub profile: Ltx2ControlProfile,
26    pub hf_repo: &'static str,
27    pub hf_filename: &'static str,
28    pub size_bytes: u64,
29    pub sha256: &'static str,
30    pub download_model: &'static str,
31    /// Whether Hugging Face requires an accepted licence before serving this
32    /// repository. Surfaced to clients so they can say "accept the licence and
33    /// set `HF_TOKEN`" *before* a download fails with a 403.
34    pub gated: bool,
35    /// Extra files the adapter cannot run without, beyond `hf_filename`.
36    ///
37    /// The HDR adapter ships pre-computed prompt embeddings alongside its
38    /// weights, so an adapter is not always exactly one file.
39    pub extra_files: &'static [Ltx2ControlAdapterFile],
40}
41
42/// A companion file an adapter needs on disk alongside its weights.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Ltx2ControlAdapterFile {
45    pub hf_filename: &'static str,
46    pub size_bytes: u64,
47    pub sha256: &'static str,
48}
49
50impl Ltx2ControlAdapter {
51    /// Every file this adapter needs, weights first.
52    pub fn files(&self) -> impl Iterator<Item = Ltx2ControlAdapterFile> + '_ {
53        std::iter::once(Ltx2ControlAdapterFile {
54            hf_filename: self.hf_filename,
55            size_bytes: self.size_bytes,
56            sha256: self.sha256,
57        })
58        .chain(self.extra_files.iter().copied())
59    }
60
61    /// Total bytes to download, across every file.
62    pub fn total_size_bytes(&self) -> u64 {
63        self.files().map(|file| file.size_bytes).sum()
64    }
65
66    /// The adapter's pre-computed text-embedding companion, if it ships one.
67    ///
68    /// Upstream's HDR pipeline takes these embeddings instead of encoding a
69    /// prompt, so the engine needs to find the file by name rather than
70    /// hardcoding it. Returns `None` for every adapter that encodes normally.
71    pub fn scene_embeddings_filename(&self) -> Option<&'static str> {
72        self.extra_files
73            .iter()
74            .map(|file| file.hf_filename)
75            .find(|name| name.contains("scene-emb"))
76    }
77}
78
79pub const LTX2_CONTROL_ADAPTERS: &[Ltx2ControlAdapter] = &[
80    Ltx2ControlAdapter {
81        id: "union",
82        label: "Union control",
83        guide: "A frame-aligned Canny, depth, or pose guide video.",
84        profile: Ltx2ControlProfile::Ltx2_19bDistilled,
85        hf_repo: "Lightricks/LTX-2-19b-IC-LoRA-Union-Control",
86        hf_filename: "ltx-2-19b-ic-lora-union-control-ref0.5.safetensors",
87        size_bytes: 654_465_296,
88        sha256: "cd342e0dcf7754d8b36135b5e768b0f0e820703acd372adc49e53de0b00a931b",
89        download_model: "ltx2-control-union-19b",
90        gated: false,
91        extra_files: &[],
92    },
93    Ltx2ControlAdapter {
94        id: "pose",
95        label: "Pose control",
96        guide: "A frame-aligned pose or OpenPose guide video.",
97        profile: Ltx2ControlProfile::Ltx2_19bDistilled,
98        hf_repo: "Lightricks/LTX-2-19b-IC-LoRA-Pose-Control",
99        hf_filename: "ltx-2-19b-ic-lora-pose-control.safetensors",
100        size_bytes: 654_465_256,
101        sha256: "61816bf0985d4470c456160deec65df69188ae45d553e5aa8f1252fc543bc8aa",
102        download_model: "ltx2-control-pose-19b",
103        gated: false,
104        extra_files: &[],
105    },
106    Ltx2ControlAdapter {
107        id: "detailer",
108        label: "Detailer",
109        guide: "The source video whose details and textures should be refined.",
110        profile: Ltx2ControlProfile::Ltx2_19bDistilled,
111        hf_repo: "Lightricks/LTX-2-19b-IC-LoRA-Detailer",
112        hf_filename: "ltx-2-19b-ic-lora-detailer.safetensors",
113        size_bytes: 2_617_401_920,
114        sha256: "05efdae9e472e06d168e122f5ebb890e7ef348cc047cf9876da6504c36d7d0e2",
115        download_model: "ltx2-control-detailer-19b",
116        gated: false,
117        extra_files: &[],
118    },
119    Ltx2ControlAdapter {
120        id: "union",
121        label: "Union control",
122        guide: "A frame-aligned Canny, depth, or pose guide video.",
123        profile: Ltx2ControlProfile::Ltx2_23_22bDistilled,
124        hf_repo: "Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control",
125        hf_filename: "ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
126        size_bytes: 654_465_352,
127        sha256: "a1b888a87f661d27f08b394ae559e8e1050be33900bcc36a5cdf659e48f88d18",
128        download_model: "ltx2-control-union-23",
129        gated: false,
130        extra_files: &[],
131    },
132    Ltx2ControlAdapter {
133        id: "motion-track",
134        label: "Motion track",
135        guide: "A video with colored spline overlays marking the desired trajectories.",
136        profile: Ltx2ControlProfile::Ltx2_23_22bDistilled,
137        hf_repo: "Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control",
138        hf_filename: "ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
139        size_bytes: 327_309_314,
140        sha256: "e279807ee3aa3db1ce60188d665ff83342860367dcd6bac19f8bd5a99a9e1dca",
141        download_model: "ltx2-control-motion-track-23",
142        gated: false,
143        extra_files: &[],
144    },
145    Ltx2ControlAdapter {
146        id: "lipdub",
147        label: "Lip dub",
148        guide: "A reference video with speech; the mouth is re-timed to new audio.",
149        profile: Ltx2ControlProfile::Ltx2_23_22bDistilled,
150        // Upstream's README still names the repository LTX-2.3-22b-IC-LoRA-LipDub,
151        // which now 307-redirects to DubIt. Pin the destination so the download
152        // does not depend on a redirect being honoured.
153        hf_repo: "Lightricks/LTX-2.3-22b-IC-LoRA-DubIt",
154        hf_filename: "ltx-2.3-22b-ic-lora-dubit-0.9.safetensors",
155        size_bytes: 2_466_665_072,
156        sha256: "fc415b12cb639e78511bc264f85080c2f7b188e334c1d9fade76b310e2bc419c",
157        download_model: "ltx2-control-lipdub-23",
158        gated: true,
159        extra_files: &[],
160    },
161    Ltx2ControlAdapter {
162        id: "hdr",
163        label: "HDR",
164        guide: "An SDR reference video; the render is re-graded to linear HDR.",
165        profile: Ltx2ControlProfile::Ltx2_23_22bDistilled,
166        hf_repo: "Lightricks/LTX-2.3-22b-IC-LoRA-HDR",
167        hf_filename: "ltx-2.3-22b-ic-lora-hdr-0.9.safetensors",
168        size_bytes: 327_309_312,
169        sha256: "c56bfa0f2e4461a8b2f318f494c61c5bf97f462f2220e31ece93ea7851ca871e",
170        download_model: "ltx2-control-hdr-23",
171        gated: true,
172        // The HDR pipeline runs from pre-computed prompt embeddings rather
173        // than encoding a prompt, so this file is not optional.
174        extra_files: &[Ltx2ControlAdapterFile {
175            hf_filename: "ltx-2.3-22b-ic-lora-hdr-scene-emb.safetensors",
176            size_bytes: 12_583_096,
177            sha256: "78bffa6049bae2649a4365ec8769db88052c21348d643e8fc1ce6d483d994c5b",
178        }],
179    },
180];
181
182/// The one adapter whose pipeline is not the generic in-context one.
183///
184/// Lip dub is a whole pipeline, not just a LoRA: the adapter stays loaded for
185/// both denoise stages, the reference clip's audio is appended as negatively
186/// positioned reference tokens, and stage 2's audio is frozen. Routing it
187/// through `ic-lora` would load the weights and then run the wrong graph.
188pub const LIP_DUB_CONTROL_ID: &str = "lipdub";
189
190/// Which LTX-2 pipeline a first-party control adapter id implies.
191pub fn pipeline_for_control_id(id: &str) -> crate::Ltx2PipelineMode {
192    if normalize_control_id(id) == LIP_DUB_CONTROL_ID {
193        crate::Ltx2PipelineMode::LipDub
194    } else {
195        crate::Ltx2PipelineMode::IcLora
196    }
197}
198
199pub fn normalize_control_id(value: &str) -> String {
200    value.trim().to_ascii_lowercase().replace('_', "-")
201}
202
203pub fn adapters_for_profile(
204    profile: Ltx2ControlProfile,
205) -> impl Iterator<Item = &'static Ltx2ControlAdapter> {
206    LTX2_CONTROL_ADAPTERS
207        .iter()
208        .filter(move |adapter| adapter.profile == profile)
209}
210
211pub fn resolve_control_adapter(
212    profile: Ltx2ControlProfile,
213    id: &str,
214) -> Result<&'static Ltx2ControlAdapter, String> {
215    let id = normalize_control_id(id);
216    adapters_for_profile(profile)
217        .find(|adapter| adapter.id == id)
218        .ok_or_else(|| {
219            let valid = adapters_for_profile(profile)
220                .map(|adapter| adapter.id)
221                .collect::<Vec<_>>()
222                .join(", ");
223            format!(
224                "IC-LoRA control '{id}' is not compatible with {}; choose one of: {valid}",
225                profile.label()
226            )
227        })
228}
229
230/// Resolve the built-in control profile from the effective runtime model
231/// configuration. This deliberately does not inspect the public model ID:
232/// catalog models use opaque `cv:` / `hf:` identifiers, while the resolved
233/// configuration retains the checkpoint architecture and distilled schedule.
234pub fn control_profile_for_config(config: &ModelConfig) -> Result<Ltx2ControlProfile, String> {
235    if config.family.as_deref() != Some("ltx2") {
236        return Err("IC-LoRA controls require an LTX-2 model".to_string());
237    }
238    if config.is_schnell != Some(true) {
239        return Err(
240            "built-in IC-LoRA controls require an effective distilled LTX-2 profile".to_string(),
241        );
242    }
243
244    let architecture_paths = [
245        config.transformer.as_deref(),
246        config.vae.as_deref(),
247        config.spatial_upscaler.as_deref(),
248    ];
249    if architecture_paths
250        .iter()
251        .flatten()
252        .any(|path| path.contains("ltx-2.3"))
253    {
254        return Ok(Ltx2ControlProfile::Ltx2_23_22bDistilled);
255    }
256    if architecture_paths
257        .iter()
258        .flatten()
259        .any(|path| path.contains("ltx-2"))
260    {
261        return Ok(Ltx2ControlProfile::Ltx2_19bDistilled);
262    }
263    Err(
264        "the installed checkpoint architecture is unknown; select an LTX-2 19B distilled or LTX-2.3 22B distilled profile"
265            .to_string(),
266    )
267}
268
269/// Resolve built-in manifests through an explicit profile table, then fall
270/// back to the effective runtime config for custom and opaque catalog IDs.
271/// The public model ID is never pattern-matched.
272pub fn control_profile_for_model(
273    model: &str,
274    config: &ModelConfig,
275) -> Result<Ltx2ControlProfile, String> {
276    let canonical = crate::manifest::resolve_model_name(model);
277    let manifest_profile = match canonical.as_str() {
278        "ltx-2-19b-distilled:fp8" => Some(Ltx2ControlProfile::Ltx2_19bDistilled),
279        "ltx-2.3-22b-distilled:fp8" | "ltx-2.3-22b-distilled:bf16" => {
280            Some(Ltx2ControlProfile::Ltx2_23_22bDistilled)
281        }
282        _ => None,
283    };
284    if let Some(profile) = manifest_profile {
285        if config.family.as_deref() == Some("ltx2") {
286            return Ok(profile);
287        }
288    }
289    control_profile_for_config(config)
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
293pub struct Ltx2ControlAdapterInfo {
294    pub id: String,
295    pub label: String,
296    pub guide: String,
297    /// Total bytes across every file the adapter needs, not just its weights.
298    pub size_bytes: u64,
299    pub installed: bool,
300    pub download_model: String,
301    pub download_repo: String,
302    pub download_filename: String,
303    pub download_sha256: String,
304    /// Whether Hugging Face requires an accepted licence first. Additive:
305    /// absent on servers that predate gated adapters, which clients read as
306    /// "not gated" — the same answer those servers would have given.
307    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
308    pub gated: bool,
309}
310
311#[cfg(test)]
312mod tests {
313    use std::collections::HashSet;
314
315    use super::*;
316
317    /// Only lip dub gets its own pipeline, and only under its exact id.
318    ///
319    /// The whole registry defaults to `ic-lora`; if a future adapter needs its
320    /// own graph this is the one place that has to learn about it, on every
321    /// surface at once.
322    #[test]
323    fn only_the_lip_dub_adapter_selects_a_pipeline_other_than_ic_lora() {
324        for adapter in LTX2_CONTROL_ADAPTERS {
325            let expected = if adapter.id == LIP_DUB_CONTROL_ID {
326                crate::Ltx2PipelineMode::LipDub
327            } else {
328                crate::Ltx2PipelineMode::IcLora
329            };
330            assert_eq!(
331                pipeline_for_control_id(adapter.id),
332                expected,
333                "adapter '{}' routes to the wrong pipeline",
334                adapter.id
335            );
336        }
337        assert!(LTX2_CONTROL_ADAPTERS
338            .iter()
339            .any(|adapter| adapter.id == LIP_DUB_CONTROL_ID));
340    }
341
342    /// The Studio surfaces reimplement this routing in TypeScript because they
343    /// have to pick the pipeline before any request is made. A mirror that
344    /// drifts sends `ic-lora` with the lip-dub adapter, which the server 422s.
345    #[test]
346    fn ltx2_control_pipeline_ts_mirror_matches_the_rust_registry() {
347        let workspace = env!("CARGO_MANIFEST_DIR")
348            .strip_suffix("/crates/mold-core")
349            .or_else(|| env!("CARGO_MANIFEST_DIR").strip_suffix("crates/mold-core"))
350            .unwrap_or(env!("CARGO_MANIFEST_DIR"));
351        let path = format!("{workspace}/studio/lib/ltx2Control.ts");
352        let source =
353            std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
354
355        assert!(
356            source.contains(&format!(
357                "export const LIP_DUB_CONTROL_ID = \"{LIP_DUB_CONTROL_ID}\";"
358            )),
359            "studio/lib/ltx2Control.ts must pin LIP_DUB_CONTROL_ID to `{LIP_DUB_CONTROL_ID}`"
360        );
361        assert!(
362            source.contains(&format!(
363                "? \"{}\"\n    : \"{}\";",
364                crate::Ltx2PipelineMode::LipDub,
365                crate::Ltx2PipelineMode::IcLora
366            )) || source.contains(&format!(
367                "? \"{}\" : \"{}\";",
368                crate::Ltx2PipelineMode::LipDub,
369                crate::Ltx2PipelineMode::IcLora
370            )),
371            "studio/lib/ltx2Control.ts must return the wire pipeline strings \
372             `{}` / `{}`",
373            crate::Ltx2PipelineMode::LipDub,
374            crate::Ltx2PipelineMode::IcLora
375        );
376    }
377
378    #[test]
379    fn registry_has_unique_profile_and_id_pairs() {
380        let mut seen = HashSet::new();
381        for adapter in LTX2_CONTROL_ADAPTERS {
382            assert!(seen.insert((adapter.profile, adapter.id)));
383            assert_eq!(adapter.sha256.len(), 64);
384            assert!(adapter.hf_filename.ends_with(".safetensors"));
385        }
386        assert_eq!(seen.len(), 7);
387    }
388
389    #[test]
390    fn official_artifact_identities_are_exact() {
391        let actual = LTX2_CONTROL_ADAPTERS
392            .iter()
393            .map(|adapter| {
394                (
395                    adapter.hf_repo,
396                    adapter.hf_filename,
397                    adapter.size_bytes,
398                    adapter.sha256,
399                )
400            })
401            .collect::<Vec<_>>();
402        assert_eq!(
403            actual,
404            vec![
405                (
406                    "Lightricks/LTX-2-19b-IC-LoRA-Union-Control",
407                    "ltx-2-19b-ic-lora-union-control-ref0.5.safetensors",
408                    654_465_296,
409                    "cd342e0dcf7754d8b36135b5e768b0f0e820703acd372adc49e53de0b00a931b",
410                ),
411                (
412                    "Lightricks/LTX-2-19b-IC-LoRA-Pose-Control",
413                    "ltx-2-19b-ic-lora-pose-control.safetensors",
414                    654_465_256,
415                    "61816bf0985d4470c456160deec65df69188ae45d553e5aa8f1252fc543bc8aa",
416                ),
417                (
418                    "Lightricks/LTX-2-19b-IC-LoRA-Detailer",
419                    "ltx-2-19b-ic-lora-detailer.safetensors",
420                    2_617_401_920,
421                    "05efdae9e472e06d168e122f5ebb890e7ef348cc047cf9876da6504c36d7d0e2",
422                ),
423                (
424                    "Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control",
425                    "ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
426                    654_465_352,
427                    "a1b888a87f661d27f08b394ae559e8e1050be33900bcc36a5cdf659e48f88d18",
428                ),
429                (
430                    "Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control",
431                    "ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
432                    327_309_314,
433                    "e279807ee3aa3db1ce60188d665ff83342860367dcd6bac19f8bd5a99a9e1dca",
434                ),
435                (
436                    "Lightricks/LTX-2.3-22b-IC-LoRA-DubIt",
437                    "ltx-2.3-22b-ic-lora-dubit-0.9.safetensors",
438                    2_466_665_072,
439                    "fc415b12cb639e78511bc264f85080c2f7b188e334c1d9fade76b310e2bc419c",
440                ),
441                (
442                    "Lightricks/LTX-2.3-22b-IC-LoRA-HDR",
443                    "ltx-2.3-22b-ic-lora-hdr-0.9.safetensors",
444                    327_309_312,
445                    "c56bfa0f2e4461a8b2f318f494c61c5bf97f462f2220e31ece93ea7851ca871e",
446                ),
447            ]
448        );
449    }
450
451    /// A multi-file adapter must report every file it needs, and its
452    /// advertised size must be the whole download — quoting only the weights
453    /// would understate the HDR adapter by its 12 MB embeddings file.
454    #[test]
455    fn adapters_enumerate_every_file_they_need() {
456        let hdr = LTX2_CONTROL_ADAPTERS
457            .iter()
458            .find(|a| a.id == "hdr")
459            .expect("hdr adapter is registered");
460        let files: Vec<_> = hdr.files().collect();
461        assert_eq!(files.len(), 2, "HDR ships weights plus scene embeddings");
462        assert_eq!(files[0].hf_filename, hdr.hf_filename, "weights come first");
463        assert!(files[1].hf_filename.contains("scene-emb"));
464        assert_eq!(
465            hdr.total_size_bytes(),
466            files.iter().map(|f| f.size_bytes).sum::<u64>()
467        );
468        assert!(hdr.total_size_bytes() > hdr.size_bytes);
469
470        // Single-file adapters are unchanged.
471        for adapter in LTX2_CONTROL_ADAPTERS.iter().filter(|a| a.id != "hdr") {
472            assert_eq!(adapter.files().count(), 1, "{}", adapter.id);
473            assert_eq!(adapter.total_size_bytes(), adapter.size_bytes);
474        }
475    }
476
477    /// Gating is per-adapter, and the generated manifests must carry it — a
478    /// gated file that claims otherwise fails late with an opaque 403 instead
479    /// of the "accept the licence, set HF_TOKEN" guidance the downloader has.
480    #[test]
481    fn gated_adapters_propagate_to_their_manifests() {
482        for adapter in LTX2_CONTROL_ADAPTERS {
483            let manifest = crate::manifest::find_manifest(adapter.download_model)
484                .unwrap_or_else(|| panic!("{} has no manifest", adapter.id));
485            assert_eq!(
486                manifest.files.len(),
487                adapter.files().count(),
488                "{} manifest file count",
489                adapter.id
490            );
491            for file in &manifest.files {
492                assert_eq!(file.gated, adapter.gated, "{} gating", adapter.id);
493            }
494        }
495        let gated: Vec<_> = LTX2_CONTROL_ADAPTERS
496            .iter()
497            .filter(|a| a.gated)
498            .map(|a| a.id)
499            .collect();
500        assert_eq!(gated, ["lipdub", "hdr"]);
501    }
502
503    /// The 2.3 adapters are for the 22B distilled profile; offering one on a
504    /// 19B checkpoint would download gigabytes that cannot load.
505    #[test]
506    fn new_adapters_resolve_only_for_the_22b_profile() {
507        for id in ["lipdub", "hdr"] {
508            assert!(resolve_control_adapter(Ltx2ControlProfile::Ltx2_23_22bDistilled, id).is_ok());
509            assert!(resolve_control_adapter(Ltx2ControlProfile::Ltx2_19bDistilled, id).is_err());
510        }
511    }
512
513    #[test]
514    fn compatibility_matrix_is_exact() {
515        let controls_19b = adapters_for_profile(Ltx2ControlProfile::Ltx2_19bDistilled)
516            .map(|adapter| adapter.id)
517            .collect::<Vec<_>>();
518        assert_eq!(controls_19b, ["union", "pose", "detailer"]);
519
520        let controls_23 = adapters_for_profile(Ltx2ControlProfile::Ltx2_23_22bDistilled)
521            .map(|adapter| adapter.id)
522            .collect::<Vec<_>>();
523        assert_eq!(controls_23, ["union", "motion-track", "lipdub", "hdr"]);
524
525        for id in ["union", "motion-track", "pose", "detailer"] {
526            assert_eq!(
527                resolve_control_adapter(Ltx2ControlProfile::Ltx2_19bDistilled, id).is_ok(),
528                matches!(id, "union" | "pose" | "detailer"),
529                "unexpected LTX-2 19B compatibility for {id}"
530            );
531            assert_eq!(
532                resolve_control_adapter(Ltx2ControlProfile::Ltx2_23_22bDistilled, id).is_ok(),
533                matches!(id, "union" | "motion-track"),
534                "unexpected LTX-2.3 compatibility for {id}"
535            );
536        }
537    }
538
539    #[test]
540    fn ids_are_normalized_without_guessing_aliases() {
541        assert_eq!(normalize_control_id(" Motion_Track "), "motion-track");
542        assert!(
543            resolve_control_adapter(Ltx2ControlProfile::Ltx2_23_22bDistilled, "MOTION_TRACK")
544                .is_ok()
545        );
546        assert!(
547            resolve_control_adapter(Ltx2ControlProfile::Ltx2_23_22bDistilled, "motion").is_err()
548        );
549    }
550
551    #[test]
552    fn effective_config_is_the_profile_authority() {
553        let config = ModelConfig {
554            family: Some("ltx2".into()),
555            is_schnell: Some(true),
556            transformer: Some("/models/catalog/opaque/ltx-2.3-22b-distilled.safetensors".into()),
557            ..Default::default()
558        };
559        assert_eq!(
560            control_profile_for_config(&config).unwrap(),
561            Ltx2ControlProfile::Ltx2_23_22bDistilled
562        );
563
564        let dev = ModelConfig {
565            is_schnell: Some(false),
566            ..config
567        };
568        assert!(control_profile_for_config(&dev)
569            .unwrap_err()
570            .contains("distilled"));
571    }
572
573    #[test]
574    fn built_in_manifest_profiles_do_not_require_landed_paths() {
575        let config = ModelConfig {
576            family: Some("ltx2".into()),
577            is_schnell: Some(true),
578            ..Default::default()
579        };
580        assert_eq!(
581            control_profile_for_model("ltx-2-19b-distilled:fp8", &config).unwrap(),
582            Ltx2ControlProfile::Ltx2_19bDistilled
583        );
584        assert_eq!(
585            control_profile_for_model("ltx-2.3-22b-distilled:fp8", &config).unwrap(),
586            Ltx2ControlProfile::Ltx2_23_22bDistilled
587        );
588        assert!(control_profile_for_model("ltx-2.3-22b-dev:bf16", &config).is_err());
589    }
590}