Skip to content

feat(architectures): serve model policy from the backend, and read it in webv2 - #146

Open
Pfannkuchensack wants to merge 40 commits into
refactor/invocations-by-architecturefrom
feat/webv2-server-driven-model-policies
Open

feat(architectures): serve model policy from the backend, and read it in webv2#146
Pfannkuchensack wants to merge 40 commits into
refactor/invocations-by-architecturefrom
feat/webv2-server-driven-model-policies

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented Aug 23, 2026

Copy link
Copy Markdown
Member

Summary

Adding a BaseModelType should not mean editing the frontend. It still did: BASE_GENERATION in webv2 held 14 architectures with grid, canvas, steps, CFG, scheduler, negative-prompt policy and six UI flags — the same facts invokeai/backend/architectures/defs/ already declares. This deletes that table and reads the policy from GET /api/v2/models/capabilities instead.

Three things had to happen for that to be safe, and each turned up a bug of its own.

Reading the table. core/ cannot reach data/ under feature-core-purity, so the rows are pushed into a registry — the shape configureHttpAuth already uses. SupportedGenerateBase becomes a declared list beside GRAPH_BUILDERS rather than keyof typeof BASE_GENERATION: which architectures we can build a graph for is the one question only the frontend can answer, and it must not depend on load state, since isSupportedGenerateModel has a dozen callers outside the widget.

Loading is gated twice. The widget shows a loading state, but the load-bearing gate is in createGenerateWidgetSyncRuntime, which reconciles at construction and persists its result into the project — without the table it would have written fallback defaults to disk, including an off-grid canvas for CogView 4.

Which VAEs an architecture accepts. The same fact was written down three times — once per loader as ui_model_base, once in webv2's VAE picker, once more in its related-models linker — and all three disagreed about Anima. It is not derivable: eight architectures share a 16-channel space at 8× compression, but SD 3.5 and CogView 4 belong to neither the Wan/Qwen family nor FLUX's. Nor does the VAE's own base settle it — the same physical file is registered as anima or qwen-image depending on which family it was installed for (byte-identical, 194 tensors), and a wan VAE is either the shared 16-channel Wan 2.1 file or TI2V-5B's 48-channel one. So the new facet declares base and, where the base is ambiguous, the channel count VAE_Checkpoint_Wan_Config has been recording all along without anyone reading it.

Loading a Qwen-Image VAE. Two layouts of that autoencoder circulate; only the diffusers one was handled, with strict=True, so a community redistribution failed with all 194 keys missing — while the byte-identical file installed under anima worked, because that path converts. Whether a VAE worked came down to which base it happened to be probed as.

Defaults change for six architectures

Deliberate, and the reason this is a feat and not a refactor. webv2 is not shipped yet, so no existing user is affected.

Base before after
flux 4 steps / 4.0 28 / 3.5, plus variant rows (Schnell 4, Fill 50 @ 30)
sd-3 30 / 7.0 40 / 4.5
cogview4 30 / 7.0 50 / 3.5
anima 30 / 4.0 35 / 4.5
z-image 8 steps 9 steps
sd-2 512×512 768×768

The variant rows are the point: one row per architecture could not express that FLUX Schnell wants 4 steps where dev wants 28, so it carried Schnell's number for every variant.

Bugs fixed on the way

  • bboxGrid.ts kept a second copy of the grid column and had drifted — krea-2, wan and ideogram-4 got 8px steps while their denoise nodes enforce multiples of 16, so the canvas offered sizes that failed at enqueue.
  • Regional guidance on krea-2 is not fixed here — that landed on main before this branch, and the only change to RegionalGuidanceSettings below is a comment. What this branch does change there is the unloaded state: it no longer claims a supported model is unsupported while the table is in flight.
  • flux2_klein_model_loader offered FLUX VAEs, claiming Klein "uses the same VAE as FLUX (16-channel)". It does not — generating with one raises AutoEncoder.decode() got an unexpected keyword argument 'return_dict'.
  • anima_model_loader declared no ui_model_base at all, so its picker offered every VAE ever installed.
  • ERNIE-Image omitted scheduler_applies_to_graph, defaulting it to False, while ernie_image_denoise builds its sampler from the scheduler field. Thirteen of fourteen agreed; nothing compared them.
  • getGenerationDefaults folded cfg_scale ?? guidance, so a guidance-distilled model's "CFG off" marker won over the guidance it actually samples with — FLUX dev generated at 1.0, not 3.5.

How it stays honest

A committed fixture of the response body is pinned against architecture_capabilities() by a backend test, feeds the frontend's unit tests, and is what scripts/mock-backend.mjs serves — so the hand-written wire type, the tests and the mock cannot drift from the real route. webv2 has no generated OpenAPI types, so without this the TS type would be an unguarded assertion.

