Skip to content

Feat(model support): ideogram4 support - #9303

Merged
lstein merged 46 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/ideogram4-support
Jul 27, 2026
Merged

Feat(model support): ideogram4 support#9303
lstein merged 46 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/ideogram4-support

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented Jun 25, 2026

Copy link
Copy Markdown
Member

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_builder node, 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.

⚠️ Built-in safety filter (baked into the weights): Ideogram 4 ships its own content filter inside the model — it is not Invoke's NSFW checker and cannot be disabled from Invoke. Empirically it also false-positives on "degenerate" captions: an empty compositional_deconstruction.elements list, or a single full-frame [0,0,1000,1000] element whose desc just 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 in models.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):

  • DiT (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-side denoise.py / text_encoding.py / sampling_utils.py / caption.py.
  • Dual-branch asymmetric CFG: positive runs the conditional transformer over [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. A transformer_pair.py wrapper 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 at num_steps-1 so at least one main (guided) step always remains, and requires steps >= 2.
  • Step previews: the denoise loop emits a low-res progress image 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.
  • Non-fp8 text-encoder loading validates the state dict (unexpected keys raise, missing keys warn) instead of silently accepting a partial load.
  • Model-manager registration: new BaseModelType.Ideogram4, a Qwen3-VL text-encoder type, config detector + diffusers config, and a loader mirroring Z-Image.
  • Five invocations: ideogram4_model_loader, ideogram4_text_encoder, ideogram4_caption_builder (Python port of the JSON assembly), ideogram4_denoise, ideogram4_latents_to_image; new Ideogram4ConditioningInfo + field/output; ideogram4_txt2img generation mode; a declared ideogram4_caption metadata field.

Frontend:

  • buildIdeogram4Prompt.tscollectIdeogram4PromptInputs: 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 the ideogram4_caption metadata field via an edge, so each batched image records its actual caption.
  • Params + UI: a Sampler Preset combobox (Quality 48 / Default 20 / Turbo 12, localized) as the primary control, plus Advanced overrides that actually apply to this model — Steps (min 2), Guidance Scale, Schedule Shift (mu) and a Color Palette picker. The irrelevant Advanced controls (VAE, CLIP Skip, CFG Rescale, Seamless, Color Compensation) are hidden for Ideogram 4.
  • Metadata recall for all of the above (guarded to only recall onto an Ideogram 4 model).

Dependencies: bumps transformers to >=5.5,<5.6 (Qwen3-VL landed in 4.57; the encoder needs it) and compel to >=2.4.0,<3, with the necessary adaptations to the FLUX / Z-Image loaders, the safety checker, the HF metadata fetcher and model_util.

Out of scope (v1):

  • img2img / inpaint / outpaint are not supported — Ideogram 4 is text-to-image only here (the graph asserts txt2img). No latent/image init path exists in this PR.
  • ControlNet / IP-Adapter / LoRA.
  • The optional local "Magic Prompt" plain-text→JSON expander (parked — see Merge Plan).

Related Issues / Discussions

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.

  1. Install & select an Ideogram 4 model; open the Canvas/Generate tab. Confirm the model shows under its own group, dimensions default to 1024×1024 (multiples of 16), and the Generation settings show the Sampler Preset control instead of Scheduler/CFG.
  2. Regions → JSON: type an overall description, add 1–2 Regional Guidance layers each with a prompt + a drawn box, and Invoke. In the result's metadata, confirm the ideogram4_caption row shows the assembled JSON with the correct key order and elements[*].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.
  3. Raw-JSON passthrough: paste a hand-written JSON object into the prompt box → it is sent unchanged (no region wrapping, palette ignored).
  4. Plain text (now wrapped to JSON): with no regions and a plain prompt, Invoke and confirm it renders (is not blocked). Check the ideogram4_caption metadata: 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).
  5. Sampler presets: Quality / Default / Turbo produce the expected step counts (min 2).
  6. Advanced overrides: Steps / Guidance Scale / mu show the active preset's value as the "auto" default and can be overridden + reset; Color Palette swatches inject style_description.color_palette (auto-build mode only — ignored for raw JSON).
  7. Step previews: during generation, confirm a (low-res) progress image is shown each step.
  8. Metadata recall: load an Ideogram 4 image and recall — preset, overrides and palette restore (and are guarded to only recall onto an Ideogram 4 model).

Frontend gates (from invokeai/frontend/web/): pnpm lint and pnpm test:no-watch (includes the caption/graph tests). tsc clean. Backend: tests/backend/ideogram4/test_caption.py covers the assembly (raw-JSON passthrough, region mapping, and the no-region default element).

Merge Plan

  • Large PR + dependency bump. This raises transformers to >=5.5,<5.6 and compel to >=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.
  • Follow-ups (not in this PR): GGUF loader for the custom DiT (more VRAM headroom), starter_models.py entries, and the optional local Magic Prompt node (blocked on the upstream system-prompts PR feat: add System Prompts library for Expand Prompt button #9152, whose migration_32 collides with main and must be renumbered to 33 first).

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable) — backend unit tests for the JSON caption assembly (test_caption.py) + frontend caption/graph tests
  • ❗Changes to a redux slice have a corresponding migration — new params fields use zod defaults; confirm if a migration is needed
  • Documentation added / updated (if applicable)models.mdx documents the model's built-in safety filter
  • Updated What's New copy (if doing a release after this PR)

Your Name and others added 20 commits February 6, 2026 19:58
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.
@github-actions github-actions Bot added api python PRs that change python files Root invocations PRs that change invocations backend PRs that change backend files frontend PRs that change frontend files labels Jun 25, 2026
@JPPhoto
JPPhoto self-requested a review July 21, 2026 20:38

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some findings from the latest group of changes:

  • invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:114: structured Ideogram prompts return positive_prompt_decoy as the graph builder's positivePrompt, while the actual ideogram4_text_encoder.prompt input is connected to ideogram4_prompt whose value was assembled before batching at invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:57 and invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:100; prepareLinearUIBatch() only injects dynamic prompt expansions into the returned node at invokeai/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 metadata positive_prompt changes through the decoy, so batched images can all use the same text conditioning. Test: build an Ideogram graph with ideogram4ColorPalette non-empty and dynamicPrompts.prompts containing two distinct prompts, run prepareLinearUIBatch(), and assert the batch data updates the node connected to ideogram4_text_encoder.prompt or that each structured caption contains the expanded prompt.

  • invokeai/backend/model_manager/load/model_loaders/ideogram4.py:178: the non-fp8 text encoder path calls model.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: monkeypatch AutoModel.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 uses useTranslation() for the field label and invokeai/frontend/web/public/locales/en.json:1635 only adds the generic samplerPreset label. Trigger: running the UI in any non-English locale. Consequence: newly added visible Ideogram controls remain partially untranslated. Test: render ParamIdeogram4SamplerPreset with 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() allows num_steps=1 through steps validation at invokeai/app/invocations/ideogram4_denoise.py:72, but polish_count = min(num_steps, ...) makes main_count zero despite the docstring promising at least one polish and one main step. Trigger: user sets the advanced Ideogram step override to 1, optionally with a guidance override. Consequence: the generated schedule is only the polish weight, so the user-specified main guidance_scale is 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).
