Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
03b1ef8
feat(invocations): discover node modules recursively
Pfannkuchensack Aug 19, 2026
a74566b
refactor(invocations): group node modules by architecture
Pfannkuchensack Aug 19, 2026
aff689a
docs(contributing): place new-architecture nodes in their package
Pfannkuchensack Aug 19, 2026
80085e7
refactor(util): extract the package walker used to fill registries
Pfannkuchensack Aug 19, 2026
1010b16
feat(architectures): add the facet registry
Pfannkuchensack Aug 19, 2026
5e78565
test(architectures): registry mechanics, completeness gate and layeri…
Pfannkuchensack Aug 19, 2026
2af9d7d
feat(app): validate the architecture registry at boot
Pfannkuchensack Aug 19, 2026
74207b8
feat(architectures): declare latent spaces, and resolve previews thro…
Pfannkuchensack Aug 19, 2026
c6ba484
feat(architectures): declare conditioning types, and build safe_globa…
Pfannkuchensack Aug 19, 2026
b6f841c
feat(architectures): declare the generation defaults each architectur…
Pfannkuchensack Aug 19, 2026
38c07e7
feat(architectures): give FLUX.1, CogView 4 and SD 3.5 the defaults t…
Pfannkuchensack Aug 19, 2026
3cc9a3c
feat(architectures): declare the Stable Diffusion generation defaults
Pfannkuchensack Aug 19, 2026
1c67836
feat(architectures): give the SDXL refiner a canvas, and require defa…
Pfannkuchensack Aug 19, 2026
6760ffd
feat(architectures): declare what each architecture can generate
Pfannkuchensack Aug 19, 2026
6a1853c
feat(architectures): declare which generation features each architect…
Pfannkuchensack Aug 19, 2026
f357dec
feat(api): serve the architecture capability table
Pfannkuchensack Aug 20, 2026
9d78d28
refactor(model_manager): split the starter catalogue into a package
Pfannkuchensack Aug 20, 2026
1ef3424
fix(invocations): compute Ideal Size from what the architecture declares
Pfannkuchensack Aug 20, 2026
7587728
feat(scripts): scaffold a new architecture, and derive what cannot be…
Pfannkuchensack Aug 20, 2026
7c03da6
fix(api): authenticate the capabilities route, and correct two stale …
Pfannkuchensack Aug 20, 2026
4d6d704
feat(architectures): declare how far each UNet downscales internally
Pfannkuchensack Aug 20, 2026
19d727b
feat(invocation_api): export every architecture's conditioning type
Pfannkuchensack Aug 20, 2026
36cede7
Merge branch 'main' into refactor/invocations-by-architecture
Pfannkuchensack Aug 20, 2026
428910c
Merge branch 'main' into refactor/invocations-by-architecture
Pfannkuchensack Aug 21, 2026
47ffa9d
feat: add support for 'ernie-image' model in generation framework
Pfannkuchensack Aug 23, 2026
1231dce
Merge remote-tracking branch 'origin/main' into refactor/invocations-…
Pfannkuchensack Aug 23, 2026
692ad1e
fix(architectures): declare the Wan video modes upstream added
Pfannkuchensack Aug 23, 2026
c1a507e
refactor(invocations): replace star import with explicit module loadi…
Pfannkuchensack Aug 23, 2026
8928832
refactor(architectures): update default settings to include scheduler…
Pfannkuchensack Aug 23, 2026
6dcc75a
Merge branch 'main' into refactor/invocations-by-architecture
Pfannkuchensack Aug 23, 2026
a4dd010
Merge remote-tracking branch 'origin/main' into refactor/invocations-…
Pfannkuchensack Aug 28, 2026
33038b5
Merge branch 'main' into refactor/invocations-by-architecture
Pfannkuchensack Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions docs/src/content/docs/contributing/new-model-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ This guide describes all the steps required to integrate a new model type into I
The code examples use a hypothetical `NewModel` architecture. The implementations of FLUX.1, FLUX.2 Klein, SD3, SDXL, and Z-Image in the InvokeAI codebase serve as excellent real-world references.
:::