Invariant tests bind each facet to the thing that enforces it: dimension_grid to the node's multiple_of, scheduler_applies_to_graph to whether the denoise node takes a scheduler, each VaeFacet to its loader's ui_model_base, and negative_prompt: never to cfg_scale == 1.0. Four of the six bugs above were found by writing those, not by review.

Related Issues / Discussions

Stacked on #133 (the architecture facet registry), which introduces the endpoint this consumes.

QA Instructions

Nothing about an existing install needs adjusting — no migration, no re-identification, no model records touched.

Generation. Open Generate and confirm the panel renders after a brief load. Pick FLUX Schnell and FLUX dev in turn: they should prefill 4 and 28 steps respectively, which the old single-row table could not do. Check SD 2.x prefills 768×768.

The grid fix. On the canvas with a Krea-2, Wan or Ideogram-4 model, the bbox should snap to 16px. Before this, 8px was offered and an off-grid size failed at enqueue — try 1032px wide on main to see the old behaviour.

Regional guidance. Add a regional guidance layer with a Krea-2 model. The negative prompt field and Auto-Negative switch should be gone; they were rendered and then discarded by the graph builder.

VAE pickers. With an Anima model, the VAE list should offer Anima, Qwen-Image and 16-channel Wan VAEs and nothing else — no FLUX VAE, which decodes an Anima latent into noise; it previously offered every VAE installed. A Wan TI2V-5B model should offer only the 48-channel Wan VAE, an A14B model only the 16-channel one. With FLUX.2 Klein, FLUX VAEs should no longer appear; selecting one on main crashes during decode.

The Qwen VAE fix. If you have a community Qwen-Image VAE registered under base qwen-image (the decoder.conv1.* layout rather than decoder.conv_in.*), generating with it failed before with "194 missing keys" and should now work.

The gate. Point the frontend at a backend without the /capabilities route, or make it 500. The Generate panel must show an error with a retry, not render fields prefilled with generic defaults.

Verified end to end on this branch: all seven installed architectures generate with previews; qwen-image with both VAE registrations; FLUX.2 Klein with the FLUX.2 VAE (and confirmed failing with the FLUX one).

Merge Plan

Merge #133 first. This branch is stacked on it and consumes the endpoint it adds.

The diff currently shows ~96 files, but only 60 are this branch's — the rest is the generate-widget redesign merged in from main and will disappear from the diff once the base branch catches up.

Nothing touches a DB schema, a redux slice or a migration. openapi.json and schema.ts are regenerated, and the delta is larger than additive:

  • Additive: two new schemas (ArchitectureVae, VaeAcceptance), one optional field on the capability row, and flow-no-lcm in SchedulerSet.
  • Not additive: the capability row no longer carries modality or features.spatial_compression; webv2, the only client, never read them.
  • Not additive: flux2_klein_model_loader's ui_model_base loses flux, and anima_model_loader's loses it too, while qwen_image_model_loader gains anima. Legacy invokeai/frontend/web filters its VAE pickers on that field, so those pickers change for the shipping client. Both are the intended correction — Klein cannot decode with a FLUX VAE, and Anima decodes one into noise — but they are a behaviour change, not a no-op.

Review follow-up

An adversarial review of this branch, plus two independent passes, produced findings that are fixed
here. The theme: moving policy behind an endpoint means every consumer needs a real subscription,
and only two modules had one.

  • Every capability reader now subscribes — the canvas bbox grid, the dims sync, the topbar's
    Invoke gate, regional guidance, gallery import and image recall. Each previously read the registry
    imperatively and latched the fallback for the rest of the session. Reopening a saved krea-2
    project before the table arrived left the bbox snapping at 8px all session, on an architecture
    whose denoise node enforces 16.
  • React Compiler is enabled, and that is why a subscription alone was not enough: a bare
    gridSizeForModelBase(base) in a component body is a pure function of base to the compiler and
    is memoised for the component's lifetime. Policy reads happen inside the store selector now.
  • The Generate widget resolves again after a successful retry. Its useMemo did not depend on
    capability readiness, so the fail-closed null cached during the error render survived the retry
    and the form came back with an empty model picker for a project that had one saved.
  • Image recall no longer persists a fallback-grid size. Recalled dimensions are snapped to the
    architecture's grid and written into the project; without the table every base read as grid 8.
  • Anima no longer offers FLUX VAEs. Same channel count, different basis: anima_latents_to_image
    takes a FluxAutoEncoder without raising and skips the Wan denormalisation. Measured on real
    weights — one Anima denoise decoded by both VAEs, 8.67 dB PSNR between the two images, a magenta
    moire in place of the subject, and no error or log line. The number is recorded in the
    declaration, the loader field and the invariant test so it does not come back "as a fallback".
  • The Qwen-Image VAE branch dispatches on positive layout markers, constructs the autoencoder
    directly (in the configured precision, with float16 raised to bfloat16) rather than reaching for a hub config over HTTP, and raises on a checkpoint
    that is neither layout instead of returning a partially-initialised model.
  • webv2 reads VAE compatibility from the served table rather than a hand-written switch over
    literal base lists, which is what the new facet was for.
  • Loading and error states are announced (role="status" / role="alert"), the retry button keeps
    keyboard focus, and eleven test files now observe the unseeded state — previously every test
    seeded the table before render, which is why none of the above was caught.

