From 5e731992dd014a8b8ceb1e84ad1e44eb30f7c45c Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 13 Aug 2026 19:24:15 -0400 Subject: [PATCH 01/15] feat(model manager): support single-file Wan 2.2 checkpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #9463. Wan 2.2 main models could only be imported as a Diffusers folder or a GGUF file. The community ships fine-tunes as a single `.safetensors` per transformer (CivitAI, ComfyUI-oriented HF repos), and no config class matched those, so the model manager reported "unidentified model". - Add `Main_Checkpoint_Wan_Config`: probes for Wan transformer keys in either the native upstream or the diffusers key layout, tolerating the ComfyUI `model.diffusion_model.` prefix, and records variant + MoE expert. Wan 2.1 is rejected on architecture (CLIP image embedder, 1536-dim transformer, VACE control blocks) rather than by demanding the filename say "wan2.2" — community fine-tunes routinely drop the version from the name, and rejecting those was the reported bug. The same architectural check is now also applied to the GGUF probe, which previously only had the filename gate. - Add `WanCheckpointModel` loader: strips the ComfyUI prefix, dequantizes ComfyUI `fp8_scaled` weights, converts native keys to the diffusers layout, and derives the `WanTransformer3DModel` config from the weights themselves. The shape inference is factored out of the GGUF loader so both share it. - Broaden the A14B expert filename heuristic to bare `high` / `low` tokens (matched at token boundaries so "slow"/"flow"/"highway" can't trip it). - Teach `wan_model_loader` that single-file means GGUF *or* checkpoint, for both the main model and the low-noise expert slot; the two experts may mix formats. Its pairing errors now name the offending models and explain that the expert comes from the filename. - Move the shared ComfyUI single-file helpers out of the Qwen Image loader into `comfyui_state_dict_utils` rather than adding a third copy. Co-Authored-By: Claude Opus 5 (1M context) --- invokeai/app/invocations/wan_model_loader.py | 99 +++--- .../backend/model_manager/configs/factory.py | 2 + .../backend/model_manager/configs/main.py | 184 +++++++++-- .../model_loaders/comfyui_state_dict_utils.py | 88 ++++++ .../load/model_loaders/qwen_image.py | 80 +---- .../model_manager/load/model_loaders/wan.py | 246 +++++++++++---- invokeai/frontend/web/openapi.json | 219 ++++++++++++- .../Advanced/ParamWanModelSelects.tsx | 19 +- .../src/services/api/hooks/modelsByType.ts | 4 +- .../frontend/web/src/services/api/schema.ts | 139 ++++++++- .../frontend/web/src/services/api/types.ts | 14 +- .../app/invocations/test_wan_model_loader.py | 83 ++++- .../configs/test_wan_checkpoint_config.py | 292 ++++++++++++++++++ .../configs/test_wan_gguf_config.py | 33 +- .../load/test_wan_checkpoint_loader.py | 209 +++++++++++++ 15 files changed, 1458 insertions(+), 253 deletions(-) create mode 100644 invokeai/backend/model_manager/load/model_loaders/comfyui_state_dict_utils.py create mode 100644 tests/backend/model_manager/configs/test_wan_checkpoint_config.py create mode 100644 tests/backend/model_manager/load/test_wan_checkpoint_loader.py diff --git a/invokeai/app/invocations/wan_model_loader.py b/invokeai/app/invocations/wan_model_loader.py index 7524e4c0ca0..0876843217e 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,42 @@ 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.") + 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_config, 'variant', None)}." + ) - # 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 +215,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 +225,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 += ( @@ -228,6 +241,12 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: "the high-noise one is usually the better choice." ) context.logger.warning(message) + if primary_expert == "low": + message += ( + " Its filename tags it as the low-noise expert; when running a single expert, " + "the high-noise one is usually the better choice." + ) + context.logger.warning(message) # Borrow the boundary_ratio recorded on the optional Diffusers # component_source, when one is wired. @@ -243,7 +262,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/main.py b/invokeai/backend/model_manager/configs/main.py index 73b68f23fba..3559e7191c0 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,81 @@ 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", - ) - 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 + shape = _wan_patch_embedding_shape(state_dict) + if shape is None or len(shape) < 2: + return None + in_channels = 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 -def _detect_wan_gguf_expert(filename: str) -> Literal["high", "low", "none"]: +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" + + if any(isinstance(key, str) and "vace_blocks." in key for key in keys): + return "state dict has VACE control blocks, which are a Wan 2.1 VACE feature" + + return None + + +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). + Community releases tag each expert in the filename — usually ``high_noise`` / + ``low_noise`` and their hyphenated/concatenated/camel-cased spellings, but + plenty of CivitAI fine-tunes shorten it to a bare ``HIGH`` / ``low`` token. + + The name is first split at camelCase boundaries so ``HighNoise`` normalises to + ``high_noise``. The bare-token fallback matches only whole tokens, so it can't + fire on a substring like the "low" inside "slow" or "flow". + Returns 'none' when neither marker is present (single-expert model or - ambiguous filename). + an untagged filename). """ - name = filename.lower() + name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", 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" + + tokens = set(re.split(r"[^a-z0-9]+", name)) + if "high" in tokens: + return "high" + if "low" in tokens: + return "low" return "none" @@ -1902,16 +1964,82 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - ) 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 = explicit_expert or _detect_wan_expert(mod.path.stem) + + 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) + + 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") + + # 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") + + explicit_expert = override_fields.pop("expert", None) + expert = explicit_expert or _detect_wan_expert(mod.path.stem) 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..cc37a0cf713 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/comfyui_state_dict_utils.py @@ -0,0 +1,88 @@ +"""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). + """ + 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..4b27c62bebb 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, @@ -48,7 +60,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 +186,81 @@ def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict: return unwrapped +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, variant: WanVariantType, 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 + + # Layer count fallback (only triggers if the auto-count loop above found + # zero blocks, which shouldn't happen for a valid file). T2V/I2V A14B have + # 40 layers; TI2V-5B has 30. + layer_count_fallback = 30 if variant == WanVariantType.TI2V_5B else 40 + + return { + "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, + } + + @ModelLoaderRegistry.register(base=BaseModelType.Wan, type=ModelType.Main, format=ModelFormat.GGUFQuantized) class WanGGUFCheckpointModel(ModelLoader): """Loader for GGUF-quantized Wan 2.2 transformer models. @@ -235,68 +324,7 @@ 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, config.variant, source="GGUF state dict") with accelerate.init_empty_weights(): model = WanTransformer3DModel(**model_config) @@ -307,6 +335,92 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: 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) + + 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, config.variant, 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) + if incompatible_keys.missing_keys: + raise RuntimeError( + f"Wan checkpoint is missing model parameters: {sorted(incompatible_keys.missing_keys)[:10]}" + ) + return model + + @ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.WanT5Encoder, format=ModelFormat.WanT5Encoder) class WanT5EncoderLoader(ModelLoader): """Loader for the standalone Wan UMT5-XXL encoder. diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index 48cfb634896..34ee0f3ee23 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" }, @@ -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/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/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts index ef6e615dff1..8834c2766e7 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, + isWanSingleFileLowNoiseMainModelConfig, 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(isWanSingleFileLowNoiseMainModelConfig)(); 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 44e470dc49d..9c6be8d4a4a 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 @@ -39482,15 +39588,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 +39625,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 +42437,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 +42469,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 +42521,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 +42628,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 +42701,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 +43436,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.ts b/invokeai/frontend/web/src/services/api/types.ts index 37e4ee2149a..04c222e1403 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -638,12 +638,16 @@ 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 => { +/** Wan single-file main models (GGUF or safetensors checkpoint) marked as the + * low-noise expert — the second half of the A14B MoE pair. Suitable for the + * Transformer (Low Noise) picker. The two experts don't have to share a format; + * both load into the same transformer class. */ +export const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => { return ( - config.type === 'main' && config.base === 'wan' && config.format === 'gguf_quantized' && config.expert === 'low' + config.type === 'main' && + config.base === 'wan' && + (config.format === 'gguf_quantized' || config.format === 'checkpoint') && + config.expert === 'low' ); }; 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..6c09aea0ed3 --- /dev/null +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -0,0 +1,292 @@ +"""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.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" + + +class TestRejects: + def test_gguf_state_dict(self, tmp_path: Path) -> None: + sd = { + "patch_embedding.weight": GGMLTensor( + data=torch.zeros((1,), dtype=torch.uint8), + ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0, + tensor_shape=torch.Size((A14B_DIM, 16, 1, 2, 2)), + compute_dtype=torch.float32, + ), + "text_embedding.0.weight": GGMLTensor( + data=torch.zeros((1,), dtype=torch.uint8), + ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0, + tensor_shape=torch.Size((A14B_DIM, 4096)), + compute_dtype=torch.float32, + ), + } + with pytest.raises(NotAMatchError, match="GGUF"): + _probe(tmp_path, "wan2.2-t2v-a14b-high_noise-Q4_K_M.gguf", 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_wan_2_1_vace(self, tmp_path: Path) -> None: + sd = _native_sd(16) + sd["vace_blocks.0.after_proj.weight"] = _t(A14B_DIM, A14B_DIM) + with pytest.raises(NotAMatchError, match="Wan 2.1"): + _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_unknown_channel_count(self, tmp_path: Path) -> None: + with pytest.raises(NotAMatchError, match="variant"): + _probe(tmp_path, "weird-wan22.safetensors", _native_sd(24)) + + +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: + @pytest.mark.parametrize( + "name, expected", + [ + # Pre-existing spellings must keep working. + ("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-ti2v-5b-Q4_K_M", "none"), + ("wan-A14B-flagship", "none"), + # Bare high/low tokens, as used by several CivitAI fine-tunes. + ("Wan2.2-A14B-SmoothMix-T2V-HIGH", "high"), + ("Wan2.2_Exitium_Victrix_low", "low"), + # ...but only as whole tokens: these must not trip the bare-token path. + ("wan22-slow-motion-a14b", "none"), + ("wan22-highway-lora-merge", "none"), + ("wan22-flow-shift-tune", "none"), + ], + ) + def test_filename_heuristic(self, name: str, expected: str) -> None: + assert _detect_wan_expert(name) == expected 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..1c7ddb60c91 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: @@ -324,3 +324,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/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py new file mode 100644 index 00000000000..2d4011b04e9 --- /dev/null +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -0,0 +1,209 @@ +"""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, WanVariantType.T2V_A14B, 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, WanVariantType.T2V_A14B, source="test") + + def test_counts_layers_from_the_highest_block_index(self) -> None: + model = _tiny_model() + sd = model.state_dict() + assert _build_wan_transformer_config(sd, WanVariantType.T2V_A14B, 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, WanVariantType.T2V_A14B, 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.""" + sd = _tiny_model().state_dict() + 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 + + 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) From 5167575067144958f8e4194c6eabfed1547e1756 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 13 Aug 2026 19:53:10 -0400 Subject: [PATCH 02/15] fix(model manager): tighten the Wan checkpoint probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses three defects found by an adversarial review of the previous commit. 1. The broadened expert filename heuristic fired on any bare `high`/`low` token, so `...-4step-low-cfg-merge`, `..._lowVRAM`, `...HighQuality` and friends were labelled as MoE experts. That is worse than not guessing: 'none' raises a clear pairing error, but a mislabelled expert satisfies the {high, low} pair check, gets swapped into the wrong slot, and silently runs the same expert for both denoise phases. A marker now has to include `noise` — adjacent (`high noise`, `noise_high`) or fused (`highnoise`). Matching is per token rather than by substring, so `slow_noise` and `flownoise` no longer match either; the original substring heuristic got those two wrong as well. 2. A Wan LoRA can carry a full replacement `patch_embedding` (I2V adapters change in_channels 16->36) plus the text projection, which is everything `_has_wan_keys` looks for. Because Main outranks LoRA in `matches_sort_key`, such a file was pulled out of the LoRA pickers into the main-model dropdown, where it could only fail to load. The probe now also requires an undecorated `blocks.0...weight`, which a LoRA never has. 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. 3. `.ckpt`/`.pt`/`.pth`/`.bin` files were claimed by the probe but the loader reads safetensors unconditionally, so they installed cleanly and then died with an opaque header error at generation time. Restricted to `.safetensors`. Also reworded the VACE rejection: Wan 2.2 VACE variants exist, so calling it a Wan 2.1 marker was wrong. The refusal stands — this loader builds a plain WanTransformer3DModel with no control branch — but the reason now says so. Co-Authored-By: Claude Opus 5 (1M context) --- .../backend/model_manager/configs/main.py | 96 +++++++++++++++---- .../configs/test_wan_checkpoint_config.py | 92 ++++++++++++++---- 2 files changed, 147 insertions(+), 41 deletions(-) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 3559e7191c0..01bd1f92fc6 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -1865,6 +1865,34 @@ def _detect_wan_variant_from_state_dict(state_dict: dict[str | int, Any]) -> Wan 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 + ) + 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. @@ -1897,7 +1925,11 @@ def _find_wan_2_1_marker(state_dict: dict[str | int, Any]) -> str | None: return "state dict has a 1536-dim transformer, which is the Wan 2.1 T2V-1.3B architecture" if any(isinstance(key, str) and "vace_blocks." in key for key in keys): - return "state dict has VACE control blocks, which are a Wan 2.1 VACE feature" + # Not strictly a 2.1 marker — Wan 2.2 VACE variants exist — but the effect is + # the same: this loader builds a plain WanTransformer3DModel, which has no + # VACE control branch, so the vace_blocks would be dropped as unexpected keys + # and the model would quietly ignore its control input. Refuse instead. + return "state dict has VACE control blocks, and VACE models are not supported yet" return None @@ -1905,28 +1937,38 @@ def _find_wan_2_1_marker(state_dict: dict[str | int, Any]) -> str | None: 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 — usually ``high_noise`` / - ``low_noise`` and their hyphenated/concatenated/camel-cased spellings, but - plenty of CivitAI fine-tunes shorten it to a bare ``HIGH`` / ``low`` token. - - The name is first split at camelCase boundaries so ``HighNoise`` normalises to - ``high_noise``. The bare-token fallback matches only whole tokens, so it can't - fire on a substring like the "low" inside "slow" or "flow". - - Returns 'none' when neither marker is present (single-expert model or - an untagged filename). + Community releases tag each expert in the filename as some spelling of + ``high_noise`` / ``low_noise``: hyphenated, underscored, spaced, concatenated + (``highnoise``), camel-cased (``HighNoise``), or occasionally reversed + (``noise_high``). The name is split at camelCase boundaries and then on runs of + non-alphanumerics, and a match requires ``high``/``low`` to sit *next to* a + ``noise`` token — or to be fused with it into one token. + + The "noise" requirement is deliberate. A bare ``high``/``low`` token is far too + common in other roles — ``lowVRAM``, ``low-cfg``, ``lowSteps``, ``highRes``, + ``HighQuality`` — and guessing wrong is worse than not guessing. An unlabelled + A14B model ('none') raises a clear error in the Wan model loader telling the + user to pair or rename it; a *mislabelled* one passes the pair check, gets + silently swapped into the wrong slot, and quietly degrades the output. + + Token matching also stops the marker firing on a substring, so ``slow_noise`` + and ``flownoise`` are correctly left alone. + + Returns 'none' when no marker is present (single-expert model such as TI2V-5B, + or an untagged filename). """ name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", 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" - - tokens = set(re.split(r"[^a-z0-9]+", name)) - if "high" in tokens: - return "high" - if "low" in tokens: - return "low" + tokens = [token for token in re.split(r"[^a-z0-9]+", name) if token] + + for marker, expert in (("high", "high"), ("low", "low")): + if f"{marker}noise" in tokens or f"noise{marker}" in tokens: + return expert # type: ignore[return-value] + for index, token in enumerate(tokens): + if token != marker: + continue + neighbours = tokens[max(index - 1, 0) : index] + tokens[index + 1 : index + 2] + if "noise" in neighbours: + return expert # type: ignore[return-value] return "none" @@ -2011,12 +2053,24 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - 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" + ) # 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* diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index 6c09aea0ed3..41406f3f00b 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -129,24 +129,31 @@ def test_untagged_community_filename(self, tmp_path: Path) -> None: 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_state_dict(self, tmp_path: Path) -> None: + 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": GGMLTensor( - data=torch.zeros((1,), dtype=torch.uint8), - ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0, - tensor_shape=torch.Size((A14B_DIM, 16, 1, 2, 2)), - compute_dtype=torch.float32, - ), - "text_embedding.0.weight": GGMLTensor( - data=torch.zeros((1,), dtype=torch.uint8), - ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0, - tensor_shape=torch.Size((A14B_DIM, 4096)), - compute_dtype=torch.float32, - ), + "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-Q4_K_M.gguf", sd) + _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) @@ -158,10 +165,12 @@ 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_wan_2_1_vace(self, tmp_path: Path) -> None: + 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="Wan 2.1"): + with pytest.raises(NotAMatchError, match="VACE"): _probe(tmp_path, "vace-model.safetensors", sd) def test_wan_2_1_filename(self, tmp_path: Path) -> None: @@ -172,6 +181,27 @@ 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)) @@ -268,6 +298,14 @@ def test_identified_record_has_a_registered_loader(self, tmp_path: Path) -> None class TestExpertFilenameHeuristic: + """A wrong expert label is worse than no label. + + 'none' on an A14B model produces a clear error from the Wan model loader. A + *mislabelled* one satisfies the {high, low} pair check, gets swapped into the + wrong slot, and silently runs the same expert for both denoise phases. So the + heuristic only fires when 'noise' is actually part of the marker. + """ + @pytest.mark.parametrize( "name, expected", [ @@ -279,13 +317,27 @@ class TestExpertFilenameHeuristic: ("Wan2.2-A14B-LowNoise-Q4", "low"), ("wan2.2-ti2v-5b-Q4_K_M", "none"), ("wan-A14B-flagship", "none"), - # Bare high/low tokens, as used by several CivitAI fine-tunes. - ("Wan2.2-A14B-SmoothMix-T2V-HIGH", "high"), - ("Wan2.2_Exitium_Victrix_low", "low"), - # ...but only as whole tokens: these must not trip the bare-token path. + # Separators the old substring check missed, plus reversed order. + ("Wan2.2 A14B high noise", "high"), + ("wan22.low.noise.v3", "low"), + ("wan22_noise_high_expert", "high"), + # A bare high/low token is NOT a marker — it almost always describes + # something else (VRAM, CFG, step count, resolution, quality). + ("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_TI2V_5B_lowVRAM", "none"), + ("Wan2.2-TI2V-5B-Turbo-lowSteps", "none"), + ("Wan2.2-TI2V-5B-HighQuality", "none"), + ("Wan2.2-A14B-SmoothMix-T2V-HIGH", "none"), + # Token matching, so the marker can't fire on a substring. The last two + # were mismatched by the original substring-based 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"), ], ) def test_filename_heuristic(self, name: str, expected: str) -> None: From 1eef35639f72550498d621824e3c5ba03728883d Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 13 Aug 2026 20:29:14 -0400 Subject: [PATCH 03/15] fix(model manager): read the bare HIGH/LOW expert convention again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-two adversarial review found the previous commit over-corrected. Requiring `noise` in the expert marker turned off detection for the convention used by the most widely mirrored single-file Wan 2.2 releases — the Kijai fp8 catalogue names every file `Wan2_2-T2V-A14B-HIGH_fp8_e4m3fn_scaled_KJ.safetensors` — leaving them unpairable, with no UI to set the expert after install. Scored against 108 real filenames pulled from the Kijai, Comfy-Org and QuantStack repo trees: pre-branch main missed=25 mislabelled=0 bare-token (1st attempt) missed= 0 mislabelled=0 noise-required (2nd) missed=25 mislabelled=0 this commit missed= 0 mislabelled=0 So a bare `high`/`low` token counts again, but only when no neighbouring token marks it as an adjective about something else (`lowVRAM`, `low-cfg`, `highRes`). That disqualifier list is deliberately short: real releases put markers next to plenty of unrelated words (`..._LOW_lightning_edition`), and a false negative is what this whole change exists to avoid. An explicit `...noise` marker anywhere in the name still outranks a bare token found earlier. The review's suggested remedy — read the expert from safetensors metadata, as the GGUF probe reads `general.name` — does not work: sampling the Kijai catalogue, 0 of 8 files carry any `__metadata__` at all. Two mislabel cases from the review are now handled structurally instead: TI2V-5B pins `expert='none'` because it is single-transformer, so `...5B-lowVRAM` can no longer leak into the low-noise expert picker. Also in this commit: - Wan Animate is now refused with an accurate reason. It is 36-channel with undecorated block weights and no VACE blocks, so nothing else turned it away, and `strict=False` would silently drop its 127 face-adapter/motion-encoder keys. Checked before the Wan 2.1 markers, since Animate carries `img_emb` too and was otherwise reported as a Wan 2.1 I2V model. - Two frontend format gates that the first commit missed: `MainModelPicker` still hid only GGUF low-noise experts from the primary dropdown, and the readiness pre-flight skipped the VAE/encoder check for checkpoint mains, so Invoke was enabled for a graph that could only fail in the loader. Both now go through one shared `isWanSingleFileMainModelConfig` guard so they cannot drift again. - `Main_GGUF_Wan_Config` gained the LoRA-vs-transformer check for symmetry. - The Wan LoRA probe now shares `_detect_wan_expert` instead of carrying a stale copy of the old heuristic under a comment claiming the two matched. Side effect worth a maintainer's eye: an expert-specific LoRA named with a bare HIGH/LOW is now tagged rather than left untagged, so it is applied to that expert alone instead of to both. Co-Authored-By: Claude Opus 5 (1M context) --- .../backend/model_manager/configs/lora.py | 21 ++- .../backend/model_manager/configs/main.py | 158 ++++++++++++++---- .../web/src/features/queue/store/readiness.ts | 11 +- .../MainModelPicker.tsx | 28 ++-- .../frontend/web/src/services/api/types.ts | 23 ++- .../configs/test_wan_checkpoint_config.py | 76 +++++++-- 6 files changed, 229 insertions(+), 88 deletions(-) diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py index fbf7cfa8b6c..e12f690e1b2 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,15 +1142,19 @@ 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. + # 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. + # + # 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: - 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" + detected = _detect_wan_expert(mod.path.stem) + if detected != "none": + instance.expert = detected # Auto-detect the model-family variant from inner_dim in the state # dict. The override field skips this if the user has set it. diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 01bd1f92fc6..8a38abe36ad 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -1924,52 +1924,130 @@ def _find_wan_2_1_marker(state_dict: dict[str | int, Any]) -> str | None: 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" - if any(isinstance(key, str) and "vace_blocks." in key for key in keys): - # Not strictly a 2.1 marker — Wan 2.2 VACE variants exist — but the effect is - # the same: this loader builds a plain WanTransformer3DModel, which has no - # VACE control branch, so the vace_blocks would be dropped as unexpected keys - # and the model would quietly ignore its control input. Refuse instead. + return 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)] + + if any("face_adapter." in key or "motion_encoder." in key for key in keys): + return ( + "state dict has face-adapter / motion-encoder branches, which belong to Wan Animate; " + "character animation and replacement are not supported yet" + ) + + if any("vace_blocks." in key for key in keys): 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( + { + "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 as some spelling of - ``high_noise`` / ``low_noise``: hyphenated, underscored, spaced, concatenated - (``highnoise``), camel-cased (``HighNoise``), or occasionally reversed - (``noise_high``). The name is split at camelCase boundaries and then on runs of - non-alphanumerics, and a match requires ``high``/``low`` to sit *next to* a - ``noise`` token — or to be fused with it into one token. - - The "noise" requirement is deliberate. A bare ``high``/``low`` token is far too - common in other roles — ``lowVRAM``, ``low-cfg``, ``lowSteps``, ``highRes``, - ``HighQuality`` — and guessing wrong is worse than not guessing. An unlabelled - A14B model ('none') raises a clear error in the Wan model loader telling the - user to pair or rename it; a *mislabelled* one passes the pair check, gets - silently swapped into the wrong slot, and quietly degrades the output. - - Token matching also stops the marker firing on a substring, so ``slow_noise`` - and ``flownoise`` are correctly left alone. - - Returns 'none' when no marker is present (single-expert model such as TI2V-5B, - or an untagged filename). + Two conventions dominate, and both have to work — the expert is not recoverable + from the weights, and single-file releases carry no metadata declaring it + (sampled across the Kijai catalogue: no ``__metadata__`` at all): + + * ``high_noise`` / ``low_noise`` and its spellings — hyphenated, underscored, + spaced, fused (``highnoise``), 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. + + An explicit ``noise`` marker always wins over a bare token found earlier in the + name, which settles names carrying both (``..._low_high_noise_...``). + + A bare token is ignored when a neighbour marks it as an adjective about + something else (``lowVRAM``, ``low-cfg``, ``highRes``) — see + ``_WAN_EXPERT_DISQUALIFIERS``. TI2V-5B is handled structurally by the callers, + which force 'none' because the model is single-transformer, so the common + ``...5B-lowVRAM`` style of name can't reach this at all. + + Matching is per token, so a marker can't fire on a substring: ``slow_noise``, + ``flownoise`` and ``highway`` are all left alone. + + Returns 'none' for an untagged filename. """ 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] - for marker, expert in (("high", "high"), ("low", "low")): - if f"{marker}noise" in tokens or f"noise{marker}" in tokens: - return expert # type: ignore[return-value] - for index, token in enumerate(tokens): + bare: Literal["high", "low", "none"] = "none" + for index, token in enumerate(tokens): + for marker in ("high", "low"): + if token in (f"{marker}noise", f"noise{marker}"): + return marker # type: ignore[return-value] if token != marker: continue neighbours = tokens[max(index - 1, 0) : index] + tokens[index + 1 : index + 2] if "noise" in neighbours: - return expert # type: ignore[return-value] - return "none" + return marker # type: ignore[return-value] + # Bare token: remember it, but keep scanning — an explicit "...noise" + # marker later in the name is the better answer. + if bare == "none" and not _WAN_EXPERT_DISQUALIFIERS.intersection(neighbours): + bare = marker # type: ignore[assignment] + return bare + + +def _resolve_wan_expert( + mod: ModelOnDisk, override_fields: dict[str, Any], variant: WanVariantType +) -> Literal["high", "low", "none"]: + """Settle the MoE expert field, consuming any explicit override. + + 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. + """ + 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" + return _detect_wan_expert(mod.path.stem) class Main_GGUF_Wan_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): @@ -2000,6 +2078,14 @@ 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() @@ -2019,8 +2105,7 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - 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_expert(mod.path.stem) + expert = _resolve_wan_expert(mod, override_fields, variant) return cls(**override_fields, variant=variant, expert=expert) @@ -2072,6 +2157,12 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - "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 @@ -2092,8 +2183,7 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - if variant is None: raise NotAMatchError("could not determine Wan variant from state dict") - explicit_expert = override_fields.pop("expert", None) - expert = explicit_expert or _detect_wan_expert(mod.path.stem) + expert = _resolve_wan_expert(mod, override_fields, variant) return cls(**override_fields, variant=variant, expert=expert) diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 81751025c9d..aa9beb213ae 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -50,6 +50,7 @@ import { isExternalApiModelConfig, isSelfContainedSDNQFlux1Pipeline, isSelfContainedSDNQPipeline, + isWanSingleFileMainModelConfig, } from 'services/api/types'; import { $isConnected } from 'services/events/stores'; @@ -401,8 +402,9 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { } } - if (model?.base === 'wan' && model.format === 'gguf_quantized') { - // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must + if (model && isWanSingleFileMainModelConfig(model)) { + // Single-file Wan mains (GGUF or safetensors checkpoint) 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 @@ -1156,8 +1158,9 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } } - if (model?.base === 'wan' && model.format === 'gguf_quantized') { - // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must + if (model && isWanSingleFileMainModelConfig(model)) { + // Single-file Wan mains (GGUF or safetensors checkpoint) 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 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..4f3fecc5ac5 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx @@ -11,30 +11,24 @@ 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 AnyModelConfig, + type AnyModelConfigWithExternal, + isNonCommercialMainModelConfig, + isWanSingleFileLowNoiseMainModelConfig, +} 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. + // 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. 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.filter((c) => !isWanSingleFileLowNoiseMainModelConfig(c as AnyModelConfig)), [allModelConfigs] ); const selectedModelConfig = useSelectedModelConfig(); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 04c222e1403..607dccbb3a1 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -638,19 +638,28 @@ export const isWanDiffusersMainModelConfig = (config: AnyModelConfig): config is return config.type === 'main' && config.base === 'wan' && config.format === 'diffusers'; }; -/** Wan single-file main models (GGUF or safetensors checkpoint) marked as the - * low-noise expert — the second half of the A14B MoE pair. Suitable for the - * Transformer (Low Noise) picker. The two experts don't have to share a format; - * both load into the same transformer class. */ -export const isWanSingleFileLowNoiseMainModelConfig = (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. */ +export const WAN_SINGLE_FILE_FORMATS = ['gguf_quantized', 'checkpoint'] as const; + +export const isWanSingleFileMainModelConfig = (config: { base?: string; type?: string; format?: string }): boolean => { return ( config.type === 'main' && config.base === 'wan' && - (config.format === 'gguf_quantized' || config.format === 'checkpoint') && - config.expert === 'low' + WAN_SINGLE_FILE_FORMATS.includes(config.format as (typeof WAN_SINGLE_FILE_FORMATS)[number]) ); }; +/** Wan single-file main models marked as the low-noise expert — the second half of + * the A14B MoE pair. Suitable for the Transformer (Low Noise) picker, and filtered + * out of the primary main dropdown. The two experts don't have to share a format; + * both load into the same transformer class. */ +export const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => { + return isWanSingleFileMainModelConfig(config) && 'expert' in config && config.expert === 'low'; +}; + export const isWanLoRAModelConfig = (config: AnyModelConfig): config is WanLoRAModelConfig => { return config.type === 'lora' && config.base === 'wan'; }; diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index 41406f3f00b..b4969371b29 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -165,6 +165,23 @@ 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_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.""" @@ -298,47 +315,70 @@ def test_identified_record_has_a_registered_loader(self, tmp_path: Path) -> None class TestExpertFilenameHeuristic: - """A wrong expert label is worse than no label. - - 'none' on an A14B model produces a clear error from the Wan model loader. A - *mislabelled* one satisfies the {high, low} pair check, gets swapped into the - wrong slot, and silently runs the same expert for both denoise phases. So the - heuristic only fires when 'noise' is actually part of the marker. + """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", [ - # Pre-existing spellings must keep working. + # --- 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-ti2v-5b-Q4_K_M", "none"), - ("wan-A14B-flagship", "none"), - # Separators the old substring check missed, plus reversed order. ("Wan2.2 A14B high noise", "high"), ("wan22.low.noise.v3", "low"), ("wan22_noise_high_expert", "high"), - # A bare high/low token is NOT a marker — it almost always describes - # something else (VRAM, CFG, step count, resolution, quality). + ("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"), + # --- 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_TI2V_5B_lowVRAM", "none"), - ("Wan2.2-TI2V-5B-Turbo-lowSteps", "none"), - ("Wan2.2-TI2V-5B-HighQuality", "none"), - ("Wan2.2-A14B-SmoothMix-T2V-HIGH", "none"), - # Token matching, so the marker can't fire on a substring. The last two - # were mismatched by the original substring-based heuristic as well. + ("Wan2.2_A14B_lowVRAM", "none"), + ("Wan2.2-A14B-HighQuality", "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 earlier --- + ("wan2.2_t2v_low_high_noise_14B_fp16", "high"), ], ) def test_filename_heuristic(self, name: str, expected: str) -> None: assert _detect_wan_expert(name) == expected + + @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" From 94cdbbcb4fbf15d4ace3c641d773979c080d1875 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 13 Aug 2026 22:55:10 -0400 Subject: [PATCH 04/15] fix(model manager): correct the expert heuristic and refuse more Wan variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round three. Two independent fresh-context reviewers, one on the identification layer and one on everything else. Findings below; the corpus figures are theirs, built from ~4,700 unique HuggingFace filenames, not from this branch's tests. **My "no metadata" claim was wrong, and the previous commit message repeated it.** Every Wan 2.2 safetensors in Kijai/WanVideo_comfy_fp8_scaled carries `__metadata__["model_type"]` naming the expert. My check missed it because it range-fetched a fixed 128 KB window while those headers are ~146 KB, and silently treated a truncated read as "no metadata". Comfy-Org's files genuinely have none, so the filename heuristic is still required — but metadata is now consulted as a fallback when the name yields nothing. Filename first, deliberately: renaming is the only lever a user has to correct a mis-detection, and letting an embedded `model_type` outrank it would take that away. Expert heuristic, three real defects: - The fused-marker test was an equality check, so `WAN2.2t2vLOWNOISEFP8` (a real 14 GB transformer) tokenized to `lownoisefp8` and matched nothing. It is now anchored with startswith/endswith — which is still not a bare substring test, so `slownoise` stays unmatched. - First-bare-marker-wins was backwards. Real names put descriptors first and the expert tag last, so `Extream Low Angle HIGH` resolved to 'low' on a HIGH file. Last marker wins now. - Files serving *both* experts (`... I2V HIGH+LOW ...`, seven real examples, all physically 2x the size of their single-expert siblings) were tagged with one of them. A surviving high/low conflict now yields 'none' — which for a LoRA means "apply to both", the correct answer. Also `angle` joins the disqualifier list, and disqualifiers now only count when they *follow* the marker: "low angle" is a camera angle, but "Angle HIGH" is the high-noise expert of a camera-angle LoRA. Two more unsupported Wan variants refused, both verified against real headers: - **S2V** (`audio_injector`, `casual_audio_encoder`, `cond_encoder`, `frame_packer` — 165 of 1260 keys) was importing as plain T2V-A14B. - **Fun-Control-Camera** (`control_adapter`) was importing as I2V-A14B. This one was the dangerous case: it ships as a correctly tagged high/low pair, so the expert-pairing check passed and it would have rendered as an ordinary I2V while silently ignoring every camera input. And a generic backstop, since enumerating families by name will always lag: both Wan loaders now refuse any state dict with keys the transformer has nowhere to put, instead of letting `strict=False` discard them. Every supported release checked yields zero unexpected keys, so there is no benign case being blocked. Frontend: `modelSelected.ts` still auto-filled the VAE/encoder slots for GGUF mains only, so selecting a checkpoint main populated nothing and immediately blocked Invoke — the readiness check demanded components that nothing offered to fill. It now shares `isWanSingleFileMainModelConfig` with readiness, and the two carry comments pointing at each other. Adds the first Wan cases to readiness.test.ts, which had none. Co-Authored-By: Claude Opus 5 (1M context) --- .../backend/model_manager/configs/main.py | 115 +++++++++++---- .../model_manager/load/model_loaders/wan.py | 40 +++++- .../listeners/modelSelected.ts | 20 ++- .../features/queue/store/readiness.test.ts | 132 ++++++++++++++++++ .../web/src/features/queue/store/readiness.ts | 12 +- .../configs/test_wan_checkpoint_config.py | 74 ++++++++++ .../load/test_wan_checkpoint_loader.py | 21 +++ 7 files changed, 374 insertions(+), 40 deletions(-) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 8a38abe36ad..d63fc062f7f 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -1942,13 +1942,32 @@ def _find_unsupported_wan_variant_marker(state_dict: dict[str | int, Any]) -> st """ keys = [key for key in state_dict.keys() if isinstance(key, str)] - if any("face_adapter." in key or "motion_encoder." in key for key in keys): + 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 any("vace_blocks." in key for key in keys): + 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 @@ -1963,6 +1982,7 @@ def _find_unsupported_wan_variant_marker(state_dict: dict[str | int, Any]) -> st # a checkpoint whose expert can only be recovered from its name. _WAN_EXPERT_DISQUALIFIERS = frozenset( { + "angle", "cfg", "guidance", "vram", @@ -1985,69 +2005,116 @@ def _find_unsupported_wan_variant_marker(state_dict: dict[str | int, Any]) -> st def _detect_wan_expert(filename: str) -> Literal["high", "low", "none"]: """Filename heuristic for the A14B dual-expert MoE. - Two conventions dominate, and both have to work — the expert is not recoverable - from the weights, and single-file releases carry no metadata declaring it - (sampled across the Kijai catalogue: no ``__metadata__`` at all): + 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``), camel-cased (``HighNoise``), reversed - (``noise_high``). This is what Comfy-Org's repackaged repos use. + 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. - An explicit ``noise`` marker always wins over a bare token found earlier in the - name, which settles names carrying both (``..._low_high_noise_...``). + Some releases also declare the expert in file metadata; ``_resolve_wan_expert`` + consults that when the name yields nothing. + + Precedence, in order: + + 1. An explicit ``noise`` marker anywhere wins outright — it settles names + carrying both, such as ``..._low_high_noise_...``. + 2. Otherwise the **last** surviving bare marker wins. Last, not first, because + real names put descriptive words up front and the expert tag at the end + (``Extream Low Angle HIGH - Wan2.2 ...`` is the HIGH expert). + 3. If bare ``high`` *and* bare ``low`` both survive, the name is describing a + file that serves both (``... I2V HIGH+LOW ...``) or is simply ambiguous, so + return 'none' rather than guess. For a LoRA 'none' means "apply to both", + which is the right answer for those. A bare token is ignored when a neighbour marks it as an adjective about - something else (``lowVRAM``, ``low-cfg``, ``highRes``) — see + 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, so the common - ``...5B-lowVRAM`` style of name can't reach this at all. + which force 'none' because the model is single-transformer. - Matching is per token, so a marker can't fire on a substring: ``slow_noise``, - ``flownoise`` and ``highway`` are all left alone. + 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 = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", filename).lower() tokens = [token for token in re.split(r"[^a-z0-9]+", name) if token] - bare: Literal["high", "low", "none"] = "none" + bare: list[str] = [] for index, token in enumerate(tokens): for marker in ("high", "low"): - if token in (f"{marker}noise", f"noise{marker}"): + # Fused with 'noise'. Anchored: startswith catches `lownoisefp8`, and + # only the reversed form may match at the end — `endswith("lownoise")` + # would wrongly claim `slownoise`. + if token.startswith(f"{marker}noise") or token.endswith(f"noise{marker}"): return marker # type: ignore[return-value] if token != marker: continue neighbours = tokens[max(index - 1, 0) : index] + tokens[index + 1 : index + 2] if "noise" in neighbours: - return marker # type: ignore[return-value] - # Bare token: remember it, but keep scanning — an explicit "...noise" - # marker later in the name is the better answer. - if bare == "none" and not _WAN_EXPERT_DISQUALIFIERS.intersection(neighbours): - bare = marker # type: ignore[assignment] - return bare + return marker + # Only the *following* token 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. + following = tokens[index + 1 : index + 2] + if not _WAN_EXPERT_DISQUALIFIERS.intersection(following): + bare.append(marker) + + if len(set(bare)) == 1: + return bare[-1] # type: ignore[return-value] + return "none" def _resolve_wan_expert( mod: ModelOnDisk, override_fields: dict[str, Any], variant: WanVariantType ) -> Literal["high", "low", "none"]: - """Settle the MoE expert field, consuming any explicit override. + """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" - return _detect_wan_expert(mod.path.stem) + + 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" class Main_GGUF_Wan_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py index 4b27c62bebb..11749044ab9 100644 --- a/invokeai/backend/model_manager/load/model_loaders/wan.py +++ b/invokeai/backend/model_manager/load/model_loaders/wan.py @@ -186,6 +186,38 @@ def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict: return unwrapped +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. Every supported Wan 2.2 + release checked against this loader yields zero unexpected keys, so there is no + benign case to allow through. + """ + 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. @@ -330,8 +362,7 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: 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 @@ -414,10 +445,7 @@ def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: self._ram_cache.make_room(new_sd_size) incompatible_keys = model.load_state_dict(sd, strict=False, assign=True) - if incompatible_keys.missing_keys: - raise RuntimeError( - f"Wan checkpoint is missing model parameters: {sorted(incompatible_keys.missing_keys)[:10]}" - ) + _raise_for_incompatible_keys(incompatible_keys, source="Wan checkpoint") return model 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..06fc047fc5a 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 @@ -76,7 +76,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'; @@ -621,16 +626,19 @@ 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. + // switches like Diffusers Wan → single-file Wan) so the user doesn't have to dig + // into Advanced when picking a single-file main. Only sets fields that are + // currently empty, and only for single-file mains (GGUF or safetensors + // checkpoint) — Diffusers mains carry everything themselves. 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) { + // 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 && isWanSingleFileMainModelConfig(newModelConfig)) { 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 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..9493b82b0a4 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts @@ -689,3 +689,135 @@ 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; + +const buildWanTabArg = (overrides: { + model?: MainModelConfig | null; + wanVaeModel?: unknown; + wanT5EncoderModel?: unknown; + wanComponentSource?: unknown; +}) => ({ + isConnected: true, + model: overrides.model ?? wanCheckpointModel, + params: { + ...baseParams, + wanVaeModel: overrides.wanVaeModel ?? null, + wanT5EncoderModel: overrides.wanT5EncoderModel ?? null, + wanComponentSource: overrides.wanComponentSource ?? null, + } as unknown as ParamsState, + refImages: baseRefImages, + loras: [], + dynamicPrompts: baseDynamicPrompts, + hasFlux2DiffusersVaeSource: false, + hasFlux2DiffusersQwen3Source: false, + hasFlux2DevDiffusersSource: false, +}); + +const hasWanComponentReason = (reasons: { content: string }[]) => + reasons.some((r) => r.content.includes('noWanComponentSourceSelected')); + +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({ + ...buildWanTabArg({ model }), + 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); + 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 aa9beb213ae..11bd4502bc4 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -404,8 +404,10 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { if (model && isWanSingleFileMainModelConfig(model)) { // Single-file Wan mains (GGUF or safetensors checkpoint) carry only the - // transformer; VAE + UMT5-XXL encoder must - // come from either standalone models or the Component Source (Diffusers). + // transformer; VAE + UMT5-XXL encoder must come from either standalone models + // or the Component Source (Diffusers). Keep this in step with the auto-fill in + // modelSelected.ts: if that doesn't offer to populate the slots for a format + // this demands them for, selecting the model just blocks Invoke. // 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). @@ -1160,8 +1162,10 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { if (model && isWanSingleFileMainModelConfig(model)) { // Single-file Wan mains (GGUF or safetensors checkpoint) carry only the - // transformer; VAE + UMT5-XXL encoder must - // come from either standalone models or the Component Source (Diffusers). + // transformer; VAE + UMT5-XXL encoder must come from either standalone models + // or the Component Source (Diffusers). Keep this in step with the auto-fill in + // modelSelected.ts: if that doesn't offer to populate the slots for a format + // this demands them for, selecting the model just blocks Invoke. // 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). diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index b4969371b29..2ceb7930369 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -19,6 +19,7 @@ _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 @@ -182,6 +183,29 @@ def test_animate(self, tmp_path: Path) -> None: 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.""" @@ -350,6 +374,8 @@ class TestExpertFilenameHeuristic: ("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"), @@ -357,6 +383,18 @@ class TestExpertFilenameHeuristic: ("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"), @@ -374,6 +412,42 @@ class TestExpertFilenameHeuristic: 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 diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py index 2d4011b04e9..dbfe97e4027 100644 --- a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -185,6 +185,27 @@ def test_scale_bookkeeping_never_reaches_the_model(self, tmp_path: Path) -> None # ...without eating scale_shift_table, which is a real Wan parameter. assert "scale_shift_table" in handed_over + 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_missing_parameter_is_reported(self, tmp_path: Path) -> None: sd = _tiny_model().state_dict() del sd["blocks.1.attn1.to_q.weight"] From 59d74517b9a57b4630a26be689d7159f41dbd196 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 14 Aug 2026 20:28:22 -0400 Subject: [PATCH 05/15] fix(model manager): stop refusing all-in-one Wan files; tighten variant + guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round four. Three fresh-context reviewers on split scopes (identification, loaders, frontend). Five blockers, each fix mutation-verified by reverting it and confirming its test fails. 1. The unexpected-key backstop was a regression on the GGUF path. Before this branch, `WanGGUFCheckpointModel` raised only on missing keys and let `strict=False` drop the rest; now any extra key raises. 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 documents exactly that, and befox/WAN2.2-14B-Rapid-AllInOne- GGUF mirrors ~110 conversions of it. Those installed and generated fine on main and died at the first Invoke here, blaming Animate/S2V/Fun-Camera. 2. Worse, the backstop contradicted this branch's own probe. The docstring on `_has_wan_transformer_block_weights` says main models with merged-in LoRA weights sometimes retain those keys, and uses a positive structural test precisely so they aren't turned away — and then the loader turned them away. Both are fixed by classifying extra keys instead of blanket-refusing them: bundled components and merged-LoRA residue are dropped with a log line, anything else still raises. The generic backstop's purpose is intact — an unenumerated conditioning branch still fails loudly, with a test to prove the allowlist didn't switch it off. 3. `_detect_wan_variant_from_state_dict` mapped the variant from in_channels alone while the transformer width sat unused in the same tuple (and is already read two functions away by the Wan 2.1 marker). The wider Wan family reuses these channel counts at other widths, so a 5120-wide 48-channel derivative was labelled TI2V-5B — which pins expert='none', selects TI2V-5B default settings and hides the low-noise partner picker. A14B is uniquely 5120-wide and TI2V-5B uniquely 3072-wide; require both to agree so an unsupported derivative falls through to unidentified rather than mislabelled. 4. readiness.ts claimed the low-noise A14B partner was optional. It is optional only for expert='high'; 'low' and 'none' are a hard ValueError in the loader, and 'none' is routine because the tag is a filename heuristic with no UI to correct it. Invoke was enabled for a graph that could only fail. The pre-flight now checks it, and the two copies of the Wan block are one shared helper so they cannot drift. Relaxable once the loader takes pairing from the wiring — see #9505, which the comment points at. 5. `isWanSingleFileMainModelConfig` took an all-optional structural type and returned plain boolean. That is a weak type: TypeScript accepts any object sharing one property, so passing `ModelIdentifierField` (base + type, no format) compiled clean and silently returned false, disabling every gate below it. The bare `format === 'gguf_quantized'` it replaced was at least a compile error there. Now takes AnyModelConfigWithExternal and returns a real type predicate — which immediately surfaced a latent mismatch at both readiness call sites. Also: the invoke-blocked string still said "GGUF Wan 2.2 models" while being shown for checkpoints, and WAN_SINGLE_FILE_FORMATS was an export with no external consumer. Co-Authored-By: Claude Opus 5 (1M context) --- .../backend/model_manager/configs/main.py | 17 ++- .../model_manager/load/model_loaders/wan.py | 76 ++++++++-- invokeai/frontend/web/public/locales/en.json | 3 +- .../features/queue/store/readiness.test.ts | 131 +++++++++++++++--- .../web/src/features/queue/store/readiness.ts | 64 +++++---- .../frontend/web/src/services/api/types.ts | 13 +- .../configs/test_wan_checkpoint_config.py | 23 +++ .../load/test_wan_checkpoint_loader.py | 51 +++++++ .../model_manager/load/test_wan_loader.py | 54 ++++++++ 9 files changed, 369 insertions(+), 63 deletions(-) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index d63fc062f7f..153a9aa7aef 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -1855,12 +1855,21 @@ def _detect_wan_variant_from_state_dict(state_dict: dict[str | int, Any]) -> Wan shape = _wan_patch_embedding_shape(state_dict) if shape is None or len(shape) < 2: return None - in_channels = shape[1] - if in_channels == 16: + 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: + if in_channels == 36 and inner_dim == 5120: return WanVariantType.I2V_A14B - if in_channels == 48: + if in_channels == 48 and inner_dim == 3072: return WanVariantType.TI2V_5B return None diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py index 11749044ab9..591d1d0c03c 100644 --- a/invokeai/backend/model_manager/load/model_loaders/wan.py +++ b/invokeai/backend/model_manager/load/model_loaders/wan.py @@ -186,7 +186,45 @@ def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict: return unwrapped -def _raise_for_incompatible_keys(incompatible_keys: Any, source: str) -> None: +# 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", + } +) + +# Substrings 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. +_MERGED_LORA_MARKERS = ("lora_a", "lora_b", "lora_down", "lora_up", "lora_magnitude", "dora_scale", ".alpha") + + +def _is_benign_extra_key(key: str) -> bool: + """True if an unexpected key is packaging rather than an unsupported branch.""" + if key.split(".")[0] in _BENIGN_EXTRA_MODULES: + return True + lowered = key.lower() + return any(marker in lowered for marker in _MERGED_LORA_MARKERS) + + +def _raise_for_incompatible_keys(incompatible_keys: Any, source: str, logger: Any) -> 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 @@ -199,19 +237,33 @@ def _raise_for_incompatible_keys(incompatible_keys: Any, source: str) -> None: ``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. Every supported Wan 2.2 - release checked against this loader yields zero unexpected keys, so there is no - benign case to allow through. + yet produces an error instead of quietly degraded output. + + Not every extra key is a conditioning branch, though, so the two categories in + ``_is_benign_extra_key`` are dropped with a log line instead of raising: bundled + VAE/text-encoder weights (the "all-in-one" packaging convention) and merged-LoRA + residue (which the main-model probe explicitly accepts). Refusing those would + reject files that load and generate correctly today. """ 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: + benign = [key for key in unexpected if _is_benign_extra_key(key)] + unsupported = [key for key in unexpected if not _is_benign_extra_key(key)] + + if benign: + modules = sorted({key.split(".")[0] for key in benign}) + logger.info( + f"{source}: ignored {len(benign)} bundled/merged weights not part of the transformer " + f"({', '.join(modules[:8])}). The VAE and text encoder come from the separately-wired models." + ) + + if unsupported: # Report the distinct top-level module names rather than hundreds of keys. - modules = sorted({key.split(".")[0] for key in unexpected}) + modules = sorted({key.split(".")[0] for key in unsupported}) raise RuntimeError( - f"{source} has {len(unexpected)} weights that WanTransformer3DModel has nowhere to put " + f"{source} has {len(unsupported)} 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." @@ -328,6 +380,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) @@ -362,7 +416,11 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: model = WanTransformer3DModel(**model_config) incompatible_keys = model.load_state_dict(sd, strict=False, assign=True) - _raise_for_incompatible_keys(incompatible_keys, source="GGUF state dict") + _raise_for_incompatible_keys( + incompatible_keys, + source="GGUF state dict", + logger=InvokeAILogger.get_logger(self.__class__.__name__), + ) return model @@ -445,7 +503,7 @@ def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: 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") + _raise_for_incompatible_keys(incompatible_keys, source="Wan checkpoint", logger=logger) return model diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 3ecdb66818c..2b57ce1a68c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1792,7 +1792,8 @@ "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", + "noWanLowNoiseExpertSelected": "Wan 2.2 A14B requires the high-noise expert as the main model. Wire the matching expert to Transformer (Low Noise), or select the high-noise file.", "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/features/queue/store/readiness.test.ts b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts index 9493b82b0a4..6d098821f40 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts @@ -730,11 +730,45 @@ const wanDiffusersModel = { 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; }) => ({ isConnected: true, model: overrides.model ?? wanCheckpointModel, @@ -743,6 +777,7 @@ const buildWanTabArg = (overrides: { wanVaeModel: overrides.wanVaeModel ?? null, wanT5EncoderModel: overrides.wanT5EncoderModel ?? null, wanComponentSource: overrides.wanComponentSource ?? null, + wanTransformerLowNoise: overrides.wanTransformerLowNoise ?? null, } as unknown as ParamsState, refImages: baseRefImages, loras: [], @@ -752,9 +787,85 @@ const buildWanTabArg = (overrides: { hasFlux2DevDiffusersSource: false, }); +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')); +const hasWanExpertReason = (reasons: { content: string }[]) => + reasons.some((r) => r.content.includes('noWanLowNoiseExpertSelected')); + +// The A14B expert pre-flight. `WanModelLoaderInvocation` raises a hard ValueError for +// an unpaired A14B main that isn't the high-noise expert, so readiness has to block it +// rather than let the user hit it at generation time. Only expert='high' degrades +// gracefully (high expert runs the whole schedule, with a warning). +describe('Wan 2.2 A14B expert pre-flight', () => { + const withComponents = { wanComponentSource: { key: 'src' } }; + + it.each([ + ['untagged (expert=none)', wanUntaggedA14bModel], + ['the low-noise expert', wanLowExpertModel], + ])('blocks an unpaired A14B main that is %s', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model, ...withComponents })); + expect(hasWanExpertReason(reasons)).toBe(true); + }); + + it.each([ + ['untagged (expert=none)', wanUntaggedA14bModel], + ['the low-noise expert', wanLowExpertModel], + ])('allows %s once a low-noise partner is wired', (_label, model) => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model, ...withComponents, wanTransformerLowNoise: { key: 'partner' } }) + ); + expect(hasWanExpertReason(reasons)).toBe(false); + }); + + it('allows an unpaired A14B high-noise expert — it degrades with a warning, not an error', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanCheckpointModel, ...withComponents }) + ); + expect(hasWanExpertReason(reasons)).toBe(false); + }); + + it('does not apply to TI2V-5B, which is single-transformer', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model: wanTi2v5bModel, ...withComponents })); + expect(hasWanExpertReason(reasons)).toBe(false); + }); + + it('does not apply to a Diffusers main, which carries both experts', () => { + const reasons = getReasonsWhyCannotEnqueueGenerateTab( + buildWanTabArg({ model: wanDiffusersModel, ...withComponents }) + ); + expect(hasWanExpertReason(reasons)).toBe(false); + }); + + it('also runs on the canvas tab', () => { + const reasons = getReasonsWhyCannotEnqueueCanvasTab( + buildWanCanvasArg({ model: wanUntaggedA14bModel, ...withComponents }) + ); + expect(hasWanExpertReason(reasons)).toBe(true); + }); +}); + describe('Wan 2.2 readiness checks – generate tab', () => { it.each([ ['GGUF', wanGgufModel], @@ -799,25 +910,7 @@ describe('Wan 2.2 readiness checks – canvas tab', () => { ['GGUF', wanGgufModel], ['single-file checkpoint', wanCheckpointModel], ])('errors when a %s main has no VAE or encoder source', (_label, model) => { - const reasons = getReasonsWhyCannotEnqueueCanvasTab({ - ...buildWanTabArg({ model }), - 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 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 11bd4502bc4..ae0b8a8274e 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -251,6 +251,42 @@ export const useReadinessWatcher = () => { const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); +const WAN_A14B_VARIANTS = ['t2v_a14b', 'i2v_a14b']; + +/** Pre-flight for single-file 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. */ +const pushWanSingleFileReasons = (model: MainOrExternalModelConfig, params: ParamsState, reasons: Reason[]): void => { + // Single-file Wan mains (GGUF or safetensors checkpoint) 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') }); + } + + // The A14B MoE pair. An unpaired A14B is only accepted when it is the *high*-noise + // expert — that case degrades to "high expert runs the whole schedule" with a warning. + // `expert` of 'low' or 'none' with nothing wired to the low-noise slot is a hard + // ValueError in the loader, so it has to block here rather than fail at generation + // time. ('none' is common: the tag is a filename heuristic, and there is no UI to + // correct it.) Revisit if the loader adopts wiring-first pairing — see #9505, which + // would make 'none' resolvable from the wiring instead of an error. + const variant = 'variant' in model ? model.variant : undefined; + const expert = 'expert' in model ? model.expert : undefined; + if ( + typeof variant === 'string' && + WAN_A14B_VARIANTS.includes(variant) && + expert !== 'high' && + !params.wanTransformerLowNoise + ) { + reasons.push({ content: i18n.t('parameters.invoke.noWanLowNoiseExpertSelected') }); + } +}; + export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainOrExternalModelConfig | null | undefined; @@ -403,19 +439,7 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { } if (model && isWanSingleFileMainModelConfig(model)) { - // Single-file Wan mains (GGUF or safetensors checkpoint) carry only the - // transformer; VAE + UMT5-XXL encoder must come from either standalone models - // or the Component Source (Diffusers). Keep this in step with the auto-fill in - // modelSelected.ts: if that doesn't offer to populate the slots for a format - // this demands them for, selecting the model just blocks Invoke. - // 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') }); - } + pushWanSingleFileReasons(model, params, reasons); } if (model?.base === 'z-image') { @@ -1161,19 +1185,7 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } if (model && isWanSingleFileMainModelConfig(model)) { - // Single-file Wan mains (GGUF or safetensors checkpoint) carry only the - // transformer; VAE + UMT5-XXL encoder must come from either standalone models - // or the Component Source (Diffusers). Keep this in step with the auto-fill in - // modelSelected.ts: if that doesn't offer to populate the slots for a format - // this demands them for, selecting the model just blocks Invoke. - // 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') }); - } + pushWanSingleFileReasons(model, params, reasons); } if (model?.base === 'z-image') { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 607dccbb3a1..9acb66056b9 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -642,13 +642,18 @@ export const isWanDiffusersMainModelConfig = (config: AnyModelConfig): config is * 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. */ -export const WAN_SINGLE_FILE_FORMATS = ['gguf_quantized', 'checkpoint'] as const; - -export const isWanSingleFileMainModelConfig = (config: { base?: string; type?: string; format?: string }): boolean => { +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' && - WAN_SINGLE_FILE_FORMATS.includes(config.format as (typeof WAN_SINGLE_FILE_FORMATS)[number]) + (WAN_SINGLE_FILE_FORMATS as readonly string[]).includes(config.format) ); }; diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index 2ceb7930369..b7121b1ad43 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -247,6 +247,29 @@ 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: diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py index dbfe97e4027..c72f16543c8 100644 --- a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -206,6 +206,57 @@ def test_extra_modules_are_refused_not_dropped(self, tmp_path: Path) -> None: 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 = _load(path) + # The transformer itself still loaded, and none of the bundled weights reached it. + assert not hasattr(model, "vae") + assert not hasattr(model, "text_encoders") + + 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 + + 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"] diff --git a/tests/backend/model_manager/load/test_wan_loader.py b/tests/backend/model_manager/load/test_wan_loader.py index 691a3850561..1d2bd8c2019 100644 --- a/tests/backend/model_manager/load/test_wan_loader.py +++ b/tests/backend/model_manager/load/test_wan_loader.py @@ -182,6 +182,60 @@ def test_plain_torch_tensor_passes_through(self): assert out["plain"] is plain +def _run_gguf_loader_with_unexpected_keys(unexpected: list[str]) -> None: + """Drive WanGGUFCheckpointModel to the incompatible-keys check with `unexpected`.""" + 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), + } + model = MagicMock() + model.load_state_dict.return_value = SimpleNamespace(missing_keys=[], unexpected_keys=unexpected) + 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) + + +def test_gguf_loader_accepts_all_in_one_bundled_components() -> 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. + + Before the unexpected-key backstop these loaded fine: `strict=False` dropped the + bundled copies and InvokeAI sourced the VAE and encoder from separately-wired + models. Refusing them is a regression on a path that already worked. + """ + _run_gguf_loader_with_unexpected_keys( + [ + "vae.decoder.conv_in.weight", + "text_encoders.umt5xxl.shared.weight", + "model_ema.patch_embedding.weight", + ] + ) + + +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_with_unexpected_keys(["vae.decoder.conv_in.weight", "audio_injector.0.proj.weight"]) + + def test_gguf_loader_rejects_missing_model_parameter() -> None: state_dict = { "patch_embedding.weight": torch.zeros(128, 16, 1, 2, 2), From 4c08eba7e01d8c1800c55616889e80beba4be127 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 14 Aug 2026 20:36:20 -0400 Subject: [PATCH 06/15] fix(model manager): read a both-experts filename as neither, in both spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `low_high_noise` names one file holding both A14B experts — moriqqe/Mabrle_wan2.2_low_high_noise and Chromatraining/v1_FGO_nitocris_morgan_wan2.2_t2v_low_high_noise_14B_fp16 are real releases — but the heuristic returned on the first marker adjacent to a `noise` token and reported it as the high-noise expert. The bare spelling of the same meaning (`...HIGH-LOW`) already returned 'none', so the two disagreed. A `noise` token now qualifies the whole run of adjacent markers rather than the one it happens to touch, and the explicit and bare tiers are reconciled the same way: one distinct marker wins, both means the file serves both, so 'none'. Two more from the same pass: - A disqualifier following the run now outranks an adjacent `noise`, which was previously short-circuited: `noise_LOW_VRAM` is describing VRAM. - The docstring advertised "the last surviving bare marker wins". That rule was unreachable — the return was guarded on the marker set being a singleton, so `bare[-1]` was always `bare[0]` — and the test offered as cover for it passes identically with either, because the `angle` disqualifier is what actually resolves that name. Documented what the code does instead. Co-Authored-By: Claude Opus 5 (1M context) --- .../backend/model_manager/configs/main.py | 99 +++++++++++++------ .../configs/test_wan_checkpoint_config.py | 21 +++- 2 files changed, 88 insertions(+), 32 deletions(-) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index 153a9aa7aef..fb318d525bb 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -2030,15 +2030,19 @@ def _detect_wan_expert(filename: str) -> Literal["high", "low", "none"]: Precedence, in order: - 1. An explicit ``noise`` marker anywhere wins outright — it settles names - carrying both, such as ``..._low_high_noise_...``. - 2. Otherwise the **last** surviving bare marker wins. Last, not first, because - real names put descriptive words up front and the expert tag at the end - (``Extream Low Angle HIGH - Wan2.2 ...`` is the HIGH expert). - 3. If bare ``high`` *and* bare ``low`` both survive, the name is describing a - file that serves both (``... I2V HIGH+LOW ...``) or is simply ambiguous, so - return 'none' rather than guess. For a LoRA 'none' means "apply to both", - which is the right answer for those. + 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 @@ -2054,28 +2058,63 @@ def _detect_wan_expert(filename: str) -> Literal["high", "low", "none"]: 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] = [] - for index, token in enumerate(tokens): - for marker in ("high", "low"): - # Fused with 'noise'. Anchored: startswith catches `lownoisefp8`, and - # only the reversed form may match at the end — `endswith("lownoise")` - # would wrongly claim `slownoise`. - if token.startswith(f"{marker}noise") or token.endswith(f"noise{marker}"): - return marker # type: ignore[return-value] - if token != marker: - continue - neighbours = tokens[max(index - 1, 0) : index] + tokens[index + 1 : index + 2] - if "noise" in neighbours: - return marker - # Only the *following* token 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. - following = tokens[index + 1 : index + 2] - if not _WAN_EXPERT_DISQUALIFIERS.intersection(following): - bare.append(marker) - - if len(set(bare)) == 1: - return bare[-1] # type: ignore[return-value] + + 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. + # + # Checked ahead of the 'noise' test below. A disqualifier sits after the run, + # so an adjacent 'noise' would have to precede it — `noise_LOW_VRAM` is + # describing VRAM, not the low-noise expert. ('low noise' can't trip this: + # 'noise' is not itself a disqualifier.) + if following in _WAN_EXPERT_DISQUALIFIERS: + 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" diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index b7121b1ad43..22a437800d3 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -428,8 +428,25 @@ class TestExpertFilenameHeuristic: ("wan22-flow-shift-tune", "none"), ("wan22-slow-noise-test", "none"), ("wan22_flownoise_v1", "none"), - # --- an explicit noise marker outranks a bare token found earlier --- - ("wan2.2_t2v_low_high_noise_14B_fp16", "high"), + # --- 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 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: From a62d475db52f1ef755ecac35c9a366e4b57dec75 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 14 Aug 2026 20:44:45 -0400 Subject: [PATCH 07/15] fix(ui): keep the Wan component slots in step with the selected variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Wan auto-fill could build a graph the loader is guaranteed to reject, and then readiness would pass it. Three separate ways: - It fell back to *any* installed Wan Diffusers model as the Component Source when no variant match existed — twelve lines below a comment explaining that a mismatched source "would silently load the wrong VAE and produce broken images". `_validate_component_source_vae` raises on exactly that. - The standalone VAE was first-match out of an unsorted entity adapter, on the stated grounds that "the standalone VAE / encoder configs don't carry variant info". The VAE configs do: `latent_channels` is 16 or 48, and `_validate_standalone_vae` compares against it. - Every write was gated on the slot being empty, and nothing anywhere clears these slots — paramsSlice carries all four across a base change and modelsLoaded has no Wan handler. So the variant matching only ever ran on a fresh slot. Selecting A14B then TI2V-5B left the 16-channel VAE wired, and because the loader prefers a standalone VAE over a Diffusers main's own, that stale slot also broke the next self-contained Diffusers model the user picked. Slots are now re-validated rather than only filled, the VAE is chosen by latent_channels, and there is no mismatched fallback — if nothing compatible is installed the slot is cleared, which reads as "pick one" instead of looking handled. Resolving the wired identifiers against the installed models also means a slot pointing at a deleted model is treated as empty. Extracted as a pure `getWanComponentUpdates`, following krea2ComponentSync, so it can be tested without driving the listener. Each of the three defects above is covered by a test that fails when the old behaviour is restored. Co-Authored-By: Claude Opus 5 (1M context) --- .../listeners/modelSelected.ts | 69 +++---- .../listeners/wanComponentSync.test.ts | 174 ++++++++++++++++++ .../listeners/wanComponentSync.ts | 109 +++++++++++ 3 files changed, 318 insertions(+), 34 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.test.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts 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 06fc047fc5a..eb459ec3863 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'; @@ -624,12 +625,15 @@ 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 + // 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. Only sets fields that are - // currently empty, and only for single-file mains (GGUF or safetensors - // checkpoint) — Diffusers mains carry everything themselves. + // 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 @@ -638,39 +642,36 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = // 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 && isWanSingleFileMainModelConfig(newModelConfig)) { + if (newModelConfig) { 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 + 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: wanT5EncoderModel, + 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))); } } } 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..1606fb5d870 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.test.ts @@ -0,0 +1,174 @@ +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, + 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('re-validates the standalone VAE for a Diffusers main too', () => { + // The loader prefers a wired standalone VAE over the Diffusers main's own, so a + // stale one breaks a model that is otherwise self-contained. + expect( + build({ + mainConfig: ti2v5bDiffusers, + isSingleFileMain: false, + selectedVae: vae16, + availableVaes: [vae16, vae48], + }) + ).toEqual({ vae: vae48 }); + }); + + it('does not wire a Component Source or encoder for a Diffusers main', () => { + expect( + build({ + mainConfig: a14bDiffusers, + isSingleFileMain: false, + availableVaes: [vae16], + availableDiffusers: [a14bDiffusers], + availableEncoders: [encoder], + }) + ).toEqual({ vae: vae16 }); + }); + + 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..3299931e125 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts @@ -0,0 +1,109 @@ +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; +}; + +const variantOf = (model: unknown): string | null => + model && typeof model === 'object' && 'variant' in model && typeof model.variant === 'string' ? model.variant : null; + +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; + availableVaes: AnyModelConfig[]; + availableDiffusers: AnyModelConfig[]; + availableEncoders: AnyModelConfig[]; +}): WanComponentUpdates => { + const { + mainConfig, + isSingleFileMain, + selectedVae, + selectedComponentSource, + selectedEncoder, + availableVaes, + availableDiffusers, + availableEncoders, + } = arg; + + const updates: WanComponentUpdates = {}; + + const isTi2v5b = variantOf(mainConfig) === 'ti2v_5b'; + const requiredLatentChannels = isTi2v5b ? 48 : 16; + + const vaeIsCompatible = (model: unknown) => + !!model && + typeof model === 'object' && + 'latent_channels' in model && + model.latent_channels === requiredLatentChannels; + + const sourceIsCompatible = (model: unknown) => (variantOf(model) === 'ti2v_5b') === isTi2v5b; + + // The standalone VAE outranks every other source in the loader — including a Diffusers + // main's own — so it is checked for any Wan main, not just single-file ones. + if (!vaeIsCompatible(selectedVae)) { + const vae = availableVaes.find(vaeIsCompatible); + // Clearing when nothing fits is deliberate: an empty slot reads as "pick one" in the + // UI, a stale one reads as already handled. + if (vae) { + updates.vae = vae; + } else if (selectedVae) { + updates.vae = null; + } + } + + // 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. + 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. + if (!selectedEncoder) { + const encoder = availableEncoders[0]; + if (encoder) { + updates.encoder = encoder; + } + } + } + + return updates; +}; From 638a910b9081ac1898b6bb773cc925dd894e6692 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 14 Aug 2026 20:53:04 -0400 Subject: [PATCH 08/15] fix(ui): route every primary-main selection through one offerable-models filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainModelPicker hid Wan low-noise experts from the main dropdown, but it was the only one of three places a primary main gets chosen. The other two were unfiltered: - InitialStateMainModelPicker (the launchpad "Select your model" picker) used the raw useMainModels() list. - modelsLoaded's handleMainModels auto-selects allMainModels[0] whenever the current selection becomes unavailable, filtered only by isNonRefinerMainModelConfig. A user whose only Wan single-file is the low-noise expert had it selected for them, silently. Either way the loader then refuses it — "An unpaired Wan A14B model must be the high-noise expert" — so the model was reachable but unusable. All three now share `isSelectableAsPrimaryMainModel`, which names the concept so a fourth entry point has something to reach for. Also pins the dequantization behaviour that `test_scale_bookkeeping_never_reaches_the_model` was quietly relying on. Its fixture pairs a bf16 weight with a scale, so the loader multiplies it, and the test asserted only that the scale *keys* were gone — it constructed a 4x-scaled weight and said nothing about it. Now asserted, with the reason there is no fp8 gate written down in `_dequantize_comfyui_fp8`: not every checkpoint using these keys stores fp8 weights, and skipping the multiply for those is as wrong as applying a stale scale. Co-Authored-By: Claude Opus 5 (1M context) --- .../model_loaders/comfyui_state_dict_utils.py | 7 +++ .../listeners/modelsLoaded.ts | 11 +++- .../listeners/modelsLoaded.wan.test.ts | 54 +++++++++++++++++++ .../MainModelPicker.tsx | 12 ++--- .../layouts/InitialStateMainModelPicker.tsx | 11 +++- .../frontend/web/src/services/api/types.ts | 15 +++++- .../load/test_wan_checkpoint_loader.py | 7 +++ 7 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.wan.test.ts 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 index cc37a0cf713..bc5d21837f0 100644 --- 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 @@ -46,6 +46,13 @@ def _dequantize_comfyui_fp8(sd: dict, compute_dtype: torch.dtype) -> int: - `.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)] 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..6aec582c60b 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 @@ -51,6 +51,7 @@ import { isQwen3VLEncoderModelConfig, isQwenImageVAEModelConfig, isRefinerMainModelModelConfig, + isSelectableAsPrimaryMainModel, isSpandrelImageToImageModelConfig, isT5EncoderModelConfigOrSubmodel, } from 'services/api/types'; @@ -132,9 +133,15 @@ 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)); + // isSelectableAsPrimaryMainModel: this auto-selects on the user's behalf whenever the + // current selection goes away, so it must not reach for a model the pickers hide and + // the loader refuses (a Wan low-noise expert). + const allMainModels = models + .filter(isNonRefinerMainModelConfig) + .filter(isSelectableAsPrimaryMainModel) + .sort((a) => (a.base === 'sdxl' ? -1 : 1)); const firstModel = allMainModels[0]; 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..f4b5c77e932 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.wan.test.ts @@ -0,0 +1,54 @@ +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 is refused by the loader as a primary main ("An unpaired Wan + * A14B model must be the high-noise expert"), so no path that chooses a primary main on + * the user's behalf may reach for one. This listener is the least visible of the three: + * it fires on every `getModelConfigs` fulfilment and swaps the selection silently. + */ + +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 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('does not auto-select a low-noise expert when it is the only Wan model installed', () => { + const dispatch = vi.fn(); + handleMainModels([wanLowExpert], makeState(), dispatch, log); + + // Nothing selectable, so the selection is left null rather than pointed at a model + // that cannot load. (`model` is already null, so no clear is dispatched either.) + expect(dispatch).not.toHaveBeenCalled(); + }); + + 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)); + }); +}); 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 4f3fecc5ac5..3b121538a55 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx @@ -12,10 +12,9 @@ import { MdMoneyOff } from 'react-icons/md'; import { useMainModels } from 'services/api/hooks/modelsByType'; import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; import { - type AnyModelConfig, type AnyModelConfigWithExternal, isNonCommercialMainModelConfig, - isWanSingleFileLowNoiseMainModelConfig, + isSelectableAsPrimaryMainModel, } from 'services/api/types'; export const MainModelPicker = memo(() => { @@ -25,12 +24,9 @@ export const MainModelPicker = memo(() => { const [allModelConfigs] = useMainModels(); // 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. Filter them out of the main model dropdown so users can't accidentally - // wire them backwards. - const modelConfigs = useMemo( - () => allModelConfigs.filter((c) => !isWanSingleFileLowNoiseMainModelConfig(c as AnyModelConfig)), - [allModelConfigs] - ); + // 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(() => allModelConfigs.filter(isSelectableAsPrimaryMainModel), [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..0646dd8623d 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, + isSelectableAsPrimaryMainModel, +} 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(() => allModelConfigs.filter(isSelectableAsPrimaryMainModel), [allModelConfigs]); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (modelConfig: AnyModelConfigWithExternal) => { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 9acb66056b9..970c91a6ee0 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -661,10 +661,23 @@ export const isWanSingleFileMainModelConfig = (config: AnyModelConfigWithExterna * the A14B MoE pair. Suitable for the Transformer (Low Noise) picker, and filtered * out of the primary main dropdown. The two experts don't have to share a format; * both load into the same transformer class. */ -export const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => { +export const isWanSingleFileLowNoiseMainModelConfig = ( + config: AnyModelConfigWithExternal +): config is MainModelConfig => { return isWanSingleFileMainModelConfig(config) && 'expert' in config && config.expert === 'low'; }; +/** Main models offerable 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. + * + * A Wan low-noise expert wired as the primary main is refused by the loader + * ("An unpaired Wan A14B model must be the high-noise expert"), so offering it can + * only lead somewhere broken. */ +export const isSelectableAsPrimaryMainModel = (config: AnyModelConfigWithExternal): boolean => + !isWanSingleFileLowNoiseMainModelConfig(config); + export const isWanLoRAModelConfig = (config: AnyModelConfig): config is WanLoRAModelConfig => { return config.type === 'lora' && config.base === 'wan'; }; diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py index c72f16543c8..3abb8010887 100644 --- a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -167,7 +167,9 @@ def test_fp8_scaled_is_dequantized_and_scales_are_dropped(self, tmp_path: Path) 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) @@ -184,6 +186,11 @@ def test_scale_bookkeeping_never_reaches_the_model(self, tmp_path: Path) -> None 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. From 1f95bee89dad3c05ceb549608eccdc42156d3e23 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 14 Aug 2026 20:59:40 -0400 Subject: [PATCH 09/15] test(model manager): cover the Wan inferences and gates that mutations survived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mutation-tested the branch's own tests by breaking each thing they claim to cover. Four survived untouched: - `out_channels` was inferred from `proj_out.weight` with a five-line comment explaining why it must not be assumed equal to `in_channels` — and every fixture was 16-in/16-out, so `out_channels = in_channels` kept the suite green. Now covered by an asymmetric 36-in/16-out model, the I2V-A14B shape the comment is about. - `Main_GGUF_Wan_Config` gained the branch-family refusal and the LoRA-vs-transformer guard for parity with the checkpoint probe, and neither had a test: deleting both left all 526 config tests passing. The checkpoint side has tests for all five behaviours, so the omission was asymmetry rather than intent. - `layer_count_fallback` was unreachable. `num_layers == 0` means no key starts with `blocks.`, and `require("blocks.0.ffn.net.0.proj.weight")` has already raised by then; setting the fallback to 999 changed nothing. It was also the only use of the `variant` argument, so dropping it makes `_build_wan_transformer_config` derive the config purely from the weights, which is what its docstring says it is for. Co-Authored-By: Claude Opus 5 (1M context) --- .../model_manager/load/model_loaders/wan.py | 19 ++++----- .../configs/test_wan_gguf_config.py | 40 +++++++++++++++++++ .../load/test_wan_checkpoint_loader.py | 23 +++++++++-- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py index 591d1d0c03c..37e590427c8 100644 --- a/invokeai/backend/model_manager/load/model_loaders/wan.py +++ b/invokeai/backend/model_manager/load/model_loaders/wan.py @@ -37,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 @@ -280,7 +279,7 @@ def _tensor_shape(tensor: Any) -> tuple[int, ...]: return tuple(int(dim) for dim in shape) -def _build_wan_transformer_config(sd: dict, variant: WanVariantType, source: str) -> dict: +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 @@ -328,16 +327,18 @@ def require(key: str) -> tuple[int, ...]: # patch_size is (1, 2, 2) → prod = 4 for the Wan 2.2 family. out_channels = require("proj_out.weight")[0] // 4 - # Layer count fallback (only triggers if the auto-count loop above found - # zero blocks, which shouldn't happen for a valid file). T2V/I2V A14B have - # 40 layers; TI2V-5B has 30. - layer_count_fallback = 30 if variant == WanVariantType.TI2V_5B else 40 + # 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 if num_layers > 0 else layer_count_fallback, + "num_layers": num_layers, "attention_head_dim": attention_head_dim, "num_attention_heads": num_attention_heads, "ffn_dim": ffn_dim, @@ -410,7 +411,7 @@ 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) - model_config = _build_wan_transformer_config(sd, config.variant, source="GGUF state dict") + model_config = _build_wan_transformer_config(sd, source="GGUF state dict") with accelerate.init_empty_weights(): model = WanTransformer3DModel(**model_config) @@ -487,7 +488,7 @@ def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: if _is_native_wan_layout(sd): sd = _convert_wan_native_to_diffusers(sd) - model_config = _build_wan_transformer_config(sd, config.variant, source="checkpoint state dict") + model_config = _build_wan_transformer_config(sd, source="checkpoint state dict") with accelerate.init_empty_weights(): model = WanTransformer3DModel(**model_config) 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 1c7ddb60c91..834efbcf715 100644 --- a/tests/backend/model_manager/configs/test_wan_gguf_config.py +++ b/tests/backend/model_manager/configs/test_wan_gguf_config.py @@ -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" diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py index 3abb8010887..f63bae6a8cb 100644 --- a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -89,23 +89,38 @@ def _load(path: Path, variant: WanVariantType = WanVariantType.T2V_A14B): class TestArchitectureInference: def test_matches_the_model_it_came_from(self) -> None: sd = _tiny_model().state_dict() - inferred = _build_wan_transformer_config(sd, WanVariantType.T2V_A14B, source="test") + 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, WanVariantType.T2V_A14B, source="test") + _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, WanVariantType.T2V_A14B, source="test")["num_layers"] == 2 + 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, WanVariantType.T2V_A14B, source="test")["num_layers"] == 1 + assert _build_wan_transformer_config(trimmed, source="test")["num_layers"] == 1 class TestEndToEnd: From c575addc432d36665a60ee1350cef11cd43bd1fa Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 15 Aug 2026 10:42:48 -0400 Subject: [PATCH 10/15] fix(ui): adopt #9505's wiring-first expert pairing across the frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9505 made the loader take the A14B expert pairing from the wiring rather than the filename tag: an unpaired or untagged A14B now runs with a warning instead of raising, and the only hard error left is two files claiming the same expert. Three things on this branch were still enforcing the old contract. - `readiness.ts` blocked Invoke when an unpaired A14B main was tagged anything but 'high'. That would now stop a generation the backend is happy to run — and 'none' is the common case for community checkpoints, which is what this branch exists to support. Rule and its string removed; the VAE/encoder rule stays. - The checkpoint path in `wan_model_loader.py` carried its own copy of the same rejection (the conflict this rebase had to resolve). It now shares #9505's wiring-first logic, with the messages generalised from "GGUF" to "single-file" since checkpoints reach all of them, and the variant-mismatch error extended to name both models and their variants — it was the only pairing error that named neither. - `isSelectableAsPrimaryMainModel` hid every Wan low-noise expert from all three primary-main entry points, justified in its own comment by "the loader refuses it". That justification is gone. Hiding is still the right steer — a low expert belongs in the Transformer (Low Noise) slot and running it alone looks worse — but unconditional hiding would leave someone whose only Wan file is a low expert with their model missing from every picker and no way to reach it. Replaced with `selectPrimaryMainModelOptions`, which hides a low expert only while a partner of the same variant is installed, so the list degrades instead of dead-ending. Co-Authored-By: Claude Opus 5 (1M context) --- invokeai/frontend/web/public/locales/en.json | 1 - .../listeners/modelsLoaded.ts | 15 +++-- .../listeners/modelsLoaded.wan.test.ts | 28 ++++++--- .../features/queue/store/readiness.test.ts | 59 ++++++------------- .../web/src/features/queue/store/readiness.ts | 35 ++++------- .../MainModelPicker.tsx | 4 +- .../layouts/InitialStateMainModelPicker.tsx | 4 +- .../frontend/web/src/services/api/types.ts | 43 +++++++++++--- 8 files changed, 94 insertions(+), 95 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 2b57ce1a68c..daa6f9bc40c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1793,7 +1793,6 @@ "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": "Single-file Wan 2.2 models require a Diffusers Component Source for VAE/encoder", - "noWanLowNoiseExpertSelected": "Wan 2.2 A14B requires the high-noise expert as the main model. Wire the matching expert to Transformer (Low Noise), or select the high-noise file.", "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/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 6aec582c60b..4eeb0a00cf3 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 @@ -51,9 +51,9 @@ import { isQwen3VLEncoderModelConfig, isQwenImageVAEModelConfig, isRefinerMainModelModelConfig, - isSelectableAsPrimaryMainModel, isSpandrelImageToImageModelConfig, isT5EncoderModelConfigOrSubmodel, + selectPrimaryMainModelOptions, } from 'services/api/types'; import type { JsonObject } from 'type-fest'; @@ -135,13 +135,12 @@ type ModelHandler = ( export const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const selectedMainModel = state.params.model; - // isSelectableAsPrimaryMainModel: this auto-selects on the user's behalf whenever the - // current selection goes away, so it must not reach for a model the pickers hide and - // the loader refuses (a Wan low-noise expert). - const allMainModels = models - .filter(isNonRefinerMainModelConfig) - .filter(isSelectableAsPrimaryMainModel) - .sort((a) => (a.base === 'sdxl' ? -1 : 1)); + // selectPrimaryMainModelOptions: this auto-selects on the user's behalf whenever the + // current selection goes away, so it must offer exactly what the pickers offer — never + // reaching for a Wan low-noise expert while its partner is installed. + const allMainModels = selectPrimaryMainModelOptions(models.filter(isNonRefinerMainModelConfig)).sort((a) => + a.base === 'sdxl' ? -1 : 1 + ); const firstModel = allMainModels[0]; 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 index f4b5c77e932..f705a398dd2 100644 --- 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 @@ -35,20 +35,32 @@ 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('does not auto-select a low-noise expert when it is the only Wan model installed', () => { + 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); - // Nothing selectable, so the selection is left null rather than pointed at a model - // that cannot load. (`model` is already null, so no clear is dispatched either.) - expect(dispatch).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledWith(modelSelected(wanLowExpert)); }); - it('auto-selects the high-noise expert over the low-noise one regardless of order', () => { + 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(); - handleMainModels([wanLowExpert, wanHighExpert], makeState(), dispatch, log); + const i2vHigh = { ...wanHighExpert, key: 'wan-i2v-high', variant: 'i2v_a14b' } as unknown as AnyModelConfig; + handleMainModels([wanLowExpert, i2vHigh], makeState(), dispatch, log); - expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith(modelSelected(wanHighExpert)); + // Both remain offerable; the sort leaves the list order, so the low expert is first. + expect(dispatch).toHaveBeenCalledWith(modelSelected(wanLowExpert)); }); }); 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 6d098821f40..b04b920bbff 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts @@ -811,58 +811,35 @@ const buildWanCanvasArg = (overrides: Parameters[0]) => const hasWanComponentReason = (reasons: { content: string }[]) => reasons.some((r) => r.content.includes('noWanComponentSourceSelected')); -const hasWanExpertReason = (reasons: { content: string }[]) => - reasons.some((r) => r.content.includes('noWanLowNoiseExpertSelected')); - -// The A14B expert pre-flight. `WanModelLoaderInvocation` raises a hard ValueError for -// an unpaired A14B main that isn't the high-noise expert, so readiness has to block it -// rather than let the user hit it at generation time. Only expert='high' degrades -// gracefully (high expert runs the whole schedule, with a warning). -describe('Wan 2.2 A14B expert pre-flight', () => { +// 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. +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], - ])('blocks an unpaired A14B main that is %s', (_label, model) => { + ['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(hasWanExpertReason(reasons)).toBe(true); + expect(reasons).toEqual([]); }); - it.each([ - ['untagged (expert=none)', wanUntaggedA14bModel], - ['the low-noise expert', wanLowExpertModel], - ])('allows %s once a low-noise partner is wired', (_label, model) => { - const reasons = getReasonsWhyCannotEnqueueGenerateTab( - buildWanTabArg({ model, ...withComponents, wanTransformerLowNoise: { key: 'partner' } }) - ); - expect(hasWanExpertReason(reasons)).toBe(false); - }); - - it('allows an unpaired A14B high-noise expert — it degrades with a warning, not an error', () => { - const reasons = getReasonsWhyCannotEnqueueGenerateTab( - buildWanTabArg({ model: wanCheckpointModel, ...withComponents }) - ); - expect(hasWanExpertReason(reasons)).toBe(false); - }); - - it('does not apply to TI2V-5B, which is single-transformer', () => { - const reasons = getReasonsWhyCannotEnqueueGenerateTab(buildWanTabArg({ model: wanTi2v5bModel, ...withComponents })); - expect(hasWanExpertReason(reasons)).toBe(false); - }); - - it('does not apply to a Diffusers main, which carries both experts', () => { - const reasons = getReasonsWhyCannotEnqueueGenerateTab( - buildWanTabArg({ model: wanDiffusersModel, ...withComponents }) - ); - expect(hasWanExpertReason(reasons)).toBe(false); - }); - - it('also runs on the canvas tab', () => { + it('also does not block on the canvas tab', () => { const reasons = getReasonsWhyCannotEnqueueCanvasTab( buildWanCanvasArg({ model: wanUntaggedA14bModel, ...withComponents }) ); - expect(hasWanExpertReason(reasons)).toBe(true); + 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); }); }); diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index ae0b8a8274e..cc6412ae465 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -251,15 +251,20 @@ export const useReadinessWatcher = () => { const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); -const WAN_A14B_VARIANTS = ['t2v_a14b', 'i2v_a14b']; - /** Pre-flight for single-file 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. */ -const pushWanSingleFileReasons = (model: MainOrExternalModelConfig, params: ParamsState, reasons: Reason[]): void => { + * nothing the user can act on. + * + * Note there is deliberately no check on the A14B expert pairing. Since #9505 the + * loader takes the pairing from the wiring rather than the filename tag, so an unpaired + * or untagged A14B runs with a warning instead of raising — the only hard error left is + * two files claiming the *same* expert, which the pickers already prevent by offering + * each slot a different list. Blocking here on `expert !== 'high'` would stop a + * generation the backend is happy to run. */ +const pushWanSingleFileReasons = (params: ParamsState, reasons: Reason[]): void => { // Single-file Wan mains (GGUF or safetensors checkpoint) 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; @@ -267,24 +272,6 @@ const pushWanSingleFileReasons = (model: MainOrExternalModelConfig, params: Para if (!hasVaeSource || !hasEncoderSource) { reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') }); } - - // The A14B MoE pair. An unpaired A14B is only accepted when it is the *high*-noise - // expert — that case degrades to "high expert runs the whole schedule" with a warning. - // `expert` of 'low' or 'none' with nothing wired to the low-noise slot is a hard - // ValueError in the loader, so it has to block here rather than fail at generation - // time. ('none' is common: the tag is a filename heuristic, and there is no UI to - // correct it.) Revisit if the loader adopts wiring-first pairing — see #9505, which - // would make 'none' resolvable from the wiring instead of an error. - const variant = 'variant' in model ? model.variant : undefined; - const expert = 'expert' in model ? model.expert : undefined; - if ( - typeof variant === 'string' && - WAN_A14B_VARIANTS.includes(variant) && - expert !== 'high' && - !params.wanTransformerLowNoise - ) { - reasons.push({ content: i18n.t('parameters.invoke.noWanLowNoiseExpertSelected') }); - } }; export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { @@ -439,7 +426,7 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { } if (model && isWanSingleFileMainModelConfig(model)) { - pushWanSingleFileReasons(model, params, reasons); + pushWanSingleFileReasons(params, reasons); } if (model?.base === 'z-image') { @@ -1185,7 +1172,7 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } if (model && isWanSingleFileMainModelConfig(model)) { - pushWanSingleFileReasons(model, params, reasons); + pushWanSingleFileReasons(params, 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 3b121538a55..7b9e7d72057 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx @@ -14,7 +14,7 @@ import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfi import { type AnyModelConfigWithExternal, isNonCommercialMainModelConfig, - isSelectableAsPrimaryMainModel, + selectPrimaryMainModelOptions, } from 'services/api/types'; export const MainModelPicker = memo(() => { @@ -26,7 +26,7 @@ export const MainModelPicker = memo(() => { // 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(() => allModelConfigs.filter(isSelectableAsPrimaryMainModel), [allModelConfigs]); + 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 0646dd8623d..3f2850792e2 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx @@ -13,7 +13,7 @@ import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfi import { type AnyModelConfigWithExternal, isNonCommercialMainModelConfig, - isSelectableAsPrimaryMainModel, + selectPrimaryMainModelOptions, } from 'services/api/types'; export const InitialStateMainModelPicker = memo(() => { @@ -23,7 +23,7 @@ export const InitialStateMainModelPicker = memo(() => { 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(() => allModelConfigs.filter(isSelectableAsPrimaryMainModel), [allModelConfigs]); + const modelConfigs = useMemo(() => selectPrimaryMainModelOptions(allModelConfigs), [allModelConfigs]); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (modelConfig: AnyModelConfigWithExternal) => { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 970c91a6ee0..9627e9e5e3f 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -667,16 +667,41 @@ export const isWanSingleFileLowNoiseMainModelConfig = ( return isWanSingleFileMainModelConfig(config) && 'expert' in config && config.expert === 'low'; }; -/** Main models offerable 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. +/** 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. * - * A Wan low-noise expert wired as the primary main is refused by the loader - * ("An unpaired Wan A14B model must be the high-noise expert"), so offering it can - * only lead somewhere broken. */ -export const isSelectableAsPrimaryMainModel = (config: AnyModelConfigWithExternal): boolean => - !isWanSingleFileLowNoiseMainModelConfig(config); + * 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'; From e73f233e9965952210674bc490eb4e3fda0ed87a Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 15 Aug 2026 11:52:33 -0400 Subject: [PATCH 11/15] fix(wan): repair the defects a fresh-context review found in the last two rounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent reviewers on split scopes. Six real defects, three of them regressions introduced by the very commits meant to fix things. Regressions: - `_detect_wan_expert` let a disqualifier consume the whole run of adjacent markers instead of only the one it qualifies, so `...-A14B-HIGH_lowVRAM_fp8_scaled_KJ` went from 'high' to 'none'. That is not a safe default: for a main it disables both pair checks, and for a LoRA it applies a single-expert distill to both experts. - `handleMainModels` tested the *selected* model against the visibility-filtered list, conflating "hidden" with "uninstalled". Installing the high-noise partner therefore read as "your model vanished" and swapped the user onto an unrelated model — firing the whole base-changed cascade (LoRAs disabled, VAE cleared, bbox resized) for what was only a file install. Availability is now tested against what exists; only the auto-pick uses the filtered list. - The `wan_model_loader.py` conflict resolution duplicated the unpaired-A14B warning block, logging it twice. The test used `any(...)` and could not see it. Gaps: - The merged-LoRA allowlist covered kohya and PEFT but not LoKr, LoHa, DoRA or OFT, which `LoRA_LyCORIS_Wan_Config` does accept — so the probe took a merged file the loader then refused, blaming Animate/S2V/Fun-Camera. It now tracks the same families, and matches path segments rather than substrings so a future branch named `..._lora_adapter` still trips the backstop instead of being swallowed. - Benign extras were classified but never removed, so an all-in-one checkpoint's bundled VAE and UMT5-XXL were dequantized, upcast to bf16 and reserved in the RAM cache before `load_state_dict` discarded them — several GB for the exact family this branch added support for. They are now dropped straight after the prefix strip, ahead of all three costs. - Readiness checked only that the Wan slots were *populated*, while the Advanced comboboxes offer every Wan VAE and Diffusers main with no variant filter and nothing re-runs the auto-fill on a hand-picked slot. Four loader errors were reachable with Invoke enabled. It now shares the compatibility predicates with the auto-fill rather than restating them, and covers the duplicate-transformer case that became reachable once low experts could appear in the main picker. Also: a self-contained Diffusers main no longer gets a standalone VAE force-wired over its own (the loader ranks the wired one higher, and clearing it just refilled on the next selection); a low-noise partner left over from another variant is cleared; the encoder slot is resolved against installed models like the other two; and the variant-mismatch error prints a value rather than a raw enum. Every fix is mutation-verified. Two tests that asserted the old behaviour were rewritten, and the tautological `hasattr(model, "vae")` assertions were replaced with ones that read the dict actually handed to `load_state_dict`. Co-Authored-By: Claude Opus 5 (1M context) --- invokeai/app/invocations/wan_model_loader.py | 9 +- .../backend/model_manager/configs/main.py | 16 ++- .../model_manager/load/model_loaders/wan.py | 102 ++++++++++----- invokeai/frontend/web/public/locales/en.json | 4 + .../listeners/modelSelected.ts | 13 +- .../listeners/modelsLoaded.ts | 19 +-- .../listeners/modelsLoaded.wan.test.ts | 39 +++++- .../listeners/wanComponentSync.test.ts | 42 ++++-- .../listeners/wanComponentSync.ts | 71 ++++++++--- .../features/queue/store/readiness.test.ts | 117 ++++++++++++++++- .../web/src/features/queue/store/readiness.ts | 120 ++++++++++++++---- .../configs/test_wan_checkpoint_config.py | 8 ++ .../load/test_wan_checkpoint_loader.py | 37 +++++- .../model_manager/load/test_wan_loader.py | 43 ++++--- 14 files changed, 507 insertions(+), 133 deletions(-) diff --git a/invokeai/app/invocations/wan_model_loader.py b/invokeai/app/invocations/wan_model_loader.py index 0876843217e..5cf07522d9b 100644 --- a/invokeai/app/invocations/wan_model_loader.py +++ b/invokeai/app/invocations/wan_model_loader.py @@ -181,10 +181,11 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: low_expert = getattr(low_config, "expert", "none") if getattr(low_config, "variant", None) != main_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_config, 'variant', None)}." + f"{getattr(low_variant, 'value', low_variant)}." ) # The expert tag is a filename heuristic, so 'none' (untagged) is common on @@ -241,12 +242,6 @@ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput: "the high-noise one is usually the better choice." ) context.logger.warning(message) - if primary_expert == "low": - message += ( - " Its filename tags it as the low-noise expert; when running a single expert, " - "the high-noise one is usually the better choice." - ) - context.logger.warning(message) # Borrow the boundary_ratio recorded on the optional Diffusers # component_source, when one is wired. diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py index fb318d525bb..609f7dc8f3b 100644 --- a/invokeai/backend/model_manager/configs/main.py +++ b/invokeai/backend/model_manager/configs/main.py @@ -2094,11 +2094,19 @@ def _detect_wan_expert(filename: str) -> Literal["high", "low", "none"]: # 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. # - # Checked ahead of the 'noise' test below. A disqualifier sits after the run, - # so an adjacent 'noise' would have to precede it — `noise_LOW_VRAM` is - # describing VRAM, not the low-noise expert. ('low noise' can't trip this: - # 'noise' is not itself a disqualifier.) + # 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) diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py index 37e590427c8..bb9691d3018 100644 --- a/invokeai/backend/model_manager/load/model_loaders/wan.py +++ b/invokeai/backend/model_manager/load/model_loaders/wan.py @@ -207,23 +207,72 @@ def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict: } ) -# Substrings marking a merged-in LoRA's leftover adapter tensors. The main-model +# 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. -_MERGED_LORA_MARKERS = ("lora_a", "lora_b", "lora_down", "lora_up", "lora_magnitude", "dora_scale", ".alpha") +# ``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.""" - if key.split(".")[0] in _BENIGN_EXTRA_MODULES: + parts = key.lower().split(".") + if parts[0] in _BENIGN_EXTRA_MODULES: + return True + if parts[-1] in _MERGED_LORA_SEGMENTS: return True - lowered = key.lower() - return any(marker in lowered for marker in _MERGED_LORA_MARKERS) + # `...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. -def _raise_for_incompatible_keys(incompatible_keys: Any, source: str, logger: Any) -> None: + 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 @@ -238,31 +287,19 @@ def _raise_for_incompatible_keys(incompatible_keys: Any, source: str, logger: An know by name; this is the generic backstop, so a derivative nobody has enumerated yet produces an error instead of quietly degraded output. - Not every extra key is a conditioning branch, though, so the two categories in - ``_is_benign_extra_key`` are dropped with a log line instead of raising: bundled - VAE/text-encoder weights (the "all-in-one" packaging convention) and merged-LoRA - residue (which the main-model probe explicitly accepts). Refusing those would - reject files that load and generate correctly today. + 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)] - benign = [key for key in unexpected if _is_benign_extra_key(key)] - unsupported = [key for key in unexpected if not _is_benign_extra_key(key)] - - if benign: - modules = sorted({key.split(".")[0] for key in benign}) - logger.info( - f"{source}: ignored {len(benign)} bundled/merged weights not part of the transformer " - f"({', '.join(modules[:8])}). The VAE and text encoder come from the separately-wired models." - ) - - if unsupported: + if unexpected: # Report the distinct top-level module names rather than hundreds of keys. - modules = sorted({key.split(".")[0] for key in unsupported}) + modules = sorted({key.split(".")[0] for key in unexpected}) raise RuntimeError( - f"{source} has {len(unsupported)} weights that WanTransformer3DModel has nowhere to put " + 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." @@ -397,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, @@ -417,11 +456,7 @@ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel: model = WanTransformer3DModel(**model_config) incompatible_keys = model.load_state_dict(sd, strict=False, assign=True) - _raise_for_incompatible_keys( - incompatible_keys, - source="GGUF state dict", - logger=InvokeAILogger.get_logger(self.__class__.__name__), - ) + _raise_for_incompatible_keys(incompatible_keys, source="GGUF state dict") return model @@ -471,6 +506,7 @@ def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: 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: @@ -504,7 +540,7 @@ def _load_from_singlefile(self, config: Main_Checkpoint_Wan_Config) -> AnyModel: 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", logger=logger) + _raise_for_incompatible_keys(incompatible_keys, source="Wan checkpoint") return model diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index daa6f9bc40c..b5a84317d5d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1793,6 +1793,10 @@ "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": "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 eb459ec3863..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 @@ -23,6 +23,7 @@ import { vaeSelected, wanComponentSourceSelected, wanT5EncoderModelSelected, + wanTransformerLowNoiseSelected, wanVaeModelSelected, zImageQwen3EncoderModelSelected, zImageQwen3SourceModelSelected, @@ -643,7 +644,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = // 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 } = state.params; + const { wanComponentSource, wanVaeModel, wanT5EncoderModel, wanTransformerLowNoise } = state.params; const configFor = (identifier: { key: string } | null) => identifier && modelConfigsResult.data ? (modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, identifier.key) ?? null) @@ -654,7 +655,8 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = isSingleFileMain: isWanSingleFileMainModelConfig(newModelConfig), selectedVae: configFor(wanVaeModel), selectedComponentSource: configFor(wanComponentSource), - selectedEncoder: wanT5EncoderModel, + selectedEncoder: configFor(wanT5EncoderModel), + selectedLowNoisePartner: configFor(wanTransformerLowNoise), availableVaes: selectWanVAEModels(state), availableDiffusers: selectWanDiffusersModels(state), availableEncoders: selectWanT5EncoderModels(state), @@ -673,6 +675,13 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = 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 4eeb0a00cf3..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 @@ -135,14 +135,17 @@ type ModelHandler = ( export const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const selectedMainModel = state.params.model; - // selectPrimaryMainModelOptions: this auto-selects on the user's behalf whenever the - // current selection goes away, so it must offer exactly what the pickers offer — never - // reaching for a Wan low-noise expert while its partner is installed. - const allMainModels = selectPrimaryMainModelOptions(models.filter(isNonRefinerMainModelConfig)).sort((a) => - a.base === 'sdxl' ? -1 : 1 - ); - - const firstModel = allMainModels[0]; + const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); + + // 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 index f705a398dd2..9524824829f 100644 --- 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 @@ -6,10 +6,13 @@ import { describe, expect, it, vi } from 'vitest'; import { handleMainModels } from './modelsLoaded'; /** - * A Wan low-noise expert is refused by the loader as a primary main ("An unpaired Wan - * A14B model must be the high-noise expert"), so no path that chooses a primary main on - * the user's behalf may reach for one. This listener is the least visible of the three: - * it fires on every `getModelConfigs` fulfilment and swaps the selection silently. + * 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 = { @@ -30,6 +33,15 @@ const wanLowExpert = { 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; @@ -63,4 +75,23 @@ describe('handleMainModels — Wan low-noise experts', () => { // 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 index 1606fb5d870..4ae9face91e 100644 --- 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 @@ -72,6 +72,7 @@ const build = (overrides: Partial[0]> selectedVae: null, selectedComponentSource: null, selectedEncoder: null, + selectedLowNoisePartner: null, availableVaes: [], availableDiffusers: [], availableEncoders: [], @@ -141,29 +142,44 @@ describe('getWanComponentUpdates', () => { ).toEqual({}); }); - it('re-validates the standalone VAE for a Diffusers main too', () => { - // The loader prefers a wired standalone VAE over the Diffusers main's own, so a - // stale one breaks a model that is otherwise self-contained. + 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: ti2v5bDiffusers, + mainConfig: a14bDiffusers, isSingleFileMain: false, - selectedVae: vae16, - availableVaes: [vae16, vae48], + availableVaes: [vae16], + availableDiffusers: [a14bDiffusers], + availableEncoders: [encoder], }) - ).toEqual({ vae: vae48 }); + ).toEqual({}); }); - it('does not wire a Component Source or encoder for a Diffusers main', () => { + 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: a14bDiffusers, + mainConfig: ti2v5bDiffusers, isSingleFileMain: false, - availableVaes: [vae16], - availableDiffusers: [a14bDiffusers], - availableEncoders: [encoder], + selectedVae: vae16, + availableVaes: [vae16, vae48], }) - ).toEqual({ vae: vae16 }); + ).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', () => { 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 index 3299931e125..556ef3426f4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts @@ -22,11 +22,38 @@ type WanComponentUpdates = { 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; +const isTi2v5b = (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 === (isTi2v5b(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' && + isTi2v5b(source) === isTi2v5b(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; @@ -40,6 +67,7 @@ export const getWanComponentUpdates = (arg: { selectedVae: AnyModelConfig | null; selectedComponentSource: AnyModelConfig | null; selectedEncoder: Identifier; + selectedLowNoisePartner: AnyModelConfig | null; availableVaes: AnyModelConfig[]; availableDiffusers: AnyModelConfig[]; availableEncoders: AnyModelConfig[]; @@ -50,6 +78,7 @@ export const getWanComponentUpdates = (arg: { selectedVae, selectedComponentSource, selectedEncoder, + selectedLowNoisePartner, availableVaes, availableDiffusers, availableEncoders, @@ -57,27 +86,21 @@ export const getWanComponentUpdates = (arg: { const updates: WanComponentUpdates = {}; - const isTi2v5b = variantOf(mainConfig) === 'ti2v_5b'; - const requiredLatentChannels = isTi2v5b ? 48 : 16; + const vaeIsCompatible = (model: unknown) => isWanVaeCompatible(mainConfig, model); + const sourceIsCompatible = (model: unknown) => isWanComponentSourceCompatible(mainConfig, model); - const vaeIsCompatible = (model: unknown) => - !!model && - typeof model === 'object' && - 'latent_channels' in model && - model.latent_channels === requiredLatentChannels; - - const sourceIsCompatible = (model: unknown) => (variantOf(model) === 'ti2v_5b') === isTi2v5b; - - // The standalone VAE outranks every other source in the loader — including a Diffusers - // main's own — so it is checked for any Wan main, not just single-file ones. - if (!vaeIsCompatible(selectedVae)) { + // 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); - // Clearing when nothing fits is deliberate: an empty slot reads as "pick one" in the - // UI, a stale one reads as already handled. if (vae) { updates.vae = vae; - } else if (selectedVae) { - updates.vae = null; } } @@ -86,7 +109,9 @@ export const getWanComponentUpdates = (arg: { 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. + // 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; @@ -96,7 +121,7 @@ export const getWanComponentUpdates = (arg: { } // The UMT5-XXL encoder is shared across every Wan variant, so first-match is correct - // and there is nothing to re-validate. + // and there is nothing to re-validate beyond the model still existing. if (!selectedEncoder) { const encoder = availableEncoders[0]; if (encoder) { @@ -105,5 +130,13 @@ export const getWanComponentUpdates = (arg: { } } + // 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/queue/store/readiness.test.ts b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts index b04b920bbff..a2761792ed0 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 }[]) => @@ -769,6 +773,9 @@ const buildWanTabArg = (overrides: { wanT5EncoderModel?: unknown; wanComponentSource?: unknown; wanTransformerLowNoise?: unknown; + wiredVae?: unknown; + wiredComponentSource?: unknown; + wiredLowNoisePartner?: unknown; }) => ({ isConnected: true, model: overrides.model ?? wanCheckpointModel, @@ -778,6 +785,7 @@ const buildWanTabArg = (overrides: { wanT5EncoderModel: overrides.wanT5EncoderModel ?? null, wanComponentSource: overrides.wanComponentSource ?? null, wanTransformerLowNoise: overrides.wanTransformerLowNoise ?? null, + model: overrides.model ?? wanCheckpointModel, } as unknown as ParamsState, refImages: baseRefImages, loras: [], @@ -785,6 +793,11 @@ const buildWanTabArg = (overrides: { 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]) => @@ -816,6 +829,108 @@ const hasWanComponentReason = (reasons: { content: string }[]) => // 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('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' } }; diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index cc6412ae465..f7c7d7f3cac 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1,8 +1,13 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; +import { + isWanComponentSourceCompatible, + isWanLowNoisePartnerCompatible, + 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,8 +49,9 @@ 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, @@ -98,6 +104,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, @@ -137,6 +158,7 @@ const debouncedUpdateReasons = debounce(async (arg: UpdateReasonsArg) => { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs: selectWanWiredConfigs(store.getState()), }); $reasonsWhyCannotEnqueue.set(reasons); } else if (tab === 'canvas') { @@ -164,6 +186,7 @@ const debouncedUpdateReasons = debounce(async (arg: UpdateReasonsArg) => { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs: selectWanWiredConfigs(store.getState()), }); $reasonsWhyCannotEnqueue.set(reasons); } else if (tab === 'workflows') { @@ -251,26 +274,59 @@ export const useReadinessWatcher = () => { const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); -/** Pre-flight for single-file Wan mains, shared by the generate and canvas tabs so the - * two can't drift. Mirrors what `WanModelLoaderInvocation` actually enforces. +/** 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. + * populate the slots this demands, selecting the model just blocks Invoke with nothing + * the user can act on. * - * Note there is deliberately no check on the A14B expert pairing. Since #9505 the - * loader takes the pairing from the wiring rather than the filename tag, so an unpaired - * or untagged A14B runs with a warning instead of raising — the only hard error left is - * two files claiming the *same* expert, which the pickers already prevent by offering - * each slot a different list. Blocking here on `expert !== 'high'` would stop a - * generation the backend is happy to run. */ -const pushWanSingleFileReasons = (params: ParamsState, reasons: Reason[]): void => { - // Single-file Wan mains (GGUF or safetensors checkpoint) 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') }); + * 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') }); + } + } + if (wired.lowNoisePartner) { + 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') }); + } } }; @@ -284,6 +340,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, @@ -295,6 +359,7 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs, } = arg; const { positivePrompt } = params; const reasons: Reason[] = []; @@ -425,8 +490,8 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: { } } - if (model && isWanSingleFileMainModelConfig(model)) { - pushWanSingleFileReasons(params, reasons); + if (model?.base === 'wan') { + pushWanReasons(model, params, wanWiredConfigs, reasons); } if (model?.base === 'z-image') { @@ -674,6 +739,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, @@ -691,6 +764,7 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { hasFlux2DiffusersVaeSource, hasFlux2DiffusersQwen3Source, hasFlux2DevDiffusersSource, + wanWiredConfigs, } = arg; const { positivePrompt } = params; const reasons: Reason[] = []; @@ -1171,8 +1245,8 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } } - if (model && isWanSingleFileMainModelConfig(model)) { - pushWanSingleFileReasons(params, reasons); + if (model?.base === 'wan') { + pushWanReasons(model, params, wanWiredConfigs, reasons); } if (model?.base === 'z-image') { diff --git a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py index 22a437800d3..67d4f887eb0 100644 --- a/tests/backend/model_manager/configs/test_wan_checkpoint_config.py +++ b/tests/backend/model_manager/configs/test_wan_checkpoint_config.py @@ -443,6 +443,14 @@ class TestExpertFilenameHeuristic: # "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"), diff --git a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py index f63bae6a8cb..1d97c7d4eb4 100644 --- a/tests/backend/model_manager/load/test_wan_checkpoint_loader.py +++ b/tests/backend/model_manager/load/test_wan_checkpoint_loader.py @@ -245,10 +245,18 @@ def test_all_in_one_bundled_components_are_dropped_not_refused(self, tmp_path: P path = tmp_path / "wan2.2-t2v-rapid-aio-v10-high_noise.safetensors" save_file(sd, path) - model = _load(path) - # The transformer itself still loaded, and none of the bundled weights reached it. - assert not hasattr(model, "vae") - assert not hasattr(model, "text_encoders") + 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 @@ -266,6 +274,27 @@ def test_merged_lora_residue_is_dropped_not_refused(self, tmp_path: Path) -> Non _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. diff --git a/tests/backend/model_manager/load/test_wan_loader.py b/tests/backend/model_manager/load/test_wan_loader.py index 1d2bd8c2019..3f711db9216 100644 --- a/tests/backend/model_manager/load/test_wan_loader.py +++ b/tests/backend/model_manager/load/test_wan_loader.py @@ -182,15 +182,27 @@ def test_plain_torch_tensor_passes_through(self): assert out["plain"] is plain -def _run_gguf_loader_with_unexpected_keys(unexpected: list[str]) -> None: - """Drive WanGGUFCheckpointModel to the incompatible-keys check with `unexpected`.""" +def _run_gguf_loader(extra_keys: list[str]) -> 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. + """ 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() - model.load_state_dict.return_value = SimpleNamespace(missing_keys=[], unexpected_keys=unexpected) + # Report as unexpected whatever the loader still hands over that isn't a real param. + real_params = {"patch_embedding.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) @@ -210,30 +222,31 @@ def _run_gguf_loader_with_unexpected_keys(unexpected: list[str]) -> None: ): loader._load_from_singlefile(config) + return model.load_state_dict.call_args.args[0] + -def test_gguf_loader_accepts_all_in_one_bundled_components() -> None: +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. - Before the unexpected-key backstop these loaded fine: `strict=False` dropped the - bundled copies and InvokeAI sourced the VAE and encoder from separately-wired - models. Refusing them is a regression on a path that already worked. + 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. """ - _run_gguf_loader_with_unexpected_keys( - [ - "vae.decoder.conv_in.weight", - "text_encoders.umt5xxl.shared.weight", - "model_ema.patch_embedding.weight", - ] - ) + 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_with_unexpected_keys(["vae.decoder.conv_in.weight", "audio_injector.0.proj.weight"]) + _run_gguf_loader(["vae.decoder.conv_in.weight", "audio_injector.0.proj.weight"]) def test_gguf_loader_rejects_missing_model_parameter() -> None: From aaf3d63c90dabd13895fe8668ee6c03b164a0517 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 15 Aug 2026 16:59:33 -0400 Subject: [PATCH 12/15] fix(wan): address Pfannkuchensack's four review findings on #9503 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. TI2V-5B LoRAs were regressed to inert. Widening the LoRA probe to the shared `_detect_wan_expert` made it read the bare high/low token convention, but it did not carry over the structural pin `_resolve_wan_expert` applies on the main-model side. A 5B LoRA whose stem happens to contain a standalone `low` ("Wan2.2_TI2V_5B_low_light_v2") was tagged, routed by `_resolve_target("auto")` into `loras_low_noise` alone, and then never read — the single-transformer 5B denoise path consumes only the primary list. The generation succeeded with the LoRA silently absent. Variant is now resolved first and only A14B gets a tag. 2. Readiness blocked Invoke on a slot the loader ignores. `pushWanReasons` judged the low-noise partner for every Wan main, but the loader reads it only for a single-file A14B: it logs "ignored for the single-expert TI2V-5B variant" and skips the pairing block, and the Diffusers branch never looks at the slot. For a TI2V-5B main this was unavoidable rather than occasional — the partner picker can only offer A14Bs, so every possible pick failed the variant check, and the error text does not name the combobox to clear. 3. Pinned the GGUF side of the unexpected-key backstop. The gate is applied to the pre-existing GGUF loader, which previously checked `missing_keys` only. Its diffusers-layout behaviour was already covered; the native-layout path, where an unmapped key is possible at all, was not. 4. An untagged expert pair was unwireable from the linear UI. The partner picker required `expert === 'low'`, so a pair probing to none/none appeared twice in the main picker and never in the low-noise one. Since #9505 the wiring is authoritative and the tag advisory, so the picker now takes anything single-file that is not tagged `high` and not TI2V-5B. `expert` is absent from `ModelRecordChanges` and records are never re-probed, so this was permanent for anything already installed. Every fix is mutation-verified: each new test fails against the unfixed code. --- .../backend/model_manager/configs/lora.py | 23 +++++-- .../listeners/wanComponentSync.ts | 8 ++- .../features/queue/store/readiness.test.ts | 36 +++++++++++ .../web/src/features/queue/store/readiness.ts | 12 +++- .../src/services/api/hooks/modelsByType.ts | 4 +- .../web/src/services/api/types.test.ts | 63 ++++++++++++++++++- .../frontend/web/src/services/api/types.ts | 38 ++++++++--- .../configs/test_wan_lora_config.py | 52 +++++++++++++++ .../model_manager/load/test_wan_loader.py | 61 +++++++++++++++--- 9 files changed, 270 insertions(+), 27 deletions(-) diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py index e12f690e1b2..c275863c9e0 100644 --- a/invokeai/backend/model_manager/configs/lora.py +++ b/invokeai/backend/model_manager/configs/lora.py @@ -1142,6 +1142,14 @@ 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 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 @@ -1149,18 +1157,21 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) - # 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: + if instance.expert is None and instance.variant != WanLoRAVariantType.Wan5B: detected = _detect_wan_expert(mod.path.stem) if detected != "none": instance.expert = detected - # Auto-detect the model-family variant from inner_dim in the state - # dict. The override field skips this if the user has set it. - if instance.variant is None: - instance.variant = detect_wan_lora_variant(mod.load_state_dict()) - return instance 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 index 556ef3426f4..d2bfe1bb2dc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/wanComponentSync.ts @@ -28,7 +28,9 @@ type WanComponentUpdates = { const variantOf = (model: unknown): string | null => model && typeof model === 'object' && 'variant' in model && typeof model.variant === 'string' ? model.variant : null; -const isTi2v5b = (model: unknown): boolean => variantOf(model) === 'ti2v_5b'; +/** 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 @@ -37,7 +39,7 @@ export const isWanVaeCompatible = (mainConfig: unknown, vae: unknown): boolean = !!vae && typeof vae === 'object' && 'latent_channels' in vae && - vae.latent_channels === (isTi2v5b(mainConfig) ? 48 : 16); + 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, @@ -47,7 +49,7 @@ export const isWanComponentSourceCompatible = (mainConfig: unknown, source: unkn typeof source === 'object' && 'format' in source && source.format === 'diffusers' && - isTi2v5b(source) === isTi2v5b(mainConfig); + 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". */ 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 a2761792ed0..ca81039bf99 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.test.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.test.ts @@ -907,6 +907,42 @@ describe('Wan 2.2 component compatibility pre-flight', () => { 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. diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index f7c7d7f3cac..e1497d78e96 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -4,6 +4,7 @@ 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'; @@ -321,7 +322,16 @@ const pushWanReasons = ( reasons.push({ content: i18n.t('parameters.invoke.incompatibleWanComponentSource') }); } } - if (wired.lowNoisePartner) { + // 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)) { diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts index 8834c2766e7..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, - isWanSingleFileLowNoiseMainModelConfig, + 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 useWanSingleFileLowNoiseModels = () => buildModelsHook(isWanSingleFileLowNoiseMainModelConfig)(); +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/types.test.ts b/invokeai/frontend/web/src/services/api/types.test.ts index 0c63dd5d25a..e4a61299fbb 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,58 @@ 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('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 9627e9e5e3f..a3dadea4e8a 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -657,16 +657,40 @@ export const isWanSingleFileMainModelConfig = (config: AnyModelConfigWithExterna ); }; -/** Wan single-file main models marked as the low-noise expert — the second half of - * the A14B MoE pair. Suitable for the Transformer (Low Noise) picker, and filtered - * out of the primary main dropdown. The two experts don't have to share a format; - * both load into the same transformer class. */ -export const isWanSingleFileLowNoiseMainModelConfig = ( - config: AnyModelConfigWithExternal -): config is MainModelConfig => { +/** 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. */ +const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfigWithExternal): config is MainModelConfig => { return isWanSingleFileMainModelConfig(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, and it is not self-healing — + * `expert` is absent from `ModelRecordChanges` and installed records are never + * re-probed, so anything already stored as `none` stays that way short of a + * delete-and-reinstall, which mints a new model key. + * + * 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 !('variant' in config && config.variant === 'ti2v_5b'); +}; + /** 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 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..40248c86889 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,58 @@ 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. + """ + 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}', which routes it nowhere" + + 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_loader.py b/tests/backend/model_manager/load/test_wan_loader.py index 3f711db9216..cc94657f038 100644 --- a/tests/backend/model_manager/load/test_wan_loader.py +++ b/tests/backend/model_manager/load/test_wan_loader.py @@ -182,24 +182,42 @@ def test_plain_torch_tensor_passes_through(self): assert out["plain"] is plain -def _run_gguf_loader(extra_keys: list[str]) -> dict: +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. """ - 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), - } + 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. - real_params = {"patch_embedding.weight", "blocks.0.ffn.net.0.proj.weight", "proj_out.weight"} + # 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] ) @@ -249,6 +267,35 @@ def test_gguf_loader_still_refuses_an_unknown_conditioning_branch() -> None: _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)`. Refusing is deliberate, and + the blast radius is narrower than it 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 genuinely new is the case below — + a whole extra branch, here VACE, whose conversion we deliberately do not ship + (see `_WAN_NATIVE_TO_DIFFUSERS_RENAMES`: "T2V subset; we don't ship VACE / motion / + face-adapter conversion"). Generating with it quietly absent is worse than refusing. + """ + with pytest.raises(RuntimeError, match="vace_blocks"): + _run_gguf_loader(["vace_blocks.0.after_proj.weight"], native_layout=True) + + +def test_gguf_loader_accepts_a_native_layout_all_in_one_bundle() -> None: + """The benign-extras drop has to survive the native-layout rewrite too. The rename + table is blind substring replacement over every key, so it runs across the bundled + VAE/encoder names as well — this pins that they are still recognised and dropped.""" + 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), From 98079579eac9014348d87a7e79eb03fc2a48d59d Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 15 Aug 2026 17:26:25 -0400 Subject: [PATCH 13/15] fix(wan): close the gaps a fresh-context review found in the last commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adversarial reviewers on split scopes. The frontend gate and picker survived an enumeration of every 1-3 model library over a 17-model universe (0 false positives, 0 models invisible in both pickers). The backend found a real hole. - The TI2V-5B expert pin only fires 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`, comes back `variant=None`, keeps its `low` tag, and then sails past `_assert_lora_variant_matches_main`, which returns early on an unknown variant. Records written before the pin are in the same position. Confirmed by probing: a LoKr 5B LoRA named `Wan2.2_TI2V_5B_low_light_v2` still yields `expert='low'`. `_warn_if_low_routing_is_inert` becomes `_correct_inert_low_routing`: where the main is TI2V-5B and the routing came out low-only, apply the LoRA to the single transformer instead of warning that it will do nothing. The main's variant is the one signal that cannot be wrong. - The GGUF native-layout test pinned a scenario the probe forecloses. `_find_unsupported_wan_variant_marker` rejects `vace_blocks.` with `NotAMatchError` at identification, so a VACE GGUF never reaches a loader. Switched to an un-enumerated branch, which is what the backstop is actually for. The bundle test's stated mechanism was also wrong: benign extras are dropped before the rename table runs, so the two passes never interact. - `selectPrimaryMainModelOptions` had no test that could catch it being keyed on the wide partner predicate — with two untagged models the wide test classes both as low experts, so neither has a partner and the mistake hides behind itself. Added the `[high, untagged]` case, which fails against that mutation. - Corrected a false claim in the picker comment: models *can* be re-probed, via the Reidentify endpoints. Re-probing an untagged file just returns `none` again, which is the actual reason the tag cannot be corrected. Mutation-verified: reverting the re-route fails 3 tests, reverting the primary filter to the wide predicate fails the new guard. --- invokeai/app/invocations/wan_lora_loader.py | 44 ++++++++++++------- .../web/src/services/api/types.test.ts | 12 +++++ .../frontend/web/src/services/api/types.ts | 8 ++-- tests/app/invocations/test_wan_lora_loader.py | 42 +++++++++++++++--- .../configs/test_wan_lora_config.py | 6 ++- .../model_manager/load/test_wan_loader.py | 41 ++++++++++------- 6 files changed, 113 insertions(+), 40 deletions(-) diff --git a/invokeai/app/invocations/wan_lora_loader.py b/invokeai/app/invocations/wan_lora_loader.py index 10f7593848e..2e8143654ce 100644 --- a/invokeai/app/invocations/wan_lora_loader.py +++ b/invokeai/app/invocations/wan_lora_loader.py @@ -66,22 +66,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]: @@ -168,7 +180,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): @@ -245,7 +257,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/frontend/web/src/services/api/types.test.ts b/invokeai/frontend/web/src/services/api/types.test.ts index e4a61299fbb..2961c701e94 100644 --- a/invokeai/frontend/web/src/services/api/types.test.ts +++ b/invokeai/frontend/web/src/services/api/types.test.ts @@ -80,6 +80,18 @@ describe('Wan low-noise partner picker', () => { expect(selectPrimaryMainModelOptions([a, b])).toHaveLength(2); }); + 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' }); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index a3dadea4e8a..cadb71889a5 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -673,10 +673,10 @@ const isWanSingleFileLowNoiseMainModelConfig = (config: AnyModelConfigWithExtern * `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, and it is not self-healing — - * `expert` is absent from `ModelRecordChanges` and installed records are never - * re-probed, so anything already stored as `none` stays that way short of a - * delete-and-reinstall, which mints a new model key. + * 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 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/backend/model_manager/configs/test_wan_lora_config.py b/tests/backend/model_manager/configs/test_wan_lora_config.py index 40248c86889..34a06f6fd68 100644 --- a/tests/backend/model_manager/configs/test_wan_lora_config.py +++ b/tests/backend/model_manager/configs/test_wan_lora_config.py @@ -278,6 +278,10 @@ def test_ti2v5b_lora_is_never_tagged_with_an_expert(self): 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: @@ -288,7 +292,7 @@ def test_ti2v5b_lora_is_never_tagged_with_an_expert(self): _overrides(f, stem), ) assert cfg.variant == "5b" - assert cfg.expert is None, f"{stem} was tagged '{cfg.expert}', which routes it nowhere" + 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 diff --git a/tests/backend/model_manager/load/test_wan_loader.py b/tests/backend/model_manager/load/test_wan_loader.py index cc94657f038..6b2b488c3ee 100644 --- a/tests/backend/model_manager/load/test_wan_loader.py +++ b/tests/backend/model_manager/load/test_wan_loader.py @@ -270,25 +270,36 @@ def test_gguf_loader_still_refuses_an_unknown_conditioning_branch() -> None: 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)`. Refusing is deliberate, and - the blast radius is narrower than it 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 genuinely new is the case below — - a whole extra branch, here VACE, whose conversion we deliberately do not ship - (see `_WAN_NATIVE_TO_DIFFUSERS_RENAMES`: "T2V subset; we don't ship VACE / motion / - face-adapter conversion"). Generating with it quietly absent is worse than refusing. + 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="vace_blocks"): - _run_gguf_loader(["vace_blocks.0.after_proj.weight"], native_layout=True) + 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 survive the native-layout rewrite too. The rename - table is blind substring replacement over every key, so it runs across the bundled - VAE/encoder names as well — this pins that they are still recognised and dropped.""" + """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) From 4aadc791dfe7279ae4c9b6a6b7e02978d064a4c7 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 16 Aug 2026 17:31:43 -0400 Subject: [PATCH 14/15] fix(wan): stop a tagged TI2V-5B falling through the gap between both pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two low-severity findings from Pfannkuchensack's second round. - `isWanLowNoisePartnerOption` excludes every TI2V-5B from the partner picker, but `selectPrimaryMainModelOptions` still hid any single-file main tagged `low` once `hasPartner` found another single-file Wan main of the same variant — and it matched `ti2v_5b` against `ti2v_5b` like anything else. A TI2V-5B carrying `expert='low'` satisfied both exclusions at once and was offered nowhere in the linear UI. Reproduced: with two TI2V-5B single-file configs, one tagged `low`, the primary picker returned only the untagged one and the partner picker returned nothing. Such a record is reachable, which is where my previous round's "0 models invisible in both pickers" claim went wrong: that enumeration assumed the TI2V pin made the combination impossible, but the pin is new here. `main`'s `_detect_wan_gguf_expert` applies the tag without consulting the variant, so a 5B named `...-low_noise.gguf` installed before this branch still carries `expert='low'` today. Both predicates now share one `isWanTi2v5bConfig` test, so they cannot drift apart again — the failure mode here was precisely the two disagreeing. - `_correct_inert_low_routing` re-points an explicit `target="low"` as well as an inferred one, which is intended, but the contract shown to the user still described the old behaviour. The `target` field description (rendered in the node editor) and the routing-table comment now say that `low` is applied to the single transformer on TI2V-5B. schema.ts and openapi.json regenerated for the description change; both diffs are that one line. Mutation-verified: dropping the 5B exclusion from the hide test fails the new `never leaves a TI2V-5B invisible in both pickers`. --- invokeai/app/invocations/wan_lora_loader.py | 9 ++++++- invokeai/frontend/web/openapi.json | 2 +- .../frontend/web/src/services/api/schema.ts | 2 +- .../web/src/services/api/types.test.ts | 17 +++++++++++++ .../frontend/web/src/services/api/types.ts | 24 ++++++++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/wan_lora_loader.py b/invokeai/app/invocations/wan_lora_loader.py index 2e8143654ce..6295ea98723 100644 --- a/invokeai/app/invocations/wan_lora_loader.py +++ b/invokeai/app/invocations/wan_lora_loader.py @@ -25,6 +25,11 @@ # - ``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 overrides all four: against a single-transformer TI2V-5B main, a +# routing that would touch only the low-noise list is re-pointed at the primary +# list by ``_correct_inert_low_routing``. That model has no low-noise expert, so +# the alternative is to accept the LoRA and silently do nothing with it. WanLoRATarget = Literal["auto", "both", "high", "low"] @@ -153,7 +158,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, diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index 66cb6448f6a..6a1f2406d75 100644 --- a/invokeai/frontend/web/openapi.json +++ b/invokeai/frontend/web/openapi.json @@ -89103,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", diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 7c5666c7668..61fbe23c3b8 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -39532,7 +39532,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} */ diff --git a/invokeai/frontend/web/src/services/api/types.test.ts b/invokeai/frontend/web/src/services/api/types.test.ts index 2961c701e94..e7c0fbc1d73 100644 --- a/invokeai/frontend/web/src/services/api/types.test.ts +++ b/invokeai/frontend/web/src/services/api/types.test.ts @@ -80,6 +80,23 @@ describe('Wan low-noise partner picker', () => { 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, diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index cadb71889a5..7c16c843699 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -657,13 +657,31 @@ export const isWanSingleFileMainModelConfig = (config: AnyModelConfigWithExterna ); }; +/** 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. */ + * 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) && 'expert' in config && config.expert === 'low'; + return ( + isWanSingleFileMainModelConfig(config) && + !isWanTi2v5bConfig(config) && + 'expert' in config && + config.expert === 'low' + ); }; /** What the Transformer (Low Noise) picker may offer. @@ -688,7 +706,7 @@ export const isWanLowNoisePartnerOption = (config: AnyModelConfigWithExternal): if ('expert' in config && config.expert === 'high') { return false; } - return !('variant' in config && config.variant === 'ti2v_5b'); + return !isWanTi2v5bConfig(config); }; /** Narrows a main-model list to what may be offered as the *primary* main. Every list From 627b998a6c3c645c1ce2f6a5402f0d45b3c0aaac Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 16 Aug 2026 17:41:42 -0400 Subject: [PATCH 15/15] docs(wan): correct the two remaining node-editor strings about 5B LoRA routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up from a fresh-context review of the previous commit, which fixed one user-facing string and left two others contradicting it. Invocation class docstrings are rendered in the workflow editor — they reach `openapi.json` as the schema `description`, which `parseSchema.ts` puts on `template.description` and `InvocationNodeInfoIcon` displays. So hovering the "Apply LoRA - Wan 2.2" node's info icon said a low-only routing "logs a warning" and is inert, while the Target field one row down said it is applied to the transformer. Both classes now describe the correction, including `WanLoRACollectionLoader`, which is the node the linear UI actually emits. Also narrowed the routing-table comment: it claimed the correction "overrides all four" targets, but `_correct_inert_low_routing` returns early unless the routing came out low-only, so `both` and `high` are never touched. Text only — no behaviour change. schema.ts and openapi.json regenerated. --- invokeai/app/invocations/wan_lora_loader.py | 19 +++++++++++++------ invokeai/frontend/web/openapi.json | 4 ++-- .../frontend/web/src/services/api/schema.ts | 9 +++++++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/invokeai/app/invocations/wan_lora_loader.py b/invokeai/app/invocations/wan_lora_loader.py index 6295ea98723..f39bea5065a 100644 --- a/invokeai/app/invocations/wan_lora_loader.py +++ b/invokeai/app/invocations/wan_lora_loader.py @@ -26,10 +26,12 @@ # - ``high``: append only to the primary list (high-noise expert). # - ``low``: append only to the low-noise list (low-noise expert). # -# One exception overrides all four: against a single-transformer TI2V-5B main, a -# routing that would touch only the low-noise list is re-pointed at the primary -# list by ``_correct_inert_low_routing``. That model has no low-noise expert, so -# the alternative is to accept the LoRA and silently do nothing with it. +# 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"] @@ -144,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( @@ -219,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( diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index 6a1f2406d75..163ce53ed01 100644 --- a/invokeai/frontend/web/openapi.json +++ b/invokeai/frontend/web/openapi.json @@ -88956,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": { @@ -89046,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": { diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 61fbe23c3b8..5fe1513f593 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -39449,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: { /** @@ -39497,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: { /**