:::tip[Start with the scaffolder]
Add the `BaseModelType` member first (step 1 below), then run:

```sh
python scripts/new_architecture.py --base newmodel --name NewModel --write
```

It writes the three files that are mechanical — the architecture's declaration under
`invokeai/backend/architectures/defs/`, its invocation package, and its starter-model module — and
then prints everything it *cannot* write, which is most of this guide. That list is derived from the
codebase on each run rather than kept in the script, so it does not go stale.

The generated declaration deliberately does not work. It carries every required facet with an
obviously wrong value, so the app refuses to boot until each one is filled in. A stub that booted
would let a half-integrated architecture reach a user, which is the failure this whole structure
exists to prevent.
:::

---

## 1. Backend: Model Manager
Expand Down Expand Up @@ -348,10 +366,32 @@ Loaders convert the files on disk (described by the config) into PyTorch models

Invocations expose your PyTorch functions as isolated execution nodes in InvokeAI's graph.

:::note[Where node files go]
Node modules live in `invokeai/app/invocations/[newmodel]/`, one package per architecture. Three
kinds of node are grouped by role instead, because they are shared across architectures rather than
owned by one:

| Node | Package |
| --- | --- |
| VAE encode / decode, latents-to-image, latents-to-video | `invocations/vae/` |
| Text encoders | `invocations/text_encoder/` |
| PiD decode and upscale | `invocations/pid/` |

Generic nodes — noise, primitives, image ops, metadata — stay at the top level. Keep the filename
prefixed with the architecture (`newmodel_denoise.py`) even inside `newmodel/`: the prefix is what
makes a file findable by name, and the cross-cutting packages mix architectures by design.

The package is discovered automatically; there is no registration list to edit. But an
`__init__.py` is required — a directory without one is not a package, and at runtime every node in
it would simply not exist. `tests/app/invocations/test_node_discovery.py` fails loudly when the
files on disk and the imported modules disagree, so this is caught in CI rather than by a user
opening a workflow.
:::

<Steps>
1. **Model Loader Invocation**

```python title="invokeai/app/invocations/[newmodel]_model_loader.py"
```python title="invokeai/app/invocations/[newmodel]/[newmodel]_model_loader.py"
@invocation("newmodel_model_loader", title="NewModel Loader", ...)
class NewModelModelLoaderInvocation(BaseInvocation):
model: ModelIdentifierField = InputField(description="Main model")
Expand All @@ -375,7 +415,7 @@ Invocations expose your PyTorch functions as isolated execution nodes in InvokeA

2. **Text Encoder Invocation**

```python title="invokeai/app/invocations/[newmodel]_text_encoder.py"
```python title="invokeai/app/invocations/text_encoder/[newmodel]_text_encoder.py"
@invocation("newmodel_text_encode", title="NewModel Text Encoder", ...)
class NewModelTextEncoderInvocation(BaseInvocation):
prompt: str = InputField()
Expand Down Expand Up @@ -427,7 +467,7 @@ Invocations expose your PyTorch functions as isolated execution nodes in InvokeA

3. **Denoise Invocation**

```python title="invokeai/app/invocations/[newmodel]_denoise.py"
```python title="invokeai/app/invocations/[newmodel]/[newmodel]_denoise.py"
@invocation("newmodel_denoise", title="NewModel Denoise", ...)
class NewModelDenoiseInvocation(BaseInvocation):
# Standard Fields
Expand Down Expand Up @@ -478,7 +518,7 @@ Invocations expose your PyTorch functions as isolated execution nodes in InvokeA

4. **VAE Encode Invocation**

