⚠️ Behavior Changes
- A metadata sidecar no longer stacks its LoRAs on top of the command line's:
--config-from-metadatarestoredlora_paths/lora_scalesby concatenation, while every other key it restores defers to explicitly given arguments. The sidecar's adapters therefore always applied and no spelling could replace them —--config-from-metadata prior.metadata.json --lora mine.safetensors 0.9ran both. Since only one of the two lists usually came from the command line, the counts then disagreed andresolve_scalespadded or truncated the difference behind a warning:--lora-paths cli.safetensorsleft the new adapter with no scale of its own, so the sidecar's went to the sidecar's adapter and the new one was padded to 1.0, and--lora-scales 0.8on a one-adapter sidecar generated at the sidecar's scale with the 0.8 discarded.--lora/--lora-pathsnow replaces the sidecar's adapters rather than adding to them,--lora-pathswith no values re-runs without any, and--lora-scaleson its own keeps the sidecar's adapters and applies the new strengths. Anything scripted around the stacking behaviour has to name every adapter it wants. Sidecars also record each adapter's resolved absolute path, so one carried between machines failed on a path that was nowhere on the command line — and failed even when the user named their own copy, since the dead path was merged in and resolved anyway; that now works, and when there is no replacement the error names the sidecar it came from and both ways out. (#577, #618) --guidanceand--schedulernow take effect onmflux-upscale-controlnet: both were registered by the parser and then never passed togenerate_image, so whatever you typed was replaced byFlux1Controlnet's own defaults. They are forwarded now, which means an invocation that passed either flag produces a different image than it did before. Omitting--guidancestill gives the 4.0 the upscaler has always run at, rather thanui_defaults.GUIDANCE_SCALE(3.5), so nothing changes for a command that never set it. (#577)- Abbreviated long options are rejected: every mflux CLI now requires flags to be written in full, so
--promno longer stands in for--prompt. Option provision is detected by scanningargv, which cannot see abbreviations, and an abbreviation that is unambiguous today starts resolving elsewhere the moment a new flag is added. Anything scripted with a shortened flag needs the full spelling. (#499)
✨ Improvements
- twine removed from mflux's dependencies: it only existed to support the release script, but every
pip install mfluxpulled in twine plus ~24 transitive packages (cryptography,keyring,rich,readme-renderer, ...). The release script's token-mode upload now shells out touv publish—--check-urlreplicates twine'sskip_existing, the token travels viaUV_PUBLISH_TOKENso it never appears in logged commands, and the transient-retry semantics are unchanged — whiletwine check dist/*survives viauv tool run twinein an ephemeral environment. CI's trusted-publishing path is untouched (pypa/gh-action-pypi-publishstill uploads). Also fixes the script invoking uv aspython -m uv, which requires the uv PyPI package that the release environment does not install; it now calls theuvbinary from PATH. Adds ajust releaserecipe that triggers the release workflow viagh. (#644) - Type checking migrated from mypy to astral's ty (pinned
ty==0.0.72in dev deps, pre-commit and CI): full-project checks in ~0.4s vs mypy's 2–9s. Pre-existing violations are baselined as ignored rules in[tool.ty.rules](with counts) to be re-enabled and fixed piecemeal; new code must pass all non-baselined rules. Addsjust typecheck. (#597) - Quieter LoRA loading: stacking a second adapter printed a
🔀line for every layer it fused — hundreds of lines between the file's own🔧line and its✅ Applied to N layerssummary, which already says how many landed. Those three per-layer messages are nowlogger.debug, like the resolution rules' tracing. (#577) - Silently dropped CLI options now warn: a command that accepts an option it cannot honour says so at parse time instead of ignoring it, for example
--guidanceon a guidance-distilled model. Addsmflux-capabilities, a machine-readable dump of every image-generating command with its options, defaults, types and the status of each option (honored, ignored, conditional or rejected), in JSON, YAML or Markdown. (#499)
✨ Improvements
--vae-tilingand--vae-tile-sizeflags: Restore user-facing control over tiled VAE decoding, decoupled from--low-ram(previously the only way to enable it).--vae-tilingenables tiled decode with the default 512px tiles;--vae-tile-size 256shrinks the tiles to further reduce peak decode memory and implies--vae-tiling. Both compose with--low-ram, whose implicit tiling defaults they override. The original--vae-tiling/--vae-tiling-splitflags were removed in the Z-Image refactor (#284); this restores the capability on top of the generalizedVAETiler. (#311, #407)
🐛 Bug Fixes
- Saved FLUX ControlNet weights were ignored on load:
FluxInitializer.init_controlnetalways fetched the ControlNet component from the remote repo, never consulting themodel_pathit honors for every other component. Reloading a checkpoint written bymflux-savesilently swapped its ControlNet weights for the hub's — a-qsave came back with that component unquantized, a fine-tune was discarded, and an otherwise complete local checkpoint still wanted the network. Only reachable since #607 mademflux-saveactually write the ControlNet component. Amodel_pathholdingtransformer_controlnet/*.safetensorsnow loads it from disk, the branchZImageInitializeralready had; a builtin name or a directory without that component keeps the hub path. (#610, #642) - A repo id shared by two registry entries resolved to the ControlNet variant: several roots share a
model_namewith a ControlNet derivative, and the resolver returned the first match inpriorityorder — which put the Z-Image ControlNet ahead of plain turbo, so--model Tongyi-MAI/Z-Image-Turbobuilt a ControlNet config in every resolver caller (the generate CLIs andmflux-savedispatch alike). Ties on a bare repo id now prefer the base entry, on the rule that a derived variant is always addressed by its own key or alias; keys and aliases resolve exactly as before. (#609, #641) - Failed LoRAs were reported as applied: every step between resolving an adapter and folding it into the weights had a path that printed a warning and carried on, and the run then announced
✅ All LoRA weights applied successfully. An adapter that could not be resolved was dropped from the list — generating from the untouched base model, and, with several adapters, shifting the scales onto the wrong ones asresolve_scalespadded the shortened list back to length. A file that was missing or unreadable at load time printed❌and returned. A target layer the model does not have, or one that turned out not to be linear, printed❌per layer and left the adapter applied to only part of the model. And baking caught every exception — including a base-weight/delta shape mismatch — printed⚠️and returned the layer unbaked, somflux-savewrote a checkpoint advertised as merged that held the original base weights, and ordinary generation (which bakes by default) quietly ran without the adapter. All of these now raise. Bake errors name the layer they failed at (blocks.0.attn.wq), andmflux-savebakes every component before it writes a single file, so a mismatch aborts instead of leaving half a checkpoint on disk. Anything scripted around a mistyped or missing adapter path now exits instead of generating an image without it. (#577) - boogu and FIBO accepted
--loraand then discarded it:mflux-generate-boogu,mflux-generate-fiboandmflux-generate-fibo-editall declared the LoRA flags, so the parser resolved every adapter — downloading it from HuggingFace when the name was a repo id — and the initializers then dropped it on the floor, whilemflux-capabilitiesreported"lora": trueand nothing warned at any point. mflux implements no LoRA mapping for either architecture, so the flags are gone rather than ignored: the three CLIs no longer take them, matchingmflux-generate-lens, and--helpand the capabilities dump now agree with the code.BooguInitializer/FIBOInitializerand theBooguImage/FIBO/FIBOEditconstructors lost their unusedlora_paths/lora_scales/bake_loraparameters so the Python API stops accepting an adapter it ignores, andmflux-save --model boogu --lora ..., which filtered the same kwargs away by signature and wrote an unmodified checkpoint, now exits with a message. (#577, #615) --modelsilently ignored on 4 CLIs:mflux-generate-krea2,mflux-generate-z-image-turbo,mflux-generate-ernie-imageandmflux-generate-ernie-image-turboaccepted--modeland then constructed a hard-coded config, so--model devsilently ran Krea-2-Turbo (etc.) whilemflux-capabilitiesreported the option as honored. Each now validates--modelthrough a sharedConfigResolution.resolve_restricted(the lens pattern): a builtin registry name must be an alias of the CLI's own model, anything else errors. Local checkpoint paths and HuggingFace repo ids — whichparse_argsroutes throughmodel_path— are untouched: they keep the CLI's own config and load weights from the path, as they always have. Validation compares registry entries by identity rather thanmodel_name, so--model z-image-controlnet— whose entry shares theTongyi-MAI/Z-Image-Turborepo id — is correctly rejected by the plain turbo CLI;lens_generate's inline check, which comparedmodel_name, now uses the same helper. (#577, #614)mflux-completionscrashed on every invocation: themflux-upscale-controlnetparser added its LoRA arguments twice, so every build path died withargparse.ArgumentError: conflicting option string: --lora-style(exit 1) before writing a single completion. A regression test now builds the parser for every command mflux-completions knows about. (#577, #613)- Qwen Image Edit conditioning resolution: Encode the transformer's image-conditioning latents at the edit target resolution, not the vision-language conditioning resolution (≈384px by area), preventing patchy/tiled artifacts in edit outputs. This changes edit output at every quantization level, not only
-q 4. (#420) - Qwen Image Edit default dimensions: Preserve the first input image dimensions by default; explicit
--width/--heightvalues or scale factors such as2xstill opt into resizing. (#420) - Qwen Image Edit CLI scheduler: Forward
--schedulerto the Qwen edit pipeline (previously ignored). (#420) - Inferred model configs lost their settings:
ConfigResolutionrebuilt inferred configs field by field, so anything resolved from a local path or variant name (/models/qwen-image-edit-q4) silently fell back to generic defaults. It now carries every field and rewrites only identity. This restores the Qwen edit sigma schedule (0.9/8192/0.02rather than1.15/4096/None), ERNIE's LoRA training guidance, andsupports_kv_cacheonflux2-klein-9b-kv. (#420) - Default
--stepsignored the model on 10 CLIs: every image CLI that picks its model inmain()rather than at parse time — boogu, krea2, lens, z-image, z-image-turbo, flux2, flux2-edit, qwen, qwen-edit, ideogram4 — resolved--stepsagainst aNonemodel and silently inherited FLUX.1-dev's 25. That is 6x the work on a 4-step distillation (boogu, lens, klein) and half the work on a 50-step base model (z-image). The step table is now keyed by canonical model rather than by alias and resolved throughAVAILABLE_MODELS, so--model klein-4band--model flux2-klein-4bno longer disagree, and a CLI declares its own model to the parser viaadd_model_arguments(default_model=...). Krea 2'sDEFAULT_STEPS = 8fallback, unreachable because the parser always supplied 25, is gone. Explicit--stepsis unaffected. (#580) --model <name>failed for most built-in models: the names the CLI accepted were a hand-maintained list (ui_defaults.MODEL_CHOICES) that had drifted fromAVAILABLE_MODELS, and anything missing from it was treated as a local checkpoint directory —mflux-generate-lens --model lens-turbodied withModel not found: 'lens-turbo'on the canonical name of the only model that CLI runs. 16 registry keys and roughly 40 aliases were affected, among themkrea-2-raw,qwen-image, and everyklein-*,boogu-*,zimage*andfiboedit*spelling. The accepted names are now derived from the registry, so a new model or alias needs no second edit.--base-modellikewise dropped its stale argparsechoices=list, which rejected valid names such asqwen-image; it is now validated againstConfigResolution's own list of root models, and the check also covers abase_modelrestored from a metadata sidecar. (#577)mflux-savewrote checkpoints under the wrong architecture: the model class was chosen by a substring chain over the raw--modelstring ending inelse: Flux1, so every name it had not been taught was saved as a FLUX.1 model —lens,lens-turbo,klein-4b/9b/9b-kvandseedvr2*all were — whilefibo-edit*was saved by the txt2img FIBO class andz-image-controlnet*by plainZImage. Nothing raised; the mismatch surfaced later as an unloadable checkpoint. Dispatch now goes through the registry (ConfigResolution.resolve_key), so canonical keys, aliases, HuggingFace repo ids and--base-modelspellings all land on the class that owns those weights, an unrecognisable name exits with a message instead of defaulting to Flux1, and the two models with no save path (Lens, SeedVR2) say so. Every rejection also lists the modelsmflux-savecan write, which--helpdoes not show — it lists models that cannot be saved. A drift test fails if a model is added toAVAILABLE_MODELSwithout a save class. Two consequences worth noting:--model dev-controlnet-canny/dev-controlnet-upscaler/schnell-controlnet-cannynow save throughFlux1Controlnet(ControlNet weights included) rather than as a bare Flux.1, andmflux-save --model booguno longer dies with aTypeErroronbake_lora, whichBooguImagedoes not accept. (#607)- ERNIE multi-frame latents:
pack_latentsindexed the VAE's temporal axis away, so anything but a single frame was silently discarded; it now fails loudly. (#577) ernie-image-turboguidance contract: the model declaredsupports_guidance: truewhile its CLI exits on any guidance other than 1.0, contradicting itself in themflux-capabilitiesdump. (#578)- Partial Krea 2 Turbo downloads passed as complete: a cached snapshot was checked against its download patterns only when the model needed no subdirectory. Krea 2 Turbo keeps its transformer at the repo root as
turbo.safetensors, so a download that fetchedvae/andtext_encoder/and then stopped reported itself complete. Hugging Face's repair download never ran, and the load died later withMissing specified weight files in <snapshot>: ['turbo.safetensors'], which reads like a corrupt cache rather than an interrupted transfer. Root-level*.safetensorspatterns are now checked in the subdirectory branch too. (#577, #593) - Saved checkpoints loaded whatever was in the directory:
mflux-savewrites amodel.safetensors.index.jsonnaming every shard, and the reload globbed the directory instead of reading it. Saving-q 4over an existing-q 8checkpoint in the same folder left the q8 tail behind, and those stale tensors overwrote the ones just written while the metadata still reported 4. A checkpoint missing a shard the index names loaded the rest and came up short. The index now decides which shards get read, and a named shard that is not on disk raises with the filenames. Checkpoints with no index, or with a damaged one, keep loading from the directory. (#577) - Metadata sidecars did not restore dimensions, the negative prompt or init images: every run embeds
height,width,negative_promptandimage_paths, but--config-from-metadataread none of them back, so a sidecar-only rerun of a 1536x768 generation came out at the 1024x1024 default with the negative prompt dropped — a different image on any CFG model, not just a differently sized one. The edit CLIs could not rerun a sidecar at all:--image-pathswasrequired=True, which argparse enforces before the restore block runs, somflux-generate-qwen-edit --config-from-metadata prior.metadata.jsonexited 2 demanding images the sidecar was carrying. All four keys are restored now, each deferring to the command line option by option, and--image-pathsis required after the restore rather than during parsing. A sidecar's init image that does not exist on this machine now errors at parse time naming the sidecar, instead of loading the whole model first and dying on a bareFileNotFoundError. (#577, #634) - The upscale commands were missing from
mflux-capabilities:COMMAND_PREFIXEScoveredmflux-generate*andmflux-concept*only, so neithermflux-upscale-controlnetnormflux-upscale-seedvr2appeared in the dump — which is how the--metadatabug above went unnoticed by the contract checks that exist to catch exactly that. Both are now published with full coverage, which meant adopting thebuild_parser()convention and declaring what their hardcoded configs cannot honour:--base-modelis ignored by both, and onmflux-upscale-controlnet--negative-promptis ignored while--modelis conditional (a path or repo id loads weights, a built-in model name does nothing, since the config is always dev-controlnet-upscaler). Both commands now warn at parse time when an ignored option is passed. (#577) mflux-upscale-seedvr2 --metadatanever wrote the sidecar: the CLI accepted the flag and then saved without forwarding it, so the upscale ran to completion and no JSON appeared. Its siblingmflux-upscale-controlnetpassesexport_json_metadataon the equivalent line. (#577)- Ideogram 4 quantization:
mflux-save -qnow actually quantizes Ideogram 4. Every weight-bearing linear in the model is anFp8Linear, which defined noto_quantizedand whose components were markedskip_quantization, so-q 4wrote an FP8 checkpoint stampedquantization_level: 4and only the VAE was touched. AddsFp8Linear.to_quantized, drops the skip flags, derives bits and group size from the stored shapes when rebuilding a saved checkpoint instead of assuming q8/group-64, and rebuilds quantized embeddings as embeddings rather than linears.-q 8is 26 GB and-q 4is 14 GB, both visually indistinguishable from FP8. (#559)
📝 Documentation
- Ideogram 4 gated weights: Document the
HF_TOKEN/hf auth loginstep alongside the existing note that access must be approved on the model card — authenticating is the half people miss, and without it an approved account still fails with a bare401/403. Adds a quantization section coveringmflux-save -qand loading a saved checkpoint with--model-path. (#559)
🧰 DX & Maintenance
- Dev workflow: Makefile replaced by justfile:
make <target>is gone; usejust <target>instead (just install,just lint,just format,just check,just test-fast, …). Running barejust(orjust --list) shows all recipes with descriptions. Semantics follow the post-#576 Makefile:uv syncinstalls and ruff is pinned from thepyproject.tomldev dependency. The CI lint job also lints the justfile itself viajust --fmt --check, using the pinnedjust-setupcomposite action. Requiresjust≥ 1.50 locally. (#590)