A second review round found three more, all the same shape as the VAE one — a control offering
values the graph rejects:

  • The guidance input allowed 0 to 100 for every model. FLUX.2 accepts at most 20 and four
    architectures reject below 1, so the UI offered values that failed at enqueue. The bounds are
    declared per architecture, pinned against the ge/le of the field each denoise node actually
    validates, and served on base and variant rows. FLUX.1 Fill keeps 30 — its node is unbounded.
  • Ideogram 4's own controls were bounded by hand and all three were wrong. mu accepts -4..4
    where the control offered 0..10: the entire negative half was unreachable and everything above 4
    was rejected. steps starts at 2, not 1.
  • The model-manager form capped guidance at 20, which this branch made unreachable — FLUX.1 Fill
    now declares 30, so opening such a model and saving any field was refused over a value the user
    never typed. That one is a regression this branch introduces: before it, FLUX models carried no
    stored defaults at all.

Out-of-range slider marks are dropped rather than clamped; a marker pinned to the end of the track
claims a default that is not there.

Still open, deliberately: a failed capability fetch is retried from the Generate panel or a control
layer's settings, not automatically.

Third review round

Findings from two further review passes, fixed here. The theme is the same as before, one level further down: a fact the backend declares should have one owner, and every place that repeated it -- in webv2, in loader field declarations, in node guards -- now reads it.

The capability table serves only what a client reads. modality (modes, metadata_slug) and features.spatial_compression had no reader in webv2 and are gone from the response. features.regional_negative had a hand-kept copy in REGIONAL_GUIDANCE_SUPPORT; the regional graph now reads the served value.

VAE compatibility is per variant, and the backend is its only owner.

  • Wan A14B decodes with the 16-channel VAE and TI2V-5B with the 48-channel one, but the served rows said both widths fit both. VaeFacet.by_variant declares the split; the ti2v_5b row carries its own list, and a test checks every variant's accepted VAEs against the latent space that variant denoises in.
  • The Generate picker, validation and every graph builder (SD, SD3, FLUX, FLUX.2, Qwen-Image, Z-Image, Krea-2, Anima, Wan) read the same served rule. The qwen-image builder used to drop an anima-registered VAE the picker offered; SD3 used to send a stale VAE the picker hid.
  • Loader field declarations derive ui_model_base from the facet (accepted_vae_bases), and the Wan and Krea-2 loaders validate a standalone VAE through accepts_vae, with the same actionable messages.
  • The video surface keeps its own Wan 16/48 rule on purpose: its widget sync runs before the table arrives, and reading the table there would drop a stored VAE and persist the loss. A test pins that rule to the served Wan rows.

Anima VAEs. Anima's encode and decode now accept the Wan 2.1 VAE in both layouts -- the original layout loads as AutoencoderKLWan, the diffusers export of the Qwen-Image VAE as AutoencoderKLQwenImage -- where the table said yes and the nodes raised. A FLUX VAE is still refused with a message naming what to choose instead, and Wan 2.2's 48-channel VAE is now refused before it is moved to the device. anima_i2l 1.0.2, anima_l2i 1.0.4. The helpers that decide which VAE fits (as_qwen_image_vae, the tiling scope) moved from backend/krea2/vae_compat.py to backend/util/qwen_image_vae.py: Qwen-Image, Krea-2 and Anima all depend on them, and a file under one architecture hid that a change there changes all three. The Anima tiling test from #241 now builds its tiny VAE with the geometry the node accepts (16 channels, 8x) instead of one the guard rejects. One measurement is quoted everywhere: an Anima denoise decoded by both VAEs, 8.67 dB PSNR between the two images.

VAE loading. Pickled checkpoints (.pt, .pth, .ckpt, .bin) load again for the anima, qwen-image and wan registrations, and a float32 precision request is kept; only float16, which is unstable on this autoencoder, is raised to bfloat16. The wan registration's test loads a real (narrow) AutoencoderKLWan checkpoint through the loader's strict load_state_dict; its mock used to accept a key layout the loader would reject.

