Feat(model support): ideogram4 support - #9303
Conversation
Switches compel from PyPI 2.1.1 to invoke-ai/compel@main fork which supports transformers 5.x. Bumps transformers floor to 5.9.0. Removes the transformers>=5.1.0 uv override that was only needed to bypass compel 2.1.1's <5.0 constraint. NOTE: compel fork pulls notebook dep (full Jupyter stack); flag to maintainer for cleanup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s 5.x transformers 5.x no longer exposes rope_theta as a top-level attribute on Qwen3Config; the value is stored in the rope_parameters (and rope_scaling) dict instead. Read it from there with a getattr fallback so the inv_freq buffer is computed from the configured base (1e6 / 256) instead of raising AttributeError. Applies to both the safetensors and GGUF Qwen3 encoder paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…whoami huggingface_hub 1.x removed get_token_permission(). HFTokenHelper.get_status() now validates the token via whoami(), which returns user info for a valid token and raises HfHubHTTPError for an invalid one. Preserves the original three-way status: VALID on success, INVALID on HfHubHTTPError (e.g. 401), UNKNOWN on any other error (e.g. network failure). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….9-compel-fork # Conflicts: # invokeai/app/api/routers/model_manager.py # invokeai/app/invocations/sd3_text_encoder.py # invokeai/backend/model_manager/metadata/fetch/huggingface.py # pyproject.toml # uv.lock
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The upstream merge left an unresolved conflict marker in _t5_encode and reintroduced T5TokenizerFast. Keep our v5 assertion (T5Tokenizer only) plus upstream's new t5_device logic, and drop the now-dead T5TokenizerFast monkeypatch in the test (the name no longer exists in the module). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- flux_text_encoder.py: drop unused typing.Union (F401) left by v5 import merge - huggingface.py: ruff format (wrap append(SimpleNamespace(...))) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
transformers 5.6 flattened CLIPTextModel (removed the self.text_model wrapper, hoisted embeddings/encoder/final_layer_norm to the top level). diffusers' single-file checkpoint loader (create_diffusers_clip_model_from_ldm) still assumes the nested layout, so loading SD1.5 .safetensors checkpoints fails on 5.6+ with 'CLIPTextModel object has no attribute text_model' and, once that read is shimmed, 'Cannot copy out of meta tensor' (weights never populate the flattened model). Pin to >=5.5,<5.6 (last pre-flattening release) which keeps both the single-file and from_pretrained paths working. The invoke-ai/compel fork accepts any 5.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore(deps): replace compel fork with official compel 2.4.0 compel 2.4.0 (released 2026-05-30) merges the transformers-5 support that the invoke-ai fork carried (both descend from upstream PR invoke-ai#129), plus the maintainer-reviewed padding rework and added diffusers/T5 smoke coverage. Switch from the git fork to the PyPI release. - pyproject: compel git+main -> compel>=2.4.0,<3 - uv.lock: compel 2.3.1 (git 8f404b45) -> 2.4.0 (pypi) - transformers stays 5.5.4 (satisfies compel >=5,<6 and our <5.6 pin) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
Vendor the Apache-2.0 Ideogram 4 reference model (DiT, FLUX2-style VAE, logit-normal flow-match scheduler, nf4/fp8 quant loading) into invokeai/backend/ideogram4/, plus InvokeAI glue (Qwen3-VL text encoding, packed-input build, dual-branch Euler denoise loop). Register the model: BaseModelType.Ideogram4, Main_Diffusers_Ideogram4_Config (detected via the Ideogram4Pipeline class name in model_index.json), and the Ideogram4DiffusersModel loader that loads both transformers as one Ideogram4TransformerPair submodel plus the Qwen3-VL encoder and VAE. Text-to-image only.
… loading End-to-end text-to-image backend for Ideogram 4, validated through the real session runner. Vendors the Apache-2.0 reference model (DiT, FLUX2-style VAE, logit-normal flow-match scheduler) into invokeai/backend/ideogram4/ with InvokeAI glue. Registers BaseModelType.Ideogram4, Main_Diffusers_Ideogram4_Config, and the Ideogram4DiffusersModel loader (two transformers as one Ideogram4TransformerPair; Qwen3-VL encoder + VAE). Both transformers and the encoder load via InvokeLinearNF4 so they work with the partial-load cache. Adds Ideogram4ConditioningInfo/Field/Output and the model_loader/text_encoder/denoise/l2i invocations. Text-to-image only.
Wires Ideogram 4 into the canvas/generate UI. buildIdeogram4Prompt assembles the structured JSON caption from the global prompt + Canvas Regional Guidance layers (each region → an obj element with a 0–1000 bbox + desc), with raw-JSON passthrough and a plain-text fallback when there are no regions. Adds buildIdeogram4Graph (text-to-image only, no negative prompt) and the enqueue switch. Structured captions use a static string node + a decoy positive-prompt node so the linear batch can't clobber the assembled JSON; plain text uses the real node so dynamic prompts/batching still work. Registers the 'ideogram-4' base (enums, color, names, model picker, grid size 16), a sampler-preset param (V4_QUALITY_48/V4_DEFAULT_20/V4_TURBO_12) replacing the steps/CFG controls, ParamIdeogram4SamplerPreset, and metadata recall. Regenerates schema.ts.
Advanced accordion now shows only Ideogram 4-relevant controls. Adds optional overrides of the sampler preset — steps, guidance scale (overrides the main gw, preserves the preset's polish tail), and schedule shift (mu) — plus a color palette editor that injects style_description.color_palette into the auto-built JSON caption (uppercase #RRGGBB, max 16, ignored for raw-JSON prompts). All are nullable (null = use preset), recallable from metadata, and the irrelevant controls (VAE, CLIP skip, CFG rescale, seamless, color compensation) are hidden for Ideogram 4. Backend denoise gains steps/guidance_scale/mu fields; schema.ts regenerated.
JPPhoto
left a comment
There was a problem hiding this comment.
Some findings from the latest group of changes:
-
invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:114: structured Ideogram prompts returnpositive_prompt_decoyas the graph builder'spositivePrompt, while the actualideogram4_text_encoder.promptinput is connected toideogram4_promptwhose value was assembled before batching atinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:57andinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:100;prepareLinearUIBatch()only injects dynamic prompt expansions into the returned node atinvokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts:74. Trigger: Ideogram 4 generation with a color palette, regional guidance, or raw JSON prompt, plus dynamic prompts or prompt batching. Consequence: the encoded prompt stays at the original static structured caption while metadatapositive_promptchanges through the decoy, so batched images can all use the same text conditioning. Test: build an Ideogram graph withideogram4ColorPalettenon-empty anddynamicPrompts.promptscontaining two distinct prompts, runprepareLinearUIBatch(), and assert the batch data updates the node connected toideogram4_text_encoder.promptor that each structured caption contains the expanded prompt. -
invokeai/backend/model_manager/load/model_loaders/ideogram4.py:178: the non-fp8 text encoder path callsmodel.load_state_dict(sd, strict=False, assign=True)and discards the returned missing and unexpected keys, unlike the fp8 helper which raises on unexpected keys and warns or raises on missing keys. Trigger: an nf4 or unquantized Ideogram text encoder checkpoint with a typo, extra component key, missing tied-weight exception other than the expected one, or mismatched architecture. Consequence: model import can appear successful with ignored checkpoint mismatch and then fail later during encoding or produce invalid conditioning. Test: monkeypatchAutoModel.from_config()with a tiny model and_load_local_state_dict()with one unexpected key and one non-tolerated missing key, then assert_load_text_encoder()raises or explicitly validates only the known tolerated missing keys. -
invokeai/frontend/web/src/features/parameters/components/Core/ParamIdeogram4SamplerPreset.tsx:11: the Ideogram sampler preset dropdown uses hardcoded English option labels (Quality (48 steps),Default (20 steps),Turbo (12 steps)) even though the component already usesuseTranslation()for the field label andinvokeai/frontend/web/public/locales/en.json:1635only adds the genericsamplerPresetlabel. Trigger: running the UI in any non-English locale. Consequence: newly added visible Ideogram controls remain partially untranslated. Test: renderParamIdeogram4SamplerPresetwith a non-English i18n resource that translates the three preset labels and assert the combobox options come from translation keys rather than literal English strings. -
invokeai/app/invocations/ideogram4_denoise.py:41:_effective_guidance_schedule()allowsnum_steps=1throughstepsvalidation atinvokeai/app/invocations/ideogram4_denoise.py:72, butpolish_count = min(num_steps, ...)makesmain_countzero despite the docstring promising at least one polish and one main step. Trigger: user sets the advanced Ideogram step override to1, optionally with a guidance override. Consequence: the generated schedule is only the polish weight, so the user-specified mainguidance_scaleis ignored and the denoise behavior contradicts the invocation contract. Test: unit test_effective_guidance_schedule(PRESETS["V4_QUALITY_48"].guidance_schedule, 48, 1, 12.0)and assert the invocation either rejects one-step overrides or returns behavior that reflects the documented guidance override.
…ntime caption) - Non-fp8 Ideogram 4 text-encoder load now validates the state dict: unexpected keys raise, missing keys warn (mirrors the fp8 helper) instead of silently accepting a partial load. - Guidance schedule: cap the polish tail at num_steps-1 so at least one main step always remains (the guidance_scale override was silently dropped at num_steps=1), and require steps >= 2 (backend field + frontend slider/marks). - Localize the Ideogram sampler-preset option labels via t() with the step count interpolated; add the three preset i18n keys. - Assemble the structured JSON caption at generation time in a new ideogram4_caption_builder node (Python port of buildIdeogram4Caption) instead of at graph-build time. The graph now wires the real prompt node -> caption builder -> text encoder and returns it as positivePrompt, so dynamic prompts / prompt batching vary the encoded caption (the decoy that dropped them is removed). The builder's output is wired to a new declared ideogram4_caption metadata field via an edge, so each batched image records its actual caption. Regenerates schema.ts for the new node + metadata field. Adds tests for caption assembly, the guidance schedule, and the graph wiring.
… filter - Emit a low-res progress preview each denoise step so the forming image is visible during generation, like the other denoise nodes. Ideogram uses a FLUX.2-style 32-channel VAE, so the packed latent is unpatchified/denormalized (get_latent_norm) and run through the FLUX.2 latent->RGB factors — no full VAE decode per step. The denoise loop now hands the callback the packed grid latent. - Document Ideogram 4's built-in content safety filter in models.mdx: it lives in the model weights (not Invoke's NSFW checker, can't be disabled from Invoke) and false-positives on benign prompts; structured JSON prompts trip it less.
…+ caption visibility
The main fix: Ideogram 4's built-in safety filter (baked into the model weights)
false-positives and returns an "Image blocked by safety filter" placeholder for
"degenerate" captions — empirically, an empty `compositional_deconstruction.elements`
list, or a single full-frame [0,0,1000,1000] element whose desc just repeats the
high_level_description. Our assembly produced empty elements whenever the user drew
no regions, so plain prompts were blocked.
- Caption assembly (build_ideogram4_caption):
- Always emit a structured JSON caption; never bare plain text (the filter
false-positives far more on plain text). Raw-JSON pastes still pass through.
- When there are no regions, synthesize one default element describing the whole
scene from the prompt with a *partial* (non-full-frame) bbox [100,100,900,900].
This never yields an empty/degenerate elements list. Verified end-to-end against
the model: the previously-blocked "golden retriever on a skateboard" now renders.
- Metadata: always wire the caption builder's output to the ideogram4_caption
metadata field, so the viewer's "Structured Caption" row shows the exact JSON that
was encoded (not just the raw prompt) for every generation.
- Denoise: emit a low-res progress preview each step (unpatchify + FLUX.2 latent->RGB
factors, since Ideogram uses a FLUX.2-style 32-channel VAE) so the forming image is
visible during generation, like the other denoise nodes.
- Docs: document the model's built-in safety filter in models.mdx (it's not Invoke's
NSFW checker, can't be disabled from Invoke, and false-positives).
Updates the caption/graph tests accordingly (also fixes latent tsc errors in the
graph-builder test's core_metadata / ideogram4_caption comparisons).
JPPhoto
left a comment
There was a problem hiding this comment.
While only the first is truly a merge blocker, I would like to see both addressed:
-
invokeai/frontend/web/src/features/controlLayers/store/types.ts:826:ideogram4Stepsstill accepts1, andIdeogram4Steps.parse()also recallsideogram4_steps: 1atinvokeai/frontend/web/src/features/metadata/parsing.tsx:942, but the backend invocation now requiressteps >= 2atinvokeai/app/invocations/ideogram4_denoise.py:85. A recalled image metadata value or rehydrated client state withideogram4_stepsset to1is dispatched without clamping atinvokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts:105and then passed directly into the graph atinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:95. Consequence: the UI can build/enqueue an Ideogram graph that violates the backend schema instead of rejecting or normalizing the stale value before enqueue. Test: recall Ideogram metadata or rehydrate params withideogram4_steps: 1, build an Ideogram graph, and assert the client rejects/clamps it before enqueue or that parsing refuses the value. -
invokeai/app/invocations/ideogram4_caption.py:16:Ideogram4Region.bboxis declared asOptional[list[int]]with no length or value constraints, while the description and model contract require exactly four normalized coordinates in the0..1000range.build_ideogram4_caption()forwards any provided bbox unchanged into the structured JSON atinvokeai/backend/ideogram4/caption.py:59, so a workflow/API caller can provide values like[0, 0, 1000],[0, 0, 1000, 1000, 7], or negative/out-of-range coordinates. Consequence: the new public caption-builder node can emit malformed structured prompts that the text encoder/model may misinterpret, ignore, or apply to the wrong region. Test: validate or executeIdeogram4CaptionBuilderInvocationwith short, long, and out-of-rangebboxarrays and assert validation fails instead of serializing them into the caption.
Address review on the Ideogram 4 PR: - The backend denoise node requires steps >= 2, but the client still accepted ideogram4_steps = 1 in three places, letting a recalled or rehydrated value build a graph that violates the backend schema. Tighten the zod schema to min(2) with `.catch(null)` (a stale/out-of-range value normalizes to null = use the preset instead of failing the whole persisted slice), normalize dispatched values through the schema in setIdeogram4Steps, and refuse an out-of-range value in the ideogram4_steps metadata recall parser. The slider was already min=2. - Ideogram4Region.bbox was an unconstrained Optional[list[int]], so a workflow/API caller could pass a wrong-length or out-of-range box that the caption builder serialized verbatim into the structured prompt. Add a field validator requiring exactly four coordinates, each in 0..1000. Add tests for both: the region bbox contract (valid/None accepted; short, long, negative, and >1000 rejected) and the ideogram4Steps normalization (valid kept, null kept, stale 1 normalized to null on both dispatch and rehydrate).
JPPhoto
left a comment
There was a problem hiding this comment.
A few more issues...
Blockers
-
invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts:73: the canvas enqueue path routes every Ideogram canvas job tobuildIdeogram4Graph(), butbuildIdeogram4Graph()assertsgenerationMode === 'txt2img'atinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:40. The compositor returnsimg2img,outpaint, orinpaintwhen visible raster/inpaint content exists atinvokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts:711, whilegetRasterLayerWarnings()andgetInpaintMaskWarnings()still return no problems atinvokeai/frontend/web/src/features/controlLayers/store/validators.ts:241andinvokeai/frontend/web/src/features/controlLayers/store/validators.ts:252. Trigger: select an Ideogram 4 model on Canvas with an enabled raster layer or inpaint mask. Consequence: readiness can allow an unsupported canvas mode and enqueue only fails during graph build. Test: create canvas state with an Ideogram model plus an opaque raster layer and with an inpaint mask, then assert canvas readiness reports an unsupported-mode reason before enqueue calls the graph builder. -
invokeai/frontend/web/src/features/queue/store/readiness.ts:562: canvas readiness has model-specific 16/32-pixel bbox checks for Flux, Flux2, CogView4, and Qwen Image throughinvokeai/frontend/web/src/features/queue/store/readiness.ts:762, but no Ideogram 4 branch.getOriginalAndScaledSizesForTextToImage()uses raw bbox dimensions whenscaleMethod === 'none'atinvokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts:132,buildIdeogram4Graph()passes those dimensions intoideogram4_denoiseatinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:116, and the backend requires both dimensions to be multiples of 16 atinvokeai/app/invocations/ideogram4_denoise.py:81. Trigger: Ideogram 4 on Canvas with bbox scaling disabled and a bbox like1025x1024. Consequence: readiness can allow a graph whose denoise node fails backend validation. Test: build canvas readiness for Ideogram 4 withbbox.scaleMethodset tononeand a non-16-multiple width or height, and assert it blocks with the same incompatible-bbox reason used for other 16-grid models. -
invokeai/frontend/web/src/features/controlLayers/store/validators.ts:50:getRegionalGuidanceWarnings()has no Ideogram 4 branch, butcollectIdeogram4PromptInputs()only readsregion.positivePromptand bbox atinvokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Prompt.ts:83. Trigger: an enabled Regional Guidance layer under Ideogram 4 with only a negative prompt, auto-negative, or reference image. Consequence: readiness can allow the layer with no warning, while the Ideogram graph silently drops those inputs and may omit the region entirely. Test: create Ideogram canvas state with regional guidance containing only a negative prompt and/or reference image, assert readiness reports unsupported ignored inputs, and assert graph construction does not silently discard a layer without a warning.
Follow-up PR
-
invokeai/app/invocations/ideogram4_caption.py:35:_validate_bbox()enforces length and0..1000range, but does not enforcey_min <= y_maxandx_min <= x_max;build_ideogram4_caption()forwards the bbox verbatim atinvokeai/backend/ideogram4/caption.py:59. Trigger: a custom workflow/API call supplies[900, 900, 100, 100]. Consequence: the caption builder emits an inverted region bbox that satisfies validation but violates the model prompt contract. Test: instantiateIdeogram4Region(prompt="x", bbox=[900, 900, 100, 100])and assert validation rejects it. -
invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx:66: the advanced accordion badge selector only distinguishes Flux and Flux2, so Ideogram 4 falls into the generic!isFlux2branch at line 77 and can show stale VAE, clip skip, CFG rescale, or seamless badges even though those controls are hidden for Ideogram at lines 116 and 122. Trigger: switch from an SD/SDXL model with VAE/clip skip/rescale/seamless settings to Ideogram 4. Consequence: the collapsed Advanced accordion advertises settings that no longer apply to the selected model. Test: renderAdvancedSettingsAccordionwith an Ideogram model and stale VAE/clipSkip/cfgRescale/seamless params, and assert only Ideogram-relevant badges appear or no stale badges appear. -
invokeai/frontend/web/src/services/api/schema.ts:14045:Ideogram4Region.bboxis generated asnumber[] | nullwith no length or range constraints, even though the backend validator atinvokeai/app/invocations/ideogram4_caption.py:24rejects anything other than exactly four coordinates in0..1000. Trigger: a workflow/API client generated from OpenAPI can construct[0, 0, 1000]or[0, 0, 1000, 1001]without schema/type feedback. Consequence: clients discover the contract only through runtime invocation validation instead of the public schema. Test: regenerate OpenAPI/types after changingbboxto schema-expressible constraints and assertopenapi.jsonincludesminItems,maxItems, and itemminimum/maximumforIdeogram4Region.bbox.
…ion inputs, constrain bbox Address the latest review on the Ideogram 4 PR: - Canvas readiness allowed unsupported generation modes: Ideogram 4 is txt2img-only (buildIdeogram4Graph asserts it), but a raster layer or inpaint mask makes the compositor pick img2img/outpaint/inpaint, failing only at graph build. Warn in getRasterLayerWarnings/getInpaintMaskWarnings for Ideogram 4 (these already flow into canvas readiness reasons), blocking enqueue up front. - Canvas readiness had no Ideogram 4 bbox check; the backend requires multiples of 16. Add the 16-grid check mirroring the other 16-grid models so an off-grid bbox (e.g. 1025x1024) is blocked instead of failing backend validation. - getRegionalGuidanceWarnings had no Ideogram 4 branch, so a region whose only input is a negative prompt, auto-negative, or reference image looked effective while the graph silently drops it. Warn those inputs are unsupported. - Ideogram4Region.bbox now also rejects inverted boxes (y_min <= y_max, x_min <= x_max). - The advanced-settings badge selector lumped Ideogram 4 into the generic branch, showing stale VAE/clip-skip/CFG-rescale/seamless badges for controls that are hidden for Ideogram. Exclude Ideogram 4 from that branch. - Model bbox as a constrained type (exactly 4 ints, each 0..1000) so the OpenAPI schema advertises minItems/maxItems and item minimum/maximum. Add readiness tests (bbox grid, raster/inpaint blocking, empty-layer allowance, regional-guidance negative/reference-image warnings) and bbox ordering-rejection tests.
JPPhoto
left a comment
There was a problem hiding this comment.
Another one I can't test so hopefully you have done so!
Only one minor change if you can do it (or put it in a follow-up), but I am approving this work anyway:
invokeai/backend/model_manager/load/model_loaders/ideogram4.py:168andinvokeai/backend/model_manager/load/model_loaders/ideogram4.py:179:_load_text_encoder()still allows missing text-encoder keys to proceed as warnings after building the model underaccelerate.init_empty_weights(). Any missing non-tied parameter can remain on the meta device, so a bad or mismatched Ideogram text encoder may appear to load and then fail later during device movement or encoding instead of being rejected at load time. Test: monkeypatchAutoModel.from_config()with a tiny meta-built model and_load_local_state_dict()with one missing non-tied parameter for both fp8 and non-fp8 paths, then assert_load_text_encoder()raises or filters only explicitly tolerated tied-weight keys and asserts no meta tensors remain.
…vice _load_text_encoder() builds the encoder under accelerate.init_empty_weights() and previously downgraded missing keys to a warning (both the fp8 path via load_fp8_state_dict(strict=False) and the non-fp8 path). A missing non-tied weight therefore stayed on the meta device, so a bad or mismatched encoder appeared to load and only failed later during device movement or encoding. Add _verify_encoder_fully_materialized(): call tie_weights() to materialize tied weights from their source, then hard-fail if any parameter or buffer remains on the meta device. Wire it into both the fp8 and non-fp8 (incl. bnb-nf4) paths and drop the missing-key warning — genuinely missing non-tied weights are now caught as leftover meta tensors, while tied weights are tolerated. This is a state-based, path-agnostic check. Add tests: passes when fully materialized, raises on a leftover meta tensor from a missing non-tied weight, and tolerates a tied weight resolved by tie_weights().
|
@Pfannkuchensack I've increased the timeout for the pytests to 30 minutes, so the CI tests should now run to completion. |
The Ideogram 4 badge-suppression branch left the wrapped block at its old indentation, failing `pnpm lint:prettier` in frontend-checks. Formatting only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@Pfannkuchensack I also fixed a lint:prettier issue, so as soon as the CI tests complete successfully, this will merge. |
…pport Resolves conflicts with upstream video generation (invoke-ai#9163), Ideogram 4 (invoke-ai#9303), T5 GGUF encoder (invoke-ai#9324) and the Qwen VAE device fix (invoke-ai#9373). Notable resolutions: - qwen_image_latents_to_image: keep the as_qwen_image_vae() reinterpretation but adopt upstream's vae_info.compute_device fix (invoke-ai#9373) - graphBuilderUtils: keep the allow-list isMainModelWithoutUnet predicate, which covers wan_model_loader automatically - generationSettingsVisibility: add 'wan' and 'ideogram-4' to
…upport Resolves conflicts with upstream video generation (invoke-ai#9163) and Ideogram 4 (invoke-ai#9303). Conflict resolutions: - Kept both new encoder types side by side (mistral_encoder for FLUX.2 [dev], wan_t5_encoder for Wan 2.2) across taxonomy, invocation fields, model manager and node types - isNonCommercialMainModelConfig now covers FLUX dev, FLUX.2 Klein 9B, FLUX.2 dev and Ideogram 4 - Regenerated uv.lock from the merged pyproject (adds imageio/imageio-ffmpeg)
FLUX.2 [dev] recorded its Mistral text encoder as an undeclared extra key, relying on the node's `extra='allow'`, while the Klein counterpart `qwen3_encoder` is a proper field. Declare it so it lands in the OpenAPI schema and is typed in the frontend instead of `unknown`. Also bumps the node version, which was left at 2.1.0 across several model integrations that widened the node: `ideogram4_caption` (invoke-ai#9303) and the generation modes for FLUX.2, Anima, Qwen-Image, Ideogram 4, Wan, Krea-2 and Ernie. All changes are additive, so this is a minor bump - saved workflows carrying a core_metadata node now auto-update to the current field set on load rather than silently keeping a stale one.
* feat(flux2): add FLUX.2 [dev] support
Adds end-to-end support for FLUX.2 [dev] alongside the existing Klein
implementation. Dev uses Mistral Small 3.1 (24B) as its sole text encoder
instead of Klein's Qwen3, with joint_attention_dim=15360 and the
guidance-distilled 32B transformer.
Backend
- taxonomy: Flux2VariantType.Dev, ModelType.MistralEncoder,
ModelFormat.MistralEncoder, MistralVariantType
- configs: probe dev via context_in_dim=15360 (main + LoRA); new
mistral_encoder.py with Diffusers / Checkpoint / GGUF configs;
Main_Diffusers_Flux2_Config accepts Flux2Pipeline class name
- loaders: new mistral_encoder.py (AutoModel for Diffusers folder,
MistralModel for single-file + GGUF with llama.cpp key conversion).
Existing Klein transformer loaders are generic enough for dev
- ModelRecordChanges.variant union extended with MistralVariantType
Invocations
- flux2_dev_model_loader, flux2_dev_text_encoder (Mistral chat-template
with FLUX2_DEV_SYSTEM_MESSAGE and layer-stacking 10/20/30),
flux2_dev_lora_loader (+ collection variant)
- MistralEncoderField on model.py; flux2_denoise / flux2_vae_decode /
flux2_vae_encode reused unchanged (already model-agnostic)
Frontend
- types/hooks/selectors for MistralEncoder, isFlux2DevMainModelConfig,
selectFlux2DevDiffusersModels, useMistralEncoderModels
- params slice fields flux2DevVaeModel / flux2DevMistralEncoderModel /
flux2DevSourceModel + reducers, selectIsFlux2Dev / selectIsFlux2Klein
- ParamFlux2DevModelSelect component, wired into AdvancedSettingsAccordion
- buildFLUXGraph dev branch with full txt2img / img2img / inpaint /
outpaint + multi-reference image editing (same flux_kontext +
collect chain as Klein, since Flux2RefImageExtension is model-agnostic)
- addFlux2DevLoRAs helper for dev LoRA wiring
- zModelType / zModelFormat / zFlux2VariantType extended for
mistral_encoder / mistral_small_3_1 / dev
- OpenAPI schema regenerated, TS types updated
Starter models
- FLUX.2 [dev] Diffusers (bf16 + NF4), three GGUFs (Q4/Q6/Q8), Mistral
encoder (bf16 + NF4)
* fix(flux2): wire dev path end-to-end, harden Mistral encoder loader
Follow-up fixes after first end-to-end run with FLUX.2 [dev] GGUF +
Mistral 3.x GGUF + standalone FLUX.2 VAE.
Frontend
- buildFLUXGraph: wire dev model loader's vae into both flux2_denoise
(required for BN statistics / inpaint) and flux2_vae_decode; missing
edge was raising RequiredConnectionException at runtime
- readiness.ts: variant-aware FLUX.2 readiness check — dev requires
flux2DevVaeModel + flux2DevMistralEncoderModel (or a Dev diffusers
source); Klein keeps Qwen3/VAE check. Threads
hasFlux2DevDiffusersSource through generate + canvas tabs and updates
buildGenerateTabArg / buildCanvasTabArg test helpers
- en.json: noFlux2DevVaeModelSelected, noFlux2DevMistralEncoderModelSelected
Mistral encoder loader (GGUF / single-file)
- Fix "Cannot copy out of meta tensor": llama.cpp conversion produced
`model.*` keys but loader instantiated bare MistralModel (no `model.`
prefix). Add _convert_for_bare_mistral_model to strip the prefix and
drop lm_head before load_state_dict
- _materialize_remaining_meta_tensors: after load_state_dict, replace any
still-meta parameters (norms→ones, others→zeros) and buffers so the
cache→VRAM move can't fail on partial state dicts, with a warning
listing what was missing
- llama.cpp converter: map attn_q_norm/attn_k_norm (Mistral 3.x qk-norm
variants), with ordering before attn_q/attn_k to avoid bad rewrites
Tokenizer / processor fallback
- _load_processor_with_offline_fallback walks a list of sources
(black-forest-labs/FLUX.2-dev tokenizer subfolder, then
mistralai/Mistral-Small-3.1-… and 3.2-…), trying AutoProcessor then
AutoTokenizer for each, cache-first then online. Final error spells
out the three workarounds (install Diffusers folder, set HF_ENDPOINT,
pre-cache the tokenizer)
- flux2_dev_text_encoder: try multimodal `[{type, text}]` chat template
first (PixtralProcessor / Mistral3Processor), fall back to plain
string content (AutoTokenizer), then to manual [INST]…[/INST]
Qwen3 encoder probe strictness
- _get_qwen3_variant_from_state_dict and _get_variant_from_config now
return None / raise NotAMatchError for unknown hidden_size instead of
silently defaulting to qwen3_4b. The old fallback meant any llama.cpp
GGUF causal LM (Mistral, Llama, …) was wrongly classified as Qwen3 —
visible when the Mistral 3.x GGUF was identified as a Qwen3-4B encoder
- Checkpoint / GGUF / Diffusers loaders propagate the strictness
* Chore Path fix
* FLUX.2 [dev]: restrict Mistral encoder to 30-layer cow + add recall handlers
Upstream Mistral Small 3.1/3.2 (40 layers) produces off-distribution embeddings
under FLUX.2's static (10, 20, 30) hidden-state extraction. The joint attention
was actually trained against BFL's 30-layer cow-mistral3-small distillation —
both Comfy-Org's safetensors and gguf-org's cow GGUFs ship the same 30-layer
weights, just packaged differently.
- Probing (configs/mistral_encoder.py) now rejects non-cow Mistrals across all
three formats (Diffusers / Checkpoint / GGUF) with a clear error.
- Loader (load/model_loaders/mistral_encoder.py) extracts the embedded Tekken
tokenizer from the `tekken_model` U8 (safetensors) / fp16-per-byte (cow GGUF)
tensor via mistral_common, falling back to the BFL HF tokenizer. Removes the
INVOKEAI_MISTRAL_TOKENIZER_SOURCE env var.
- Starter models: drop upstream Mistral 3.x entries, add Comfy-Org bf16/fp8/fp4
variants alongside the cow GGUFs.
- MistralVariantType: drop Small3_1, keep only Cow.
- pyproject.toml: add mistral-common dependency.
Frontend recall:
- Add Flux2DevVAEModel + Flux2DevMistralEncoderModel handlers, disambiguating
Klein vs dev via presence of `mistral_encoder` / `qwen3_encoder` metadata
fields (both bases are `flux2`).
- Wire both into the Recall Parameters panel (hardcoded list was missing them).
- Add `metadata.mistralEncoder` i18n key + colocated tests.
* feat(flux2-dev): match ComfyUI's Mistral reference + accept 40-layer encoders
After studying ComfyUI's `Flux2Tokenizer` / `Mistral3_24BModel` reference
implementation, align the FLUX.2 [dev] text-encoder path with their setup:
- Probing now accepts both 30-layer (cow distillation) and 40-layer (Mistral
Small 3, BFL canonical / upstream) Mistrals. Re-adds `MistralVariantType.Mistral24B`
alongside `Cow`. All three configs (Diffusers / Checkpoint / GGUF) updated.
- Loaders strip `model.norm` (replace with Identity) when the loaded weights
are the 30-layer cow distillation. Matches Comfy's `final_norm=False` for
the pruned variant; for transformers' `MistralModel` the final RMSNorm is
always built but the cow was trained against the raw post-layer-29 state.
- 40-layer loads now log a clear warning that upstream Mistral 3.1 / 3.2 is
NOT what FLUX.2's joint attention was trained against and recommends the
Comfy-Org bf16/fp8/fp4 or gguf-org cow GGUF variants. BFL's canonical
bundled text_encoder is also 40-layer so we don't hard-reject; the warning
is opt-in self-discipline.
- Text encoder invocation switches from `apply_chat_template(messages, ...)`
to a raw text template `[SYSTEM_PROMPT]{sys}[/SYSTEM_PROMPT][INST]{prompt}[/INST]`
fed straight to the tokenizer — byte-for-byte matches Comfy's
`Flux2Tokenizer.llama_template.format(text)`. System prompt now includes
the literal `\n` between "object" and "attribution" Comfy ships.
- `_TekkenChatTemplateAdapter` renamed to `_TekkenRawTextAdapter` and exposes
a `__call__(text, padding_side='left', ...)` interface that Tekken-encodes
the raw string (BOS=1, no EOS) and left-pads with token id 11. Matches
Comfy's `pad_left=True` / `pad_token=11` settings.
Frontend types extended for the new `mistral3_24b` variant
(zMistralVariantType, MODEL_VARIANT_TO_LONG_NAME, schema.ts).
* fix(ui): remove unused exports flagged by knip on FLUX.2 [dev] branch
Knip reported 6 unused exports. Each was dead code rather than incomplete
wiring, verified against the actual consumers:
- Drop the vestigial `flux2DevSourceModel` param end-to-end (state field,
default, migration, reducer, action, selector, test). The FLUX graph
builder auto-picks the diffusers source itself and never read this param;
no UI set it. Mirrors how the Klein path already works.
- Delete `selectIsFlux2Klein`; the graph builder computes this locally and
only `selectIsFlux2Dev` is consumed.
- Un-export `zMistralVariantType`; used only in the local `zAnyModelVariant`
union, like `zQwenImageVariantType`.
- Delete `selectMistralEncoderModels`; components use the
`useMistralEncoderModels` hook instead.
- Un-export `isFlux2DevMainModelConfig`; used only within types.ts, like its
`isFluxDevMainModelConfig` / `isFlux2Klein9BMainModelConfig` siblings.
* Chore OpenApi
* Chore Ruff
* chore(deps): lock mistral-common for FLUX.2 [dev] Mistral encoder
* fix(flux2): disambiguate dev/Klein VAE recall by model variant
The dev-vs-Klein VAE recall keyed off the presence of a mistral_encoder
metadata field, but that field is only written when a standalone Mistral
encoder is selected. A FLUX.2 [dev] image whose encoder came from a
Diffusers source has a vae field but no mistral_encoder, so its VAE was
silently recalled into the Klein slice.
Resolve the image's own main model and check variant === 'dev' instead —
the same signal the graph builder uses. Add regression coverage for the
mistral_encoder-absent dev case, and add the missing modelManager.flux2Dev*
i18n keys so the [dev] VAE/encoder labels are translatable.
* fix(flux2): pass prompt as text= keyword to Mistral processor
The diffusers FLUX.2-dev text encoder loads a PixtralProcessor, whose
first positional __call__ parameter is `images`, not `text`. Passing the
prompt positionally routed it into `images`, breaking the diffusers
encoder path (only single-file/GGUF encoders, which use a text-first
adapter, had been exercised). Pass text= explicitly.
* fix(flux2): pass prompt as text= keyword to Mistral processor
The diffusers FLUX.2-dev text encoder loads a PixtralProcessor, whose
first positional __call__ parameter is `images`, not `text`. Passing the
prompt positionally raised "Incorrect image source", breaking the
diffusers encoder path entirely. Only single-file/GGUF encoders (text-first
adapter) had been exercised. Verified against transformers 5.5.4.
fix(flux2): emit Tekken special tokens in the embedded-tokenizer adapter
_TekkenRawTextAdapter used mistral_common's raw Tekkenizer.encode, which
runs with SpecialTokenPolicy.IGNORE and BPE-encodes the FLUX.2 markers
([SYSTEM_PROMPT], [/SYSTEM_PROMPT], [INST], [/INST]) as literal text — 54
tokens instead of 36, corrupting the prompt structure fed to FLUX.2 on the
single-file and GGUF paths. Resolve the marker ids from the tokenizer's
special vocab and splice them in; output is now byte-identical to the
reference PixtralProcessor.
* Add FLux2.dev to readme
* fix(flux2-dev): address review — regional guidance, model classification, LoRA guards, encoder probes
- Wire FLUX.2 [dev] regional guidance through addRegions instead of dropping it silently
- Require pipeline layout for Main_Diffusers_Flux2_Config so transformer-only checkouts don't register as broken main models
- Reject Klein<->dev LoRA cross-wiring on both frontend (variant filter) and backend (loaders raise)
- Discriminate non-Mistral GGUFs via vocab-size floor; accept text_encoder.-prefixed encoder layouts at probe time
- Dequantize fp8 checkpoints per-tensor to target dtype and drop lm_head before casting (avoid whole-dict fp32 peak)
- Raise on unexpected Mistral layer count instead of inventing extraction indices
- Fail Klein VAE recall closed when the main model is unresolvable
- Add missing modelManager.mistralEncoder i18n key
- Dedup: single-pass GGUF metadata read, consistent norm materialization, cat-based conditioning, drop redundant t() defaultValues
* Feat: FLUX.2 [dev] review fixes, dedup, and shared-source refactors
Address the PR #9234 review (correctness, install-probe gaps, polish) plus the
deduplication follow-ups.
Correctness
- Wire FLUX.2 [dev] regional guidance through addRegions instead of silently
dropping it (posCondCollect + flux2_dev_text_encoder handling case)
- Require a full pipeline layout (model_index.json / transformer/) for
Main_Diffusers_Flux2_Config so transformer-only checkouts don't register as
broken main models and OSError mid-queue
- Reject Klein<->dev LoRA cross-wiring on both ends: frontend filters LoRAs by
variant in both graph builders; dev/Klein loaders raise instead of warn
- Discriminate non-Mistral GGUFs via a vocab-size floor so Llama-2-13B and
similar 5120-hidden/40-layer LMs no longer install as Mistral encoders
- Accept text_encoder.-prefixed encoder layouts at install probe (matches the
loader's prefix stripping)
- Dequantize fp8 Mistral checkpoints per-tensor to the target dtype and drop
lm_head before casting (avoid a whole-dict fp32 transient that can OOM)
- Raise on an unexpected Mistral layer count instead of inventing extraction
indices that silently degrade output
- Fail Klein VAE recall closed when the image's main model is unresolvable
- Add the missing modelManager.mistralEncoder i18n key
Dedup / single source of truth
- Consolidate the FLUX.2 dimension->variant tables (context/vec/hidden) into a
shared configs/flux2_variant.py used by main.py and lora.py
- Mistral loaders key the final-RMSNorm / warning decision on config.variant
instead of re-deriving from num_hidden_layers==30
- Merge the separate Klein/dev VAE redux slots into one flux2VaeModel
(slice migration v3->v4) and collapse the two metadata VAE handlers into one,
removing the recall-disambiguation
- Parameterize the near-identical dev/Klein canvas graph blocks into one shared
addFlux2Features closure; add dev-path coverage to buildFLUXGraph.test.ts
- Single-pass GGUF metadata read, consistent norm materialization, cat-based
conditioning tensor, and drop redundant t() defaultValues in
ParamFlux2DevModelSelect
Tests: model_identification suite green; frontend parsing / graph /
readiness / modelSelected suites green.
* Fix: bump paramsSlice persist version to 4 for the shared FLUX.2 VAE slot
The v3->v4 migration (Klein/dev VAE slots -> flux2VaeModel) bumped _version
but left zParamsState._version at literal(3) and the initial state at 3, so
migrate()'s final zParamsState.parse rejected with "expected 3". Bump the
schema literal + initial state to 4 and add a v3->v4 migration test.
* Fix: address FLUX.2 [dev] round-2 review (4 blockers + 6 cleanups)
Blockers:
- params migration: seed flux2DevMistralEncoderModel in the v3->v4 step so a
genuine v3 blob passes zParamsState.parse() instead of wiping the whole params
slice on upgrade; rebuild the migration test fixture as a field-accurate v3
object so it actually covers the regression.
- guidance for [dev]: resolve the image's own model in the Guidance metadata
parse gate and exempt variant === 'dev' so guidance is displayed/recalled for
[dev] (still skipped for Klein); render the guidance slider for FLUX.2 [dev].
- source-model variant guard: require variant == Dev where the dev loader
validates its Mistral/VAE source, and reject a [dev] source in the Klein loader
— a mismatched pipeline otherwise fails with an opaque matmul error in denoise.
- tokenizer offline load: drop the dead root-dir fallback + duplicated pre-try
and add a root-directory AutoProcessor step to _load_tokenizer_for_model so
processor files alongside the encoder weights load offline.
Cleanups:
- extract _reinit_inv_freq() with a rope_theta -> rope_parameters/rope_scaling
fallback (fixes a latent AttributeError on pinned transformers 5.5, removes a
verbatim duplicate).
- flux2_dev_lora_collection_loader: replace the base assert with a ValueError
that rejects non-FLUX.2 LoRAs, mirroring the Klein collection loader.
- diffusers Mistral load: drop the never-run vision_tower/multi_modal_projector
(~0.8GB) so they stay out of the cache and VRAM transfers.
- clear flux2DevMistralEncoderModel on base switch and intra-flux2 variant switch.
- pin mistral-common>=1.5.4,<2 (validated against 1.11.6).
- fix contradictory 40-layer docstrings to match the taxonomy/loader story.
* Chore openapi
* fix(ui): bump params persist schema to v5 to resolve the dual-v4 collision
main and this branch both shipped _version 4 with different new keys (PiD
fields vs the flux2 VAE merge + Mistral encoder slot), so a v4 blob written
by either parent would fail zParamsState.parse() after the merge and wipe
the whole params slice. Keep main's v3->v4 step verbatim and move the flux2
slot merge + Mistral seed to a new v4->v5 step with conditional seeding for
both v4 shapes.
Also seed the five Wan component fields in v3->v4: they were added to the
schema without a version bump while releases were still writing v3 blobs,
so a genuine released-build (v6.13.x) v3 blob fails parse() on them today
- same wipe, inherited from main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Chore openapi
* fix(flux2): scope the cross-variant source guard to encoder extraction, widen the tokenizer ladder
The FLUX.2 loaders ran one validator on both the VAE- and the encoder-extraction
call site, so the cross-variant check also rejected VAE-only sourcing. Klein and
[dev] share the same 32-channel AutoencoderKLFlux2 and the linear UI relies on
that -- buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the
VAE is needed, and readiness does not filter by variant. A Klein GGUF main plus a
standalone Qwen3 encoder plus a [dev] pipeline as the only diffusers model
therefore hit a ValueError behind an enabled Invoke button. Split the validator:
format-only for the VAE path, format + variant for the encoder path.
The Mistral tokenizer ladder's local-directory rungs tried AutoProcessor only.
On transformers 5.5.4 that raises OSError for a mistral3 config.json without
preprocessor_config.json -- exactly the BFL-style standalone-encoder layout the
rungs target -- so the ladder fell through to the HF fetch and failed offline.
Both rungs now loop (AutoProcessor, AutoTokenizer), with KeyError in the except
tuple for tekken-only directories.
* Chore openapi
* feat(metadata): declare mistral_encoder on core_metadata, bump to 2.2.0
FLUX.2 [dev] recorded its Mistral text encoder as an undeclared extra key,
relying on the node's `extra='allow'`, while the Klein counterpart
`qwen3_encoder` is a proper field. Declare it so it lands in the OpenAPI
schema and is typed in the frontend instead of `unknown`.
Also bumps the node version, which was left at 2.1.0 across several model
integrations that widened the node: `ideogram4_caption` (#9303) and the
generation modes for FLUX.2, Anima, Qwen-Image, Ideogram 4, Wan, Krea-2 and
Ernie. All changes are additive, so this is a minor bump - saved workflows
carrying a core_metadata node now auto-update to the current field set on
load rather than silently keeping a stale one.
* fix(flux2): make flux2_dev_text_encoder idle-GPU-offloadable
Main's #9428 marked every text-encoder node idle_gpu_offloadable and added a
registry guard asserting the marker on all *_text_encoder nodes; the merge
brought that guard onto this branch where flux2_dev_text_encoder (which
neither parent knew about) fails it.
The flag alone would be wrong: the marker's contract is that the saved
conditioning is CPU-backed, because the borrowed GPU's pool lock is released
the moment the node returns. Move the Mistral embeds to CPU before save
(the placeholder clip_embeds follows their device), add the marker, bump to
1.0.1, and add the same output-device regression test the Klein encoder has.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flux2): stop the Mistral tokenizer ladder from crashing and from silently mis-encoding Tekken
Round-4 review blockers, both reproduced against transformers 5.5.4 with real files before fixing.
1. An AttributeError from a probe rung killed the whole load. `AutoTokenizer.from_pretrained` on a
directory whose `tokenizer_config.json` names a `tokenizer_class` the installed transformers does
not know resolves that class to None and dereferences it without a guard — exactly the layout
`MistralCommonTokenizer.save_pretrained` writes. That is not in `_TOKENIZER_LOAD_ERRORS`, so it
escaped `_try_load_tokenizer_from_dir` and crashed a load the HF rung would have completed. The
probes now catch broadly; the expected-error tuple only selects the log level, so an unexpected
failure is still logged loudly instead of being swallowed.
2. A root `tekken.json` next to `config.json` did not fail in `AutoTokenizer` — it resolved to a
mistral-common-backed tokenizer that BPE-encodes `[SYSTEM_PROMPT]`/`[INST]` as literal text
instead of splicing them as single Tekken ids. The encode "worked" and conditioning was silently
off-distribution. Fixed on two independent paths: the ladder now reads a standalone `tekken.json`
itself, ahead of the transformers probes, and any mistral-common-backed result is re-wrapped in
`_TekkenRawTextAdapter` through its underlying `MistralTokenizer` rather than used as-is. The
vocab is fine — only its `__call__` is wrong — so re-wrapping beats discarding, which would have
traded silent corruption for an offline RuntimeError. Verified: the re-wrapped ids are identical
to the reference adapter's.
Also closes both non-blockers:
- `_validate_encoder_source` in the Klein loader rejected only [dev], so a Klein 9B pipeline passed
as `qwen3_source_model` for a Klein 4B transformer and hit the very matmul error the guard exists
to prevent (the frontend and the standalone-encoder path both enforce the family match; the
workflow editor's source field was the only way in). It is now an allowlist keyed on a shared
`_KLEIN_TO_QWEN3_VARIANT` map — mirroring the frontend's `KLEIN_TO_QWEN3_VARIANT_MAP` — and checks
the source's Qwen3 family against the main model, so a future third FLUX.2 variant fails closed on
the Klein side too, not just on [dev]. `_validate_qwen3_encoder_variant` shares that map and now
uses `getattr` instead of `hasattr`, which turned a None variant into an AttributeError in the
error path rather than the intended ValueError.
- The [dev] loader's `_validate_diffusers_format` docstring claimed the linear UI relies on the
permissive VAE path. That holds for Klein, but the [dev] builder sources from dev-only pipelines
and readiness gates on one, so there the cross-variant VAE case is reachable through the workflow
editor only. The justification now states what actually holds: the 32-channel AutoencoderKLFlux2
is shared (the repo ships the Klein-sourced `flux2_vae` as a dependency of every [dev] GGUF
starter), and `mistral_source_model` is not variant-filtered in the editor.
Tests: a structurally valid Tekken fixture, so the ladder exercises the success path rather than
only the raise path the previous fake produced; regression tests for both blockers on the directory
and HF rungs; Klein-family coverage including same-family acceptance and the standalone-encoder
guard's negative path, which had no coverage at all. All new tests mutation-verified — reverting the
broad catch, the tekken rung, the re-wrap, the family check, or the allowlist each fails at least
one.
tests/app + tests/backend/model_manager: 3010 passed. The 9 failures are the pre-existing
network-dependent ones in test_model_install / test_load_api / test_download_queue.
---------
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds first-class Ideogram 4 (text-to-image) support to InvokeAI — a new open-weight 9.3B single-stream DiT with a Qwen3-VL-8B text encoder and flow-matching sampler.
The defining trait of this model is that it is trained on a structured JSON prompt that describes the scene as a list of regions, each with a bounding box (
[y_min, x_min, y_max, x_max], normalized 0–1000, origin top-left) and a text description. To stay on the model's trained distribution, InvokeAI always feeds it structured JSON — even a plain prompt is wrapped into a one-element caption (see the safety-filter note below).The headline feature here is that this JSON is auto-assembled from the existing Canvas Regional Guidance layers: the global prompt becomes the overall description, and each enabled region contributes one element (its drawn rect → bbox, its prompt → description). Users can also paste raw JSON to drive the model directly (passed through unchanged). Assembly happens at generation time in a dedicated
ideogram4_caption_buildernode, so dynamic prompts / prompt batching that vary the global prompt are folded into the encoded caption, and each generated image records the exact JSON it was given.compositional_deconstruction.elementslist, or a single full-frame[0,0,1000,1000]element whosedescjust repeats the high-level description, returns an "Image blocked by safety filter" placeholder. The caption assembly is built to avoid this: it never emits bare plain text, and when no regions are drawn it synthesizes one element describing the whole scene with a partial bbox[100,100,900,900]. This is documented inmodels.mdx.Why the backend only ever sees one prompt string (no mask conditioning): Ideogram 4 does not use spatial attention masks for regions (unlike FLUX/SDXL/Z-Image regional guidance) — the region boxes are encoded as text inside the single JSON string fed to Qwen3-VL. So no mask-conditioning code is touched; the "regional" feature is purely caption assembly.
How
Backend (
invokeai/backend/ideogram4/, vendored from the Apache-2.0 reference, copyright headers retained):modeling_ideogram4.py), FLUX2-style KL VAE (autoencoder.py+latent_norm.py), logit-normal flow-match scheduler + presets (scheduler.py,sampler_configs.py), nf4/fp8 quantized loading, and InvokeAI-sidedenoise.py/text_encoding.py/sampling_utils.py/caption.py.[text]+[image]tokens, negative runs the unconditional transformer over image-only tokens with zeroed LLM features (v = gw·pos + (1−gw)·neg). ⇒ no negative prompt. Atransformer_pair.pywrapper keeps both transformers co-resident through the loop so the cache doesn't swap them every step (nf4 ≈ 10 GB resident during denoise; fits 24 GB). The guidance schedule caps its polish tail atnum_steps-1so at least one main (guided) step always remains, and requiressteps >= 2.BaseModelType.Ideogram4, a Qwen3-VL text-encoder type, config detector + diffusers config, and a loader mirroring Z-Image.ideogram4_model_loader,ideogram4_text_encoder,ideogram4_caption_builder(Python port of the JSON assembly),ideogram4_denoise,ideogram4_latents_to_image; newIdeogram4ConditioningInfo+ field/output;ideogram4_txt2imggeneration mode; a declaredideogram4_captionmetadata field.Frontend:
buildIdeogram4Prompt.ts—collectIdeogram4PromptInputs: gathers the raw inputs (global prompt, enabled regions →{prompt, bbox}clamped/rounded to 0–1000, color palette) for the caption builder node. The JSON assembly itself now lives in the backend node (single source of truth, Python-tested); raw-JSON passthrough and stable key order are handled there.buildIdeogram4Graph.ts— text2img-only graph builder + enqueue wiring:prompt (string) → ideogram4_caption_builder → text_encoder. The real prompt node carries the global prompt, so the linear-UI batch injector (dynamic prompts / prompt batching) writes expansions into it and they flow through the caption builder into the encoder — the earlier "decoy string node" hack is removed. The builder's output is wired to theideogram4_captionmetadata field via an edge, so each batched image records its actual caption.Dependencies: bumps
transformersto>=5.5,<5.6(Qwen3-VL landed in 4.57; the encoder needs it) andcompelto>=2.4.0,<3, with the necessary adaptations to the FLUX / Z-Image loaders, the safety checker, the HF metadata fetcher andmodel_util.Out of scope (v1):
txt2img). No latent/image init path exists in this PR.Related Issues / Discussions
transformers5.x bump tracked in PR feat - Migrate to Transformers 5.5.4 #9248 — this branch carries the same bump (>=5.5,<5.6) plus the cross-model adaptations it requires.QA Instructions
Requires the gated weights (
ideogram-ai/ideogram-4-nf4— nf4 is the 24 GB path, CUDA/bitsandbytes only) plus the Qwen3-VL encoder + VAE sub-dependencies.ideogram4_captionrow shows the assembled JSON with the correct key order andelements[*].bbox(0–1000,[y_min, x_min, y_max, x_max]) matching where you drew the boxes, and that element placement in the image roughly matches.ideogram4_captionmetadata: the prompt was wrapped into a one-element caption with a partial bbox[100,100,900,900]— this is the safety-filter workaround. Dynamic prompts / prompt batching still expand (they now flow through the real prompt node → caption builder).style_description.color_palette(auto-build mode only — ignored for raw JSON).Frontend gates (from
invokeai/frontend/web/):pnpm lintandpnpm test:no-watch(includes the caption/graph tests).tscclean. Backend:tests/backend/ideogram4/test_caption.pycovers the assembly (raw-JSON passthrough, region mapping, and the no-region default element).Merge Plan
transformersto>=5.5,<5.6andcompelto>=2.4.0,<3, which touches many models (FLUX, Z-Image, safety checker, HF metadata fetch). Coordinate with / sequence after PR feat - Migrate to Transformers 5.5.4 #9248 (the transformers 5.x bump) to avoid a double-bump conflict, and time it to not collide with a pending release. Broad regression QA across existing model types is warranted, not just Ideogram 4.starter_models.pyentries, and the optional local Magic Prompt node (blocked on the upstream system-prompts PR feat: add System Prompts library for Expand Prompt button #9152, whosemigration_32collides withmainand must be renumbered to 33 first).Checklist
test_caption.py) + frontend caption/graph testsmodels.mdxdocuments the model's built-in safety filterWhat's Newcopy (if doing a release after this PR)