New Model Type Integration Checklist
This guide describes all the steps required to integrate a new model type into InvokeAI, from the backend model manager up to the React frontend.
1. Backend: Model Manager
Section titled “1. Backend: Model Manager”-
Add
BaseModelTypeDeclare your new model in the base model taxonomy.
invokeai/backend/model_manager/taxonomy.py class BaseModelType(str, Enum):# Existing typesStableDiffusion1 = "sd-1"StableDiffusion2 = "sd-2"StableDiffusionXL = "sdxl"Flux = "flux"Flux2 = "flux2" # FLUX.2 KleinSD3 = "sd-3"ZImage = "z-image"NewModel = "newmodel" # NEW -
Add Variant Type (if needed)
If your model comes in different structural variants (e.g., different parameter counts or distilled versions), define a variant enum.
invokeai/backend/model_manager/taxonomy.py # Examples of existing variants:class FluxVariantType(str, Enum):Schnell = "schnell"Dev = "dev"DevFill = "dev_fill"class Flux2VariantType(str, Enum):Klein4B = "klein_4b" # Qwen3 4B encoderKlein9B = "klein_9b" # Qwen3 8B distilledKlein9BBase = "klein_9b_base"# NEW (if needed):class NewModelVariantType(str, Enum):VariantA = "variant_a"VariantB = "variant_b" -
Define Default Settings
Provide default generation parameters for the UI to use when this model is selected.
invokeai/backend/model_manager/configs/main.py class MainModelDefaultSettings:@staticmethoddef from_base(base: BaseModelType, variant: AnyVariant | None = None):match base:case BaseModelType.Flux2:if variant == Flux2VariantType.Klein9BBase:return MainModelDefaultSettings(steps=28, cfg_scale=1.0, ...)return MainModelDefaultSettings(steps=4, cfg_scale=1.0, ...)case BaseModelType.NewModel: # NEWreturn MainModelDefaultSettings(steps=20, cfg_scale=7.0, ...)
2. Backend: Model Configs
Section titled “2. Backend: Model Configs”InvokeAI needs to know how to identify your model from a .safetensors file or a diffusers folder.
-
Create Main Model Config
Define the configuration schemas for your model format(s).
invokeai/backend/model_manager/configs/main.py # Checkpoint Format@ModelConfigFactory.registerclass Main_Checkpoint_NewModel_Config(Checkpoint_Config_Base):type: Literal[ModelType.Main] = ModelType.Mainbase: Literal[BaseModelType.NewModel] = BaseModelType.NewModelformat: Literal[ModelFormat.Checkpoint] = ModelFormat.Checkpointvariant: NewModelVariantType = NewModelVariantType.VariantA@classmethoddef from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict) -> Self:if not cls._validate_is_newmodel(mod):raise NotAMatchError("Not a NewModel")variant = cls._get_variant_or_raise(mod)return cls(..., variant=variant)# Diffusers Format@ModelConfigFactory.registerclass Main_Diffusers_NewModel_Config(Diffusers_Config_Base):type: Literal[ModelType.Main] = ModelType.Mainbase: Literal[BaseModelType.NewModel] = BaseModelType.NewModelformat: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers -
Detection Helper Functions
Write helpers to inspect the state dictionary keys and shapes to uniquely identify your architecture.
invokeai/backend/model_manager/configs/main.py def _is_newmodel(state_dict: dict) -> bool:"""Detect if state dict belongs to NewModel architecture."""# Example FLUX.2 Klein detection:# - context_embedder.weight shape[1] > 4096 (Qwen3 vs T5)# - img_in.weight shape[1] == 128 (32 latent channels × 4)required_keys = ["transformer_blocks.0.attn.to_q.weight", ...]return all(key in state_dict for key in required_keys)def _get_newmodel_variant(state_dict: dict) -> NewModelVariantType:"""Determine variant from state dict."""# Example FLUX.2: context_in_dim distinguishes Klein 4B/9Bcontext_dim = state_dict["context_embedder.weight"].shape[1]if context_dim == 7680:return NewModelVariantType.VariantAreturn NewModelVariantType.VariantB -
VAE Config (if custom VAE)
invokeai/backend/model_manager/configs/vae.py @ModelConfigFactory.registerclass VAE_Checkpoint_NewModel_Config(VAE_Checkpoint_Base):type: Literal[ModelType.VAE] = ModelType.VAEbase: Literal[BaseModelType.NewModel] = BaseModelType.NewModel@classmethoddef from_model_on_disk(cls, mod: ModelOnDisk, ...) -> Self:if not _is_newmodel_vae(mod.state_dict):raise NotAMatchError()return cls(...)def _is_newmodel_vae(state_dict: dict) -> bool:# Example FLUX.2: Check for BN layers (bn.running_mean)return "encoder.bn.running_mean" in state_dict -
Text Encoder Config (if custom encoder)
invokeai/backend/model_manager/configs/[encoder_type].py def _has_newmodel_encoder_keys(state_dict: dict) -> bool:"""Check if state dict contains NewModel encoder keys."""required_keys = ["model.layers.0.", "model.embed_tokens.weight"]return any(key.startswith(indicator) or key == indicatorfor key in state_dict.keys()for indicator in required_keysif isinstance(key, str))@ModelConfigFactory.registerclass NewModelEncoder_Checkpoint_Config(Checkpoint_Config_Base):"""Configuration for single-file NewModel Encoder models."""base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any)type: Literal[ModelType.NewModelEncoder] = Field(default=ModelType.NewModelEncoder)format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)@classmethoddef from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict) -> Self:raise_if_not_file(mod)raise_for_override_fields(cls, override_fields)if not _has_newmodel_encoder_keys(mod.load_state_dict()):raise NotAMatchError("state dict does not look like a NewModel encoder")return cls(**override_fields)@ModelConfigFactory.registerclass NewModelEncoder_Diffusers_Config(Config_Base):"""Configuration for NewModel Encoder in diffusers directory format."""base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any)type: Literal[ModelType.NewModelEncoder] = Field(default=ModelType.NewModelEncoder)format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)@classmethoddef from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict) -> Self:raise_if_not_dir(mod)raise_for_override_fields(cls, override_fields)# Check for text_encoder configconfig_path = mod.path / "text_encoder" / "config.json"if not config_path.exists():raise NotAMatchError(f"config file not found: {config_path}")raise_for_class_name(config_path, {"NewModelForCausalLM"})return cls(**override_fields)Examples of existing implementations:
t5_encoder.py— T5 Encoder for FLUX.1, SD3qwen3_encoder.py— Qwen3 Encoder for FLUX.2 Klein, Z-Imageclip_embed.py— CLIP Encoder for SDXL, SD3
-
Update
AnyModelConfigUnionRegister your new configs so the application knows to check them when scanning directories.
invokeai/backend/model_manager/configs/factory.py AnyModelConfig = Annotated[# ... existing configsMain_Checkpoint_NewModel_Config |Main_Diffusers_NewModel_Config |VAE_Checkpoint_NewModel_Config,Discriminator(...)]
3. Backend: Model Loader
Section titled “3. Backend: Model Loader”Loaders convert the files on disk (described by the config) into PyTorch models in memory.
-
Create Model Loader
invokeai/backend/model_manager/load/model_loaders/[newmodel].py @ModelLoaderRegistry.register(base=BaseModelType.NewModel,type=ModelType.Main,format=ModelFormat.Checkpoint)class NewModelLoader(ModelLoader):def _load_model(self, config: AnyModelConfig, submodel_type: SubModelType | None) -> AnyModel:# Load and convert state dictstate_dict = self._load_state_dict(config.path)# If format conversion needed (e.g., BFL → Diffusers):if self._is_bfl_format(state_dict):state_dict = self._convert_bfl_to_diffusers(state_dict)# Instantiate modelmodel = NewModelTransformer(config=model_config)model.load_state_dict(state_dict)return model -
VAE Loader (if custom VAE)
invokeai/backend/model_manager/load/model_loaders/[newmodel].py @ModelLoaderRegistry.register(base=BaseModelType.NewModel,type=ModelType.VAE,format=ModelFormat.Checkpoint)class NewModelVAELoader(ModelLoader):def _load_model(self, config, submodel_type) -> AnyModel:# Example FLUX.2: AutoencoderKLFlux2 with BN layersfrom diffusers import AutoencoderKLFlux2vae = AutoencoderKLFlux2.from_single_file(config.path)return vae -
Text Encoder Loader (if custom encoder)
invokeai/backend/model_manager/load/model_loaders/[newmodel].py @ModelLoaderRegistry.register(base=BaseModelType.Any,type=ModelType.NewModelEncoder,format=ModelFormat.Checkpoint)class NewModelEncoderLoader(ModelLoader):"""Load single-file NewModel Encoder models."""def _load_model(self, config: AnyModelConfig, submodel_type: SubModelType | None) -> AnyModel:match submodel_type:case SubModelType.TextEncoder:return self._load_text_encoder(config)case SubModelType.Tokenizer:# Load tokenizer from HuggingFace or local pathreturn AutoTokenizer.from_pretrained("org/newmodel-base")raise ValueError(f"Unsupported submodel: {submodel_type}")def _load_text_encoder(self, config: AnyModelConfig) -> AnyModel:from safetensors.torch import load_filefrom transformers import NewModelConfig, NewModelForCausalLM# Load state dict and determine model configurationsd = load_file(config.path)# Detect model architecture from weightslayer_count = self._count_layers(sd)hidden_size = sd["model.embed_tokens.weight"].shape[1]# Create model with detected configurationmodel_config = NewModelConfig(hidden_size=hidden_size,num_hidden_layers=layer_count,# ... other config parameters)with accelerate.init_empty_weights():model = NewModelForCausalLM(model_config)model.load_state_dict(sd, assign=True)return model
4. Backend: Invocations
Section titled “4. Backend: Invocations”Invocations expose your PyTorch functions as isolated execution nodes in InvokeAI’s graph.
-
Model Loader Invocation
invokeai/app/invocations/[newmodel]_model_loader.py @invocation("newmodel_model_loader", title="NewModel Loader", ...)class NewModelModelLoaderInvocation(BaseInvocation):model: ModelIdentifierField = InputField(description="Main model")vae_model: ModelIdentifierField | None = InputField(default=None)encoder_model: ModelIdentifierField | None = InputField(default=None)def invoke(self, context: InvocationContext) -> NewModelLoaderOutput:# Load transformertransformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})# Load VAE (from main model or separately)if self.vae_model:vae = self.vae_model.model_copy(...)else:vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})return NewModelLoaderOutput(transformer=transformer, vae=vae, ...) -
Text Encoder Invocation
invokeai/app/invocations/[newmodel]_text_encoder.py @invocation("newmodel_text_encode", title="NewModel Text Encoder", ...)class NewModelTextEncoderInvocation(BaseInvocation):prompt: str = InputField()encoder: EncoderField = InputField()def invoke(self, context: InvocationContext) -> ConditioningOutput:# 1. Tokenize the promptwith context.models.load(self.encoder.tokenizer) as tokenizer:input_ids = tokenizer(self.prompt,return_tensors="pt",padding="max_length",max_length=256,truncation=True).input_ids# 2. Run encoder and extract hidden states# Example FLUX.2 Klein/Z-Image: Extract specific layers and stack them# Different models use different layer extraction strategies:# - Some use the final hidden state only# - Others stack multiple intermediate layers for richer representationswith context.models.load(self.encoder.text_encoder) as encoder:outputs = encoder(input_ids, output_hidden_states=True)hidden_states = outputs.hidden_states# Stack layers 9, 18, 27 to create combined text embedding# This captures features at different abstraction levels# Shape: (batch, seq_len, hidden_size) -> (batch, seq_len, hidden_size * 3)stacked_embeddings = torch.cat([hidden_states[9],hidden_states[18],hidden_states[27]], dim=-1)# 3. Create conditioning data structure# The stacked embeddings become the text conditioning that guides denoisingconditioning_data = ConditioningFieldData(conditionings=[BasicConditioningInfo(embeds=stacked_embeddings)])# 4. Save conditioning to context and return referenceconditioning_name = context.conditioning.save(conditioning_data)return ConditioningOutput(conditioning=ConditioningField(conditioning_name=conditioning_name)) -
Denoise Invocation
invokeai/app/invocations/[newmodel]_denoise.py @invocation("newmodel_denoise", title="NewModel Denoise", ...)class NewModelDenoiseInvocation(BaseInvocation):# Standard Fieldslatents: LatentsField | None = InputField(default=None)positive_conditioning: ConditioningField = InputField()negative_conditioning: ConditioningField | None = InputField(default=None)# Model Fieldstransformer: TransformerField = InputField()# Denoise Parametersdenoising_start: float = InputField(default=0.0, ge=0, le=1)denoising_end: float = InputField(default=1.0, ge=0, le=1)steps: int = InputField(default=20, ge=1)cfg_scale: float = InputField(default=7.0)# Image-to-Image / Inpaintingdenoise_mask: DenoiseMaskField | None = InputField(default=None)# Scheduler (if model-specific)scheduler: Literal["euler", "heun", "lcm"] = InputField(default="euler")def invoke(self, context: InvocationContext) -> LatentsOutput:# 1. Generate noisenoise = get_noise_newmodel(seed, height, width, ...)# 2. Pack latents (if needed)x = pack_newmodel(latents)# 3. Compute scheduletimesteps = get_schedule_newmodel(num_steps, denoising_start, denoising_end)# 4. Denoising loopx = denoise(model=transformer,x=x,timesteps=timesteps,conditioning=conditioning,cfg_scale=self.cfg_scale,inpaint_extension=inpaint_extension, # For inpainting)# 5. Unpack latentslatents = unpack_newmodel(x)return LatentsOutput(latents=latents) -
VAE Encode Invocation
invokeai/app/invocations/[newmodel]_vae_encode.py @invocation("newmodel_vae_encode", title="Image to Latents - NewModel", ...)class NewModelVaeEncodeInvocation(BaseInvocation):image: ImageField = InputField()vae: VAEField = InputField()def invoke(self, context: InvocationContext) -> LatentsOutput:image = context.images.get_pil(self.image.image_name)image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))with context.models.load(self.vae.vae) as vae:latent_dist = vae.encode(image_tensor)latents = latent_dist.mode() # Deterministicreturn LatentsOutput(latents=latents) -
VAE Decode Invocation
invokeai/app/invocations/[newmodel]_vae_decode.py @invocation("newmodel_vae_decode", title="Latents to Image - NewModel", ...)class NewModelVaeDecodeInvocation(BaseInvocation):latents: LatentsField = InputField()vae: VAEField = InputField()def invoke(self, context: InvocationContext) -> ImageOutput:latents = context.tensors.load(self.latents.latents_name)with context.models.load(self.vae.vae) as vae:# Example FLUX.2: BN denormalization before decodeif hasattr(vae, "bn"):latents = self._bn_denormalize(latents, vae)image = vae.decode(latents).sampleimage = (image / 2 + 0.5).clamp(0, 1)return ImageOutput(image=image)
5. Backend: Sampling and Denoise
Section titled “5. Backend: Sampling and Denoise”This is where the actual mathematical implementation of the model lives.
-
Sampling Utilities
invokeai/backend/[newmodel]/sampling_utils.py def get_noise_newmodel(num_samples: int,height: int,width: int,seed: int,device: torch.device,dtype: torch.dtype,) -> torch.Tensor:"""Generate noise for NewModel.Example FLUX.2: 32 latent channels (vs 16 for FLUX.1)"""latent_channels = 32 # Model-specificlatent_h = height // 8latent_w = width // 8generator = torch.Generator(device=device).manual_seed(seed)return torch.randn((num_samples, latent_channels, latent_h, latent_w),generator=generator,device=device,dtype=dtype,)def pack_newmodel(x: torch.Tensor) -> torch.Tensor:"""Pack latents for transformer input.Example FLUX: 2×2 patches → (B, H/2*W/2, C*4)"""return rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)def unpack_newmodel(x: torch.Tensor, height: int, width: int) -> torch.Tensor:"""Unpack transformer output to latents."""return rearrange(x, "b (h w) (c ph pw) -> b c (h ph) (w pw)",h=height // 16, w=width // 16, ph=2, pw=2)def get_schedule_newmodel(num_steps: int,denoising_start: float = 0.0,denoising_end: float = 1.0,) -> list[float]:"""Create timestep schedule.Example FLUX.2 Klein: Linear schedule from 1.0 → 0.0"""start_step = int(num_steps * denoising_start)end_step = int(num_steps * denoising_end)sigmas = torch.linspace(1.0, 0.0, num_steps + 1)return sigmas[start_step:end_step + 1].tolist()def generate_img_ids_newmodel(batch_size: int, height: int, width: int) -> torch.Tensor:"""Generate position IDs for transformer.Example FLUX.2: 4D position IDs (T, H, W, L)"""# Model-specific position encodingpassIf the architecture supports external noise, prefer extending the standard
invokeai/app/invocations/noise.pynode’snoise_typeselector instead of adding a brand new noise node. Only add a dedicated noise invocation when the architecture’s noise tensor rank or layout cannot be expressed by the standard node. -
Denoise Function
invokeai/backend/[newmodel]/denoise.py def denoise(model: nn.Module,img: torch.Tensor,img_ids: torch.Tensor,txt: torch.Tensor,txt_ids: torch.Tensor,timesteps: list[float],cfg_scale: list[float],neg_txt: torch.Tensor | None = None,neg_txt_ids: torch.Tensor | None = None,scheduler: Any = None,inpaint_extension: RectifiedFlowInpaintExtension | None = None,step_callback: Callable | None = None,) -> torch.Tensor:"""Main denoising loop.Example FLUX.2 Klein:- No guidance_embeds (unlike FLUX.1 Dev)- Supports Euler, Heun, LCM schedulers- Integration with RectifiedFlowInpaintExtension"""total_steps = len(timesteps) - 1for step_index in range(total_steps):t_curr = timesteps[step_index]t_prev = timesteps[step_index + 1]# CFGif cfg_scale[step_index] > 1.0 and neg_txt is not None:pred_pos = model(img, t_curr, txt, txt_ids, img_ids)pred_neg = model(img, t_curr, neg_txt, neg_txt_ids, img_ids)pred = pred_neg + cfg_scale[step_index] * (pred_pos - pred_neg)else:pred = model(img, t_curr, txt, txt_ids, img_ids)# Scheduler step or manual Eulerif scheduler is not None:img = scheduler.step(pred, t_curr, img).prev_sampleelse:# Manual Euler: x = x + (t_prev - t_curr) * predimg = img + (t_prev - t_curr) * pred# Inpainting mergeif inpaint_extension is not None:img = inpaint_extension.merge_intermediate_latents_with_init_latents(img, t_prev)# Progress callbackif step_callback:step_callback(PipelineIntermediateState(step=step_index + 1, ...))return img -
Scheduler (if model-specific)
invokeai/backend/[newmodel]/schedulers.py # Existing schedulers in invokeai/backend/flux/schedulers.py:# - FlowMatchEulerDiscreteScheduler# - FlowMatchHeunDiscreteScheduler# - FlowMatchLCMSchedulerNEWMODEL_SCHEDULER_MAP = {"euler": FlowMatchEulerDiscreteScheduler,"heun": FlowMatchHeunDiscreteScheduler,"lcm": FlowMatchLCMScheduler,}
6. Frontend: Graph Building
Section titled “6. Frontend: Graph Building”The UI doesn’t know about Python functions; it only knows how to build graphs of Invocations.
-
Create Graph Builder
invokeai/frontend/web/src/features/nodes/util/graph/generation/buildNewModelGraph.ts export const buildNewModelGraph = async (arg: GraphBuilderArg): Promise<GraphBuilderResult> => {const { state, manager } = arg;const { model } = state.params;const g = new Graph();// 1. Model Loaderconst modelLoader = g.addNode({id: NEWMODEL_MODEL_LOADER,type: 'newmodel_model_loader',model: Graph.getModelMetadataField(model),});// 2. Text Encoderconst positivePrompt = g.addNode({id: POSITIVE_CONDITIONING,type: 'newmodel_text_encode',prompt: positivePromptText,});g.addEdge(modelLoader, 'encoder', positivePrompt, 'encoder');// 3. Denoise Nodeconst denoise = g.addNode({id: NEWMODEL_DENOISE,type: 'newmodel_denoise',steps,cfg_scale: cfg,scheduler: newmodelScheduler,denoising_start: 0,denoising_end: 1,});g.addEdge(modelLoader, 'transformer', denoise, 'transformer');g.addEdge(positivePrompt, 'conditioning', denoise, 'positive_conditioning');// 4. VAE Decodeconst l2i = g.addNode({id: NEWMODEL_VAE_DECODE,type: 'newmodel_vae_decode',});g.addEdge(modelLoader, 'vae', l2i, 'vae');g.addEdge(denoise, 'latents', l2i, 'latents');// 5. Generation Mode Handlinglet canvasOutput: Invocation<ImageOutputNodes> = l2i;switch (generationMode) {case 'txt2img':canvasOutput = addTextToImage({ g, state, denoise, l2i });g.upsertMetadata({ generation_mode: 'newmodel_txt2img' });break;case 'img2img':const i2l = g.addNode({ type: 'newmodel_vae_encode' });canvasOutput = await addImageToImage({ g, state, manager, denoise, l2i, i2l, ... });g.upsertMetadata({ generation_mode: 'newmodel_img2img' });break;case 'inpaint':canvasOutput = await addInpaint({ g, state, manager, denoise, l2i, i2l, ... });g.upsertMetadata({ generation_mode: 'newmodel_inpaint' });break;case 'outpaint':canvasOutput = await addOutpaint({ g, state, manager, denoise, l2i, i2l, ... });g.upsertMetadata({ generation_mode: 'newmodel_outpaint' });break;}return { g, noise, denoise, posCond: positivePrompt, ... };}; -
Register Graph Builder
invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts switch (base) {case 'sd-1':case 'sd-2':case 'sdxl':return buildSD1Graph(arg);case 'flux':return buildFLUXGraph(arg);case 'flux2':return buildFLUXGraph(arg); // FLUX.2 uses the same buildercase 'sd-3':return buildSD3Graph(arg);case 'z-image':return buildZImageGraph(arg);case 'newmodel': // NEWreturn buildNewModelGraph(arg);} -
Update Type Definitions
invokeai/frontend/web/src/features/nodes/util/graph/types.ts // Add node types:export type ImageOutputNodes =| 'l2i' | 'flux_vae_decode' | 'flux2_vae_decode'| 'sd3_l2i' | 'newmodel_vae_decode';export type LatentToImageNodes =| 'l2i' | 'flux_vae_decode' | 'flux2_vae_decode'| 'sd3_l2i' | 'newmodel_vae_decode';export type ImageToLatentsNodes =| 'i2l' | 'flux_vae_encode' | 'flux2_vae_encode'| 'sd3_i2l' | 'newmodel_vae_encode';export type DenoiseLatentsNodes =| 'denoise_latents' | 'flux_denoise' | 'flux2_denoise'| 'sd3_denoise' | 'newmodel_denoise';export type MainModelLoaderNodes =| 'main_model_loader' | 'flux_model_loader' | 'flux2_klein_model_loader'| 'sd3_model_loader' | 'newmodel_model_loader'; -
Update Generation Mode Utilities
Update
addImageToImage.ts,addInpaint.ts, andaddOutpaint.tsto recognize your denoise node type.invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts // Extend the type check:if (denoise.type === 'cogview4_denoise' ||denoise.type === 'flux_denoise' ||denoise.type === 'flux2_denoise' ||denoise.type === 'newmodel_denoise' // NEW) {// Rectified flow models: denoising_start instead of noise}
7. Frontend: State Management
Section titled “7. Frontend: State Management”-
Add Parameter State
invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts // Extend state interface:interface ParamsState {// Existing fieldsfluxScheduler: 'euler' | 'heun' | 'lcm';zImageScheduler: 'euler' | 'heun' | 'lcm';// NEW: NewModel specific parametersnewmodelScheduler: 'euler' | 'heun' | 'lcm';newmodelVaeModel: ParameterVAEModel | null;newmodelEncoderModel: ParameterModel | null;}// Initial state:const initialState: ParamsState = {newmodelScheduler: 'euler',newmodelVaeModel: null,newmodelEncoderModel: null,};// Reducers:reducers: {setNewmodelScheduler: (state, action: PayloadAction<'euler' | 'heun' | 'lcm'>) => {state.newmodelScheduler = action.payload;},newmodelVaeModelSelected: (state, action: PayloadAction<ParameterVAEModel | null>) => {state.newmodelVaeModel = action.payload;},newmodelEncoderModelSelected: (state, action: PayloadAction<ParameterModel | null>) => {state.newmodelEncoderModel = action.payload;},} -
Create Selectors
invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts export const selectNewmodelScheduler = createSelector(selectParamsSlice,(params) => params.newmodelScheduler);export const selectNewmodelVaeModel = createSelector(selectParamsSlice,(params) => params.newmodelVaeModel);export const selectNewmodelEncoderModel = createSelector(selectParamsSlice,(params) => params.newmodelEncoderModel);
8. Frontend: Parameter Recall
Section titled “8. Frontend: Parameter Recall”Ensure users can extract parameters from previously generated images.
// Add parameter recall handlers:const recallNewmodelScheduler = (metadata: CoreMetadata) => { if (metadata.scheduler) { dispatch(setNewmodelScheduler(metadata.scheduler)); }};
const recallNewmodelVaeModel = async (metadata: CoreMetadata) => { if (metadata.vae) { const vaeModel = await fetchModelConfig(metadata.vae); dispatch(newmodelVaeModelSelected(vaeModel)); }};
const recallNewmodelEncoderModel = async (metadata: CoreMetadata) => { if (metadata.encoder_model) { const encoderModel = await fetchModelConfig(metadata.encoder_model); dispatch(newmodelEncoderModelSelected(encoderModel)); }};9. Metadata and Generation Modes
Section titled “9. Metadata and Generation Modes”-
Add Generation Modes
invokeai/app/invocations/metadata.py GENERATION_MODES = Literal[# Existing modes"txt2img", "img2img", "inpaint", "outpaint","sdxl_txt2img", "sdxl_img2img", "sdxl_inpaint", "sdxl_outpaint","flux_txt2img", "flux_img2img", "flux_inpaint", "flux_outpaint","flux2_txt2img", "flux2_img2img", "flux2_inpaint", "flux2_outpaint","sd3_txt2img", "sd3_img2img", "sd3_inpaint", "sd3_outpaint",# NEW:"newmodel_txt2img","newmodel_img2img","newmodel_inpaint","newmodel_outpaint",] -
Extend
CoreMetadata(if needed)invokeai/app/invocations/metadata.py @invocation_output("core_metadata_output")class CoreMetadataOutput(BaseInvocationOutput):# Existing fieldsmodel: ModelIdentifierField | None = Nonesteps: int | None = Nonecfg_scale: float | None = None# NEW: Model-specific metadata fieldsnewmodel_encoder: ModelIdentifierField | None = Nonenewmodel_custom_param: float | None = None
10. Starter Models
Section titled “10. Starter Models”To allow users to easily download your model from the Model Manager UI, add it to the starter models list.
# Main Modelnewmodel_main = StarterModel( name="NewModel Main", base=BaseModelType.NewModel, source="organization/newmodel-main", # HuggingFace repo description="NewModel main transformer. ~10GB", type=ModelType.Main,)
# VAE (if separate)newmodel_vae = StarterModel( name="NewModel VAE", base=BaseModelType.NewModel, source="organization/newmodel::vae", # Submodel syntax description="NewModel VAE. ~500MB", type=ModelType.VAE,)
# Text Encoder (if separate)newmodel_encoder = StarterModel( name="NewModel Encoder", base=BaseModelType.Any, source="organization/newmodel::text_encoder+tokenizer", description="NewModel text encoder. ~5GB", type=ModelType.TextEncoder,)
# Quantized variantsnewmodel_fp8 = StarterModel( name="NewModel (FP8)", base=BaseModelType.NewModel, source="https://huggingface.co/org/newmodel-fp8/resolve/main/model.safetensors", description="FP8 quantized version. ~5GB", type=ModelType.Main, dependencies=[newmodel_vae, newmodel_encoder], # Dependencies!)
# Add to STARTER_MODELS list:STARTER_MODELS: list[StarterModel] = [ # ... existing models newmodel_main, newmodel_vae, newmodel_encoder, newmodel_fp8,]11. Optional Features
Section titled “11. Optional Features”ControlNet Support
Section titled “ControlNet Support”Backend Config — invokeai/backend/model_manager/configs/controlnet.py:
@ModelConfigFactory.registerclass ControlNet_Checkpoint_NewModel_Config(ControlNet_Checkpoint_Base): base: Literal[BaseModelType.NewModel] = BaseModelType.NewModel
@classmethod def from_model_on_disk(cls, mod: ModelOnDisk, ...) -> Self: if not _is_newmodel_controlnet(mod.state_dict): raise NotAMatchError() return cls(...)Backend Invocation — invokeai/app/invocations/[newmodel]_controlnet.py:
@invocation("newmodel_controlnet", ...)class NewModelControlNetInvocation(BaseInvocation): image: ImageField = InputField() controlnet_model: ControlNetField = InputField() control_weight: float = InputField(default=1.0)
def invoke(self, context) -> ControlNetOutput: # Compute ControlNet conditioning passFrontend Graph:
const { controlNets } = await addControlNets({ g, manager, denoise });IP-Adapter / Reference Images
Section titled “IP-Adapter / Reference Images”Backend Invocation — invokeai/app/invocations/[newmodel]_ip_adapter.py:
@invocation("newmodel_ip_adapter", ...)class NewModelIPAdapterInvocation(BaseInvocation): image: ImageField = InputField() ip_adapter_model: IPAdapterField = InputField() weight: float = InputField(default=1.0)Frontend Graph:
const { ipAdapters } = await addIPAdapters({ g, manager, denoise });LoRA Support
Section titled “LoRA Support”Backend Config — invokeai/backend/model_manager/configs/lora.py:
@ModelConfigFactory.registerclass LoRA_LyCORIS_NewModel_Config(LoRA_LyCORIS_Base): base: Literal[BaseModelType.NewModel] = BaseModelType.NewModelBackend Model Loader Integration:
class NewModelModelLoaderOutput(BaseInvocationOutput): transformer: TransformerField # TransformerField already contains loras: list[LoRAField]Frontend Graph:
const { loras } = await addLoRAs({ g, manager, denoise, modelLoader });Scheduler UI
Section titled “Scheduler UI”Frontend Component — invokeai/frontend/web/src/features/parameters/components/NewModelScheduler.tsx:
export const NewModelSchedulerSelect = () => { const dispatch = useAppDispatch(); const scheduler = useAppSelector(selectNewmodelScheduler);
return ( <Select value={scheduler} onChange={(value) => dispatch(setNewmodelScheduler(value))} options={[ { value: 'euler', label: 'Euler' }, { value: 'heun', label: 'Heun' }, { value: 'lcm', label: 'LCM' }, ]} /> );};Summary: Minimal Integration
Section titled “Summary: Minimal Integration”For a minimal txt2img integration, the following files are required:
Directoryinvokeai
Directoryapp/invocations
- metadata.py
[newmodel]_model_loader.py[newmodel]_text_encoder.py[newmodel]_denoise.py[newmodel]_vae_decode.py
Directorybackend
Directorymodel_manager
- taxonomy.py
Directoryconfigs
- main.py
- factory.py
Directoryload/model_loaders
[newmodel].py
Directory
[newmodel]- sampling_utils.py
- denoise.py
Directoryfrontend/web/src/features
Directorynodes/util/graph
- generation/buildNewModelGraph.ts
- types.ts
- queue/hooks/useEnqueueCanvas.ts
- controlLayers/store/paramsSlice.ts
For img2img / inpaint / outpaint, additionally:
Directoryinvokeai
Directoryapp/invocations
[newmodel]_vae_encode.py
Directoryfrontend/web/src/features/nodes/util/graph/generation
- addImageToImage.ts
- addInpaint.ts
- addOutpaint.ts
Reference: Existing Implementations
Section titled “Reference: Existing Implementations”| Feature | FLUX.1 | FLUX.2 Klein | SD3 | SDXL | Z-Image |
|---|---|---|---|---|---|
| Latent Channels | 16 | 32 | 16 | 4 | 32 |
| Text Encoder | CLIP + T5 | Qwen3 | CLIP×3 + T5 | CLIP×2 | Qwen3 |
| VAE | 16ch | 32ch+BN | 16ch | 4ch | 32ch |
| CFG | Optional | Optional | Yes | Yes | Optional |
| Guidance Embed | Dev only | No | No | No | No |
| Pack/Unpack | Yes | Yes | No | No | Yes |