Schedulers. Z-Image Base's "no LCM" rule was special-cased twice in webv2 against a flow-no-lcm set the backend never served. It is declared per variant (scheduler_set_by_variant) and pinned to z_image_denoise's own description. Ideogram 4's steps / guidance / mu bounds stay frontend constants -- they belong to controls only that model has -- and a test reads them against the node's ge/le.

States while the table is missing.

  • Canvas scaling, control-layer settings, the control-layer warning, and the Invoke gate re-read policy when the table (or, for workflows, the node templates) finish loading, instead of latching their first answer under React Compiler. Canvas scaling also passes the model variant, so TI2V-5B shows the size it generates at.
  • A control layer no longer calls a valid adapter unsupported during a capability outage: it says the check is waiting for model capabilities, and after a failed load shows the error with a Retry.
  • The Generate panel's retry shows a localized headline with the request's detail, and a successful retry moves focus into the form instead of dropping it to <body>.
  • External generators keep the generic dimension grid; they never get a row, so recall and the canvas size sync no longer wait for one.
  • Recall All, Remix and CLIP skip recall wait for the table. Recall All used to apply the model's fallback defaults and skip the size, and the project kept that after the table arrived. The menu no longer offers them during an outage, and a click explains why; prompts and seed stay recallable.
  • The retry flows settle through the promise ensureArchitectureCapabilitiesLoaded now returns instead of useEffect subscriptions; the control-layer panel hands focus over from a ref cleanup on its failure surface.

Also in this branch

  • .gitattributes (commit 7df4aac27f) checks frontend sources out with LF regardless of core.autocrlf. Without it format:check fails for every file on a Windows checkout, so formatting errors only surfaced in CI. It is a checkout setting only; the repository content is unchanged.
  • Performance baseline growth, measured against refactor(architectures): declare architecture facts in one file per architecture #133: the editor route grows by 2,694 brotli bytes and 3 requests, because the capability table's modules (architectureCapabilities.ts, supportedBases.ts, the capabilities API client and store) now load with the editor. The launchpad grows by 131 brotli bytes. The focus helper shared by the Generate panel and control-layer settings (focusIfUnclaimed.ts) is grouped into the widget-hosts chunk, so it adds no request; it is pinned in the three editor source-owner sets and stays out of the launchpad.

Contract changes for the generated schema

openapi.json and schema.ts are regenerated. Not additive: the capability row loses modality and features.spatial_compression, and SchedulerSet gains flow-no-lcm. ui_model_base on the krea-2 and qwen-image loaders changes order only (anima, qwen-image).

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration — n/a, no redux in webv2
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

🤖 Generated with Claude Code

- Added `scheduler_applies_to_graph` to ERNIE-Image architecture definition to ensure proper UI behavior.
- Introduced a new fixture for architecture capabilities to maintain consistency between backend and frontend.
- Enhanced generation policies to correctly prioritize guidance settings based on architecture labels.
- Added tests to verify that the scheduler application flag aligns with the node's capabilities.
… in webv2

`BASE_GENERATION` in webv2 held 14 architectures with grid, canvas, steps, CFG, scheduler,
negative-prompt policy and six UI flags -- the same facts `architectures/defs/` declares. Adding a
model type meant editing the frontend too, even when nothing about it was special. It is deleted;
the policy now comes from `GET /api/v2/models/capabilities`.

## Reading the table

`core/` cannot reach `data/` under `feature-core-purity`, so the table is pushed in through a
registry, the shape `configureHttpAuth` already uses. `SupportedGenerateBase` is no longer
`keyof typeof BASE_GENERATION` but a declared list beside `GRAPH_BUILDERS`: which architectures we
can build a graph for is the one question only the frontend can answer, and it must not depend on
load state -- `isSupportedGenerateModel` has a dozen callers outside the widget.

Loading is gated on two levels. The widget shows a loading state, but the load-bearing gate is in
`createGenerateWidgetSyncRuntime`: it reconciles at construction and *persists* the result into the
project, so without the table it would write fallback defaults to disk -- an off-grid canvas for
CogView 4 among them. `resolveGenerateWidgetValues` returns null until the table is there.

Defaults change for six architectures, deliberately. FLUX moves to 28 steps at guidance 3.5 with
real variant rows (Schnell 4, Fill 50 at guidance 30) -- the single row could not express that and
carried Schnell's step count for every variant. sd-3 40/4.5, cogview4 50/3.5, anima 35/4.5, z-image
9 steps, SD 2.x 768px.

Control kinds, reference-image limits and regional-guidance support are read from the table too;
their data was verified identical to the hardcoded rules first, so those are behaviour-preserving.
The limits in `controlValidation.ts` and the FLUX-Fill exception stay -- the endpoint has no column
for them, and a comment says so.

## Which VAEs an architecture accepts