```python title="invokeai/app/invocations/[newmodel]_vae_encode.py"
```python title="invokeai/app/invocations/vae/[newmodel]_vae_encode.py"
@invocation("newmodel_vae_encode", title="Image to Latents - NewModel", ...)
class NewModelVaeEncodeInvocation(BaseInvocation):
image: ImageField = InputField()
Expand All @@ -497,7 +537,7 @@ Invocations expose your PyTorch functions as isolated execution nodes in InvokeA

5. **VAE Decode Invocation**

```python title="invokeai/app/invocations/[newmodel]_vae_decode.py"
```python title="invokeai/app/invocations/vae/[newmodel]_vae_decode.py"
@invocation("newmodel_vae_decode", title="Latents to Image - NewModel", ...)
class NewModelVaeDecodeInvocation(BaseInvocation):
latents: LatentsField = InputField()
Expand Down Expand Up @@ -1069,9 +1109,9 @@ class ControlNet_Checkpoint_NewModel_Config(ControlNet_Checkpoint_Base):
return cls(...)
```

**Backend Invocation** — `invokeai/app/invocations/[newmodel]_controlnet.py`:
**Backend Invocation** — `invokeai/app/invocations/[newmodel]/[newmodel]_controlnet.py`:

```python title="invokeai/app/invocations/[newmodel]_controlnet.py"
```python title="invokeai/app/invocations/[newmodel]/[newmodel]_controlnet.py"
@invocation("newmodel_controlnet", ...)
class NewModelControlNetInvocation(BaseInvocation):
image: ImageField = InputField()
Expand All @@ -1091,9 +1131,9 @@ const { controlNets } = await addControlNets({ g, manager, denoise });

### IP-Adapter / Reference Images

**Backend Invocation** — `invokeai/app/invocations/[newmodel]_ip_adapter.py`:
**Backend Invocation** — `invokeai/app/invocations/[newmodel]/[newmodel]_ip_adapter.py`:

```python title="invokeai/app/invocations/[newmodel]_ip_adapter.py"
```python title="invokeai/app/invocations/[newmodel]/[newmodel]_ip_adapter.py"
@invocation("newmodel_ip_adapter", ...)
class NewModelIPAdapterInvocation(BaseInvocation):
image: ImageField = InputField()
Expand All @@ -1119,7 +1159,7 @@ class LoRA_LyCORIS_NewModel_Config(LoRA_LyCORIS_Base):

**Backend Model Loader Integration:**

```python title="invokeai/app/invocations/[newmodel]_model_loader.py"
```python title="invokeai/app/invocations/[newmodel]/[newmodel]_model_loader.py"
class NewModelModelLoaderOutput(BaseInvocationOutput):
transformer: TransformerField # TransformerField already contains loras: list[LoRAField]
```
Expand Down Expand Up @@ -1185,10 +1225,13 @@ For a **minimal txt2img integration**, the following files are required:
- invokeai
- app/invocations
- metadata.py
- `[newmodel]_model_loader.py`
- `[newmodel]_text_encoder.py`
- `[newmodel]_denoise.py`
- `[newmodel]_vae_decode.py`
- `[newmodel]`
- `[newmodel]_model_loader.py`
- `[newmodel]_denoise.py`
- text_encoder
- `[newmodel]_text_encoder.py`
- vae
- `[newmodel]_vae_decode.py`
- backend
- model_manager
- taxonomy.py
Expand All @@ -1213,7 +1256,8 @@ For **img2img / inpaint / outpaint**, additionally:
<FileTree>
- invokeai
- app/invocations
- `[newmodel]_vae_encode.py`
- vae
- `[newmodel]_vae_encode.py`
- frontend/web/src/features/nodes/util/graph/generation
- addImageToImage.ts
- addInpaint.ts
Expand Down
46 changes: 14 additions & 32 deletions invokeai/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,9 @@
from invokeai.app.services.wildcard_records.wildcard_records_sqlite import SqliteWildcardRecordsStorage
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_disk import WorkflowThumbnailFileStorageDisk
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
AnimaConditioningInfo,
BasicConditioningInfo,
CogView4ConditioningInfo,
ConditioningFieldData,
ErnieImageConditioningInfo,
FLUXConditioningInfo,
Ideogram4ConditioningInfo,
Krea2ConditioningInfo,
MiniMaxH3ConditioningInfo,
QwenImageConditioningInfo,
SD3ConditioningInfo,
SDXLConditioningInfo,
WanConditioningInfo,
ZImageConditioningInfo,
)
from invokeai.backend.architectures import conditioning_infos
from invokeai.backend.architectures import validate as validate_architectures
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.version.invokeai_version import __version__