@Pfannkuchensack
Pfannkuchensack requested a review from JPPhoto July 25, 2026 16:00

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: ideogram4Steps still accepts 1, and Ideogram4Steps.parse() also recalls ideogram4_steps: 1 at invokeai/frontend/web/src/features/metadata/parsing.tsx:942, but the backend invocation now requires steps >= 2 at invokeai/app/invocations/ideogram4_denoise.py:85. A recalled image metadata value or rehydrated client state with ideogram4_steps set to 1 is dispatched without clamping at invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts:105 and then passed directly into the graph at invokeai/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 with ideogram4_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.bbox is declared as Optional[list[int]] with no length or value constraints, while the description and model contract require exactly four normalized coordinates in the 0..1000 range. build_ideogram4_caption() forwards any provided bbox unchanged into the structured JSON at invokeai/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 execute Ideogram4CaptionBuilderInvocation with short, long, and out-of-range bbox arrays 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).
@Pfannkuchensack
Pfannkuchensack requested a review from JPPhoto July 26, 2026 16:57

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more issues...

Blockers

  • invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts:73: the canvas enqueue path routes every Ideogram canvas job to buildIdeogram4Graph(), but buildIdeogram4Graph() asserts generationMode === 'txt2img' at invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:40. The compositor returns img2img, outpaint, or inpaint when visible raster/inpaint content exists at invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts:711, while getRasterLayerWarnings() and getInpaintMaskWarnings() still return no problems at invokeai/frontend/web/src/features/controlLayers/store/validators.ts:241 and invokeai/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 through invokeai/frontend/web/src/features/queue/store/readiness.ts:762, but no Ideogram 4 branch. getOriginalAndScaledSizesForTextToImage() uses raw bbox dimensions when scaleMethod === 'none' at invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts:132, buildIdeogram4Graph() passes those dimensions into ideogram4_denoise at invokeai/frontend/web/src/features/nodes/util/graph/generation/buildIdeogram4Graph.ts:116, and the backend requires both dimensions to be multiples of 16 at invokeai/app/invocations/ideogram4_denoise.py:81. Trigger: Ideogram 4 on Canvas with bbox scaling disabled and a bbox like 1025x1024. Consequence: readiness can allow a graph whose denoise node fails backend validation. Test: build canvas readiness for Ideogram 4 with bbox.scaleMethod set to none and 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, but collectIdeogram4PromptInputs() only reads region.positivePrompt and bbox at invokeai/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 and 0..1000 range, but does not enforce y_min <= y_max and x_min <= x_max; build_ideogram4_caption() forwards the bbox verbatim at invokeai/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: instantiate Ideogram4Region(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 !isFlux2 branch 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: render AdvancedSettingsAccordion with 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.bbox is generated as number[] | null with no length or range constraints, even though the backend validator at invokeai/app/invocations/ideogram4_caption.py:24 rejects anything other than exactly four coordinates in 0..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 changing bbox to schema-expressible constraints and assert openapi.json includes minItems, maxItems, and item minimum/maximum for Ideogram4Region.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
JPPhoto self-requested a review July 26, 2026 21:04

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:168 and invokeai/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 under accelerate.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: monkeypatch AutoModel.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.

Pfannkuchensack and others added 2 commits July 26, 2026 23:44
…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().
@lstein

lstein commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

@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>
@lstein

lstein commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

@Pfannkuchensack I also fixed a lint:prettier issue, so as soon as the CI tests complete successfully, this will merge.

@lstein
lstein enabled auto-merge (squash) July 27, 2026 20:52
@lstein
lstein merged commit 7a37c94 into invoke-ai:main Jul 27, 2026
17 checks passed
@Pfannkuchensack
Pfannkuchensack deleted the feat/ideogram4-support branch July 27, 2026 21:42
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Jul 28, 2026
…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
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Jul 28, 2026
…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)
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Jul 31, 2026
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.
lstein added a commit that referenced this pull request Aug 7, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.0 api backend PRs that change backend files docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-deps PRs that change python dependencies python-tests PRs that change python tests Root

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

4 participants