The same fact was written down three times -- once per loader as `ui_model_base`, once in webv2's
VAE picker, once more in its related-models linker -- and all three disagreed about Anima. It is not
derivable: eight architectures share a 16-channel space at 8x compression, but SD 3.5 and CogView 4
belong to neither the Wan/Qwen family nor FLUX's. Nor does the VAE's own base settle it. The same
physical file is registered as `anima` or `qwen-image` depending on which family it was installed
for -- byte-identical, 194 tensors -- and a `wan` VAE is either the shared 16-channel Wan 2.1 file
or TI2V-5B's 48-channel Wan2.2-VAE, which fits nothing else. So the new facet declares base *and*,
where the base is ambiguous, the channel count `VAE_Checkpoint_Wan_Config` has been recording all
along without anyone reading it.

Two loader declarations were wrong and are corrected. `anima_model_loader` declared nothing, so its
picker offered every VAE ever installed; it accepts the Wan-family file under any of its three bases
plus a FLUX one, since `anima_l2i` branches on `isinstance(vae, (AutoencoderKLWan,
FluxAutoEncoder))` with a separate decode path for each. `flux2_klein_model_loader` offered FLUX
VAEs, claiming Klein "uses the same VAE as FLUX (16-channel)" -- it does not: generating with one
raises `AutoEncoder.decode() got an unexpected keyword argument 'return_dict'`, the legacy
16-channel class against a 32-channel latent.

## Loading a Qwen-Image VAE

Two layouts of that autoencoder are in circulation. Repo exports carry diffusers keys
(`decoder.conv_in.weight`); community redistributions carry the original layout
(`decoder.conv1.weight`) and need converting. Only the first was handled, and
`AutoencoderKLQwenImage` registers no single-file conversion, so a redistributed file failed
`load_state_dict(strict=True)` with all 194 keys missing.

`strict=True` was right; the assumption in front of it was not. The byte-identical checkpoint
installed under `anima` loaded fine all along through `AutoencoderKLWan.from_single_file`, which
converts -- so whether a VAE worked came down to which base it happened to be probed as, and
`VAE_Checkpoint_Wan_Config` deliberately hands 16-channel files to the Qwen-Image config unless the
filename says "wan". The loader now looks at the state dict: diffusers keys take the direct path
with `strict=True` unchanged, anything else is converted as the Wan-family file it is. No
installation needs adjusting -- identification, model records and bases are untouched.

## Bugs that fall out of removing the duplicates

- `bboxGrid.ts` kept its own copy of the grid column and had drifted: krea-2, wan and ideogram-4 got
  8px steps while their denoise nodes enforce multiples of 16, so the canvas offered sizes that
  failed at enqueue.
- `RegionalGuidanceSettings` asked "is this the FLUX family?" where the graph builder asks
  `regional_negative`. On krea-2 it rendered a negative prompt and an Auto-Negative switch that
  `addRegionalGuidance` then discarded.
- ERNIE-Image's `FeaturesFacet` omitted `scheduler_applies_to_graph`, defaulting it to False, while
  `ernie_image_denoise` builds its sampler from the `scheduler` field. Thirteen of fourteen agreed;
  nothing compared them.
- `getGenerationDefaults` folded `cfg_scale ?? guidance`, so a guidance-distilled model's "CFG off"
  marker won over the guidance it actually samples with. FLUX dev generated at guidance 1.0 instead
  of 3.5.

## Keeping it honest

A committed fixture of the response body is pinned against `architecture_capabilities()` by a
backend test, feeds the frontend's unit tests, and is what `scripts/mock-backend.mjs` serves -- so
the hand-written wire type, the tests and the mock cannot drift from the real route. Invariant tests
bind each facet to the node that enforces it: `dimension_grid` to `multiple_of`,
`scheduler_applies_to_graph` to whether the denoise node takes a scheduler, and each `VaeFacet` to
its loader's `ui_model_base`.

Tests seed the registry through `architectureCapabilities.testing.ts` rather than a global setup
file, so a test that wants to observe the unloaded behaviour still can.

Verified end to end against real models: all seven installed architectures generate, previews
included, and a `qwen-image`-registered VAE that failed before now works.
Pfannkuchensack and others added 4 commits August 23, 2026 21:19
Four CI jobs failed, from two causes.

`oxfmt --check` rejected 28 files. They were linted and typechecked but never formatted: on a
Windows checkout `core.autocrlf` gives every file CRLF while oxfmt writes LF, so `format:check`
fails for all 1898 files locally and is useless as a signal. Formatted the ones this branch
touches.

`openapi.json` and `schema.ts` were stale. Correcting `ui_model_base` on the Anima, Qwen-Image and
FLUX.2 Klein loaders changes the invocation schema, and the artifacts were last regenerated before
that.