Expand Down Expand Up @@ -115,6 +102,12 @@ def initialize(
loop: asyncio.AbstractEventLoop,
logger: Logger = logger,
) -> None:
# Before anything else, and at module scope above rather than lazily inside a function: the
# registry has to be filled before `ObjectSerializerDisk` is constructed below, because that
# mutates process-global torch state (`add_safe_globals`). Also covers every embedder that
# never goes through `run_app` — tests, and scripts/generate_openapi_schema.py.
validate_architectures()

logger.info(f"InvokeAI version {__version__}")
logger.info(f"Root directory = {str(config.root_path)}")

Expand Down Expand Up @@ -164,22 +157,11 @@ def initialize(
conditioning = ObjectSerializerForwardCache(
ObjectSerializerDisk[ConditioningFieldData](
output_folder / "conditioning",
safe_globals=[
ConditioningFieldData,
BasicConditioningInfo,
SDXLConditioningInfo,
FLUXConditioningInfo,
SD3ConditioningInfo,
CogView4ConditioningInfo,
ZImageConditioningInfo,
ErnieImageConditioningInfo,
Ideogram4ConditioningInfo,
QwenImageConditioningInfo,
Krea2ConditioningInfo,
AnimaConditioningInfo,
WanConditioningInfo,
MiniMaxH3ConditioningInfo,
],
# Every architecture's conditioning class, from what each declares under
# invokeai/backend/architectures/defs/. Missing one here fails nowhere near
# here: the encoder runs, writes its output, and the denoise node then dies
# unpickling it.
safe_globals=[ConditioningFieldData, *conditioning_infos()],
ephemeral=True,
),
)
Expand Down
25 changes: 25 additions & 0 deletions invokeai/app/api/routers/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from invokeai.app.services.orphaned_models import CONVERSION_SCRATCH_DIRNAME, OrphanedModelInfo
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.util.suppress_output import SuppressOutput
from invokeai.backend.architectures import ArchitectureCapabilities, architecture_capabilities
from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.configs.factory import AnyModelConfig, ModelConfigFactory
from invokeai.backend.model_manager.configs.main import (
Expand Down Expand Up @@ -229,6 +230,30 @@ def prepare_model_config_for_response(config: AnyModelConfig, dependencies: Type
##############################################################################


@model_manager_router.get(
"/capabilities",
operation_id="list_architecture_capabilities",
responses={200: {"description": "What each model architecture supports"}},
)
def list_architecture_capabilities(current_user: CurrentUserOrDefault) -> list[ArchitectureCapabilities]:
"""What each model architecture can generate, and which generation features it supports.

A static table, the same for every install and every user, derived from what the architectures
declare under `invokeai/backend/architectures/defs/`. Fetch it once and join it against model
records locally: look up `(base, variant)`, fall back to `(base, null)`.

Deliberately not a field on the model records themselves — it is the same for every model of an
architecture, and putting it there would add these fields to all 115 config schemas.

Authenticated like every other route here even though the response holds nothing user-specific:
the allowlist for public routes is short and deliberate, and this is not a reason to lengthen it.

Declared `def`, not `async def`: it awaits nothing, so FastAPI runs it in a threadpool instead of
on the event loop. See docs/contributing/blocking-work-in-api-routes.
"""
return architecture_capabilities()


@model_manager_router.get(
"/",
operation_id="list_model_records",
Expand Down
34 changes: 31 additions & 3 deletions invokeai/app/invocations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
"""Core invocation modules.

Every module in this package registers its `@invocation`-decorated classes with
`InvocationRegistry` as it is imported, so the app is only correct once *all* of them have been
imported. `load_all_modules()` does that, and `invokeai.app.services.shared.graph` calls it.

Importing them here, in the package body, would be the obvious shortcut and is wrong: it makes
`import invokeai.app.invocations.anything` -- including the `baseinvocation` import that
`invokeai.invocation_api` starts with -- pull in the whole tree. A node module that imports
`invocation_api` back (`composition-nodes.py` does) then closes a cycle, and
`import invokeai.invocation_api` fails outright with a partially initialized module. That is the
first import in the documented node-pack guide, so it has to stay cheap.

Discovery walks the whole package tree rather than globbing `*.py` in this directory. Node modules
are grouped into per-architecture subpackages (`flux/`, `wan/`, ...) and cross-cutting ones (`vae/`,
`text_encoder/`, `pid/`), and a flat glob would skip every one of them silently -- the failure would
not surface at boot but later, as an "unknown node type" when a user opens a workflow that uses one.
"""

from importlib import import_module
from pathlib import Path
from types import ModuleType

from invokeai.backend.util.module_discovery import discover_modules


def load_all_modules() -> dict[str, ModuleType]:
"""Import every node module in this package, registering the invocations it declares.

# add core nodes to __all__
python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py"))
__all__ = [f.stem for f in python_files] # type: ignore
Idempotent: `import_module` returns the cached module on later calls, so callers do not have to
coordinate who invokes it first.
"""
return {name: import_module(name) for name in discover_modules(Path(__file__).parent, f"{__name__}.")}
1 change: 1 addition & 0 deletions invokeai/app/invocations/anima/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Anima nodes (Cosmos Predict2 DiT + LLM adapter)."""
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from torchvision.transforms.functional import to_tensor
from tqdm import tqdm

from invokeai.app.invocations.anima_lllite import AnimaLLLiteField
from invokeai.app.invocations.anima.anima_lllite import AnimaLLLiteField
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
AnimaConditioningField,
Expand Down
1 change: 1 addition & 0 deletions invokeai/app/invocations/cogview4/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""CogView 4 nodes."""
1 change: 1 addition & 0 deletions invokeai/app/invocations/ernie_image/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Baidu ERNIE-Image nodes."""
1 change: 1 addition & 0 deletions invokeai/app/invocations/flux/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""FLUX.1 nodes (Dev, Schnell, Fill, Kontext)."""
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
InputField,
LatentsField,
)
from invokeai.app.invocations.flux_controlnet import FluxControlNetField
from invokeai.app.invocations.flux_vae_encode import FluxVaeEncodeInvocation
from invokeai.app.invocations.ip_adapter import IPAdapterField
from invokeai.app.invocations.flux.flux_controlnet import FluxControlNetField
from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
from invokeai.app.invocations.model import ControlLoRAField, LoRAField, TransformerField, VAEField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.invocations.sd.ip_adapter import IPAdapterField
from invokeai.app.invocations.vae.flux_vae_encode import FluxVaeEncodeInvocation
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux
from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import InputField
from invokeai.app.invocations.ip_adapter import (
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.invocations.sd.ip_adapter import (
CLIP_VISION_MODEL_MAP,
IPAdapterField,
IPAdapterInvocation,
IPAdapterOutput,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.configs.ip_adapter import IPAdapter_Checkpoint_FLUX_Config
Expand Down
1 change: 1 addition & 0 deletions invokeai/app/invocations/flux2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""FLUX.2 nodes ([dev] and Klein)."""
Loading
Loading