diff --git a/invokeai/app/invocations/wan_lora_loader.py b/invokeai/app/invocations/wan_lora_loader.py index 10f7593848e..f39bea5065a 100644 --- a/invokeai/app/invocations/wan_lora_loader.py +++ b/invokeai/app/invocations/wan_lora_loader.py @@ -25,6 +25,13 @@ # - ``both``: append to both lists regardless of the config. # - ``high``: append only to the primary list (high-noise expert). # - ``low``: append only to the low-noise list (low-noise expert). +# +# One exception applies afterwards, to whichever of the four produced a low-only +# routing — ``auto`` on a low-tagged LoRA, or an explicit ``low``. Against a +# single-transformer TI2V-5B main, ``_correct_inert_low_routing`` re-points it at +# the primary list, because that model has no low-noise expert and the alternative +# is to accept the LoRA and silently do nothing with it. ``both`` and ``high`` +# always reach the primary list, so they are never affected. WanLoRATarget = Literal["auto", "both", "high", "low"] @@ -66,22 +73,34 @@ def _assert_lora_variant_matches_main(lora_config: object, main_config: object, ) -def _warn_if_low_routing_is_inert( +def _correct_inert_low_routing( context: InvocationContext, main_config: object, lora_key: str, to_primary: bool, to_low_noise: bool -) -> None: - """Warn when a LoRA is routed only to the low-noise list of a TI2V-5B main. - - The single-transformer TI2V-5B denoise path consumes only the primary list, so - such a LoRA silently has no effect — the node would otherwise report success - while doing nothing. +) -> tuple[bool, bool]: + """Re-point a low-only routing at the primary list when the main is TI2V-5B. + + TI2V-5B is single-transformer: the denoise path only ever reads the primary LoRA + list, so a LoRA routed low-only has no effect at all and the node still reports + success. There is no ambiguity about what to do instead — the model has exactly one + transformer — so correct the routing rather than merely warning about it. + + This is the backstop for the probe-side pin in ``LoRA_LyCORIS_Wan_Config``, which + can only suppress the expert tag when it managed to detect the variant. + ``detect_wan_lora_variant`` reads the inner dim off an ``attn1.to_q`` LoRA pair, so + it returns None for a LoKr/LoHa adapter or one that patches only ``to_k``/``to_v``, + and the tag survives. Records written before that pin existed are in the same + position. Here the main model's own variant is known for certain, which is the one + signal that cannot be wrong. """ if to_primary or not to_low_noise: - return - if getattr(main_config, "variant", None) == WanVariantType.TI2V_5B: - context.logger.warning( - f"LoRA '{lora_key}' is routed only to the low-noise expert, which the single-transformer " - "TI2V-5B variant never uses — the LoRA will have no effect." - ) + return to_primary, to_low_noise + if getattr(main_config, "variant", None) != WanVariantType.TI2V_5B: + return to_primary, to_low_noise + context.logger.warning( + f"LoRA '{lora_key}' is tagged as the low-noise expert, but the single-transformer " + "TI2V-5B variant has no such expert. Applying it to the transformer instead — " + "the alternative is to silently do nothing." + ) + return True, False def _resolve_target(target: WanLoRATarget, lora_expert: str | None) -> tuple[bool, bool]: @@ -127,8 +146,9 @@ class WanLoRALoaderInvocation(BaseInvocation): field to override. For TI2V-5B (single transformer) only the primary list is used at denoise - time; a LoRA routed only to the low-noise list would be inert, so that - routing logs a warning. + time, so a LoRA that would land only in the low-noise list is applied to + the transformer instead, with a warning. The alternative is to accept the + LoRA and silently have no effect. """ lora: ModelIdentifierField = InputField( @@ -141,7 +161,9 @@ class WanLoRALoaderInvocation(BaseInvocation): target: WanLoRATarget = InputField( default="auto", description="Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's " - "recorded expert tag (or both if untagged); 'both'/'high'/'low' override it.", + "recorded expert tag (or both if untagged); 'both'/'high'/'low' override it. " + "On the single-transformer TI2V-5B, which has no low-noise expert, 'low' is " + "applied to the transformer instead of being discarded.", ) transformer: WanTransformerField | None = InputField( default=None, @@ -168,7 +190,7 @@ def invoke(self, context: InvocationContext) -> WanLoRALoaderOutput: lora_expert = getattr(lora_config, "expert", None) to_primary, to_low_noise = _resolve_target(self.target, lora_expert) - _warn_if_low_routing_is_inert(context, main_config, lora_key, to_primary, to_low_noise) + to_primary, to_low_noise = _correct_inert_low_routing(context, main_config, lora_key, to_primary, to_low_noise) # Reject duplicates on whichever list(s) we're about to append to. if to_primary and any(item.lora.key == lora_key for item in self.transformer.loras): @@ -200,6 +222,10 @@ class WanLoRACollectionLoader(BaseInvocation): Each LoRA is routed to the primary and/or low-noise list based on its recorded ``expert`` tag (set by the probe from the filename). Untagged LoRAs go to both lists. + + Against a TI2V-5B main, which is a single transformer with no low-noise + expert, a LoRA that would land only in the low-noise list is applied to + the transformer instead, with a warning. """ loras: Optional[LoRAField | list[LoRAField]] = InputField( @@ -245,7 +271,9 @@ def invoke(self, context: InvocationContext) -> WanLoRALoaderOutput: lora_expert = getattr(lora_config, "expert", None) to_primary, to_low_noise = _resolve_target("auto", lora_expert) - _warn_if_low_routing_is_inert(context, main_config, lora_key, to_primary, to_low_noise) + to_primary, to_low_noise = _correct_inert_low_routing( + context, main_config, lora_key, to_primary, to_low_noise + ) # Reject LoRAs already applied upstream (same invariant the single loader # enforces) — re-appending would silently double the effective weight. diff --git a/invokeai/app/invocations/wan_model_loader.py b/invokeai/app/invocations/wan_model_loader.py index 7524e4c0ca0..5cf07522d9b 100644 --- a/invokeai/app/invocations/wan_model_loader.py +++ b/invokeai/app/invocations/wan_model_loader.py @@ -17,6 +17,10 @@ from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType, WanVariantType +# Transformer-only Wan formats: one file holds exactly one expert, so the A14B MoE +# pair has to be wired up by hand and the VAE / T5 encoder come from elsewhere. +_SINGLE_FILE_FORMATS = frozenset({ModelFormat.GGUFQuantized, ModelFormat.Checkpoint}) + @invocation_output("wan_model_loader_output") class WanModelLoaderOutput(BaseInvocationOutput): @@ -38,6 +42,9 @@ class WanModelLoaderOutput(BaseInvocationOutput): title="Main Model - Wan 2.2", tags=["model", "wan"], category="model", + # Not bumped for the single-file-checkpoint support: no stored node data changed, + # only the live template's model-picker filter widened. Bumping would flag every + # saved Wan workflow as needing an update for no benefit. version="1.0.1", classification=Classification.Prototype, ) @@ -49,15 +56,16 @@ class WanModelLoaderInvocation(BaseInvocation): - Transformer(s): * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/`` from the same model record. - * GGUF main: emits the single GGUF as the primary transformer; for A14B - the second-expert GGUF must be wired to ``Transformer (Low Noise)``. + * Single-file main (GGUF or safetensors checkpoint): emits the file as the + primary transformer; for A14B the second-expert file must be wired to + ``Transformer (Low Noise)``. - VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers). - UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) > Component Source (Diffusers). The Component Source slot lets users supply a Diffusers Wan main model purely for VAE / encoder extraction when the actual transformer is in a single-file - format. Together, the standalone VAE + standalone encoder let a GGUF + format. Together, the standalone VAE + standalone encoder let a single-file transformer run without a full ~30 GB Diffusers install. """ @@ -71,14 +79,14 @@ class WanModelLoaderInvocation(BaseInvocation): transformer_low_noise_model: Optional[ModelIdentifierField] = InputField( default=None, - description="Optional second GGUF transformer for the A14B low-noise expert. " - "Only relevant when the main model is a single-file GGUF and the variant is A14B; " - "ignored when the main is a Diffusers A14B (both experts are pulled from " - "transformer/ and transformer_2/ already) or when the variant is TI2V-5B.", + description="Optional second single-file transformer for the A14B low-noise expert. " + "Only relevant when the main model is a single-file GGUF or safetensors checkpoint and " + "the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled " + "from transformer/ and transformer_2/ already) or when the variant is TI2V-5B.", input=Input.Direct, ui_model_base=BaseModelType.Wan, ui_model_type=ModelType.Main, - ui_model_format=ModelFormat.GGUFQuantized, + ui_model_format=[ModelFormat.GGUFQuantized, ModelFormat.Checkpoint], title="Transformer (Low Noise)", ) @@ -118,9 +126,9 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: self._validate_main_config(main_config, "Wan main") main_format = main_config.format main_is_diffusers = main_format == ModelFormat.Diffusers - main_is_gguf = main_format == ModelFormat.GGUFQuantized + main_is_single_file = main_format in _SINGLE_FILE_FORMATS main_variant = getattr(main_config, "variant", None) - if main_is_gguf and self.component_source is not None: + if main_is_single_file and self.component_source is not None: self._validate_component_source_format(context, self.component_source) # Resolve transformer + dual-expert wiring + boundary_ratio. @@ -129,11 +137,11 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: # low-noise expert (A14B only). boundary_ratio comes from the probed # model_index.json. # - # GGUF main: the file itself is one expert (high or low). For A14B, - # the user wires the other expert to transformer_low_noise_model. - # We swap so the *high*-noise expert is always the primary if needed. - # boundary_ratio falls back to 0.875 unless a Diffusers component_source - # provides a recorded value. + # Single-file main (GGUF or safetensors checkpoint): the file itself is one + # expert (high or low). For A14B, the user wires the other expert to + # transformer_low_noise_model. We swap so the *high*-noise expert is always + # the primary if needed. boundary_ratio falls back to 0.875 unless a + # Diffusers component_source provides a recorded value. boundary_ratio = 0.9 if main_variant == WanVariantType.I2V_A14B else 0.875 transformer_low_noise: Optional[ModelIdentifierField] = None @@ -144,7 +152,7 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: recorded = getattr(main_config, "boundary_ratio", None) if recorded is not None: boundary_ratio = float(recorded) - elif main_is_gguf: + elif main_is_single_file: primary_expert = getattr(main_config, "expert", "none") primary_id = self.model.model_copy(update={"submodel_type": SubModelType.Transformer}) @@ -157,36 +165,43 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: if self.transformer_low_noise_model.key == self.model.key: raise ValueError( "The same model is wired to both 'Transformer' and 'Transformer (Low Noise)'. " - "A Wan A14B expert pair needs two different GGUF models." + "A Wan A14B expert pair needs two different single-file models." ) low_config = context.models.get_config(self.transformer_low_noise_model) self._validate_main_config(low_config, "Transformer (Low Noise)") - if low_config.format != ModelFormat.GGUFQuantized: + # The two experts don't have to share a format — both single-file + # loaders produce a plain WanTransformer3DModel, so a GGUF high-noise + # expert pairs fine with a safetensors low-noise one. + if low_config.format not in _SINGLE_FILE_FORMATS: raise ValueError( - f"'Transformer (Low Noise)' must be a GGUF-format Wan model. " + f"'Transformer (Low Noise)' must be a single-file Wan model (GGUF or checkpoint). " f"'{low_config.name}' is in {low_config.format.value} format." ) low_id = self.transformer_low_noise_model.model_copy(update={"submodel_type": SubModelType.Transformer}) low_expert = getattr(low_config, "expert", "none") if getattr(low_config, "variant", None) != main_variant: - raise ValueError("The high-noise and low-noise GGUF models must use the same Wan variant.") + low_variant = getattr(low_config, "variant", None) + raise ValueError( + "The high-noise and low-noise models must use the same Wan variant, but " + f"'{main_config.name}' is {main_variant.value} and '{low_config.name}' is " + f"{getattr(low_variant, 'value', low_variant)}." + ) - # The expert tag is a filename heuristic, so 'none' (untagged) - # is common on community finetunes. The wiring itself is - # explicit user intent — main slot = high, low-noise slot = - # low — so an untagged file is taken at its wired position (or - # inferred as the complement of its tagged partner). Only a - # genuine conflict, both files claiming the *same* expert, is - # an error. + # The expert tag is a filename heuristic, so 'none' (untagged) is common on + # community finetunes. The wiring itself is explicit user intent — main slot + # = high, low-noise slot = low — so an untagged file is taken at its wired + # position (or inferred as the complement of its tagged partner). Only a + # genuine conflict, both files claiming the *same* expert, is an error. if primary_expert == low_expert != "none": raise ValueError( - f"Both selected GGUF models are tagged as the {primary_expert}-noise expert. " - "A Wan A14B expert pair must contain one high and one low expert." + f"Both selected models are tagged as the {primary_expert}-noise expert " + f"('{main_config.name}' and '{low_config.name}'). A Wan A14B expert pair " + "must contain one high and one low expert." ) if primary_expert == "none" and low_expert == "none": context.logger.warning( - "Neither Wan A14B GGUF filename identifies its expert, so 'Transformer' is assumed to " + "Neither Wan A14B filename identifies its expert, so 'Transformer' is assumed to " "be the high-noise expert and 'Transformer (Low Noise)' the low-noise expert. If the " "output looks wrong, swap the two models." ) @@ -201,7 +216,7 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: # filename tag, so say so: a mistagged file is otherwise an # invisible expert inversion. context.logger.warning( - f"The wired Wan A14B GGUF experts look reversed, so they were swapped: " + f"The wired Wan A14B experts look reversed, so they were swapped: " f"'{low_config.name}' (tagged '{low_expert}') runs as the high-noise expert and " f"'{main_config.name}' (tagged '{primary_expert}') as the low-noise expert. " "The tags come from the filenames — if the output looks wrong, a filename is lying." @@ -211,16 +226,15 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: transformer_low_noise = low_id else: transformer = primary_id - # A14B without a paired low-noise GGUF will produce degraded - # quality (only one expert runs). Warn but don't abort — a - # single wired transformer is explicit intent just like a pair - # is, and the tag is only a filename guess, so an untagged file - # must not be fatal here when the paired path accepts it. - # TI2V-5B GGUFs are single-expert and totally fine. + # A14B without a paired low-noise expert will produce degraded quality + # (only one expert runs). Warn but don't abort — a single wired transformer + # is explicit intent just like a pair is, and the tag is only a filename + # guess, so an untagged file must not be fatal here when the paired path + # accepts it. TI2V-5B is single-expert and totally fine. if main_variant in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B): message = ( - "An A14B GGUF is wired to 'Transformer' without a paired 'Transformer (Low Noise)'. " - "Only this one expert will run; image quality will be reduced." + "An A14B single-file main is wired to 'Transformer' without a paired " + "'Transformer (Low Noise)'. Only this one expert will run; quality will be reduced." ) if primary_expert == "low": message += ( @@ -243,7 +257,7 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: else: raise ValueError( f"Unsupported main model format for Wan: {main_format.value}. " - "Use a Diffusers folder or a GGUF single-file checkpoint." + "Use a Diffusers folder, a GGUF file, or a single-file safetensors checkpoint." ) # VAE: standalone override > main (if Diffusers) > component source. diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py index aef2be7df08..f57b520696f 100644 --- a/invokeai/backend/model_manager/configs/factory.py +++ b/invokeai/backend/model_manager/configs/factory.py @@ -78,6 +78,7 @@ Main_Checkpoint_SD2_Config, Main_Checkpoint_SDXL_Config, Main_Checkpoint_SDXLRefiner_Config, + Main_Checkpoint_Wan_Config, Main_Checkpoint_ZImage_Config, Main_Diffusers_CogView4_Config, Main_Diffusers_ErnieImage_Config, @@ -286,6 +287,7 @@ def has_model_export(module: Any, name: Any, expected_bases: tuple[type, ...]) - Annotated[Main_Checkpoint_Flux2_Config, Main_Checkpoint_Flux2_Config.get_tag()], Annotated[Main_Checkpoint_FLUX_Config, Main_Checkpoint_FLUX_Config.get_tag()], Annotated[Main_Checkpoint_QwenImage_Config, Main_Checkpoint_QwenImage_Config.get_tag()], + Annotated[Main_Checkpoint_Wan_Config, Main_Checkpoint_Wan_Config.get_tag()], Annotated[Main_Checkpoint_ZImage_Config, Main_Checkpoint_ZImage_Config.get_tag()], Annotated[Main_Checkpoint_Krea2_Config, Main_Checkpoint_Krea2_Config.get_tag()], Annotated[Main_Checkpoint_Anima_Config, Main_Checkpoint_Anima_Config.get_tag()], diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py index fbf7cfa8b6c..c275863c9e0 100644 --- a/invokeai/backend/model_manager/configs/lora.py +++ b/invokeai/backend/model_manager/configs/lora.py @@ -27,6 +27,7 @@ state_dict_has_any_keys_ending_with, state_dict_has_any_keys_starting_with, ) +from invokeai.backend.model_manager.configs.main import _detect_wan_expert from invokeai.backend.model_manager.model_on_disk import ModelOnDisk from invokeai.backend.model_manager.omi import flux_dev_1_lora, stable_diffusion_xl_1_lora from invokeai.backend.model_manager.taxonomy import ( @@ -1141,21 +1142,36 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - # Run the base-class probe (file-check, lora-suffix, base detection). instance = super().from_model_on_disk(mod, override_fields) - # Auto-detect the expert tag from the filename if the user didn't - # override it. ``high_noise`` / ``low_noise`` / hyphenated / concatenated - # variants — mirrors the GGUF transformer probe's heuristic. - if instance.expert is None: - name = mod.path.stem.lower() - if any(s in name for s in ("high_noise", "high-noise", "highnoise")): - instance.expert = "high" - elif any(s in name for s in ("low_noise", "low-noise", "lownoise")): - instance.expert = "low" - # Auto-detect the model-family variant from inner_dim in the state # dict. The override field skips this if the user has set it. + # + # Resolved *before* the expert tag because the expert is only meaningful for + # A14B — see below. if instance.variant is None: instance.variant = detect_wan_lora_variant(mod.load_state_dict()) + # Auto-detect the expert tag from the filename if the user didn't override + # it, using the same helper as the transformer probes so the two can't drift + # apart. That also picks up the bare ``HIGH``/``LOW`` convention, which + # matters here: an expert-specific LoRA left untagged is applied to *both* + # experts by the Wan LoRA loader, which is wrong for the high/low pairs the + # Lightning-style distills ship in. + # + # TI2V-5B is single-transformer, so it has no experts and the denoise path + # reads only the primary LoRA list. Tagging a 5B LoRA would route it through + # ``_resolve_target("auto", ...)`` into ``loras_low_noise`` alone, where it is + # silently inert. The bare-token convention makes that reachable on ordinary + # names — ``Wan2.2_TI2V_5B_low_light_v2`` has ``low`` as a standalone token — + # so pin the field the same way ``_resolve_wan_expert`` pins the main-model + # probe. Only A14B (or an inconclusive variant) gets a tag. + # + # Note 'none' vs None: this config uses None for "untagged, apply to both", + # so a 'none' result must leave the field alone. + if instance.expert is None and instance.variant != WanLoRAVariantType.Wan5B: + detected = _detect_wan_expert(mod.path.stem) + if detected != "none": + instance.expert = detected + return instance diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 73b68f23fba..609f7dc8f3b 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -1816,8 +1816,30 @@ def _is_native_wan_layout(state_dict: dict[str | int, Any]) -> bool: return any((p + "text_embedding.0.weight") in keys for p in prefixes) -def _detect_wan_gguf_variant(state_dict: dict[str | int, Any]) -> WanVariantType | None: - """Determine A14B (T2V vs I2V) vs TI2V-5B from the GGUF state dict. +_WAN_KEY_PREFIXES = ("", "model.diffusion_model.", "diffusion_model.") + + +def _wan_patch_embedding_shape(state_dict: dict[str | int, Any]) -> tuple[int, ...] | None: + """Return the shape of ``patch_embedding.weight``, tolerating ComfyUI prefixes. + + Works for both plain tensors and GGMLTensors (which carry the logical shape on + ``tensor_shape`` because their storage is the packed quantized blob). + """ + for prefix in _WAN_KEY_PREFIXES: + tensor = state_dict.get(prefix + "patch_embedding.weight") + if tensor is None: + continue + shape = getattr(tensor, "tensor_shape", None) + if shape is None: + shape = getattr(tensor, "shape", None) + if shape is None: + return None + return tuple(int(dim) for dim in shape) + return None + + +def _detect_wan_variant_from_state_dict(state_dict: dict[str | int, Any]) -> WanVariantType | None: + """Determine A14B (T2V vs I2V) vs TI2V-5B from the transformer state dict. ``patch_embedding.weight`` has shape ``[inner_dim, in_channels, T, H, W]``; ``in_channels`` uniquely identifies the Wan 2.2 variant: @@ -1830,41 +1852,324 @@ def _detect_wan_gguf_variant(state_dict: dict[str | int, Any]) -> WanVariantType Returns None if the tensor is missing or the channel count is unrecognised. """ - candidates = ( - "patch_embedding.weight", - "model.diffusion_model.patch_embedding.weight", - "diffusion_model.patch_embedding.weight", + shape = _wan_patch_embedding_shape(state_dict) + if shape is None or len(shape) < 2: + return None + inner_dim, in_channels = shape[0], shape[1] + + # in_channels alone is ambiguous outside the three supported releases: the wider + # Wan family reuses these channel counts at other widths (Fun-Control-14B is + # 48-channel but 5120-wide, i.e. A14B-sized, not TI2V-5B). Require the width to + # agree, so a derivative we don't support falls through to None rather than being + # mislabelled — a wrong variant pins `expert`, picks the wrong default settings, + # and hides the low-noise partner picker. + # + # A14B is uniquely 5120-wide and TI2V-5B uniquely 3072-wide across Wan 2.2. + if in_channels == 16 and inner_dim == 5120: + return WanVariantType.T2V_A14B + if in_channels == 36 and inner_dim == 5120: + return WanVariantType.I2V_A14B + if in_channels == 48 and inner_dim == 3072: + return WanVariantType.TI2V_5B + return None + + +def _has_wan_transformer_block_weights(state_dict: dict[str | int, Any]) -> bool: + """True if the state dict carries a transformer block's *own* attention weight. + + ``_has_wan_keys`` only looks at the input conv and the text projection, and a + Wan LoRA can legitimately ship both: I2V adapters bundle a full replacement + ``patch_embedding`` because they change ``in_channels`` from 16 to 36. Such a + file matches both the LoRA and the main-model probes, and ``matches_sort_key`` + ranks Main above LoRA — so it would be pulled out of the LoRA pickers and into + the main-model dropdown, where it can only fail to load. + + Requiring a bare ``blocks.0...weight`` separates them positively: a + LoRA stores ``...q.lora_A.weight`` / ``...q.lora_down.weight`` and never the + undecorated weight. Keys are matched exactly, so a LoRA's decorated key cannot + satisfy this. + + Deliberately a *positive* structural test rather than a "reject anything with + lora_A keys" exclusion: main models with merged-in LoRA weights sometimes retain + those keys (see ``LoRA_LyCORIS_*_Config._validate_looks_like_lora``), and + rejecting them would be the same over-restrictiveness this probe exists to fix. + """ + attention_weights = ( + "blocks.0.self_attn.q.weight", # native upstream / ComfyUI layout + "blocks.0.attn1.to_q.weight", # diffusers layout ) - for key in candidates: - if key in state_dict: - tensor = state_dict[key] - shape = getattr(tensor, "tensor_shape", None) or getattr(tensor, "shape", None) - if shape is None or len(shape) < 2: - return None - in_channels = int(shape[1]) - if in_channels == 16: - return WanVariantType.T2V_A14B - if in_channels == 36: - return WanVariantType.I2V_A14B - if in_channels == 48: - return WanVariantType.TI2V_5B - return None + keys = state_dict.keys() + return any((prefix + weight) in keys for prefix in _WAN_KEY_PREFIXES for weight in attention_weights) + + +def _find_wan_2_1_marker(state_dict: dict[str | int, Any]) -> str | None: + """Return a human-readable reason if the state dict is architecturally Wan 2.1. + + Wan 2.1 and Wan 2.2 share a key layout, so the two families can only be told + apart by architecture. Three markers are decisive, and all three describe + things Wan 2.2 never ships: + + * **CLIP image embedder** (``img_emb.proj.*`` / ``condition_embedder.image_embedder.*``). + Wan 2.1 I2V conditioned on CLIP-vision features via ``image_dim``. Wan 2.2 + I2V-A14B dropped that entirely and concatenates VAE latents instead, so any + 36-channel model carrying an image embedder is Wan 2.1. + * **1536-dim inner width** — the Wan 2.1 T2V-1.3B model. The Wan 2.2 family is + 5120 (A14B) or 3072 (TI2V-5B). + * **VACE blocks** (``vace_blocks.*``) — the Wan 2.1 VACE editing variant, which + needs a control branch InvokeAI's Wan pipeline doesn't drive. + + Wan 2.1 T2V-14B is *not* detectable this way: it is shape-identical to a single + Wan 2.2 A14B expert. Callers that care fall back to the filename/metadata gate. + """ + keys = state_dict.keys() + image_embedder_markers = ("img_emb.proj.0.weight", "condition_embedder.image_embedder.norm1.weight") + if any((prefix + marker) in keys for prefix in _WAN_KEY_PREFIXES for marker in image_embedder_markers): + return ( + "state dict has a CLIP image embedder (img_emb), which is a Wan 2.1 I2V feature; " + "Wan 2.2 I2V conditions on VAE latents instead" + ) + + shape = _wan_patch_embedding_shape(state_dict) + if shape is not None and len(shape) >= 1 and shape[0] == 1536: + return "state dict has a 1536-dim transformer, which is the Wan 2.1 T2V-1.3B architecture" + return None -def _detect_wan_gguf_expert(filename: str) -> Literal["high", "low", "none"]: +def _find_unsupported_wan_variant_marker(state_dict: dict[str | int, Any]) -> str | None: + """Return a reason if this is a Wan variant the plain transformer can't represent. + + These are Wan 2.2-era models built on extra conditioning branches that + ``WanTransformer3DModel`` simply doesn't have. Loading them would not error: + ``load_state_dict(strict=False)`` drops the extra modules as unexpected keys and + produces a model that silently ignores the conditioning it was built around + (real ``wan2.2_animate_14B_bf16.safetensors``: 127 of its 1441 keys are + ``face_adapter``/``motion_encoder``). Refusing is the honest outcome. + + Checked before the Wan 2.1 markers because Animate also carries ``img_emb``, so + it would otherwise be reported as a Wan 2.1 I2V model. + """ + keys = [key for key in state_dict.keys() if isinstance(key, str)] + + def has(*markers: str) -> bool: + return any(marker in key for key in keys for marker in markers) + + if has("face_adapter.", "motion_encoder."): + return ( + "state dict has face-adapter / motion-encoder branches, which belong to Wan Animate; " + "character animation and replacement are not supported yet" + ) + + if has("audio_injector.", "casual_audio_encoder.", "cond_encoder.", "frame_packer."): + return ( + "state dict has audio-conditioning branches, which belong to Wan S2V; " + "audio-driven video is not supported yet" + ) + + if has("control_adapter."): + # Fun-Control-Camera is the dangerous one: 36 in-channels and otherwise + # key-identical to plain I2V-A14B, and it ships as a properly tagged + # high/low pair, so the expert-pairing check passes too. Without this it + # would load and render as an ordinary I2V, ignoring every camera input. + return ( + "state dict has a control-adapter branch, which belongs to the Wan Fun-Control family; " + "camera and control conditioning are not supported yet" + ) + + if has("vace_blocks."): + return "state dict has VACE control blocks, and VACE models are not supported yet" + + return None + + +# Tokens that turn an adjacent bare ``high``/``low`` into an adjective about +# something other than the noise level, so it must not be read as an MoE expert. +# +# Deliberately short. Real releases sit expert markers next to plenty of unrelated +# words — ``..._LOW_lightning_edition``, ``..._high_lighting_fp16`` — so anything +# broader here starts costing true positives, which is the more damaging error for +# a checkpoint whose expert can only be recovered from its name. +_WAN_EXPERT_DISQUALIFIERS = frozenset( + { + "angle", + "cfg", + "guidance", + "vram", + "ram", + "mem", + "memory", + "step", + "steps", + "res", + "resolution", + "quality", + "speed", + "fps", + "bit", + "bits", + } +) + + +def _detect_wan_expert(filename: str) -> Literal["high", "low", "none"]: """Filename heuristic for the A14B dual-expert MoE. - Community releases tag each expert in the filename — typically - ``high_noise`` / ``low_noise`` (or hyphenated/concatenated variants). - Returns 'none' when neither marker is present (single-expert model or - ambiguous filename). + Two conventions dominate and both have to work, because the expert cannot be + read off the weights: + + * ``high_noise`` / ``low_noise`` and its spellings — hyphenated, underscored, + spaced, fused (``highnoise``, ``LOWNOISEFP8``), camel-cased (``HighNoise``), + reversed (``noise_high``). This is what Comfy-Org's repackaged repos use. + * A **bare** ``HIGH`` / ``LOW`` token, e.g. + ``Wan2_2-T2V-A14B-HIGH_fp8_e4m3fn_scaled_KJ.safetensors``. This is what the + widely-mirrored Kijai fp8 catalogue and many CivitAI fine-tunes use, so + refusing to read it would leave most single-file A14B models unpairable. + + Some releases also declare the expert in file metadata; ``_resolve_wan_expert`` + consults that when the name yields nothing. + + Precedence, in order: + + 1. Explicit ``..._noise`` markers outrank bare tokens found elsewhere in the + name, so ``..._4step_LOW_lightning_high_noise`` reads as the high-noise file. + 2. Within a tier, one distinct marker wins. If **both** appear, the file serves + both experts (``... I2V HIGH+LOW ...``, ``..._low_high_noise_...`` — both are + real release patterns) or the name is simply ambiguous, so return 'none' + rather than guess. For a LoRA 'none' means "apply to both", which is the + right answer for those; for a main it surfaces as a pairing error the user + can act on, which beats silently running one expert for both phases. + + Note the tiers are reconciled the same way. An earlier revision returned on the + first ``noise`` marker it saw, which meant the two spellings of the same + both-experts name disagreed: ``HIGH-LOW`` gave 'none' but ``low_high_noise`` + gave 'high'. + + A bare token is ignored when a neighbour marks it as an adjective about + something else (``lowVRAM``, ``low-cfg``, ``Low Angle``) — see + ``_WAN_EXPERT_DISQUALIFIERS``. TI2V-5B is handled structurally by the callers, + which force 'none' because the model is single-transformer. + + Matching is per token, so a bare marker can't fire on a substring + (``highway``), and the fused form is anchored at a token boundary so + ``slow_noise`` and ``flownoise`` are left alone. + + Returns 'none' for an untagged filename. """ - name = filename.lower() - if any(s in name for s in ("high_noise", "high-noise", "highnoise")): - return "high" - if any(s in name for s in ("low_noise", "low-noise", "lownoise")): - return "low" + name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", filename).lower() + tokens = [token for token in re.split(r"[^a-z0-9]+", name) if token] + + markers = ("high", "low") + explicit: list[str] = [] + bare: list[str] = [] + + index = 0 + while index < len(tokens): + token = tokens[index] + + # Fused with 'noise'. Anchored: startswith catches `lownoisefp8`, and only the + # reversed form may match at the end — `endswith("lownoise")` would wrongly + # claim `slownoise`. + fused = next((m for m in markers if token.startswith(f"{m}noise") or token.endswith(f"noise{m}")), None) + if fused is not None: + explicit.append(fused) + index += 1 + continue + + if token not in markers: + index += 1 + continue + + # Consume the whole *run* of adjacent marker tokens, so a single 'noise' + # qualifies all of them. `low_high_noise` names one file holding both experts + # (real: moriqqe/Mabrle_wan2.2_low_high_noise); scoring only the marker + # touching 'noise' would read it as the high-noise expert alone. + start = index + while index < len(tokens) and tokens[index] in markers: + index += 1 + run = tokens[start:index] + previous = tokens[start - 1] if start > 0 else None + following = tokens[index] if index < len(tokens) else None + + # Only the token *following* the run can disqualify: these are adjective-noun + # pairs, so the noun comes second. "low angle" is a camera angle, but + # "Angle HIGH" is the high-noise expert of a camera-angle LoRA. + # + # A disqualifier only consumes the marker it is actually attached to — the last + # one in the run — not the whole run. `HIGH_lowVRAM` is the high-noise expert of + # a low-VRAM build: dropping both markers there loses a correct tag, which for a + # main model disables the pair checks and for a LoRA silently applies a + # single-expert distill to both experts. + # + # Checked ahead of the 'noise' test below, and it clears `following` with it: a + # disqualifier sits after the run, so an adjacent 'noise' would have to precede + # it. ('low noise' can't trip this — 'noise' is not itself a disqualifier.) + if following in _WAN_EXPERT_DISQUALIFIERS: + run = run[:-1] + following = None + if not run: + continue + if previous == "noise" or following == "noise": + explicit.extend(run) + else: + bare.extend(run) + + # An explicit `..._noise` marker outranks a bare token found elsewhere in the + # name. Within each tier, both experts named means the file serves both — real + # releases do ship that way — so return 'none' rather than pick one. For a LoRA + # 'none' means "apply to both", which is the right answer for those. + for candidates in (explicit, bare): + distinct = set(candidates) + if len(distinct) == 1: + return candidates[0] # type: ignore[return-value] + if distinct: + return "none" + return "none" + + +def _resolve_wan_expert( + mod: ModelOnDisk, override_fields: dict[str, Any], variant: WanVariantType +) -> Literal["high", "low", "none"]: + """Settle the MoE expert field: explicit override, then filename, then metadata. + + The override is consumed here so it can't reach the constructor twice. Note it is + not reachable through the install API today — ``ModelRecordChanges`` has no + ``expert`` field and ``ModelConfigFactory.build_common_fields`` forwards a fixed + whitelist that excludes it — so in practice a mis-detected expert can only be + corrected by renaming the file and re-importing. + + TI2V-5B is a single-transformer model, so the expert is meaningless there and is + pinned to 'none'. That is not cosmetic: the frontend's low-noise expert picker + selects on ``expert == 'low'``, so a TI2V-5B file whose name happens to contain a + bare ``low`` (``...-5B-lowVRAM``, ``...-Turbo-lowSteps``) would otherwise be + offered as an A14B partner expert it can never be. + + Metadata is consulted only as a fallback, not as the primary signal, even though + it is the more trustworthy of the two. Renaming a file is the one lever a user + has to correct a mis-detected expert — there is no UI for the field — and the + Wan model loader's error message tells them to use it. Letting an embedded + ``model_type`` override the name would take that lever away. + + Coverage is uneven, which is why both signals are needed. Sampled 2026-08-13: + every Wan 2.2 safetensors in ``Kijai/WanVideo_comfy_fp8_scaled`` carries + ``__metadata__["model_type"]`` naming the expert (``Wan2_2-I2V-A14B-high``), + while none of the ``Comfy-Org/Wan_2.2_ComfyUI_Repackaged`` files carry any + ``__metadata__`` at all. GGUF releases use ``general.name`` instead. + """ + explicit_expert = override_fields.pop("expert", None) + if explicit_expert is not None: + return explicit_expert # type: ignore[no-any-return] + + if variant == WanVariantType.TI2V_5B: + return "none" + + expert = _detect_wan_expert(mod.path.stem) + if expert != "none": + return expert + + metadata = mod.metadata() + declared = metadata.get("model_type") or metadata.get("general.name") or "" + if declared: + return _detect_wan_expert(declared) return "none" @@ -1896,22 +2201,112 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - raise NotAMatchError("state dict does not look like GGUF quantized") if not _has_wan_keys(sd): raise NotAMatchError("state dict does not look like a Wan transformer") + if not _has_wan_transformer_block_weights(sd): + raise NotAMatchError( + "state dict has no undecorated transformer block weights — it looks like a Wan LoRA " + "or adapter rather than a full transformer" + ) + unsupported_reason = _find_unsupported_wan_variant_marker(sd) + if unsupported_reason is not None: + raise NotAMatchError(unsupported_reason) gguf_name = mod.metadata().get("general.name", "") normalized_identity = "".join( character for character in f"{mod.path.stem} {gguf_name}".lower() if character.isalnum() ) if "wan21" in normalized_identity: raise NotAMatchError("Wan 2.1 GGUF models are not supported by the Wan 2.2 loader") + # A misnamed Wan 2.1 GGUF slips past the name check above; the architectural + # markers don't care what the file is called. + wan_2_1_reason = _find_wan_2_1_marker(sd) + if wan_2_1_reason is not None: + raise NotAMatchError(f"Wan 2.1 GGUF models are not supported by the Wan 2.2 loader: {wan_2_1_reason}") explicit_variant = override_fields.pop("variant", None) - variant = explicit_variant or _detect_wan_gguf_variant(sd) + variant = explicit_variant or _detect_wan_variant_from_state_dict(sd) if variant is None: raise NotAMatchError("could not determine Wan variant from state dict") if variant in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B) and "wan22" not in normalized_identity: raise NotAMatchError("Wan A14B GGUF filename or metadata must identify the model as Wan 2.2") - explicit_expert = override_fields.pop("expert", None) - expert = explicit_expert or _detect_wan_gguf_expert(mod.path.stem) + expert = _resolve_wan_expert(mod, override_fields, variant) + + return cls(**override_fields, variant=variant, expert=expert) + + +class Main_Checkpoint_Wan_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for single-file Wan 2.2 transformer checkpoints (safetensors). + + This is the format the community ships on CivitAI and in ComfyUI-oriented + Hugging Face repos: one ``.safetensors`` per transformer, in either the native + upstream key layout or the diffusers one, optionally under a + ``model.diffusion_model.`` prefix, and optionally ComfyUI ``fp8_scaled`` + quantized. The loader normalises all of those. + + As with GGUF, A14B's MoE arrives as two files (one per expert); ``expert`` + records which one this is so the Wan model loader invocation can pair them. + TI2V-5B is single-transformer and stores ``expert='none'``. + """ + + base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + variant: WanVariantType = Field() + expert: Literal["high", "low", "none"] = Field( + default="none", + description="For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, " + "'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B).", + ) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + raise_for_override_fields(cls, override_fields) + + # The loader reads this format with safetensors.torch.load_file, so claiming a + # pickle here would let the model install and then fail with an opaque + # "header too large" error at first generation. Wan isn't distributed as + # .ckpt/.pt/.bin, so refusing them costs nothing. + if mod.path.suffix.lower() != ".safetensors": + raise NotAMatchError(f"single-file Wan checkpoints must be .safetensors, not {mod.path.suffix or 'None'}") + + sd = mod.load_state_dict() + + if not _has_wan_keys(sd): + raise NotAMatchError("state dict does not look like a Wan transformer") + if _has_ggml_tensors(sd): + raise NotAMatchError("state dict looks like GGUF quantized") + if not _has_wan_transformer_block_weights(sd): + raise NotAMatchError( + "state dict has no undecorated transformer block weights — it looks like a Wan LoRA " + "or adapter rather than a full transformer" + ) + + # Checked before the Wan 2.1 markers: Animate carries img_emb too, so the + # order is what makes the rejection reason accurate. + unsupported_reason = _find_unsupported_wan_variant_marker(sd) + if unsupported_reason is not None: + raise NotAMatchError(unsupported_reason) + + # Wan 2.1 shares Wan 2.2's key layout, so reject it on architecture rather + # than on the filename. Unlike the GGUF probe we deliberately do *not* + # require the name to say "wan2.2": community fine-tunes routinely drop the + # version from the filename, and rejecting them was the whole complaint in + # #9463. The residual ambiguity is Wan 2.1 T2V-14B, which is shape-identical + # to a Wan 2.2 A14B expert — that one is caught by the explicit "wan2.1" name + # check below, and otherwise imports as A14B. + wan_2_1_reason = _find_wan_2_1_marker(sd) + if wan_2_1_reason is not None: + raise NotAMatchError(f"Wan 2.1 models are not supported by the Wan 2.2 loader: {wan_2_1_reason}") + + normalized_identity = "".join(character for character in mod.path.stem.lower() if character.isalnum()) + if "wan21" in normalized_identity: + raise NotAMatchError("Wan 2.1 models are not supported by the Wan 2.2 loader") + + explicit_variant = override_fields.pop("variant", None) + variant = explicit_variant or _detect_wan_variant_from_state_dict(sd) + if variant is None: + raise NotAMatchError("could not determine Wan variant from state dict") + + expert = _resolve_wan_expert(mod, override_fields, variant) return cls(**override_fields, variant=variant, expert=expert) diff --git a/invokeai/backend/model_manager/load/model_loaders/comfyui_state_dict_utils.py b/invokeai/backend/model_manager/load/model_loaders/comfyui_state_dict_utils.py new file mode 100644 index 00000000000..bc5d21837f0 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/comfyui_state_dict_utils.py @@ -0,0 +1,95 @@ +"""Helpers for normalising ComfyUI-flavoured single-file checkpoints. + +Community single-file releases (CivitAI, ComfyUI-oriented Hugging Face repos) +share a small set of conventions regardless of which architecture they wrap: +an optional ``model.diffusion_model.`` key prefix, and optional fp8 weights +paired with per-tensor scale factors. These helpers undo both so a state dict +can be handed to a plain diffusers module. + +Originally written for the Qwen Image loader; shared so the Wan loader doesn't +need a second copy. +""" + +import torch + + +def _strip_comfyui_prefix(sd: dict) -> dict: + """Strip ComfyUI-style `model.diffusion_model.` / `diffusion_model.` prefixes from keys.""" + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + if prefix_to_strip is None: + return sd + stripped: dict = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped[key[len(prefix_to_strip) :]] = value + else: + stripped[key] = value + return stripped + + +def _dequantize_comfyui_fp8(sd: dict, compute_dtype: torch.dtype) -> int: + """Dequantize ComfyUI-style fp8_scaled weights in-place. Returns count of dequantized tensors. + + Weights are dequantized directly to `compute_dtype` (typically bf16) instead of via a + full-precision float32 intermediate. The previous float32 path materialised a complete + 4-byte/param copy of the model before a separate downcast pass, spiking peak RAM to ~2x the + final bf16 size (~80GB for the 20B Qwen-Image transformer). Multiplying in the target dtype + keeps the dict at the bf16 model size plus a single transient tensor. fp8 has only 3 mantissa + bits and bf16 shares float32's exponent range, so the bf16 multiply loses no meaningful + precision here. + + Two key naming schemes are in the wild: + - `.weight` + `.weight_scale` (FLUX, Z-Image style) + - `.weight` + `.scale_weight` (Qwen2.5-VL fp8_scaled style, also + emits `.scale_input` for activation scaling that we discard). + + The scale is applied whatever dtype the weight is stored in. There is deliberately no + "only if the weight is fp8" gate: not every checkpoint using these keys stores fp8 + weights, and skipping the multiply for those would produce a silently wrong model in + the same way applying a stale scale would. The assumption is that a scale key present + in the file is a scale that still needs applying — i.e. a checkpoint must not ship + already-dequantized weights alongside their scales. + """ + scale_suffixes = (".weight_scale", ".scale_weight") + weight_scale_keys = [k for k in sd.keys() if isinstance(k, str) and k.endswith(scale_suffixes)] + count = 0 + for scale_key in weight_scale_keys: + for suffix in scale_suffixes: + if scale_key.endswith(suffix): + weight_key = scale_key[: -len(suffix)] + ".weight" + break + if weight_key not in sd: + continue + weight = sd[weight_key].to(compute_dtype) + scale = sd[scale_key].to(compute_dtype) + if scale.shape != weight.shape and scale.numel() > 1: + for dim in range(len(weight.shape)): + if dim < len(scale.shape) and scale.shape[dim] != weight.shape[dim]: + block_size = weight.shape[dim] // scale.shape[dim] + if block_size > 1: + scale = scale.repeat_interleave(block_size, dim=dim) + sd[weight_key] = weight * scale + count += 1 + return count + + +def _strip_quantization_metadata(sd: dict) -> None: + """Strip ComfyUI fp8 quantization metadata keys in-place.""" + keys_to_drop = [ + k + for k in sd.keys() + if isinstance(k, str) + and ( + k.endswith(".weight_scale") + or k.endswith(".scale_weight") + or k.endswith(".scale_input") + or "comfy_quant" in k + or k == "scaled_fp8" + ) + ] + for k in keys_to_drop: + del sd[k] diff --git a/invokeai/backend/model_manager/load/model_loaders/qwen_image.py b/invokeai/backend/model_manager/load/model_loaders/qwen_image.py index 90919794999..1342cbe1075 100644 --- a/invokeai/backend/model_manager/load/model_loaders/qwen_image.py +++ b/invokeai/backend/model_manager/load/model_loaders/qwen_image.py @@ -16,6 +16,11 @@ ) from invokeai.backend.model_manager.load.load_default import ModelLoader from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.comfyui_state_dict_utils import ( + _dequantize_comfyui_fp8, + _strip_comfyui_prefix, + _strip_quantization_metadata, +) from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader from invokeai.backend.model_manager.taxonomy import ( AnyModel, @@ -30,63 +35,6 @@ from invokeai.backend.util.devices import TorchDevice -def _strip_comfyui_prefix(sd: dict) -> dict: - """Strip ComfyUI-style `model.diffusion_model.` / `diffusion_model.` prefixes from keys.""" - prefix_to_strip = None - for prefix in ["model.diffusion_model.", "diffusion_model."]: - if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): - prefix_to_strip = prefix - break - if prefix_to_strip is None: - return sd - stripped: dict = {} - for key, value in sd.items(): - if isinstance(key, str) and key.startswith(prefix_to_strip): - stripped[key[len(prefix_to_strip) :]] = value - else: - stripped[key] = value - return stripped - - -def _dequantize_comfyui_fp8(sd: dict, compute_dtype: torch.dtype) -> int: - """Dequantize ComfyUI-style fp8_scaled weights in-place. Returns count of dequantized tensors. - - Weights are dequantized directly to `compute_dtype` (typically bf16) instead of via a - full-precision float32 intermediate. The previous float32 path materialised a complete - 4-byte/param copy of the model before a separate downcast pass, spiking peak RAM to ~2x the - final bf16 size (~80GB for the 20B Qwen-Image transformer). Multiplying in the target dtype - keeps the dict at the bf16 model size plus a single transient tensor. fp8 has only 3 mantissa - bits and bf16 shares float32's exponent range, so the bf16 multiply loses no meaningful - precision here. - - Two key naming schemes are in the wild: - - `.weight` + `.weight_scale` (FLUX, Z-Image style) - - `.weight` + `.scale_weight` (Qwen2.5-VL fp8_scaled style, also - emits `.scale_input` for activation scaling that we discard). - """ - scale_suffixes = (".weight_scale", ".scale_weight") - weight_scale_keys = [k for k in sd.keys() if isinstance(k, str) and k.endswith(scale_suffixes)] - count = 0 - for scale_key in weight_scale_keys: - for suffix in scale_suffixes: - if scale_key.endswith(suffix): - weight_key = scale_key[: -len(suffix)] + ".weight" - break - if weight_key not in sd: - continue - weight = sd[weight_key].to(compute_dtype) - scale = sd[scale_key].to(compute_dtype) - if scale.shape != weight.shape and scale.numel() > 1: - for dim in range(len(weight.shape)): - if dim < len(scale.shape) and scale.shape[dim] != weight.shape[dim]: - block_size = weight.shape[dim] // scale.shape[dim] - if block_size > 1: - scale = scale.repeat_interleave(block_size, dim=dim) - sd[weight_key] = weight * scale - count += 1 - return count - - def _remap_qwen_vl_checkpoint_keys(sd: dict) -> dict: """Remap legacy ComfyUI Qwen2.5-VL single-file keys to the transformers layout. @@ -124,24 +72,6 @@ def _remap_qwen_vl_checkpoint_keys(sd: dict) -> dict: return remapped_sd -def _strip_quantization_metadata(sd: dict) -> None: - """Strip ComfyUI fp8 quantization metadata keys in-place.""" - keys_to_drop = [ - k - for k in sd.keys() - if isinstance(k, str) - and ( - k.endswith(".weight_scale") - or k.endswith(".scale_weight") - or k.endswith(".scale_input") - or "comfy_quant" in k - or k == "scaled_fp8" - ) - ] - for k in keys_to_drop: - del sd[k] - - def _build_qwen_image_transformer_config(sd: dict, is_edit: bool) -> dict: """Auto-detect Qwen Image transformer architecture parameters from the state dict. diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py index ef51e089998..bb9691d3018 100644 --- a/invokeai/backend/model_manager/load/model_loaders/wan.py +++ b/invokeai/backend/model_manager/load/model_loaders/wan.py @@ -2,22 +2,34 @@ Currently covers: - Main: Diffusers format (T2V-A14B with dual experts via Transformer + - Transformer2 submodels, plus TI2V-5B). Phase 4 will add a GGUFQuantized loader. + Transformer2 submodels, plus TI2V-5B). +- Main: GGUFQuantized and single-file Checkpoint (safetensors) transformers. + Both are transformer-only — one file per A14B expert — and rely on a + standalone VAE + T5 encoder for the rest of the pipeline. - WanT5Encoder: standalone UMT5-XXL encoder folder (``text_encoder/`` + ``tokenizer/`` subdirs, or a flat ``text_encoder/`` folder). - VAE: handled in ``vae.py`` (registered for type=VAE generically). """ from pathlib import Path -from typing import Optional +from typing import Any, Optional import torch from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base from invokeai.backend.model_manager.configs.factory import AnyModelConfig -from invokeai.backend.model_manager.configs.main import Main_GGUF_Wan_Config, _is_native_wan_layout +from invokeai.backend.model_manager.configs.main import ( + Main_Checkpoint_Wan_Config, + Main_GGUF_Wan_Config, + _is_native_wan_layout, +) from invokeai.backend.model_manager.load.load_default import ModelLoader from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.comfyui_state_dict_utils import ( + _dequantize_comfyui_fp8, + _strip_comfyui_prefix, + _strip_quantization_metadata, +) from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader from invokeai.backend.model_manager.taxonomy import ( AnyModel, @@ -25,7 +37,6 @@ ModelFormat, ModelType, SubModelType, - WanVariantType, ) from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader @@ -48,7 +59,9 @@ def _load_model( submodel_type: Optional[SubModelType] = None, ) -> AnyModel: if isinstance(config, Checkpoint_Config_Base): - raise NotImplementedError("Single-file checkpoint format is not yet supported for Wan models.") + # Defensive: the registry keys on format, so single-file configs are + # routed to WanGGUFCheckpointModel / WanCheckpointModel, not here. + raise TypeError(f"{type(config).__name__} is a single-file config; it does not belong to this loader.") if submodel_type is None: raise Exception("A submodel type must be provided when loading Wan main pipelines.") @@ -172,6 +185,204 @@ def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict: return unwrapped +# Top-level modules that legitimately ride along in a single-file Wan checkpoint +# without being part of the transformer. Dropping these is correct: the pipeline +# sources its VAE and text encoder from separately-wired models, and the EMA copy +# is not the weight set we generate with. +# +# ``vae`` / ``text_encoders`` / ``clip`` / ``cond_stage_model`` / ``first_stage_model`` +# are the "all-in-one" packaging convention — one file holding transformer + VAE + +# CLIP so ComfyUI's ``Load Checkpoint`` node can supply all three. The +# Phr00t/WAN2.2-14B-Rapid-AllInOne family and its ~110 GGUF conversions +# (befox/WAN2.2-14B-Rapid-AllInOne-GGUF) ship this way and loaded fine before the +# unexpected-key check existed, so refusing them would be a regression. +_BENIGN_EXTRA_MODULES = frozenset( + { + "vae", + "first_stage_model", + "text_encoders", + "cond_stage_model", + "clip", + "model_ema", + } +) + +# Trailing segments marking a merged-in LoRA's leftover adapter tensors. The main-model +# probe deliberately admits checkpoints that retain these — see +# ``configs.main._has_wan_transformer_block_weights``, which uses a *positive* structural +# test precisely so merged-LoRA mains aren't turned away — so the loader has to admit +# them too, or the two halves disagree about the same file. +# +# Kept in step with the suffix set ``LoRA_LyCORIS_Wan_Config`` matches on +# (``configs/lora.py``): kohya, PEFT, DoRA and LoKr. Matched against the *last* path +# segment rather than as a substring anywhere in the key, so a future conditioning branch +# that merely contains "lora_a" in a module name still trips the backstop instead of +# being silently discarded by it. +_MERGED_LORA_SEGMENTS = frozenset( + { + "alpha", + "dora_scale", + "lora_magnitude_vector", + "lokr_w1", + "lokr_w2", + "lokr_w1_a", + "lokr_w1_b", + "lokr_w2_a", + "lokr_w2_b", + "hada_w1_a", + "hada_w1_b", + "hada_w2_a", + "hada_w2_b", + "oft_blocks", + } +) +_MERGED_LORA_PENULTIMATE = frozenset({"lora_a", "lora_b", "lora_down", "lora_up", "lora_mid", "lora_magnitude"}) + + +def _is_benign_extra_key(key: str) -> bool: + """True if an unexpected key is packaging rather than an unsupported branch.""" + parts = key.lower().split(".") + if parts[0] in _BENIGN_EXTRA_MODULES: + return True + if parts[-1] in _MERGED_LORA_SEGMENTS: + return True + # `...to_q.lora_down.weight` — the marker is the segment before the tensor name. + return len(parts) >= 2 and parts[-2] in _MERGED_LORA_PENULTIMATE + + +def _drop_benign_extra_keys(sd: dict, source: str, logger: Any) -> None: + """Remove packaging weights the transformer has no use for, in place. + + Done up front rather than left to ``load_state_dict(strict=False)`` because + everything between here and there costs real memory: the fp8 dequant pass, the + blanket cast to the compute dtype, and the RAM-cache reservation all run over the + whole dict. An all-in-one checkpoint bundles a full VAE and UMT5-XXL text encoder — + several GB, upcast to bf16 and reserved in the cache — only for + ``load_state_dict`` to discard them one line later. + """ + dropped = [key for key in sd if isinstance(key, str) and _is_benign_extra_key(key)] + if not dropped: + return + modules = sorted({key.split(".")[0] for key in dropped}) + for key in dropped: + del sd[key] + logger.info( + f"{source}: ignored {len(dropped)} bundled/merged weights not part of the transformer " + f"({', '.join(modules[:8])}). The VAE and text encoder come from the separately-wired models." + ) + + +def _raise_for_incompatible_keys(incompatible_keys: Any, source: str) -> None: + """Fail loudly on anything ``load_state_dict(strict=False)`` quietly discarded. + + Missing keys are the obvious error. Unexpected keys matter just as much here and + are far easier to miss: several Wan 2.2 derivatives are supersets of the plain + transformer — Fun-Camera adds ``control_adapter.*`` (6 keys), S2V adds + ``audio_injector``/``cond_encoder``/``frame_packer`` (165 keys), Animate adds + ``face_adapter``/``motion_encoder`` (127 keys). They match the probe, build a + correctly-shaped ``WanTransformer3DModel``, report zero missing keys, and then + generate with the entire branch they were built around silently absent. + + ``configs.main._find_unsupported_wan_variant_marker`` turns away the families we + know by name; this is the generic backstop, so a derivative nobody has enumerated + yet produces an error instead of quietly degraded output. + + Benign extras — bundled VAE/text-encoder weights and merged-LoRA residue — have + already been removed by ``_drop_benign_extra_keys``, so anything reaching here is + genuinely unplaceable. + """ + if incompatible_keys.missing_keys: + raise RuntimeError(f"{source} is missing model parameters: {sorted(incompatible_keys.missing_keys)[:10]}") + + unexpected = [key for key in incompatible_keys.unexpected_keys if isinstance(key, str)] + if unexpected: + # Report the distinct top-level module names rather than hundreds of keys. + modules = sorted({key.split(".")[0] for key in unexpected}) + raise RuntimeError( + f"{source} has {len(unexpected)} weights that WanTransformer3DModel has nowhere to put " + f"(modules: {', '.join(modules[:8])}). This is a Wan variant with extra conditioning " + "branches — Animate, S2V, Fun-Camera and similar — which InvokeAI cannot run faithfully; " + "loading it anyway would silently ignore that conditioning." + ) + + +def _tensor_shape(tensor: Any) -> tuple[int, ...]: + """Logical shape of a tensor, unwrapping GGMLTensor's packed storage. + + A GGMLTensor's ``.shape`` describes the packed quantized blob, not the weight, + so the logical dimensions live on ``.tensor_shape``. + """ + shape = tensor.tensor_shape if isinstance(tensor, GGMLTensor) else tensor.shape + return tuple(int(dim) for dim in shape) + + +def _build_wan_transformer_config(sd: dict, source: str) -> dict: + """Derive ``WanTransformer3DModel`` constructor kwargs from a state dict. + + The state dict must already be prefix-stripped and in the diffusers key + layout. Shared by the GGUF and single-file checkpoint loaders so a community + release is described by its own weights rather than by a hard-coded table of + known repos. + + ``source`` only flavours the error messages. + """ + num_layers = 0 + for key in sd.keys(): + if isinstance(key, str) and key.startswith("blocks."): + parts = key.split(".") + if len(parts) >= 2: + try: + num_layers = max(num_layers, int(parts[1]) + 1) + except ValueError: + pass + + def require(key: str) -> tuple[int, ...]: + tensor = sd.get(key) + if tensor is None: + raise RuntimeError(f"{source} is missing {key} after prefix strip and key conversion") + return _tensor_shape(tensor) + + # Patch embedding gives us in_channels (16/36=A14B, 48=TI2V-5B) and inner dim. + patch_shape = require("patch_embedding.weight") + inner_dim = patch_shape[0] + in_channels = patch_shape[1] + + # Wan uses head_dim=128 throughout the family; num_heads = inner_dim / 128. + attention_head_dim = 128 + num_attention_heads = inner_dim // attention_head_dim + + ffn_dim = require("blocks.0.ffn.net.0.proj.weight")[0] + + text_w = sd.get("condition_embedder.text_embedder.linear_1.weight") + text_dim = _tensor_shape(text_w)[1] if text_w is not None else 4096 + + # out_channels is read from proj_out.weight directly rather than assumed + # equal to in_channels: I2V-A14B has in_channels=36 (16 noise + 16 + # ref-image latents + 4 mask, concatenated by the denoise loop) but + # out_channels=16 (only the noise prediction comes back). proj_out is + # ``nn.Linear(inner_dim, out_channels * prod(patch_size))`` and + # patch_size is (1, 2, 2) → prod = 4 for the Wan 2.2 family. + out_channels = require("proj_out.weight")[0] // 4 + + # No fallback for num_layers. It cannot be zero here: that would mean no key starts + # with `blocks.`, and `require("blocks.0.ffn.net.0.proj.weight")` above has already + # raised. An earlier revision carried a variant-keyed default (40 for A14B, 30 for + # TI2V-5B) that was unreachable, and it was the only thing the `variant` argument + # was used for — so the config is now derived entirely from the weights, which is + # the point of this helper. + + return { + "patch_size": (1, 2, 2), + "in_channels": in_channels, + "out_channels": out_channels, + "num_layers": num_layers, + "attention_head_dim": attention_head_dim, + "num_attention_heads": num_attention_heads, + "ffn_dim": ffn_dim, + "text_dim": text_dim, + } + + @ModelLoaderRegistry.register(base=BaseModelType.Wan, type=ModelType.Main, format=ModelFormat.GGUFQuantized) class WanGGUFCheckpointModel(ModelLoader): """Loader for GGUF-quantized Wan 2.2 transformer models. @@ -207,6 +418,8 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: import accelerate from diffusers import WanTransformer3DModel + from invokeai.backend.util.logging import InvokeAILogger + model_path = Path(config.path) target_device = TorchDevice.choose_torch_device() compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) @@ -221,6 +434,8 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: } break + _drop_benign_extra_keys(sd, "GGUF state dict", InvokeAILogger.get_logger(self.__class__.__name__)) + # QuantStack and other community releases ship the native upstream Wan key # layout (text_embedding.0, self_attn/cross_attn, ffn.0/2, head.head, ...); # diffusers' WanTransformer3DModel expects condition_embedder.*, attn1/attn2, @@ -235,75 +450,97 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: # so the wrapper's underlying storage dtype reaches PyTorch directly). sd = _unwrap_unquantized_to_compute_dtype(sd) - # Auto-detect architecture from the state dict. - num_layers = 0 - for key in sd.keys(): - if isinstance(key, str) and key.startswith("blocks."): - parts = key.split(".") - if len(parts) >= 2: - try: - num_layers = max(num_layers, int(parts[1]) + 1) - except ValueError: - pass - - # Patch embedding gives us in_channels (16=A14B, 48=TI2V-5B) and inner dim. - patch_w = sd.get("patch_embedding.weight") - if patch_w is None: - raise RuntimeError("GGUF state dict missing patch_embedding.weight after prefix strip") - patch_shape = patch_w.tensor_shape if isinstance(patch_w, GGMLTensor) else patch_w.shape - inner_dim = int(patch_shape[0]) - in_channels = int(patch_shape[1]) - - # Wan uses head_dim=128 throughout the family; num_heads = inner_dim / 128. - attention_head_dim = 128 - num_attention_heads = inner_dim // attention_head_dim - - ffn_w = sd.get("blocks.0.ffn.net.0.proj.weight") - if ffn_w is None: - raise RuntimeError("GGUF state dict missing blocks.0.ffn.net.0.proj.weight after prefix strip") - ffn_shape = ffn_w.tensor_shape if isinstance(ffn_w, GGMLTensor) else ffn_w.shape - ffn_dim = int(ffn_shape[0]) - - text_w = sd.get("condition_embedder.text_embedder.linear_1.weight") - text_dim = 4096 - if text_w is not None: - text_shape = text_w.tensor_shape if isinstance(text_w, GGMLTensor) else text_w.shape - text_dim = int(text_shape[1]) - - # out_channels is read from proj_out.weight directly rather than assumed - # equal to in_channels: I2V-A14B has in_channels=36 (16 noise + 16 - # ref-image latents + 4 mask, concatenated by the denoise loop) but - # out_channels=16 (only the noise prediction comes back). proj_out is - # ``nn.Linear(inner_dim, out_channels * prod(patch_size))`` and - # patch_size is (1, 2, 2) → prod = 4 for the Wan 2.2 family. - proj_out_w = sd.get("proj_out.weight") - if proj_out_w is None: - raise RuntimeError("GGUF state dict missing proj_out.weight after prefix strip") - proj_out_shape = proj_out_w.tensor_shape if isinstance(proj_out_w, GGMLTensor) else proj_out_w.shape - out_channels = int(proj_out_shape[0]) // 4 - - # Layer count fallback (only triggers if the auto-count loop above - # found zero blocks, which shouldn't happen for a valid GGUF). T2V/I2V - # A14B have 40 layers; TI2V-5B has 30. - layer_count_fallback = 30 if config.variant == WanVariantType.TI2V_5B else 40 - - model_config: dict = { - "patch_size": (1, 2, 2), - "in_channels": in_channels, - "out_channels": out_channels, - "num_layers": num_layers if num_layers > 0 else layer_count_fallback, - "attention_head_dim": attention_head_dim, - "num_attention_heads": num_attention_heads, - "ffn_dim": ffn_dim, - "text_dim": text_dim, - } + model_config = _build_wan_transformer_config(sd, source="GGUF state dict") with accelerate.init_empty_weights(): model = WanTransformer3DModel(**model_config) incompatible_keys = model.load_state_dict(sd, strict=False, assign=True) - if incompatible_keys.missing_keys: - raise RuntimeError(f"GGUF state dict is missing model parameters: {incompatible_keys.missing_keys}") + _raise_for_incompatible_keys(incompatible_keys, source="GGUF state dict") + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Wan, type=ModelType.Main, format=ModelFormat.Checkpoint) +class WanCheckpointModel(ModelLoader): + """Loader for single-file Wan 2.2 transformer checkpoints (safetensors). + + This is what CivitAI fine-tunes and ComfyUI-oriented Hugging Face repos ship. + Handles the full matrix of community conventions: the optional + ``model.diffusion_model.`` key prefix, the native upstream key layout as well + as the diffusers one, ComfyUI ``fp8_scaled`` weights (dequantized to the + compute dtype at load time), and plain ``float8_e4m3fn`` weights with no + scales (cast the same way as any other non-bf16 dtype). + + Like the GGUF loader, one file is one expert; A14B pairing happens at the + WanModelLoaderInvocation layer. + """ + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Main_Checkpoint_Wan_Config): + raise TypeError(f"Expected Main_Checkpoint_Wan_Config, got {type(config).__name__}.") + + if submodel_type != SubModelType.Transformer: + raise ValueError( + "Only the Transformer submodel is available from a single-file Wan checkpoint. " + "Pair with a standalone Wan VAE and Wan T5 encoder for the other components." + ) + + return self._load_from_singlefile(config) + + def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: + import accelerate + from diffusers import WanTransformer3DModel + from safetensors.torch import load_file + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + model_path = Path(config.path) + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + sd = load_file(str(model_path)) + sd = _strip_comfyui_prefix(sd) + _drop_benign_extra_keys(sd, "Wan checkpoint", logger) + + dequantized = _dequantize_comfyui_fp8(sd, model_dtype) + if dequantized > 0: + logger.info(f"Dequantized {dequantized} ComfyUI-quantized weights") + # Drop the scale tensors themselves — they've been folded into the weights + # above and are not parameters of WanTransformer3DModel. load_state_dict + # runs with strict=False and would ignore them anyway, but dropping them + # here keeps the dtype cast and the RAM-cache reservation below honest. + _strip_quantization_metadata(sd) + + # Community releases ship the native upstream Wan key layout + # (text_embedding.0, self_attn/cross_attn, ffn.0/2, head.head, ...); + # diffusers' WanTransformer3DModel expects condition_embedder.*, + # attn1/attn2, ffn.net.*, proj_out. Convert if needed. + if _is_native_wan_layout(sd): + sd = _convert_wan_native_to_diffusers(sd) + + model_config = _build_wan_transformer_config(sd, source="checkpoint state dict") + + with accelerate.init_empty_weights(): + model = WanTransformer3DModel(**model_config) + + # Cast every float tensor to the compute dtype. Dequantized fp8_scaled + # weights are already there; this catches plain fp16/fp32/fp8 checkpoints + # and makes the cache reservation below reflect the post-cast sizes. + for key in list(sd.keys()): + if sd[key].is_floating_point(): + sd[key] = sd[key].to(model_dtype) + + new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values()) + self._ram_cache.make_room(new_sd_size) + + incompatible_keys = model.load_state_dict(sd, strict=False, assign=True) + _raise_for_incompatible_keys(incompatible_keys, source="Wan checkpoint") return model diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index 63051f90f00..163ce53ed01 100644 --- a/invokeai/frontend/web/openapi.json +++ b/invokeai/frontend/web/openapi.json @@ -877,6 +877,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -1296,6 +1299,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -1715,6 +1721,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -2179,6 +2188,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -2667,6 +2679,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -3985,6 +4000,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -14174,6 +14192,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -58015,6 +58036,183 @@ ], "title": "Main_Checkpoint_SDXL_Config" }, + "Main_Checkpoint_Wan_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "wan", + "title": "Base", + "default": "wan" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "variant": { + "$ref": "#/components/schemas/WanVariantType" + }, + "expert": { + "type": "string", + "enum": ["high", "low", "none"], + "title": "Expert", + "description": "For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, 'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B).", + "default": "none" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant", + "expert" + ], + "title": "Main_Checkpoint_Wan_Config", + "description": "Model config for single-file Wan 2.2 transformer checkpoints (safetensors).\n\nThis is the format the community ships on CivitAI and in ComfyUI-oriented\nHugging Face repos: one ``.safetensors`` per transformer, in either the native\nupstream key layout or the diffusers one, optionally under a\n``model.diffusion_model.`` prefix, and optionally ComfyUI ``fp8_scaled``\nquantized. The loader normalises all of those.\n\nAs with GGUF, A14B's MoE arrives as two files (one per expert); ``expert``\nrecords which one this is so the Wan model loader invocation can pair them.\nTI2V-5B is single-transformer and stores ``expert='none'``." + }, "Main_Checkpoint_ZImage_Config": { "properties": { "key": { @@ -66377,6 +66575,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -67042,6 +67243,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -67592,6 +67796,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -67998,6 +68205,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -68874,6 +69084,9 @@ { "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config" }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Wan_Config" + }, { "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" }, @@ -88743,7 +88956,7 @@ "category": "model", "class": "invocation", "classification": "prototype", - "description": "Apply a collection of LoRAs to the Wan 2.2 transformer(s).\n\nEach LoRA is routed to the primary and/or low-noise list based on its\nrecorded ``expert`` tag (set by the probe from the filename). Untagged\nLoRAs go to both lists.", + "description": "Apply a collection of LoRAs to the Wan 2.2 transformer(s).\n\nEach LoRA is routed to the primary and/or low-noise list based on its\nrecorded ``expert`` tag (set by the probe from the filename). Untagged\nLoRAs go to both lists.\n\nAgainst a TI2V-5B main, which is a single transformer with no low-noise\nexpert, a LoRA that would land only in the low-noise list is applied to\nthe transformer instead, with a warning.", "node_pack": "invokeai", "properties": { "id": { @@ -88833,7 +89046,7 @@ "category": "model", "class": "invocation", "classification": "prototype", - "description": "Apply a LoRA to the Wan 2.2 transformer(s).\n\nFor A14B (dual expert) the LoRA's recorded ``expert`` field determines\nwhich expert list it lands in: ``\"high\"`` -> primary list, ``\"low\"`` ->\nlow-noise list, ``None`` (untagged) -> both lists. Use the ``target``\nfield to override.\n\nFor TI2V-5B (single transformer) only the primary list is used at denoise\ntime; a LoRA routed only to the low-noise list would be inert, so that\nrouting logs a warning.", + "description": "Apply a LoRA to the Wan 2.2 transformer(s).\n\nFor A14B (dual expert) the LoRA's recorded ``expert`` field determines\nwhich expert list it lands in: ``\"high\"`` -> primary list, ``\"low\"`` ->\nlow-noise list, ``None`` (untagged) -> both lists. Use the ``target``\nfield to override.\n\nFor TI2V-5B (single transformer) only the primary list is used at denoise\ntime, so a LoRA that would land only in the low-noise list is applied to\nthe transformer instead, with a warning. The alternative is to accept the\nLoRA and silently have no effect.", "node_pack": "invokeai", "properties": { "id": { @@ -88890,7 +89103,7 @@ }, "target": { "default": "auto", - "description": "Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it.", + "description": "Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it. On the single-transformer TI2V-5B, which has no low-noise expert, 'low' is applied to the transformer instead of being discarded.", "enum": ["auto", "both", "high", "low"], "field_kind": "input", "input": "any", @@ -88974,7 +89187,7 @@ "category": "model", "class": "invocation", "classification": "prototype", - "description": "Loads a Wan 2.2 model, outputting its submodels.\n\nComponents can be mixed and matched, mirroring the Qwen Image loader pattern:\n\n- Transformer(s):\n * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/``\n from the same model record.\n * GGUF main: emits the single GGUF as the primary transformer; for A14B\n the second-expert GGUF must be wired to ``Transformer (Low Noise)``.\n- VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers).\n- UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) >\n Component Source (Diffusers).\n\nThe Component Source slot lets users supply a Diffusers Wan main model purely\nfor VAE / encoder extraction when the actual transformer is in a single-file\nformat. Together, the standalone VAE + standalone encoder let a GGUF\ntransformer run without a full ~30 GB Diffusers install.", + "description": "Loads a Wan 2.2 model, outputting its submodels.\n\nComponents can be mixed and matched, mirroring the Qwen Image loader pattern:\n\n- Transformer(s):\n * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/``\n from the same model record.\n * Single-file main (GGUF or safetensors checkpoint): emits the file as the\n primary transformer; for A14B the second-expert file must be wired to\n ``Transformer (Low Noise)``.\n- VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers).\n- UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) >\n Component Source (Diffusers).\n\nThe Component Source slot lets users supply a Diffusers Wan main model purely\nfor VAE / encoder extraction when the actual transformer is in a single-file\nformat. Together, the standalone VAE + standalone encoder let a single-file\ntransformer run without a full ~30 GB Diffusers install.", "node_pack": "invokeai", "properties": { "id": { @@ -89021,14 +89234,14 @@ } ], "default": null, - "description": "Optional second GGUF transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B.", + "description": "Optional second single-file transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF or safetensors checkpoint and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B.", "field_kind": "input", "input": "direct", "orig_default": null, "orig_required": false, "title": "Transformer (Low Noise)", "ui_model_base": ["wan"], - "ui_model_format": ["gguf_quantized"], + "ui_model_format": ["gguf_quantized", "checkpoint"], "ui_model_type": ["main"] }, "vae_model": { diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b7dbb0a82d7..045a7021e2d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1792,7 +1792,11 @@ "noFlux2DevVaeModelSelected": "No VAE selected. Non-diffusers FLUX.2 [dev] models require a standalone FLUX.2 VAE", "noFlux2DevMistralEncoderModelSelected": "No Mistral Encoder selected. Non-diffusers FLUX.2 [dev] models require a standalone Mistral text encoder", "noQwenImageComponentSourceSelected": "GGUF Qwen Image models require a Diffusers Component Source for VAE/encoder", - "noWanComponentSourceSelected": "GGUF Wan 2.2 models require a Diffusers Component Source for VAE/encoder", + "noWanComponentSourceSelected": "Single-file Wan 2.2 models require a Diffusers Component Source for VAE/encoder", + "incompatibleWanVae": "The selected VAE does not match the Wan transformer. TI2V-5B needs the 48-channel Wan 2.2 VAE; A14B needs the 16-channel Wan 2.1 VAE.", + "incompatibleWanComponentSource": "The selected Component Source does not match the Wan transformer. Pick a Diffusers Wan model of the same family (TI2V-5B or A14B).", + "incompatibleWanLowNoiseExpert": "The Transformer (Low Noise) model is a different Wan variant from the main transformer. Both experts must be the same variant.", + "duplicateWanTransformer": "The same model is selected as both Transformer and Transformer (Low Noise). An A14B expert pair needs two different models.", "noZImageVaeSourceSelected": "No VAE source: Select VAE (FLUX) or Qwen3 Source model", "noZImageQwen3EncoderSourceSelected": "No Qwen3 Encoder source: Select Qwen3 Encoder or Qwen3 Source model", "noKrea2VaeModelSelected": "Non-diffusers Krea-2: select a VAE in Advanced settings", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index bf9a2b7e2e9..fb7f9ee8434 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,4 +1,5 @@ import { logger } from 'app/logging/logger'; +import { getWanComponentUpdates } from 'app/store/middleware/listenerMiddleware/listeners/wanComponentSync'; import type { AppStartListening } from 'app/store/store'; import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; @@ -22,6 +23,7 @@ import { vaeSelected, wanComponentSourceSelected, wanT5EncoderModelSelected, + wanTransformerLowNoiseSelected, wanVaeModelSelected, zImageQwen3EncoderModelSelected, zImageQwen3SourceModelSelected, @@ -76,7 +78,12 @@ import { selectZImageDiffusersModels, } from 'services/api/hooks/modelsByType'; import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types'; -import { isExternalApiModelConfig, isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types'; +import { + isExternalApiModelConfig, + isFluxKontextModelConfig, + isFluxReduxModelConfig, + isWanSingleFileMainModelConfig, +} from 'services/api/types'; import { getKrea2ComponentUpdates } from './krea2ComponentSync'; @@ -619,50 +626,61 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - // Wan 2.2: auto-default Component Source / standalone VAE / standalone T5 encoder - // when the new model is Wan. Runs on every Wan selection (including same-base - // switches like Diffusers Wan → GGUF Wan) so the user doesn't have to dig into - // Advanced when picking a GGUF main. Only sets fields that are currently empty - // and only does it for GGUF mains — Diffusers mains carry everything themselves. + // Wan 2.2: keep Component Source / standalone VAE / standalone T5 encoder in step + // with the selected main. Runs on every Wan selection (including same-base + // switches like Diffusers Wan → single-file Wan) so the user doesn't have to dig + // into Advanced when picking a single-file main. + // + // This both fills empty slots and re-points ones left over from a previous + // selection: nothing else clears them (paramsSlice carries all four across a base + // change and modelsLoaded has no Wan handler), and the loader validates them + // against the new variant. if (newBase === 'wan') { const modelConfigsResult = selectModelConfigsQuery(state); const newModelConfig = modelConfigsResult.data ? modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key) : null; - const isNewModelGGUF = newModelConfig?.type === 'main' && newModelConfig.format === 'gguf_quantized'; - if (isNewModelGGUF) { - const { wanComponentSource, wanVaeModel, wanT5EncoderModel } = state.params; - // Match component source by variant family — A14B (t2v_a14b/i2v_a14b) and - // TI2V-5B use different VAEs (16-ch vs 48-ch); a mismatched component source - // would silently load the wrong VAE and produce broken images. The standalone - // VAE / encoder configs don't carry variant info, so those still go first-match. - const newVariant = - newModelConfig && 'variant' in newModelConfig && typeof newModelConfig.variant === 'string' - ? newModelConfig.variant + // Must stay in step with the readiness pre-flight: if that demands a VAE and + // encoder for this format but this doesn't offer to fill them, selecting the + // model immediately blocks Invoke with nothing populated. + if (newModelConfig) { + const { wanComponentSource, wanVaeModel, wanT5EncoderModel, wanTransformerLowNoise } = state.params; + const configFor = (identifier: { key: string } | null) => + identifier && modelConfigsResult.data + ? (modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, identifier.key) ?? null) : null; - const a14bFamily = newVariant === 't2v_a14b' || newVariant === 'i2v_a14b'; - if (!wanComponentSource) { - const availableWanDiffusers = selectWanDiffusersModels(state); - const matchingFamily = availableWanDiffusers.find((m) => { - const v = 'variant' in m && typeof m.variant === 'string' ? m.variant : null; - return a14bFamily ? v === 't2v_a14b' || v === 'i2v_a14b' : v === newVariant; - }); - const diffusersModel = matchingFamily ?? availableWanDiffusers[0]; - if (diffusersModel) { - dispatch(wanComponentSourceSelected(zModelIdentifierField.parse(diffusersModel))); - } + + const updates = getWanComponentUpdates({ + mainConfig: newModelConfig, + isSingleFileMain: isWanSingleFileMainModelConfig(newModelConfig), + selectedVae: configFor(wanVaeModel), + selectedComponentSource: configFor(wanComponentSource), + selectedEncoder: configFor(wanT5EncoderModel), + selectedLowNoisePartner: configFor(wanTransformerLowNoise), + availableVaes: selectWanVAEModels(state), + availableDiffusers: selectWanDiffusersModels(state), + availableEncoders: selectWanT5EncoderModels(state), + }); + + if (updates.vae !== undefined) { + dispatch(wanVaeModelSelected(updates.vae && zModelIdentifierField.parse(updates.vae))); } - if (!wanVaeModel) { - const vae = selectWanVAEModels(state)[0]; - if (vae) { - dispatch(wanVaeModelSelected(zModelIdentifierField.parse(vae))); - } + if (updates.componentSource !== undefined) { + dispatch( + wanComponentSourceSelected( + updates.componentSource && zModelIdentifierField.parse(updates.componentSource) + ) + ); } - if (!wanT5EncoderModel) { - const encoder = selectWanT5EncoderModels(state)[0]; - if (encoder) { - dispatch(wanT5EncoderModelSelected(zModelIdentifierField.parse(encoder))); - } + if (updates.encoder !== undefined) { + dispatch(wanT5EncoderModelSelected(updates.encoder && zModelIdentifierField.parse(updates.encoder))); + } + if (updates.lowNoisePartner !== undefined) { + dispatch( + wanTransformerLowNoiseSelected( + updates.lowNoisePartner && zModelIdentifierField.parse(updates.lowNoisePartner) + ) + ); } } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 130a0972963..6851e9fb44b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -53,6 +53,7 @@ import { isRefinerMainModelModelConfig, isSpandrelImageToImageModelConfig, isT5EncoderModelConfigOrSubmodel, + selectPrimaryMainModelOptions, } from 'services/api/types'; import type { JsonObject } from 'type-fest'; @@ -132,11 +133,19 @@ type ModelHandler = ( log: Logger ) => undefined; -const handleMainModels: ModelHandler = (models, state, dispatch, log) => { +export const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const selectedMainModel = state.params.model; const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); - const firstModel = allMainModels[0]; + // Availability and offerability are different questions, and conflating them here + // would be destructive. `selectPrimaryMainModelOptions` hides a Wan low-noise expert + // once its partner is installed — so if the *selected* model were tested against the + // filtered list, installing that partner would read as "your model vanished" and swap + // the user onto an unrelated model, firing the whole base-changed cascade (LoRAs + // disabled, VAE cleared, bbox resized) for an action that was just a file install. + // Keep the selection test on what exists; filter only what we may pick *for* them. + const selectableModels = selectPrimaryMainModelOptions(allMainModels); + const firstModel = selectableModels[0] ?? allMainModels[0]; // If we have no models, we may need to clear the selected model if (!firstModel) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.wan.test.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.wan.test.ts new file mode 100644 index 00000000000..9524824829f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.wan.test.ts @@ -0,0 +1,97 @@ +import type { RootState } from 'app/store/store'; +import { modelSelected } from 'features/parameters/store/actions'; +import type { AnyModelConfig } from 'services/api/types'; +import { describe, expect, it, vi } from 'vitest'; + +import { handleMainModels } from './modelsLoaded'; + +/** + * A Wan low-noise expert belongs in the Transformer (Low Noise) slot; running it alone as + * the primary main is accepted by the loader since #9505 but gives visibly worse output. + * So no path that chooses a primary main *on the user's behalf* should reach for one + * while its partner is installed. This listener is the least visible of the three: it + * fires on every `getModelConfigs` fulfilment and swaps the selection silently. + * + * Which is exactly why it must not treat "hidden" as "uninstalled" — see the last test. + */ + +const wanHighExpert = { + key: 'wan-high', + hash: 'h', + name: 'Wan2.2-T2V-A14B-HIGH', + base: 'wan', + type: 'main', + format: 'checkpoint', + variant: 't2v_a14b', + expert: 'high', +} as unknown as AnyModelConfig; + +const wanLowExpert = { + ...wanHighExpert, + key: 'wan-low', + name: 'Wan2.2-T2V-A14B-LOW', + expert: 'low', +} as unknown as AnyModelConfig; + +const sdxlModel = { + key: 'sdxl', + hash: 'h', + name: 'SDXL', + base: 'sdxl', + type: 'main', + format: 'checkpoint', +} as unknown as AnyModelConfig; + +const makeState = () => ({ params: { model: null } }) as unknown as RootState; + +const log = { debug: vi.fn(), info: vi.fn(), error: vi.fn(), warn: vi.fn() } as never; + +describe('handleMainModels — Wan low-noise experts', () => { + it('auto-selects the high-noise expert over the low-noise one regardless of order', () => { + const dispatch = vi.fn(); + handleMainModels([wanLowExpert, wanHighExpert], makeState(), dispatch, log); + + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith(modelSelected(wanHighExpert)); + }); + + it('does offer the low-noise expert when its partner is not installed', () => { + // Since #9505 the loader runs an unpaired low expert with a warning instead of + // refusing it, so hiding it here with no alternative would be a dead end — the + // user's only Wan model would be missing from every picker. + const dispatch = vi.fn(); + handleMainModels([wanLowExpert], makeState(), dispatch, log); + + expect(dispatch).toHaveBeenCalledWith(modelSelected(wanLowExpert)); + }); + + it('treats a different-variant high expert as no partner at all', () => { + // An I2V high expert cannot pair with a T2V low one — the loader rejects the + // variant mismatch — so it must not be the reason the T2V low expert is hidden. + const dispatch = vi.fn(); + const i2vHigh = { ...wanHighExpert, key: 'wan-i2v-high', variant: 'i2v_a14b' } as unknown as AnyModelConfig; + handleMainModels([wanLowExpert, i2vHigh], makeState(), dispatch, log); + + // Both remain offerable; the sort leaves the list order, so the low expert is first. + expect(dispatch).toHaveBeenCalledWith(modelSelected(wanLowExpert)); + }); + + it('does not swap the user off a selected low expert when its partner is installed', () => { + // The regression this guards: hiding is a *visibility* rule, and the availability + // check must not use it. Otherwise installing the high-noise partner reads as "your + // model was uninstalled" and silently moves the user to another model — firing the + // base-changed cascade for what was just a file install. + const dispatch = vi.fn(); + const state = { params: { model: wanLowExpert } } as unknown as RootState; + handleMainModels([wanLowExpert, wanHighExpert, sdxlModel], state, dispatch, log); + + expect(dispatch).not.toHaveBeenCalled(); + }); + + it('never auto-selects a hidden low expert when a partner exists', () => { + const dispatch = vi.fn(); + handleMainModels([wanLowExpert, wanHighExpert, sdxlModel], makeState(), dispatch, log); + + expect(dispatch).toHaveBeenCalledWith(modelSelected(sdxlModel)); + }); +}); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.test.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.test.ts new file mode 100644 index 00000000000..4ae9face91e --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.test.ts @@ -0,0 +1,190 @@ +import type { AnyModelConfig } from 'services/api/types'; +import { describe, expect, it } from 'vitest'; + +import { getWanComponentUpdates } from './wanComponentSync'; + +const a14bCheckpoint = { + key: 'a14b-ckpt', + hash: 'h', + name: 'Wan2.2 T2V A14B', + base: 'wan', + type: 'main', + format: 'checkpoint', + variant: 't2v_a14b', + expert: 'high', +} as unknown as AnyModelConfig; + +const ti2v5bCheckpoint = { + ...a14bCheckpoint, + key: 'ti2v-ckpt', + name: 'Wan2.2 TI2V 5B', + variant: 'ti2v_5b', + expert: 'none', +} as unknown as AnyModelConfig; + +const a14bDiffusers = { + key: 'a14b-diffusers', + hash: 'h', + name: 'Wan2.2 T2V A14B Diffusers', + base: 'wan', + type: 'main', + format: 'diffusers', + variant: 't2v_a14b', +} as unknown as AnyModelConfig; + +const ti2v5bDiffusers = { + ...a14bDiffusers, + key: 'ti2v-diffusers', + name: 'Wan2.2 TI2V 5B Diffusers', + variant: 'ti2v_5b', +} as unknown as AnyModelConfig; + +/** 16-channel Wan 2.1 VAE — what A14B needs. */ +const vae16 = { + key: 'vae-16', + hash: 'h', + name: 'Wan 2.1 VAE', + base: 'wan', + type: 'vae', + latent_channels: 16, +} as unknown as AnyModelConfig; + +/** 48-channel Wan 2.2 VAE — what TI2V-5B needs. */ +const vae48 = { + ...vae16, + key: 'vae-48', + name: 'Wan 2.2 VAE', + latent_channels: 48, +} as unknown as AnyModelConfig; + +const encoder = { + key: 'umt5', + hash: 'h', + name: 'UMT5-XXL', + base: 'wan', + type: 'wan_t5_encoder', +} as unknown as AnyModelConfig; + +const build = (overrides: Partial[0]> = {}) => + getWanComponentUpdates({ + mainConfig: a14bCheckpoint, + isSingleFileMain: true, + selectedVae: null, + selectedComponentSource: null, + selectedEncoder: null, + selectedLowNoisePartner: null, + availableVaes: [], + availableDiffusers: [], + availableEncoders: [], + ...overrides, + }); + +describe('getWanComponentUpdates', () => { + it('fills empty slots for a single-file main', () => { + expect( + build({ availableVaes: [vae16], availableDiffusers: [a14bDiffusers], availableEncoders: [encoder] }) + ).toEqual({ vae: vae16, componentSource: a14bDiffusers, encoder }); + }); + + it('picks the VAE by latent_channels, not by install order', () => { + // The 16-channel VAE is installed first. A TI2V-5B main needs the 48-channel one; + // first-match would wire the wrong VAE and the loader would reject it. + expect(build({ mainConfig: ti2v5bCheckpoint, availableVaes: [vae16, vae48] }).vae).toEqual(vae48); + expect(build({ mainConfig: a14bCheckpoint, availableVaes: [vae48, vae16] }).vae).toEqual(vae16); + }); + + it('never falls back to a mismatched Component Source', () => { + // Only an A14B Diffusers model is installed and the main is TI2V-5B. Wiring it would + // load the 16-channel VAE for a 48-channel transformer. + expect( + build({ mainConfig: ti2v5bCheckpoint, availableDiffusers: [a14bDiffusers] }).componentSource + ).toBeUndefined(); + expect(build({ mainConfig: ti2v5bCheckpoint, availableDiffusers: [ti2v5bDiffusers] }).componentSource).toEqual( + ti2v5bDiffusers + ); + }); + + it('re-points slots left over from a previous main of a different variant', () => { + // A14B was selected, auto-filling the 16-channel VAE; now TI2V-5B is selected. + expect( + build({ + mainConfig: ti2v5bCheckpoint, + selectedVae: vae16, + selectedComponentSource: a14bDiffusers, + availableVaes: [vae16, vae48], + availableDiffusers: [a14bDiffusers, ti2v5bDiffusers], + }) + ).toEqual({ vae: vae48, componentSource: ti2v5bDiffusers }); + }); + + it('clears an incompatible slot when nothing compatible is installed', () => { + expect( + build({ + mainConfig: ti2v5bCheckpoint, + selectedVae: vae16, + selectedComponentSource: a14bDiffusers, + availableVaes: [vae16], + availableDiffusers: [a14bDiffusers], + }) + ).toEqual({ vae: null, componentSource: null }); + }); + + it('leaves a compatible selection alone', () => { + expect( + build({ + selectedVae: vae16, + selectedComponentSource: a14bDiffusers, + selectedEncoder: encoder, + availableVaes: [vae16, vae48], + availableDiffusers: [a14bDiffusers, ti2v5bDiffusers], + availableEncoders: [encoder], + }) + ).toEqual({}); + }); + + it('wires nothing at all for a self-contained Diffusers main', () => { + // It carries its own VAE and encoder. Auto-wiring a standalone VAE would silently + // override the one the model ships with — the loader ranks a wired standalone VAE + // above a Diffusers main's own — and the user could not undo it: clearing the + // combobox would just refill on the next selection. + expect( + build({ + mainConfig: a14bDiffusers, + isSingleFileMain: false, + availableVaes: [vae16], + availableDiffusers: [a14bDiffusers], + availableEncoders: [encoder], + }) + ).toEqual({}); + }); + + it('still corrects an incompatible VAE already wired against a Diffusers main', () => { + // Filling and correcting are different: a wired VAE outranks the Diffusers main's + // own, so an incompatible one is the user's problem to see fixed either way. + expect( + build({ + mainConfig: ti2v5bDiffusers, + isSingleFileMain: false, + selectedVae: vae16, + availableVaes: [vae16, vae48], + }) + ).toEqual({ vae: vae48 }); + }); + + it('clears a low-noise partner whose variant no longer matches the main', () => { + // Exact variant equality, stricter than the VAE's TI2V/A14B split — switching + // t2v -> i2v leaves a partner the loader refuses. There is no safe auto-repoint + // (which file is the partner is the user's call), so it is cleared. + const t2vLow = { ...a14bCheckpoint, key: 't2v-low', expert: 'low' } as unknown as AnyModelConfig; + const i2vMain = { ...a14bCheckpoint, key: 'i2v-high', variant: 'i2v_a14b' } as unknown as AnyModelConfig; + + expect(build({ mainConfig: i2vMain, selectedLowNoisePartner: t2vLow }).lowNoisePartner).toBeNull(); + expect(build({ mainConfig: a14bCheckpoint, selectedLowNoisePartner: t2vLow }).lowNoisePartner).toBeUndefined(); + }); + + it('treats a slot pointing at a deleted model as empty', () => { + // The caller resolves identifiers against installed models and passes null when the + // lookup fails, so a deleted VAE is re-filled rather than left dangling. + expect(build({ selectedVae: null, availableVaes: [vae16] }).vae).toEqual(vae16); + }); +}); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts new file mode 100644 index 00000000000..d2bfe1bb2dc --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts @@ -0,0 +1,144 @@ +import type { AnyModelConfig } from 'services/api/types'; + +/** + * Wan 2.2 single-file mains are transformer-only: the VAE and UMT5-XXL encoder have to + * come from standalone models or from a Diffusers "Component Source". This computes + * which of those slots need to change when the main model is selected. + * + * It both fills empty slots and re-points stale ones. Nothing else clears them — + * `paramsSlice` carries all four across a base change and `modelsLoaded` has no Wan + * handler — so a slot auto-filled for a previous main survives into the next one, where + * the loader validates it against the new variant and refuses. + * + * The variant matters because A14B (t2v_a14b / i2v_a14b) and TI2V-5B use different VAEs: + * 16-channel Wan 2.1 vs 48-channel Wan 2.2. `WanModelLoaderInvocation._validate_standalone_vae` + * and `_validate_component_source_vae` reject a mismatch outright. + */ + +type Identifier = { key: string } | null; + +type WanComponentUpdates = { + /** Present only when the slot should change. `null` means clear it. */ + vae?: AnyModelConfig | null; + componentSource?: AnyModelConfig | null; + encoder?: AnyModelConfig | null; + lowNoisePartner?: AnyModelConfig | null; +}; + +const variantOf = (model: unknown): string | null => + model && typeof model === 'object' && 'variant' in model && typeof model.variant === 'string' ? model.variant : null; + +/** Exported so the readiness pre-flight can gate on the same single-expert test the + * loader uses, instead of restating `variant === 'ti2v_5b'` in a second place. */ +export const isWanTi2v5b = (model: unknown): boolean => variantOf(model) === 'ti2v_5b'; + +/** Mirrors `WanModelLoaderInvocation._validate_standalone_vae`: TI2V-5B needs the + * 48-channel Wan 2.2 VAE, A14B the 16-channel Wan 2.1 one. Exported so the readiness + * pre-flight tests the same rule rather than a second, drifting copy of it. */ +export const isWanVaeCompatible = (mainConfig: unknown, vae: unknown): boolean => + !!vae && + typeof vae === 'object' && + 'latent_channels' in vae && + vae.latent_channels === (isWanTi2v5b(mainConfig) ? 48 : 16); + +/** Mirrors `_validate_component_source_vae` plus `_validate_component_source_format`: + * the source must be a Diffusers Wan main on the same side of the TI2V-5B / A14B split, + * because its VAE is what gets used. */ +export const isWanComponentSourceCompatible = (mainConfig: unknown, source: unknown): boolean => + !!source && + typeof source === 'object' && + 'format' in source && + source.format === 'diffusers' && + isWanTi2v5b(source) === isWanTi2v5b(mainConfig); + +/** The low-noise partner must match the main's variant exactly — a stricter rule than the + * TI2V/A14B split above. See `wan_model_loader.py`'s "must use the same Wan variant". */ +export const isWanLowNoisePartnerCompatible = (mainConfig: unknown, partner: unknown): boolean => + variantOf(partner) === variantOf(mainConfig); + +export const getWanComponentUpdates = (arg: { + /** The newly selected Wan main model's config. */ + mainConfig: AnyModelConfig; + /** True for GGUF / safetensors-checkpoint mains; false for Diffusers. */ + isSingleFileMain: boolean; + /** + * Configs of the currently wired slots, resolved against the installed models — + * `null` when the slot is empty *or* points at a model that no longer exists, which + * are handled the same way. + */ + selectedVae: AnyModelConfig | null; + selectedComponentSource: AnyModelConfig | null; + selectedEncoder: Identifier; + selectedLowNoisePartner: AnyModelConfig | null; + availableVaes: AnyModelConfig[]; + availableDiffusers: AnyModelConfig[]; + availableEncoders: AnyModelConfig[]; +}): WanComponentUpdates => { + const { + mainConfig, + isSingleFileMain, + selectedVae, + selectedComponentSource, + selectedEncoder, + selectedLowNoisePartner, + availableVaes, + availableDiffusers, + availableEncoders, + } = arg; + + const updates: WanComponentUpdates = {}; + + const vaeIsCompatible = (model: unknown) => isWanVaeCompatible(mainConfig, model); + const sourceIsCompatible = (model: unknown) => isWanComponentSourceCompatible(mainConfig, model); + + // A wired standalone VAE outranks every other source in the loader — including a + // Diffusers main's own — so an incompatible one has to be corrected for *any* Wan main. + // Filling an empty slot is different: only a single-file main needs one. Auto-wiring a + // standalone VAE for a self-contained Diffusers main would silently override the VAE it + // ships with, and the user could not undo it (clearing the combobox would just refill + // on the next selection). + if (selectedVae && !vaeIsCompatible(selectedVae)) { + updates.vae = availableVaes.find(vaeIsCompatible) ?? null; + } else if (!selectedVae && isSingleFileMain) { + const vae = availableVaes.find(vaeIsCompatible); + if (vae) { + updates.vae = vae; + } + } + + // The Component Source only feeds a single-file main; a Diffusers main carries its own + // components and the loader never consults it for the VAE. + if (isSingleFileMain) { + if (!selectedComponentSource || !sourceIsCompatible(selectedComponentSource)) { + // No "any Wan Diffusers model" fallback. Picking an arbitrary one here produces + // exactly the mismatch the loader validation exists to catch. Clearing when nothing + // fits is deliberate: an empty slot reads as "pick one" in the UI, a stale one reads + // as already handled. + const source = availableDiffusers.find(sourceIsCompatible); + if (source) { + updates.componentSource = source; + } else if (selectedComponentSource) { + updates.componentSource = null; + } + } + + // The UMT5-XXL encoder is shared across every Wan variant, so first-match is correct + // and there is nothing to re-validate beyond the model still existing. + if (!selectedEncoder) { + const encoder = availableEncoders[0]; + if (encoder) { + updates.encoder = encoder; + } + } + } + + // The low-noise partner. Its check is exact variant equality, stricter than the VAE's + // TI2V/A14B split, so switching t2v -> i2v leaves a partner the loader will refuse. + // There is no safe auto-repoint here — which file is the partner is the user's call — + // so an incompatible one is cleared and the slot goes back to reading "pick one". + if (selectedLowNoisePartner && !isWanLowNoisePartnerCompatible(mainConfig, selectedLowNoisePartner)) { + updates.lowNoisePartner = null; + } + + return updates; +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamWanModelSelects.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamWanModelSelects.tsx index 49dd38cd9cd..8c8d2b57aca 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamWanModelSelects.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamWanModelSelects.tsx @@ -16,7 +16,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useWanDiffusersModels, - useWanGGUFLowNoiseModels, + useWanSingleFileLowNoiseModels, useWanT5EncoderModels, useWanVAEModels, } from 'services/api/hooks/modelsByType'; @@ -25,15 +25,16 @@ import type { MainModelConfig, VAEModelConfig, WanT5EncoderModelConfig } from 's /** * Wan 2.2 Transformer (Low Noise) Select * - * Picks the second-expert GGUF transformer for an A14B MoE workflow. Only - * relevant when the main Wan model is a GGUF — Diffusers A14B already carries - * both experts in transformer/ and transformer_2/ subfolders. + * Picks the second-expert transformer for an A14B MoE workflow. Only relevant + * when the main Wan model is a single file (GGUF or safetensors checkpoint) — + * Diffusers A14B already carries both experts in transformer/ and + * transformer_2/ subfolders. */ const ParamWanTransformerLowNoiseSelect = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const value = useAppSelector(selectWanTransformerLowNoise); - const [modelConfigs, { isLoading }] = useWanGGUFLowNoiseModels(); + const [modelConfigs, { isLoading }] = useWanSingleFileLowNoiseModels(); const _onChange = useCallback( (model: MainModelConfig | null) => { @@ -79,9 +80,9 @@ ParamWanTransformerLowNoiseSelect.displayName = 'ParamWanTransformerLowNoiseSele * Wan 2.2 Component Source Select * * Picks a Diffusers Wan model whose VAE and UMT5-XXL encoder will be extracted - * for the workflow. Required when the main Wan model is a GGUF (since GGUF - * mains are transformer-only). Ignored for Diffusers mains, which carry their - * own VAE and encoder. + * for the workflow. Required when the main Wan model is a single file (GGUF or + * safetensors checkpoint), since those are transformer-only. Ignored for + * Diffusers mains, which carry their own VAE and encoder. */ const ParamWanComponentSourceSelect = memo(() => { const dispatch = useAppDispatch(); @@ -227,7 +228,7 @@ ParamWanT5EncoderModelSelect.displayName = 'ParamWanT5EncoderModelSelect'; * Combined Wan 2.2 component selectors (low-noise transformer + standalone * VAE + standalone T5 encoder + Component Source). * - * Only relevant for GGUF workflows. Diffusers Wan mains have everything + * Only relevant for single-file workflows. Diffusers Wan mains have everything * built in; TI2V-5B is a single-expert model with no low-noise pair. Showing * these always is fine since they're optional — but the AdvancedSettingsAccordion * still gates the render on `isWan` so they don't pollute other tabs. diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts index 8b0fc55db5e..ca81039bf99 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts @@ -12,7 +12,7 @@ vi.mock('i18next', () => ({ import type { ParamsState, RefImagesState } from 'features/controlLayers/store/types'; import type { DynamicPromptsState } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import type { MainModelConfig } from 'services/api/types'; +import type { AnyModelConfig, MainModelConfig } from 'services/api/types'; import { getReasonsWhyCannotEnqueueCanvasTab, getReasonsWhyCannotEnqueueGenerateTab } from './readiness'; @@ -113,6 +113,7 @@ const buildGenerateTabArg = (overrides: { hasFlux2DiffusersVaeSource: overrides.hasFlux2DiffusersVaeSource ?? false, hasFlux2DiffusersQwen3Source: overrides.hasFlux2DiffusersQwen3Source ?? false, hasFlux2DevDiffusersSource: overrides.hasFlux2DevDiffusersSource ?? false, + wanWiredConfigs: { vae: null, componentSource: null, lowNoisePartner: null }, }); const buildCanvasTabArg = (overrides: { @@ -152,6 +153,7 @@ const buildCanvasTabArg = (overrides: { hasFlux2DiffusersVaeSource: overrides.hasFlux2DiffusersVaeSource ?? false, hasFlux2DiffusersQwen3Source: overrides.hasFlux2DiffusersQwen3Source ?? false, hasFlux2DevDiffusersSource: overrides.hasFlux2DevDiffusersSource ?? false, + wanWiredConfigs: { vae: null, componentSource: null, lowNoisePartner: null }, }); const hasFlux2VaeReason = (reasons: { content: string }[]) => @@ -307,6 +309,7 @@ const buildZImageTabArg = (overrides: { hasFlux2DiffusersVaeSource: false, hasFlux2DiffusersQwen3Source: false, hasFlux2DevDiffusersSource: false, + wanWiredConfigs: { vae: null, componentSource: null, lowNoisePartner: null }, }); const hasZImageVaeReason = (reasons: { content: string }[]) => @@ -482,6 +485,7 @@ const buildPidCanvasArg = (model: MainModelConfig, bboxSide: number) => ({ hasFlux2DiffusersVaeSource: false, hasFlux2DiffusersQwen3Source: false, hasFlux2DevDiffusersSource: false, + wanWiredConfigs: { vae: null, componentSource: null, lowNoisePartner: null }, }); const hasBboxGridReason = (reasons: { content: string }[]) => @@ -689,3 +693,352 @@ describe('FLUX.1 readiness – self-contained SDNQ pipeline', () => { expect(flux1ComponentReasons(reasons)).toHaveLength(3); }); }); + +// --- Wan 2.2 ----------------------------------------------------------------- +// +// Regression cover for #9463: single-file Wan mains are transformer-only and need a +// VAE + UMT5-XXL encoder from elsewhere. That was originally gated on the GGUF +// format alone, so when the safetensors checkpoint format was added the pre-flight +// silently skipped it and Invoke was enabled for a graph that could only fail in +// the model loader. + +const wanGgufModel = { + key: 'wan-gguf', + hash: 'h', + name: 'Wan 2.2 T2V A14B GGUF', + base: 'wan', + type: 'main', + format: 'gguf_quantized', + variant: 't2v_a14b', + expert: 'high', +} as unknown as MainModelConfig; + +const wanCheckpointModel = { + key: 'wan-checkpoint', + hash: 'h', + name: 'Wan 2.2 T2V A14B safetensors', + base: 'wan', + type: 'main', + format: 'checkpoint', + variant: 't2v_a14b', + expert: 'high', +} as unknown as MainModelConfig; + +const wanDiffusersModel = { + key: 'wan-diffusers', + hash: 'h', + name: 'Wan 2.2 T2V A14B Diffusers', + base: 'wan', + type: 'main', + format: 'diffusers', + variant: 't2v_a14b', +} as unknown as MainModelConfig; + +/** A14B checkpoint whose filename carried no high/low marker, so the probe recorded + * expert='none'. Extremely common on community finetunes — the tag is a filename + * heuristic and there is no UI to correct it. */ +const wanUntaggedA14bModel = { + key: 'wan-untagged', + hash: 'h', + name: 'wan2.2_t2v_A14B_fp8_e4m3fn', + base: 'wan', + type: 'main', + format: 'checkpoint', + variant: 't2v_a14b', + expert: 'none', +} as unknown as MainModelConfig; + +const wanLowExpertModel = { + ...wanUntaggedA14bModel, + key: 'wan-low', + name: 'Wan2.2-T2V-A14B-LOW', + expert: 'low', +} as unknown as MainModelConfig; + +/** TI2V-5B is single-transformer, so the A14B expert pairing does not apply to it. */ +const wanTi2v5bModel = { + key: 'wan-5b', + hash: 'h', + name: 'Wan2.2 TI2V 5B', + base: 'wan', + type: 'main', + format: 'checkpoint', + variant: 'ti2v_5b', + expert: 'none', +} as unknown as MainModelConfig; + +const buildWanTabArg = (overrides: { + model?: MainModelConfig | null; + wanVaeModel?: unknown; + wanT5EncoderModel?: unknown; + wanComponentSource?: unknown; + wanTransformerLowNoise?: unknown; + wiredVae?: unknown; + wiredComponentSource?: unknown; + wiredLowNoisePartner?: unknown; +}) => ({ + isConnected: true, + model: overrides.model ?? wanCheckpointModel, + params: { + ...baseParams, + wanVaeModel: overrides.wanVaeModel ?? null, + wanT5EncoderModel: overrides.wanT5EncoderModel ?? null, + wanComponentSource: overrides.wanComponentSource ?? null, + wanTransformerLowNoise: overrides.wanTransformerLowNoise ?? null, + model: overrides.model ?? wanCheckpointModel, + } as unknown as ParamsState, + refImages: baseRefImages, + loras: [], + dynamicPrompts: baseDynamicPrompts, + hasFlux2DiffusersVaeSource: false, + hasFlux2DiffusersQwen3Source: false, + hasFlux2DevDiffusersSource: false, + wanWiredConfigs: { + vae: (overrides.wiredVae ?? null) as AnyModelConfig | null, + componentSource: (overrides.wiredComponentSource ?? null) as AnyModelConfig | null, + lowNoisePartner: (overrides.wiredLowNoisePartner ?? null) as AnyModelConfig | null, + }, +}); + +const buildWanCanvasArg = (overrides: Parameters[0]) => + ({ + ...buildWanTabArg(overrides), + canvas: { + bbox: { + scaleMethod: 'none', + rect: { width: 1024, height: 1024 }, + scaledSize: { width: 1024, height: 1024 }, + }, + controlLayers: { entities: [] }, + regionalGuidance: { entities: [] }, + rasterLayers: { entities: [] }, + inpaintMasks: { entities: [] }, + }, + canvasIsFiltering: false, + canvasIsTransforming: false, + canvasIsRasterizing: false, + canvasIsCompositing: false, + canvasIsSelectingObject: false, + }) as never; + +const hasWanComponentReason = (reasons: { content: string }[]) => + reasons.some((r) => r.content.includes('noWanComponentSourceSelected')); + +// Since #9505 the loader takes the A14B expert pairing from the wiring rather than the +// filename tag: an unpaired or untagged A14B runs with a warning instead of raising. The +// pre-flight must therefore NOT block on `expert`, or it would stop a generation the +// backend is happy to run — untagged community checkpoints are the common case this +// whole branch exists to support. +const vae16 = { key: 'vae16', name: 'Wan 2.1 VAE', base: 'wan', type: 'vae', latent_channels: 16 }; +const vae48 = { key: 'vae48', name: 'Wan 2.2 VAE', base: 'wan', type: 'vae', latent_channels: 48 }; +const a14bDiffusers = { + key: 'a14b-diff', + name: 'A14B Diffusers', + base: 'wan', + type: 'main', + format: 'diffusers', + variant: 't2v_a14b', +}; +const ti2vDiffusers = { + key: 'ti2v-diff', + name: 'TI2V Diffusers', + base: 'wan', + type: 'main', + format: 'diffusers', + variant: 'ti2v_5b', +}; + +// The Advanced comboboxes offer every Wan VAE and every Wan Diffusers main with no +// variant filtering, and nothing re-runs the auto-fill when the user picks by hand — so +// presence alone is not enough. These are the loader's own checks, mirrored. +describe('Wan 2.2 component compatibility pre-flight', () => { + const src = { wanComponentSource: { key: 'src' } }; + + it('blocks a VAE whose channel count does not match the transformer', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanTi2v5bModel, ...src, wanVaeModel: vae16, wiredVae: vae16 }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanVae'))).toBe(true); + }); + + it('accepts the matching VAE for each variant', () => { + for (const [model, vae] of [ + [wanTi2v5bModel, vae48], + [wanCheckpointModel, vae16], + ] as const) { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model, ...src, wanVaeModel: vae, wiredVae: vae }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanVae'))).toBe(false); + } + }); + + it('checks the VAE for a Diffusers main too — a wired standalone outranks its own', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanDiffusersModel, wanVaeModel: vae48, wiredVae: vae48 }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanVae'))).toBe(true); + }); + + it('blocks a Component Source from the other variant family', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanTi2v5bModel, wanComponentSource: a14bDiffusers, wiredComponentSource: a14bDiffusers }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanComponentSource'))).toBe(true); + }); + + it('accepts a same-family Component Source', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanTi2v5bModel, wanComponentSource: ti2vDiffusers, wiredComponentSource: ti2vDiffusers }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanComponentSource'))).toBe(false); + }); + + it('blocks a low-noise partner of a different variant', () => { + const i2vLow = { ...wanLowExpertModel, key: 'i2v-low', variant: 'i2v_a14b' }; + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ + model: wanCheckpointModel, + ...src, + wanTransformerLowNoise: i2vLow, + wiredLowNoisePartner: i2vLow, + }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanLowNoiseExpert'))).toBe(true); + }); + + it('ignores a low-noise partner wired against a TI2V-5B main', () => { + // The loader logs "'Transformer (Low Noise)' is ignored for the single-expert TI2V-5B + // variant" and skips the pairing block entirely, so blocking here disables Invoke over + // a wire the backend shrugs off. For TI2V-5B it is unavoidable rather than occasional: + // the partner picker cannot offer a 5B (single-transformer models have no partner), so + // every possible pick is a variant mismatch — and the combobox is rendered for every + // Wan main. Recovery would mean clearing a combobox the error text never names. + const a14bLow = { ...wanLowExpertModel, key: 'a14b-low', variant: 't2v_a14b' }; + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ + model: wanTi2v5bModel, + wanComponentSource: ti2vDiffusers, + wiredComponentSource: ti2vDiffusers, + wanVaeModel: vae48, + wiredVae: vae48, + wanTransformerLowNoise: a14bLow, + wiredLowNoisePartner: a14bLow, + }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanLowNoiseExpert'))).toBe(false); + }); + + it('ignores a low-noise partner wired against a Diffusers main', () => { + // The Diffusers branch of the loader never reads the slot — a dual-expert Diffusers + // A14B carries both experts as its own submodels. + const i2vLow = { ...wanLowExpertModel, key: 'i2v-low', variant: 'i2v_a14b' }; + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ + model: wanDiffusersModel, + wanTransformerLowNoise: i2vLow, + wiredLowNoisePartner: i2vLow, + }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanLowNoiseExpert'))).toBe(false); + }); + + it('blocks the same model wired to both transformer slots', () => { + // Reachable whenever a low expert has no partner: it is offered by the main picker + // (nothing else to offer) and by the low-noise picker at the same time. + const params = { key: wanLowExpertModel.key }; + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ + model: wanLowExpertModel, + ...src, + wanTransformerLowNoise: params, + wiredLowNoisePartner: wanLowExpertModel, + }) + ); + expect(reasons.some((r) => r.content.includes('duplicateWanTransformer'))).toBe(true); + }); + + it('does not judge a slot pointing at a deleted model as incompatible', () => { + // wiredVae null means "empty or dangling"; neither can be checked for compatibility. + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanTi2v5bModel, ...src, wanVaeModel: { key: 'gone' }, wiredVae: null }) + ); + expect(reasons.some((r) => r.content.includes('incompatibleWanVae'))).toBe(false); + }); +}); + +describe('Wan 2.2 A14B expert pairing is not a readiness blocker', () => { + const withComponents = { wanComponentSource: { key: 'src' } }; + + it.each([ + ['untagged (expert=none)', wanUntaggedA14bModel], + ['the low-noise expert', wanLowExpertModel], + ['the high-noise expert', wanCheckpointModel], + ['TI2V-5B', wanTi2v5bModel], + ])('does not block an unpaired A14B main that is %s', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model, ...withComponents })); + expect(reasons).toEqual([]); + }); + + it('also does not block on the canvas tab', () => { + const reasons = getReasonsWhyCannotEnqueueCanvasTab( + buildWanCanvasArg({ model: wanUntaggedA14bModel, ...withComponents }) + ); + expect(reasons).toEqual([]); + }); + + it('still blocks when the VAE/encoder source is missing, whatever the expert', () => { + // The component-source rule is independent of the pairing and must survive. + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model: wanUntaggedA14bModel })); + expect(hasWanComponentReason(reasons)).toBe(true); + }); +}); + +describe('Wan 2.2 readiness checks – generate tab', () => { + it.each([ + ['GGUF', wanGgufModel], + ['single-file checkpoint', wanCheckpointModel], + ])('errors when a %s main has no VAE or encoder source', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model })); + expect(hasWanComponentReason(reasons)).toBe(true); + }); + + it.each([ + ['GGUF', wanGgufModel], + ['single-file checkpoint', wanCheckpointModel], + ])('no error when a %s main has standalone VAE + encoder', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model, wanVaeModel: { key: 'vae' }, wanT5EncoderModel: { key: 't5' } }) + ); + expect(hasWanComponentReason(reasons)).toBe(false); + }); + + it('errors when only one of VAE / encoder is supplied', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanCheckpointModel, wanVaeModel: { key: 'vae' } }) + ); + expect(hasWanComponentReason(reasons)).toBe(true); + }); + + it('no error when a Component Source supplies both', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanCheckpointModel, wanComponentSource: { key: 'src' } }) + ); + expect(hasWanComponentReason(reasons)).toBe(false); + }); + + it('no error for a Diffusers main, which carries its own components', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model: wanDiffusersModel })); + expect(hasWanComponentReason(reasons)).toBe(false); + }); +}); + +describe('Wan 2.2 readiness checks – canvas tab', () => { + it.each([ + ['GGUF', wanGgufModel], + ['single-file checkpoint', wanCheckpointModel], + ])('errors when a %s main has no VAE or encoder source', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueCanvasTab(buildWanCanvasArg({ model })); + expect(hasWanComponentReason(reasons)).toBe(true); + }); +}); diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 81751025c9d..e1497d78e96 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1,8 +1,14 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; +import { + isWanComponentSourceCompatible, + isWanLowNoisePartnerCompatible, + isWanTi2v5b, + isWanVaeCompatible, +} from 'app/store/middleware/listenerMiddleware/listeners/wanComponentSync'; import { $false } from 'app/store/nanostores/util'; -import type { AppDispatch, AppStore } from 'app/store/store'; +import type { AppDispatch, AppStore, RootState } from 'app/store/store'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { debounce, groupBy, upperFirst } from 'es-toolkit/compat'; @@ -44,12 +50,14 @@ import type { TabName } from 'features/ui/store/uiTypes'; import i18n from 'i18next'; import { atom, computed } from 'nanostores'; import { useEffect } from 'react'; +import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { selectFlux2DevDiffusersModels, selectFlux2DiffusersModels } from 'services/api/hooks/modelsByType'; -import type { MainOrExternalModelConfig } from 'services/api/types'; +import type { AnyModelConfig, MainOrExternalModelConfig } from 'services/api/types'; import { isExternalApiModelConfig, isSelfContainedSDNQFlux1Pipeline, isSelfContainedSDNQPipeline, + isWanSingleFileMainModelConfig, } from 'services/api/types'; import { $isConnected } from 'services/events/stores'; @@ -97,6 +105,21 @@ type UpdateReasonsArg = { store: AppStore; }; +/** Resolve the wired Wan slot identifiers to their installed configs. Returns null for a + * slot that is empty or points at a model that has since been deleted — the pre-flight + * treats those the same way, since neither can be judged incompatible. */ +const selectWanWiredConfigs = (state: RootState) => { + const { wanVaeModel, wanComponentSource, wanTransformerLowNoise } = selectParamsSlice(state); + const query = selectModelConfigsQuery(state); + const configFor = (identifier: { key: string } | null) => + identifier && query.data ? (modelConfigsAdapterSelectors.selectById(query.data, identifier.key) ?? null) : null; + return { + vae: configFor(wanVaeModel), + componentSource: configFor(wanComponentSource), + lowNoisePartner: configFor(wanTransformerLowNoise), + }; +}; + const debouncedUpdateReasons = debounce(async (arg: UpdateReasonsArg) => { const { tab, @@ -136,6 +159,7 @@ const debouncedUpdateReasons = debounce(async (arg: UpdateReasonsArg) => { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs: selectWanWiredConfigs(store.getState()), }); $reasonsWhyCannotEnqueue.set(reasons); } else if (tab === 'canvas') { @@ -163,6 +187,7 @@ const debouncedUpdateReasons = debounce(async (arg: UpdateReasonsArg) => { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs: selectWanWiredConfigs(store.getState()), }); $reasonsWhyCannotEnqueue.set(reasons); } else if (tab === 'workflows') { @@ -250,6 +275,71 @@ export const useReadinessWatcher = () => { const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); +/** Pre-flight for Wan mains, shared by the generate and canvas tabs so the two can't drift. + * Mirrors what `WanModelLoaderInvocation` actually enforces. + * + * Keep in step with the auto-fill in `modelSelected.ts`: if that doesn't offer to + * populate the slots this demands, selecting the model just blocks Invoke with nothing + * the user can act on. + * + * Presence is not enough — the Advanced comboboxes offer every Wan VAE and every Wan + * Diffusers main with no variant filtering, so a hand-picked slot can be present and + * still rejected by the loader. The compatibility predicates are imported from + * `wanComponentSync` rather than restated, so the auto-fill and the pre-flight cannot + * disagree about what "compatible" means. + * + * There is deliberately no check on the A14B expert *tag*. Since #9505 the loader takes + * the pairing from the wiring, so an unpaired or untagged A14B runs with a warning + * rather than raising. */ +const pushWanReasons = ( + model: MainOrExternalModelConfig, + params: ParamsState, + wired: { + vae: AnyModelConfig | null; + componentSource: AnyModelConfig | null; + lowNoisePartner: AnyModelConfig | null; + }, + reasons: Reason[] +): void => { + if (isWanSingleFileMainModelConfig(model)) { + // Single-file Wan mains carry only the transformer; VAE + UMT5-XXL encoder must come + // from standalone models or the Component Source. + const hasVaeSource = params.wanVaeModel !== null || params.wanComponentSource !== null; + const hasEncoderSource = params.wanT5EncoderModel !== null || params.wanComponentSource !== null; + if (!hasVaeSource || !hasEncoderSource) { + reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') }); + } + } + + // A wired standalone VAE outranks the Diffusers main's own, so this applies to any Wan + // main. `wired.vae` is null when the slot is empty *or* dangling; only judge a resolved + // config, or a deleted model would read as a compatibility failure. + if (wired.vae && !isWanVaeCompatible(model, wired.vae)) { + reasons.push({ content: i18n.t('parameters.invoke.incompatibleWanVae') }); + } + if (isWanSingleFileMainModelConfig(model) && wired.componentSource) { + if (!isWanComponentSourceCompatible(model, wired.componentSource)) { + reasons.push({ content: i18n.t('parameters.invoke.incompatibleWanComponentSource') }); + } + } + // Only judge the low-noise slot when the loader actually reads it — a single-file + // A14B main. `wan_model_loader.py` logs "'Transformer (Low Noise)' is ignored for the + // single-expert TI2V-5B variant" and skips the whole pairing block, and the Diffusers + // branch never looks at the slot at all. Validating it regardless disables Invoke over + // a wire the backend would shrug off, and for a TI2V-5B main that is unavoidable rather + // than occasional: every model the partner picker can offer is an A14B, so every + // possible pick fails the variant check. The combobox is rendered for all Wan mains + // (see `ParamWanModelSelects`), so this is reachable by picking one, and the error text + // does not name the slot the user would have to clear to recover. + if (wired.lowNoisePartner && isWanSingleFileMainModelConfig(model) && !isWanTi2v5b(model)) { + if (params.wanTransformerLowNoise?.key === params.model?.key) { + reasons.push({ content: i18n.t('parameters.invoke.duplicateWanTransformer') }); + } else if (!isWanLowNoisePartnerCompatible(model, wired.lowNoisePartner)) { + reasons.push({ content: i18n.t('parameters.invoke.incompatibleWanLowNoiseExpert') }); + } + } +}; + export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainOrExternalModelConfig | null | undefined; @@ -260,6 +350,14 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { hasFlux2DiffusersVaeSource: boolean; hasFlux2DiffusersQwen3Source: boolean; hasFlux2DevDiffusersSource: boolean; + /** Resolved configs of the wired Wan slots — null when empty or pointing at a model + * that no longer exists. Resolved by the caller because readiness only receives + * identifiers, and compatibility can only be judged from the config. */ + wanWiredConfigs: { + vae: AnyModelConfig | null; + componentSource: AnyModelConfig | null; + lowNoisePartner: AnyModelConfig | null; + }; }) => { const { isConnected, @@ -271,6 +369,7 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs, } = arg; const { positivePrompt } = params; const reasons: Reason[] = []; @@ -401,17 +500,8 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { } } - if (model?.base === 'wan' && model.format === 'gguf_quantized') { - // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must - // come from either standalone models or the Component Source (Diffusers). - // The low-noise A14B partner expert is optional — if omitted, the loader - // will use the high-noise expert for the whole schedule (lower quality - // but still produces an image). - const hasVaeSource = params.wanVaeModel !== null || params.wanComponentSource !== null; - const hasEncoderSource = params.wanT5EncoderModel !== null || params.wanComponentSource !== null; - if (!hasVaeSource || !hasEncoderSource) { - reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') }); - } + if (model?.base === 'wan') { + pushWanReasons(model, params, wanWiredConfigs, reasons); } if (model?.base === 'z-image') { @@ -659,6 +749,14 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { hasFlux2DiffusersVaeSource: boolean; hasFlux2DiffusersQwen3Source: boolean; hasFlux2DevDiffusersSource: boolean; + /** Resolved configs of the wired Wan slots — null when empty or pointing at a model + * that no longer exists. Resolved by the caller because readiness only receives + * identifiers, and compatibility can only be judged from the config. */ + wanWiredConfigs: { + vae: AnyModelConfig | null; + componentSource: AnyModelConfig | null; + lowNoisePartner: AnyModelConfig | null; + }; }) => { const { isConnected, @@ -676,6 +774,7 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs, } = arg; const { positivePrompt } = params; const reasons: Reason[] = []; @@ -1156,17 +1255,8 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } } - if (model?.base === 'wan' && model.format === 'gguf_quantized') { - // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must - // come from either standalone models or the Component Source (Diffusers). - // The low-noise A14B partner expert is optional — if omitted, the loader - // will use the high-noise expert for the whole schedule (lower quality - // but still produces an image). - const hasVaeSource = params.wanVaeModel !== null || params.wanComponentSource !== null; - const hasEncoderSource = params.wanT5EncoderModel !== null || params.wanComponentSource !== null; - if (!hasVaeSource || !hasEncoderSource) { - reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') }); - } + if (model?.base === 'wan') { + pushWanReasons(model, params, wanWiredConfigs, reasons); } if (model?.base === 'z-image') { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx index 134ab5f1e62..7b9e7d72057 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx @@ -11,32 +11,22 @@ import { useTranslation } from 'react-i18next'; import { MdMoneyOff } from 'react-icons/md'; import { useMainModels } from 'services/api/hooks/modelsByType'; import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; -import { type AnyModelConfigWithExternal, isNonCommercialMainModelConfig } from 'services/api/types'; +import { + type AnyModelConfigWithExternal, + isNonCommercialMainModelConfig, + selectPrimaryMainModelOptions, +} from 'services/api/types'; export const MainModelPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const activeTab = useAppSelector(selectActiveTab); const [allModelConfigs] = useMainModels(); - // Low-noise Wan GGUFs belong in the Transformer (Low Noise) slot of the - // Wan advanced section, not as a primary main. Filter them out of the main - // model dropdown so users can't accidentally wire them backwards. - const modelConfigs = useMemo( - () => - allModelConfigs.filter((c) => { - if ( - c.type === 'main' && - c.base === 'wan' && - c.format === 'gguf_quantized' && - 'expert' in c && - c.expert === 'low' - ) { - return false; - } - return true; - }), - [allModelConfigs] - ); + // Low-noise Wan single-file experts (GGUF or safetensors checkpoint) belong in + // the Transformer (Low Noise) slot of the Wan advanced section, not as a primary + // main. Shared with the other two places a primary main can be chosen so the + // three cannot disagree about what is offerable. + const modelConfigs = useMemo(() => selectPrimaryMainModelOptions(allModelConfigs), [allModelConfigs]); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (modelConfig: AnyModelConfigWithExternal) => { diff --git a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx index 5c5a304a303..3f2850792e2 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx @@ -10,13 +10,20 @@ import { useTranslation } from 'react-i18next'; import { MdMoneyOff } from 'react-icons/md'; import { useMainModels } from 'services/api/hooks/modelsByType'; import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; -import { type AnyModelConfigWithExternal, isNonCommercialMainModelConfig } from 'services/api/types'; +import { + type AnyModelConfigWithExternal, + isNonCommercialMainModelConfig, + selectPrimaryMainModelOptions, +} from 'services/api/types'; export const InitialStateMainModelPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const activeTab = useAppSelector(selectActiveTab); - const [modelConfigs] = useMainModels(); + const [allModelConfigs] = useMainModels(); + // Same filter as MainModelPicker — a Wan low-noise expert offered here is just as + // unusable as a primary main, and this picker is the launchpad's first impression. + const modelConfigs = useMemo(() => selectPrimaryMainModelOptions(allModelConfigs), [allModelConfigs]); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (modelConfig: AnyModelConfigWithExternal) => { diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts index ef6e615dff1..1a616962747 100644 --- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts +++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts @@ -44,7 +44,7 @@ import { isTIModelConfig, isVAEModelConfigOrSubmodel, isWanDiffusersMainModelConfig, - isWanGGUFLowNoiseMainModelConfig, + isWanLowNoisePartnerOption, isWanT5EncoderModelConfig, isWanVAEModelConfig, isZImageDiffusersMainModelConfig, @@ -126,7 +126,7 @@ export const useQwenVLEncoderModels = () => buildModelsHook(isQwenVLEncoderModel export const useQwen3EncoderModels = () => buildModelsHook(isQwen3EncoderModelConfig)(); export const useQwen3VLEncoderModels = () => buildModelsHook(isQwen3VLEncoderModelConfig)(); export const useWanDiffusersModels = () => buildModelsHook(isWanDiffusersMainModelConfig)(); -export const useWanGGUFLowNoiseModels = () => buildModelsHook(isWanGGUFLowNoiseMainModelConfig)(); +export const useWanSingleFileLowNoiseModels = () => buildModelsHook(isWanLowNoisePartnerOption)(); export const useWanVAEModels = () => buildModelsHook(isWanVAEModelConfig)(); export const useWanT5EncoderModels = () => buildModelsHook(isWanT5EncoderModelConfig)(); export const usePiDDecoderModels = buildModelsHook(isPiDDecoderModelConfig); diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index f05af125fe6..5fe1513f593 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4112,7 +4112,7 @@ export type components = { */ type: "anima_text_encoder"; }; - AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; /** * AppVersion * @description App Version Response @@ -23921,6 +23921,112 @@ export type components = { */ base: "sdxl"; }; + /** + * Main_Checkpoint_Wan_Config + * @description Model config for single-file Wan 2.2 transformer checkpoints (safetensors). + * + * This is the format the community ships on CivitAI and in ComfyUI-oriented + * Hugging Face repos: one ``.safetensors`` per transformer, in either the native + * upstream key layout or the diffusers one, optionally under a + * ``model.diffusion_model.`` prefix, and optionally ComfyUI ``fp8_scaled`` + * quantized. The loader normalises all of those. + * + * As with GGUF, A14B's MoE arrives as two files (one per expert); ``expert`` + * records which one this is so the Wan model loader invocation can pair them. + * TI2V-5B is single-transformer and stores ``expert='none'``. + */ + Main_Checkpoint_Wan_Config: { + /** + * Key + * @description A unique key for this model. + */ + key: string; + /** + * Hash + * @description The hash of the model file(s). + */ + hash: string; + /** + * Path + * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory. + */ + path: string; + /** + * File Size + * @description The size of the model in bytes. + */ + file_size: number; + /** + * Name + * @description Name of the model. + */ + name: string; + /** + * Description + * @description Model description + */ + description: string | null; + /** + * Source + * @description The original source of the model (path, URL or repo_id). + */ + source: string; + /** @description The type of source */ + source_type: components["schemas"]["ModelSourceType"]; + /** + * Source Api Response + * @description The original API response from the source, as stringified JSON. + */ + source_api_response: string | null; + /** + * Source Url + * @description Optional URL for the model (e.g. download page or model page). + */ + source_url: string | null; + /** + * Cover Image + * @description Url for image to preview model + */ + cover_image: string | null; + /** + * Type + * @default main + * @constant + */ + type: "main"; + /** + * Trigger Phrases + * @description Set of trigger phrases for this model + */ + trigger_phrases: string[] | null; + /** @description Default settings for this model */ + default_settings: components["schemas"]["MainModelDefaultSettings"] | null; + /** + * Config Path + * @description Path to the config for this model, if any. + */ + config_path: string | null; + /** + * Base + * @default wan + * @constant + */ + base: "wan"; + /** + * Format + * @default checkpoint + * @constant + */ + format: "checkpoint"; + variant: components["schemas"]["WanVariantType"]; + /** + * Expert + * @description For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, 'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B). + * @default none + * @enum {string} + */ + expert: "high" | "low" | "none"; + }; /** * Main_Checkpoint_ZImage_Config * @description Model config for Z-Image single-file checkpoint models (safetensors, etc). @@ -28203,7 +28309,7 @@ export type components = { * Config * @description The installed model's config */ - config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; /** * ModelInstallDownloadProgressEvent @@ -28369,7 +28475,7 @@ export type components = { * Config Out * @description After successful installation, this will hold the configuration object. */ - config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]) | null; + config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]) | null; /** * Inplace * @description Leave model in its current location; otherwise install under models directory @@ -28455,7 +28561,7 @@ export type components = { * Config * @description The model's config */ - config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; /** * @description The submodel type, if any * @default null @@ -28482,7 +28588,7 @@ export type components = { * Config * @description The model's config */ - config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; /** * @description The submodel type, if any * @default null @@ -28709,7 +28815,7 @@ export type components = { */ ModelsList: { /** Models */ - models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"])[]; + models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"])[]; }; /** * Multiply Integers @@ -39343,6 +39449,10 @@ export type components = { * Each LoRA is routed to the primary and/or low-noise list based on its * recorded ``expert`` tag (set by the probe from the filename). Untagged * LoRAs go to both lists. + * + * Against a TI2V-5B main, which is a single transformer with no low-noise + * expert, a LoRA that would land only in the low-noise list is applied to + * the transformer instead, with a warning. */ WanLoRACollectionLoader: { /** @@ -39391,8 +39501,9 @@ export type components = { * field to override. * * For TI2V-5B (single transformer) only the primary list is used at denoise - * time; a LoRA routed only to the low-noise list would be inert, so that - * routing logs a warning. + * time, so a LoRA that would land only in the low-noise list is applied to + * the transformer instead, with a warning. The alternative is to accept the + * LoRA and silently have no effect. */ WanLoRALoaderInvocation: { /** @@ -39426,7 +39537,7 @@ export type components = { weight?: number; /** * Target - * @description Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it. + * @description Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it. On the single-transformer TI2V-5B, which has no low-noise expert, 'low' is applied to the transformer instead of being discarded. * @default auto * @enum {string} */ @@ -39482,15 +39593,16 @@ export type components = { * - Transformer(s): * * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/`` * from the same model record. - * * GGUF main: emits the single GGUF as the primary transformer; for A14B - * the second-expert GGUF must be wired to ``Transformer (Low Noise)``. + * * Single-file main (GGUF or safetensors checkpoint): emits the file as the + * primary transformer; for A14B the second-expert file must be wired to + * ``Transformer (Low Noise)``. * - VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers). * - UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) > * Component Source (Diffusers). * * The Component Source slot lets users supply a Diffusers Wan main model purely * for VAE / encoder extraction when the actual transformer is in a single-file - * format. Together, the standalone VAE + standalone encoder let a GGUF + * format. Together, the standalone VAE + standalone encoder let a single-file * transformer run without a full ~30 GB Diffusers install. */ WanModelLoaderInvocation: { @@ -39518,7 +39630,7 @@ export type components = { model: components["schemas"]["ModelIdentifierField"]; /** * Transformer (Low Noise) - * @description Optional second GGUF transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B. + * @description Optional second single-file transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF or safetensors checkpoint and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B. * @default null */ transformer_low_noise_model?: components["schemas"]["ModelIdentifierField"] | null; @@ -42330,7 +42442,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Validation Error */ @@ -42362,7 +42474,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Validation Error */ @@ -42414,7 +42526,7 @@ export interface operations { * "upcast_attention": false * } */ - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Bad request */ @@ -42521,7 +42633,7 @@ export interface operations { * "upcast_attention": false * } */ - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Bad request */ @@ -42594,7 +42706,7 @@ export interface operations { * "upcast_attention": false * } */ - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Bad request */ @@ -43329,7 +43441,7 @@ export interface operations { * "upcast_attention": false * } */ - "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; + "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Diffusers_ErnieImage_Config"] | components["schemas"]["Main_Diffusers_Ideogram4_Config"] | components["schemas"]["Main_Diffusers_Krea2_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_Wan_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Krea2_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["Main_GGUF_Krea2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_Flux2_Config"] | components["schemas"]["Main_SDNQ_Diffusers_FLUX_Config"] | components["schemas"]["Main_SDNQ_Diffusers_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["PiDDecoder_Checkpoint_FLUX_Config"] | components["schemas"]["PiDDecoder_Checkpoint_Flux2_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SD3_Config"] | components["schemas"]["PiDDecoder_Checkpoint_SDXL_Config"] | components["schemas"]["PiDDecoder_Checkpoint_QwenImage_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_Krea2_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["T5Encoder_SDNQ_Config"] | components["schemas"]["T5Encoder_GGUF_Config"] | components["schemas"]["Qwen3VLEncoder_Checkpoint_Config"] | components["schemas"]["Qwen3VLEncoder_Qwen3VLEncoder_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["Qwen3Encoder_SDNQ_Folder_Config"] | components["schemas"]["MistralEncoder_Diffusers_Config"] | components["schemas"]["MistralEncoder_Checkpoint_Config"] | components["schemas"]["MistralEncoder_GGUF_Config"] | components["schemas"]["Gemma2Encoder_Gemma2Encoder_Config"] | components["schemas"]["Gemma2Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]; }; }; /** @description Bad request */ diff --git a/invokeai/frontend/web/src/services/api/types.test.ts b/invokeai/frontend/web/src/services/api/types.test.ts index 0c63dd5d25a..e7c0fbc1d73 100644 --- a/invokeai/frontend/web/src/services/api/types.test.ts +++ b/invokeai/frontend/web/src/services/api/types.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { isFlux2DiffusersMainModelConfig, isZImageDiffusersMainModelConfig } from './types'; +import type { MainModelConfig } from './types'; +import { + isFlux2DiffusersMainModelConfig, + isWanLowNoisePartnerOption, + isZImageDiffusersMainModelConfig, + selectPrimaryMainModelOptions, +} from './types'; const partialConfig = (base: 'flux2' | 'z-image', submodels: Record) => ({ type: 'main', @@ -26,3 +32,87 @@ describe('SDNQ pipeline model predicates', () => { expect(predicate(partialConfig(base, { vae: {}, text_encoder: {}, tokenizer: {} }) as never)).toBe(false); }); }); + +const wanMain = (over: Record) => + ({ + key: 'k', + type: 'main', + base: 'wan', + format: 'checkpoint', + variant: 't2v_a14b', + name: 'wan', + ...over, + }) as unknown as MainModelConfig; + +describe('Wan low-noise partner picker', () => { + it('offers an untagged single-file A14B', () => { + // The case the whole branch exists to support. `expert` comes from a filename + // heuristic, is absent from `ModelRecordChanges`, and installed records are never + // re-probed — so requiring `expert === 'low'` here strands untagged pairs + // permanently, with no correction short of delete-and-reinstall. + expect(isWanLowNoisePartnerOption(wanMain({ key: 'untagged', expert: 'none' }))).toBe(true); + }); + + it('offers a tagged low expert, and a GGUF one — the pair need not share a format', () => { + expect(isWanLowNoisePartnerOption(wanMain({ key: 'low', expert: 'low' }))).toBe(true); + expect(isWanLowNoisePartnerOption(wanMain({ key: 'low-gguf', expert: 'low', format: 'gguf_quantized' }))).toBe( + true + ); + }); + + it('does not offer a tagged high expert or a Diffusers main', () => { + expect(isWanLowNoisePartnerOption(wanMain({ key: 'high', expert: 'high' }))).toBe(false); + expect(isWanLowNoisePartnerOption(wanMain({ key: 'diff', format: 'diffusers' }))).toBe(false); + }); + + it('does not offer a TI2V-5B — it is single-transformer and has no partner', () => { + expect(isWanLowNoisePartnerOption(wanMain({ key: '5b', variant: 'ti2v_5b', expert: 'none' }))).toBe(false); + }); + + it('lets an untagged pair be assembled: both halves stay in the primary picker too', () => { + // The two pickers have to agree. Widening this one must not start hiding untagged + // models from the primary list — `selectPrimaryMainModelOptions` keys on the narrow + // tag test for exactly that reason. + const a = wanMain({ key: 'a', expert: 'none', name: 'pair-part-1' }); + const b = wanMain({ key: 'b', expert: 'none', name: 'pair-part-2' }); + + expect([a, b].filter(isWanLowNoisePartnerOption)).toHaveLength(2); + expect(selectPrimaryMainModelOptions([a, b])).toHaveLength(2); + }); + + it('never leaves a TI2V-5B invisible in both pickers', () => { + // The partner picker excludes every TI2V-5B, so the primary picker must not hide one + // either — a single-transformer model has no partner slot to be steered toward. The + // two exclusions have to agree or the model is reachable from nowhere in the linear UI. + // + // Reachable with a real record: the pre-branch GGUF probe applied the expert tag + // without consulting the variant, so a 5B whose stem contained `low_noise` was stored + // as `expert='low'` and still is. `hasPartner` then matches it against any second + // TI2V-5B, which is all it takes to hide it. + const taggedLow5b = wanMain({ key: 'ti2v-low', variant: 'ti2v_5b', expert: 'low' }); + const plain5b = wanMain({ key: 'ti2v-plain', variant: 'ti2v_5b', expert: 'none' }); + const library = [taggedLow5b, plain5b]; + + expect(selectPrimaryMainModelOptions(library).map((c) => c.key)).toEqual(['ti2v-low', 'ti2v-plain']); + expect(library.filter(isWanLowNoisePartnerOption)).toHaveLength(0); + }); + + it('keeps an untagged model in the primary picker even next to a tagged high expert', () => { + // The case that actually catches `selectPrimaryMainModelOptions` being switched to the + // wide predicate. With two untagged models the wide test classes both as low experts, + // so neither has a partner and neither is hidden — the mistake hides behind itself. + // Add a same-variant `high` and the untagged model suddenly has a partner, so keying + // the primary filter on the wide test would drop it from the main picker entirely. + const high = wanMain({ key: 'high', expert: 'high', name: 'high' }); + const untagged = wanMain({ key: 'untagged', expert: 'none', name: 'untagged' }); + + expect(selectPrimaryMainModelOptions([high, untagged]).map((c) => c.key)).toEqual(['high', 'untagged']); + }); + + it('still hides a tagged low expert from the primary picker when it has a partner', () => { + const high = wanMain({ key: 'high', expert: 'high', name: 'high' }); + const low = wanMain({ key: 'low', expert: 'low', name: 'low' }); + + expect(selectPrimaryMainModelOptions([high, low]).map((c) => c.key)).toEqual(['high']); + }); +}); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 37e4ee2149a..7c16c843699 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -638,15 +638,113 @@ export const isWanDiffusersMainModelConfig = (config: AnyModelConfig): config is return config.type === 'main' && config.base === 'wan' && config.format === 'diffusers'; }; -/** Wan GGUF main models marked as the low-noise expert (the second half - * of the A14B MoE pair). Suitable for the Transformer (Low Noise) picker; - * also used to filter low-noise GGUFs out of the primary main dropdown. */ -export const isWanGGUFLowNoiseMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => { +/** The single-file Wan main formats. Both are transformer-only: one file holds one + * A14B expert, and the VAE + UMT5-XXL encoder have to come from somewhere else. + * Anything gating on that property must use this, not a bare `=== 'gguf_quantized'` + * — the two formats are interchangeable here and drifting apart has bitten us. */ +const WAN_SINGLE_FILE_FORMATS = ['gguf_quantized', 'checkpoint'] as const; + +export const isWanSingleFileMainModelConfig = (config: AnyModelConfigWithExternal): config is MainModelConfig => { + // Takes AnyModelConfig, not a structural `{base?; type?; format?}`. An all-optional + // parameter type is a *weak type*, which TypeScript satisfies with any object sharing + // one property name — so `ModelIdentifierField` (base + type, no format) would compile + // and silently return false, disabling every gate below it. The bare + // `format === 'gguf_quantized'` this replaced was at least a compile error there. return ( - config.type === 'main' && config.base === 'wan' && config.format === 'gguf_quantized' && config.expert === 'low' + config.type === 'main' && + config.base === 'wan' && + (WAN_SINGLE_FILE_FORMATS as readonly string[]).includes(config.format) ); }; +/** TI2V-5B is the single-transformer Wan variant: it has no expert pair, so no expert + * tag on it means anything. Both predicates below have to agree about that, or a file + * can fall through the gap between them. */ +const isWanTi2v5bConfig = (config: AnyModelConfigWithExternal): boolean => + 'variant' in config && config.variant === 'ti2v_5b'; + +/** Wan single-file main models *tagged* as the low-noise expert. This is the narrow, + * tag-based test, and its only job is deciding what to hide from the primary main + * dropdown — see `selectPrimaryMainModelOptions`, its one caller. Deliberately not + * exported: the Transformer (Low Noise) picker needs the wider test below, and reaching + * for this one there is the mistake that left untagged pairs unwireable. + * + * TI2V-5B is excluded for the same reason it is excluded from the partner picker. The + * two exclusions have to match: hiding a 5B from the primary list steers it toward a + * partner slot that will not offer it either, which is how a model ends up reachable + * from nowhere. Such a record is not hypothetical — the pre-branch GGUF probe applied + * the tag without consulting the variant, so a 5B named `...-low_noise.gguf` installed + * before this branch still carries `expert='low'` today. */ +const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfigWithExternal): config is MainModelConfig => { + return ( + isWanSingleFileMainModelConfig(config) && + !isWanTi2v5bConfig(config) && + 'expert' in config && + config.expert === 'low' + ); +}; + +/** What the Transformer (Low Noise) picker may offer. + * + * Deliberately wider than the tag test above. Since #9505 the *wiring* decides which + * expert a file is used as and the `expert` tag is only advisory, so requiring + * `expert === 'low'` here strands every pair that probes to `none`/`none`: both halves + * show up in the primary picker (which hides only models tagged `low`) and neither + * shows up here, leaving the pair impossible to assemble outside the workflow editor. + * That is the exact case this branch exists to support. It is also not something the + * user can tag their way out of: `expert` is absent from `ModelRecordChanges`, so no + * edit sets it. Re-probing via Reidentify recomputes it, but only from the filename, + * which for an untagged file returns `none` again. + * + * Two exclusions. Files tagged `high` belong in the primary slot — the loader would + * only swap them back. TI2V-5B is single-transformer, so it has no partner at all and + * offering one could only produce the variant mismatch the loader rejects. */ +export const isWanLowNoisePartnerOption = (config: AnyModelConfigWithExternal): config is MainModelConfig => { + if (!isWanSingleFileMainModelConfig(config)) { + return false; + } + if ('expert' in config && config.expert === 'high') { + return false; + } + return !isWanTi2v5bConfig(config); +}; + +/** Narrows a main-model list to what may be offered as the *primary* main. Every list + * the user can pick a primary main from must go through this — there are three + * (MainModelPicker, InitialStateMainModelPicker, and the auto-select in the + * modelsLoaded listener), and filtering in only some of them means the excluded models + * are still reachable. + * + * It only hides Wan A14B low-noise experts, and only when the user has a partner to + * pick instead. The steer is worth making — a low-noise expert belongs in the + * Transformer (Low Noise) slot, and running it alone gives visibly worse output — but + * since #9505 the loader accepts an unpaired low expert with a warning rather than + * refusing it. Hiding unconditionally would leave someone whose only Wan file is a low + * expert staring at a list that doesn't contain their model, with nothing to do about + * it. Partner-aware, the list degrades instead of dead-ending. + * + * A partner is another single-file Wan main of the same variant that isn't itself + * tagged low — i.e. the high-noise or untagged half of the same pair. */ +export const selectPrimaryMainModelOptions = (configs: T[]): T[] => { + // Annotated `: boolean` rather than left as an inferred type predicate. Two predicates + // narrowing to the same type would make the negated one resolve `candidate` to `never` + // below, and the `variant` read would stop compiling. + const isLowExpert = (config: T): boolean => isWanSingleFileLowNoiseMainModelConfig(config); + const variantOf = (config: T): string | null => + 'variant' in config && typeof config.variant === 'string' ? config.variant : null; + + const hasPartner = (low: T): boolean => + configs.some( + (candidate) => + candidate.key !== low.key && + isWanSingleFileMainModelConfig(candidate) && + !isLowExpert(candidate) && + variantOf(candidate) === variantOf(low) + ); + + return configs.filter((config) => !isLowExpert(config) || !hasPartner(config)); +}; + export const isWanLoRAModelConfig = (config: AnyModelConfig): config is WanLoRAModelConfig => { return config.type === 'lora' && config.base === 'wan'; }; diff --git a/tests/app/invocations/test_wan_lora_loader.py b/tests/app/invocations/test_wan_lora_loader.py index 12851c88883..05972c042de 100644 --- a/tests/app/invocations/test_wan_lora_loader.py +++ b/tests/app/invocations/test_wan_lora_loader.py @@ -404,12 +404,13 @@ def test_new_key_appends_alongside_existing(self): # -------------------------------------------------------------------------- # TI2V-5B inert low routing — the single-transformer path never consumes the -# low-noise list, so low-only routing must warn (JPPhoto review 2026-07-21). +# low-noise list, so low-only routing is corrected to the primary list rather +# than merely warned about (JPPhoto review 2026-07-21). # -------------------------------------------------------------------------- -class TestInertLowRoutingWarning: - def test_single_loader_warns_for_low_only_routing_on_ti2v(self): +class TestInertLowRoutingCorrection: + def test_single_loader_reroutes_low_only_routing_on_ti2v(self): inv = WanLoRALoaderInvocation( id="inv-1", lora=_make_lora_field(), target="low", transformer=_make_transformer_field() ) @@ -421,9 +422,13 @@ def test_single_loader_warns_for_low_only_routing_on_ti2v(self): out = inv.invoke(ctx) assert out.transformer is not None ctx.logger.warning.assert_called_once() - assert "no effect" in ctx.logger.warning.call_args.args[0] + assert "Applying it to the transformer instead" in ctx.logger.warning.call_args.args[0] + # The correction is the point: the 5B denoise path reads only the primary list, + # so leaving it on `loras_low_noise` is indistinguishable from dropping the LoRA. + assert [item.lora.key for item in out.transformer.loras] == ["lora-1"] + assert out.transformer.loras_low_noise == [] - def test_collection_loader_warns_for_low_tagged_lora_on_ti2v(self): + def test_collection_loader_reroutes_low_tagged_lora_on_ti2v(self): lora = LoRAField(lora=_make_lora_field(), weight=1.0) inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field()) ctx = _make_context( @@ -434,6 +439,8 @@ def test_collection_loader_warns_for_low_tagged_lora_on_ti2v(self): out = inv.invoke(ctx) assert out.transformer is not None ctx.logger.warning.assert_called_once() + assert [item.lora.key for item in out.transformer.loras] == ["lora-1"] + assert out.transformer.loras_low_noise == [] @pytest.mark.parametrize("target", ["auto", "both", "high"]) def test_no_warning_when_primary_list_is_reached(self, target): @@ -449,6 +456,31 @@ def test_no_warning_when_primary_list_is_reached(self, target): inv.invoke(ctx) ctx.logger.warning.assert_not_called() + def test_reroutes_a_low_tagged_lora_whose_variant_could_not_be_detected(self): + """The probe-side pin can only fire when the variant was detected, and + `detect_wan_lora_variant` reads the inner dim off an `attn1.to_q` LoRA pair. + + A LoKr/LoHa adapter, or one patching only `to_k`/`to_v`, yields `variant=None`, + so the filename's bare `low` token still lands on the record — and + `_assert_lora_variant_matches_main` returns early on an unknown variant, so + nothing downstream catches it either. Records written before the pin existed are + in the same position. The main model's variant is the one signal that is known + for certain, which is why the correction lives here as well as in the probe. + """ + lora = LoRAField(lora=_make_lora_field(), weight=1.0) + inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field()) + ctx = _make_context( + lora_expert="low", + lora_config=_make_lora_config(expert="low", variant=None), + main_variant=WanVariantType.TI2V_5B, + ) + + out = inv.invoke(ctx) + + assert out.transformer is not None + assert [item.lora.key for item in out.transformer.loras] == ["lora-1"] + assert out.transformer.loras_low_noise == [] + def test_no_warning_for_low_routing_on_a14b(self): inv = WanLoRALoaderInvocation( id="inv-1", lora=_make_lora_field(), target="low", transformer=_make_transformer_field() diff --git a/tests/app/invocations/test_wan_model_loader.py b/tests/app/invocations/test_wan_model_loader.py index 93442b265b6..50450749bfa 100644 --- a/tests/app/invocations/test_wan_model_loader.py +++ b/tests/app/invocations/test_wan_model_loader.py @@ -227,7 +227,7 @@ def test_gguf_loader_warns_when_neither_expert_is_tagged() -> None: ) invocation.invoke(context) - assert any("Neither Wan A14B GGUF filename identifies its expert" in warning for warning in _warnings(context)) + assert any("Neither Wan A14B filename identifies its expert" in warning for warning in _warnings(context)) @pytest.mark.parametrize( @@ -412,3 +412,84 @@ def test_loader_rejects_forged_component_source_even_with_standalone_components( type=ModelType.Main, ), ) + + +# --- Single-file safetensors checkpoints (#9463) --------------------------------- + + +@pytest.mark.parametrize("variant", [WanVariantType.T2V_A14B, WanVariantType.I2V_A14B]) +def test_checkpoint_main_accepts_expert_pair(variant: WanVariantType) -> None: + output = _invoke( + _config("main", variant, "high", format=ModelFormat.Checkpoint), + _config("low", variant, "low", format=ModelFormat.Checkpoint), + ) + + assert output.transformer.transformer.key == "main" + assert output.transformer.transformer_low_noise is not None + assert output.transformer.transformer_low_noise.key == "low" + + +@pytest.mark.parametrize( + "main_format,low_format", + [ + (ModelFormat.Checkpoint, ModelFormat.GGUFQuantized), + (ModelFormat.GGUFQuantized, ModelFormat.Checkpoint), + ], +) +def test_experts_may_mix_single_file_formats(main_format: ModelFormat, low_format: ModelFormat) -> None: + """Both single-file loaders produce a plain WanTransformer3DModel, so a GGUF + high-noise expert pairs fine with a safetensors low-noise one.""" + output = _invoke( + _config("main", WanVariantType.T2V_A14B, "high", format=main_format), + _config("low", WanVariantType.T2V_A14B, "low", format=low_format), + ) + + assert output.transformer.transformer.key == "main" + assert output.transformer.transformer_low_noise is not None + assert output.transformer.transformer_low_noise.key == "low" + + +def test_checkpoint_ti2v_5b_runs_unpaired() -> None: + output = _invoke(_config("main", WanVariantType.TI2V_5B, "none", format=ModelFormat.Checkpoint)) + + assert output.transformer.transformer.key == "main" + assert output.transformer.transformer_low_noise is None + + +@pytest.mark.parametrize("expert", ["high", "low", "none"]) +def test_checkpoint_main_runs_unpaired_whatever_its_tag(expert: str) -> None: + """The checkpoint path takes the same wiring-first rule #9505 gave the GGUF path: a + single wired transformer is explicit intent, and the tag is only a filename guess. + Untagged community checkpoints are the common case this branch exists to support, so + they must not be fatal here when the paired path accepts them.""" + invocation, context = _prepare(_config("main", WanVariantType.T2V_A14B, expert, format=ModelFormat.Checkpoint)) + output = invocation.invoke(context) + + assert output.transformer.transformer.key == "main" + assert output.transformer.transformer_low_noise is None + assert any("only this one expert will run" in warning.lower() for warning in _warnings(context)) + + +def test_low_noise_slot_rejects_a_diffusers_model() -> None: + with pytest.raises(ValueError, match="single-file"): + _invoke( + _config("main", WanVariantType.T2V_A14B, "high", format=ModelFormat.Checkpoint), + _config("low", WanVariantType.T2V_A14B, "low", format=ModelFormat.Diffusers), + ) + + +def test_checkpoint_main_uses_component_source_boundary() -> None: + output = _invoke( + _config("main", WanVariantType.T2V_A14B, "high", format=ModelFormat.Checkpoint), + _config("low", WanVariantType.T2V_A14B, "low", format=ModelFormat.Checkpoint), + _config( + "component", + WanVariantType.T2V_A14B, + "none", + format=ModelFormat.Diffusers, + has_dual_expert=True, + boundary_ratio=0.5, + ), + ) + + assert output.transformer.boundary_ratio == 0.5 diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py new file mode 100644 index 00000000000..67d4f887eb0 --- /dev/null +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -0,0 +1,506 @@ +"""Tests for the single-file Wan 2.2 checkpoint probe (Main_Checkpoint_Wan_Config). + +Regression coverage for #9463: community Wan 2.2 fine-tunes ship as one +``.safetensors`` per transformer, which InvokeAI previously refused to identify +at all ("unidentified model") because only Diffusers folders and GGUF files had +a matching config class. +""" + +from pathlib import Path +from unittest.mock import MagicMock + +import gguf +import pytest +import torch + +from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError +from invokeai.backend.model_manager.configs.main import ( + Main_Checkpoint_Wan_Config, + _detect_wan_expert, + _find_wan_2_1_marker, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, WanVariantType +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + +A14B_DIM = 5120 +TI2V_DIM = 3072 + + +def _t(*shape: int, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor: + return torch.zeros(shape, dtype=dtype) + + +def _native_sd(in_channels: int = 16, dim: int = A14B_DIM, prefix: str = "") -> dict: + """Native upstream / ComfyUI Wan key layout — what CivitAI fine-tunes ship.""" + sd = { + f"{prefix}patch_embedding.weight": _t(dim, in_channels, 1, 2, 2), + f"{prefix}text_embedding.0.weight": _t(dim, 4096), + f"{prefix}text_embedding.2.weight": _t(dim, dim), + f"{prefix}time_embedding.0.weight": _t(dim, 256), + f"{prefix}head.head.weight": _t(64, dim), + f"{prefix}head.modulation": _t(1, 2, dim), + f"{prefix}blocks.0.self_attn.q.weight": _t(dim, dim), + f"{prefix}blocks.0.cross_attn.q.weight": _t(dim, dim), + f"{prefix}blocks.0.ffn.0.weight": _t(13824, dim), + } + return sd + + +def _diffusers_sd(in_channels: int = 48, dim: int = TI2V_DIM) -> dict: + """Diffusers Wan key layout, as shipped by Wan-AI/*-Diffusers single files.""" + return { + "patch_embedding.weight": _t(dim, in_channels, 1, 2, 2), + "condition_embedder.text_embedder.linear_1.weight": _t(dim, 4096), + "blocks.0.attn1.to_q.weight": _t(dim, dim), + "blocks.0.ffn.net.0.proj.weight": _t(14336, dim), + "proj_out.weight": _t(in_channels * 4, dim), + } + + +def _build_overrides(model_path: Path, name: str) -> dict: + return { + "hash": "test-hash", + "path": str(model_path), + "file_size": 0, + "name": name, + "source": str(model_path), + "source_type": "path", + } + + +def _make_mod(path: Path, sd: dict) -> MagicMock: + mod = MagicMock() + mod.path = path + mod.load_state_dict.return_value = sd + mod.metadata.return_value = {} + return mod + + +def _probe(tmp_path: Path, filename: str, sd: dict) -> Main_Checkpoint_Wan_Config: + path = tmp_path / filename + path.touch() + return Main_Checkpoint_Wan_Config.from_model_on_disk(_make_mod(path, sd), _build_overrides(path, path.stem)) + + +class TestAccepts: + def test_native_layout_t2v_a14b(self, tmp_path: Path) -> None: + config = _probe(tmp_path, "Wan2.2-T2V-A14B-high_noise.safetensors", _native_sd(16)) + assert config.base == BaseModelType.Wan + assert config.format == ModelFormat.Checkpoint + assert config.variant == WanVariantType.T2V_A14B + assert config.expert == "high" + + def test_native_layout_i2v_a14b(self, tmp_path: Path) -> None: + config = _probe(tmp_path, "Wan2.2-I2V-A14B-LowNoise.safetensors", _native_sd(36)) + assert config.variant == WanVariantType.I2V_A14B + assert config.expert == "low" + + def test_diffusers_layout_ti2v_5b(self, tmp_path: Path) -> None: + config = _probe(tmp_path, "wanDamme-RapidWan2.2-5B-ti2v-4step.safetensors", _diffusers_sd(48)) + assert config.variant == WanVariantType.TI2V_5B + assert config.expert == "none" + + @pytest.mark.parametrize("prefix", ["model.diffusion_model.", "diffusion_model."]) + def test_comfyui_key_prefix(self, tmp_path: Path, prefix: str) -> None: + config = _probe(tmp_path, "wan22_t2v_high_noise.safetensors", _native_sd(16, prefix=prefix)) + assert config.variant == WanVariantType.T2V_A14B + assert config.expert == "high" + + def test_comfyui_fp8_scaled(self, tmp_path: Path) -> None: + """fp8_scaled files carry extra scale tensors; they must not confuse the probe.""" + sd = _native_sd(16) + sd["patch_embedding.weight"] = _t(A14B_DIM, 16, 1, 2, 2, dtype=torch.float8_e4m3fn) + sd["blocks.0.self_attn.q.weight"] = _t(A14B_DIM, A14B_DIM, dtype=torch.float8_e4m3fn) + sd["blocks.0.self_attn.q.scale_weight"] = _t(1, dtype=torch.float32) + sd["scaled_fp8"] = _t(1, dtype=torch.float8_e4m3fn) + + config = _probe(tmp_path, "Wan2.2-T2V-A14B-HighNoise-fp8_scaled.safetensors", sd) + assert config.variant == WanVariantType.T2V_A14B + assert config.expert == "high" + + def test_untagged_community_filename(self, tmp_path: Path) -> None: + """The #9463 case: a fine-tune whose filename never says "Wan 2.2". + + Unlike the GGUF probe, the checkpoint probe must not demand a version + marker in the name — rejecting these was the reported bug. + """ + config = _probe(tmp_path, "SmoothMix_HighNoise.safetensors", _native_sd(16)) + assert config.variant == WanVariantType.T2V_A14B + assert config.expert == "high" + + +def _ggml(*shape: int) -> GGMLTensor: + return GGMLTensor( + data=torch.zeros((1,), dtype=torch.uint8), + ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0, + tensor_shape=torch.Size(shape), + compute_dtype=torch.float32, + ) + + +class TestRejects: + def test_gguf_file(self, tmp_path: Path) -> None: + """A .gguf goes to Main_GGUF_Wan_Config; the suffix guard turns it away first.""" + with pytest.raises(NotAMatchError, match="safetensors"): + _probe(tmp_path, "wan2.2-t2v-a14b-high_noise-Q4_K_M.gguf", _native_sd(16)) + + def test_gguf_tensors(self, tmp_path: Path) -> None: + """Belt-and-braces behind the suffix guard: GGML tensors are never a match + for this config regardless of what the file is called.""" + sd = { + "patch_embedding.weight": _ggml(A14B_DIM, 16, 1, 2, 2), + "text_embedding.0.weight": _ggml(A14B_DIM, 4096), + "blocks.0.self_attn.q.weight": _ggml(A14B_DIM, A14B_DIM), + } + with pytest.raises(NotAMatchError, match="GGUF"): + _probe(tmp_path, "wan2.2-t2v-a14b-high_noise.safetensors", sd) + + def test_wan_2_1_i2v_via_clip_image_embedder(self, tmp_path: Path) -> None: + sd = _native_sd(36) + sd["img_emb.proj.0.weight"] = _t(1280, 1280) + with pytest.raises(NotAMatchError, match="Wan 2.1"): + _probe(tmp_path, "some-i2v-model.safetensors", sd) + + def test_wan_2_1_1_3b_via_inner_dim(self, tmp_path: Path) -> None: + with pytest.raises(NotAMatchError, match="Wan 2.1"): + _probe(tmp_path, "renamed-t2v-model.safetensors", _native_sd(16, dim=1536)) + + def test_animate(self, tmp_path: Path) -> None: + """Wan Animate is 36-channel with undecorated block weights and no VACE + blocks, so nothing else turns it away. Its face-adapter and motion-encoder + branches have no counterpart in WanTransformer3DModel — real + wan2.2_animate_14B_bf16.safetensors carries 127 such keys out of 1441 — and + strict=False would drop every one of them silently. + + It also carries img_emb, so this must be reported as Animate rather than as + a Wan 2.1 I2V model. + """ + sd = _native_sd(36) + sd["face_adapter.fuser_blocks.0.k_norm.weight"] = _t(A14B_DIM) + sd["motion_encoder.dec.direction.weight"] = _t(512, 512) + sd["img_emb.proj.0.bias"] = _t(1280) + with pytest.raises(NotAMatchError, match="Animate"): + _probe(tmp_path, "wan2.2_animate_14B_bf16.safetensors", sd) + + def test_s2v(self, tmp_path: Path) -> None: + """Wan S2V is 16-channel and otherwise key-identical to plain T2V-A14B, so + only its audio branches distinguish it. Prefixes taken from the real + wan2.2_s2v_14B_bf16.safetensors header (165 such keys of 1260).""" + sd = _native_sd(16) + sd["audio_injector.injector.0.k.weight"] = _t(A14B_DIM, A14B_DIM) + sd["casual_audio_encoder.encoder.final_proj.weight"] = _t(A14B_DIM, 1024) + sd["cond_encoder.weight"] = _t(A14B_DIM, 16, 1, 2, 2) + sd["frame_packer.proj.weight"] = _t(A14B_DIM, 16) + with pytest.raises(NotAMatchError, match="S2V"): + _probe(tmp_path, "wan2.2_s2v_14B_bf16.safetensors", sd) + + def test_fun_control_camera(self, tmp_path: Path) -> None: + """The most dangerous of the unsupported variants: 36 channels, key-identical + to plain I2V-A14B apart from control_adapter, AND shipped as a properly + tagged high/low pair — so the expert-pairing check would pass and it would + render as an ordinary I2V, silently ignoring every camera input.""" + sd = _native_sd(36) + sd["control_adapter.conv.weight"] = _t(A14B_DIM, 36, 1, 2, 2) + sd["control_adapter.residual_blocks.0.conv1.weight"] = _t(A14B_DIM, A14B_DIM) + with pytest.raises(NotAMatchError, match="Fun-Control"): + _probe(tmp_path, "wan2.2_fun_camera_high_noise_14B_bf16.safetensors", sd) + + def test_vace(self, tmp_path: Path) -> None: + """VACE exists for both Wan 2.1 and 2.2; either way this loader has no + control branch, so it must refuse rather than silently ignore the input.""" + sd = _native_sd(16) + sd["vace_blocks.0.after_proj.weight"] = _t(A14B_DIM, A14B_DIM) + with pytest.raises(NotAMatchError, match="VACE"): + _probe(tmp_path, "vace-model.safetensors", sd) + + def test_wan_2_1_filename(self, tmp_path: Path) -> None: + with pytest.raises(NotAMatchError, match="Wan 2.1"): + _probe(tmp_path, "Wan2.1-T2V-14B.safetensors", _native_sd(16)) + + def test_unrecognised_state_dict(self, tmp_path: Path) -> None: + with pytest.raises(NotAMatchError, match="Wan transformer"): + _probe(tmp_path, "junk.safetensors", {"random.key": _t(4, 4)}) + + def test_lora_carrying_a_full_patch_embedding(self, tmp_path: Path) -> None: + """Wan I2V adapters bundle a replacement patch_embedding (in_channels 16->36) + plus the text projection, which is everything ``_has_wan_keys`` looks for. The + main-model probe must not claim them — Main outranks LoRA in + ``matches_sort_key``, so it would pull the file out of every LoRA picker.""" + sd = { + "diffusion_model.patch_embedding.weight": _t(A14B_DIM, 36, 1, 2, 2), + "diffusion_model.text_embedding.0.weight": _t(A14B_DIM, 4096), + "diffusion_model.blocks.0.self_attn.q.lora_A.weight": _t(32, A14B_DIM), + "diffusion_model.blocks.0.self_attn.q.lora_B.weight": _t(A14B_DIM, 32), + } + with pytest.raises(NotAMatchError, match="LoRA"): + _probe(tmp_path, "Wan2.2-I2V-A14B-adapter-low_noise.safetensors", sd) + + @pytest.mark.parametrize("suffix", [".ckpt", ".pt", ".pth", ".bin"]) + def test_non_safetensors_containers(self, tmp_path: Path, suffix: str) -> None: + """The loader reads these with safetensors.load_file, so claiming one would + install cleanly and then die with an opaque header error at generation time.""" + with pytest.raises(NotAMatchError, match="safetensors"): + _probe(tmp_path, f"Wan2.2-T2V-A14B-HighNoise{suffix}", _native_sd(16)) + + def test_unknown_channel_count(self, tmp_path: Path) -> None: + with pytest.raises(NotAMatchError, match="variant"): + _probe(tmp_path, "weird-wan22.safetensors", _native_sd(24)) + + @pytest.mark.parametrize( + "in_channels,dim,label", + [ + # A14B-width but 48-channel: the wider Wan family reuses TI2V-5B's channel + # count at A14B's width (Fun-Control-14B). Reading in_channels alone labels + # this TI2V-5B, which pins expert='none', picks TI2V-5B default settings and + # hides the low-noise partner picker — a mislabel, not a refusal. + (48, A14B_DIM, "48-channel at A14B width"), + # ...and the converse: TI2V-5B width with an A14B channel count. + (16, TI2V_DIM, "16-channel at TI2V-5B width"), + (36, TI2V_DIM, "36-channel at TI2V-5B width"), + ], + ) + def test_channel_count_must_agree_with_transformer_width( + self, tmp_path: Path, in_channels: int, dim: int, label: str + ) -> None: + """A14B is uniquely 5120-wide and TI2V-5B uniquely 3072-wide. A combination that + matches neither is a Wan derivative we don't support, so it must fall through to + 'unidentified' rather than be labelled as the variant it merely shares a channel + count with.""" + with pytest.raises(NotAMatchError, match="variant"): + _probe(tmp_path, f"wan22-{label.replace(' ', '-')}.safetensors", _native_sd(in_channels, dim=dim)) + + +class TestWan21Marker: + def test_clean_wan_2_2_state_dicts_have_no_marker(self) -> None: + assert _find_wan_2_1_marker(_native_sd(16)) is None + assert _find_wan_2_1_marker(_native_sd(36)) is None + assert _find_wan_2_1_marker(_diffusers_sd(48)) is None + + def test_diffusers_layout_image_embedder_is_detected(self) -> None: + sd = _diffusers_sd(36, dim=A14B_DIM) + sd["condition_embedder.image_embedder.norm1.weight"] = _t(1280) + assert _find_wan_2_1_marker(sd) is not None + + def test_marker_survives_comfyui_prefix(self) -> None: + sd = _native_sd(36, prefix="model.diffusion_model.") + sd["model.diffusion_model.img_emb.proj.0.weight"] = _t(1280, 1280) + assert _find_wan_2_1_marker(sd) is not None + + +class TestEndToEndIdentification: + """Drive the real install path — a file on disk through ModelConfigFactory. + + The class-level tests above call ``from_model_on_disk`` directly, so they'd + still pass if the config were left out of the ``AnyModelConfig`` union. These + reproduce #9463 as reported: the model imports as "unidentified" unless the + config is actually wired into the factory, and the resulting record must + dispatch to a registered loader. + """ + + @staticmethod + def _write(tmp_path: Path, filename: str, sd: dict) -> Path: + from safetensors.torch import save_file + + path = tmp_path / filename + save_file(sd, path) + return path + + @pytest.mark.parametrize( + "filename, state_dict, expected_variant, expected_expert", + [ + ("Wan2.2-A14B-SmoothMix-T2V-HighNoise.safetensors", _native_sd(16), WanVariantType.T2V_A14B, "high"), + ("Wan2.2-A14B-SmoothMix-I2V-LowNoise.safetensors", _native_sd(36), WanVariantType.I2V_A14B, "low"), + ("wanDamme-RapidWan2.2-5B-ti2v-4step.safetensors", _diffusers_sd(48), WanVariantType.TI2V_5B, "none"), + ], + ) + def test_community_models_from_the_issue_are_identified( + self, + tmp_path: Path, + filename: str, + state_dict: dict, + expected_variant: WanVariantType, + expected_expert: str, + ) -> None: + from invokeai.backend.model_manager.configs.factory import ModelConfigFactory + + path = self._write(tmp_path, filename, state_dict) + config = ModelConfigFactory.from_model_on_disk(path, allow_unknown=True).config + + assert config is not None + assert isinstance(config, Main_Checkpoint_Wan_Config) + assert config.variant == expected_variant + assert config.expert == expected_expert + + def test_record_survives_a_serialization_round_trip(self, tmp_path: Path) -> None: + """Identification alone isn't enough — the config also has to be a member of + the ``AnyModelConfig`` union, or the record can't be read back out of the + model-records DB or served over the API.""" + from invokeai.backend.model_manager.configs.factory import ModelConfigFactory + + path = self._write(tmp_path, "Wan2.2-A14B-SmoothMix-T2V-HighNoise.safetensors", _native_sd(16)) + config = ModelConfigFactory.from_model_on_disk(path, allow_unknown=True).config + assert config is not None + + restored = ModelConfigFactory.from_json(config.model_dump_json()) + assert isinstance(restored, Main_Checkpoint_Wan_Config) + assert restored.variant == WanVariantType.T2V_A14B + assert restored.expert == "high" + + def test_identified_record_has_a_registered_loader(self, tmp_path: Path) -> None: + from invokeai.backend.model_manager.configs.factory import ModelConfigFactory + from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry + from invokeai.backend.model_manager.load.model_loaders.wan import WanCheckpointModel + from invokeai.backend.model_manager.taxonomy import SubModelType + + path = self._write(tmp_path, "Wan2.2-A14B-SmoothMix-T2V-HighNoise.safetensors", _native_sd(16)) + config = ModelConfigFactory.from_model_on_disk(path, allow_unknown=True).config + assert config is not None + + implementation, _, _ = ModelLoaderRegistry.get_implementation(config, SubModelType.Transformer) + assert implementation is WanCheckpointModel + + +class TestExpertFilenameHeuristic: + """The A14B expert is not recoverable from the weights and single-file releases + carry no metadata declaring it, so the filename is the only signal there is. + + Two conventions are both common in the wild and both have to work: an explicit + ``high_noise``/``low_noise``, and a bare ``HIGH``/``LOW`` token (the whole Kijai + fp8 catalogue). Scored against 108 real filenames pulled from the Kijai, + Comfy-Org and QuantStack repos, this table's rules give 0 missed and 0 + mislabelled. + """ + + @pytest.mark.parametrize( + "name, expected", + [ + # --- explicit noise markers, all spellings --- + ("wan2.2-t2v-a14b-high_noise-Q4_K_M", "high"), + ("Wan2.2-T2V-A14B-High-Noise-Q4_K_M", "high"), + ("wan_a14b_highnoise_q4", "high"), + ("wan2.2-t2v-a14b-low_noise-Q4_K_M", "low"), + ("Wan2.2-A14B-LowNoise-Q4", "low"), + ("Wan2.2 A14B high noise", "high"), + ("wan22.low.noise.v3", "low"), + ("wan22_noise_high_expert", "high"), + ("low_noise14B", "low"), + # --- bare HIGH/LOW: real releases that MUST be detected (#9463 follow-up). + # Regressing these leaves most single-file A14B models unpairable. + ("Wan2_2-T2V-A14B-HIGH_fp8_e4m3fn_scaled_KJ", "high"), + ("Wan2_2-I2V-A14B-LOW_fp8_e5m2_scaled_KJ", "low"), + ("Wan2_2-Fun-InP-A14B-HIGH_fp8_e4m3fn_scaled_KJ", "high"), + ("Wan2_2-T2V-A14B-LOW-HoloCine-full_fp8_e4m3fn_scaled_KJ", "low"), + ("SmoothMix_I2V_v2_High-Q4_K_M", "high"), + ("smoothMixWan22I2VT2V_t2vHigh-Q6_K", "high"), + # A marker sitting next to an unrelated descriptor is still a marker. + ("Wan2.2_NSFW_i2v_14b_high_lighting_fp16_v2.1", "high"), + ("wan2.2_T2V_fp8_LOW_lightning_edition", "low"), + ("wan22EnhancedNSFWSVICamera_nsfwFASTMOVEFP8Low", "low"), + # A marker fused to an adjacent run of characters, not just to "noise". + ("WAN2.2t2vLOWNOISEFP8", "low"), + # --- bare high/low used as an adjective about something else --- + ("Wan2.2-A14B-T2V-lowCFG-merge", "none"), + ("wan2.2-a14b-t2v-4step-low-cfg-merge", "none"), + ("Wan22_A14B_T2V_low_step_v2", "none"), + ("Wan2.2_A14B_highRes_finetune", "none"), + ("Wan2.2_A14B_lowVRAM", "none"), + ("Wan2.2-A14B-HighQuality", "none"), + ("wan-14B_vace_phantom_v2_LowSteps[Causvid]_fp8_e4m3fn", "none"), + # ...but only the FOLLOWING token disqualifies. "Low Angle" is a camera + # angle; "Angle HIGH" is the high-noise expert of a camera-angle LoRA. + ("Extream Low Angle HIGH - Wan2.2 - v1 low-angle shot", "high"), + ("Wan22_I2V_VBVR_HIGH_rank_64_fp16", "high"), + ("Wan2.2_Remix_NSFW_i2v_14b_low_lighting_fp16_v2.1", "low"), + # --- a file that serves BOTH experts must not be tagged with one. + # For a LoRA, 'none' means "apply to both", which is the right answer. --- + ("Wan2.2-I2V-AnalBlasting-HIGH-LOW", "none"), + ("Nier yorha HIGH + LOW - Wan2.2 - nier", "none"), + ("Anal Insertion I2V LOW + HIGH - Wan2.2 - v2", "none"), + ("wan2.2-i2v-low-to-high-lora", "none"), + # --- no marker at all --- + ("wan2.2-ti2v-5b-Q4_K_M", "none"), + ("wan-A14B-flagship", "none"), + # --- token matching: a marker must never fire on a substring. The last + # two were mismatched by the original substring heuristic as well. --- + ("wan22-slow-motion-a14b", "none"), + ("wan22-highway-lora-merge", "none"), + ("wan22-flow-shift-tune", "none"), + ("wan22-slow-noise-test", "none"), + ("wan22_flownoise_v1", "none"), + # --- an explicit noise marker outranks a bare token found elsewhere --- + ("wan2.2_t2v_4step_LOW_lightning_high_noise", "high"), + # ...but a `noise` qualifies the whole run of markers next to it, so a name + # listing both experts is a file that serves both, not the one that happens + # to touch the word. Real: moriqqe/Mabrle_wan2.2_low_high_noise and + # Chromatraining/v1_FGO_nitocris_morgan_wan2.2_t2v_low_high_noise_14B_fp16. + # This used to return 'high', which disagreed with the bare spelling + # ("...HIGH-LOW" -> 'none') for the same meaning. + ("wan2.2_t2v_low_high_noise_14B_fp16", "none"), + ("Mabrle_wan2.2_low_high_noise", "none"), + ("Wan2.2-I2V-A14B-HIGH_NOISE-LOW_NOISE-merged", "none"), + # A disqualifier following the run still wins over an adjacent `noise`: + # "noise ... LOW VRAM" is describing VRAM. + ("Wan2.2-A14B-add-noise-LOW-VRAM", "none"), + ("wan22_a14b_noise_low_cfg", "none"), + # A disqualifier consumes only the marker it is attached to, not every + # marker beside it. `HIGH_lowVRAM` is the high-noise expert of a low-VRAM + # build; swallowing the run would drop a correct tag, which disables the + # pair checks for a main and applies a single-expert LoRA to both experts. + ("Wan2_2-T2V-A14B-HIGH_lowVRAM_fp8_scaled_KJ", "high"), + ("Wan2.2-I2V-A14B-HIGH-low-vram", "high"), + ("Wan2.2-T2V-A14B-LOW-high-res", "low"), + ("wan22_a14b_HIGH_low_mem", "high"), + # A conflict in the explicit tier is final — a stray bare marker later in + # the name must not rescue it into a confident answer. + ("wan2.2_low_high_noise_merged_LOW", "none"), + ("wan22_high_noise_low_noise_pack_high", "none"), + ], + ) + def test_filename_heuristic(self, name: str, expected: str) -> None: + assert _detect_wan_expert(name) == expected + + @pytest.mark.parametrize( + "declared, expected", + [ + ({"model_type": "Wan22-I2V-A14B-low"}, "low"), + ({"model_type": "Wan2_2-T2V-A14B-HIGH"}, "high"), + ({"general.name": "Wan2.2 T2V A14B high noise"}, "high"), + ({"model_type": "Wan2_2-I2V-A14B"}, "none"), + ({"format": "pt"}, "none"), + ], + ) + def test_expert_falls_back_to_declared_metadata(self, tmp_path: Path, declared: dict, expected: str) -> None: + """Sampled 2026-08-13: every Wan 2.2 safetensors in Kijai/WanVideo_comfy_fp8_scaled + carries __metadata__["model_type"] naming the expert, while the Comfy-Org + repackaged files carry no __metadata__ at all — hence filename first, metadata + as the fallback, and neither alone is sufficient.""" + from safetensors.torch import save_file + + # A name with no marker at all, so only the metadata can settle the expert. + path = tmp_path / "my-wan-model.safetensors" + save_file(_native_sd(36), path, metadata={k: str(v) for k, v in declared.items()}) + + config = Main_Checkpoint_Wan_Config.from_model_on_disk(ModelOnDisk(path), _build_overrides(path, path.stem)) + assert config.expert == expected + + def test_filename_outranks_metadata(self, tmp_path: Path) -> None: + """Renaming the file is the only lever a user has to correct a mis-detected + expert — there is no UI for the field, and the Wan model loader's error tells + them to use it. An embedded model_type must not override that.""" + from safetensors.torch import save_file + + path = tmp_path / "Wan2.2-A14B-I2V-high_noise.safetensors" + save_file(_native_sd(36), path, metadata={"model_type": "Wan22-I2V-A14B-low"}) + + config = Main_Checkpoint_Wan_Config.from_model_on_disk(ModelOnDisk(path), _build_overrides(path, path.stem)) + assert config.expert == "high" + + @pytest.mark.parametrize("name", ["Wan2.2_TI2V_5B_lowVRAM", "Wan2.2-TI2V-5B-Turbo-lowSteps", "wan22-5b-LOW"]) + def test_ti2v_5b_never_gets_an_expert(self, tmp_path: Path, name: str) -> None: + """TI2V-5B is single-transformer, so the expert field is meaningless — and a + stray 'low' would put the model in the Transformer (Low Noise) picker, where + it can never be a valid A14B partner.""" + config = _probe(tmp_path, f"{name}.safetensors", _diffusers_sd(48)) + assert config.variant == WanVariantType.TI2V_5B + assert config.expert == "none" diff --git a/tests/backend/model_manager/configs/test_wan_gguf_config.py b/tests/backend/model_manager/configs/test_wan_gguf_config.py index 5678c381817..834efbcf715 100644 --- a/tests/backend/model_manager/configs/test_wan_gguf_config.py +++ b/tests/backend/model_manager/configs/test_wan_gguf_config.py @@ -11,8 +11,8 @@ from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError from invokeai.backend.model_manager.configs.main import ( Main_GGUF_Wan_Config, - _detect_wan_gguf_expert, - _detect_wan_gguf_variant, + _detect_wan_expert, + _detect_wan_variant_from_state_dict, _has_wan_keys, _is_native_wan_layout, ) @@ -144,26 +144,26 @@ def test_diffusers_ti2v_is_not_native(self): class TestVariantDetection: def test_a14b_from_16ch(self): sd = _wan_a14b_state_dict() - assert _detect_wan_gguf_variant(sd) == WanVariantType.T2V_A14B + assert _detect_wan_variant_from_state_dict(sd) == WanVariantType.T2V_A14B def test_ti2v_from_48ch(self): sd = _wan_ti2v_state_dict() - assert _detect_wan_gguf_variant(sd) == WanVariantType.TI2V_5B + assert _detect_wan_variant_from_state_dict(sd) == WanVariantType.TI2V_5B def test_i2v_a14b_from_36ch(self): """Wan 2.2 I2V has the same A14B architecture as T2V but with in_channels=36 because the ref-image latents and first-frame mask are concatenated to the noise along the channel dim before patch embedding.""" sd = _wan_i2v_a14b_state_dict() - assert _detect_wan_gguf_variant(sd) == WanVariantType.I2V_A14B + assert _detect_wan_variant_from_state_dict(sd) == WanVariantType.I2V_A14B def test_unknown_channel_count_returns_none(self): sd = {"patch_embedding.weight": _ggml((1, 32, 1, 2, 2))} - assert _detect_wan_gguf_variant(sd) is None + assert _detect_wan_variant_from_state_dict(sd) is None def test_missing_patch_embedding_returns_none(self): sd = {"blocks.0.attn1.to_q.weight": _ggml((1, 1))} - assert _detect_wan_gguf_variant(sd) is None + assert _detect_wan_variant_from_state_dict(sd) is None class TestExpertFilenameHeuristic: @@ -180,7 +180,7 @@ class TestExpertFilenameHeuristic: ], ) def test_filename_heuristic(self, name: str, expected: str): - assert _detect_wan_gguf_expert(name) == expected + assert _detect_wan_expert(name) == expected class TestProbe: @@ -202,6 +202,46 @@ def test_rejects_wan_2_1(self, filename: str, state_dict: dict) -> None: _build_overrides(path, "unsupported Wan 2.1"), ) + def test_rejects_an_unsupported_wan_variant(self) -> None: + """The branch-family refusals were added to this probe alongside the checkpoint + one, but only the checkpoint side was covered — deleting the check here left the + whole suite green. A Fun-Camera / S2V / Animate GGUF builds a correctly shaped + transformer and would generate with its conditioning branch silently absent.""" + sd = _wan_a14b_state_dict() + sd["control_adapter.conv.weight"] = _ggml((5120, 16, 1, 2, 2)) + + with TemporaryDirectory() as tmp: + path = Path(tmp) / "Wan2.2-Fun-Camera-A14B-high_noise-Q4_K_M.gguf" + path.touch() + + with pytest.raises(NotAMatchError, match="Fun"): + Main_GGUF_Wan_Config.from_model_on_disk( + _make_mod(path, sd), + _build_overrides(path, "Fun-Camera"), + ) + + def test_rejects_a_lora_carrying_a_full_patch_embedding(self) -> None: + """Same asymmetry: the LoRA-vs-transformer guard was added here for parity with + the checkpoint probe and had no test. A Wan I2V adapter bundles a replacement + patch_embedding, so without the undecorated-block-weight requirement it matches + the main probe, outranks the LoRA probe, and lands in the main dropdown.""" + sd = { + "diffusion_model.patch_embedding.weight": _ggml((5120, 36, 1, 2, 2)), + "diffusion_model.condition_embedder.text_embedder.linear_1.weight": _ggml((5120, 4096)), + "diffusion_model.blocks.0.attn1.to_q.lora_down.weight": _ggml((16, 5120)), + "diffusion_model.blocks.0.attn1.to_q.lora_up.weight": _ggml((5120, 16)), + } + + with TemporaryDirectory() as tmp: + path = Path(tmp) / "Wan2.2-I2V-A14B-high_noise-lora-Q4_K_M.gguf" + path.touch() + + with pytest.raises(NotAMatchError): + Main_GGUF_Wan_Config.from_model_on_disk( + _make_mod(path, sd), + _build_overrides(path, "Wan LoRA"), + ) + def test_rejects_ambiguous_a14b_filename(self) -> None: with TemporaryDirectory() as tmp: path = Path(tmp) / "renamed-high_noise-Q4_K_M.gguf" @@ -324,3 +364,20 @@ def test_explicit_expert_override(self): overrides, ) assert cfg.expert == "low" + + +def test_rejects_misnamed_wan_2_1_i2v_gguf() -> None: + """A Wan 2.1 I2V GGUF renamed to look like Wan 2.2 must still be refused — + its CLIP image embedder is a Wan 2.1-only feature.""" + sd = _wan_i2v_a14b_state_dict() + sd["img_emb.proj.0.weight"] = _ggml((1280, 1280)) + + with TemporaryDirectory() as tmp: + path = Path(tmp) / "Wan2.2-I2V-A14B-high_noise-Q4_K_M.gguf" + path.touch() + + with pytest.raises(NotAMatchError, match="Wan 2.1"): + Main_GGUF_Wan_Config.from_model_on_disk( + _make_mod(path, sd), + _build_overrides(path, "misnamed Wan 2.1 I2V"), + ) diff --git a/tests/backend/model_manager/configs/test_wan_lora_config.py b/tests/backend/model_manager/configs/test_wan_lora_config.py index 43f55db06b2..34a06f6fd68 100644 --- a/tests/backend/model_manager/configs/test_wan_lora_config.py +++ b/tests/backend/model_manager/configs/test_wan_lora_config.py @@ -265,6 +265,62 @@ def test_variant_detected_as_5b_when_inner_dim_3072(self): assert cfg.base == BaseModelType.Wan assert cfg.variant == "5b" + def test_ti2v5b_lora_is_never_tagged_with_an_expert(self): + """TI2V-5B is single-transformer, so an expert tag on a 5B LoRA is not merely + meaningless — it makes the LoRA inert. + + ``_resolve_target("auto", "low")`` routes to ``loras_low_noise`` alone, and the + 5B denoise path only ever reads the primary list, so the generation succeeds with + the LoRA silently absent. The linear UI hard-codes ``target="auto"`` and surfaces + nothing, so the only signal is a server-log warning. + + The bare-token convention makes this reachable on perfectly ordinary names: the + disqualifier list can't help, because ``low`` here is followed by an unrelated + descriptive word rather than a known false-positive marker. Mirrors the same pin + in ``_resolve_wan_expert`` on the main-model side. + + Only the ``low`` half is inert — a ``high`` tag routes to the primary list, which + the 5B path does read — but neither is meaningful on a single-transformer model, + and leaving the field unset keeps the record honest about what it knows. + """ + for stem in ("Wan2.2_TI2V_5B_low_light_v2", "wan2_2_5B_HIGH_detail_v1"): + with TemporaryDirectory() as tmp: + f = Path(tmp) / f"{stem}.safetensors" + f.touch() + cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk( + _make_mod(f, self._wan_ti2v5b_sd()), + _overrides(f, stem), + ) + assert cfg.variant == "5b" + assert cfg.expert is None, f"{stem} was tagged '{cfg.expert}' on a single-transformer model" + + def test_a14b_lora_with_the_same_name_shape_still_gets_tagged(self): + """The 5B pin must key on the variant, not on the name — the bare-token reading + is what makes A14B Lightning-style distill pairs route to the right expert.""" + with TemporaryDirectory() as tmp: + f = Path(tmp) / "Wan2.2_A14B_low_light_v2.safetensors" + f.touch() + cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk( + _make_mod(f, self._wan_diffusers_sd()), + _overrides(f, "a14b low"), + ) + assert cfg.variant == "a14b" + assert cfg.expert == "low" + + def test_expert_override_survives_the_ti2v5b_pin(self): + """The pin is an inference guard, not a veto — an explicit override still wins, + the same way it does for every other auto-detected field.""" + with TemporaryDirectory() as tmp: + f = Path(tmp) / "ti2v5b-manual.safetensors" + f.touch() + overrides = _overrides(f, "manual") + overrides["expert"] = "low" + cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk( + _make_mod(f, self._wan_ti2v5b_sd()), + overrides, + ) + assert cfg.expert == "low" + def test_variant_none_when_unrecognised_inner_dim(self): """A future Wan family or a LoRA touching only ffn at non-attn dims should map to variant=None rather than mis-classify.""" diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py new file mode 100644 index 00000000000..1d97c7d4eb4 --- /dev/null +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -0,0 +1,332 @@ +"""Tests for the single-file Wan 2.2 checkpoint loader (WanCheckpointModel). + +Round-trips a real (tiny) ``WanTransformer3DModel`` through the on-disk formats +the community actually ships, so the shape-driven architecture inference is +checked against diffusers' own module tree rather than against a hand-written +expectation. +""" + +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest +import torch +from safetensors.torch import save_file + +from invokeai.backend.model_manager.load.model_loaders.wan import ( + _WAN_NATIVE_TO_DIFFUSERS_RENAMES, + WanCheckpointModel, + _build_wan_transformer_config, +) +from invokeai.backend.model_manager.taxonomy import SubModelType, WanVariantType + +# A structurally faithful but tiny Wan transformer. attention_head_dim must stay +# at 128 — the loader derives num_attention_heads as inner_dim // 128, matching +# the whole Wan 2.2 family. +TINY_MODEL_KWARGS = { + "patch_size": (1, 2, 2), + "in_channels": 16, + "out_channels": 16, + "num_layers": 2, + "attention_head_dim": 128, + "num_attention_heads": 1, + "ffn_dim": 64, + "text_dim": 32, +} + + +def _tiny_model(): + from diffusers import WanTransformer3DModel + + return WanTransformer3DModel(**TINY_MODEL_KWARGS) + + +def _to_native_layout(sd: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """Invert the loader's native->diffusers rename table. + + Applied longest-replacement-first so that, e.g., ``condition_embedder. + text_embedder.linear_1`` is rewritten before the shorter ``scale_shift_table`` + rules can bite into it. + """ + rules = sorted(_WAN_NATIVE_TO_DIFFUSERS_RENAMES, key=lambda pair: len(pair[1]), reverse=True) + # norm2/norm3 swap: the forward table routes through a placeholder, so mirror + # that here rather than trying to reuse the entries directly. + rules = [(diffusers, native) for native, diffusers in rules if not native.startswith("norm")] + + native_sd: dict[str, torch.Tensor] = {} + for key, value in sd.items(): + new_key = key + for needle, replacement in rules: + new_key = new_key.replace(needle, replacement) + new_key = new_key.replace("norm2", "norm__placeholder").replace("norm3", "norm2") + new_key = new_key.replace("norm__placeholder", "norm3") + native_sd[new_key] = value + return native_sd + + +def _make_loader() -> WanCheckpointModel: + loader = object.__new__(WanCheckpointModel) + loader._ram_cache = MagicMock() + return loader + + +def _load(path: Path, variant: WanVariantType = WanVariantType.T2V_A14B): + config = MagicMock() + config.path = str(path) + config.variant = variant + + with ( + patch("invokeai.backend.model_manager.load.model_loaders.wan.TorchDevice.choose_torch_device"), + patch( + "invokeai.backend.model_manager.load.model_loaders.wan.TorchDevice.choose_bfloat16_safe_dtype", + return_value=torch.bfloat16, + ), + ): + return _make_loader()._load_from_singlefile(config) + + +class TestArchitectureInference: + def test_matches_the_model_it_came_from(self) -> None: + sd = _tiny_model().state_dict() + inferred = _build_wan_transformer_config(sd, source="test") + assert inferred == TINY_MODEL_KWARGS + + def test_reports_the_missing_key(self) -> None: + sd = _tiny_model().state_dict() + del sd["proj_out.weight"] + with pytest.raises(RuntimeError, match="proj_out.weight"): + _build_wan_transformer_config(sd, source="test") + + def test_out_channels_is_read_from_proj_out_not_assumed_equal_to_in_channels(self) -> None: + """I2V-A14B takes 36 channels in (16 noise + 16 ref-image latents + 4 mask) and + returns 16 — only the noise prediction comes back. Every other fixture here is + 16-in/16-out, which cannot tell the two apart.""" + from diffusers import WanTransformer3DModel + + asymmetric = {**TINY_MODEL_KWARGS, "in_channels": 36, "out_channels": 16} + sd = WanTransformer3DModel(**asymmetric).state_dict() + + inferred = _build_wan_transformer_config(sd, source="test") + + assert inferred["in_channels"] == 36 + assert inferred["out_channels"] == 16 + assert inferred == asymmetric + + def test_counts_layers_from_the_highest_block_index(self) -> None: + model = _tiny_model() + sd = model.state_dict() + assert _build_wan_transformer_config(sd, source="test")["num_layers"] == 2 + # Dropping the last block's keys must drop the inferred layer count too — + # the count comes from the weights, not from a per-variant lookup table. + trimmed = {k: v for k, v in sd.items() if not k.startswith("blocks.1.")} + assert _build_wan_transformer_config(trimmed, source="test")["num_layers"] == 1 + + +class TestEndToEnd: + def test_diffusers_layout_round_trip(self, tmp_path: Path) -> None: + reference = _tiny_model() + path = tmp_path / "wan22-t2v-a14b-high_noise.safetensors" + save_file(reference.state_dict(), path) + + model = _load(path) + + assert set(model.state_dict().keys()) == set(reference.state_dict().keys()) + assert all(t.dtype == torch.bfloat16 for t in model.state_dict().values() if t.is_floating_point()) + + def test_native_layout_round_trip(self, tmp_path: Path) -> None: + reference = _tiny_model() + native_sd = _to_native_layout(reference.state_dict()) + # Sanity check that the fixture really is in the native layout. + assert "text_embedding.0.weight" in native_sd + assert "condition_embedder.text_embedder.linear_1.weight" not in native_sd + + path = tmp_path / "SmoothMix_HighNoise.safetensors" + save_file(native_sd, path) + + model = _load(path) + + assert set(model.state_dict().keys()) == set(reference.state_dict().keys()) + + def test_comfyui_prefixed_round_trip(self, tmp_path: Path) -> None: + reference = _tiny_model() + prefixed = {f"model.diffusion_model.{k}": v for k, v in reference.state_dict().items()} + path = tmp_path / "wan22_t2v_low_noise.safetensors" + save_file(prefixed, path) + + model = _load(path) + + assert set(model.state_dict().keys()) == set(reference.state_dict().keys()) + + def test_fp8_scaled_is_dequantized_and_scales_are_dropped(self, tmp_path: Path) -> None: + reference = _tiny_model() + sd = {k: v.clone() for k, v in reference.state_dict().items()} + + target = "blocks.0.attn1.to_q.weight" + sd[target] = torch.full_like(sd[target], 0.5).to(torch.float8_e4m3fn) + sd["blocks.0.attn1.to_q.scale_weight"] = torch.tensor([4.0]) + sd["blocks.0.attn1.to_q.scale_input"] = torch.tensor([1.0]) + sd["scaled_fp8"] = torch.zeros(1, dtype=torch.float8_e4m3fn) + + path = tmp_path / "Wan2.2-A14B-HighNoise-fp8_scaled.safetensors" + save_file(sd, path) + + model = _load(path) + + loaded = model.state_dict() + assert set(loaded.keys()) == set(reference.state_dict().keys()) + # 0.5 * 4.0, materialised at the compute dtype. + assert loaded[target].dtype == torch.bfloat16 + assert torch.allclose(loaded[target].float(), torch.full_like(loaded[target].float(), 2.0)) + + def test_scale_bookkeeping_never_reaches_the_model(self, tmp_path: Path) -> None: + """``load_state_dict(strict=False)`` silently ignores unexpected keys, so + assert on what the loader actually hands over rather than on the result.""" + target = "blocks.0.attn1.to_q.weight" + sd = _tiny_model().state_dict() + sd[target] = torch.full_like(sd[target], 0.5) + sd["blocks.0.attn1.to_q.scale_weight"] = torch.tensor([4.0]) + sd["blocks.0.attn1.to_q.scale_input"] = torch.tensor([1.0]) + sd["scaled_fp8"] = torch.zeros(1, dtype=torch.float8_e4m3fn) + + path = tmp_path / "Wan2.2-A14B-HighNoise-fp8_scaled.safetensors" + save_file(sd, path) + + model = MagicMock() + model.load_state_dict.return_value = SimpleNamespace(missing_keys=[], unexpected_keys=[]) + with patch("diffusers.WanTransformer3DModel", return_value=model): + _load(path) + + handed_over = model.load_state_dict.call_args.args[0] + assert not [k for k in handed_over if k.endswith((".scale_weight", ".scale_input")) or k == "scaled_fp8"] + # ...without eating scale_shift_table, which is a real Wan parameter. + assert "scale_shift_table" in handed_over + # The scale is applied whatever the weight's dtype — `_dequantize_comfyui_fp8` + # has no fp8 gate, deliberately, because "scaled" checkpoints are not all fp8. + # Asserted rather than left implicit: this fixture's weight is bf16, so without + # this line the test would construct a 4x-scaled weight and say nothing about it. + assert torch.allclose(handed_over[target].float(), torch.full_like(handed_over[target].float(), 2.0)) + + def test_extra_modules_are_refused_not_dropped(self, tmp_path: Path) -> None: + """`strict=False` silently discards weights the model has nowhere to put. + + Several Wan 2.2 derivatives are supersets of the plain transformer — real + wan2.2_fun_camera_high_noise_14B_bf16.safetensors adds 6 `control_adapter.*` + keys, S2V adds 165, Animate adds 127. They build a correctly-shaped model and + report zero missing keys, so without this check they load clean and then + generate with their entire conditioning branch absent. + + The probe turns away the families we know by name; this is the generic + backstop for the ones nobody has enumerated yet. + """ + sd = _tiny_model().state_dict() + sd["control_adapter.conv.weight"] = torch.zeros(128, 16, 1, 2, 2) + sd["control_adapter.residual_blocks.0.conv1.weight"] = torch.zeros(128, 128) + path = tmp_path / "wan22-fun-camera-high_noise.safetensors" + save_file(sd, path) + + with pytest.raises(RuntimeError, match="control_adapter"): + _load(path) + + def test_all_in_one_bundled_components_are_dropped_not_refused(self, tmp_path: Path) -> None: + """The "all-in-one" packaging convention bundles transformer + VAE + CLIP in one + file so ComfyUI's `Load Checkpoint` node can supply all three. + Phr00t/WAN2.2-14B-Rapid-AllInOne and its ~110 GGUF conversions + (befox/WAN2.2-14B-Rapid-AllInOne-GGUF) ship this way. + + These loaded fine before the unexpected-key backstop existed — InvokeAI sources + the VAE and encoder from separately-wired models and simply ignores the bundled + copies — so refusing them is a regression, not a safety check. + """ + sd = _tiny_model().state_dict() + sd["vae.decoder.conv_in.weight"] = torch.zeros(96, 16, 3, 3) + sd["text_encoders.umt5xxl.shared.weight"] = torch.zeros(256, 32) + sd["model_ema.patch_embedding.weight"] = torch.zeros(128, 16, 1, 2, 2) + path = tmp_path / "wan2.2-t2v-rapid-aio-v10-high_noise.safetensors" + save_file(sd, path) + + model = MagicMock() + model.load_state_dict.return_value = SimpleNamespace(missing_keys=[], unexpected_keys=[]) + with patch("diffusers.WanTransformer3DModel", return_value=model): + _load(path) + + # Assert on the dict actually handed over. `hasattr(model, "vae")` would be a + # tautology — a freshly built WanTransformer3DModel has no such attribute either + # way — and it would not catch the bundled weights being cast and RAM-reserved + # before load_state_dict discarded them, which is the cost this avoids. + handed_over = model.load_state_dict.call_args.args[0] + assert [k for k in handed_over if k.startswith(("vae.", "text_encoders.", "model_ema."))] == [] + assert "patch_embedding.weight" in handed_over + + def test_merged_lora_residue_is_dropped_not_refused(self, tmp_path: Path) -> None: + """`configs.main._has_wan_transformer_block_weights` deliberately admits main + models that retain merged-in LoRA tensors, using a *positive* structural test + rather than a "reject anything with lora keys" exclusion. The loader has to + agree, or the probe accepts a file that the loader then refuses with a reason + blaming Animate/S2V/Fun-Camera. + """ + sd = _tiny_model().state_dict() + sd["blocks.0.attn1.to_q.lora_down.weight"] = torch.zeros(8, 128) + sd["blocks.0.attn1.to_q.lora_up.weight"] = torch.zeros(128, 8) + sd["blocks.0.attn1.to_q.alpha"] = torch.zeros(()) + path = tmp_path / "Wan2.2-T2V-A14B-high_noise-merged.safetensors" + save_file(sd, path) + + _load(path) # must not raise + + @pytest.mark.parametrize( + "residue", + [ + pytest.param(["blocks.0.attn1.to_q.lora_A.weight", "blocks.0.attn1.to_q.lora_B.weight"], id="peft"), + pytest.param(["blocks.0.attn1.to_q.lokr_w1", "blocks.0.attn1.to_q.lokr_w2"], id="lokr"), + pytest.param(["blocks.0.attn1.to_q.hada_w1_a", "blocks.0.attn1.to_q.hada_w2_a"], id="loha"), + pytest.param(["blocks.0.attn1.to_q.dora_scale"], id="dora"), + ], + ) + def test_every_lycoris_family_the_probe_accepts_also_loads(self, tmp_path: Path, residue: list[str]) -> None: + """The allowlist has to cover the same families as `LoRA_LyCORIS_Wan_Config`'s + suffix set, or the probe accepts a merged file the loader then refuses — with a + message blaming Animate/S2V/Fun-Camera, which is no help at all.""" + sd = _tiny_model().state_dict() + for key in residue: + sd[key] = torch.zeros(8, 8) + path = tmp_path / "Wan2.2-T2V-A14B-high_noise-merged.safetensors" + save_file(sd, path) + + _load(path) # must not raise + + def test_unknown_extra_module_is_still_refused(self, tmp_path: Path) -> None: + """The allowlist above must not turn the backstop off. A conditioning branch + nobody has enumerated yet still has to fail loudly rather than load degraded. + """ + sd = _tiny_model().state_dict() + sd["vae.decoder.conv_in.weight"] = torch.zeros(96, 16, 3, 3) # benign, alongside + sd["mystery_adapter.proj.weight"] = torch.zeros(128, 128) + path = tmp_path / "wan22-unknown-variant-high_noise.safetensors" + save_file(sd, path) + + with pytest.raises(RuntimeError, match="mystery_adapter"): + _load(path) + + def test_missing_parameter_is_reported(self, tmp_path: Path) -> None: + sd = _tiny_model().state_dict() + del sd["blocks.1.attn1.to_q.weight"] + path = tmp_path / "wan22-truncated-high_noise.safetensors" + save_file(sd, path) + + with pytest.raises(RuntimeError, match="blocks.1.attn1.to_q.weight"): + _load(path) + + +class TestSubmodelGuard: + @pytest.mark.parametrize("submodel", [None, SubModelType.VAE, SubModelType.TextEncoder]) + def test_only_the_transformer_submodel_is_served(self, submodel) -> None: + from invokeai.backend.model_manager.configs.main import Main_Checkpoint_Wan_Config + + config = MagicMock(spec=Main_Checkpoint_Wan_Config) + with pytest.raises(ValueError, match="Transformer"): + _make_loader()._load_model(config, submodel) + + def test_wrong_config_class_is_rejected(self) -> None: + with pytest.raises(TypeError, match="Main_Checkpoint_Wan_Config"): + _make_loader()._load_model(MagicMock(), SubModelType.Transformer) diff --git a/tests/backend/model_manager/load/test_wan_loader.py b/tests/backend/model_manager/load/test_wan_loader.py index 691a3850561..6b2b488c3ee 100644 --- a/tests/backend/model_manager/load/test_wan_loader.py +++ b/tests/backend/model_manager/load/test_wan_loader.py @@ -182,6 +182,131 @@ def test_plain_torch_tensor_passes_through(self): assert out["plain"] is plain +def _run_gguf_loader(extra_keys: list[str], native_layout: bool = False) -> dict: + """Drive WanGGUFCheckpointModel over a state dict carrying `extra_keys`. + + The extras go into the *state dict*, not into a mocked `unexpected_keys`, so the + loader's own classification runs. Returns the dict actually handed to + `load_state_dict`, which is what proves a key was dropped rather than merely + tolerated. + + With `native_layout`, the base keys use the upstream ComfyUI/QuantStack naming so + `_convert_wan_native_to_diffusers` runs first — a different path to the same gate, + and the one where an unmapped key is possible at all. + """ + if native_layout: + state_dict = { + "patch_embedding.weight": torch.zeros(128, 16, 1, 2, 2), + "text_embedding.0.weight": torch.zeros(128, 4096), + "blocks.0.ffn.0.weight": torch.zeros(256, 128), + "head.head.weight": torch.zeros(64, 128), + } + else: + state_dict = { + "patch_embedding.weight": torch.zeros(128, 16, 1, 2, 2), + "blocks.0.ffn.net.0.proj.weight": torch.zeros(256, 128), + "proj_out.weight": torch.zeros(64, 128), + } + for key in extra_keys: + state_dict[key] = torch.zeros(4, 4) + model = MagicMock() + # Report as unexpected whatever the loader still hands over that isn't a real param. + # Named post-conversion, since that is what reaches `load_state_dict`. + real_params = { + "patch_embedding.weight", + "condition_embedder.text_embedder.linear_1.weight", + "blocks.0.ffn.net.0.proj.weight", + "proj_out.weight", + } + model.load_state_dict.side_effect = lambda sd, **_: SimpleNamespace( + missing_keys=[], unexpected_keys=[k for k in sd if k not in real_params] + ) + config = SimpleNamespace(path="/models/wan.gguf", variant=WanVariantType.T2V_A14B) + loader = object.__new__(WanGGUFCheckpointModel) + + with ( + patch("invokeai.backend.model_manager.load.model_loaders.wan.gguf_sd_loader", return_value=state_dict), + patch( + "invokeai.backend.model_manager.load.model_loaders.wan._unwrap_unquantized_to_compute_dtype", + side_effect=lambda value: value, + ), + patch("invokeai.backend.model_manager.load.model_loaders.wan.TorchDevice.choose_torch_device"), + patch( + "invokeai.backend.model_manager.load.model_loaders.wan.TorchDevice.choose_bfloat16_safe_dtype", + return_value=torch.bfloat16, + ), + patch("accelerate.init_empty_weights", return_value=nullcontext()), + patch("diffusers.WanTransformer3DModel", return_value=model), + ): + loader._load_from_singlefile(config) + + return model.load_state_dict.call_args.args[0] + + +def test_gguf_loader_drops_all_in_one_bundled_components_before_loading() -> None: + """The "all-in-one" GGUF convention bundles the VAE and text encoder alongside the + transformer — befox/WAN2.2-14B-Rapid-AllInOne-GGUF ships ~110 such files, converted + from Phr00t/WAN2.2-14B-Rapid-AllInOne. + + They must load (refusing them regressed a path that worked before the unexpected-key + backstop existed) *and* the bundled weights must be gone before the compute-dtype + cast and the RAM-cache reservation, not merely tolerated at load_state_dict — the + bundled UMT5-XXL alone is several GB that would otherwise be upcast and reserved. + """ + bundled = ["vae.decoder.conv_in.weight", "text_encoders.umt5xxl.shared.weight", "model_ema.patch_embedding.weight"] + + handed_over = _run_gguf_loader(bundled) + + assert [key for key in bundled if key in handed_over] == [] + + +def test_gguf_loader_still_refuses_an_unknown_conditioning_branch() -> None: + """The allowlist must not switch the backstop off — an unenumerated Wan derivative + still has to fail loudly rather than generate with its conditioning branch absent.""" + with pytest.raises(RuntimeError, match="audio_injector"): + _run_gguf_loader(["vae.decoder.conv_in.weight", "audio_injector.0.proj.weight"]) + + +def test_gguf_loader_refuses_a_native_layout_key_the_rename_table_does_not_map() -> None: + """Pins the intended outcome for the one case the unexpected-key backstop newly + changes for GGUF: a native-layout key that survives `_convert_wan_native_to_diffusers` + unrenamed. Before the backstop the GGUF loader checked `missing_keys` only, so such a + key was silently discarded by `load_state_dict(strict=False)`. + + The branch here is deliberately one the probe does *not* enumerate. Every family + `_find_unsupported_wan_variant_marker` knows about — Animate, S2V, Fun-Control, VACE — + is turned away with `NotAMatchError` at identification time and can never reach a + loader, so using one of those names would pin a scenario that cannot occur. This + backstop exists for the families nobody has enumerated yet, which is exactly what an + unmapped native key looks like. + + The blast radius is narrower than the change looks: an unmapped key that *should* + have become a real parameter also leaves that parameter unfilled, which the + pre-existing `missing_keys` check already caught. What is new is a whole extra + conditioning branch riding along, and generating with it quietly absent is worse + than refusing. + """ + with pytest.raises(RuntimeError, match="pose_adapter"): + _run_gguf_loader(["pose_adapter.0.proj.weight"], native_layout=True) + + +def test_gguf_loader_accepts_a_native_layout_all_in_one_bundle() -> None: + """The benign-extras drop has to keep working on the native-layout path. + + It runs *before* the rename table (`wan.py`: `_drop_benign_extra_keys` at the top of + `_load_from_singlefile`, `_convert_wan_native_to_diffusers` several lines later), so + the bundled names never reach the rewrite at all — the two passes do not interact. + What this pins is the whole native-layout pipeline end to end: it is the test that + fails if the rename table stops producing the diffusers names, because then the + genuine transformer keys arrive unrenamed and trip the backstop. + """ + bundled = ["vae.decoder.conv_in.weight", "text_encoders.umt5xxl.shared.weight"] + + handed_over = _run_gguf_loader(bundled, native_layout=True) + + assert [key for key in bundled if key in handed_over] == [] + + def test_gguf_loader_rejects_missing_model_parameter() -> None: state_dict = { "patch_embedding.weight": torch.zeros(128, 16, 1, 2, 2),