Two more surfaced once formatting stopped short-circuiting the lint job:

- The test helper was named `useArchitectureCapabilitiesFixture`, so `react-hooks/rules-of-hooks`
  read it as a hook called at module top level. It is not one; `seedArchitectureCapabilities` says
  what it does anyway.
- Dead imports left behind by deleting `BASE_GENERATION` and moving the ordered-bases pin out.
Four CI jobs failed, from two causes.

`oxfmt --check` rejected 28 files. They were linted and typechecked but never formatted: on a
Windows checkout `core.autocrlf` gives every file CRLF while oxfmt writes LF, so `format:check`
fails for all 1898 files locally and is useless as a signal. Formatted the ones this branch
touches.

`openapi.json` and `schema.ts` were stale. Correcting `ui_model_base` on the Anima, Qwen-Image and
FLUX.2 Klein loaders changes the invocation schema, and the artifacts were last regenerated before
that.

Two more surfaced once formatting stopped short-circuiting the lint job:

- The test helper was named `useArchitectureCapabilitiesFixture`, so `react-hooks/rules-of-hooks`
  read it as a hook called at module top level. It is not one; `seedArchitectureCapabilities` says
  what it does anyway.
- Dead imports left behind by deleting `BASE_GENERATION` and moving the ordered-bases pin out.
`compileUpscaleGraph` passes Upscale's own `kdpm_2` through `coerceSchedulerForGraph`, which reads
architecture policy. Without the table the fallback config applies, and its
`schedulerAppliesToGraph: false` makes the function return the fallback scheduler rather than pass
the requested one through -- so the compiled graph carried `euler_a`.

Missed because the webv2 suite was run in subsets while iterating. It completes in under two
minutes; there was never a reason not to run all of it.
…test

`deriveImageRecallCapabilities` reports whether an image's metadata can be recalled, and clip skip
depends on the architecture's `clipSkipMax`. That comes from the capability table now, so without
it the fallback applies and the capability reads false.

Missed because `vitest.config.mts` excludes `*.browser.test.*` -- they run under
`vitest.browser.config.mts`, which had not been run at all. Both suites pass now: 807 browser tests
and 6435 node tests.
Reading architecture policy from the backend adds four modules to the editor and launchpad bundles:
+52 bytes owned JavaScript on launchpad, +281 on editor, and one additional script request per
editor route.

Only the structural numbers are updated. `--update-baseline` also re-records timings, and those
come from whichever machine ran it -- the fresh numbers were ~10% above the committed ones, which
would have loosened a gated tolerance and let real regressions of that size through. The 56 timing
values are kept as committed.
`--update-baseline` writes `architecture-baseline.json` itself and not in oxfmt's style, so
`format:check` rejected it. Values are unchanged -- the only difference is how one array is wrapped.
`oxfmt` and `prettier` read the file on disk, so on a Windows checkout `core.autocrlf=true` makes
`format:check` fail for all 1898 files -- including every file the contributor never touched. The
check is therefore unusable locally, and formatting errors are only found by CI. That happened four
times in a row on one PR.

`* text=auto` already normalizes these files to LF in the repository, so this changes the working
copy only: `git add --renormalize invokeai/frontend` reports nothing, and no file content differs
after re-checkout. `docker/**` has carried the same rule for the same reason.

Verified after re-checkout: `pnpm run format:check` reports "All matched files use the correct
format", and `prettier --check` passes for the legacy frontend.
@github-actions github-actions Bot added the root label Aug 24, 2026
…erver-driven-model-policies

The base grew a regional-guidance support matrix covering eight bases while the
FeaturesFacet still declared five; that only becomes visible once the frontend
reads the policy from the backend, so sd-2, z-image and anima now declare
supports_regional_guidance (regional_negative for sd-2 alone), and the
capabilities fixture and the two declaration guards follow.

The backend answers whether a base supports regional guidance; the matrix
supplies the graph knowledge that answer selects. Guards pin that the two agree
on regional negatives and that every base declared supported has a matrix row,
since the type narrowing now rests on the backend's answer.

Re-measured architecture-baseline.json for the merged bundle: this branch's four
capability modules enter the editor's initial graph, +281 B owned JS (+0.21%)
and one request. browser-baseline.json still needs a capture on the reference
machine; its timings and browser path are machine-specific.
The generator emits raw json.dumps output; both openapi-checks and the frontend
prettier check compare against a prettier-formatted file.
…erver-driven-model-policies

Four architecture defs gained a facet on each side -- VaeFacet here, VariantFacet
on the base -- and both are kept. Regenerated openapi.json and schema.ts.
…erver-driven-model-policies

Both branches had found and fixed the same two things independently; the merge
keeps this branch's versions, which are the better-structured ones:

- the guidance/cfg_scale choice, as `getRecordGuidanceValue` keyed on
  `guidanceLabel` rather than an inline ternary
- ernie-image's `scheduler_applies_to_graph`

Removed as obsolete here: #133's `capabilityContract.test.ts`, its snapshot and
`test_frontend_capability_parity.py`. They pinned webv2's `BASE_GENERATION` to
the registry, and this branch deletes that table in favour of reading the
capabilities endpoint — `test_capabilities_fixture.py` is the contract now, in
the other direction. Their oxfmt exclusion went with them.

Kept from #133 because this branch does not have it: the guidance slider could
not hold a value above its 10-wide track, so FLUX Fill's 30 clamped to 10 on
first blur. Its regression tests were folded into this branch's own guidance
describe block, dropping the three cases already covered there and re-pointing
the per-base sweep at the policy instead of the deleted table.
Serving generation policy from the backend puts four modules in the app
shell's static graph, and this branch updated architecture-baseline.json for
it but not browser-baseline.json. Re-measured:

- otherRawBytes 152115 -> 153662 on every route. That bucket is exactly
  dist/locales/en.json plus dist/index.html; the growth is the three strings
  the capabilities fetch needs for its loading, error and retry states.
- requestCount and scriptRequestCount +1 on the four editor routes, plus
  ~15 KB of scriptRawBytes, for the store and its two helpers. launchpad is
  unchanged -- there the modules fold into chunks it already requests.
- activatedResourceLimits.largestAssetRawBytes tightened 322887 -> 310251.

editor-gallery now loads a table it never reads. That is real waste, but it is
the debt those routes already carry as `deepen-widget-registry-loading`, not a
new category, so it is left to that ticket rather than reshaped here.

Captured on Windows: the enforced resource numbers reproduce the CI runner's
byte for byte, but browserExecutable and the timing medians are this machine's.
Timings are not gated (timingPolicy.enforce is false, runner unconfigured);
re-capture on the reference machine before turning them on.
Pfannkuchensack and others added 4 commits September 10, 2026 00:37
The capability gate covered the Generate widget's resolver, so the canvas and
topbar Invoke paths still compiled against FALLBACK_GENERATION_CONFIG — a grid
of 8 for bases that reject anything but 16, which is the enqueue failure this
feature exists to prevent. Moved to `getGenerationValidationReasons`, the one
function graph.ts, compileCanvasGraph.ts and previewGraph.ts all call, and
placed before the unsupported-model check so a loaded-late table no longer
reports itself as "needs a supported model". Three workbench suites seed the
table now, as app boot does.

`VaeFacet` had no production reader — `capabilities.py` never rendered it, so
the fact never reached a client and webv2's own copies could not be bound to
it. It is served now, and a test drives every declared pairing through the
picker's filter. That immediately surfaced two desyncs this PR introduced by
widening the backend lists alone: qwen-image accepts an anima-registered VAE,
and anima accepts the 16-channel wan file — the same 194-tensor checkpoint
under whichever base it was installed. Both are the picker refusing a VAE the
backend would load.

`isRegionalGuidanceSupportedForBase` narrowed to the key set of a hardcoded
matrix while deciding from the backend's answer, so a base declared supported
with no row returned `undefined` typed as a support object. The guard written
to catch that used `not.toBeNull()`, which `undefined` passes. Both fixed; the
drift now fails six tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…licy


- Every capability reader subscribes to the table instead of latching the
  fallback: the canvas grid, the dims sync, the topbar, regional guidance and
  gallery import. Reads happen inside the store selector -- React Compiler
  memoises a bare policy call in a component body for the component's life.
- The Generate widget resolves again after a successful retry, and its loading
  and error states are announced and keep focus on the retry button.
- Anima no longer offers FLUX VAEs: same channel count, different basis, and
  the decode is measured at 6.10 dB PSNR against 46.18 for the correct one.
- The Qwen VAE branch dispatches on positive layout markers, loads in bfloat16
  without a network round trip, and raises on a checkpoint that is neither.
- webv2 reads VAE compatibility from the served table rather than a fourth
  hand-written copy, and joins the dimension grid on (base, variant).
…erver-driven-model-policies

Both sides had rewritten the same declarations, so three conflicts needed
merging rather than picking: capabilities.py renders the VAE policy and the
per-variant grid together, and the two VaeAcceptance models were opened up like
the four the base branch already had -- a response model gains nothing from
extra="forbid" but stamps the schema shut.

The performance baselines were re-recorded from a build of the merged tree.
Neither side's numbers were right: the merge carries both sets of changes and
measures above both, so taking either would have under-budgeted the gate.

test_frontend_capability_parity.py stays deleted. It compared the backend
against a frontend table this branch removes; test_capabilities_fixture.py
checks the served payload by equality instead, and validate() already refuses
to boot an architecture that is missing.
Recalled dimensions are snapped to the architecture's grid and then written
into the project. Without the served table every base reads as grid 8, so a
krea-2 or CogView 4 project stored a size its own denoise node rejects, and
nothing re-derived it. Both size helpers now fail closed, the way the resolver
and the canvas gate already do, so the recall reports no size instead.

The blocking reason no longer says capabilities have "not loaded yet": after a
failed fetch nothing re-kicks it except the Generate panel's retry, and `core`
cannot see the store that tells loading from failed.
@Pfannkuchensack
Pfannkuchensack marked this pull request as ready for review September 12, 2026 01:09
The picker stopped offering FLUX VAEs, but the nodes still accepted one: a
hand-built graph, a saved workflow or a metadata recall reached the same path.
Measured on a real denoise -- one latent, two decodes in one graph -- the FLUX
decode scores 8.67 dB PSNR against the correct one, mean absolute error 84 of
255, and the run still reports success.

Both nodes now refuse before the model reaches a device, and the message names
the VAEs that do work. The encode side had the same gap in reverse: it would
have handed the denoiser a latent in the wrong basis.
The guidance input allowed 0 to 100 for every model, so a value the UI offered
was rejected at enqueue: FLUX.2 accepts at most 20, and four architectures
reject below 1. The bounds now come from the served capability row, joined on
(base, variant) like the dimension grid. FLUX.1 Fill still types and keeps 30 --
its node is genuinely unbounded, and the local cap stays where the architecture
declares no ceiling.

A stored value is repaired on model selection, beside the existing CLIP-skip
clamp, and validated at enqueue for everything that does not pass through
selection -- a reopened project, a metadata recall, settings written by API.

Ideogram 4's own controls were bounded by hand and all three were wrong: mu
accepts -4..4 where the UI offered 0..10, so the whole negative half was
unreachable and everything above 4 was rejected.

Out-of-range slider marks are dropped rather than clamped -- a marker pinned to
the end of the track claims a default that is not there.

The model-manager form capped guidance at 20, which this branch made
unreachable: FLUX.1 Fill now declares 30, so opening such a model and saving any
field was refused over a value the user never typed. The form mirrors the
record's own bound instead.
The previous commit staged the file while it was being regenerated, so it landed
empty. The content is unchanged from the merge commit before it: the guidance
bounds reached the schema with the backend half, and nothing in that commit
touches the API surface.
…erver-driven-model-policies

- Re-record webv2 performance baselines in the delta-gated schema from main
- Seed the capability table in the recall tests added on main
- Resolve the invocation route inside the capabilities selector so React Compiler cannot serve the stale result
- Keep the models mock snapshot stable so the regression test fails without the fix
…e-aware VAE loading

- Picker, validation and every graph builder (incl. SD and SD3) read VAE compatibility from the served row
- VAE loader reads pickled checkpoints and keeps float32; only float16 is raised to bfloat16
- External generators keep the generic dimension grid for recall and canvas sync
…VAEs

- Drop modality and spatial_compression from the capabilities table; read regional_negative from it
- VaeFacet.by_variant: Wan A14B takes the 16-channel VAE, TI2V-5B the 48-channel one
- Canvas scaling, control layers and Invoke re-read policy when capabilities or node templates load
…bility outage states

- Anima VAE nodes accept both Wan 2.1 VAE layouts; loaders derive VAE bases and validation from VaeFacet
- Z-Image Base scheduler set declared per variant; Ideogram 4 bounds pinned to the node
- Control layers and the Generate retry handle a missing capability table without false warnings or lost focus
@github-actions github-actions Bot added the ci-cd label Sep 14, 2026
Pfannkuchensack and others added 5 commits September 14, 2026 09:20
- as_qwen_image_vae and the tiling scope live in backend/util: Qwen-Image, Krea-2 and Anima all use them
- Anima tiling test builds its tiny VAE with the geometry the node accepts
- focusIfUnclaimed joins the widget-hosts chunk and is pinned in the editor source-owner sets
- Recall All, Remix and CLIP skip wait for the capability table; prompts and seed stay recallable
- capability retries settle through the load promise instead of useEffect subscriptions
- the Wan VAE loader test loads a real checkpoint through strict state-dict validation
…ture' into feat/webv2-server-driven-model-policies
…bsent

- API recall, image recall onto a fresh project and queue recall no longer compute or persist fallback policy; prompts and seed still apply
- Reference-image availability re-reads when the table lands instead of latching its first answer
- Outage notices come from the locale catalog
Place Workflow before routing checks and select an overflowing filmstrip list.
Match the Settings menu label with its shortcut suffix.
@joshistoast

Copy link
Copy Markdown
Collaborator

🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants