From 371d90de0e40922134086e294f3fc3cbc7c74105 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Wed, 4 Feb 2026 16:57:59 +0100
Subject: [PATCH 01/50] feat: initial external model support
---
docs/contributing/EXTERNAL_PROVIDERS.md | 57 ++
docs/contributing/index.md | 4 +
invokeai/app/api/dependencies.py | 10 +
invokeai/app/api/routers/app_info.py | 138 +++-
invokeai/app/api/routers/model_manager.py | 16 +-
.../invocations/external_image_generation.py | 148 +++++
.../app/services/config/config_default.py | 10 +
.../services/external_generation/__init__.py | 23 +
.../services/external_generation/errors.py | 18 +
.../external_generation_base.py | 40 ++
.../external_generation_common.py | 55 ++
.../external_generation_default.py | 291 +++++++++
.../external_generation/image_utils.py | 19 +
.../external_generation/providers/__init__.py | 4 +
.../external_generation/providers/gemini.py | 249 ++++++++
.../external_generation/providers/openai.py | 105 ++++
invokeai/app/services/invocation_services.py | 3 +
.../model_install/model_install_common.py | 17 +-
.../model_install/model_install_default.py | 72 ++-
.../model_records/model_records_base.py | 19 +-
.../app/services/shared/invocation_context.py | 8 +
.../model_manager/configs/external_api.py | 80 +++
.../backend/model_manager/configs/factory.py | 2 +
.../backend/model_manager/starter_models.py | 112 ++++
invokeai/backend/model_manager/taxonomy.py | 4 +
invokeai/frontend/web/public/locales/en.json | 38 +-
.../components/StagingArea/context.tsx | 11 +-
.../components/StagingArea/shared.test.ts | 27 +
.../components/StagingArea/shared.ts | 5 +-
.../controlLayers/store/paramsSlice.test.ts | 61 ++
.../controlLayers/store/paramsSlice.ts | 74 ++-
.../controlLayers/store/validators.ts | 12 +-
.../web/src/features/modelManagerV2/models.ts | 13 +-
.../store/installModelsStore.ts | 7 +-
.../ExternalProvidersForm.tsx | 281 +++++++++
.../LaunchpadForm/LaunchpadForm.tsx | 12 +-
.../subpanels/InstallModels.tsx | 21 +-
.../ModelManagerPanel/ModelFormatBadge.tsx | 2 +
.../ModelPanel/Fields/BaseModelSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelFormatSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelTypeSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelVariantSelect.tsx | 4 +-
.../Fields/PredictionTypeSelect.tsx | 4 +-
.../subpanels/ModelPanel/ModelEdit.tsx | 192 +++++-
.../subpanels/ModelPanel/ModelView.tsx | 23 +-
.../web/src/features/nodes/types/common.ts | 3 +
.../generation/buildExternalGraph.test.ts | 154 +++++
.../graph/generation/buildExternalGraph.ts | 129 ++++
.../Bbox/BboxAspectRatioSelect.test.tsx | 44 ++
.../components/Bbox/BboxAspectRatioSelect.tsx | 5 +-
.../DimensionsAspectRatioSelect.test.tsx | 44 ++
.../DimensionsAspectRatioSelect.tsx | 10 +-
.../MainModel/mainModelPickerUtils.test.ts | 61 ++
.../MainModel/mainModelPickerUtils.ts | 14 +
.../parameters/components/ModelPicker.tsx | 24 +-
.../features/queue/hooks/useEnqueueCanvas.ts | 3 +
.../queue/hooks/useEnqueueGenerate.ts | 3 +
.../web/src/features/queue/store/readiness.ts | 23 +-
.../MainModelPicker.tsx | 11 +-
.../ExternalProviderStatusList.tsx | 39 ++
.../SettingsModal/SettingsModal.tsx | 8 +-
.../externalProviderStatusUtils.test.ts | 38 ++
.../externalProviderStatusUtils.ts | 26 +
.../layouts/InitialStateMainModelPicker.tsx | 11 +-
.../web/src/services/api/endpoints/appInfo.ts | 40 +-
.../src/services/api/hooks/modelsByType.ts | 11 +-
.../frontend/web/src/services/api/schema.ts | 588 +++++++++++++++++-
.../frontend/web/src/services/api/types.ts | 51 ++
.../test_external_image_generation.py | 120 ++++
tests/app/routers/test_app_info.py | 93 +++
tests/app/routers/test_model_manager.py | 71 +++
.../test_external_generation_service.py | 243 ++++++++
.../test_external_provider_adapters.py | 346 +++++++++++
.../model_install/test_model_install.py | 16 +
.../app/services/model_load/test_load_api.py | 23 +
.../model_manager/test_external_api_config.py | 54 ++
tests/conftest.py | 2 +
77 files changed, 4478 insertions(+), 133 deletions(-)
create mode 100644 docs/contributing/EXTERNAL_PROVIDERS.md
create mode 100644 invokeai/app/invocations/external_image_generation.py
create mode 100644 invokeai/app/services/external_generation/__init__.py
create mode 100644 invokeai/app/services/external_generation/errors.py
create mode 100644 invokeai/app/services/external_generation/external_generation_base.py
create mode 100644 invokeai/app/services/external_generation/external_generation_common.py
create mode 100644 invokeai/app/services/external_generation/external_generation_default.py
create mode 100644 invokeai/app/services/external_generation/image_utils.py
create mode 100644 invokeai/app/services/external_generation/providers/__init__.py
create mode 100644 invokeai/app/services/external_generation/providers/gemini.py
create mode 100644 invokeai/app/services/external_generation/providers/openai.py
create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
create mode 100644 tests/app/invocations/test_external_image_generation.py
create mode 100644 tests/app/routers/test_app_info.py
create mode 100644 tests/app/routers/test_model_manager.py
create mode 100644 tests/app/services/external_generation/test_external_generation_service.py
create mode 100644 tests/app/services/external_generation/test_external_provider_adapters.py
create mode 100644 tests/backend/model_manager/test_external_api_config.py
diff --git a/docs/contributing/EXTERNAL_PROVIDERS.md b/docs/contributing/EXTERNAL_PROVIDERS.md
new file mode 100644
index 00000000000..c2371971db9
--- /dev/null
+++ b/docs/contributing/EXTERNAL_PROVIDERS.md
@@ -0,0 +1,57 @@
+# External Provider Integration
+
+This guide covers how to add new external image generation providers and model configs.
+
+## Provider Adapter Steps
+
+1) Create a provider adapter in `invokeai/app/services/external_generation/providers/` that inherits from `ExternalProvider`.
+2) Implement `is_configured()` using `InvokeAIAppConfig` fields, and `generate()` to map `ExternalGenerationRequest` to the provider API.
+3) Use helpers from `invokeai/app/services/external_generation/image_utils.py` for image encoding/decoding.
+4) Raise `ExternalProviderRequestError` on non-200 responses or empty payloads.
+5) Register the provider in `invokeai/app/api/dependencies.py` when building the `ExternalGenerationService` registry.
+
+## Config + Env Vars
+
+Add provider API keys to `InvokeAIAppConfig` with the `INVOKEAI_` prefix:
+
+- `INVOKEAI_EXTERNAL_GEMINI_API_KEY`
+- `INVOKEAI_EXTERNAL_OPENAI_API_KEY`
+
+These can also be set in `invokeai.yaml` under `external_gemini_api_key` and `external_openai_api_key`.
+
+## Example External Model Config
+
+External models are stored in the model manager like any other config. This example can be used as the `config` payload
+for `POST /api/v2/models/install?source=external://openai/gpt-image-1`:
+
+```json
+{
+ "key": "openai_gpt_image_1",
+ "name": "OpenAI GPT-Image-1",
+ "base": "external",
+ "type": "external_image_generator",
+ "format": "external_api",
+ "provider_id": "openai",
+ "provider_model_id": "gpt-image-1",
+ "capabilities": {
+ "modes": ["txt2img", "img2img", "inpaint"],
+ "supports_negative_prompt": true,
+ "supports_seed": true,
+ "supports_guidance": true,
+ "supports_reference_images": false,
+ "max_images_per_request": 1
+ },
+ "default_settings": {
+ "width": 1024,
+ "height": 1024,
+ "steps": 30
+ },
+ "tags": ["external", "openai"],
+ "is_default": false
+}
+```
+
+Notes:
+
+- `path`, `source`, and `hash` will auto-populate if omitted.
+- Set `capabilities` conservatively; the external generation service enforces them at runtime.
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index 79c1082746d..b8002a18024 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -8,6 +8,10 @@ We welcome contributions, whether features, bug fixes, code cleanup, testing, co
If you’d like to help with development, please see our [development guide](contribution_guides/development.md).
+## External Providers
+
+If you are adding external image generation providers or configs, see our [external provider integration guide](EXTERNAL_PROVIDERS.md).
+
**New Contributors:** If you’re unfamiliar with contributing to open source projects, take a look at our [new contributor guide](contribution_guides/newContributorChecklist.md).
## Nodes
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index 466a57f804c..79dde681d32 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -13,6 +13,8 @@
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.download.download_default import DownloadQueueService
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.app.services.external_generation.providers import GeminiProvider, OpenAIProvider
from invokeai.app.services.events.events_fastapievents import FastAPIEventService
from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
@@ -136,6 +138,13 @@ def initialize(
),
)
download_queue_service = DownloadQueueService(app_config=configuration, event_bus=events)
+ external_generation = ExternalGenerationService(
+ providers={
+ GeminiProvider.provider_id: GeminiProvider(app_config=configuration, logger=logger),
+ OpenAIProvider.provider_id: OpenAIProvider(app_config=configuration, logger=logger),
+ },
+ logger=logger,
+ )
model_images_service = ModelImageFileStorageDisk(model_images_folder / "model_images")
model_manager = ModelManagerService.build_model_manager(
app_config=configuration,
@@ -174,6 +183,7 @@ def initialize(
model_relationships=model_relationships,
model_relationship_records=model_relationship_records,
download_queue=download_queue_service,
+ external_generation=external_generation,
names=names,
performance_statistics=performance_statistics,
session_processor=session_processor,
diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py
index d8f3bb2f807..cbb3e6fb47d 100644
--- a/invokeai/app/api/routers/app_info.py
+++ b/invokeai/app/api/routers/app_info.py
@@ -2,12 +2,18 @@
from importlib.metadata import distributions
import torch
-from fastapi import Body
+from fastapi import Body, HTTPException, Path
from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
-from invokeai.app.services.config.config_default import InvokeAIAppConfig, get_config
+from invokeai.app.services.config.config_default import (
+ DefaultInvokeAIAppConfig,
+ InvokeAIAppConfig,
+ get_config,
+ load_and_migrate_config,
+)
+from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
from invokeai.backend.util.logging import logging
@@ -41,7 +47,7 @@ async def get_version() -> AppVersion:
async def get_app_deps() -> dict[str, str]:
deps: dict[str, str] = {dist.metadata["Name"]: dist.version for dist in distributions()}
try:
- cuda = torch.version.cuda or "N/A"
+ cuda = getattr(getattr(torch, "version", None), "cuda", None) or "N/A" # pyright: ignore[reportAttributeAccessIssue]
except Exception:
cuda = "N/A"
@@ -64,6 +70,29 @@ class InvokeAIAppConfigWithSetFields(BaseModel):
config: InvokeAIAppConfig = Field(description="The InvokeAI App Config")
+class ExternalProviderStatusModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ configured: bool = Field(description="Whether credentials are configured for the provider")
+ message: str | None = Field(default=None, description="Optional provider status detail")
+
+
+class ExternalProviderConfigUpdate(BaseModel):
+ api_key: str | None = Field(default=None, description="API key for the external provider")
+ base_url: str | None = Field(default=None, description="Optional base URL override for the provider")
+
+
+class ExternalProviderConfigModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ api_key_configured: bool = Field(description="Whether an API key is configured")
+ base_url: str | None = Field(default=None, description="Optional base URL override")
+
+
+EXTERNAL_PROVIDER_FIELDS: dict[str, tuple[str, str]] = {
+ "gemini": ("external_gemini_api_key", "external_gemini_base_url"),
+ "openai": ("external_openai_api_key", "external_openai_base_url"),
+}
+
+
@app_router.get(
"/runtime_config", operation_id="get_runtime_config", status_code=200, response_model=InvokeAIAppConfigWithSetFields
)
@@ -72,6 +101,109 @@ async def get_runtime_config() -> InvokeAIAppConfigWithSetFields:
return InvokeAIAppConfigWithSetFields(set_fields=config.model_fields_set, config=config)
+@app_router.get(
+ "/external_providers/status",
+ operation_id="get_external_provider_statuses",
+ status_code=200,
+ response_model=list[ExternalProviderStatusModel],
+)
+async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
+ statuses = ApiDependencies.invoker.services.external_generation.get_provider_statuses()
+ return [status_to_model(status) for status in statuses.values()]
+
+
+@app_router.get(
+ "/external_providers/config",
+ operation_id="get_external_provider_configs",
+ status_code=200,
+ response_model=list[ExternalProviderConfigModel],
+)
+async def get_external_provider_configs() -> list[ExternalProviderConfigModel]:
+ config = get_config()
+ return [_build_external_provider_config(provider_id, config) for provider_id in EXTERNAL_PROVIDER_FIELDS]
+
+
+@app_router.post(
+ "/external_providers/config/{provider_id}",
+ operation_id="set_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def set_external_provider_config(
+ provider_id: str = Path(description="The external provider identifier"),
+ update: ExternalProviderConfigUpdate = Body(description="External provider configuration settings"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ updates: dict[str, str | None] = {}
+
+ if update.api_key is not None:
+ api_key = update.api_key.strip()
+ updates[api_key_field] = api_key or None
+ if update.base_url is not None:
+ base_url = update.base_url.strip()
+ updates[base_url_field] = base_url or None
+
+ if not updates:
+ raise HTTPException(status_code=400, detail="No external provider config fields provided")
+
+ _apply_external_provider_update(updates)
+ return _build_external_provider_config(provider_id, get_config())
+
+
+@app_router.delete(
+ "/external_providers/config/{provider_id}",
+ operation_id="reset_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def reset_external_provider_config(
+ provider_id: str = Path(description="The external provider identifier"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ _apply_external_provider_update({api_key_field: None, base_url_field: None})
+ return _build_external_provider_config(provider_id, get_config())
+
+
+def status_to_model(status: ExternalProviderStatus) -> ExternalProviderStatusModel:
+ return ExternalProviderStatusModel(
+ provider_id=status.provider_id,
+ configured=status.configured,
+ message=status.message,
+ )
+
+
+def _get_external_provider_fields(provider_id: str) -> tuple[str, str]:
+ if provider_id not in EXTERNAL_PROVIDER_FIELDS:
+ raise HTTPException(status_code=404, detail=f"Unknown external provider '{provider_id}'")
+ return EXTERNAL_PROVIDER_FIELDS[provider_id]
+
+
+def _apply_external_provider_update(updates: dict[str, str | None]) -> None:
+ runtime_config = get_config()
+ config_path = runtime_config.config_file_path
+ if config_path.exists():
+ file_config = load_and_migrate_config(config_path)
+ else:
+ file_config = DefaultInvokeAIAppConfig()
+
+ for config in (runtime_config, file_config):
+ config.update_config(updates)
+ for field_name, value in updates.items():
+ if value is None:
+ config.model_fields_set.discard(field_name)
+
+ file_config.write_file(config_path, as_example=False)
+
+
+def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ return ExternalProviderConfigModel(
+ provider_id=provider_id,
+ api_key_configured=bool(getattr(config, api_key_field)),
+ base_url=getattr(config, base_url_field),
+ )
+
+
@app_router.get(
"/logging",
operation_id="get_log_level",
diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py
index f67393f2ea2..426a081c072 100644
--- a/invokeai/app/api/routers/model_manager.py
+++ b/invokeai/app/api/routers/model_manager.py
@@ -29,6 +29,7 @@
)
from invokeai.app.services.orphaned_models import OrphanedModelInfo
from invokeai.app.util.suppress_output import SuppressOutput
+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 (
Main_Checkpoint_SD1_Config,
@@ -144,8 +145,16 @@ async def list_model_records(
found_models.extend(
record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format)
)
- for model in found_models:
+ for index, model in enumerate(found_models):
model = add_cover_image_to_model_config(model, ApiDependencies)
+ if isinstance(model, ExternalApiModelConfig):
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == model.source), None)
+ if starter_match is not None:
+ if starter_match.capabilities is not None:
+ setattr(model, "capabilities", starter_match.capabilities)
+ if starter_match.default_settings is not None:
+ setattr(model, "default_settings", starter_match.default_settings)
+ found_models[index] = model
return ModelsList(models=found_models)
@@ -165,6 +174,8 @@ async def list_missing_models() -> ModelsList:
missing_models: list[AnyModelConfig] = []
for model_config in record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
if not (models_path / model_config.path).resolve().exists():
missing_models.append(model_config)
@@ -248,7 +259,8 @@ async def reidentify_model(
result.config.name = config.name
result.config.description = config.description
result.config.cover_image = config.cover_image
- result.config.trigger_phrases = config.trigger_phrases
+ if hasattr(result.config, "trigger_phrases") and hasattr(config, "trigger_phrases"):
+ setattr(result.config, "trigger_phrases", getattr(config, "trigger_phrases"))
result.config.source = config.source
result.config.source_type = config.source_type
diff --git a/invokeai/app/invocations/external_image_generation.py b/invokeai/app/invocations/external_image_generation.py
new file mode 100644
index 00000000000..c70ecb40795
--- /dev/null
+++ b/invokeai/app/invocations/external_image_generation.py
@@ -0,0 +1,148 @@
+from typing import Any
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ MetadataField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageCollectionOutput
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+
+@invocation(
+ "external_image_generation",
+ title="External Image Generation",
+ tags=["external", "generation"],
+ category="image",
+ version="1.0.0",
+)
+class ExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generate images using an external provider."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ )
+ mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode")
+ prompt: str = InputField(description="Prompt")
+ negative_prompt: str | None = InputField(default=None, description="Negative prompt")
+ seed: int | None = InputField(default=None, description=FieldDescriptions.seed)
+ num_images: int = InputField(default=1, gt=0, description="Number of images to generate")
+ width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width)
+ height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height)
+ steps: int | None = InputField(default=None, gt=0, description=FieldDescriptions.steps)
+ guidance: float | None = InputField(default=None, ge=0, description="Guidance strength")
+ init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint")
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint")
+ reference_images: list[ImageField] = InputField(default=[], description="Reference images")
+ reference_image_weights: list[float] | None = InputField(default=None, description="Reference image weights")
+ reference_image_modes: list[str] | None = InputField(default=None, description="Reference image modes")
+
+ def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
+ model_config = context.models.get_config(self.model)
+ if not isinstance(model_config, ExternalApiModelConfig):
+ raise ValueError("Selected model is not an external API model")
+
+ init_image = None
+ if self.init_image is not None:
+ init_image = context.images.get_pil(self.init_image.image_name, mode="RGB")
+
+ mask_image = None
+ if self.mask_image is not None:
+ mask_image = context.images.get_pil(self.mask_image.image_name, mode="L")
+
+ if self.reference_image_weights is not None and len(self.reference_image_weights) != len(self.reference_images):
+ raise ValueError("reference_image_weights must match reference_images length")
+
+ if self.reference_image_modes is not None and len(self.reference_image_modes) != len(self.reference_images):
+ raise ValueError("reference_image_modes must match reference_images length")
+
+ reference_images: list[ExternalReferenceImage] = []
+ for index, image_field in enumerate(self.reference_images):
+ reference_image = context.images.get_pil(image_field.image_name, mode="RGB")
+ weight = None
+ mode = None
+ if self.reference_image_weights is not None:
+ weight = self.reference_image_weights[index]
+ if self.reference_image_modes is not None:
+ mode = self.reference_image_modes[index]
+ reference_images.append(ExternalReferenceImage(image=reference_image, weight=weight, mode=mode))
+
+ request = ExternalGenerationRequest(
+ model=model_config,
+ mode=self.mode,
+ prompt=self.prompt,
+ negative_prompt=self.negative_prompt,
+ seed=self.seed,
+ num_images=self.num_images,
+ width=self.width,
+ height=self.height,
+ steps=self.steps,
+ guidance=self.guidance,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images,
+ metadata=self._build_request_metadata(),
+ )
+
+ result = context._services.external_generation.generate(request)
+
+ outputs: list[ImageField] = []
+ for generated in result.images:
+ metadata = self._build_output_metadata(model_config, result, generated.seed)
+ image_dto = context.images.save(image=generated.image, metadata=metadata)
+ outputs.append(ImageField(image_name=image_dto.image_name))
+
+ return ImageCollectionOutput(collection=outputs)
+
+ def _build_request_metadata(self) -> dict[str, Any] | None:
+ if self.metadata is None:
+ return None
+ return self.metadata.root
+
+ def _build_output_metadata(
+ self,
+ model_config: ExternalApiModelConfig,
+ result: ExternalGenerationResult,
+ image_seed: int | None,
+ ) -> MetadataField | None:
+ metadata: dict[str, Any] = {}
+
+ if self.metadata is not None:
+ metadata.update(self.metadata.root)
+
+ metadata.update(
+ {
+ "external_provider": model_config.provider_id,
+ "external_model_id": model_config.provider_model_id,
+ }
+ )
+
+ provider_request_id = getattr(result, "provider_request_id", None)
+ if provider_request_id:
+ metadata["external_request_id"] = provider_request_id
+
+ provider_metadata = getattr(result, "provider_metadata", None)
+ if provider_metadata:
+ metadata["external_provider_metadata"] = provider_metadata
+
+ if image_seed is not None:
+ metadata["external_seed"] = image_seed
+
+ if not metadata:
+ return None
+ return MetadataField(root=metadata)
diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py
index 93b2d01e1ec..0bb29a5dfa6 100644
--- a/invokeai/app/services/config/config_default.py
+++ b/invokeai/app/services/config/config_default.py
@@ -203,6 +203,16 @@ class InvokeAIAppConfig(BaseSettings):
unsafe_disable_picklescan: bool = Field(default=False, description="UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.")
allow_unknown_models: bool = Field(default=True, description="Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.")
+ # EXTERNAL PROVIDERS
+ external_gemini_api_key: Optional[str] = Field(default=None, description="API key for Gemini image generation.")
+ external_openai_api_key: Optional[str] = Field(default=None, description="API key for OpenAI image generation.")
+ external_gemini_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Gemini image generation."
+ )
+ external_openai_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for OpenAI image generation."
+ )
+
# fmt: on
model_config = SettingsConfigDict(env_prefix="INVOKEAI_", env_ignore_empty=True)
diff --git a/invokeai/app/services/external_generation/__init__.py b/invokeai/app/services/external_generation/__init__.py
new file mode 100644
index 00000000000..692da64643a
--- /dev/null
+++ b/invokeai/app/services/external_generation/__init__.py
@@ -0,0 +1,23 @@
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalGeneratedImage,
+ ExternalProviderStatus,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+
+__all__ = [
+ "ExternalGenerationRequest",
+ "ExternalGenerationResult",
+ "ExternalGeneratedImage",
+ "ExternalGenerationService",
+ "ExternalGenerationServiceBase",
+ "ExternalProvider",
+ "ExternalProviderStatus",
+ "ExternalReferenceImage",
+]
diff --git a/invokeai/app/services/external_generation/errors.py b/invokeai/app/services/external_generation/errors.py
new file mode 100644
index 00000000000..9980b39bc43
--- /dev/null
+++ b/invokeai/app/services/external_generation/errors.py
@@ -0,0 +1,18 @@
+class ExternalGenerationError(Exception):
+ """Base error for external generation."""
+
+
+class ExternalProviderNotFoundError(ExternalGenerationError):
+ """Raised when no provider is registered for a model."""
+
+
+class ExternalProviderNotConfiguredError(ExternalGenerationError):
+ """Raised when a provider is missing required credentials."""
+
+
+class ExternalProviderCapabilityError(ExternalGenerationError):
+ """Raised when a request is not supported by provider capabilities."""
+
+
+class ExternalProviderRequestError(ExternalGenerationError):
+ """Raised when a provider rejects the request or returns an error."""
diff --git a/invokeai/app/services/external_generation/external_generation_base.py b/invokeai/app/services/external_generation/external_generation_base.py
new file mode 100644
index 00000000000..2145ff5ca42
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_base.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+
+
+class ExternalProvider(ABC):
+ provider_id: str
+
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ @abstractmethod
+ def is_configured(self) -> bool:
+ raise NotImplementedError
+
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ def get_status(self) -> ExternalProviderStatus:
+ return ExternalProviderStatus(provider_id=self.provider_id, configured=self.is_configured())
+
+
+class ExternalGenerationServiceBase(ABC):
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ @abstractmethod
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ raise NotImplementedError
diff --git a/invokeai/app/services/external_generation/external_generation_common.py b/invokeai/app/services/external_generation/external_generation_common.py
new file mode 100644
index 00000000000..c1e2f4706f5
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_common.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+from PIL.Image import Image as PILImageType
+
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+
+
+@dataclass(frozen=True)
+class ExternalReferenceImage:
+ image: PILImageType
+ weight: float | None = None
+ mode: str | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGenerationRequest:
+ model: ExternalApiModelConfig
+ mode: ExternalGenerationMode
+ prompt: str
+ negative_prompt: str | None
+ seed: int | None
+ num_images: int
+ width: int
+ height: int
+ steps: int | None
+ guidance: float | None
+ init_image: PILImageType | None
+ mask_image: PILImageType | None
+ reference_images: list[ExternalReferenceImage]
+ metadata: dict[str, Any] | None
+
+
+@dataclass(frozen=True)
+class ExternalGeneratedImage:
+ image: PILImageType
+ seed: int | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGenerationResult:
+ images: list[ExternalGeneratedImage]
+ seed_used: int | None = None
+ provider_request_id: str | None = None
+ provider_metadata: dict[str, Any] | None = None
+ content_filters: dict[str, str] | None = None
+
+
+@dataclass(frozen=True)
+class ExternalProviderStatus:
+ provider_id: str
+ configured: bool
+ message: str | None = None
diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py
new file mode 100644
index 00000000000..c72e16cde8d
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_default.py
@@ -0,0 +1,291 @@
+from __future__ import annotations
+
+from logging import Logger
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderNotConfiguredError,
+ ExternalProviderNotFoundError,
+)
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalImageSize
+from invokeai.backend.model_manager.starter_models import STARTER_MODELS
+
+
+class ExternalGenerationService(ExternalGenerationServiceBase):
+ def __init__(self, providers: dict[str, ExternalProvider], logger: Logger) -> None:
+ self._providers = providers
+ self._logger = logger
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ provider = self._providers.get(request.model.provider_id)
+ if provider is None:
+ raise ExternalProviderNotFoundError(f"No external provider registered for '{request.model.provider_id}'")
+
+ if not provider.is_configured():
+ raise ExternalProviderNotConfiguredError(f"Provider '{request.model.provider_id}' is missing credentials")
+
+ request = self._refresh_model_capabilities(request)
+ request = self._bucket_request(request)
+
+ self._validate_request(request)
+ return provider.generate(request)
+
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()}
+
+ def _validate_request(self, request: ExternalGenerationRequest) -> None:
+ capabilities = request.model.capabilities
+
+ self._logger.debug(
+ "Validating external request provider=%s model=%s mode=%s supported=%s",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.mode,
+ capabilities.modes,
+ )
+
+ if request.mode not in capabilities.modes:
+ raise ExternalProviderCapabilityError(f"Mode '{request.mode}' is not supported by {request.model.name}")
+
+ if request.negative_prompt and not capabilities.supports_negative_prompt:
+ raise ExternalProviderCapabilityError(f"Negative prompts are not supported by {request.model.name}")
+
+ if request.seed is not None and not capabilities.supports_seed:
+ raise ExternalProviderCapabilityError(f"Seed control is not supported by {request.model.name}")
+
+ if request.guidance is not None and not capabilities.supports_guidance:
+ raise ExternalProviderCapabilityError(f"Guidance is not supported by {request.model.name}")
+
+ if request.reference_images and not capabilities.supports_reference_images:
+ raise ExternalProviderCapabilityError(f"Reference images are not supported by {request.model.name}")
+
+ if capabilities.max_reference_images is not None:
+ if len(request.reference_images) > capabilities.max_reference_images:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_reference_images} reference images"
+ )
+
+ if capabilities.max_images_per_request is not None and request.num_images > capabilities.max_images_per_request:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_images_per_request} images per request"
+ )
+
+ if capabilities.max_image_size is not None:
+ if request.width > capabilities.max_image_size.width or request.height > capabilities.max_image_size.height:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports a maximum size of {capabilities.max_image_size.width}x{capabilities.max_image_size.height}"
+ )
+
+ if capabilities.allowed_aspect_ratios:
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ if aspect_ratio not in capabilities.allowed_aspect_ratios:
+ size_ratio = None
+ if capabilities.aspect_ratio_sizes:
+ size_ratio = _ratio_for_size(request.width, request.height, capabilities.aspect_ratio_sizes)
+ if size_ratio is None or size_ratio not in capabilities.allowed_aspect_ratios:
+ ratio_label = size_ratio or aspect_ratio
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} does not support aspect ratio {ratio_label}"
+ )
+
+ required_modes = capabilities.input_image_required_for or ["img2img", "inpaint"]
+ if request.mode in required_modes and request.init_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires an init image for {request.model.name}"
+ )
+
+ if request.mode == "inpaint" and request.mask_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires a mask image for {request.model.name}"
+ )
+
+ def _refresh_model_capabilities(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ try:
+ from invokeai.app.api.dependencies import ApiDependencies
+
+ record = ApiDependencies.invoker.services.model_manager.store.get_model(request.model.key)
+ except Exception:
+ record = None
+
+ if not isinstance(record, ExternalApiModelConfig):
+ return request
+
+ if record.key != request.model.key:
+ return request
+
+ if record.provider_id != request.model.provider_id:
+ return request
+
+ if record.provider_model_id != request.model.provider_model_id:
+ return request
+
+ record = _apply_starter_overrides(record)
+
+ if record == request.model:
+ return request
+
+ return ExternalGenerationRequest(
+ model=record,
+ mode=request.mode,
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=request.width,
+ height=request.height,
+ steps=request.steps,
+ guidance=request.guidance,
+ init_image=request.init_image,
+ mask_image=request.mask_image,
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ )
+
+ def _bucket_request(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ capabilities = request.model.capabilities
+ if not capabilities.allowed_aspect_ratios:
+ return request
+
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ size = None
+ if capabilities.aspect_ratio_sizes:
+ size = capabilities.aspect_ratio_sizes.get(aspect_ratio)
+
+ if size is not None:
+ if request.width == size.width and request.height == size.height:
+ return request
+ return self._bucket_to_size(request, size.width, size.height, aspect_ratio)
+
+ if aspect_ratio in capabilities.allowed_aspect_ratios:
+ return request
+
+ if not capabilities.aspect_ratio_sizes:
+ return request
+
+ closest = _select_closest_ratio(
+ request.width,
+ request.height,
+ capabilities.allowed_aspect_ratios,
+ )
+ if closest is None:
+ return request
+
+ size = capabilities.aspect_ratio_sizes.get(closest)
+ if size is None:
+ return request
+
+ return self._bucket_to_size(request, size.width, size.height, closest)
+
+ def _bucket_to_size(
+ self,
+ request: ExternalGenerationRequest,
+ width: int,
+ height: int,
+ ratio: str,
+ ) -> ExternalGenerationRequest:
+ self._logger.info(
+ "Bucketing external request provider=%s model=%s %sx%s -> %sx%s (ratio %s)",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.width,
+ request.height,
+ width,
+ height,
+ ratio,
+ )
+
+ return ExternalGenerationRequest(
+ model=request.model,
+ mode=request.mode,
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=width,
+ height=height,
+ steps=request.steps,
+ guidance=request.guidance,
+ init_image=_resize_image(request.init_image, width, height, "RGB"),
+ mask_image=_resize_image(request.mask_image, width, height, "L"),
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ )
+
+
+def _format_aspect_ratio(width: int, height: int) -> str:
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _select_closest_ratio(width: int, height: int, ratios: list[str]) -> str | None:
+ ratio = width / height
+ parsed: list[tuple[str, float]] = []
+ for value in ratios:
+ parsed_ratio = _parse_ratio(value)
+ if parsed_ratio is not None:
+ parsed.append((value, parsed_ratio))
+ if not parsed:
+ return None
+ return min(parsed, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _ratio_for_size(width: int, height: int, sizes: dict[str, ExternalImageSize]) -> str | None:
+ for ratio, size in sizes.items():
+ if size.width == width and size.height == height:
+ return ratio
+ return None
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
+
+
+def _resize_image(image: PILImageType | None, width: int, height: int, mode: str) -> PILImageType | None:
+ if image is None:
+ return None
+ if image.width == width and image.height == height:
+ return image
+ return image.convert(mode).resize((width, height), Image.Resampling.LANCZOS)
+
+
+def _apply_starter_overrides(model: ExternalApiModelConfig) -> ExternalApiModelConfig:
+ source = model.source or f"external://{model.provider_id}/{model.provider_model_id}"
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == source), None)
+ if starter_match is None:
+ return model
+ updates: dict[str, object] = {}
+ if starter_match.capabilities is not None:
+ updates["capabilities"] = starter_match.capabilities
+ if starter_match.default_settings is not None:
+ updates["default_settings"] = starter_match.default_settings
+ if not updates:
+ return model
+ return model.model_copy(update=updates)
diff --git a/invokeai/app/services/external_generation/image_utils.py b/invokeai/app/services/external_generation/image_utils.py
new file mode 100644
index 00000000000..a23c1f11d66
--- /dev/null
+++ b/invokeai/app/services/external_generation/image_utils.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import base64
+import io
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+
+def encode_image_base64(image: PILImageType, format: str = "PNG") -> str:
+ buffer = io.BytesIO()
+ image.save(buffer, format=format)
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
+
+
+def decode_image_base64(encoded: str) -> PILImageType:
+ data = base64.b64decode(encoded)
+ image = Image.open(io.BytesIO(data))
+ return image.convert("RGB")
diff --git a/invokeai/app/services/external_generation/providers/__init__.py b/invokeai/app/services/external_generation/providers/__init__.py
new file mode 100644
index 00000000000..9e380fca1e1
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/__init__.py
@@ -0,0 +1,4 @@
+from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
+from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+
+__all__ = ["GeminiProvider", "OpenAIProvider"]
diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py
new file mode 100644
index 00000000000..4d43431a14a
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/gemini.py
@@ -0,0 +1,249 @@
+from __future__ import annotations
+
+import json
+import uuid
+
+import requests
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+
+class GeminiProvider(ExternalProvider):
+ provider_id = "gemini"
+ _SYSTEM_INSTRUCTION = (
+ "You are an image generation model. Always respond with an image based on the user's prompt. "
+ "Do not return text-only responses. If the user input is not an edit instruction, "
+ "interpret it as a request to create a new image."
+ )
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_gemini_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_gemini_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Gemini API key is not configured")
+
+ base_url = (self._app_config.external_gemini_base_url or "https://generativelanguage.googleapis.com").rstrip(
+ "/"
+ )
+ if not base_url.endswith("/v1") and not base_url.endswith("/v1beta"):
+ base_url = f"{base_url}/v1beta"
+ model_id = request.model.provider_model_id.removeprefix("models/")
+ endpoint = f"{base_url}/models/{model_id}:generateContent"
+
+ request_parts: list[dict[str, object]] = []
+
+ if request.init_image is not None:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(request.init_image),
+ }
+ }
+ )
+
+ request_parts.append({"text": request.prompt})
+
+ for reference in request.reference_images:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(reference.image),
+ }
+ }
+ )
+
+ generation_config: dict[str, object] = {
+ "candidateCount": request.num_images,
+ "responseModalities": ["IMAGE"],
+ }
+ aspect_ratio = _select_aspect_ratio(
+ request.width,
+ request.height,
+ request.model.capabilities.allowed_aspect_ratios,
+ )
+ system_instruction = self._SYSTEM_INSTRUCTION
+ if request.init_image is not None:
+ system_instruction = (
+ f"{system_instruction} An input image is provided. "
+ "Treat the prompt as an edit instruction and modify the image accordingly. "
+ "Do not return the original image unchanged."
+ )
+ if aspect_ratio is not None:
+ system_instruction = f"{system_instruction} Use an aspect ratio of {aspect_ratio}."
+
+ payload: dict[str, object] = {
+ "systemInstruction": {"parts": [{"text": system_instruction}]},
+ "contents": [{"role": "user", "parts": request_parts}],
+ "generationConfig": generation_config,
+ }
+
+ self._dump_debug_payload("request", payload)
+
+ response = requests.post(
+ endpoint,
+ params={"key": api_key},
+ json=payload,
+ timeout=120,
+ )
+
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}"
+ )
+
+ data = response.json()
+ self._dump_debug_payload("response", data)
+ if not isinstance(data, dict):
+ raise ExternalProviderRequestError("Gemini response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ text_parts: list[str] = []
+ finish_messages: list[str] = []
+ candidates = data.get("candidates")
+ if not isinstance(candidates, list):
+ raise ExternalProviderRequestError("Gemini response payload missing candidates")
+ for candidate in candidates:
+ if not isinstance(candidate, dict):
+ continue
+ finish_message = candidate.get("finishMessage")
+ finish_reason = candidate.get("finishReason")
+ if isinstance(finish_message, str):
+ finish_messages.append(finish_message)
+ elif isinstance(finish_reason, str):
+ finish_messages.append(f"Finish reason: {finish_reason}")
+ for part in _iter_response_parts(candidate):
+ inline_data = part.get("inline_data") or part.get("inlineData")
+ if isinstance(inline_data, dict):
+ encoded = inline_data.get("data")
+ if encoded:
+ image = decode_image_base64(encoded)
+ images.append(ExternalGeneratedImage(image=image, seed=request.seed))
+ self._dump_debug_image(image)
+ continue
+ file_data = part.get("fileData") or part.get("file_data")
+ if isinstance(file_data, dict):
+ file_uri = file_data.get("fileUri") or file_data.get("file_uri")
+ if isinstance(file_uri, str) and file_uri:
+ raise ExternalProviderRequestError(
+ f"Gemini returned fileUri instead of inline image data: {file_uri}"
+ )
+ text = part.get("text")
+ if isinstance(text, str):
+ text_parts.append(text)
+
+ if not images:
+ self._logger.error("Gemini response contained no images: %s", data)
+ detail = ""
+ if finish_messages:
+ combined = " ".join(message.strip() for message in finish_messages if message.strip())
+ if combined:
+ detail = f" Response status: {combined[:500]}"
+ elif text_parts:
+ combined = " ".join(text_parts).strip()
+ if combined:
+ detail = f" Response text: {combined[:500]}"
+ raise ExternalProviderRequestError(f"Gemini response contained no images.{detail}")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_metadata={"model": request.model.provider_model_id},
+ )
+
+ def _dump_debug_payload(self, label: str, payload: object) -> None:
+ """TODO: remove debug payload dump once Gemini is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "gemini"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"{label}_{uuid.uuid4().hex}.json"
+ path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
+ except Exception as exc:
+ self._logger.debug("Failed to write Gemini debug payload: %s", exc)
+
+ def _dump_debug_image(self, image: "PILImageType") -> None:
+ """TODO: remove debug image dump once Gemini is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "gemini"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"decoded_{uuid.uuid4().hex}.png"
+ image.save(path, format="PNG")
+ except Exception as exc:
+ self._logger.debug("Failed to write Gemini debug image: %s", exc)
+
+
+def _iter_response_parts(candidate: dict[str, object]) -> list[dict[str, object]]:
+ content = candidate.get("content")
+ if isinstance(content, dict):
+ content_parts = content.get("parts")
+ if isinstance(content_parts, list):
+ return [part for part in content_parts if isinstance(part, dict)]
+ contents = candidate.get("contents")
+ if isinstance(contents, list):
+ parts: list[dict[str, object]] = []
+ for item in contents:
+ if not isinstance(item, dict):
+ continue
+ item_parts = item.get("parts")
+ if isinstance(item_parts, list):
+ parts.extend([part for part in item_parts if isinstance(part, dict)])
+ if parts:
+ return parts
+ return []
+
+
+def _select_aspect_ratio(width: int, height: int, allowed: list[str] | None) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ ratio = width / height
+ default_ratio = _format_aspect_ratio(width, height)
+ if not allowed:
+ return default_ratio
+ parsed = [(value, _parse_ratio(value)) for value in allowed]
+ filtered = [(value, parsed_ratio) for value, parsed_ratio in parsed if parsed_ratio is not None]
+ if not filtered:
+ return default_ratio
+ return min(filtered, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _format_aspect_ratio(width: int, height: int) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py
new file mode 100644
index 00000000000..e31a493b7a1
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/openai.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import io
+
+import requests
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64
+
+
+class OpenAIProvider(ExternalProvider):
+ provider_id = "openai"
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_openai_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_openai_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("OpenAI API key is not configured")
+
+ size = f"{request.width}x{request.height}"
+ base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/")
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ if request.mode == "txt2img":
+ payload: dict[str, object] = {
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ "response_format": "b64_json",
+ }
+ if request.seed is not None:
+ payload["seed"] = request.seed
+ response = requests.post(
+ f"{base_url}/v1/images/generations",
+ headers=headers,
+ json=payload,
+ timeout=120,
+ )
+ else:
+ files: dict[str, tuple[str, io.BytesIO, str]] = {}
+ if request.init_image is None:
+ raise ExternalProviderRequestError("OpenAI img2img/inpaint requires an init image")
+
+ image_buffer = io.BytesIO()
+ request.init_image.save(image_buffer, format="PNG")
+ image_buffer.seek(0)
+ files["image"] = ("image.png", image_buffer, "image/png")
+
+ if request.mask_image is not None:
+ mask_buffer = io.BytesIO()
+ request.mask_image.save(mask_buffer, format="PNG")
+ mask_buffer.seek(0)
+ files["mask"] = ("mask.png", mask_buffer, "image/png")
+
+ data: dict[str, object] = {
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ "response_format": "b64_json",
+ }
+ response = requests.post(
+ f"{base_url}/v1/images/edits",
+ headers=headers,
+ data=data,
+ files=files,
+ timeout=120,
+ )
+
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"OpenAI request failed with status {response.status_code}: {response.text}"
+ )
+
+ payload = response.json()
+ if not isinstance(payload, dict):
+ raise ExternalProviderRequestError("OpenAI response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ data_items = payload.get("data")
+ if not isinstance(data_items, list):
+ raise ExternalProviderRequestError("OpenAI response payload missing image data")
+ for item in data_items:
+ if not isinstance(item, dict):
+ continue
+ encoded = item.get("b64_json")
+ if not encoded:
+ continue
+ images.append(ExternalGeneratedImage(image=decode_image_base64(encoded), seed=request.seed))
+
+ if not images:
+ raise ExternalProviderRequestError("OpenAI response contained no images")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_request_id=response.headers.get("x-request-id"),
+ provider_metadata={"model": request.model.provider_model_id},
+ )
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
index 52fb064596d..f023bed15c2 100644
--- a/invokeai/app/services/invocation_services.py
+++ b/invokeai/app/services/invocation_services.py
@@ -21,6 +21,7 @@
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.app.services.download import DownloadQueueServiceBase
from invokeai.app.services.events.events_base import EventServiceBase
+ from invokeai.app.services.external_generation.external_generation_base import ExternalGenerationServiceBase
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
from invokeai.app.services.images.images_base import ImageServiceABC
@@ -62,6 +63,7 @@ def __init__(
model_relationships: "ModelRelationshipsServiceABC",
model_relationship_records: "ModelRelationshipRecordStorageBase",
download_queue: "DownloadQueueServiceBase",
+ external_generation: "ExternalGenerationServiceBase",
performance_statistics: "InvocationStatsServiceBase",
session_queue: "SessionQueueBase",
session_processor: "SessionProcessorBase",
@@ -92,6 +94,7 @@ def __init__(
self.model_relationships = model_relationships
self.model_relationship_records = model_relationship_records
self.download_queue = download_queue
+ self.external_generation = external_generation
self.performance_statistics = performance_statistics
self.session_queue = session_queue
self.session_processor = session_processor
diff --git a/invokeai/app/services/model_install/model_install_common.py b/invokeai/app/services/model_install/model_install_common.py
index aa0f6d6d2ff..d2add604152 100644
--- a/invokeai/app/services/model_install/model_install_common.py
+++ b/invokeai/app/services/model_install/model_install_common.py
@@ -138,12 +138,27 @@ def __str__(self) -> str:
return str(self.url)
-ModelSource = Annotated[Union[LocalModelSource, HFModelSource, URLModelSource], Field(discriminator="type")]
+class ExternalModelSource(StringLikeSource):
+ """An external provider model identifier."""
+
+ provider_id: str
+ provider_model_id: str
+ type: Literal["external"] = "external"
+
+ def __str__(self) -> str:
+ return f"external://{self.provider_id}/{self.provider_model_id}"
+
+
+ModelSource = Annotated[
+ Union[LocalModelSource, HFModelSource, URLModelSource, ExternalModelSource],
+ Field(discriminator="type"),
+]
MODEL_SOURCE_TO_TYPE_MAP = {
URLModelSource: ModelSourceType.Url,
HFModelSource: ModelSourceType.HFRepoID,
LocalModelSource: ModelSourceType.Path,
+ ExternalModelSource: ModelSourceType.Url,
}
diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py
index 77dc3dfa70a..1301d5f6bed 100644
--- a/invokeai/app/services/model_install/model_install_default.py
+++ b/invokeai/app/services/model_install/model_install_default.py
@@ -27,6 +27,7 @@
from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
from invokeai.app.services.model_install.model_install_common import (
MODEL_SOURCE_TO_TYPE_MAP,
+ ExternalModelSource,
HFModelSource,
InstallStatus,
InvalidModelConfigException,
@@ -43,6 +44,11 @@
AnyModelConfig,
ModelConfigFactory,
)
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.configs.unknown import Unknown_Config
from invokeai.backend.model_manager.metadata import (
AnyModelRepoMetadata,
@@ -53,7 +59,7 @@
)
from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata
from invokeai.backend.model_manager.search import ModelSearch
-from invokeai.backend.model_manager.taxonomy import ModelRepoVariant, ModelSourceType
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelRepoVariant, ModelSourceType
from invokeai.backend.model_manager.util.lora_metadata_extractor import apply_lora_metadata
from invokeai.backend.util import InvokeAILogger
from invokeai.backend.util.catch_sigint import catch_sigint
@@ -267,6 +273,9 @@ def import_model(self, source: ModelSource, config: Optional[ModelRecordChanges]
install_job = self._import_from_hf(source, config)
elif isinstance(source, URLModelSource):
install_job = self._import_from_url(source, config)
+ elif isinstance(source, ExternalModelSource):
+ install_job = self._import_external_model(source, config)
+ self._put_in_queue(install_job)
else:
raise ValueError(f"Unsupported model source: '{type(source)}'")
@@ -494,7 +503,13 @@ def _guess_source(self, source: str) -> ModelSource:
source_obj: Optional[StringLikeSource] = None
source_stripped = source.strip('"')
- if Path(source_stripped).exists(): # A local file or directory
+ if source_stripped.startswith("external://"):
+ external_id = source_stripped.removeprefix("external://")
+ provider_id, _, provider_model_id = external_id.partition("/")
+ if not provider_id or not provider_model_id:
+ raise ValueError(f"Invalid external model source: '{source_stripped}'")
+ source_obj = ExternalModelSource(provider_id=provider_id, provider_model_id=provider_model_id)
+ elif Path(source_stripped).exists(): # A local file or directory
source_obj = LocalModelSource(path=Path(source_stripped))
elif match := re.match(hf_repoid_re, source):
source_obj = HFModelSource(
@@ -586,6 +601,9 @@ def _install_next_item(self) -> None:
self._logger.info(f"Installer thread {threading.get_ident()} exiting")
def _register_or_install(self, job: ModelInstallJob) -> None:
+ if isinstance(job.source, ExternalModelSource):
+ self._register_external_model(job)
+ return
# local jobs will be in waiting state, remote jobs will be downloading state
job.total_bytes = self._stat_size(job.local_path)
job.bytes = job.total_bytes
@@ -603,6 +621,41 @@ def _register_or_install(self, job: ModelInstallJob) -> None:
job.config_out = self.record_store.get_model(key)
self._signal_job_completed(job)
+ def _register_external_model(self, job: ModelInstallJob) -> None:
+ job.total_bytes = 0
+ job.bytes = 0
+ self._signal_job_running(job)
+ job.config_in.source = str(job.source)
+ job.config_in.source_type = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
+
+ provider_id = job.source.provider_id
+ provider_model_id = job.source.provider_model_id
+ capabilities = job.config_in.capabilities or ExternalModelCapabilities()
+ default_settings = (
+ job.config_in.default_settings
+ if isinstance(job.config_in.default_settings, ExternalApiModelDefaultSettings)
+ else None
+ )
+ name = job.config_in.name or f"{provider_id} {provider_model_id}"
+
+ config = ExternalApiModelConfig(
+ key=job.config_in.key or slugify(f"{provider_id}-{provider_model_id}"),
+ name=name,
+ description=job.config_in.description,
+ provider_id=provider_id,
+ provider_model_id=provider_model_id,
+ capabilities=capabilities,
+ default_settings=default_settings,
+ source=str(job.source),
+ source_type=MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__],
+ path="",
+ hash="",
+ file_size=0,
+ )
+ self.record_store.add_model(config)
+ job.config_out = self.record_store.get_model(config.key)
+ self._signal_job_completed(job)
+
def _set_error(self, install_job: ModelInstallJob, excp: Exception) -> None:
multifile_download_job = install_job._multifile_job
if multifile_download_job and any(
@@ -631,6 +684,8 @@ def _scan_for_missing_models(self) -> list[AnyModelConfig]:
"""Scan the models directory for missing models and return a list of them."""
missing_models: list[AnyModelConfig] = []
for model_config in self.record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
if not (self.app_config.models_path / model_config.path).resolve().exists():
missing_models.append(model_config)
return missing_models
@@ -772,6 +827,19 @@ def _import_from_url(
remote_files=remote_files,
)
+ def _import_external_model(
+ self,
+ source: ExternalModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ return ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config or ModelRecordChanges(),
+ local_path=self._app_config.models_path,
+ inplace=True,
+ )
+
def _import_remote_model(
self,
source: HFModelSource | URLModelSource,
diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py
index 45f9d75e3ca..911888d35a7 100644
--- a/invokeai/app/services/model_records/model_records_base.py
+++ b/invokeai/app/services/model_records/model_records_base.py
@@ -13,6 +13,10 @@
from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
from invokeai.backend.model_manager.configs.controlnet import ControlAdapterDefaultSettings
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.configs.lora import LoraModelDefaultSettings
from invokeai.backend.model_manager.configs.main import MainModelDefaultSettings
@@ -85,8 +89,19 @@ class ModelRecordChanges(BaseModelExcludeNull):
file_size: Optional[int] = Field(description="Size of model file", default=None)
format: Optional[str] = Field(description="format of model file", default=None)
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
- default_settings: Optional[MainModelDefaultSettings | LoraModelDefaultSettings | ControlAdapterDefaultSettings] = (
- Field(description="Default settings for this model", default=None)
+ default_settings: Optional[
+ MainModelDefaultSettings
+ | LoraModelDefaultSettings
+ | ControlAdapterDefaultSettings
+ | ExternalApiModelDefaultSettings
+ ] = Field(description="Default settings for this model", default=None)
+
+ # External API model changes
+ provider_id: Optional[str] = Field(description="External provider identifier", default=None)
+ provider_model_id: Optional[str] = Field(description="External provider model identifier", default=None)
+ capabilities: Optional[ExternalModelCapabilities] = Field(
+ description="External model capabilities",
+ default=None,
)
cpu_only: Optional[bool] = Field(description="Whether this model should run on CPU only", default=None)
diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py
index 4ee43b29b7e..e8afc900d82 100644
--- a/invokeai/app/services/shared/invocation_context.py
+++ b/invokeai/app/services/shared/invocation_context.py
@@ -385,6 +385,8 @@ def load(
submodel_type = submodel_type or identifier.submodel_type
model = self._services.model_manager.store.get_model(identifier.key)
+ self._raise_if_external(model)
+
message = f"Loading model {model.name}"
if submodel_type:
message += f" ({submodel_type.value})"
@@ -414,12 +416,18 @@ def load_by_attrs(
if len(configs) > 1:
raise ValueError(f"More than one model found with name {name}, base {base}, and type {type}")
+ self._raise_if_external(configs[0])
message = f"Loading model {name}"
if submodel_type:
message += f" ({submodel_type.value})"
self._util.signal_progress(message)
return self._services.model_manager.load.load_model(configs[0], submodel_type)
+ @staticmethod
+ def _raise_if_external(model: AnyModelConfig) -> None:
+ if model.base == BaseModelType.External or model.format == ModelFormat.ExternalApi:
+ raise ValueError("External API models cannot be loaded from disk")
+
def get_config(self, identifier: Union[str, "ModelIdentifierField"]) -> AnyModelConfig:
"""Get a model's config.
diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py
index e69de29bb2d..f57b4404e00 100644
--- a/invokeai/backend/model_manager/configs/external_api.py
+++ b/invokeai/backend/model_manager/configs/external_api.py
@@ -0,0 +1,80 @@
+from typing import Literal, Self
+
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+from invokeai.backend.model_manager.configs.base import Config_Base
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelSourceType, ModelType
+
+ExternalGenerationMode = Literal["txt2img", "img2img", "inpaint"]
+ExternalMaskFormat = Literal["alpha", "binary", "none"]
+
+
+class ExternalImageSize(BaseModel):
+ width: int = Field(gt=0)
+ height: int = Field(gt=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalModelCapabilities(BaseModel):
+ modes: list[ExternalGenerationMode] = Field(default_factory=lambda: ["txt2img"])
+ supports_reference_images: bool = Field(default=False)
+ supports_negative_prompt: bool = Field(default=True)
+ supports_seed: bool = Field(default=False)
+ supports_guidance: bool = Field(default=False)
+ max_images_per_request: int | None = Field(default=None, gt=0)
+ max_image_size: ExternalImageSize | None = Field(default=None)
+ allowed_aspect_ratios: list[str] | None = Field(default=None)
+ aspect_ratio_sizes: dict[str, ExternalImageSize] | None = Field(default=None)
+ max_reference_images: int | None = Field(default=None, gt=0)
+ mask_format: ExternalMaskFormat = Field(default="none")
+ input_image_required_for: list[ExternalGenerationMode] | None = Field(default=None)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalApiModelDefaultSettings(BaseModel):
+ width: int | None = Field(default=None, gt=0)
+ height: int | None = Field(default=None, gt=0)
+ steps: int | None = Field(default=None, gt=0)
+ guidance: float | None = Field(default=None, gt=0)
+ num_images: int | None = Field(default=None, gt=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalApiModelConfig(Config_Base):
+ base: Literal[BaseModelType.External] = Field(default=BaseModelType.External)
+ type: Literal[ModelType.ExternalImageGenerator] = Field(default=ModelType.ExternalImageGenerator)
+ format: Literal[ModelFormat.ExternalApi] = Field(default=ModelFormat.ExternalApi)
+
+ provider_id: str = Field(min_length=1, description="External provider ID")
+ provider_model_id: str = Field(min_length=1, description="Provider-specific model ID")
+ capabilities: ExternalModelCapabilities = Field(description="Provider capability matrix")
+ default_settings: ExternalApiModelDefaultSettings | None = Field(default=None)
+ tags: list[str] | None = Field(default=None)
+ is_default: bool = Field(default=False)
+
+ source_type: ModelSourceType = Field(default=ModelSourceType.Url)
+ path: str = Field(default="")
+ source: str = Field(default="")
+ hash: str = Field(default="")
+ file_size: int = Field(default=0, ge=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+ @model_validator(mode="after")
+ def _populate_external_fields(self) -> "ExternalApiModelConfig":
+ if not self.path:
+ self.path = f"external://{self.provider_id}/{self.provider_model_id}"
+ if not self.source:
+ self.source = self.path
+ if not self.hash:
+ self.hash = f"external:{self.provider_id}:{self.provider_model_id}"
+ return self
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, object]) -> Self:
+ raise NotAMatchError("external API models are not probed from disk")
diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py
index 021752d47bd..701f09c4dec 100644
--- a/invokeai/backend/model_manager/configs/factory.py
+++ b/invokeai/backend/model_manager/configs/factory.py
@@ -26,6 +26,7 @@
ControlNet_Diffusers_SD2_Config,
ControlNet_Diffusers_SDXL_Config,
)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.configs.flux_redux import FLUXRedux_Checkpoint_Config
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.configs.ip_adapter import (
@@ -248,6 +249,7 @@
Annotated[SigLIP_Diffusers_Config, SigLIP_Diffusers_Config.get_tag()],
Annotated[FLUXRedux_Checkpoint_Config, FLUXRedux_Checkpoint_Config.get_tag()],
Annotated[LlavaOnevision_Diffusers_Config, LlavaOnevision_Diffusers_Config.get_tag()],
+ Annotated[ExternalApiModelConfig, ExternalApiModelConfig.get_tag()],
# Unknown model (fallback)
Annotated[Unknown_Config, Unknown_Config.get_tag()],
],
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index ef7cd80cd29..59d7ceba205 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -2,6 +2,11 @@
from pydantic import BaseModel
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
@@ -13,6 +18,8 @@ class StarterModelWithoutDependencies(BaseModel):
type: ModelType
format: Optional[ModelFormat] = None
is_installed: bool = False
+ capabilities: ExternalModelCapabilities | None = None
+ default_settings: ExternalApiModelDefaultSettings | None = None
# allows us to track what models a user has installed across name changes within starter models
# if you update a starter model name, please add the old one to this list for that starter model
previous_names: list[str] = []
@@ -862,6 +869,108 @@ class StarterModelBundle(BaseModel):
)
# endregion
+# region External API
+gemini_flash_image = StarterModel(
+ name="Gemini 2.5 Flash Image",
+ base=BaseModelType.External,
+ source="external://gemini/gemini-2.5-flash-image",
+ description="Google Gemini 2.5 Flash image generation model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=True,
+ max_images_per_request=1,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "4:5",
+ "5:4",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "2:3": ExternalImageSize(width=832, height=1248),
+ "3:2": ExternalImageSize(width=1248, height=832),
+ "3:4": ExternalImageSize(width=864, height=1184),
+ "4:3": ExternalImageSize(width=1184, height=864),
+ "4:5": ExternalImageSize(width=896, height=1152),
+ "5:4": ExternalImageSize(width=1152, height=896),
+ "9:16": ExternalImageSize(width=768, height=1344),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ "21:9": ExternalImageSize(width=1536, height=672),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+gemini_pro_image_preview = StarterModel(
+ name="Gemini 3 Pro Image Preview",
+ base=BaseModelType.External,
+ source="external://gemini/gemini-3-pro-image-preview",
+ description="Google Gemini 3 Pro image generation preview model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=True,
+ max_images_per_request=1,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "4:5",
+ "5:4",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "2:3": ExternalImageSize(width=832, height=1248),
+ "3:2": ExternalImageSize(width=1248, height=832),
+ "3:4": ExternalImageSize(width=864, height=1184),
+ "4:3": ExternalImageSize(width=1184, height=864),
+ "4:5": ExternalImageSize(width=896, height=1152),
+ "5:4": ExternalImageSize(width=1152, height=896),
+ "9:16": ExternalImageSize(width=768, height=1344),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ "21:9": ExternalImageSize(width=1536, height=672),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+openai_gpt_image_1 = StarterModel(
+ name="ChatGPT Image",
+ base=BaseModelType.External,
+ source="external://openai/gpt-image-1",
+ description="OpenAI GPT-Image-1 image generation model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=False,
+ max_images_per_request=1,
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+# endregion
+
# List of starter models, displayed on the frontend.
# The order/sort of this list is not changed by the frontend - set it how you want it here.
STARTER_MODELS: list[StarterModel] = [
@@ -957,6 +1066,9 @@ class StarterModelBundle(BaseModel):
z_image_qwen3_encoder_quantized,
z_image_controlnet_union,
z_image_controlnet_tile,
+ gemini_flash_image,
+ gemini_pro_image_preview,
+ openai_gpt_image_1,
]
sd1_bundle: list[StarterModel] = [
diff --git a/invokeai/backend/model_manager/taxonomy.py b/invokeai/backend/model_manager/taxonomy.py
index aa0660152a5..0121788b8dc 100644
--- a/invokeai/backend/model_manager/taxonomy.py
+++ b/invokeai/backend/model_manager/taxonomy.py
@@ -52,6 +52,8 @@ class BaseModelType(str, Enum):
"""Indicates the model is associated with CogView 4 model architecture."""
ZImage = "z-image"
"""Indicates the model is associated with Z-Image model architecture, including Z-Image-Turbo."""
+ External = "external"
+ """Indicates the model is hosted by an external provider."""
Unknown = "unknown"
"""Indicates the model's base architecture is unknown."""
@@ -76,6 +78,7 @@ class ModelType(str, Enum):
SigLIP = "siglip"
FluxRedux = "flux_redux"
LlavaOnevision = "llava_onevision"
+ ExternalImageGenerator = "external_image_generator"
Unknown = "unknown"
@@ -160,6 +163,7 @@ class ModelFormat(str, Enum):
BnbQuantizedLlmInt8b = "bnb_quantized_int8b"
BnbQuantizednf4b = "bnb_quantized_nf4b"
GGUFQuantized = "gguf_quantized"
+ ExternalApi = "external_api"
Unknown = "unknown"
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 8c5b72e57a6..9165def30ad 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -930,6 +930,22 @@
"fileSize": "File Size",
"filterModels": "Filter models",
"fluxRedux": "FLUX Redux",
+ "externalImageGenerator": "External Image Generator",
+ "externalProviders": "External Providers",
+ "externalSetupTitle": "External Providers Setup",
+ "externalSetupDescription": "Connect an API key to enable external image generation and optionally install curated external models.",
+ "externalInstallDefaults": "Auto-install starter models",
+ "externalProvidersUnavailable": "External providers are not available in this build.",
+ "externalSetupFooter": "External providers use remote APIs; usage may incur provider-side costs.",
+ "externalProviderCardDescription": "Configure {{providerId}} credentials for external image generation.",
+ "externalApiKey": "API Key",
+ "externalApiKeyPlaceholder": "Paste your API key",
+ "externalApiKeyPlaceholderSet": "API key configured",
+ "externalApiKeyHelper": "Stored in your InvokeAI config file.",
+ "externalBaseUrl": "Base URL (optional)",
+ "externalBaseUrlPlaceholder": "https://...",
+ "externalBaseUrlHelper": "Override the default API base URL if needed.",
+ "externalResetHelper": "Clear API key and base URL.",
"height": "Height",
"huggingFace": "HuggingFace",
"huggingFacePlaceholder": "owner/model-name",
@@ -997,6 +1013,21 @@
"modelUpdated": "Model Updated",
"modelUpdateFailed": "Model Update Failed",
"name": "Name",
+ "externalProvider": "External Provider",
+ "externalCapabilities": "External Capabilities",
+ "externalDefaults": "External Defaults",
+ "providerId": "Provider ID",
+ "providerModelId": "Provider Model ID",
+ "supportedModes": "Supported Modes",
+ "supportsNegativePrompt": "Supports Negative Prompt",
+ "supportsReferenceImages": "Supports Reference Images",
+ "supportsSeed": "Supports Seed",
+ "supportsGuidance": "Supports Guidance",
+ "maxImagesPerRequest": "Max Images Per Request",
+ "maxReferenceImages": "Max Reference Images",
+ "maxImageWidth": "Max Image Width",
+ "maxImageHeight": "Max Image Height",
+ "numImages": "Num Images",
"modelPickerFallbackNoModelsInstalled": "No models installed.",
"modelPickerFallbackNoModelsInstalled2": "Visit the Model Manager to install models.",
"noModelsInstalledDesc1": "Install models with the",
@@ -1039,6 +1070,7 @@
"urlDescription": "Install models from a URL or local file path. Perfect for specific models you want to add.",
"huggingFaceDescription": "Browse and install models directly from HuggingFace repositories.",
"scanFolderDescription": "Scan a local folder to automatically detect and install models.",
+ "externalDescription": "Connect to Gemini or OpenAI to generate images with external APIs.",
"recommendedModels": "Recommended Models",
"exploreStarter": "Or browse all available starter models",
"quickStart": "Quick Start Bundles",
@@ -1511,7 +1543,11 @@
"intermediatesCleared_one": "Cleared {{count}} Intermediate",
"intermediatesCleared_other": "Cleared {{count}} Intermediates",
"intermediatesClearedFailed": "Problem Clearing Intermediates",
- "reloadingIn": "Reloading in"
+ "reloadingIn": "Reloading in",
+ "externalProviders": "External Providers",
+ "externalProviderConfigured": "Configured",
+ "externalProviderNotConfigured": "API Key Required",
+ "externalProviderNotConfiguredHint": "Add your API key in Model Manager or the server config to enable this provider."
},
"toast": {
"addedToBoard": "Added to board {{name}}'s assets",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
index 6b8da8dc4da..c8333600c56 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
@@ -72,10 +72,17 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
onAccept: (item, imageDTO) => {
const bboxRect = selectBboxRect(store.getState());
const { x, y } = bboxRect;
- const imageObject = imageDTOToImageObject(imageDTO);
+ const imageObject = imageDTOToImageObject(imageDTO, { usePixelBbox: false });
+ const scale = Math.min(bboxRect.width / imageDTO.width, bboxRect.height / imageDTO.height);
+ const scaledWidth = Math.round(imageDTO.width * scale);
+ const scaledHeight = Math.round(imageDTO.height * scale);
+ const position = {
+ x: x + Math.round((bboxRect.width - scaledWidth) / 2),
+ y: y + Math.round((bboxRect.height - scaledHeight) / 2),
+ };
const selectedEntityIdentifier = selectSelectedEntityIdentifier(store.getState());
const overrides: Partial = {
- position: { x, y },
+ position,
objects: [imageObject],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
index f16b9023164..9268fc7570f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
@@ -181,6 +181,33 @@ describe('StagingAreaApi Utility Functions', () => {
expect(result).toBe('first-image.png');
});
+ it('should return first image from image collections', () => {
+ const queueItem: S['SessionQueueItem'] = {
+ item_id: 1,
+ status: 'completed',
+ priority: 0,
+ destination: 'test-session',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ started_at: '2024-01-01T00:00:01Z',
+ completed_at: '2024-01-01T00:01:00Z',
+ error: null,
+ session: {
+ id: 'test-session',
+ source_prepared_mapping: {
+ canvas_output: ['output-node-id'],
+ },
+ results: {
+ 'output-node-id': {
+ images: [{ image_name: 'first.png' }, { image_name: 'second.png' }],
+ },
+ },
+ },
+ } as unknown as S['SessionQueueItem'];
+
+ expect(getOutputImageName(queueItem)).toBe('first.png');
+ });
+
it('should handle empty session mapping', () => {
const queueItem: S['SessionQueueItem'] = {
item_id: 1,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
index fe98408df58..1fe461e9993 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
@@ -1,4 +1,4 @@
-import { isImageField } from 'features/nodes/types/common';
+import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
import type { S } from 'services/api/types';
import { formatProgressMessage } from 'services/events/stores';
@@ -29,6 +29,9 @@ export const getOutputImageName = (item: S['SessionQueueItem']) => {
if (isImageField(value)) {
return value.image_name;
}
+ if (isImageFieldCollection(value)) {
+ return value[0]?.image_name ?? null;
+ }
}
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
new file mode 100644
index 00000000000..03de58908f0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
@@ -0,0 +1,61 @@
+import { zModelIdentifierField } from 'features/nodes/types/common';
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+} from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { selectModelSupportsNegativePrompt, selectModelSupportsRefImages } from './paramsSlice';
+
+const createExternalConfig = (capabilities: ExternalModelCapabilities): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities: { ...capabilities, max_image_size: maxImageSize },
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ };
+};
+
+describe('paramsSlice selectors for external models', () => {
+ it('uses external capabilities for negative prompt support', () => {
+ const config = createExternalConfig({
+ modes: ['txt2img'],
+ supports_negative_prompt: true,
+ supports_reference_images: false,
+ });
+ const model = zModelIdentifierField.parse(config);
+
+ expect(selectModelSupportsNegativePrompt.resultFunc(model, config)).toBe(true);
+ });
+
+ it('uses external capabilities for ref image support', () => {
+ const config = createExternalConfig({
+ modes: ['txt2img'],
+ supports_negative_prompt: false,
+ supports_reference_images: false,
+ });
+ const model = zModelIdentifierField.parse(config);
+
+ expect(selectModelSupportsRefImages.resultFunc(model, config)).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 3cc26061720..f54927e5479 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -42,7 +42,7 @@ import type {
} from 'features/parameters/types/parameterSchemas';
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
-import { isNonRefinerMainModelConfig } from 'services/api/types';
+import { isExternalApiModelConfig, isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const slice = createSlice({
@@ -631,15 +631,42 @@ export const selectOptimizedDenoisingEnabled = createParamsSelector((params) =>
export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt);
export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt);
export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? '');
+export const selectModelConfig = createSelector(
+ selectModelConfigsQuery,
+ selectParamsSlice,
+ (modelConfigs, { model }) => {
+ if (!modelConfigs.data) {
+ return null;
+ }
+ if (!model) {
+ return null;
+ }
+ return modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key) ?? null;
+ }
+);
export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null);
export const selectModelSupportsNegativePrompt = createSelector(
selectModel,
- (model) => !!model && SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base)
-);
-export const selectModelSupportsRefImages = createSelector(
- selectModel,
- (model) => !!model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)
+ selectModelConfig,
+ (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_negative_prompt ?? false;
+ }
+ return SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base);
+ }
);
+export const selectModelSupportsRefImages = createSelector(selectModel, selectModelConfig, (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_reference_images ?? false;
+ }
+ return SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base);
+});
export const selectModelSupportsOptimizedDenoising = createSelector(
selectModel,
(model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base)
@@ -686,24 +713,23 @@ export const selectHeight = createParamsSelector((params) => params.dimensions.h
export const selectAspectRatioID = createParamsSelector((params) => params.dimensions.aspectRatio.id);
export const selectAspectRatioValue = createParamsSelector((params) => params.dimensions.aspectRatio.value);
export const selectAspectRatioIsLocked = createParamsSelector((params) => params.dimensions.aspectRatio.isLocked);
+export const selectAllowedAspectRatioIDs = createSelector(selectModelConfig, (modelConfig) => {
+ if (!modelConfig || !isExternalApiModelConfig(modelConfig)) {
+ return null;
+ }
+ const allowed = modelConfig.capabilities.allowed_aspect_ratios;
+ return allowed?.length ? allowed : null;
+});
-export const selectMainModelConfig = createSelector(
- selectModelConfigsQuery,
- selectParamsSlice,
- (modelConfigs, { model }) => {
- if (!modelConfigs.data) {
- return null;
- }
- if (!model) {
- return null;
- }
- const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
- if (!modelConfig) {
- return null;
- }
- if (!isNonRefinerMainModelConfig(modelConfig)) {
- return null;
- }
+export const selectMainModelConfig = createSelector(selectModelConfig, (modelConfig) => {
+ if (!modelConfig) {
+ return null;
+ }
+ if (isExternalApiModelConfig(modelConfig)) {
return modelConfig;
}
-);
+ if (!isNonRefinerMainModelConfig(modelConfig)) {
+ return null;
+ }
+ return modelConfig;
+});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
index 3406e9e7ee6..e431c10558f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
@@ -6,7 +6,7 @@ import type {
RefImageState,
} from 'features/controlLayers/store/types';
import type { ModelIdentifierField } from 'features/nodes/types/common';
-import type { AnyModelConfig, MainModelConfig } from 'services/api/types';
+import type { AnyModelConfig, MainOrExternalModelConfig } from 'services/api/types';
const WARNINGS = {
UNSUPPORTED_MODEL: 'controlLayers.warnings.unsupportedModel',
@@ -28,7 +28,7 @@ type WarningTKey = (typeof WARNINGS)[keyof typeof WARNINGS];
export const getRegionalGuidanceWarnings = (
entity: CanvasRegionalGuidanceState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -112,7 +112,7 @@ export const areBasesCompatibleForRefImage = (
export const getGlobalReferenceImageWarnings = (
entity: RefImageState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -147,7 +147,7 @@ export const getGlobalReferenceImageWarnings = (
export const getControlLayerWarnings = (
entity: CanvasControlLayerState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -181,7 +181,7 @@ export const getControlLayerWarnings = (
export const getRasterLayerWarnings = (
_entity: CanvasRasterLayerState,
- _model: MainModelConfig | null | undefined
+ _model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -192,7 +192,7 @@ export const getRasterLayerWarnings = (
export const getInpaintMaskWarnings = (
_entity: CanvasInpaintMaskState,
- _model: MainModelConfig | null | undefined
+ _model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/models.ts b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
index c4dd56f8113..fea3df4ed2c 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/models.ts
+++ b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
@@ -1,10 +1,11 @@
import type { AnyModelVariant, BaseModelType, ModelFormat, ModelType } from 'features/nodes/types/common';
-import type { AnyModelConfig } from 'services/api/types';
import {
+ type AnyModelConfig,
isCLIPEmbedModelConfig,
isCLIPVisionModelConfig,
isControlLoRAModelConfig,
isControlNetModelConfig,
+ isExternalApiModelConfig,
isFluxReduxModelConfig,
isIPAdapterModelConfig,
isLLaVAModelConfig,
@@ -121,6 +122,11 @@ export const MODEL_CATEGORIES: Record = {
i18nKey: 'modelManager.llavaOnevision',
filter: isLLaVAModelConfig,
},
+ external_image_generator: {
+ category: 'external_image_generator',
+ i18nKey: 'modelManager.externalImageGenerator',
+ filter: isExternalApiModelConfig,
+ },
};
export const MODEL_CATEGORIES_AS_LIST = objectEntries(MODEL_CATEGORIES).map(([category, { i18nKey, filter }]) => ({
@@ -143,6 +149,7 @@ export const MODEL_BASE_TO_COLOR: Record = {
flux2: 'gold',
cogview4: 'red',
'z-image': 'cyan',
+ external: 'orange',
unknown: 'red',
};
@@ -167,6 +174,7 @@ export const MODEL_TYPE_TO_LONG_NAME: Record = {
clip_embed: 'CLIP Embed',
siglip: 'SigLIP',
flux_redux: 'FLUX Redux',
+ external_image_generator: 'External Image Generator',
unknown: 'Unknown',
};
@@ -184,6 +192,7 @@ export const MODEL_BASE_TO_LONG_NAME: Record = {
flux2: 'FLUX.2',
cogview4: 'CogView4',
'z-image': 'Z-Image',
+ external: 'External',
unknown: 'Unknown',
};
@@ -201,6 +210,7 @@ export const MODEL_BASE_TO_SHORT_NAME: Record = {
flux2: 'FLUX.2',
cogview4: 'CogView4',
'z-image': 'Z-Image',
+ external: 'External',
unknown: 'Unknown',
};
@@ -226,6 +236,7 @@ export const MODEL_FORMAT_TO_LONG_NAME: Record = {
checkpoint: 'Checkpoint',
lycoris: 'LyCORIS',
onnx: 'ONNX',
+ external_api: 'External API',
olive: 'Olive',
embedding_file: 'Embedding (file)',
embedding_folder: 'Embedding (folder)',
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
index b99a1212fec..79b7bfe31a7 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
+++ b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
@@ -1,13 +1,14 @@
import { atom } from 'nanostores';
-type InstallModelsTabName = 'launchpad' | 'urlOrLocal' | 'huggingface' | 'scanFolder' | 'starterModels';
+type InstallModelsTabName = 'launchpad' | 'urlOrLocal' | 'huggingface' | 'external' | 'scanFolder' | 'starterModels';
const TAB_TO_INDEX_MAP: Record = {
launchpad: 0,
urlOrLocal: 1,
huggingface: 2,
- scanFolder: 3,
- starterModels: 4,
+ external: 3,
+ scanFolder: 4,
+ starterModels: 5,
};
export const setInstallModelsTabByName = (tab: InstallModelsTabName) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
new file mode 100644
index 00000000000..26820cb0e29
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
@@ -0,0 +1,281 @@
+import {
+ Badge,
+ Button,
+ Card,
+ Flex,
+ FormControl,
+ FormHelperText,
+ FormLabel,
+ Heading,
+ Input,
+ Switch,
+ Text,
+ Tooltip,
+} from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall';
+import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
+import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCheckBold, PiWarningBold } from 'react-icons/pi';
+import {
+ useGetExternalProviderConfigsQuery,
+ useResetExternalProviderConfigMutation,
+ useSetExternalProviderConfigMutation,
+} from 'services/api/endpoints/appInfo';
+import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
+import type { ExternalProviderConfig, StarterModel } from 'services/api/types';
+
+const PROVIDER_SORT_ORDER = ['gemini', 'openai'];
+
+type ProviderCardProps = {
+ provider: ExternalProviderConfig;
+ onInstallModels: (providerId: string) => void;
+};
+
+type UpdatePayload = {
+ provider_id: string;
+ api_key?: string;
+ base_url?: string;
+};
+
+export const ExternalProvidersForm = memo(() => {
+ const { t } = useTranslation();
+ const { data, isLoading } = useGetExternalProviderConfigsQuery();
+ const { data: starterModels } = useGetStarterModelsQuery();
+ const [installModel] = useInstallModel();
+ const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg();
+ const [installDefaults, setInstallDefaults] = useState(true);
+ const tabIndex = useStore($installModelsTabIndex);
+
+ const toggleInstallDefaults = useCallback((event: ChangeEvent) => {
+ setInstallDefaults(event.target.checked);
+ }, []);
+
+ const externalModelsByProvider = useMemo(() => {
+ const groups = new Map();
+ for (const model of starterModels?.starter_models ?? []) {
+ if (!model.source.startsWith('external://')) {
+ continue;
+ }
+ const providerId = model.source.replace('external://', '').split('/')[0];
+ if (!providerId) {
+ continue;
+ }
+ const models = groups.get(providerId) ?? [];
+ models.push(model);
+ groups.set(providerId, models);
+ }
+
+ for (const [providerId, models] of groups.entries()) {
+ models.sort((a, b) => a.name.localeCompare(b.name));
+ groups.set(providerId, models);
+ }
+
+ return groups;
+ }, [starterModels]);
+
+ const handleInstallProviderModels = useCallback(
+ (providerId: string) => {
+ if (!installDefaults) {
+ return;
+ }
+ const models = externalModelsByProvider.get(providerId);
+ if (!models?.length) {
+ return;
+ }
+ const modelsToInstall = models.filter((model) => !getIsInstalled(model)).map(buildModelInstallArg);
+ modelsToInstall.forEach((model) => installModel(model));
+ },
+ [buildModelInstallArg, externalModelsByProvider, getIsInstalled, installDefaults, installModel]
+ );
+
+ const sortedProviders = useMemo(() => {
+ if (!data) {
+ return [];
+ }
+ return [...data].sort((a, b) => {
+ const aIndex = PROVIDER_SORT_ORDER.indexOf(a.provider_id);
+ const bIndex = PROVIDER_SORT_ORDER.indexOf(b.provider_id);
+ if (aIndex === -1 && bIndex === -1) {
+ return a.provider_id.localeCompare(b.provider_id);
+ }
+ if (aIndex === -1) {
+ return 1;
+ }
+ if (bIndex === -1) {
+ return -1;
+ }
+ return aIndex - bIndex;
+ });
+ }, [data]);
+
+ return (
+
+
+
+ {t('modelManager.externalSetupTitle')}
+ {t('modelManager.externalSetupDescription')}
+
+
+ {t('modelManager.externalInstallDefaults')}
+
+
+
+
+
+ {isLoading && {t('common.loading')}}
+ {!isLoading && sortedProviders.length === 0 && (
+ {t('modelManager.externalProvidersUnavailable')}
+ )}
+ {sortedProviders.map((provider) => (
+
+ ))}
+
+
+ {tabIndex === 3 && (
+
+ {t('modelManager.externalSetupFooter')}
+
+ )}
+
+ );
+});
+
+ExternalProvidersForm.displayName = 'ExternalProvidersForm';
+
+const ProviderCard = memo(({ provider, onInstallModels }: ProviderCardProps) => {
+ const { t } = useTranslation();
+ const [apiKey, setApiKey] = useState('');
+ const [baseUrl, setBaseUrl] = useState(provider.base_url ?? '');
+ const [saveConfig, { isLoading }] = useSetExternalProviderConfigMutation();
+ const [resetConfig, { isLoading: isResetting }] = useResetExternalProviderConfigMutation();
+
+ useEffect(() => {
+ setBaseUrl(provider.base_url ?? '');
+ }, [provider.base_url]);
+
+ const handleSave = useCallback(() => {
+ const trimmedApiKey = apiKey.trim();
+ const trimmedBaseUrl = baseUrl.trim();
+ const updatePayload: UpdatePayload = {
+ provider_id: provider.provider_id,
+ };
+ if (trimmedApiKey) {
+ updatePayload.api_key = trimmedApiKey;
+ }
+ if (trimmedBaseUrl !== (provider.base_url ?? '')) {
+ updatePayload.base_url = trimmedBaseUrl;
+ }
+
+ if (!updatePayload.api_key && updatePayload.base_url === undefined) {
+ return;
+ }
+
+ saveConfig(updatePayload)
+ .unwrap()
+ .then((result) => {
+ if (result.api_key_configured) {
+ setApiKey('');
+ onInstallModels(provider.provider_id);
+ }
+ if (result.base_url !== undefined) {
+ setBaseUrl(result.base_url ?? '');
+ }
+ });
+ }, [apiKey, baseUrl, onInstallModels, provider.base_url, provider.provider_id, saveConfig]);
+
+ const handleReset = useCallback(() => {
+ resetConfig(provider.provider_id)
+ .unwrap()
+ .then((result) => {
+ setApiKey('');
+ setBaseUrl(result.base_url ?? '');
+ });
+ }, [provider.provider_id, resetConfig]);
+
+ const handleApiKeyChange = useCallback((event: ChangeEvent) => {
+ setApiKey(event.target.value);
+ }, []);
+
+ const handleBaseUrlChange = useCallback((event: ChangeEvent) => {
+ setBaseUrl(event.target.value);
+ }, []);
+
+ const statusBadge = provider.api_key_configured ? (
+
+
+ {t('settings.externalProviderConfigured')}
+
+ ) : (
+
+
+ {t('settings.externalProviderNotConfigured')}
+
+ );
+
+ return (
+
+
+
+
+ {provider.provider_id}
+
+
+ {t('modelManager.externalProviderCardDescription', { providerId: provider.provider_id })}
+
+
+ {statusBadge}
+
+
+
+ {t('modelManager.externalApiKey')}
+
+ {t('modelManager.externalApiKeyHelper')}
+
+
+ {t('modelManager.externalBaseUrl')}
+
+ {t('modelManager.externalBaseUrlHelper')}
+
+
+
+
+
+
+
+
+
+ );
+});
+
+ProviderCard.displayName = 'ProviderCard';
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
index fc99bcec7bf..591c61a4b23 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
@@ -6,7 +6,7 @@ import { StarterBundleButton } from 'features/modelManagerV2/subpanels/AddModelP
import { StarterBundleTooltipContentCompact } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleTooltipContentCompact';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi';
+import { PiFolderOpenBold, PiLinkBold, PiPlugBold, PiStarBold } from 'react-icons/pi';
import { SiHuggingface } from 'react-icons/si';
import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
@@ -28,6 +28,10 @@ export const LaunchpadForm = memo(() => {
setInstallModelsTabByName('scanFolder');
}, []);
+ const navigateToExternalTab = useCallback(() => {
+ setInstallModelsTabByName('external');
+ }, []);
+
const navigateToStarterModelsTab = useCallback(() => {
setInstallModelsTabByName('starterModels');
}, []);
@@ -63,6 +67,12 @@ export const LaunchpadForm = memo(() => {
title={t('modelManager.scanFolder')}
description={t('modelManager.launchpad.scanFolderDescription')}
/>
+
{/* Recommended Section */}
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
index 9039c0f85f4..5bc4c9713fc 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
@@ -2,18 +2,18 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore';
+import { ExternalProvidersForm } from 'features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm';
+import { HuggingFaceForm } from 'features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
+import { InstallModelForm } from 'features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm';
+import { LaunchpadForm } from 'features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm';
+import { ModelInstallQueue } from 'features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue';
+import { ScanModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm';
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiCubeBold, PiFolderOpenBold, PiLinkSimpleBold, PiShootingStarBold } from 'react-icons/pi';
+import { PiCubeBold, PiFolderOpenBold, PiLinkSimpleBold, PiPlugBold, PiShootingStarBold } from 'react-icons/pi';
import { SiHuggingface } from 'react-icons/si';
-import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
-import { InstallModelForm } from './AddModelPanel/InstallModelForm';
-import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm';
-import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue';
-import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
-
const installModelsTabSx: SystemStyleObject = {
display: 'flex',
gap: 2,
@@ -61,6 +61,10 @@ export const InstallModels = memo(() => {
{t('modelManager.huggingFace')}
+
+
+ {t('modelManager.externalProviders')}
+
{t('modelManager.scanFolder')}
@@ -80,6 +84,9 @@ export const InstallModels = memo(() => {
+
+
+
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
index 7d44ee54637..ff4dbe88fc8 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
@@ -19,6 +19,7 @@ const FORMAT_NAME_MAP: Record = {
bnb_quantized_nf4b: 'quantized',
gguf_quantized: 'gguf',
omi: 'omi',
+ external_api: 'external_api',
unknown: 'unknown',
olive: 'olive',
onnx: 'onnx',
@@ -40,6 +41,7 @@ const FORMAT_COLOR_MAP: Record = {
unknown: 'red',
olive: 'base',
onnx: 'base',
+ external_api: 'base',
};
const ModelFormatBadge = ({ format }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
index 8235d26efef..e4c8752e569 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_BASE_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_BASE_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_BASE_TO_LONG_NAME).map(([v
}));
type Props = {
- control: Control;
+ control: Control;
};
const BaseModelSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
index 1057ab7784c..2bd3eb954e5 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_FORMAT_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_FORMAT_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_FORMAT_TO_LONG_NAME).map((
}));
type Props = {
- control: Control;
+ control: Control;
};
const ModelFormatSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
index 44b41f01518..b35ce7f96df 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_TYPE_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_TYPE_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_TYPE_TO_LONG_NAME).map(([v
}));
type Props = {
- control: Control;
+ control: Control;
};
const ModelTypeSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
index 52eb2a4749d..d8e8c6a5b8a 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
@@ -5,13 +5,13 @@ import { MODEL_VARIANT_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_VARIANT_TO_LONG_NAME).map(([value, label]) => ({ label, value }));
type Props = {
- control: Control;
+ control: Control;
};
const ModelVariantSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
index dcef95b4243..593bc4c4136 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
@@ -4,7 +4,7 @@ import { typedMemo } from 'common/util/typedMemo';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
const options: ComboboxOption[] = [
{ value: 'none', label: '-' },
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = [
];
type Props = {
- control: Control;
+ control: Control;
};
const PredictionTypeSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
index d845eca3eec..7cde65bf072 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
@@ -15,12 +15,17 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader';
import { toast } from 'features/toast/toast';
-import { memo, useCallback } from 'react';
-import { type SubmitHandler, useForm } from 'react-hook-form';
+import { memo, useCallback, useMemo } from 'react';
+import { type SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { PiCheckBold, PiXBold } from 'react-icons/pi';
import { type UpdateModelArg, useUpdateModelMutation } from 'services/api/endpoints/models';
-import type { AnyModelConfig } from 'services/api/types';
+import {
+ type AnyModelConfig,
+ type ExternalModelCapabilities,
+ isExternalApiModelConfig,
+ type UpdateModelBody,
+} from 'services/api/types';
import BaseModelSelect from './Fields/BaseModelSelect';
import ModelFormatSelect from './Fields/ModelFormatSelect';
@@ -33,6 +38,8 @@ type Props = {
modelConfig: AnyModelConfig;
};
+type ModelEditFormValues = UpdateModelBody;
+
const stringFieldOptions = {
validate: (value?: string | null) => (value && value.trim().length > 3) || 'Must be at least 3 characters',
};
@@ -41,19 +48,54 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation();
const dispatch = useAppDispatch();
+ const isExternal = useMemo(() => isExternalApiModelConfig(modelConfig), [modelConfig]);
- const form = useForm({
+ const form = useForm({
defaultValues: modelConfig,
mode: 'onChange',
});
- const onSubmit = useCallback>(
+ const externalModes = useWatch({
+ control: form.control,
+ name: 'capabilities.modes',
+ }) as ExternalModelCapabilities['modes'] | undefined;
+
+ const modeSet = useMemo(() => new Set(externalModes ?? []), [externalModes]);
+
+ const toggleMode = useCallback(
+ (mode: ExternalModelCapabilities['modes'][number]) => {
+ const nextModes = modeSet.has(mode)
+ ? externalModes?.filter((value) => value !== mode)
+ : [...(externalModes ?? []), mode];
+ form.setValue('capabilities.modes', nextModes ?? [], { shouldDirty: true, shouldValidate: true });
+ },
+ [externalModes, form, modeSet]
+ );
+
+ const handleToggleTxt2Img = useCallback(() => toggleMode('txt2img'), [toggleMode]);
+ const handleToggleImg2Img = useCallback(() => toggleMode('img2img'), [toggleMode]);
+ const handleToggleInpaint = useCallback(() => toggleMode('inpaint'), [toggleMode]);
+
+ const parseOptionalNumber = useCallback((value: string | null | undefined) => {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ if (typeof value !== 'string') {
+ return Number.isNaN(Number(value)) ? null : Number(value);
+ }
+ if (value.trim() === '') {
+ return null;
+ }
+ const parsed = Number(value);
+ return Number.isNaN(parsed) ? null : parsed;
+ }, []);
+
+ const onSubmit = useCallback>(
(values) => {
const responseBody: UpdateModelArg = {
key: modelConfig.key,
body: values,
};
-
updateModel(responseBody)
.unwrap()
.then((payload) => {
@@ -160,6 +202,144 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
+ {isExternal && (
+ <>
+
+ {t('modelManager.externalProvider')}
+
+
+
+ {t('modelManager.providerId')}
+
+
+
+ {t('modelManager.providerModelId')}
+
+
+
+
+ {t('modelManager.externalCapabilities')}
+
+
+
+ {t('modelManager.supportedModes')}
+
+
+ txt2img
+
+
+ img2img
+
+
+ inpaint
+
+
+
+
+ {t('modelManager.supportsNegativePrompt')}
+
+
+
+ {t('modelManager.supportsReferenceImages')}
+
+
+
+ {t('modelManager.supportsSeed')}
+
+
+
+ {t('modelManager.supportsGuidance')}
+
+
+
+ {t('modelManager.maxImagesPerRequest')}
+
+
+
+ {t('modelManager.maxReferenceImages')}
+
+
+
+ {t('modelManager.maxImageWidth')}
+
+
+
+ {t('modelManager.maxImageHeight')}
+
+
+
+
+ {t('modelManager.externalDefaults')}
+
+
+
+ {t('modelManager.width')}
+
+
+
+ {t('modelManager.height')}
+
+
+
+ {t('parameters.steps')}
+
+
+
+ {t('parameters.guidance')}
+
+
+
+ {t('modelManager.numImages')}
+
+
+
+ >
+ )}
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index c2d20ccbbda..a91655168ca 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -9,14 +9,15 @@ import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/Tri
import { filesize } from 'filesize';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import type {
- AnyModelConfig,
- CLIPEmbedModelConfig,
- CLIPVisionModelConfig,
- LlavaOnevisionModelConfig,
- Qwen3EncoderModelConfig,
- SigLIPModelConfig,
- T5EncoderModelConfig,
+import {
+ isExternalApiModelConfig,
+ type AnyModelConfig,
+ type CLIPEmbedModelConfig,
+ type CLIPVisionModelConfig,
+ type LlavaOnevisionModelConfig,
+ type Qwen3EncoderModelConfig,
+ type SigLIPModelConfig,
+ type T5EncoderModelConfig,
} from 'services/api/types';
import { isExternalModel } from './isExternalModel';
@@ -98,6 +99,12 @@ export const ModelView = memo(({ modelConfig }: Props) => {
+ {isExternalApiModelConfig(modelConfig) && (
+ <>
+
+
+ >
+ )}
{'variant' in modelConfig && modelConfig.variant && (
)}
diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts
index 2cfe637bee3..44bacfadf5d 100644
--- a/invokeai/frontend/web/src/features/nodes/types/common.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/common.ts
@@ -93,6 +93,7 @@ export const zBaseModelType = z.enum([
'flux2',
'cogview4',
'z-image',
+ 'external',
'unknown',
]);
export type BaseModelType = z.infer;
@@ -117,6 +118,7 @@ export const zModelType = z.enum([
'clip_embed',
'siglip',
'flux_redux',
+ 'external_image_generator',
'unknown',
]);
export type ModelType = z.infer;
@@ -164,6 +166,7 @@ export const zModelFormat = z.enum([
'bnb_quantized_int8b',
'bnb_quantized_nf4b',
'gguf_quantized',
+ 'external_api',
'unknown',
]);
export type ModelFormat = z.infer;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
new file mode 100644
index 00000000000..fd787456381
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
@@ -0,0 +1,154 @@
+import type { RootState } from 'app/store/store';
+import type { ParamsState, RefImagesState } from 'features/controlLayers/store/types';
+import { imageDTOToCroppableImage, initialIPAdapter } from 'features/controlLayers/store/util';
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+ ImageDTO,
+ Invocation,
+} from 'services/api/types';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { buildExternalGraph } from './buildExternalGraph';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+ const capabilities: ExternalModelCapabilities = {
+ modes: ['txt2img'],
+ supports_negative_prompt: true,
+ supports_reference_images: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_image_size: maxImageSize,
+ };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities,
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ ...overrides,
+ };
+};
+
+let mockModelConfig: ExternalApiModelConfig | null = null;
+let mockParams: ParamsState;
+let mockRefImages: RefImagesState;
+let mockPrompts: { positive: string; negative: string };
+let mockSizes: { scaledSize: { width: number; height: number } };
+
+const mockOutputFields = {
+ id: 'external_output',
+ use_cache: false,
+ is_intermediate: false,
+ board: undefined,
+};
+
+vi.mock('features/controlLayers/store/paramsSlice', () => ({
+ selectModelConfig: () => mockModelConfig,
+ selectParamsSlice: () => mockParams,
+}));
+
+vi.mock('features/controlLayers/store/refImagesSlice', () => ({
+ selectRefImagesSlice: () => mockRefImages,
+}));
+
+vi.mock('features/nodes/util/graph/graphBuilderUtils', () => ({
+ getOriginalAndScaledSizesForTextToImage: () => mockSizes,
+ getOriginalAndScaledSizesForOtherModes: () => ({
+ scaledSize: { width: 512, height: 512 },
+ rect: { x: 0, y: 0, width: 512, height: 512 },
+ }),
+ selectCanvasOutputFields: () => mockOutputFields,
+ selectPresetModifiedPrompts: () => mockPrompts,
+}));
+
+beforeEach(() => {
+ mockParams = {
+ steps: 20,
+ guidance: 4.5,
+ } as ParamsState;
+ mockPrompts = { positive: 'a test prompt', negative: 'bad prompt' };
+ mockSizes = { scaledSize: { width: 768, height: 512 } };
+
+ const imageDTO = { image_name: 'ref.png', width: 64, height: 64 } as ImageDTO;
+ mockRefImages = {
+ selectedEntityId: null,
+ isPanelOpen: false,
+ entities: [
+ {
+ id: 'ref-image-1',
+ isEnabled: true,
+ config: {
+ ...initialIPAdapter,
+ weight: 0.5,
+ image: imageDTOToCroppableImage(imageDTO),
+ },
+ },
+ ],
+ };
+});
+
+describe('buildExternalGraph', () => {
+ it('builds txt2img graph with reference images and seed', async () => {
+ const modelConfig = createExternalModel();
+ mockModelConfig = modelConfig;
+
+ const { g } = await buildExternalGraph({
+ generationMode: 'txt2img',
+ state: {} as RootState,
+ manager: null,
+ });
+ const graph = g.getGraph();
+ const externalNode = Object.values(graph.nodes).find(
+ (node) => node.type === 'external_image_generation'
+ ) as Invocation<'external_image_generation'>;
+
+ expect(externalNode).toBeDefined();
+ expect(externalNode.mode).toBe('txt2img');
+ expect(externalNode.width).toBe(768);
+ expect(externalNode.height).toBe(512);
+ expect(externalNode.negative_prompt).toBe('bad prompt');
+ expect(externalNode.guidance).toBe(4.5);
+ expect(externalNode.reference_images?.[0]).toEqual({ image_name: 'ref.png' });
+ expect(externalNode.reference_image_weights).toEqual([0.5]);
+
+ const seedEdge = graph.edges.find((edge) => edge.destination.field === 'seed');
+ expect(seedEdge).toBeDefined();
+ });
+
+ it('throws when mode is unsupported', async () => {
+ const modelConfig = createExternalModel({
+ capabilities: {
+ modes: ['img2img'],
+ },
+ });
+ mockModelConfig = modelConfig;
+
+ await expect(
+ buildExternalGraph({
+ generationMode: 'txt2img',
+ state: {} as RootState,
+ manager: null,
+ })
+ ).rejects.toThrow('does not support txt2img');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
new file mode 100644
index 00000000000..02c030aa3b8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
@@ -0,0 +1,129 @@
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import { zImageField } from 'features/nodes/types/common';
+import { Graph } from 'features/nodes/util/graph/generation/Graph';
+import {
+ getOriginalAndScaledSizesForOtherModes,
+ getOriginalAndScaledSizesForTextToImage,
+ selectCanvasOutputFields,
+ selectPresetModifiedPrompts,
+} from 'features/nodes/util/graph/graphBuilderUtils';
+import {
+ type GraphBuilderArg,
+ type GraphBuilderReturn,
+ UnsupportedGenerationModeError,
+} from 'features/nodes/util/graph/types';
+import { type Invocation, isExternalApiModelConfig } from 'services/api/types';
+import { assert } from 'tsafe';
+
+export const buildExternalGraph = async (arg: GraphBuilderArg): Promise => {
+ const { generationMode, state, manager } = arg;
+
+ const model = selectModelConfig(state);
+ assert(model, 'No model selected');
+ assert(isExternalApiModelConfig(model), 'Selected model is not an external API model');
+
+ const requestedMode = generationMode === 'outpaint' ? 'inpaint' : generationMode;
+ if (!model.capabilities.modes.includes(requestedMode)) {
+ throw new UnsupportedGenerationModeError(`${model.name} does not support ${requestedMode} mode`);
+ }
+
+ const params = selectParamsSlice(state);
+ const refImages = selectRefImagesSlice(state);
+ const prompts = selectPresetModifiedPrompts(state);
+
+ const g = new Graph(getPrefixedId('external_graph'));
+
+ const seed = model.capabilities.supports_seed
+ ? g.addNode({
+ id: getPrefixedId('seed'),
+ type: 'integer',
+ })
+ : null;
+
+ const positivePrompt = g.addNode({
+ id: getPrefixedId('positive_prompt'),
+ type: 'string',
+ });
+
+ const externalNode = g.addNode({
+ id: getPrefixedId('external_image_generation'),
+ type: 'external_image_generation',
+ model,
+ mode: requestedMode,
+ negative_prompt: model.capabilities.supports_negative_prompt ? prompts.negative : null,
+ steps: params.steps,
+ guidance: model.capabilities.supports_guidance ? params.guidance : null,
+ num_images: 1,
+ });
+
+ if (seed) {
+ g.addEdge(seed, 'value', externalNode, 'seed');
+ }
+ g.addEdge(positivePrompt, 'value', externalNode, 'prompt');
+
+ if (model.capabilities.supports_reference_images) {
+ const referenceImages = refImages.entities
+ .filter((entity) => entity.isEnabled)
+ .map((entity) => entity.config)
+ .filter((config) => config.image)
+ .map((config) => zImageField.parse(config.image?.crop?.image ?? config.image?.original.image));
+
+ const referenceWeights = refImages.entities
+ .filter((entity) => entity.isEnabled)
+ .map((entity) => entity.config)
+ .filter((config) => config.image)
+ .map((config) => (config.type === 'ip_adapter' ? config.weight : null));
+
+ if (referenceImages.length > 0) {
+ externalNode.reference_images = referenceImages;
+ if (referenceWeights.every((weight): weight is number => weight !== null)) {
+ externalNode.reference_image_weights = referenceWeights;
+ }
+ }
+ }
+
+ if (generationMode === 'txt2img') {
+ const { scaledSize } = getOriginalAndScaledSizesForTextToImage(state);
+ externalNode.width = scaledSize.width;
+ externalNode.height = scaledSize.height;
+ } else {
+ assert(manager, 'Canvas manager is required for img2img/inpaint');
+ const canvasSettings = selectCanvasSettingsSlice(state);
+ const { scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state);
+ externalNode.width = scaledSize.width;
+ externalNode.height = scaledSize.height;
+
+ const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
+ const initImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
+ is_intermediate: true,
+ silent: true,
+ });
+ externalNode.init_image = { image_name: initImage.image_name };
+
+ if (generationMode === 'inpaint' || generationMode === 'outpaint') {
+ const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
+ const maskImage = await manager.compositor.getGrayscaleMaskCompositeImageDTO(
+ inpaintMaskAdapters,
+ rect,
+ 'denoiseLimit',
+ canvasSettings.preserveMask,
+ {
+ is_intermediate: true,
+ silent: true,
+ }
+ );
+ externalNode.mask_image = { image_name: maskImage.image_name };
+ }
+ }
+
+ g.updateNode(externalNode, selectCanvasOutputFields(state));
+
+ return {
+ g,
+ seed: seed ?? undefined,
+ positivePrompt: positivePrompt as Invocation<'string'>,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
new file mode 100644
index 00000000000..1ae1dcdc3a8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
@@ -0,0 +1,44 @@
+import type { ExternalApiModelConfig } from 'services/api/types';
+import { describe, expect, test } from 'vitest';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => ({
+ key: 'external-test',
+ name: 'External Test',
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'gemini',
+ provider_model_id: 'gemini-2.5-flash-image',
+ description: 'Test model',
+ source: 'external://gemini/gemini-2.5-flash-image',
+ source_type: 'url',
+ source_api_response: null,
+ path: '',
+ file_size: 0,
+ hash: 'external:gemini:gemini-2.5-flash-image',
+ cover_image: null,
+ is_default: false,
+ tags: ['external'],
+ capabilities: {
+ modes: ['txt2img'],
+ supports_reference_images: false,
+ supports_negative_prompt: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_images_per_request: 1,
+ max_image_size: null,
+ allowed_aspect_ratios: ['1:1', '16:9'],
+ max_reference_images: null,
+ mask_format: 'none',
+ input_image_required_for: null,
+ },
+ default_settings: null,
+ ...overrides,
+});
+
+describe('external model aspect ratios (bbox)', () => {
+ test('uses allowed aspect ratios for external models', () => {
+ const model = createExternalModel();
+ expect(model.capabilities.allowed_aspect_ratios).toEqual(['1:1', '16:9']);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
index a237896c676..28dcb54cd7b 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectAllowedAspectRatioIDs } from 'features/controlLayers/store/paramsSlice';
import { selectAspectRatioID } from 'features/controlLayers/store/selectors';
import { isAspectRatioID, zAspectRatioID } from 'features/controlLayers/store/types';
import type { ChangeEventHandler } from 'react';
@@ -15,6 +16,8 @@ export const BboxAspectRatioSelect = memo(() => {
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
const isStaging = useCanvasIsStaging();
+ const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs);
+ const options = allowedAspectRatios ?? zAspectRatioID.options;
const onChange = useCallback>(
(e) => {
@@ -32,7 +35,7 @@ export const BboxAspectRatioSelect = memo(() => {
{t('parameters.aspect')}
}>
- {zAspectRatioID.options.map((ratio) => (
+ {options.map((ratio) => (
diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
new file mode 100644
index 00000000000..636260d1d25
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
@@ -0,0 +1,44 @@
+import type { ExternalApiModelConfig } from 'services/api/types';
+import { describe, expect, test } from 'vitest';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => ({
+ key: 'external-test',
+ name: 'External Test',
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'gemini',
+ provider_model_id: 'gemini-2.5-flash-image',
+ description: 'Test model',
+ source: 'external://gemini/gemini-2.5-flash-image',
+ source_type: 'url',
+ source_api_response: null,
+ path: '',
+ file_size: 0,
+ hash: 'external:gemini:gemini-2.5-flash-image',
+ cover_image: null,
+ is_default: false,
+ tags: ['external'],
+ capabilities: {
+ modes: ['txt2img'],
+ supports_reference_images: false,
+ supports_negative_prompt: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_images_per_request: 1,
+ max_image_size: null,
+ allowed_aspect_ratios: ['1:1', '16:9'],
+ max_reference_images: null,
+ mask_format: 'none',
+ input_image_required_for: null,
+ },
+ default_settings: null,
+ ...overrides,
+});
+
+describe('external model aspect ratios', () => {
+ test('uses allowed aspect ratios for external models', () => {
+ const model = createExternalModel();
+ expect(model.capabilities.allowed_aspect_ratios).toEqual(['1:1', '16:9']);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
index 4d3edc6e4bd..5e2952552c9 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
@@ -1,7 +1,11 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
-import { aspectRatioIdChanged, selectAspectRatioID } from 'features/controlLayers/store/paramsSlice';
+import {
+ aspectRatioIdChanged,
+ selectAllowedAspectRatioIDs,
+ selectAspectRatioID,
+} from 'features/controlLayers/store/paramsSlice';
import { isAspectRatioID, zAspectRatioID } from 'features/controlLayers/store/types';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback } from 'react';
@@ -12,6 +16,8 @@ export const DimensionsAspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
+ const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs);
+ const options = allowedAspectRatios ?? zAspectRatioID.options;
const onChange = useCallback>(
(e) => {
@@ -29,7 +35,7 @@ export const DimensionsAspectRatioSelect = memo(() => {
{t('parameters.aspect')}
}>
- {zAspectRatioID.options.map((ratio) => (
+ {options.map((ratio) => (
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
new file mode 100644
index 00000000000..b908efa096e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
@@ -0,0 +1,61 @@
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+} from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { isExternalModelUnsupportedForTab } from './mainModelPickerUtils';
+
+const createExternalConfig = (modes: ExternalModelCapabilities['modes']): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities: {
+ modes,
+ supports_negative_prompt: true,
+ supports_reference_images: false,
+ max_image_size: maxImageSize,
+ },
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ };
+};
+
+describe('isExternalModelUnsupportedForTab', () => {
+ it('disables external models without txt2img for generate', () => {
+ const model = createExternalConfig(['img2img', 'inpaint']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'generate')).toBe(true);
+ });
+
+ it('allows external models with txt2img for generate', () => {
+ const model = createExternalConfig(['txt2img']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'generate')).toBe(false);
+ });
+
+ it('allows external models on canvas', () => {
+ const model = createExternalConfig(['inpaint']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'canvas')).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
new file mode 100644
index 00000000000..bc20c1a1184
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
@@ -0,0 +1,14 @@
+import type { TabName } from 'features/ui/store/uiTypes';
+import { type AnyModelConfig, isExternalApiModelConfig } from 'services/api/types';
+
+export const isExternalModelUnsupportedForTab = (model: AnyModelConfig, tab: TabName): boolean => {
+ if (!isExternalApiModelConfig(model)) {
+ return false;
+ }
+
+ if (tab === 'generate') {
+ return !model.capabilities.modes.includes('txt2img');
+ }
+
+ return false;
+};
diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
index c5397791b84..b8631f5f742 100644
--- a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
@@ -1,5 +1,6 @@
import type { BoxProps, ButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
import {
+ Badge,
Button,
Flex,
Icon,
@@ -33,7 +34,7 @@ import { memo, useCallback, useMemo, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiLinkSimple } from 'react-icons/pi';
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
-import type { AnyModelConfig } from 'services/api/types';
+import { type AnyModelConfig, type ExternalApiModelConfig, isExternalApiModelConfig } from 'services/api/types';
const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => {
const keys: string[] = [];
@@ -94,9 +95,7 @@ const NoOptionsFallback = memo(({ noOptionsText }: { noOptionsText?: string }) =
});
NoOptionsFallback.displayName = 'NoOptionsFallback';
-const getGroupIDFromModelConfig = (modelConfig: AnyModelConfig): string => {
- return modelConfig.base;
-};
+const getGroupIDFromModelConfig = (modelConfig: AnyModelConfig): string => modelConfig.base;
const getGroupNameFromModelConfig = (modelConfig: AnyModelConfig): string => {
return MODEL_BASE_TO_LONG_NAME[modelConfig.base];
@@ -387,6 +386,10 @@ const optionNameSx: SystemStyleObject = {
const PickerOptionComponent = typedMemo(
({ option, ...rest }: { option: WithStarred } & BoxProps) => {
const { isCompactView } = usePickerContext>();
+ const externalOption = isExternalApiModelConfig(option as AnyModelConfig)
+ ? (option as ExternalApiModelConfig)
+ : null;
+ const providerLabel = externalOption ? externalOption.provider_id.toUpperCase() : null;
return (
@@ -397,6 +400,15 @@ const PickerOptionComponent = typedMemo(
{option.name}
+ {!isCompactView && externalOption && (
+
+ {providerLabel}
+
+ )}
{option.file_size > 0 && (
(model: WithStarred, searchTerm: string) => {
const regex = getRegex(searchTerm);
const bases = BASE_KEYWORDS[model.base] ?? [model.base];
+ const externalModel = isExternalApiModelConfig(model as AnyModelConfig) ? (model as ExternalApiModelConfig) : null;
+ const externalSearch = externalModel ? ` ${externalModel.provider_id} ${externalModel.provider_model_id}` : '';
const testString =
- `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}`.toLowerCase();
+ `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}${externalSearch}`.toLowerCase();
if (testString.includes(searchTerm) || regex.test(testString)) {
return true;
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
index 652cf4c5b24..5bfc31d10fd 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
@@ -9,6 +9,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
+import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildExternalGraph';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
@@ -59,6 +60,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
return await buildCogView4Graph(graphBuilderArg);
case 'z-image':
return await buildZImageGraph(graphBuilderArg);
+ case 'external':
+ return await buildExternalGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
index cf00a12ee5f..c50f833ba85 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
@@ -7,6 +7,7 @@ import { withResult, withResultAsync } from 'common/util/result';
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
+import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildExternalGraph';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
@@ -52,6 +53,8 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
return await buildCogView4Graph(graphBuilderArg);
case 'z-image':
return await buildZImageGraph(graphBuilderArg);
+ case 'external':
+ return await buildExternalGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index 8fa97eff4a9..61955e82c92 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -39,7 +39,8 @@ import type { TabName } from 'features/ui/store/uiTypes';
import i18n from 'i18next';
import { atom, computed } from 'nanostores';
import { useEffect } from 'react';
-import type { MainModelConfig } from 'services/api/types';
+import type { MainOrExternalModelConfig } from 'services/api/types';
+import { isExternalApiModelConfig } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
/**
@@ -221,7 +222,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok
const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
isConnected: boolean;
- model: MainModelConfig | null | undefined;
+ model: MainOrExternalModelConfig | null | undefined;
params: ParamsState;
refImages: RefImagesState;
loras: LoRA[];
@@ -243,7 +244,11 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
- if (model?.base === 'flux') {
+ if (!model) {
+ // nothing else to validate
+ } else if (isExternalApiModelConfig(model)) {
+ // external models don't require local sub-models
+ } else if (model.base === 'flux') {
if (!params.t5EncoderModel) {
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
}
@@ -280,7 +285,7 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
}
}
- if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
+ if (model && !isExternalApiModelConfig(model) && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
enabledRefImages.forEach((entity, i) => {
@@ -431,7 +436,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: {
const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
isConnected: boolean;
- model: MainModelConfig | null | undefined;
+ model: MainOrExternalModelConfig | null | undefined;
canvas: CanvasState;
params: ParamsState;
refImages: RefImagesState;
@@ -488,7 +493,11 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
- if (model?.base === 'flux') {
+ if (!model) {
+ // nothing else to validate
+ } else if (isExternalApiModelConfig(model)) {
+ // external models don't require local sub-models
+ } else if (model.base === 'flux') {
if (!params.t5EncoderModel) {
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
}
@@ -682,7 +691,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
}
});
- if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
+ if (model && !isExternalApiModelConfig(model) && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
enabledRefImages.forEach((entity, i) => {
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
index 773b67e39bb..91f5f1efd0a 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
@@ -1,9 +1,11 @@
import { Flex, FormLabel, Icon } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import { isExternalModelUnsupportedForTab } from 'features/parameters/components/MainModel/mainModelPickerUtils';
import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
import { ModelPicker } from 'features/parameters/components/ModelPicker';
import { modelSelected } from 'features/parameters/store/actions';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdMoneyOff } from 'react-icons/md';
@@ -14,6 +16,7 @@ import { type AnyModelConfig, isNonCommercialMainModelConfig } from 'services/ap
export const MainModelPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
+ const activeTab = useAppSelector(selectActiveTab);
const [modelConfigs] = useMainModels();
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
@@ -28,6 +31,11 @@ export const MainModelPicker = memo(() => {
[selectedModelConfig]
);
+ const getIsOptionDisabled = useCallback(
+ (modelConfig: AnyModelConfig) => isExternalModelUnsupportedForTab(modelConfig, activeTab),
+ [activeTab]
+ );
+
return (
@@ -46,6 +54,7 @@ export const MainModelPicker = memo(() => {
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
+ getIsOptionDisabled={getIsOptionDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
new file mode 100644
index 00000000000..ea36cf4c65a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
@@ -0,0 +1,39 @@
+import { Badge, Flex, FormControl, FormLabel, Text, Tooltip } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetExternalProviderStatusesQuery } from 'services/api/endpoints/appInfo';
+
+import { getExternalProviderStatusBadgeInfo } from './externalProviderStatusUtils';
+
+export const ExternalProviderStatusList = memo(() => {
+ const { t } = useTranslation();
+ const { data } = useGetExternalProviderStatusesQuery();
+
+ if (!data || data.length === 0) {
+ return null;
+ }
+
+ const sortedProviders = [...data].sort((a, b) => a.provider_id.localeCompare(b.provider_id));
+
+ return (
+
+ {t('settings.externalProviders')}
+
+ {sortedProviders.map((status) => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(status);
+ const tooltip = badgeInfo.tooltipMessage ?? (badgeInfo.tooltipKey ? t(badgeInfo.tooltipKey) : null);
+ return (
+
+ {status.provider_id}
+
+ {t(badgeInfo.labelKey)}
+
+
+ );
+ })}
+
+
+ );
+});
+
+ExternalProviderStatusList.displayName = 'ExternalProviderStatusList';
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
index 6c7ebada8f0..b94669e92f0 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
@@ -20,6 +20,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice';
+import { ExternalProviderStatusList } from 'features/system/components/SettingsModal/ExternalProviderStatusList';
import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
@@ -48,8 +49,7 @@ import {
} from 'features/system/store/systemSlice';
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
-import type { ChangeEvent, ReactElement } from 'react';
-import { cloneElement, memo, useCallback, useEffect } from 'react';
+import { type ChangeEvent, cloneElement, memo, type ReactElement, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingsLanguageSelect } from './SettingsLanguageSelect';
@@ -198,6 +198,10 @@ const SettingsModal = (props: { children: ReactElement }) => {
+
+
+
+
{t('settings.showProgressInViewer')}
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
new file mode 100644
index 00000000000..98ae63004c3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
@@ -0,0 +1,38 @@
+import type { ExternalProviderStatus } from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { getExternalProviderStatusBadgeInfo } from './externalProviderStatusUtils';
+
+const buildStatus = (overrides: Partial = {}): ExternalProviderStatus => ({
+ provider_id: 'openai',
+ configured: false,
+ message: null,
+ ...overrides,
+});
+
+describe('getExternalProviderStatusBadgeInfo', () => {
+ it('marks configured providers as configured', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus({ configured: true }));
+
+ expect(badgeInfo.labelKey).toBe('settings.externalProviderConfigured');
+ expect(badgeInfo.tooltipKey).toBeNull();
+ expect(badgeInfo.tooltipMessage).toBeNull();
+ expect(badgeInfo.colorScheme).toBe('green');
+ });
+
+ it('adds hint when provider is not configured', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus());
+
+ expect(badgeInfo.labelKey).toBe('settings.externalProviderNotConfigured');
+ expect(badgeInfo.tooltipKey).toBe('settings.externalProviderNotConfiguredHint');
+ expect(badgeInfo.tooltipMessage).toBeNull();
+ expect(badgeInfo.colorScheme).toBe('warning');
+ });
+
+ it('prefers status messages when present', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus({ message: 'Missing key' }));
+
+ expect(badgeInfo.tooltipKey).toBeNull();
+ expect(badgeInfo.tooltipMessage).toBe('Missing key');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
new file mode 100644
index 00000000000..fb1f764e2a4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
@@ -0,0 +1,26 @@
+import type { ExternalProviderStatus } from 'services/api/types';
+
+type ExternalProviderStatusBadgeInfo = {
+ labelKey: 'settings.externalProviderConfigured' | 'settings.externalProviderNotConfigured';
+ tooltipKey: 'settings.externalProviderNotConfiguredHint' | null;
+ tooltipMessage: string | null;
+ colorScheme: 'green' | 'warning';
+};
+
+export const getExternalProviderStatusBadgeInfo = (status: ExternalProviderStatus): ExternalProviderStatusBadgeInfo => {
+ if (status.configured) {
+ return {
+ labelKey: 'settings.externalProviderConfigured',
+ tooltipKey: null,
+ tooltipMessage: status.message ?? null,
+ colorScheme: 'green',
+ };
+ }
+
+ return {
+ labelKey: 'settings.externalProviderNotConfigured',
+ tooltipKey: status.message ? null : 'settings.externalProviderNotConfiguredHint',
+ tooltipMessage: status.message ?? null,
+ colorScheme: 'warning',
+ };
+};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
index 0d71b621734..07da9e6e18b 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
@@ -1,8 +1,10 @@
import { Flex, FormControl, FormLabel, Icon } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import { isExternalModelUnsupportedForTab } from 'features/parameters/components/MainModel/mainModelPickerUtils';
import { ModelPicker } from 'features/parameters/components/ModelPicker';
import { modelSelected } from 'features/parameters/store/actions';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdMoneyOff } from 'react-icons/md';
@@ -13,6 +15,7 @@ import { type AnyModelConfig, isNonCommercialMainModelConfig } from 'services/ap
export const InitialStateMainModelPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
+ const activeTab = useAppSelector(selectActiveTab);
const [modelConfigs] = useMainModels();
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
@@ -27,6 +30,11 @@ export const InitialStateMainModelPicker = memo(() => {
[selectedModelConfig]
);
+ const getIsOptionDisabled = useCallback(
+ (modelConfig: AnyModelConfig) => isExternalModelUnsupportedForTab(modelConfig, activeTab),
+ [activeTab]
+ );
+
return (
@@ -45,6 +53,7 @@ export const InitialStateMainModelPicker = memo(() => {
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
+ getIsOptionDisabled={getIsOptionDisabled}
/>
);
diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
index f72d6ad81e8..1f605c6ae49 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
@@ -1,7 +1,7 @@
import type { OpenAPIV3_1 } from 'openapi-types';
import type { stringify } from 'querystring';
import type { paths } from 'services/api/schema';
-import type { AppVersion } from 'services/api/types';
+import type { AppVersion, ExternalProviderConfig, ExternalProviderStatus } from 'services/api/types';
import { api, buildV1Url } from '..';
@@ -52,6 +52,35 @@ export const appInfoApi = api.injectEndpoints({
method: 'GET',
}),
}),
+ getExternalProviderStatuses: build.query({
+ query: () => ({
+ url: buildAppInfoUrl('external_providers/status'),
+ method: 'GET',
+ }),
+ providesTags: ['FetchOnReconnect'],
+ }),
+ getExternalProviderConfigs: build.query({
+ query: () => ({
+ url: buildAppInfoUrl('external_providers/config'),
+ method: 'GET',
+ }),
+ providesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
+ setExternalProviderConfig: build.mutation({
+ query: ({ provider_id, ...body }) => ({
+ url: buildAppInfoUrl(`external_providers/config/${provider_id}`),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
+ resetExternalProviderConfig: build.mutation({
+ query: (provider_id) => ({
+ url: buildAppInfoUrl(`external_providers/config/${provider_id}`),
+ method: 'DELETE',
+ }),
+ invalidatesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
getInvocationCacheStatus: build.query<
paths['/api/v1/app/invocation_cache/status']['get']['responses']['200']['content']['application/json'],
void
@@ -95,6 +124,10 @@ export const {
useGetAppDepsQuery,
useGetPatchmatchStatusQuery,
useGetRuntimeConfigQuery,
+ useGetExternalProviderStatusesQuery,
+ useGetExternalProviderConfigsQuery,
+ useSetExternalProviderConfigMutation,
+ useResetExternalProviderConfigMutation,
useClearInvocationCacheMutation,
useDisableInvocationCacheMutation,
useEnableInvocationCacheMutation,
@@ -102,3 +135,8 @@ export const {
useGetOpenAPISchemaQuery,
useLazyGetOpenAPISchemaQuery,
} = appInfoApi;
+
+type SetExternalProviderConfigArg =
+ paths['/api/v1/app/external_providers/config/{provider_id}']['post']['requestBody']['content']['application/json'] & {
+ provider_id: paths['/api/v1/app/external_providers/config/{provider_id}']['post']['parameters']['path']['provider_id'];
+ };
diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index 98d7dd1e8df..fa3218d400e 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -9,11 +9,12 @@ import {
useGetMissingModelsQuery,
useGetModelConfigsQuery,
} from 'services/api/endpoints/models';
-import type { AnyModelConfig } from 'services/api/types';
+import type { AnyModelConfig, MainOrExternalModelConfig } from 'services/api/types';
import {
isCLIPEmbedModelConfigOrSubmodel,
isControlLayerModelConfig,
isControlNetModelConfig,
+ isExternalApiModelConfig,
isFlux1VAEModelConfig,
isFlux2VAEModelConfig,
isFluxKontextModelConfig,
@@ -21,7 +22,7 @@ import {
isFluxVAEModelConfig,
isIPAdapterModelConfig,
isLoRAModelConfig,
- isNonRefinerMainModelConfig,
+ isMainOrExternalModelConfig,
isQwen3EncoderModelConfig,
isRefinerMainModelModelConfig,
isSpandrelImageToImageModelConfig,
@@ -50,13 +51,13 @@ const buildModelsHook =
return modelConfigsAdapterSelectors
.selectAll(result.data)
.filter((config) => typeGuard(config))
- .filter((config) => !missingModelKeys.has(config.key))
+ .filter((config) => !missingModelKeys.has(config.key) || isExternalApiModelConfig(config))
.filter(filter);
}, [filter, result.data, missingModelsData]);
return [modelConfigs, result] as const;
};
-export const useMainModels = buildModelsHook(isNonRefinerMainModelConfig);
+export const useMainModels = buildModelsHook(isMainOrExternalModelConfig);
export const useRefinerModels = buildModelsHook(isRefinerMainModelModelConfig);
export const useLoRAModels = buildModelsHook(isLoRAModelConfig);
export const useControlLayerModels = buildModelsHook(isControlLayerModelConfig);
@@ -94,7 +95,7 @@ const buildModelsSelector =
return modelConfigsAdapterSelectors
.selectAll(result.data)
.filter(typeGuard)
- .filter((config) => !missingModelKeys.has(config.key));
+ .filter((config) => !missingModelKeys.has(config.key) || isExternalApiModelConfig(config));
};
export const selectIPAdapterModels = buildModelsSelector(isIPAdapterModelConfig);
export const selectGlobalRefImageModels = buildModelsSelector(
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index c6e096f474c..77816f68915 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1164,6 +1164,58 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/app/external_providers/status": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get External Provider Statuses */
+ get: operations["get_external_provider_statuses"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/app/external_providers/config": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get External Provider Configs */
+ get: operations["get_external_provider_configs"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/app/external_providers/config/{provider_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Set External Provider Config */
+ post: operations["set_external_provider_config"];
+ /** Reset External Provider Config */
+ delete: operations["reset_external_provider_config"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/app/logging": {
parameters: {
query?: never;
@@ -2171,7 +2223,7 @@ export type components = {
*/
type: "alpha_mask_to_tensor";
};
- AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* AppVersion
* @description App Version Response
@@ -2323,7 +2375,7 @@ export type components = {
* fallback/null value `BaseModelType.Any` for these models, instead of making the model base optional.
* @enum {string}
*/
- BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "flux2" | "cogview4" | "z-image" | "unknown";
+ BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "flux2" | "cogview4" | "z-image" | "external" | "unknown";
/** Batch */
Batch: {
/**
@@ -7413,6 +7465,345 @@ export type components = {
/** Fieldname */
fieldName: string;
};
+ /** ExternalApiModelConfig */
+ ExternalApiModelConfig: {
+ /**
+ * Key
+ * @description A unique key for this model.
+ */
+ key: string;
+ /**
+ * Hash
+ * @default
+ */
+ hash: string;
+ /**
+ * Path
+ * @default
+ */
+ path: string;
+ /**
+ * File Size
+ * @default 0
+ */
+ file_size: number;
+ /**
+ * Name
+ * @description Name of the model.
+ */
+ name: string;
+ /**
+ * Description
+ * @description Model description
+ */
+ description: string | null;
+ /**
+ * Source
+ * @default
+ */
+ source: string;
+ /** @default url */
+ source_type: components["schemas"]["ModelSourceType"];
+ /**
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
+ */
+ source_api_response: string | null;
+ /**
+ * Cover Image
+ * @description Url for image to preview model
+ */
+ cover_image: string | null;
+ /**
+ * Base
+ * @default external
+ * @constant
+ */
+ base: "external";
+ /**
+ * Type
+ * @default external_image_generator
+ * @constant
+ */
+ type: "external_image_generator";
+ /**
+ * Format
+ * @default external_api
+ * @constant
+ */
+ format: "external_api";
+ /**
+ * Provider Id
+ * @description External provider ID
+ */
+ provider_id: string;
+ /**
+ * Provider Model Id
+ * @description Provider-specific model ID
+ */
+ provider_model_id: string;
+ /** @description Provider capability matrix */
+ capabilities: components["schemas"]["ExternalModelCapabilities"];
+ default_settings: components["schemas"]["ExternalApiModelDefaultSettings"] | null;
+ /** Tags */
+ tags: string[] | null;
+ /**
+ * Is Default
+ * @default false
+ */
+ is_default: boolean;
+ };
+ /** ExternalApiModelDefaultSettings */
+ ExternalApiModelDefaultSettings: {
+ /** Width */
+ width?: number | null;
+ /** Height */
+ height?: number | null;
+ /** Steps */
+ steps?: number | null;
+ /** Guidance */
+ guidance?: number | null;
+ /** Num Images */
+ num_images?: number | null;
+ };
+ /**
+ * External Image Generation
+ * @description Generate images using an external provider.
+ */
+ ExternalImageGenerationInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description Main model (UNet, VAE, CLIP) to load
+ * @default null
+ */
+ model?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * Mode
+ * @description Generation mode
+ * @default txt2img
+ * @enum {string}
+ */
+ mode?: "txt2img" | "img2img" | "inpaint";
+ /**
+ * Prompt
+ * @description Prompt
+ * @default null
+ */
+ prompt?: string | null;
+ /**
+ * Negative Prompt
+ * @description Negative prompt
+ * @default null
+ */
+ negative_prompt?: string | null;
+ /**
+ * Seed
+ * @description Seed for random number generation
+ * @default null
+ */
+ seed?: number | null;
+ /**
+ * Num Images
+ * @description Number of images to generate
+ * @default 1
+ */
+ num_images?: number;
+ /**
+ * Width
+ * @description Width of output (px)
+ * @default 1024
+ */
+ width?: number;
+ /**
+ * Height
+ * @description Height of output (px)
+ * @default 1024
+ */
+ height?: number;
+ /**
+ * Steps
+ * @description Number of steps to run
+ * @default null
+ */
+ steps?: number | null;
+ /**
+ * Guidance
+ * @description Guidance strength
+ * @default null
+ */
+ guidance?: number | null;
+ /**
+ * @description Init image for img2img/inpaint
+ * @default null
+ */
+ init_image?: components["schemas"]["ImageField"] | null;
+ /**
+ * @description Mask image for inpaint
+ * @default null
+ */
+ mask_image?: components["schemas"]["ImageField"] | null;
+ /**
+ * Reference Images
+ * @description Reference images
+ * @default []
+ */
+ reference_images?: components["schemas"]["ImageField"][];
+ /**
+ * Reference Image Weights
+ * @description Reference image weights
+ * @default null
+ */
+ reference_image_weights?: number[] | null;
+ /**
+ * Reference Image Modes
+ * @description Reference image modes
+ * @default null
+ */
+ reference_image_modes?: string[] | null;
+ /**
+ * type
+ * @default external_image_generation
+ * @constant
+ */
+ type: "external_image_generation";
+ };
+ /** ExternalImageSize */
+ ExternalImageSize: {
+ /** Width */
+ width: number;
+ /** Height */
+ height: number;
+ };
+ /** ExternalModelCapabilities */
+ ExternalModelCapabilities: {
+ /** Modes */
+ modes?: ("txt2img" | "img2img" | "inpaint")[];
+ /**
+ * Supports Reference Images
+ * @default false
+ */
+ supports_reference_images?: boolean;
+ /**
+ * Supports Negative Prompt
+ * @default true
+ */
+ supports_negative_prompt?: boolean;
+ /**
+ * Supports Seed
+ * @default false
+ */
+ supports_seed?: boolean;
+ /**
+ * Supports Guidance
+ * @default false
+ */
+ supports_guidance?: boolean;
+ /** Max Images Per Request */
+ max_images_per_request?: number | null;
+ max_image_size?: components["schemas"]["ExternalImageSize"] | null;
+ /** Allowed Aspect Ratios */
+ allowed_aspect_ratios?: string[] | null;
+ /** Max Reference Images */
+ max_reference_images?: number | null;
+ /**
+ * Mask Format
+ * @default none
+ * @enum {string}
+ */
+ mask_format?: "alpha" | "binary" | "none";
+ /** Input Image Required For */
+ input_image_required_for?: ("txt2img" | "img2img" | "inpaint")[] | null;
+ };
+ /**
+ * ExternalModelSource
+ * @description An external provider model identifier.
+ */
+ ExternalModelSource: {
+ /** Provider Id */
+ provider_id: string;
+ /** Provider Model Id */
+ provider_model_id: string;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "external";
+ };
+ /** ExternalProviderConfigModel */
+ ExternalProviderConfigModel: {
+ /**
+ * Provider Id
+ * @description The external provider identifier
+ */
+ provider_id: string;
+ /**
+ * Api Key Configured
+ * @description Whether an API key is configured
+ */
+ api_key_configured: boolean;
+ /**
+ * Base Url
+ * @description Optional base URL override
+ */
+ base_url?: string | null;
+ };
+ /** ExternalProviderConfigUpdate */
+ ExternalProviderConfigUpdate: {
+ /**
+ * Api Key
+ * @description API key for the external provider
+ */
+ api_key?: string | null;
+ /**
+ * Base Url
+ * @description Optional base URL override for the provider
+ */
+ base_url?: string | null;
+ };
+ /** ExternalProviderStatusModel */
+ ExternalProviderStatusModel: {
+ /**
+ * Provider Id
+ * @description The external provider identifier
+ */
+ provider_id: string;
+ /**
+ * Configured
+ * @description Whether credentials are configured for the provider
+ */
+ configured: boolean;
+ /**
+ * Message
+ * @description Optional provider status detail
+ */
+ message?: string | null;
+ };
/**
* Apply LoRA Collection - FLUX
* @description Applies a collection of LoRAs to a FLUX transformer.
@@ -10013,7 +10404,7 @@ export type components = {
* @description The nodes in this graph
*/
nodes?: {
- [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
};
/**
* Edges
@@ -13229,7 +13620,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -13287,7 +13678,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -13353,6 +13744,7 @@ export type components = {
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
esrgan: components["schemas"]["ImageOutput"];
expand_mask_with_fade: components["schemas"]["ImageOutput"];
+ external_image_generation: components["schemas"]["ImageCollectionOutput"];
face_identifier: components["schemas"]["ImageOutput"];
face_mask_detection: components["schemas"]["FaceMaskOutput"];
face_off: components["schemas"]["FaceOffOutput"];
@@ -13585,7 +13977,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -13654,7 +14046,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -14092,6 +14484,26 @@ export type components = {
* @default true
*/
allow_unknown_models?: boolean;
+ /**
+ * External Gemini Api Key
+ * @description API key for Gemini image generation.
+ */
+ external_gemini_api_key?: string | null;
+ /**
+ * External Openai Api Key
+ * @description API key for OpenAI image generation.
+ */
+ external_openai_api_key?: string | null;
+ /**
+ * External Gemini Base Url
+ * @description Base URL override for Gemini image generation.
+ */
+ external_gemini_base_url?: string | null;
+ /**
+ * External Openai Base Url
+ * @description Base URL override for OpenAI image generation.
+ */
+ external_openai_base_url?: string | null;
};
/**
* InvokeAIAppConfigWithSetFields
@@ -19862,7 +20274,7 @@ export type components = {
* @description Storage format of model.
* @enum {string}
*/
- ModelFormat: "omi" | "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "qwen3_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b" | "gguf_quantized" | "unknown";
+ ModelFormat: "omi" | "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "qwen3_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b" | "gguf_quantized" | "external_api" | "unknown";
/** ModelIdentifierField */
ModelIdentifierField: {
/**
@@ -19963,7 +20375,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
};
/**
* ModelInstallCompleteEvent
@@ -19984,7 +20396,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
/**
* Key
* @description Model config record key
@@ -19999,7 +20411,7 @@ export type components = {
* Config
* @description The installed model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
/**
* ModelInstallDownloadProgressEvent
@@ -20020,7 +20432,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
/**
* Local Path
* @description Where model is downloading to
@@ -20063,7 +20475,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
/**
* Local Path
* @description Where model is downloading to
@@ -20106,7 +20518,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
};
/**
* ModelInstallErrorEvent
@@ -20127,7 +20539,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
/**
* Error Type
* @description The name of the exception
@@ -20165,7 +20577,7 @@ export type components = {
* Config Out
* @description After successful installation, this will hold the configuration object.
*/
- config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"]) | null;
+ config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]) | null;
/**
* Inplace
* @description Leave model in its current location; otherwise install under models directory
@@ -20176,7 +20588,7 @@ export type components = {
* Source
* @description Source (URL, repo_id, or local path) of model
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
/**
* Local Path
* Format: path
@@ -20235,7 +20647,7 @@ export type components = {
* Source
* @description Source of the model; local path, repo_id or url
*/
- source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"];
+ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["URLModelSource"] | components["schemas"]["ExternalModelSource"];
};
/**
* ModelLoadCompleteEvent
@@ -20251,7 +20663,7 @@ export type components = {
* Config
* @description The model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* @description The submodel type, if any
* @default null
@@ -20272,7 +20684,7 @@ export type components = {
* Config
* @description The model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* @description The submodel type, if any
* @default null
@@ -20371,7 +20783,19 @@ export type components = {
* Default Settings
* @description Default settings for this model
*/
- default_settings?: components["schemas"]["MainModelDefaultSettings"] | components["schemas"]["LoraModelDefaultSettings"] | components["schemas"]["ControlAdapterDefaultSettings"] | null;
+ default_settings?: components["schemas"]["MainModelDefaultSettings"] | components["schemas"]["LoraModelDefaultSettings"] | components["schemas"]["ControlAdapterDefaultSettings"] | components["schemas"]["ExternalApiModelDefaultSettings"] | null;
+ /**
+ * Provider Id
+ * @description External provider identifier
+ */
+ provider_id?: string | null;
+ /**
+ * Provider Model Id
+ * @description External provider model identifier
+ */
+ provider_model_id?: string | null;
+ /** @description External model capabilities */
+ capabilities?: components["schemas"]["ExternalModelCapabilities"] | null;
/**
* Cpu Only
* @description Whether this model should run on CPU only
@@ -20433,7 +20857,7 @@ export type components = {
* @description Model type.
* @enum {string}
*/
- ModelType: "onnx" | "main" | "vae" | "lora" | "control_lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "clip_embed" | "t2i_adapter" | "t5_encoder" | "qwen3_encoder" | "spandrel_image_to_image" | "siglip" | "flux_redux" | "llava_onevision" | "unknown";
+ ModelType: "onnx" | "main" | "vae" | "lora" | "control_lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "clip_embed" | "t2i_adapter" | "t5_encoder" | "qwen3_encoder" | "spandrel_image_to_image" | "siglip" | "flux_redux" | "llava_onevision" | "external_image_generator" | "unknown";
/**
* ModelVariantType
* @description Variant type.
@@ -20446,7 +20870,7 @@ export type components = {
*/
ModelsList: {
/** Models */
- models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"])[];
+ models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"])[];
};
/**
* Multiply Integers
@@ -27732,7 +28156,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Validation Error */
@@ -27782,7 +28206,7 @@ export interface operations {
* "repo_variant": "fp16",
* "upcast_attention": false
* } */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Bad request */
@@ -27887,7 +28311,7 @@ export interface operations {
* "repo_variant": "fp16",
* "upcast_attention": false
* } */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Bad request */
@@ -27958,7 +28382,7 @@ export interface operations {
* "repo_variant": "fp16",
* "upcast_attention": false
* } */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Bad request */
@@ -28498,7 +28922,7 @@ export interface operations {
* "repo_variant": "fp16",
* "upcast_attention": false
* } */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Bad request */
@@ -30288,6 +30712,114 @@ export interface operations {
};
};
};
+ get_external_provider_statuses: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ExternalProviderStatusModel"][];
+ };
+ };
+ };
+ };
+ get_external_provider_configs: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ExternalProviderConfigModel"][];
+ };
+ };
+ };
+ };
+ set_external_provider_config: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The external provider identifier */
+ provider_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ExternalProviderConfigUpdate"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ExternalProviderConfigModel"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ reset_external_provider_config: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The external provider identifier */
+ provider_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ExternalProviderConfigModel"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
get_log_level: {
parameters: {
query?: never;
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 5d56c346f87..0c06e04dcd6 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -43,6 +43,9 @@ export type InvocationJSONSchemaExtra = S['UIConfigBase'];
// App Info
export type AppVersion = S['AppVersion'];
+export type ExternalProviderStatus = S['ExternalProviderStatusModel'];
+export type ExternalProviderConfig = S['ExternalProviderConfigModel'];
+export type UpdateModelBody = paths['/api/v2/models/i/{key}']['patch']['requestBody']['content']['application/json'];
const zResourceOrigin = z.enum(['internal', 'external']);
type ResourceOrigin = z.infer;
@@ -110,6 +113,46 @@ export type ChatGPT4oModelConfig = ApiModelConfig;
export type Gemini2_5ModelConfig = ApiModelConfig;
type SubmodelDefinition = S['SubmodelDefinition'];
+export type ExternalImageSize = {
+ width: number;
+ height: number;
+};
+
+export type ExternalModelCapabilities = {
+ modes: ('txt2img' | 'img2img' | 'inpaint')[];
+ supports_reference_images?: boolean;
+ supports_negative_prompt?: boolean;
+ supports_seed?: boolean;
+ supports_guidance?: boolean;
+ max_images_per_request?: number | null;
+ max_image_size?: ExternalImageSize | null;
+ allowed_aspect_ratios?: string[] | null;
+ max_reference_images?: number | null;
+ mask_format?: 'alpha' | 'binary' | 'none';
+ input_image_required_for?: ('txt2img' | 'img2img' | 'inpaint')[] | null;
+};
+
+export type ExternalApiModelDefaultSettings = {
+ width?: number | null;
+ height?: number | null;
+ steps?: number | null;
+ guidance?: number | null;
+ num_images?: number | null;
+};
+
+export type ExternalApiModelConfig = AnyModelConfig & {
+ base: 'external';
+ type: 'external_image_generator';
+ format: 'external_api';
+ provider_id: string;
+ provider_model_id: string;
+ capabilities: ExternalModelCapabilities;
+ default_settings?: ExternalApiModelDefaultSettings | null;
+ tags?: string[] | null;
+ is_default?: boolean;
+};
+export type MainOrExternalModelConfig = MainModelConfig | ExternalApiModelConfig;
+
/**
* Checks if a list of submodels contains any that match a given variant or type
* @param submodels The list of submodels to check
@@ -290,6 +333,10 @@ export const isFluxReduxModelConfig = (config: AnyModelConfig): config is FLUXRe
return config.type === 'flux_redux';
};
+export const isExternalApiModelConfig = (config: AnyModelConfig): config is ExternalApiModelConfig => {
+ return (config as { format?: string }).format === 'external_api';
+};
+
export const isUnknownModelConfig = (config: AnyModelConfig): config is UnknownModelConfig => {
return config.type === 'unknown';
};
@@ -302,6 +349,10 @@ export const isNonRefinerMainModelConfig = (config: AnyModelConfig): config is M
return config.type === 'main' && config.base !== 'sdxl-refiner';
};
+export const isMainOrExternalModelConfig = (config: AnyModelConfig): config is MainOrExternalModelConfig => {
+ return isNonRefinerMainModelConfig(config) || isExternalApiModelConfig(config);
+};
+
export const isRefinerMainModelModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
return config.type === 'main' && config.base === 'sdxl-refiner';
};
diff --git a/tests/app/invocations/test_external_image_generation.py b/tests/app/invocations/test_external_image_generation.py
new file mode 100644
index 00000000000..3ede7aef421
--- /dev/null
+++ b/tests/app/invocations/test_external_image_generation.py
@@ -0,0 +1,120 @@
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+from PIL import Image
+
+from invokeai.app.invocations.external_image_generation import ExternalImageGenerationInvocation
+from invokeai.app.invocations.fields import ImageField
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.shared.graph import Graph, GraphExecutionState
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+
+
+def _build_model() -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img"],
+ supports_reference_images=True,
+ supports_negative_prompt=True,
+ supports_seed=True,
+ ),
+ )
+
+
+def _build_context(model_config: ExternalApiModelConfig, generated_image: Image.Image) -> MagicMock:
+ context = MagicMock()
+ context.models.get_config.return_value = model_config
+ context.images.get_pil.return_value = generated_image
+ context.images.save.return_value = SimpleNamespace(image_name="result.png")
+ context._services.external_generation.generate.return_value = ExternalGenerationResult(
+ images=[ExternalGeneratedImage(image=generated_image, seed=42)],
+ provider_request_id="req-123",
+ provider_metadata={"model": model_config.provider_model_id},
+ )
+ return context
+
+
+def test_external_invocation_builds_request_and_outputs() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ negative_prompt="bad",
+ seed=123,
+ num_images=1,
+ width=512,
+ height=512,
+ steps=10,
+ guidance=4.5,
+ reference_images=[ImageField(image_name="ref.png")],
+ reference_image_weights=[0.6],
+ )
+
+ output = invocation.invoke(context)
+
+ request = context._services.external_generation.generate.call_args[0][0]
+ assert request.prompt == "A prompt"
+ assert request.negative_prompt == "bad"
+ assert request.seed == 123
+ assert len(request.reference_images) == 1
+ assert request.reference_images[0].weight == 0.6
+ assert output.collection[0].image_name == "result.png"
+
+
+def test_external_invocation_rejects_mismatched_reference_weights() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ reference_images=[ImageField(image_name="ref.png")],
+ reference_image_weights=[0.1, 0.2],
+ )
+
+ with pytest.raises(ValueError, match="reference_image_weights"):
+ invocation.invoke(context)
+
+
+def test_external_graph_execution_state_runs_node() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ )
+
+ graph = Graph()
+ graph.add_node(invocation)
+
+ session = GraphExecutionState(graph=graph)
+ node = session.next()
+ assert node is not None
+ output = node.invoke(context)
+ session.complete(node.id, output)
+
+ assert session.results[node.id] == output
diff --git a/tests/app/routers/test_app_info.py b/tests/app/routers/test_app_info.py
new file mode 100644
index 00000000000..12201249ef4
--- /dev/null
+++ b/tests/app/routers/test_app_info.py
@@ -0,0 +1,93 @@
+import os
+import os
+from pathlib import Path
+from typing import Any
+
+import pytest
+from fastapi.testclient import TestClient
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api_app import app
+from invokeai.app.services.config.config_default import get_config, load_and_migrate_config
+from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
+from invokeai.app.services.invoker import Invoker
+
+
+@pytest.fixture(autouse=True, scope="module")
+def client(invokeai_root_dir: Path) -> TestClient:
+ os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
+ return TestClient(app)
+
+
+class MockApiDependencies(ApiDependencies):
+ invoker: Invoker
+
+ def __init__(self, invoker: Invoker) -> None:
+ self.invoker = invoker
+
+
+def test_get_external_provider_statuses(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ statuses = {
+ "gemini": ExternalProviderStatus(provider_id="gemini", configured=True, message=None),
+ "openai": ExternalProviderStatus(provider_id="openai", configured=False, message="Missing key"),
+ }
+
+ monkeypatch.setattr("invokeai.app.api.routers.app_info.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr(mock_invoker.services.external_generation, "get_provider_statuses", lambda: statuses)
+
+ response = client.get("/api/v1/app/external_providers/status")
+
+ assert response.status_code == 200
+ payload = sorted(response.json(), key=lambda item: item["provider_id"])
+ assert payload == [
+ {"provider_id": "gemini", "configured": True, "message": None},
+ {"provider_id": "openai", "configured": False, "message": "Missing key"},
+ ]
+
+
+def test_external_provider_config_update_and_reset(client: TestClient) -> None:
+ for provider_id in ("gemini", "openai"):
+ response = client.delete(f"/api/v1/app/external_providers/config/{provider_id}")
+ assert response.status_code == 200
+
+ response = client.get("/api/v1/app/external_providers/config")
+ assert response.status_code == 200
+ payload = response.json()
+ openai_config = _get_provider_config(payload, "openai")
+ assert openai_config["api_key_configured"] is False
+ assert openai_config["base_url"] is None
+
+ response = client.post(
+ "/api/v1/app/external_providers/config/openai",
+ json={"api_key": "openai-key", "base_url": "https://api.openai.test"},
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["api_key_configured"] is True
+ assert payload["base_url"] == "https://api.openai.test"
+
+ response = client.get("/api/v1/app/external_providers/config")
+ assert response.status_code == 200
+ payload = response.json()
+ openai_config = _get_provider_config(payload, "openai")
+ assert openai_config["api_key_configured"] is True
+ assert openai_config["base_url"] == "https://api.openai.test"
+
+ config_path = get_config().config_file_path
+ file_config = load_and_migrate_config(config_path)
+ assert file_config.external_openai_api_key == "openai-key"
+ assert file_config.external_openai_base_url == "https://api.openai.test"
+
+ response = client.delete("/api/v1/app/external_providers/config/openai")
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["api_key_configured"] is False
+ assert payload["base_url"] is None
+
+ file_config = load_and_migrate_config(config_path)
+ assert file_config.external_openai_api_key is None
+ assert file_config.external_openai_base_url is None
+
+
+def _get_provider_config(payload: list[dict[str, Any]], provider_id: str) -> dict[str, Any]:
+ return next(item for item in payload if item["provider_id"] == provider_id)
diff --git a/tests/app/routers/test_model_manager.py b/tests/app/routers/test_model_manager.py
new file mode 100644
index 00000000000..8f69ffce371
--- /dev/null
+++ b/tests/app/routers/test_model_manager.py
@@ -0,0 +1,71 @@
+import os
+from pathlib import Path
+from typing import Any
+
+import pytest
+from fastapi.testclient import TestClient
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api_app import app
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+from invokeai.backend.model_manager.taxonomy import ModelType
+
+
+@pytest.fixture(autouse=True, scope="module")
+def client(invokeai_root_dir: Path) -> TestClient:
+ os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
+ return TestClient(app)
+
+
+class DummyModelImages:
+ def get_url(self, key: str) -> str:
+ return f"https://example.com/models/{key}.png"
+
+
+class DummyInvoker:
+ def __init__(self, services: Any) -> None:
+ self.services = services
+
+
+class MockApiDependencies(ApiDependencies):
+ invoker: DummyInvoker
+
+ def __init__(self, invoker: DummyInvoker) -> None:
+ self.invoker = invoker
+
+
+def test_model_manager_external_config_round_trip(
+ monkeypatch: Any, client: TestClient, mm2_model_manager: Any, mm2_app_config: Any
+) -> None:
+ config = ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(modes=["txt2img"]),
+ )
+ mm2_model_manager.store.add_model(config)
+
+ services = type("Services", (), {})()
+ services.model_manager = mm2_model_manager
+ services.model_images = DummyModelImages()
+ services.configuration = mm2_app_config
+
+ invoker = DummyInvoker(services)
+ monkeypatch.setattr("invokeai.app.api.routers.model_manager.ApiDependencies", MockApiDependencies(invoker))
+
+ response = client.get("/api/v2/models/", params={"model_type": ModelType.ExternalImageGenerator.value})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert len(payload["models"]) == 1
+ assert payload["models"][0]["key"] == "external_test"
+ assert payload["models"][0]["provider_id"] == "openai"
+ assert payload["models"][0]["cover_image"] == "https://example.com/models/external_test.png"
+
+ get_response = client.get("/api/v2/models/i/external_test")
+
+ assert get_response.status_code == 200
+ model_payload = get_response.json()
+ assert model_payload["provider_model_id"] == "gpt-image-1"
+ assert model_payload["cover_image"] == "https://example.com/models/external_test.png"
diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py
new file mode 100644
index 00000000000..8379b8b754c
--- /dev/null
+++ b/tests/app/services/external_generation/test_external_generation_service.py
@@ -0,0 +1,243 @@
+import logging
+
+import pytest
+from PIL import Image
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderNotConfiguredError,
+ ExternalProviderNotFoundError,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
+
+
+class DummyProvider(ExternalProvider):
+ def __init__(self, provider_id: str, configured: bool, result: ExternalGenerationResult | None = None) -> None:
+ super().__init__(InvokeAIAppConfig(), logging.getLogger("test"))
+ self.provider_id = provider_id
+ self._configured = configured
+ self._result = result
+ self.last_request: ExternalGenerationRequest | None = None
+
+ def is_configured(self) -> bool:
+ return self._configured
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ self.last_request = request
+ assert self._result is not None
+ return self._result
+
+
+def _build_model(capabilities: ExternalModelCapabilities) -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=capabilities,
+ )
+
+
+def _build_request(
+ *,
+ model: ExternalApiModelConfig,
+ mode: str = "txt2img",
+ negative_prompt: str | None = None,
+ seed: int | None = None,
+ num_images: int = 1,
+ guidance: float | None = None,
+ width: int = 64,
+ height: int = 64,
+ init_image: Image.Image | None = None,
+ mask_image: Image.Image | None = None,
+ reference_images: list[ExternalReferenceImage] | None = None,
+) -> ExternalGenerationRequest:
+ return ExternalGenerationRequest(
+ model=model,
+ mode=mode, # type: ignore[arg-type]
+ prompt="A test prompt",
+ negative_prompt=negative_prompt,
+ seed=seed,
+ num_images=num_images,
+ width=width,
+ height=height,
+ steps=10,
+ guidance=guidance,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images or [],
+ metadata=None,
+ )
+
+
+def _make_image() -> Image.Image:
+ return Image.new("RGB", (64, 64), color="black")
+
+
+def test_generate_requires_registered_provider() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model)
+ service = ExternalGenerationService({}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderNotFoundError):
+ service.generate(request)
+
+
+def test_generate_requires_configured_provider() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model)
+ provider = DummyProvider("openai", configured=False)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderNotConfiguredError):
+ service.generate(request)
+
+
+def test_generate_validates_mode_support() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model, mode="img2img", init_image=_make_image())
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Mode 'img2img'"):
+ service.generate(request)
+
+
+def test_generate_validates_negative_prompt_support() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=False))
+ request = _build_request(model=model, negative_prompt="bad")
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Negative prompts"):
+ service.generate(request)
+
+
+def test_generate_requires_init_image_for_img2img() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["img2img"]))
+ request = _build_request(model=model, mode="img2img")
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="requires an init image"):
+ service.generate(request)
+
+
+def test_generate_requires_mask_for_inpaint() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["inpaint"]))
+ request = _build_request(model=model, mode="inpaint", init_image=_make_image())
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="requires a mask"):
+ service.generate(request)
+
+
+def test_generate_validates_reference_images() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_reference_images=False))
+ request = _build_request(
+ model=model,
+ reference_images=[ExternalReferenceImage(image=_make_image(), weight=0.8)],
+ )
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Reference images"):
+ service.generate(request)
+
+
+def test_generate_validates_limits() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ supports_reference_images=True,
+ max_reference_images=1,
+ max_images_per_request=1,
+ )
+ )
+ request = _build_request(
+ model=model,
+ num_images=2,
+ reference_images=[
+ ExternalReferenceImage(image=_make_image()),
+ ExternalReferenceImage(image=_make_image()),
+ ],
+ )
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="supports at most"):
+ service.generate(request)
+
+
+def test_generate_validates_allowed_aspect_ratios() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1", "16:9"],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ },
+ )
+ )
+ request = _build_request(model=model)
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+ assert response.images == []
+ assert provider.last_request is not None
+ assert provider.last_request.width == 1024
+ assert provider.last_request.height == 1024
+
+
+def test_generate_validates_allowed_aspect_ratios_with_bucket_sizes() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1", "16:9"],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ },
+ )
+ )
+ request = _build_request(model=model, width=160, height=90)
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert response.images == []
+ assert provider.last_request is not None
+ assert provider.last_request.width == 1344
+ assert provider.last_request.height == 768
+
+
+def test_generate_happy_path() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True, supports_seed=True)
+ )
+ request = _build_request(model=model, negative_prompt="", seed=42)
+ result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=_make_image(), seed=42)])
+ provider = DummyProvider("openai", configured=True, result=result)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert response is result
+ assert provider.last_request == request
diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py
new file mode 100644
index 00000000000..38f4c9e3d52
--- /dev/null
+++ b/tests/app/services/external_generation/test_external_provider_adapters.py
@@ -0,0 +1,346 @@
+import io
+import logging
+
+import pytest
+from PIL import Image
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
+from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+
+
+class DummyResponse:
+ def __init__(self, ok: bool, status_code: int = 200, json_data: dict | None = None, text: str = "") -> None:
+ self.ok = ok
+ self.status_code = status_code
+ self._json_data = json_data or {}
+ self.text = text
+ self.headers: dict[str, str] = {}
+
+ def json(self) -> dict:
+ return self._json_data
+
+
+def _make_image(color: str = "black") -> Image.Image:
+ return Image.new("RGB", (32, 32), color=color)
+
+
+def _build_model(provider_id: str, provider_model_id: str) -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key=f"{provider_id}_test",
+ name=f"{provider_id.title()} Test",
+ provider_id=provider_id,
+ provider_model_id=provider_model_id,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_reference_images=True,
+ supports_seed=True,
+ supports_guidance=True,
+ ),
+ )
+
+
+def _build_request(
+ model: ExternalApiModelConfig,
+ mode: str = "txt2img",
+ init_image: Image.Image | None = None,
+ mask_image: Image.Image | None = None,
+ reference_images: list[ExternalReferenceImage] | None = None,
+) -> ExternalGenerationRequest:
+ return ExternalGenerationRequest(
+ model=model,
+ mode=mode, # type: ignore[arg-type]
+ prompt="A test prompt",
+ negative_prompt="",
+ seed=123,
+ num_images=1,
+ width=256,
+ height=256,
+ steps=20,
+ guidance=5.5,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images or [],
+ metadata=None,
+ )
+
+
+def test_gemini_generate_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ api_key = "gemini-key"
+ config = InvokeAIAppConfig(external_gemini_api_key=api_key)
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ init_image = _make_image("blue")
+ ref_image = _make_image("red")
+ request = _build_request(
+ model,
+ init_image=init_image,
+ reference_images=[ExternalReferenceImage(image=ref_image, weight=0.6)],
+ )
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["params"] = params
+ captured["json"] = json
+ captured["timeout"] = timeout
+ return DummyResponse(
+ ok=True,
+ json_data={
+ "candidates": [
+ {"content": {"parts": [{"inlineData": {"data": encoded}}]}},
+ ]
+ },
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert (
+ captured["url"]
+ == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent"
+ )
+ assert captured["params"] == {"key": api_key}
+ payload = captured["json"]
+ assert isinstance(payload, dict)
+ system_instruction = payload.get("systemInstruction")
+ assert isinstance(system_instruction, dict)
+ system_parts = system_instruction.get("parts")
+ assert isinstance(system_parts, list)
+ system_text = str(system_parts[0]).lower()
+ assert "image" in system_text
+ generation_config = payload.get("generationConfig")
+ assert isinstance(generation_config, dict)
+ assert generation_config["candidateCount"] == 1
+ assert generation_config["responseModalities"] == ["IMAGE"]
+ contents = payload.get("contents")
+ assert isinstance(contents, list)
+ first_content = contents[0]
+ assert isinstance(first_content, dict)
+ parts = first_content.get("parts")
+ assert isinstance(parts, list)
+ assert len(parts) >= 3
+ part0 = parts[0]
+ part1 = parts[1]
+ part2 = parts[2]
+ assert isinstance(part0, dict)
+ assert isinstance(part1, dict)
+ assert isinstance(part2, dict)
+ inline0 = part0.get("inlineData")
+ assert isinstance(inline0, dict)
+ assert part1["text"] == request.prompt
+ inline1 = part2.get("inlineData")
+ assert isinstance(inline1, dict)
+ assert inline0["data"] == encode_image_base64(init_image)
+ assert inline1["data"] == encode_image_base64(ref_image)
+ assert result.images[0].seed == request.seed
+ assert result.provider_metadata == {"model": request.model.provider_model_id}
+
+
+def test_gemini_generate_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_gemini_api_key="gemini-key")
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(ok=False, status_code=400, text="bad request")
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ with pytest.raises(ExternalProviderRequestError, match="Gemini request failed"):
+ provider.generate(request)
+
+
+def test_gemini_generate_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_gemini_api_key="gemini-key",
+ external_gemini_base_url="https://proxy.gemini",
+ )
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.gemini/v1beta/models/gemini-2.5-flash-image:generateContent"
+
+
+def test_gemini_generate_keeps_base_url_version(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_gemini_api_key="gemini-key",
+ external_gemini_base_url="https://proxy.gemini/v1",
+ )
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.gemini/v1/models/gemini-2.5-flash-image:generateContent"
+
+
+def test_gemini_generate_strips_models_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_gemini_api_key="gemini-key")
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "models/gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert (
+ captured["url"]
+ == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent"
+ )
+
+
+def test_openai_generate_txt2img_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ api_key = "openai-key"
+ config = InvokeAIAppConfig(external_openai_api_key=api_key)
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("purple"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["headers"] = headers
+ captured["json"] = json
+ response = DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+ response.headers["x-request-id"] = "req-123"
+ return response
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/generations"
+ headers = captured["headers"]
+ assert isinstance(headers, dict)
+ assert headers["Authorization"] == f"Bearer {api_key}"
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ assert json_payload["prompt"] == request.prompt
+ assert result.provider_request_id == "req-123"
+ assert result.images[0].seed == request.seed
+ assert decode_image_base64(encoded).size == result.images[0].image.size
+
+
+def test_openai_generate_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_openai_api_key="openai-key",
+ external_openai_base_url="https://proxy.openai/",
+ )
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("purple"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.openai/v1/images/generations"
+
+
+def test_openai_generate_txt2img_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(ok=False, status_code=500, text="server error")
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ with pytest.raises(ExternalProviderRequestError, match="OpenAI request failed"):
+ provider.generate(request)
+
+
+def test_openai_generate_inpaint_uses_edit_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(
+ model,
+ mode="inpaint",
+ init_image=_make_image("white"),
+ mask_image=_make_image("black"),
+ )
+ encoded = encode_image_base64(_make_image("orange"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["data"] = data
+ captured["files"] = files
+ response = DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+ return response
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/edits"
+ data_payload = captured["data"]
+ assert isinstance(data_payload, dict)
+ assert data_payload["prompt"] == request.prompt
+ files = captured["files"]
+ assert isinstance(files, dict)
+ assert "image" in files
+ assert "mask" in files
+ image_tuple = files["image"]
+ assert isinstance(image_tuple, tuple)
+ assert image_tuple[0] == "image.png"
+ assert isinstance(image_tuple[1], io.BytesIO)
+ assert result.images
diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py
index d19eb95a8c2..c3d5d18e06c 100644
--- a/tests/app/services/model_install/test_model_install.py
+++ b/tests/app/services/model_install/test_model_install.py
@@ -33,6 +33,7 @@
URLModelSource,
)
from invokeai.app.services.model_records import ModelRecordChanges, UnknownModelException
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ModelFormat,
@@ -213,6 +214,21 @@ def test_inplace_install(
assert Path(job.config_out.path).exists()
+def test_external_install(mm2_installer: ModelInstallServiceBase) -> None:
+ config = ModelRecordChanges(name="ChatGPT Image", description="External model", key="chatgpt_image")
+ job = mm2_installer.heuristic_import("external://openai/gpt-image-1", config=config)
+
+ mm2_installer.wait_for_installs()
+
+ assert job.status == InstallStatus.COMPLETED
+ assert job.config_out is not None
+ assert isinstance(job.config_out, ExternalApiModelConfig)
+ assert job.config_out.provider_id == "openai"
+ assert job.config_out.provider_model_id == "gpt-image-1"
+ assert job.config_out.base == BaseModelType.External
+ assert job.config_out.type == ModelType.ExternalImageGenerator
+
+
def test_delete_install(
mm2_installer: ModelInstallServiceBase, embedding_file: Path, mm2_app_config: InvokeAIAppConfig
) -> None:
diff --git a/tests/app/services/model_load/test_load_api.py b/tests/app/services/model_load/test_load_api.py
index c0760cd3cad..8f7f8449723 100644
--- a/tests/app/services/model_load/test_load_api.py
+++ b/tests/app/services/model_load/test_load_api.py
@@ -4,6 +4,7 @@
import torch
from diffusers import AutoencoderTiny
+from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.model_manager import ModelManagerServiceBase
from invokeai.app.services.shared.invocation_context import (
@@ -11,6 +12,7 @@
InvocationContextData,
build_invocation_context,
)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
from invokeai.backend.model_manager.load.load_base import LoadedModelWithoutConfig
from tests.backend.model_manager.model_manager_fixtures import * # noqa F403
@@ -78,6 +80,27 @@ def test_download_and_load(mock_context: InvocationContext) -> None:
assert loaded_model_1.model is loaded_model_2.model # should be cached copy
+def test_external_model_load_raises(
+ mock_context: InvocationContext, mm2_model_manager: ModelManagerServiceBase
+) -> None:
+ config = ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(modes=["txt2img"]),
+ )
+ mm2_model_manager.store.add_model(config)
+
+ model_field = ModelIdentifierField.from_config(config)
+
+ with pytest.raises(ValueError, match="External API models"):
+ mock_context.models.load(model_field)
+
+ with pytest.raises(ValueError, match="External API models"):
+ mock_context.models.load_by_attrs(name=config.name, base=config.base, type=config.type)
+
+
def test_download_diffusers(mock_context: InvocationContext) -> None:
model_path = mock_context.models.download_and_cache_model("stabilityai/sdxl-turbo")
assert (model_path / "model_index.json").exists()
diff --git a/tests/backend/model_manager/test_external_api_config.py b/tests/backend/model_manager/test_external_api_config.py
new file mode 100644
index 00000000000..943a5a79918
--- /dev/null
+++ b/tests/backend/model_manager/test_external_api_config.py
@@ -0,0 +1,54 @@
+import pytest
+from pydantic import ValidationError
+
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
+
+
+def test_external_api_model_config_defaults() -> None:
+ capabilities = ExternalModelCapabilities(modes=["txt2img"], supports_seed=True)
+
+ config = ExternalApiModelConfig(
+ name="Test External",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=capabilities,
+ )
+
+ assert config.path == "external://openai/gpt-image-1"
+ assert config.source == "external://openai/gpt-image-1"
+ assert config.hash == "external:openai:gpt-image-1"
+ assert config.file_size == 0
+ assert config.default_settings is None
+ assert config.capabilities.supports_seed is True
+
+
+def test_external_api_model_capabilities_allows_aspect_ratio_sizes() -> None:
+ capabilities = ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1"],
+ aspect_ratio_sizes={"1:1": ExternalImageSize(width=1024, height=1024)},
+ )
+
+ assert capabilities.aspect_ratio_sizes is not None
+ assert capabilities.aspect_ratio_sizes["1:1"].width == 1024
+
+
+def test_external_api_model_config_rejects_extra_fields() -> None:
+ with pytest.raises(ValidationError):
+ ExternalModelCapabilities(modes=["txt2img"], supports_seed=True, extra_field=True) # type: ignore
+
+ with pytest.raises(ValidationError):
+ ExternalApiModelDefaultSettings(width=512, extra_field=True) # type: ignore
+
+
+def test_external_api_model_config_validates_limits() -> None:
+ with pytest.raises(ValidationError):
+ ExternalModelCapabilities(modes=["txt2img"], max_images_per_request=0)
+
+ with pytest.raises(ValidationError):
+ ExternalApiModelDefaultSettings(width=0)
diff --git a/tests/conftest.py b/tests/conftest.py
index d2835120e9e..594b6ba7b4d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -14,6 +14,7 @@
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
from invokeai.app.services.images.images_default import ImageService
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_services import InvocationServices
@@ -48,6 +49,7 @@ def mock_services() -> InvocationServices:
model_images=None, # type: ignore
model_manager=None, # type: ignore
download_queue=None, # type: ignore
+ external_generation=ExternalGenerationService({}, logger),
names=None, # type: ignore
performance_statistics=InvocationStatsService(),
session_processor=None, # type: ignore
From 207ba076eca98ab417e8a8a5c457f27e2fe80214 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:13:50 +0100
Subject: [PATCH 02/50] feat: support reference images for external models
---
.../external_generation_default.py | 35 ++++++++++-
.../external_generation/providers/openai.py | 31 ++++++---
.../backend/model_manager/starter_models.py | 2 +-
.../components/RefImage/RefImagePreview.tsx | 12 ++--
.../components/RefImage/RefImageSettings.tsx | 13 ++--
.../controlLayers/store/validators.ts | 10 ++-
.../test_external_generation_service.py | 23 +++++++
.../test_external_provider_adapters.py | 63 +++++++++++++++++--
8 files changed, 160 insertions(+), 29 deletions(-)
diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py
index c72e16cde8d..c96b5af711e 100644
--- a/invokeai/app/services/external_generation/external_generation_default.py
+++ b/invokeai/app/services/external_generation/external_generation_default.py
@@ -15,6 +15,7 @@
ExternalProvider,
)
from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
ExternalGenerationRequest,
ExternalGenerationResult,
ExternalProviderStatus,
@@ -37,10 +38,17 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
raise ExternalProviderNotConfiguredError(f"Provider '{request.model.provider_id}' is missing credentials")
request = self._refresh_model_capabilities(request)
+ resize_to_original_inpaint_size = _get_resize_target_for_inpaint(request)
request = self._bucket_request(request)
self._validate_request(request)
- return provider.generate(request)
+ result = provider.generate(request)
+
+ if resize_to_original_inpaint_size is None:
+ return result
+
+ width, height = resize_to_original_inpaint_size
+ return _resize_result_images(result, width, height)
def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()}
@@ -276,6 +284,31 @@ def _resize_image(image: PILImageType | None, width: int, height: int, mode: str
return image.convert(mode).resize((width, height), Image.Resampling.LANCZOS)
+def _get_resize_target_for_inpaint(request: ExternalGenerationRequest) -> tuple[int, int] | None:
+ if request.mode != "inpaint" or request.init_image is None:
+ return None
+ return request.init_image.width, request.init_image.height
+
+
+def _resize_result_images(result: ExternalGenerationResult, width: int, height: int) -> ExternalGenerationResult:
+ resized_images = [
+ ExternalGeneratedImage(
+ image=generated.image
+ if generated.image.width == width and generated.image.height == height
+ else generated.image.resize((width, height), Image.Resampling.LANCZOS),
+ seed=generated.seed,
+ )
+ for generated in result.images
+ ]
+ return ExternalGenerationResult(
+ images=resized_images,
+ seed_used=result.seed_used,
+ provider_request_id=result.provider_request_id,
+ provider_metadata=result.provider_metadata,
+ content_filters=result.content_filters,
+ )
+
+
def _apply_starter_overrides(model: ExternalApiModelConfig) -> ExternalApiModelConfig:
source = model.source or f"external://{model.provider_id}/{model.provider_model_id}"
starter_match = next((starter for starter in STARTER_MODELS if starter.source == source), None)
diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py
index e31a493b7a1..f06491a225b 100644
--- a/invokeai/app/services/external_generation/providers/openai.py
+++ b/invokeai/app/services/external_generation/providers/openai.py
@@ -3,6 +3,7 @@
import io
import requests
+from PIL.Image import Image as PILImageType
from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
@@ -29,7 +30,9 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/")
headers = {"Authorization": f"Bearer {api_key}"}
- if request.mode == "txt2img":
+ use_edits_endpoint = request.mode != "txt2img" or bool(request.reference_images)
+
+ if not use_edits_endpoint:
payload: dict[str, object] = {
"prompt": request.prompt,
"n": request.num_images,
@@ -45,20 +48,28 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
timeout=120,
)
else:
- files: dict[str, tuple[str, io.BytesIO, str]] = {}
- if request.init_image is None:
- raise ExternalProviderRequestError("OpenAI img2img/inpaint requires an init image")
-
- image_buffer = io.BytesIO()
- request.init_image.save(image_buffer, format="PNG")
- image_buffer.seek(0)
- files["image"] = ("image.png", image_buffer, "image/png")
+ images: list[PILImageType] = []
+ if request.init_image is not None:
+ images.append(request.init_image)
+ images.extend(reference.image for reference in request.reference_images)
+ if not images:
+ raise ExternalProviderRequestError(
+ "OpenAI image edits require at least one image (init image or reference image)"
+ )
+
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]] = []
+ image_field_name = "image" if len(images) == 1 else "image[]"
+ for index, image in enumerate(images):
+ image_buffer = io.BytesIO()
+ image.save(image_buffer, format="PNG")
+ image_buffer.seek(0)
+ files.append((image_field_name, (f"image_{index}.png", image_buffer, "image/png")))
if request.mask_image is not None:
mask_buffer = io.BytesIO()
request.mask_image.save(mask_buffer, format="PNG")
mask_buffer.seek(0)
- files["mask"] = ("mask.png", mask_buffer, "image/png")
+ files.append(("mask", ("mask.png", mask_buffer, "image/png")))
data: dict[str, object] = {
"prompt": request.prompt,
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index 59d7ceba205..183edc04ba7 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -964,7 +964,7 @@ class StarterModelBundle(BaseModel):
supports_negative_prompt=True,
supports_seed=True,
supports_guidance=True,
- supports_reference_images=False,
+ supports_reference_images=True,
max_images_per_request=1,
),
default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
index 84c1b2fc37b..ddbdb8b131c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
@@ -15,6 +15,7 @@ import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/va
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { PiExclamationMarkBold, PiEyeSlashBold, PiImageBold } from 'react-icons/pi';
import { useImageDTOFromCroppableImage } from 'services/api/endpoints/images';
+import { isExternalApiModelConfig } from 'services/api/types';
import { RefImageWarningTooltipContent } from './RefImageWarningTooltipContent';
@@ -71,18 +72,19 @@ export const RefImagePreview = memo(() => {
const selectedEntityId = useAppSelector(selectSelectedRefEntityId);
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
+ const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
const imageDTO = useImageDTOFromCroppableImage(entity.config.image);
const sx = useMemo(() => {
- if (!isIPAdapterConfig(entity.config)) {
+ if (!isIPAdapterConfig(entity.config) || isExternalModel) {
return baseSx;
}
return getImageSxWithWeight(entity.config.weight);
- }, [entity.config]);
+ }, [entity.config, isExternalModel]);
useEffect(() => {
- if (!isIPAdapterConfig(entity.config)) {
+ if (!isIPAdapterConfig(entity.config) || isExternalModel) {
return;
}
setShowWeightDisplay(true);
@@ -92,7 +94,7 @@ export const RefImagePreview = memo(() => {
return () => {
window.clearTimeout(timeout);
};
- }, [entity.config]);
+ }, [entity.config, isExternalModel]);
const warnings = useMemo(() => {
return getGlobalReferenceImageWarnings(entity, mainModelConfig);
@@ -154,7 +156,7 @@ export const RefImagePreview = memo(() => {
) : (
)}
- {isIPAdapterConfig(entity.config) && (
+ {isIPAdapterConfig(entity.config) && !isExternalModel && (
{
const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
const config = useAppSelector(selectConfig);
const tab = useAppSelector(selectActiveTab);
+ const mainModelConfig = useAppSelector(selectMainModelConfig);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
@@ -120,9 +122,10 @@ const RefImageSettingsContent = memo(() => {
);
const isFLUX = useAppSelector(selectIsFLUX);
+ const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
- // FLUX.2 Klein has built-in reference image support - no model selector needed
- const showModelSelector = !isFlux2ReferenceImageConfig(config);
+ // FLUX.2 Klein and external API models do not require a ref image model selection.
+ const showModelSelector = !isFlux2ReferenceImageConfig(config) && !isExternalModel;
return (
@@ -150,14 +153,14 @@ const RefImageSettingsContent = memo(() => {
)}
- {isIPAdapterConfig(config) && (
+ {isIPAdapterConfig(config) && !isExternalModel && (
{!isFLUX && }
)}
- {isFLUXReduxConfig(config) && (
+ {isFLUXReduxConfig(config) && !isExternalModel && (
None:
assert response is result
assert provider.last_request == request
+
+
+def test_generate_resizes_inpaint_result_to_original_init_size() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["inpaint"]))
+ request = _build_request(
+ model=model,
+ mode="inpaint",
+ width=128,
+ height=128,
+ init_image=_make_image(),
+ mask_image=_make_image(),
+ )
+ generated_large = Image.new("RGB", (128, 128), color="black")
+ result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=generated_large, seed=1)])
+ provider = DummyProvider("openai", configured=True, result=result)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert request.init_image is not None
+ assert response.images[0].image.width == request.init_image.width
+ assert response.images[0].image.height == request.init_image.height
+ assert response.images[0].seed == 1
diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py
index 38f4c9e3d52..c4da4c913be 100644
--- a/tests/app/services/external_generation/test_external_provider_adapters.py
+++ b/tests/app/services/external_generation/test_external_provider_adapters.py
@@ -320,7 +320,13 @@ def test_openai_generate_inpaint_uses_edit_endpoint(monkeypatch: pytest.MonkeyPa
encoded = encode_image_base64(_make_image("orange"))
captured: dict[str, object] = {}
- def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) -> DummyResponse:
+ def fake_post(
+ url: str,
+ headers: dict,
+ data: dict,
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]],
+ timeout: int,
+ ) -> DummyResponse:
captured["url"] = url
captured["data"] = data
captured["files"] = files
@@ -336,11 +342,56 @@ def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) ->
assert isinstance(data_payload, dict)
assert data_payload["prompt"] == request.prompt
files = captured["files"]
- assert isinstance(files, dict)
- assert "image" in files
- assert "mask" in files
- image_tuple = files["image"]
+ assert isinstance(files, list)
+ image_file = next((file for file in files if file[0] == "image"), None)
+ mask_file = next((file for file in files if file[0] == "mask"), None)
+ assert image_file is not None
+ assert mask_file is not None
+ image_tuple = image_file[1]
assert isinstance(image_tuple, tuple)
- assert image_tuple[0] == "image.png"
+ assert image_tuple[0] == "image_0.png"
assert isinstance(image_tuple[1], io.BytesIO)
assert result.images
+
+
+def test_openai_generate_txt2img_with_references_uses_edit_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(
+ model,
+ reference_images=[
+ ExternalReferenceImage(image=_make_image("red")),
+ ExternalReferenceImage(image=_make_image("blue")),
+ ],
+ )
+ encoded = encode_image_base64(_make_image("orange"))
+ captured: dict[str, object] = {}
+
+ def fake_post(
+ url: str,
+ headers: dict,
+ data: dict,
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]],
+ timeout: int,
+ ) -> DummyResponse:
+ captured["url"] = url
+ captured["data"] = data
+ captured["files"] = files
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/edits"
+ data_payload = captured["data"]
+ assert isinstance(data_payload, dict)
+ assert data_payload["prompt"] == request.prompt
+ files = captured["files"]
+ assert isinstance(files, list)
+ image_files = [file for file in files if file[0] == "image[]"]
+ assert len(image_files) == 2
+ assert image_files[0][1][0] == "image_0.png"
+ assert image_files[1][1][0] == "image_1.png"
+ assert result.images
From 7671304195518fe010b6e9b080f7a30b752015ca Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:23:16 +0100
Subject: [PATCH 03/50] fix: sorting lint error
---
.../features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index a91655168ca..795c4c68862 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -10,10 +10,10 @@ import { filesize } from 'filesize';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
- isExternalApiModelConfig,
type AnyModelConfig,
type CLIPEmbedModelConfig,
type CLIPVisionModelConfig,
+ isExternalApiModelConfig,
type LlavaOnevisionModelConfig,
type Qwen3EncoderModelConfig,
type SigLIPModelConfig,
From 3c8369224b70e208a33ecfc526009bd5298964ac Mon Sep 17 00:00:00 2001
From: Alexander Eichhorn
Date: Sat, 21 Feb 2026 19:16:42 +0100
Subject: [PATCH 04/50] feat: add Seedream provider and capability-driven
settings visibility
Add BytePlus Seedream as external image generation provider with four
models (seedream-4.5, seedream-4.0, seedream-3.0-t2i) including debug
dump support and batch generation.
Hide irrelevant canvas settings for external models by using
ExternalModelCapabilities as the source of truth for UI rendering.
Scheduler, LoRA, CFG Scale, and all Advanced settings (VAE, CLIP Skip,
Seamless, etc.) are now hidden for external models. Steps, Guidance,
and Seed controls are only shown when the model declares support via
its capability flags. Adds supports_steps capability field and gates
the graph builder accordingly.
---
invokeai/app/api/dependencies.py | 3 +-
invokeai/app/api/routers/app_info.py | 1 +
.../app/services/config/config_default.py | 6 +
.../external_generation/providers/__init__.py | 3 +-
.../external_generation/providers/seedream.py | 138 ++++++++
.../model_manager/configs/external_api.py | 1 +
.../backend/model_manager/starter_models.py | 116 +++++++
.../controlLayers/store/paramsSlice.ts | 28 ++
.../ExternalProvidersForm.tsx | 2 +-
.../graph/generation/buildExternalGraph.ts | 2 +-
.../AdvancedSettingsAccordion.tsx | 6 +
.../GenerationSettingsAccordion.tsx | 54 ++--
.../CanvasTabImageSettingsAccordion.tsx | 4 +-
.../GenerateTabImageSettingsAccordion.tsx | 4 +-
.../ParametersPanelCanvas.tsx | 7 +-
.../ParametersPanelGenerate.tsx | 5 +-
.../frontend/web/src/services/api/schema.ts | 27 +-
.../frontend/web/src/services/api/types.ts | 1 +
.../test_seedream_provider.py | 305 ++++++++++++++++++
19 files changed, 680 insertions(+), 33 deletions(-)
create mode 100644 invokeai/app/services/external_generation/providers/seedream.py
create mode 100644 tests/app/services/external_generation/test_seedream_provider.py
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index 79dde681d32..e70d62d5987 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -14,7 +14,7 @@
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.download.download_default import DownloadQueueService
from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
-from invokeai.app.services.external_generation.providers import GeminiProvider, OpenAIProvider
+from invokeai.app.services.external_generation.providers import GeminiProvider, OpenAIProvider, SeedreamProvider
from invokeai.app.services.events.events_fastapievents import FastAPIEventService
from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
@@ -142,6 +142,7 @@ def initialize(
providers={
GeminiProvider.provider_id: GeminiProvider(app_config=configuration, logger=logger),
OpenAIProvider.provider_id: OpenAIProvider(app_config=configuration, logger=logger),
+ SeedreamProvider.provider_id: SeedreamProvider(app_config=configuration, logger=logger),
},
logger=logger,
)
diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py
index cbb3e6fb47d..02e1dc8f068 100644
--- a/invokeai/app/api/routers/app_info.py
+++ b/invokeai/app/api/routers/app_info.py
@@ -90,6 +90,7 @@ class ExternalProviderConfigModel(BaseModel):
EXTERNAL_PROVIDER_FIELDS: dict[str, tuple[str, str]] = {
"gemini": ("external_gemini_api_key", "external_gemini_base_url"),
"openai": ("external_openai_api_key", "external_openai_base_url"),
+ "seedream": ("external_seedream_api_key", "external_seedream_base_url"),
}
diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py
index 0bb29a5dfa6..9da953c6f6c 100644
--- a/invokeai/app/services/config/config_default.py
+++ b/invokeai/app/services/config/config_default.py
@@ -212,6 +212,12 @@ class InvokeAIAppConfig(BaseSettings):
external_openai_base_url: Optional[str] = Field(
default=None, description="Base URL override for OpenAI image generation."
)
+ external_seedream_api_key: Optional[str] = Field(
+ default=None, description="API key for Seedream image generation."
+ )
+ external_seedream_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Seedream image generation."
+ )
# fmt: on
diff --git a/invokeai/app/services/external_generation/providers/__init__.py b/invokeai/app/services/external_generation/providers/__init__.py
index 9e380fca1e1..ea901d7e123 100644
--- a/invokeai/app/services/external_generation/providers/__init__.py
+++ b/invokeai/app/services/external_generation/providers/__init__.py
@@ -1,4 +1,5 @@
from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+from invokeai.app.services.external_generation.providers.seedream import SeedreamProvider
-__all__ = ["GeminiProvider", "OpenAIProvider"]
+__all__ = ["GeminiProvider", "OpenAIProvider", "SeedreamProvider"]
diff --git a/invokeai/app/services/external_generation/providers/seedream.py b/invokeai/app/services/external_generation/providers/seedream.py
new file mode 100644
index 00000000000..5875c6baeb9
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/seedream.py
@@ -0,0 +1,138 @@
+from __future__ import annotations
+
+import json
+import uuid
+
+import requests
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+_SEEDREAM_4X_PREFIXES = ("seedream-4.5", "seedream-4.0", "seedream-4-5", "seedream-4-0")
+
+
+class SeedreamProvider(ExternalProvider):
+ provider_id = "seedream"
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_seedream_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_seedream_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Seedream API key is not configured")
+
+ base_url = (self._app_config.external_seedream_base_url or "https://ark.ap-southeast.bytepluses.com").rstrip("/")
+ endpoint = f"{base_url}/api/v3/images/generations"
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ model_id = request.model.provider_model_id
+ is_4x = any(model_id.startswith(prefix) for prefix in _SEEDREAM_4X_PREFIXES)
+
+ payload: dict[str, object] = {
+ "model": model_id,
+ "prompt": request.prompt,
+ "size": f"{request.width}x{request.height}",
+ "response_format": "b64_json",
+ "watermark": False,
+ }
+
+ # Seed is only supported on 3.0 models
+ if not is_4x and request.seed is not None and request.seed >= 0:
+ payload["seed"] = request.seed
+
+ # guidance_scale is only supported on 3.0 models
+ if not is_4x and request.guidance is not None:
+ payload["guidance_scale"] = request.guidance
+
+ # Batch generation for 4.x models
+ if is_4x:
+ if request.num_images > 1:
+ payload["sequential_image_generation"] = "auto"
+ payload["sequential_image_generation_options"] = {"max_images": request.num_images}
+ else:
+ payload["sequential_image_generation"] = "disabled"
+
+ # Image input: init_image for img2img, reference images for 4.x
+ images_b64: list[str] = []
+ if request.init_image is not None:
+ images_b64.append(f"data:image/png;base64,{encode_image_base64(request.init_image)}")
+ for reference in request.reference_images:
+ images_b64.append(f"data:image/png;base64,{encode_image_base64(reference.image)}")
+
+ if images_b64:
+ payload["image"] = images_b64 if len(images_b64) > 1 else images_b64[0]
+
+ self._dump_debug_payload("request", payload)
+
+ response = requests.post(endpoint, headers=headers, json=payload, timeout=120)
+
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"Seedream request failed with status {response.status_code}: {response.text}"
+ )
+
+ body = response.json()
+ self._dump_debug_payload("response", body)
+ if not isinstance(body, dict):
+ raise ExternalProviderRequestError("Seedream response payload was not a JSON object")
+
+ generated_images: list[ExternalGeneratedImage] = []
+ data_items = body.get("data")
+ if not isinstance(data_items, list):
+ raise ExternalProviderRequestError("Seedream response payload missing image data")
+
+ for item in data_items:
+ if not isinstance(item, dict):
+ continue
+ # Items may be error objects for failed images in batch
+ if "error" in item:
+ continue
+ encoded = item.get("b64_json")
+ if not encoded:
+ continue
+ image = decode_image_base64(encoded)
+ self._dump_debug_image(image)
+ generated_images.append(ExternalGeneratedImage(image=image, seed=request.seed))
+
+ if not generated_images:
+ raise ExternalProviderRequestError("Seedream response contained no images")
+
+ return ExternalGenerationResult(
+ images=generated_images,
+ seed_used=request.seed,
+ provider_metadata={"model": model_id},
+ )
+
+ def _dump_debug_payload(self, label: str, payload: object) -> None:
+ """TODO: remove debug payload dump once Seedream is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "seedream"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"{label}_{uuid.uuid4().hex}.json"
+ path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
+ except Exception as exc:
+ self._logger.debug("Failed to write Seedream debug payload: %s", exc)
+
+ def _dump_debug_image(self, image: PILImageType) -> None:
+ """TODO: remove debug image dump once Seedream is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "seedream"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"decoded_{uuid.uuid4().hex}.png"
+ image.save(path, format="PNG")
+ except Exception as exc:
+ self._logger.debug("Failed to write Seedream debug image: %s", exc)
diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py
index f57b4404e00..7e52b162bd8 100644
--- a/invokeai/backend/model_manager/configs/external_api.py
+++ b/invokeai/backend/model_manager/configs/external_api.py
@@ -24,6 +24,7 @@ class ExternalModelCapabilities(BaseModel):
supports_negative_prompt: bool = Field(default=True)
supports_seed: bool = Field(default=False)
supports_guidance: bool = Field(default=False)
+ supports_steps: bool = Field(default=False)
max_images_per_request: int | None = Field(default=None, gt=0)
max_image_size: ExternalImageSize | None = Field(default=None)
allowed_aspect_ratios: list[str] | None = Field(default=None)
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index 183edc04ba7..c563348d3a0 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -969,6 +969,119 @@ class StarterModelBundle(BaseModel):
),
default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
)
+seedream_4_5 = StarterModel(
+ name="Seedream 4.5",
+ base=BaseModelType.External,
+ source="external://seedream/seedream-4-5-251128",
+ description="BytePlus Seedream 4.5 image generation model (external API). Supports txt2img, img2img, batch generation, and multi-image reference input.",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img"],
+ supports_negative_prompt=False,
+ supports_seed=False,
+ supports_guidance=False,
+ supports_reference_images=True,
+ max_reference_images=14,
+ max_images_per_request=15,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=2048, height=2048),
+ "2:3": ExternalImageSize(width=1664, height=2496),
+ "3:2": ExternalImageSize(width=2496, height=1664),
+ "3:4": ExternalImageSize(width=1728, height=2304),
+ "4:3": ExternalImageSize(width=2304, height=1728),
+ "9:16": ExternalImageSize(width=1440, height=2560),
+ "16:9": ExternalImageSize(width=2560, height=1440),
+ "21:9": ExternalImageSize(width=3024, height=1296),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1),
+)
+seedream_4_0 = StarterModel(
+ name="Seedream 4.0",
+ base=BaseModelType.External,
+ source="external://seedream/seedream-4-0-250828",
+ description="BytePlus Seedream 4.0 image generation model (external API). Supports txt2img, img2img, batch generation, and multi-image reference input.",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img"],
+ supports_negative_prompt=False,
+ supports_seed=False,
+ supports_guidance=False,
+ supports_reference_images=True,
+ max_reference_images=14,
+ max_images_per_request=15,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=2048, height=2048),
+ "2:3": ExternalImageSize(width=1664, height=2496),
+ "3:2": ExternalImageSize(width=2496, height=1664),
+ "3:4": ExternalImageSize(width=1728, height=2304),
+ "4:3": ExternalImageSize(width=2304, height=1728),
+ "9:16": ExternalImageSize(width=1440, height=2560),
+ "16:9": ExternalImageSize(width=2560, height=1440),
+ "21:9": ExternalImageSize(width=3024, height=1296),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1),
+)
+seedream_3_0_t2i = StarterModel(
+ name="Seedream 3.0 T2I",
+ base=BaseModelType.External,
+ source="external://seedream/seedream-3-0-t2i-250415",
+ description="BytePlus Seedream 3.0 text-to-image model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img"],
+ supports_negative_prompt=False,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=False,
+ max_images_per_request=1,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "2:3": ExternalImageSize(width=832, height=1248),
+ "3:2": ExternalImageSize(width=1248, height=832),
+ "3:4": ExternalImageSize(width=864, height=1152),
+ "4:3": ExternalImageSize(width=1152, height=864),
+ "9:16": ExternalImageSize(width=720, height=1280),
+ "16:9": ExternalImageSize(width=1280, height=720),
+ "21:9": ExternalImageSize(width=1512, height=648),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
# endregion
# List of starter models, displayed on the frontend.
@@ -1069,6 +1182,9 @@ class StarterModelBundle(BaseModel):
gemini_flash_image,
gemini_pro_image_preview,
openai_gpt_image_1,
+ seedream_4_5,
+ seedream_4_0,
+ seedream_3_0_t2i,
]
sd1_bundle: list[StarterModel] = [
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index f54927e5479..28d0f9d5880 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -587,6 +587,7 @@ export const selectIsSD3 = createParamsSelector((params) => params.model?.base =
export const selectIsCogView4 = createParamsSelector((params) => params.model?.base === 'cogview4');
export const selectIsZImage = createParamsSelector((params) => params.model?.base === 'z-image');
export const selectIsFlux2 = createParamsSelector((params) => params.model?.base === 'flux2');
+export const selectIsExternal = createParamsSelector((params) => params.model?.base === 'external');
export const selectIsFluxKontext = createParamsSelector((params) => {
if (params.model?.base === 'flux' && params.model?.name.toLowerCase().includes('kontext')) {
return true;
@@ -671,6 +672,33 @@ export const selectModelSupportsOptimizedDenoising = createSelector(
selectModel,
(model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base)
);
+export const selectModelSupportsGuidance = createSelector(selectModel, selectModelConfig, (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_guidance ?? false;
+ }
+ return true;
+});
+export const selectModelSupportsSeed = createSelector(selectModel, selectModelConfig, (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_seed ?? false;
+ }
+ return true;
+});
+export const selectModelSupportsSteps = createSelector(selectModel, selectModelConfig, (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_steps ?? false;
+ }
+ return true;
+});
export const selectScheduler = createParamsSelector((params) => params.scheduler);
export const selectFluxScheduler = createParamsSelector((params) => params.fluxScheduler);
export const selectFluxDypePreset = createParamsSelector((params) => params.fluxDypePreset);
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
index 26820cb0e29..6a722ecf2db 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
@@ -29,7 +29,7 @@ import {
import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
import type { ExternalProviderConfig, StarterModel } from 'services/api/types';
-const PROVIDER_SORT_ORDER = ['gemini', 'openai'];
+const PROVIDER_SORT_ORDER = ['gemini', 'openai', 'seedream'];
type ProviderCardProps = {
provider: ExternalProviderConfig;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
index 02c030aa3b8..1724774465f 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
@@ -54,7 +54,7 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise {
const isFlux2 = useAppSelector(selectIsFlux2);
const isSD3 = useAppSelector(selectIsSD3);
const isZImage = useAppSelector(selectIsZImage);
+ const isExternal = useAppSelector(selectIsExternal);
const selectBadges = useMemo(
() =>
@@ -91,6 +93,10 @@ export const AdvancedSettingsAccordion = memo(() => {
defaultIsOpen: false,
});
+ if (isExternal) {
+ return null;
+ }
+
return (
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index ffdbc4ce778..1d291147c7c 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -7,10 +7,13 @@ import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
import {
selectFluxDypePreset,
selectIsCogView4,
+ selectIsExternal,
selectIsFLUX,
selectIsFlux2,
selectIsSD3,
selectIsZImage,
+ selectModelSupportsGuidance,
+ selectModelSupportsSteps,
} from 'features/controlLayers/store/paramsSlice';
import { LoRAList } from 'features/lora/components/LoRAList';
import LoRASelect from 'features/lora/components/LoRASelect';
@@ -44,7 +47,13 @@ export const GenerationSettingsAccordion = memo(() => {
const isSD3 = useAppSelector(selectIsSD3);
const isCogView4 = useAppSelector(selectIsCogView4);
const isZImage = useAppSelector(selectIsZImage);
+ const isExternal = useAppSelector(selectIsExternal);
const fluxDypePreset = useAppSelector(selectFluxDypePreset);
+ const modelSupportsGuidance = useAppSelector(selectModelSupportsGuidance);
+ const modelSupportsSteps = useAppSelector(selectModelSupportsSteps);
+
+ // Determine if the expander has any content to show
+ const hasExpanderContent = isExternal ? modelSupportsGuidance || modelSupportsSteps : true;
const selectBadges = useMemo(
() =>
@@ -69,32 +78,37 @@ export const GenerationSettingsAccordion = memo(() => {
return (
-
+
-
-
+ {!isExternal && }
+ {!isExternal && }
-
-
-
- {!isFLUX && !isFlux2 && !isSD3 && !isCogView4 && !isZImage && }
- {isFLUX && }
- {isZImage && }
-
- {(isFLUX || isFlux2) && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && }
- {!isFLUX && !isFlux2 && }
- {isFLUX && }
- {isFLUX && fluxDypePreset === 'manual' && }
- {isFLUX && fluxDypePreset === 'manual' && }
-
- {isZImage && }
-
-
+ {hasExpanderContent && (
+
+
+
+ {!isExternal && !isFLUX && !isFlux2 && !isSD3 && !isCogView4 && !isZImage && }
+ {!isExternal && isFLUX && }
+ {!isExternal && isZImage && }
+ {modelSupportsSteps && }
+ {isExternal && modelSupportsGuidance && }
+ {!isExternal && (isFLUX || isFlux2) && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && (
+
+ )}
+ {!isExternal && !isFLUX && !isFlux2 && }
+ {!isExternal && isFLUX && }
+ {!isExternal && isFLUX && fluxDypePreset === 'manual' && }
+ {!isExternal && isFLUX && fluxDypePreset === 'manual' && }
+
+ {!isExternal && isZImage && }
+
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/CanvasTabImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/CanvasTabImageSettingsAccordion.tsx
index 0fb69173de8..fef0c7f848c 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/CanvasTabImageSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/CanvasTabImageSettingsAccordion.tsx
@@ -5,6 +5,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectModelSupportsOptimizedDenoising,
+ selectModelSupportsSeed,
selectShouldRandomizeSeed,
} from 'features/controlLayers/store/paramsSlice';
import { selectBbox, selectScaleMethod } from 'features/controlLayers/store/selectors';
@@ -61,6 +62,7 @@ export const CanvasTabImageSettingsAccordion = memo(() => {
defaultIsOpen: false,
});
const modelSupportsOptimizedDenoising = useAppSelector(selectModelSupportsOptimizedDenoising);
+ const modelSupportsSeed = useAppSelector(selectModelSupportsSeed);
return (
{
>
-
+ {modelSupportsSeed && }
{modelSupportsOptimizedDenoising && }
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion.tsx
index ad5cd0d2194..73a65b0abd0 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion.tsx
@@ -6,6 +6,7 @@ import {
selectAspectRatioID,
selectAspectRatioIsLocked,
selectHeight,
+ selectModelSupportsSeed,
selectShouldRandomizeSeed,
selectWidth,
} from 'features/controlLayers/store/paramsSlice';
@@ -43,6 +44,7 @@ const selectBadges = createMemoizedSelector(
export const GenerateTabImageSettingsAccordion = memo(() => {
const { t } = useTranslation();
const badges = useAppSelector(selectBadges);
+ const modelSupportsSeed = useAppSelector(selectModelSupportsSeed);
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
id: 'image-settings-generate-tab',
defaultIsOpen: true,
@@ -57,7 +59,7 @@ export const GenerateTabImageSettingsAccordion = memo(() => {
>
-
+ {modelSupportsSeed && }
);
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
index ddd75d91cf6..38f99590ecb 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx
@@ -2,7 +2,7 @@ import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
-import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
+import { selectIsCogView4, selectIsExternal, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
@@ -24,6 +24,7 @@ const overlayScrollbarsStyles: CSSProperties = {
export const ParametersPanelCanvas = memo(() => {
const isSDXL = useAppSelector(selectIsSDXL);
const isCogview4 = useAppSelector(selectIsCogView4);
+ const isExternal = useAppSelector(selectIsExternal);
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
return (
@@ -43,9 +44,9 @@ export const ParametersPanelCanvas = memo(() => {
-
+ {!isExternal && }
{isSDXL && }
- {!isCogview4 && }
+ {!isCogview4 && !isExternal && }
diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
index f955f6793ca..5d1ea8193b1 100644
--- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelGenerate.tsx
@@ -2,7 +2,7 @@ import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
-import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
+import { selectIsCogView4, selectIsExternal, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
@@ -23,6 +23,7 @@ const overlayScrollbarsStyles: CSSProperties = {
export const ParametersPanelGenerate = memo(() => {
const isSDXL = useAppSelector(selectIsSDXL);
const isCogview4 = useAppSelector(selectIsCogView4);
+ const isExternal = useAppSelector(selectIsExternal);
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
return (
@@ -43,7 +44,7 @@ export const ParametersPanelGenerate = memo(() => {
{isSDXL && }
- {!isCogview4 && }
+ {!isCogview4 && !isExternal && }
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 77816f68915..66b7edcd27e 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -7724,11 +7724,20 @@ export type components = {
* @default false
*/
supports_guidance?: boolean;
+ /**
+ * Supports Steps
+ * @default false
+ */
+ supports_steps?: boolean;
/** Max Images Per Request */
max_images_per_request?: number | null;
max_image_size?: components["schemas"]["ExternalImageSize"] | null;
/** Allowed Aspect Ratios */
allowed_aspect_ratios?: string[] | null;
+ /** Aspect Ratio Sizes */
+ aspect_ratio_sizes?: {
+ [key: string]: components["schemas"]["ExternalImageSize"];
+ } | null;
/** Max Reference Images */
max_reference_images?: number | null;
/**
@@ -14203,14 +14212,14 @@ export type components = {
* Convert Cache Dir
* Format: path
* @description Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).
- * @default models/.convert_cache
+ * @default models\.convert_cache
*/
convert_cache_dir?: string;
/**
* Download Cache Dir
* Format: path
* @description Path to the directory that contains dynamically downloaded models.
- * @default models/.download_cache
+ * @default models\.download_cache
*/
download_cache_dir?: string;
/**
@@ -14504,6 +14513,16 @@ export type components = {
* @description Base URL override for OpenAI image generation.
*/
external_openai_base_url?: string | null;
+ /**
+ * External Seedream Api Key
+ * @description API key for Seedream image generation.
+ */
+ external_seedream_api_key?: string | null;
+ /**
+ * External Seedream Base Url
+ * @description Base URL override for Seedream image generation.
+ */
+ external_seedream_base_url?: string | null;
};
/**
* InvokeAIAppConfigWithSetFields
@@ -24295,6 +24314,8 @@ export type components = {
* @default false
*/
is_installed?: boolean;
+ capabilities?: components["schemas"]["ExternalModelCapabilities"] | null;
+ default_settings?: components["schemas"]["ExternalApiModelDefaultSettings"] | null;
/**
* Previous Names
* @default []
@@ -24335,6 +24356,8 @@ export type components = {
* @default false
*/
is_installed?: boolean;
+ capabilities?: components["schemas"]["ExternalModelCapabilities"] | null;
+ default_settings?: components["schemas"]["ExternalApiModelDefaultSettings"] | null;
/**
* Previous Names
* @default []
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 0c06e04dcd6..2a97a9407d8 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -124,6 +124,7 @@ export type ExternalModelCapabilities = {
supports_negative_prompt?: boolean;
supports_seed?: boolean;
supports_guidance?: boolean;
+ supports_steps?: boolean;
max_images_per_request?: number | null;
max_image_size?: ExternalImageSize | null;
allowed_aspect_ratios?: string[] | null;
diff --git a/tests/app/services/external_generation/test_seedream_provider.py b/tests/app/services/external_generation/test_seedream_provider.py
new file mode 100644
index 00000000000..a6583c03cda
--- /dev/null
+++ b/tests/app/services/external_generation/test_seedream_provider.py
@@ -0,0 +1,305 @@
+import logging
+
+import pytest
+from PIL import Image
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.image_utils import encode_image_base64
+from invokeai.app.services.external_generation.providers.seedream import SeedreamProvider
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+
+
+class DummyResponse:
+ def __init__(self, ok: bool, status_code: int = 200, json_data: dict | None = None, text: str = "") -> None:
+ self.ok = ok
+ self.status_code = status_code
+ self._json_data = json_data or {}
+ self.text = text
+ self.headers: dict[str, str] = {}
+
+ def json(self) -> dict:
+ return self._json_data
+
+
+def _make_image(color: str = "black") -> Image.Image:
+ return Image.new("RGB", (32, 32), color=color)
+
+
+def _build_model(provider_model_id: str, modes: list[str] | None = None) -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key="seedream_test",
+ name="Seedream Test",
+ provider_id="seedream",
+ provider_model_id=provider_model_id,
+ capabilities=ExternalModelCapabilities(
+ modes=modes or ["txt2img", "img2img"],
+ supports_negative_prompt=False,
+ supports_reference_images=True,
+ supports_seed=True,
+ supports_guidance=True,
+ ),
+ )
+
+
+def _build_request(
+ model: ExternalApiModelConfig,
+ mode: str = "txt2img",
+ init_image: Image.Image | None = None,
+ reference_images: list[ExternalReferenceImage] | None = None,
+ num_images: int = 1,
+ guidance: float | None = None,
+ seed: int | None = 123,
+) -> ExternalGenerationRequest:
+ return ExternalGenerationRequest(
+ model=model,
+ mode=mode, # type: ignore[arg-type]
+ prompt="A test prompt",
+ negative_prompt="",
+ seed=seed,
+ num_images=num_images,
+ width=2048,
+ height=2048,
+ steps=20,
+ guidance=guidance,
+ init_image=init_image,
+ mask_image=None,
+ reference_images=reference_images or [],
+ metadata=None,
+ )
+
+
+def test_seedream_is_configured() -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="test-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ assert provider.is_configured() is True
+
+
+def test_seedream_not_configured() -> None:
+ config = InvokeAIAppConfig()
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ assert provider.is_configured() is False
+
+
+def test_seedream_txt2img_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ api_key = "seedream-key"
+ config = InvokeAIAppConfig(external_seedream_api_key=api_key)
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["headers"] = headers
+ captured["json"] = json
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://ark.ap-southeast.bytepluses.com/api/v3/images/generations"
+ headers = captured["headers"]
+ assert isinstance(headers, dict)
+ assert headers["Authorization"] == f"Bearer {api_key}"
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ assert json_payload["model"] == "seedream-4-5-251128"
+ assert json_payload["prompt"] == "A test prompt"
+ assert json_payload["size"] == "2048x2048"
+ assert json_payload["response_format"] == "b64_json"
+ assert json_payload["watermark"] is False
+ assert json_payload["sequential_image_generation"] == "disabled"
+ # Seed should not be sent for 4.x models
+ assert "seed" not in json_payload
+ # Guidance should not be sent for 4.x models
+ assert "guidance_scale" not in json_payload
+ assert len(result.images) == 1
+ assert result.images[0].seed == 123
+
+
+def test_seedream_3_0_t2i_sends_seed_and_guidance(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-3-0-t2i-250415", modes=["txt2img"])
+ request = _build_request(model, seed=42, guidance=2.5)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["json"] = json
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ assert json_payload["seed"] == 42
+ assert json_payload["guidance_scale"] == 2.5
+ # 3.0 models should not have sequential_image_generation
+ assert "sequential_image_generation" not in json_payload
+
+
+def test_seedream_batch_generation(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model, num_images=3)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["json"] = json
+ return DummyResponse(
+ ok=True,
+ json_data={"data": [{"b64_json": encoded}, {"b64_json": encoded}, {"b64_json": encoded}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ assert json_payload["sequential_image_generation"] == "auto"
+ assert json_payload["sequential_image_generation_options"] == {"max_images": 3}
+ assert len(result.images) == 3
+
+
+def test_seedream_img2img_with_reference_images(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ init_image = _make_image("blue")
+ ref_image = _make_image("red")
+ request = _build_request(
+ model,
+ mode="img2img",
+ init_image=init_image,
+ reference_images=[ExternalReferenceImage(image=ref_image, weight=0.5)],
+ )
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["json"] = json
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ images = json_payload["image"]
+ assert isinstance(images, list)
+ assert len(images) == 2 # init_image + reference
+ assert images[0].startswith("data:image/png;base64,")
+ assert images[1].startswith("data:image/png;base64,")
+ assert len(result.images) == 1
+
+
+def test_seedream_single_image_not_array(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-3-0-t2i-250415", modes=["txt2img"])
+ init_image = _make_image("blue")
+ request = _build_request(model, mode="txt2img", init_image=init_image, guidance=5.5)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["json"] = json
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ # Single image should be a string, not an array
+ image = json_payload["image"]
+ assert isinstance(image, str)
+ assert image.startswith("data:image/png;base64,")
+
+
+def test_seedream_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model)
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(ok=False, status_code=400, text="bad request")
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ with pytest.raises(ExternalProviderRequestError, match="Seedream request failed"):
+ provider.generate(request)
+
+
+def test_seedream_no_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig()
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model)
+
+ with pytest.raises(ExternalProviderRequestError, match="API key is not configured"):
+ provider.generate(request)
+
+
+def test_seedream_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_seedream_api_key="seedream-key",
+ external_seedream_base_url="https://proxy.seedream/",
+ )
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.seedream/api/v3/images/generations"
+
+
+def test_seedream_batch_skips_error_items(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_seedream_api_key="seedream-key")
+ provider = SeedreamProvider(config, logging.getLogger("test"))
+ model = _build_model("seedream-4-5-251128")
+ request = _build_request(model, num_images=3)
+ encoded = encode_image_base64(_make_image("green"))
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(
+ ok=True,
+ json_data={
+ "data": [
+ {"b64_json": encoded},
+ {"error": {"code": "content_filter", "message": "filtered"}},
+ {"b64_json": encoded},
+ ]
+ },
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert len(result.images) == 2
From 19650f6ada297d5766e4a89d9eb21f33898c6160 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Wed, 4 Feb 2026 16:57:59 +0100
Subject: [PATCH 05/50] feat: initial external model support
---
docs/contributing/EXTERNAL_PROVIDERS.md | 57 +++
docs/contributing/index.md | 4 +
invokeai/app/api/dependencies.py | 10 +
invokeai/app/api/routers/app_info.py | 138 ++++++-
invokeai/app/api/routers/model_manager.py | 16 +-
.../invocations/external_image_generation.py | 148 ++++++++
.../app/services/config/config_default.py | 10 +
.../services/external_generation/__init__.py | 23 ++
.../services/external_generation/errors.py | 18 +
.../external_generation_base.py | 40 ++
.../external_generation_common.py | 55 +++
.../external_generation_default.py | 291 +++++++++++++++
.../external_generation/image_utils.py | 19 +
.../external_generation/providers/__init__.py | 4 +
.../external_generation/providers/gemini.py | 249 +++++++++++++
.../external_generation/providers/openai.py | 105 ++++++
invokeai/app/services/invocation_services.py | 3 +
.../model_install/model_install_common.py | 17 +-
.../model_install/model_install_default.py | 72 +++-
.../model_records/model_records_base.py | 19 +-
.../app/services/shared/invocation_context.py | 8 +
.../model_manager/configs/external_api.py | 80 ++++
.../backend/model_manager/configs/factory.py | 2 +
.../backend/model_manager/starter_models.py | 112 ++++++
invokeai/backend/model_manager/taxonomy.py | 4 +
invokeai/frontend/web/public/locales/en.json | 38 +-
.../components/StagingArea/context.tsx | 11 +-
.../components/StagingArea/shared.test.ts | 27 ++
.../components/StagingArea/shared.ts | 5 +-
.../controlLayers/store/paramsSlice.test.ts | 61 +++
.../controlLayers/store/paramsSlice.ts | 74 ++--
.../controlLayers/store/validators.ts | 12 +-
.../web/src/features/modelManagerV2/models.ts | 13 +-
.../store/installModelsStore.ts | 7 +-
.../ExternalProvidersForm.tsx | 281 ++++++++++++++
.../LaunchpadForm/LaunchpadForm.tsx | 12 +-
.../subpanels/InstallModels.tsx | 21 +-
.../ModelManagerPanel/ModelFormatBadge.tsx | 2 +
.../ModelPanel/Fields/BaseModelSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelFormatSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelTypeSelect.tsx | 4 +-
.../ModelPanel/Fields/ModelVariantSelect.tsx | 4 +-
.../Fields/PredictionTypeSelect.tsx | 4 +-
.../subpanels/ModelPanel/ModelEdit.tsx | 192 +++++++++-
.../subpanels/ModelPanel/ModelView.tsx | 23 +-
.../web/src/features/nodes/types/common.ts | 3 +
.../generation/buildExternalGraph.test.ts | 154 ++++++++
.../graph/generation/buildExternalGraph.ts | 129 +++++++
.../Bbox/BboxAspectRatioSelect.test.tsx | 44 +++
.../components/Bbox/BboxAspectRatioSelect.tsx | 5 +-
.../DimensionsAspectRatioSelect.test.tsx | 44 +++
.../DimensionsAspectRatioSelect.tsx | 10 +-
.../MainModel/mainModelPickerUtils.test.ts | 61 +++
.../MainModel/mainModelPickerUtils.ts | 14 +
.../parameters/components/ModelPicker.tsx | 24 +-
.../features/queue/hooks/useEnqueueCanvas.ts | 3 +
.../queue/hooks/useEnqueueGenerate.ts | 3 +
.../web/src/features/queue/store/readiness.ts | 23 +-
.../MainModelPicker.tsx | 11 +-
.../ExternalProviderStatusList.tsx | 39 ++
.../SettingsModal/SettingsModal.tsx | 8 +-
.../externalProviderStatusUtils.test.ts | 38 ++
.../externalProviderStatusUtils.ts | 26 ++
.../layouts/InitialStateMainModelPicker.tsx | 11 +-
.../web/src/services/api/endpoints/appInfo.ts | 40 +-
.../src/services/api/hooks/modelsByType.ts | 11 +-
.../frontend/web/src/services/api/schema.ts | 10 +-
.../frontend/web/src/services/api/types.ts | 51 +++
.../test_external_image_generation.py | 120 ++++++
tests/app/routers/test_app_info.py | 93 +++++
tests/app/routers/test_model_manager.py | 71 ++++
.../test_external_generation_service.py | 243 ++++++++++++
.../test_external_provider_adapters.py | 346 ++++++++++++++++++
.../model_install/test_model_install.py | 16 +
.../app/services/model_load/test_load_api.py | 23 ++
.../model_manager/test_external_api_config.py | 54 +++
tests/conftest.py | 2 +
77 files changed, 3923 insertions(+), 110 deletions(-)
create mode 100644 docs/contributing/EXTERNAL_PROVIDERS.md
create mode 100644 invokeai/app/invocations/external_image_generation.py
create mode 100644 invokeai/app/services/external_generation/__init__.py
create mode 100644 invokeai/app/services/external_generation/errors.py
create mode 100644 invokeai/app/services/external_generation/external_generation_base.py
create mode 100644 invokeai/app/services/external_generation/external_generation_common.py
create mode 100644 invokeai/app/services/external_generation/external_generation_default.py
create mode 100644 invokeai/app/services/external_generation/image_utils.py
create mode 100644 invokeai/app/services/external_generation/providers/__init__.py
create mode 100644 invokeai/app/services/external_generation/providers/gemini.py
create mode 100644 invokeai/app/services/external_generation/providers/openai.py
create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
create mode 100644 invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
create mode 100644 tests/app/invocations/test_external_image_generation.py
create mode 100644 tests/app/routers/test_app_info.py
create mode 100644 tests/app/routers/test_model_manager.py
create mode 100644 tests/app/services/external_generation/test_external_generation_service.py
create mode 100644 tests/app/services/external_generation/test_external_provider_adapters.py
create mode 100644 tests/backend/model_manager/test_external_api_config.py
diff --git a/docs/contributing/EXTERNAL_PROVIDERS.md b/docs/contributing/EXTERNAL_PROVIDERS.md
new file mode 100644
index 00000000000..c2371971db9
--- /dev/null
+++ b/docs/contributing/EXTERNAL_PROVIDERS.md
@@ -0,0 +1,57 @@
+# External Provider Integration
+
+This guide covers how to add new external image generation providers and model configs.
+
+## Provider Adapter Steps
+
+1) Create a provider adapter in `invokeai/app/services/external_generation/providers/` that inherits from `ExternalProvider`.
+2) Implement `is_configured()` using `InvokeAIAppConfig` fields, and `generate()` to map `ExternalGenerationRequest` to the provider API.
+3) Use helpers from `invokeai/app/services/external_generation/image_utils.py` for image encoding/decoding.
+4) Raise `ExternalProviderRequestError` on non-200 responses or empty payloads.
+5) Register the provider in `invokeai/app/api/dependencies.py` when building the `ExternalGenerationService` registry.
+
+## Config + Env Vars
+
+Add provider API keys to `InvokeAIAppConfig` with the `INVOKEAI_` prefix:
+
+- `INVOKEAI_EXTERNAL_GEMINI_API_KEY`
+- `INVOKEAI_EXTERNAL_OPENAI_API_KEY`
+
+These can also be set in `invokeai.yaml` under `external_gemini_api_key` and `external_openai_api_key`.
+
+## Example External Model Config
+
+External models are stored in the model manager like any other config. This example can be used as the `config` payload
+for `POST /api/v2/models/install?source=external://openai/gpt-image-1`:
+
+```json
+{
+ "key": "openai_gpt_image_1",
+ "name": "OpenAI GPT-Image-1",
+ "base": "external",
+ "type": "external_image_generator",
+ "format": "external_api",
+ "provider_id": "openai",
+ "provider_model_id": "gpt-image-1",
+ "capabilities": {
+ "modes": ["txt2img", "img2img", "inpaint"],
+ "supports_negative_prompt": true,
+ "supports_seed": true,
+ "supports_guidance": true,
+ "supports_reference_images": false,
+ "max_images_per_request": 1
+ },
+ "default_settings": {
+ "width": 1024,
+ "height": 1024,
+ "steps": 30
+ },
+ "tags": ["external", "openai"],
+ "is_default": false
+}
+```
+
+Notes:
+
+- `path`, `source`, and `hash` will auto-populate if omitted.
+- Set `capabilities` conservatively; the external generation service enforces them at runtime.
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index 79c1082746d..b8002a18024 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -8,6 +8,10 @@ We welcome contributions, whether features, bug fixes, code cleanup, testing, co
If you’d like to help with development, please see our [development guide](contribution_guides/development.md).
+## External Providers
+
+If you are adding external image generation providers or configs, see our [external provider integration guide](EXTERNAL_PROVIDERS.md).
+
**New Contributors:** If you’re unfamiliar with contributing to open source projects, take a look at our [new contributor guide](contribution_guides/newContributorChecklist.md).
## Nodes
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index 339a0ceadb4..ccb9388abcc 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -15,6 +15,8 @@
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.download.download_default import DownloadQueueService
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.app.services.external_generation.providers import GeminiProvider, OpenAIProvider
from invokeai.app.services.events.events_fastapievents import FastAPIEventService
from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
@@ -145,6 +147,13 @@ def initialize(
),
)
download_queue_service = DownloadQueueService(app_config=configuration, event_bus=events)
+ external_generation = ExternalGenerationService(
+ providers={
+ GeminiProvider.provider_id: GeminiProvider(app_config=configuration, logger=logger),
+ OpenAIProvider.provider_id: OpenAIProvider(app_config=configuration, logger=logger),
+ },
+ logger=logger,
+ )
model_images_service = ModelImageFileStorageDisk(model_images_folder / "model_images")
model_manager = ModelManagerService.build_model_manager(
app_config=configuration,
@@ -184,6 +193,7 @@ def initialize(
model_relationships=model_relationships,
model_relationship_records=model_relationship_records,
download_queue=download_queue_service,
+ external_generation=external_generation,
names=names,
performance_statistics=performance_statistics,
session_processor=session_processor,
diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py
index d8f3bb2f807..cbb3e6fb47d 100644
--- a/invokeai/app/api/routers/app_info.py
+++ b/invokeai/app/api/routers/app_info.py
@@ -2,12 +2,18 @@
from importlib.metadata import distributions
import torch
-from fastapi import Body
+from fastapi import Body, HTTPException, Path
from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
-from invokeai.app.services.config.config_default import InvokeAIAppConfig, get_config
+from invokeai.app.services.config.config_default import (
+ DefaultInvokeAIAppConfig,
+ InvokeAIAppConfig,
+ get_config,
+ load_and_migrate_config,
+)
+from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
from invokeai.backend.util.logging import logging
@@ -41,7 +47,7 @@ async def get_version() -> AppVersion:
async def get_app_deps() -> dict[str, str]:
deps: dict[str, str] = {dist.metadata["Name"]: dist.version for dist in distributions()}
try:
- cuda = torch.version.cuda or "N/A"
+ cuda = getattr(getattr(torch, "version", None), "cuda", None) or "N/A" # pyright: ignore[reportAttributeAccessIssue]
except Exception:
cuda = "N/A"
@@ -64,6 +70,29 @@ class InvokeAIAppConfigWithSetFields(BaseModel):
config: InvokeAIAppConfig = Field(description="The InvokeAI App Config")
+class ExternalProviderStatusModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ configured: bool = Field(description="Whether credentials are configured for the provider")
+ message: str | None = Field(default=None, description="Optional provider status detail")
+
+
+class ExternalProviderConfigUpdate(BaseModel):
+ api_key: str | None = Field(default=None, description="API key for the external provider")
+ base_url: str | None = Field(default=None, description="Optional base URL override for the provider")
+
+
+class ExternalProviderConfigModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ api_key_configured: bool = Field(description="Whether an API key is configured")
+ base_url: str | None = Field(default=None, description="Optional base URL override")
+
+
+EXTERNAL_PROVIDER_FIELDS: dict[str, tuple[str, str]] = {
+ "gemini": ("external_gemini_api_key", "external_gemini_base_url"),
+ "openai": ("external_openai_api_key", "external_openai_base_url"),
+}
+
+
@app_router.get(
"/runtime_config", operation_id="get_runtime_config", status_code=200, response_model=InvokeAIAppConfigWithSetFields
)
@@ -72,6 +101,109 @@ async def get_runtime_config() -> InvokeAIAppConfigWithSetFields:
return InvokeAIAppConfigWithSetFields(set_fields=config.model_fields_set, config=config)
+@app_router.get(
+ "/external_providers/status",
+ operation_id="get_external_provider_statuses",
+ status_code=200,
+ response_model=list[ExternalProviderStatusModel],
+)
+async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
+ statuses = ApiDependencies.invoker.services.external_generation.get_provider_statuses()
+ return [status_to_model(status) for status in statuses.values()]
+
+
+@app_router.get(
+ "/external_providers/config",
+ operation_id="get_external_provider_configs",
+ status_code=200,
+ response_model=list[ExternalProviderConfigModel],
+)
+async def get_external_provider_configs() -> list[ExternalProviderConfigModel]:
+ config = get_config()
+ return [_build_external_provider_config(provider_id, config) for provider_id in EXTERNAL_PROVIDER_FIELDS]
+
+
+@app_router.post(
+ "/external_providers/config/{provider_id}",
+ operation_id="set_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def set_external_provider_config(
+ provider_id: str = Path(description="The external provider identifier"),
+ update: ExternalProviderConfigUpdate = Body(description="External provider configuration settings"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ updates: dict[str, str | None] = {}
+
+ if update.api_key is not None:
+ api_key = update.api_key.strip()
+ updates[api_key_field] = api_key or None
+ if update.base_url is not None:
+ base_url = update.base_url.strip()
+ updates[base_url_field] = base_url or None
+
+ if not updates:
+ raise HTTPException(status_code=400, detail="No external provider config fields provided")
+
+ _apply_external_provider_update(updates)
+ return _build_external_provider_config(provider_id, get_config())
+
+
+@app_router.delete(
+ "/external_providers/config/{provider_id}",
+ operation_id="reset_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def reset_external_provider_config(
+ provider_id: str = Path(description="The external provider identifier"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ _apply_external_provider_update({api_key_field: None, base_url_field: None})
+ return _build_external_provider_config(provider_id, get_config())
+
+
+def status_to_model(status: ExternalProviderStatus) -> ExternalProviderStatusModel:
+ return ExternalProviderStatusModel(
+ provider_id=status.provider_id,
+ configured=status.configured,
+ message=status.message,
+ )
+
+
+def _get_external_provider_fields(provider_id: str) -> tuple[str, str]:
+ if provider_id not in EXTERNAL_PROVIDER_FIELDS:
+ raise HTTPException(status_code=404, detail=f"Unknown external provider '{provider_id}'")
+ return EXTERNAL_PROVIDER_FIELDS[provider_id]
+
+
+def _apply_external_provider_update(updates: dict[str, str | None]) -> None:
+ runtime_config = get_config()
+ config_path = runtime_config.config_file_path
+ if config_path.exists():
+ file_config = load_and_migrate_config(config_path)
+ else:
+ file_config = DefaultInvokeAIAppConfig()
+
+ for config in (runtime_config, file_config):
+ config.update_config(updates)
+ for field_name, value in updates.items():
+ if value is None:
+ config.model_fields_set.discard(field_name)
+
+ file_config.write_file(config_path, as_example=False)
+
+
+def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ return ExternalProviderConfigModel(
+ provider_id=provider_id,
+ api_key_configured=bool(getattr(config, api_key_field)),
+ base_url=getattr(config, base_url_field),
+ )
+
+
@app_router.get(
"/logging",
operation_id="get_log_level",
diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py
index a1f6b3a744a..4e1b14d18c1 100644
--- a/invokeai/app/api/routers/model_manager.py
+++ b/invokeai/app/api/routers/model_manager.py
@@ -30,6 +30,7 @@
)
from invokeai.app.services.orphaned_models import OrphanedModelInfo
from invokeai.app.util.suppress_output import SuppressOutput
+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 (
Main_Checkpoint_SD1_Config,
@@ -145,8 +146,16 @@ async def list_model_records(
found_models.extend(
record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format)
)
- for model in found_models:
+ for index, model in enumerate(found_models):
model = add_cover_image_to_model_config(model, ApiDependencies)
+ if isinstance(model, ExternalApiModelConfig):
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == model.source), None)
+ if starter_match is not None:
+ if starter_match.capabilities is not None:
+ setattr(model, "capabilities", starter_match.capabilities)
+ if starter_match.default_settings is not None:
+ setattr(model, "default_settings", starter_match.default_settings)
+ found_models[index] = model
return ModelsList(models=found_models)
@@ -166,6 +175,8 @@ async def list_missing_models() -> ModelsList:
missing_models: list[AnyModelConfig] = []
for model_config in record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
if not (models_path / model_config.path).resolve().exists():
missing_models.append(model_config)
@@ -250,7 +261,8 @@ async def reidentify_model(
result.config.name = config.name
result.config.description = config.description
result.config.cover_image = config.cover_image
- result.config.trigger_phrases = config.trigger_phrases
+ if hasattr(result.config, "trigger_phrases") and hasattr(config, "trigger_phrases"):
+ setattr(result.config, "trigger_phrases", getattr(config, "trigger_phrases"))
result.config.source = config.source
result.config.source_type = config.source_type
diff --git a/invokeai/app/invocations/external_image_generation.py b/invokeai/app/invocations/external_image_generation.py
new file mode 100644
index 00000000000..c70ecb40795
--- /dev/null
+++ b/invokeai/app/invocations/external_image_generation.py
@@ -0,0 +1,148 @@
+from typing import Any
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ MetadataField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageCollectionOutput
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+
+@invocation(
+ "external_image_generation",
+ title="External Image Generation",
+ tags=["external", "generation"],
+ category="image",
+ version="1.0.0",
+)
+class ExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generate images using an external provider."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ )
+ mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode")
+ prompt: str = InputField(description="Prompt")
+ negative_prompt: str | None = InputField(default=None, description="Negative prompt")
+ seed: int | None = InputField(default=None, description=FieldDescriptions.seed)
+ num_images: int = InputField(default=1, gt=0, description="Number of images to generate")
+ width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width)
+ height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height)
+ steps: int | None = InputField(default=None, gt=0, description=FieldDescriptions.steps)
+ guidance: float | None = InputField(default=None, ge=0, description="Guidance strength")
+ init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint")
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint")
+ reference_images: list[ImageField] = InputField(default=[], description="Reference images")
+ reference_image_weights: list[float] | None = InputField(default=None, description="Reference image weights")
+ reference_image_modes: list[str] | None = InputField(default=None, description="Reference image modes")
+
+ def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
+ model_config = context.models.get_config(self.model)
+ if not isinstance(model_config, ExternalApiModelConfig):
+ raise ValueError("Selected model is not an external API model")
+
+ init_image = None
+ if self.init_image is not None:
+ init_image = context.images.get_pil(self.init_image.image_name, mode="RGB")
+
+ mask_image = None
+ if self.mask_image is not None:
+ mask_image = context.images.get_pil(self.mask_image.image_name, mode="L")
+
+ if self.reference_image_weights is not None and len(self.reference_image_weights) != len(self.reference_images):
+ raise ValueError("reference_image_weights must match reference_images length")
+
+ if self.reference_image_modes is not None and len(self.reference_image_modes) != len(self.reference_images):
+ raise ValueError("reference_image_modes must match reference_images length")
+
+ reference_images: list[ExternalReferenceImage] = []
+ for index, image_field in enumerate(self.reference_images):
+ reference_image = context.images.get_pil(image_field.image_name, mode="RGB")
+ weight = None
+ mode = None
+ if self.reference_image_weights is not None:
+ weight = self.reference_image_weights[index]
+ if self.reference_image_modes is not None:
+ mode = self.reference_image_modes[index]
+ reference_images.append(ExternalReferenceImage(image=reference_image, weight=weight, mode=mode))
+
+ request = ExternalGenerationRequest(
+ model=model_config,
+ mode=self.mode,
+ prompt=self.prompt,
+ negative_prompt=self.negative_prompt,
+ seed=self.seed,
+ num_images=self.num_images,
+ width=self.width,
+ height=self.height,
+ steps=self.steps,
+ guidance=self.guidance,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images,
+ metadata=self._build_request_metadata(),
+ )
+
+ result = context._services.external_generation.generate(request)
+
+ outputs: list[ImageField] = []
+ for generated in result.images:
+ metadata = self._build_output_metadata(model_config, result, generated.seed)
+ image_dto = context.images.save(image=generated.image, metadata=metadata)
+ outputs.append(ImageField(image_name=image_dto.image_name))
+
+ return ImageCollectionOutput(collection=outputs)
+
+ def _build_request_metadata(self) -> dict[str, Any] | None:
+ if self.metadata is None:
+ return None
+ return self.metadata.root
+
+ def _build_output_metadata(
+ self,
+ model_config: ExternalApiModelConfig,
+ result: ExternalGenerationResult,
+ image_seed: int | None,
+ ) -> MetadataField | None:
+ metadata: dict[str, Any] = {}
+
+ if self.metadata is not None:
+ metadata.update(self.metadata.root)
+
+ metadata.update(
+ {
+ "external_provider": model_config.provider_id,
+ "external_model_id": model_config.provider_model_id,
+ }
+ )
+
+ provider_request_id = getattr(result, "provider_request_id", None)
+ if provider_request_id:
+ metadata["external_request_id"] = provider_request_id
+
+ provider_metadata = getattr(result, "provider_metadata", None)
+ if provider_metadata:
+ metadata["external_provider_metadata"] = provider_metadata
+
+ if image_seed is not None:
+ metadata["external_seed"] = image_seed
+
+ if not metadata:
+ return None
+ return MetadataField(root=metadata)
diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py
index 2cc2aaf273c..2fc2e9710ae 100644
--- a/invokeai/app/services/config/config_default.py
+++ b/invokeai/app/services/config/config_default.py
@@ -207,6 +207,16 @@ class InvokeAIAppConfig(BaseSettings):
# MULTIUSER
multiuser: bool = Field(default=False, description="Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.")
+ # EXTERNAL PROVIDERS
+ external_gemini_api_key: Optional[str] = Field(default=None, description="API key for Gemini image generation.")
+ external_openai_api_key: Optional[str] = Field(default=None, description="API key for OpenAI image generation.")
+ external_gemini_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Gemini image generation."
+ )
+ external_openai_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for OpenAI image generation."
+ )
+
# fmt: on
model_config = SettingsConfigDict(env_prefix="INVOKEAI_", env_ignore_empty=True)
diff --git a/invokeai/app/services/external_generation/__init__.py b/invokeai/app/services/external_generation/__init__.py
new file mode 100644
index 00000000000..692da64643a
--- /dev/null
+++ b/invokeai/app/services/external_generation/__init__.py
@@ -0,0 +1,23 @@
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalGeneratedImage,
+ ExternalProviderStatus,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+
+__all__ = [
+ "ExternalGenerationRequest",
+ "ExternalGenerationResult",
+ "ExternalGeneratedImage",
+ "ExternalGenerationService",
+ "ExternalGenerationServiceBase",
+ "ExternalProvider",
+ "ExternalProviderStatus",
+ "ExternalReferenceImage",
+]
diff --git a/invokeai/app/services/external_generation/errors.py b/invokeai/app/services/external_generation/errors.py
new file mode 100644
index 00000000000..9980b39bc43
--- /dev/null
+++ b/invokeai/app/services/external_generation/errors.py
@@ -0,0 +1,18 @@
+class ExternalGenerationError(Exception):
+ """Base error for external generation."""
+
+
+class ExternalProviderNotFoundError(ExternalGenerationError):
+ """Raised when no provider is registered for a model."""
+
+
+class ExternalProviderNotConfiguredError(ExternalGenerationError):
+ """Raised when a provider is missing required credentials."""
+
+
+class ExternalProviderCapabilityError(ExternalGenerationError):
+ """Raised when a request is not supported by provider capabilities."""
+
+
+class ExternalProviderRequestError(ExternalGenerationError):
+ """Raised when a provider rejects the request or returns an error."""
diff --git a/invokeai/app/services/external_generation/external_generation_base.py b/invokeai/app/services/external_generation/external_generation_base.py
new file mode 100644
index 00000000000..2145ff5ca42
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_base.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+
+
+class ExternalProvider(ABC):
+ provider_id: str
+
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ @abstractmethod
+ def is_configured(self) -> bool:
+ raise NotImplementedError
+
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ def get_status(self) -> ExternalProviderStatus:
+ return ExternalProviderStatus(provider_id=self.provider_id, configured=self.is_configured())
+
+
+class ExternalGenerationServiceBase(ABC):
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ @abstractmethod
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ raise NotImplementedError
diff --git a/invokeai/app/services/external_generation/external_generation_common.py b/invokeai/app/services/external_generation/external_generation_common.py
new file mode 100644
index 00000000000..c1e2f4706f5
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_common.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+from PIL.Image import Image as PILImageType
+
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+
+
+@dataclass(frozen=True)
+class ExternalReferenceImage:
+ image: PILImageType
+ weight: float | None = None
+ mode: str | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGenerationRequest:
+ model: ExternalApiModelConfig
+ mode: ExternalGenerationMode
+ prompt: str
+ negative_prompt: str | None
+ seed: int | None
+ num_images: int
+ width: int
+ height: int
+ steps: int | None
+ guidance: float | None
+ init_image: PILImageType | None
+ mask_image: PILImageType | None
+ reference_images: list[ExternalReferenceImage]
+ metadata: dict[str, Any] | None
+
+
+@dataclass(frozen=True)
+class ExternalGeneratedImage:
+ image: PILImageType
+ seed: int | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGenerationResult:
+ images: list[ExternalGeneratedImage]
+ seed_used: int | None = None
+ provider_request_id: str | None = None
+ provider_metadata: dict[str, Any] | None = None
+ content_filters: dict[str, str] | None = None
+
+
+@dataclass(frozen=True)
+class ExternalProviderStatus:
+ provider_id: str
+ configured: bool
+ message: str | None = None
diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py
new file mode 100644
index 00000000000..c72e16cde8d
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_default.py
@@ -0,0 +1,291 @@
+from __future__ import annotations
+
+from logging import Logger
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderNotConfiguredError,
+ ExternalProviderNotFoundError,
+)
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalImageSize
+from invokeai.backend.model_manager.starter_models import STARTER_MODELS
+
+
+class ExternalGenerationService(ExternalGenerationServiceBase):
+ def __init__(self, providers: dict[str, ExternalProvider], logger: Logger) -> None:
+ self._providers = providers
+ self._logger = logger
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ provider = self._providers.get(request.model.provider_id)
+ if provider is None:
+ raise ExternalProviderNotFoundError(f"No external provider registered for '{request.model.provider_id}'")
+
+ if not provider.is_configured():
+ raise ExternalProviderNotConfiguredError(f"Provider '{request.model.provider_id}' is missing credentials")
+
+ request = self._refresh_model_capabilities(request)
+ request = self._bucket_request(request)
+
+ self._validate_request(request)
+ return provider.generate(request)
+
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()}
+
+ def _validate_request(self, request: ExternalGenerationRequest) -> None:
+ capabilities = request.model.capabilities
+
+ self._logger.debug(
+ "Validating external request provider=%s model=%s mode=%s supported=%s",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.mode,
+ capabilities.modes,
+ )
+
+ if request.mode not in capabilities.modes:
+ raise ExternalProviderCapabilityError(f"Mode '{request.mode}' is not supported by {request.model.name}")
+
+ if request.negative_prompt and not capabilities.supports_negative_prompt:
+ raise ExternalProviderCapabilityError(f"Negative prompts are not supported by {request.model.name}")
+
+ if request.seed is not None and not capabilities.supports_seed:
+ raise ExternalProviderCapabilityError(f"Seed control is not supported by {request.model.name}")
+
+ if request.guidance is not None and not capabilities.supports_guidance:
+ raise ExternalProviderCapabilityError(f"Guidance is not supported by {request.model.name}")
+
+ if request.reference_images and not capabilities.supports_reference_images:
+ raise ExternalProviderCapabilityError(f"Reference images are not supported by {request.model.name}")
+
+ if capabilities.max_reference_images is not None:
+ if len(request.reference_images) > capabilities.max_reference_images:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_reference_images} reference images"
+ )
+
+ if capabilities.max_images_per_request is not None and request.num_images > capabilities.max_images_per_request:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_images_per_request} images per request"
+ )
+
+ if capabilities.max_image_size is not None:
+ if request.width > capabilities.max_image_size.width or request.height > capabilities.max_image_size.height:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports a maximum size of {capabilities.max_image_size.width}x{capabilities.max_image_size.height}"
+ )
+
+ if capabilities.allowed_aspect_ratios:
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ if aspect_ratio not in capabilities.allowed_aspect_ratios:
+ size_ratio = None
+ if capabilities.aspect_ratio_sizes:
+ size_ratio = _ratio_for_size(request.width, request.height, capabilities.aspect_ratio_sizes)
+ if size_ratio is None or size_ratio not in capabilities.allowed_aspect_ratios:
+ ratio_label = size_ratio or aspect_ratio
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} does not support aspect ratio {ratio_label}"
+ )
+
+ required_modes = capabilities.input_image_required_for or ["img2img", "inpaint"]
+ if request.mode in required_modes and request.init_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires an init image for {request.model.name}"
+ )
+
+ if request.mode == "inpaint" and request.mask_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires a mask image for {request.model.name}"
+ )
+
+ def _refresh_model_capabilities(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ try:
+ from invokeai.app.api.dependencies import ApiDependencies
+
+ record = ApiDependencies.invoker.services.model_manager.store.get_model(request.model.key)
+ except Exception:
+ record = None
+
+ if not isinstance(record, ExternalApiModelConfig):
+ return request
+
+ if record.key != request.model.key:
+ return request
+
+ if record.provider_id != request.model.provider_id:
+ return request
+
+ if record.provider_model_id != request.model.provider_model_id:
+ return request
+
+ record = _apply_starter_overrides(record)
+
+ if record == request.model:
+ return request
+
+ return ExternalGenerationRequest(
+ model=record,
+ mode=request.mode,
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=request.width,
+ height=request.height,
+ steps=request.steps,
+ guidance=request.guidance,
+ init_image=request.init_image,
+ mask_image=request.mask_image,
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ )
+
+ def _bucket_request(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ capabilities = request.model.capabilities
+ if not capabilities.allowed_aspect_ratios:
+ return request
+
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ size = None
+ if capabilities.aspect_ratio_sizes:
+ size = capabilities.aspect_ratio_sizes.get(aspect_ratio)
+
+ if size is not None:
+ if request.width == size.width and request.height == size.height:
+ return request
+ return self._bucket_to_size(request, size.width, size.height, aspect_ratio)
+
+ if aspect_ratio in capabilities.allowed_aspect_ratios:
+ return request
+
+ if not capabilities.aspect_ratio_sizes:
+ return request
+
+ closest = _select_closest_ratio(
+ request.width,
+ request.height,
+ capabilities.allowed_aspect_ratios,
+ )
+ if closest is None:
+ return request
+
+ size = capabilities.aspect_ratio_sizes.get(closest)
+ if size is None:
+ return request
+
+ return self._bucket_to_size(request, size.width, size.height, closest)
+
+ def _bucket_to_size(
+ self,
+ request: ExternalGenerationRequest,
+ width: int,
+ height: int,
+ ratio: str,
+ ) -> ExternalGenerationRequest:
+ self._logger.info(
+ "Bucketing external request provider=%s model=%s %sx%s -> %sx%s (ratio %s)",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.width,
+ request.height,
+ width,
+ height,
+ ratio,
+ )
+
+ return ExternalGenerationRequest(
+ model=request.model,
+ mode=request.mode,
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=width,
+ height=height,
+ steps=request.steps,
+ guidance=request.guidance,
+ init_image=_resize_image(request.init_image, width, height, "RGB"),
+ mask_image=_resize_image(request.mask_image, width, height, "L"),
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ )
+
+
+def _format_aspect_ratio(width: int, height: int) -> str:
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _select_closest_ratio(width: int, height: int, ratios: list[str]) -> str | None:
+ ratio = width / height
+ parsed: list[tuple[str, float]] = []
+ for value in ratios:
+ parsed_ratio = _parse_ratio(value)
+ if parsed_ratio is not None:
+ parsed.append((value, parsed_ratio))
+ if not parsed:
+ return None
+ return min(parsed, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _ratio_for_size(width: int, height: int, sizes: dict[str, ExternalImageSize]) -> str | None:
+ for ratio, size in sizes.items():
+ if size.width == width and size.height == height:
+ return ratio
+ return None
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
+
+
+def _resize_image(image: PILImageType | None, width: int, height: int, mode: str) -> PILImageType | None:
+ if image is None:
+ return None
+ if image.width == width and image.height == height:
+ return image
+ return image.convert(mode).resize((width, height), Image.Resampling.LANCZOS)
+
+
+def _apply_starter_overrides(model: ExternalApiModelConfig) -> ExternalApiModelConfig:
+ source = model.source or f"external://{model.provider_id}/{model.provider_model_id}"
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == source), None)
+ if starter_match is None:
+ return model
+ updates: dict[str, object] = {}
+ if starter_match.capabilities is not None:
+ updates["capabilities"] = starter_match.capabilities
+ if starter_match.default_settings is not None:
+ updates["default_settings"] = starter_match.default_settings
+ if not updates:
+ return model
+ return model.model_copy(update=updates)
diff --git a/invokeai/app/services/external_generation/image_utils.py b/invokeai/app/services/external_generation/image_utils.py
new file mode 100644
index 00000000000..a23c1f11d66
--- /dev/null
+++ b/invokeai/app/services/external_generation/image_utils.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import base64
+import io
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+
+def encode_image_base64(image: PILImageType, format: str = "PNG") -> str:
+ buffer = io.BytesIO()
+ image.save(buffer, format=format)
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
+
+
+def decode_image_base64(encoded: str) -> PILImageType:
+ data = base64.b64decode(encoded)
+ image = Image.open(io.BytesIO(data))
+ return image.convert("RGB")
diff --git a/invokeai/app/services/external_generation/providers/__init__.py b/invokeai/app/services/external_generation/providers/__init__.py
new file mode 100644
index 00000000000..9e380fca1e1
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/__init__.py
@@ -0,0 +1,4 @@
+from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
+from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+
+__all__ = ["GeminiProvider", "OpenAIProvider"]
diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py
new file mode 100644
index 00000000000..4d43431a14a
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/gemini.py
@@ -0,0 +1,249 @@
+from __future__ import annotations
+
+import json
+import uuid
+
+import requests
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+
+class GeminiProvider(ExternalProvider):
+ provider_id = "gemini"
+ _SYSTEM_INSTRUCTION = (
+ "You are an image generation model. Always respond with an image based on the user's prompt. "
+ "Do not return text-only responses. If the user input is not an edit instruction, "
+ "interpret it as a request to create a new image."
+ )
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_gemini_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_gemini_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Gemini API key is not configured")
+
+ base_url = (self._app_config.external_gemini_base_url or "https://generativelanguage.googleapis.com").rstrip(
+ "/"
+ )
+ if not base_url.endswith("/v1") and not base_url.endswith("/v1beta"):
+ base_url = f"{base_url}/v1beta"
+ model_id = request.model.provider_model_id.removeprefix("models/")
+ endpoint = f"{base_url}/models/{model_id}:generateContent"
+
+ request_parts: list[dict[str, object]] = []
+
+ if request.init_image is not None:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(request.init_image),
+ }
+ }
+ )
+
+ request_parts.append({"text": request.prompt})
+
+ for reference in request.reference_images:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(reference.image),
+ }
+ }
+ )
+
+ generation_config: dict[str, object] = {
+ "candidateCount": request.num_images,
+ "responseModalities": ["IMAGE"],
+ }
+ aspect_ratio = _select_aspect_ratio(
+ request.width,
+ request.height,
+ request.model.capabilities.allowed_aspect_ratios,
+ )
+ system_instruction = self._SYSTEM_INSTRUCTION
+ if request.init_image is not None:
+ system_instruction = (
+ f"{system_instruction} An input image is provided. "
+ "Treat the prompt as an edit instruction and modify the image accordingly. "
+ "Do not return the original image unchanged."
+ )
+ if aspect_ratio is not None:
+ system_instruction = f"{system_instruction} Use an aspect ratio of {aspect_ratio}."
+
+ payload: dict[str, object] = {
+ "systemInstruction": {"parts": [{"text": system_instruction}]},
+ "contents": [{"role": "user", "parts": request_parts}],
+ "generationConfig": generation_config,
+ }
+
+ self._dump_debug_payload("request", payload)
+
+ response = requests.post(
+ endpoint,
+ params={"key": api_key},
+ json=payload,
+ timeout=120,
+ )
+
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}"
+ )
+
+ data = response.json()
+ self._dump_debug_payload("response", data)
+ if not isinstance(data, dict):
+ raise ExternalProviderRequestError("Gemini response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ text_parts: list[str] = []
+ finish_messages: list[str] = []
+ candidates = data.get("candidates")
+ if not isinstance(candidates, list):
+ raise ExternalProviderRequestError("Gemini response payload missing candidates")
+ for candidate in candidates:
+ if not isinstance(candidate, dict):
+ continue
+ finish_message = candidate.get("finishMessage")
+ finish_reason = candidate.get("finishReason")
+ if isinstance(finish_message, str):
+ finish_messages.append(finish_message)
+ elif isinstance(finish_reason, str):
+ finish_messages.append(f"Finish reason: {finish_reason}")
+ for part in _iter_response_parts(candidate):
+ inline_data = part.get("inline_data") or part.get("inlineData")
+ if isinstance(inline_data, dict):
+ encoded = inline_data.get("data")
+ if encoded:
+ image = decode_image_base64(encoded)
+ images.append(ExternalGeneratedImage(image=image, seed=request.seed))
+ self._dump_debug_image(image)
+ continue
+ file_data = part.get("fileData") or part.get("file_data")
+ if isinstance(file_data, dict):
+ file_uri = file_data.get("fileUri") or file_data.get("file_uri")
+ if isinstance(file_uri, str) and file_uri:
+ raise ExternalProviderRequestError(
+ f"Gemini returned fileUri instead of inline image data: {file_uri}"
+ )
+ text = part.get("text")
+ if isinstance(text, str):
+ text_parts.append(text)
+
+ if not images:
+ self._logger.error("Gemini response contained no images: %s", data)
+ detail = ""
+ if finish_messages:
+ combined = " ".join(message.strip() for message in finish_messages if message.strip())
+ if combined:
+ detail = f" Response status: {combined[:500]}"
+ elif text_parts:
+ combined = " ".join(text_parts).strip()
+ if combined:
+ detail = f" Response text: {combined[:500]}"
+ raise ExternalProviderRequestError(f"Gemini response contained no images.{detail}")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_metadata={"model": request.model.provider_model_id},
+ )
+
+ def _dump_debug_payload(self, label: str, payload: object) -> None:
+ """TODO: remove debug payload dump once Gemini is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "gemini"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"{label}_{uuid.uuid4().hex}.json"
+ path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
+ except Exception as exc:
+ self._logger.debug("Failed to write Gemini debug payload: %s", exc)
+
+ def _dump_debug_image(self, image: "PILImageType") -> None:
+ """TODO: remove debug image dump once Gemini is stable."""
+ try:
+ outputs_path = self._app_config.outputs_path
+ if outputs_path is None:
+ return
+ debug_dir = outputs_path / "external_debug" / "gemini"
+ debug_dir.mkdir(parents=True, exist_ok=True)
+ path = debug_dir / f"decoded_{uuid.uuid4().hex}.png"
+ image.save(path, format="PNG")
+ except Exception as exc:
+ self._logger.debug("Failed to write Gemini debug image: %s", exc)
+
+
+def _iter_response_parts(candidate: dict[str, object]) -> list[dict[str, object]]:
+ content = candidate.get("content")
+ if isinstance(content, dict):
+ content_parts = content.get("parts")
+ if isinstance(content_parts, list):
+ return [part for part in content_parts if isinstance(part, dict)]
+ contents = candidate.get("contents")
+ if isinstance(contents, list):
+ parts: list[dict[str, object]] = []
+ for item in contents:
+ if not isinstance(item, dict):
+ continue
+ item_parts = item.get("parts")
+ if isinstance(item_parts, list):
+ parts.extend([part for part in item_parts if isinstance(part, dict)])
+ if parts:
+ return parts
+ return []
+
+
+def _select_aspect_ratio(width: int, height: int, allowed: list[str] | None) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ ratio = width / height
+ default_ratio = _format_aspect_ratio(width, height)
+ if not allowed:
+ return default_ratio
+ parsed = [(value, _parse_ratio(value)) for value in allowed]
+ filtered = [(value, parsed_ratio) for value, parsed_ratio in parsed if parsed_ratio is not None]
+ if not filtered:
+ return default_ratio
+ return min(filtered, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _format_aspect_ratio(width: int, height: int) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py
new file mode 100644
index 00000000000..e31a493b7a1
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/openai.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import io
+
+import requests
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64
+
+
+class OpenAIProvider(ExternalProvider):
+ provider_id = "openai"
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_openai_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_openai_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("OpenAI API key is not configured")
+
+ size = f"{request.width}x{request.height}"
+ base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/")
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ if request.mode == "txt2img":
+ payload: dict[str, object] = {
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ "response_format": "b64_json",
+ }
+ if request.seed is not None:
+ payload["seed"] = request.seed
+ response = requests.post(
+ f"{base_url}/v1/images/generations",
+ headers=headers,
+ json=payload,
+ timeout=120,
+ )
+ else:
+ files: dict[str, tuple[str, io.BytesIO, str]] = {}
+ if request.init_image is None:
+ raise ExternalProviderRequestError("OpenAI img2img/inpaint requires an init image")
+
+ image_buffer = io.BytesIO()
+ request.init_image.save(image_buffer, format="PNG")
+ image_buffer.seek(0)
+ files["image"] = ("image.png", image_buffer, "image/png")
+
+ if request.mask_image is not None:
+ mask_buffer = io.BytesIO()
+ request.mask_image.save(mask_buffer, format="PNG")
+ mask_buffer.seek(0)
+ files["mask"] = ("mask.png", mask_buffer, "image/png")
+
+ data: dict[str, object] = {
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ "response_format": "b64_json",
+ }
+ response = requests.post(
+ f"{base_url}/v1/images/edits",
+ headers=headers,
+ data=data,
+ files=files,
+ timeout=120,
+ )
+
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"OpenAI request failed with status {response.status_code}: {response.text}"
+ )
+
+ payload = response.json()
+ if not isinstance(payload, dict):
+ raise ExternalProviderRequestError("OpenAI response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ data_items = payload.get("data")
+ if not isinstance(data_items, list):
+ raise ExternalProviderRequestError("OpenAI response payload missing image data")
+ for item in data_items:
+ if not isinstance(item, dict):
+ continue
+ encoded = item.get("b64_json")
+ if not encoded:
+ continue
+ images.append(ExternalGeneratedImage(image=decode_image_base64(encoded), seed=request.seed))
+
+ if not images:
+ raise ExternalProviderRequestError("OpenAI response contained no images")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_request_id=response.headers.get("x-request-id"),
+ provider_metadata={"model": request.model.provider_model_id},
+ )
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
index 7a33f49940c..2c95f87b41d 100644
--- a/invokeai/app/services/invocation_services.py
+++ b/invokeai/app/services/invocation_services.py
@@ -21,6 +21,7 @@
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.app.services.download import DownloadQueueServiceBase
from invokeai.app.services.events.events_base import EventServiceBase
+ from invokeai.app.services.external_generation.external_generation_base import ExternalGenerationServiceBase
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
from invokeai.app.services.images.images_base import ImageServiceABC
@@ -63,6 +64,7 @@ def __init__(
model_relationships: "ModelRelationshipsServiceABC",
model_relationship_records: "ModelRelationshipRecordStorageBase",
download_queue: "DownloadQueueServiceBase",
+ external_generation: "ExternalGenerationServiceBase",
performance_statistics: "InvocationStatsServiceBase",
session_queue: "SessionQueueBase",
session_processor: "SessionProcessorBase",
@@ -94,6 +96,7 @@ def __init__(
self.model_relationships = model_relationships
self.model_relationship_records = model_relationship_records
self.download_queue = download_queue
+ self.external_generation = external_generation
self.performance_statistics = performance_statistics
self.session_queue = session_queue
self.session_processor = session_processor
diff --git a/invokeai/app/services/model_install/model_install_common.py b/invokeai/app/services/model_install/model_install_common.py
index 1006135a95e..11b7cd1f9dc 100644
--- a/invokeai/app/services/model_install/model_install_common.py
+++ b/invokeai/app/services/model_install/model_install_common.py
@@ -139,12 +139,27 @@ def __str__(self) -> str:
return str(self.url)
-ModelSource = Annotated[Union[LocalModelSource, HFModelSource, URLModelSource], Field(discriminator="type")]
+class ExternalModelSource(StringLikeSource):
+ """An external provider model identifier."""
+
+ provider_id: str
+ provider_model_id: str
+ type: Literal["external"] = "external"
+
+ def __str__(self) -> str:
+ return f"external://{self.provider_id}/{self.provider_model_id}"
+
+
+ModelSource = Annotated[
+ Union[LocalModelSource, HFModelSource, URLModelSource, ExternalModelSource],
+ Field(discriminator="type"),
+]
MODEL_SOURCE_TO_TYPE_MAP = {
URLModelSource: ModelSourceType.Url,
HFModelSource: ModelSourceType.HFRepoID,
LocalModelSource: ModelSourceType.Path,
+ ExternalModelSource: ModelSourceType.Url,
}
diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py
index c47267eab5f..2176ba926a9 100644
--- a/invokeai/app/services/model_install/model_install_default.py
+++ b/invokeai/app/services/model_install/model_install_default.py
@@ -28,6 +28,7 @@
from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
from invokeai.app.services.model_install.model_install_common import (
MODEL_SOURCE_TO_TYPE_MAP,
+ ExternalModelSource,
HFModelSource,
InstallStatus,
InvalidModelConfigException,
@@ -45,6 +46,11 @@
AnyModelConfig,
ModelConfigFactory,
)
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.configs.unknown import Unknown_Config
from invokeai.backend.model_manager.metadata import (
AnyModelRepoMetadata,
@@ -55,7 +61,7 @@
)
from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata
from invokeai.backend.model_manager.search import ModelSearch
-from invokeai.backend.model_manager.taxonomy import ModelRepoVariant, ModelSourceType
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelRepoVariant, ModelSourceType
from invokeai.backend.model_manager.util.lora_metadata_extractor import apply_lora_metadata
from invokeai.backend.util import InvokeAILogger
from invokeai.backend.util.catch_sigint import catch_sigint
@@ -451,6 +457,9 @@ def import_model(self, source: ModelSource, config: Optional[ModelRecordChanges]
install_job = self._import_from_hf(source, config)
elif isinstance(source, URLModelSource):
install_job = self._import_from_url(source, config)
+ elif isinstance(source, ExternalModelSource):
+ install_job = self._import_external_model(source, config)
+ self._put_in_queue(install_job)
else:
raise ValueError(f"Unsupported model source: '{type(source)}'")
@@ -748,7 +757,13 @@ def _guess_source(self, source: str) -> ModelSource:
source_obj: Optional[StringLikeSource] = None
source_stripped = source.strip('"')
- if Path(source_stripped).exists(): # A local file or directory
+ if source_stripped.startswith("external://"):
+ external_id = source_stripped.removeprefix("external://")
+ provider_id, _, provider_model_id = external_id.partition("/")
+ if not provider_id or not provider_model_id:
+ raise ValueError(f"Invalid external model source: '{source_stripped}'")
+ source_obj = ExternalModelSource(provider_id=provider_id, provider_model_id=provider_model_id)
+ elif Path(source_stripped).exists(): # A local file or directory
source_obj = LocalModelSource(path=Path(source_stripped))
elif match := re.match(hf_repoid_re, source):
source_obj = HFModelSource(
@@ -840,6 +855,9 @@ def _install_next_item(self) -> None:
self._logger.info(f"Installer thread {threading.get_ident()} exiting")
def _register_or_install(self, job: ModelInstallJob) -> None:
+ if isinstance(job.source, ExternalModelSource):
+ self._register_external_model(job)
+ return
# local jobs will be in waiting state, remote jobs will be downloading state
job.total_bytes = self._stat_size(job.local_path)
job.bytes = job.total_bytes
@@ -860,6 +878,41 @@ def _register_or_install(self, job: ModelInstallJob) -> None:
job.config_out = self.record_store.get_model(key)
self._signal_job_completed(job)
+ def _register_external_model(self, job: ModelInstallJob) -> None:
+ job.total_bytes = 0
+ job.bytes = 0
+ self._signal_job_running(job)
+ job.config_in.source = str(job.source)
+ job.config_in.source_type = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
+
+ provider_id = job.source.provider_id
+ provider_model_id = job.source.provider_model_id
+ capabilities = job.config_in.capabilities or ExternalModelCapabilities()
+ default_settings = (
+ job.config_in.default_settings
+ if isinstance(job.config_in.default_settings, ExternalApiModelDefaultSettings)
+ else None
+ )
+ name = job.config_in.name or f"{provider_id} {provider_model_id}"
+
+ config = ExternalApiModelConfig(
+ key=job.config_in.key or slugify(f"{provider_id}-{provider_model_id}"),
+ name=name,
+ description=job.config_in.description,
+ provider_id=provider_id,
+ provider_model_id=provider_model_id,
+ capabilities=capabilities,
+ default_settings=default_settings,
+ source=str(job.source),
+ source_type=MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__],
+ path="",
+ hash="",
+ file_size=0,
+ )
+ self.record_store.add_model(config)
+ job.config_out = self.record_store.get_model(config.key)
+ self._signal_job_completed(job)
+
def _set_error(self, install_job: ModelInstallJob, excp: Exception) -> None:
multifile_download_job = install_job._multifile_job
if multifile_download_job and any(
@@ -895,6 +948,8 @@ def _scan_for_missing_models(self) -> list[AnyModelConfig]:
"""Scan the models directory for missing models and return a list of them."""
missing_models: list[AnyModelConfig] = []
for model_config in self.record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
if not (self.app_config.models_path / model_config.path).resolve().exists():
missing_models.append(model_config)
return missing_models
@@ -1036,6 +1091,19 @@ def _import_from_url(
remote_files=remote_files,
)
+ def _import_external_model(
+ self,
+ source: ExternalModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ return ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config or ModelRecordChanges(),
+ local_path=self._app_config.models_path,
+ inplace=True,
+ )
+
def _import_remote_model(
self,
source: HFModelSource | URLModelSource,
diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py
index 96e12d3b0a3..318ebb000e6 100644
--- a/invokeai/app/services/model_records/model_records_base.py
+++ b/invokeai/app/services/model_records/model_records_base.py
@@ -13,6 +13,10 @@
from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
from invokeai.backend.model_manager.configs.controlnet import ControlAdapterDefaultSettings
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.configs.lora import LoraModelDefaultSettings
from invokeai.backend.model_manager.configs.main import MainModelDefaultSettings
@@ -86,8 +90,19 @@ class ModelRecordChanges(BaseModelExcludeNull):
file_size: Optional[int] = Field(description="Size of model file", default=None)
format: Optional[str] = Field(description="format of model file", default=None)
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
- default_settings: Optional[MainModelDefaultSettings | LoraModelDefaultSettings | ControlAdapterDefaultSettings] = (
- Field(description="Default settings for this model", default=None)
+ default_settings: Optional[
+ MainModelDefaultSettings
+ | LoraModelDefaultSettings
+ | ControlAdapterDefaultSettings
+ | ExternalApiModelDefaultSettings
+ ] = Field(description="Default settings for this model", default=None)
+
+ # External API model changes
+ provider_id: Optional[str] = Field(description="External provider identifier", default=None)
+ provider_model_id: Optional[str] = Field(description="External provider model identifier", default=None)
+ capabilities: Optional[ExternalModelCapabilities] = Field(
+ description="External model capabilities",
+ default=None,
)
cpu_only: Optional[bool] = Field(description="Whether this model should run on CPU only", default=None)
diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py
index 67e3c99f1ad..e38766d5ba2 100644
--- a/invokeai/app/services/shared/invocation_context.py
+++ b/invokeai/app/services/shared/invocation_context.py
@@ -388,6 +388,8 @@ def load(
submodel_type = submodel_type or identifier.submodel_type
model = self._services.model_manager.store.get_model(identifier.key)
+ self._raise_if_external(model)
+
message = f"Loading model {model.name}"
if submodel_type:
message += f" ({submodel_type.value})"
@@ -417,12 +419,18 @@ def load_by_attrs(
if len(configs) > 1:
raise ValueError(f"More than one model found with name {name}, base {base}, and type {type}")
+ self._raise_if_external(configs[0])
message = f"Loading model {name}"
if submodel_type:
message += f" ({submodel_type.value})"
self._util.signal_progress(message)
return self._services.model_manager.load.load_model(configs[0], submodel_type)
+ @staticmethod
+ def _raise_if_external(model: AnyModelConfig) -> None:
+ if model.base == BaseModelType.External or model.format == ModelFormat.ExternalApi:
+ raise ValueError("External API models cannot be loaded from disk")
+
def get_config(self, identifier: Union[str, "ModelIdentifierField"]) -> AnyModelConfig:
"""Get a model's config.
diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py
index e69de29bb2d..f57b4404e00 100644
--- a/invokeai/backend/model_manager/configs/external_api.py
+++ b/invokeai/backend/model_manager/configs/external_api.py
@@ -0,0 +1,80 @@
+from typing import Literal, Self
+
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+from invokeai.backend.model_manager.configs.base import Config_Base
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelSourceType, ModelType
+
+ExternalGenerationMode = Literal["txt2img", "img2img", "inpaint"]
+ExternalMaskFormat = Literal["alpha", "binary", "none"]
+
+
+class ExternalImageSize(BaseModel):
+ width: int = Field(gt=0)
+ height: int = Field(gt=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalModelCapabilities(BaseModel):
+ modes: list[ExternalGenerationMode] = Field(default_factory=lambda: ["txt2img"])
+ supports_reference_images: bool = Field(default=False)
+ supports_negative_prompt: bool = Field(default=True)
+ supports_seed: bool = Field(default=False)
+ supports_guidance: bool = Field(default=False)
+ max_images_per_request: int | None = Field(default=None, gt=0)
+ max_image_size: ExternalImageSize | None = Field(default=None)
+ allowed_aspect_ratios: list[str] | None = Field(default=None)
+ aspect_ratio_sizes: dict[str, ExternalImageSize] | None = Field(default=None)
+ max_reference_images: int | None = Field(default=None, gt=0)
+ mask_format: ExternalMaskFormat = Field(default="none")
+ input_image_required_for: list[ExternalGenerationMode] | None = Field(default=None)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalApiModelDefaultSettings(BaseModel):
+ width: int | None = Field(default=None, gt=0)
+ height: int | None = Field(default=None, gt=0)
+ steps: int | None = Field(default=None, gt=0)
+ guidance: float | None = Field(default=None, gt=0)
+ num_images: int | None = Field(default=None, gt=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class ExternalApiModelConfig(Config_Base):
+ base: Literal[BaseModelType.External] = Field(default=BaseModelType.External)
+ type: Literal[ModelType.ExternalImageGenerator] = Field(default=ModelType.ExternalImageGenerator)
+ format: Literal[ModelFormat.ExternalApi] = Field(default=ModelFormat.ExternalApi)
+
+ provider_id: str = Field(min_length=1, description="External provider ID")
+ provider_model_id: str = Field(min_length=1, description="Provider-specific model ID")
+ capabilities: ExternalModelCapabilities = Field(description="Provider capability matrix")
+ default_settings: ExternalApiModelDefaultSettings | None = Field(default=None)
+ tags: list[str] | None = Field(default=None)
+ is_default: bool = Field(default=False)
+
+ source_type: ModelSourceType = Field(default=ModelSourceType.Url)
+ path: str = Field(default="")
+ source: str = Field(default="")
+ hash: str = Field(default="")
+ file_size: int = Field(default=0, ge=0)
+
+ model_config = ConfigDict(extra="forbid")
+
+ @model_validator(mode="after")
+ def _populate_external_fields(self) -> "ExternalApiModelConfig":
+ if not self.path:
+ self.path = f"external://{self.provider_id}/{self.provider_model_id}"
+ if not self.source:
+ self.source = self.path
+ if not self.hash:
+ self.hash = f"external:{self.provider_id}:{self.provider_model_id}"
+ return self
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, object]) -> Self:
+ raise NotAMatchError("external API models are not probed from disk")
diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py
index 7702d4a5d9b..81464a1a971 100644
--- a/invokeai/backend/model_manager/configs/factory.py
+++ b/invokeai/backend/model_manager/configs/factory.py
@@ -26,6 +26,7 @@
ControlNet_Diffusers_SD2_Config,
ControlNet_Diffusers_SDXL_Config,
)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.configs.flux_redux import FLUXRedux_Checkpoint_Config
from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
from invokeai.backend.model_manager.configs.ip_adapter import (
@@ -256,6 +257,7 @@
Annotated[SigLIP_Diffusers_Config, SigLIP_Diffusers_Config.get_tag()],
Annotated[FLUXRedux_Checkpoint_Config, FLUXRedux_Checkpoint_Config.get_tag()],
Annotated[LlavaOnevision_Diffusers_Config, LlavaOnevision_Diffusers_Config.get_tag()],
+ Annotated[ExternalApiModelConfig, ExternalApiModelConfig.get_tag()],
# Unknown model (fallback)
Annotated[Unknown_Config, Unknown_Config.get_tag()],
],
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index ef7cd80cd29..59d7ceba205 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -2,6 +2,11 @@
from pydantic import BaseModel
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
@@ -13,6 +18,8 @@ class StarterModelWithoutDependencies(BaseModel):
type: ModelType
format: Optional[ModelFormat] = None
is_installed: bool = False
+ capabilities: ExternalModelCapabilities | None = None
+ default_settings: ExternalApiModelDefaultSettings | None = None
# allows us to track what models a user has installed across name changes within starter models
# if you update a starter model name, please add the old one to this list for that starter model
previous_names: list[str] = []
@@ -862,6 +869,108 @@ class StarterModelBundle(BaseModel):
)
# endregion
+# region External API
+gemini_flash_image = StarterModel(
+ name="Gemini 2.5 Flash Image",
+ base=BaseModelType.External,
+ source="external://gemini/gemini-2.5-flash-image",
+ description="Google Gemini 2.5 Flash image generation model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=True,
+ max_images_per_request=1,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "4:5",
+ "5:4",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "2:3": ExternalImageSize(width=832, height=1248),
+ "3:2": ExternalImageSize(width=1248, height=832),
+ "3:4": ExternalImageSize(width=864, height=1184),
+ "4:3": ExternalImageSize(width=1184, height=864),
+ "4:5": ExternalImageSize(width=896, height=1152),
+ "5:4": ExternalImageSize(width=1152, height=896),
+ "9:16": ExternalImageSize(width=768, height=1344),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ "21:9": ExternalImageSize(width=1536, height=672),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+gemini_pro_image_preview = StarterModel(
+ name="Gemini 3 Pro Image Preview",
+ base=BaseModelType.External,
+ source="external://gemini/gemini-3-pro-image-preview",
+ description="Google Gemini 3 Pro image generation preview model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=True,
+ max_images_per_request=1,
+ allowed_aspect_ratios=[
+ "1:1",
+ "2:3",
+ "3:2",
+ "3:4",
+ "4:3",
+ "4:5",
+ "5:4",
+ "9:16",
+ "16:9",
+ "21:9",
+ ],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "2:3": ExternalImageSize(width=832, height=1248),
+ "3:2": ExternalImageSize(width=1248, height=832),
+ "3:4": ExternalImageSize(width=864, height=1184),
+ "4:3": ExternalImageSize(width=1184, height=864),
+ "4:5": ExternalImageSize(width=896, height=1152),
+ "5:4": ExternalImageSize(width=1152, height=896),
+ "9:16": ExternalImageSize(width=768, height=1344),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ "21:9": ExternalImageSize(width=1536, height=672),
+ },
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+openai_gpt_image_1 = StarterModel(
+ name="ChatGPT Image",
+ base=BaseModelType.External,
+ source="external://openai/gpt-image-1",
+ description="OpenAI GPT-Image-1 image generation model (external API).",
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_seed=True,
+ supports_guidance=True,
+ supports_reference_images=False,
+ max_images_per_request=1,
+ ),
+ default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
+)
+# endregion
+
# List of starter models, displayed on the frontend.
# The order/sort of this list is not changed by the frontend - set it how you want it here.
STARTER_MODELS: list[StarterModel] = [
@@ -957,6 +1066,9 @@ class StarterModelBundle(BaseModel):
z_image_qwen3_encoder_quantized,
z_image_controlnet_union,
z_image_controlnet_tile,
+ gemini_flash_image,
+ gemini_pro_image_preview,
+ openai_gpt_image_1,
]
sd1_bundle: list[StarterModel] = [
diff --git a/invokeai/backend/model_manager/taxonomy.py b/invokeai/backend/model_manager/taxonomy.py
index c002418a6bd..855a39441d9 100644
--- a/invokeai/backend/model_manager/taxonomy.py
+++ b/invokeai/backend/model_manager/taxonomy.py
@@ -52,6 +52,8 @@ class BaseModelType(str, Enum):
"""Indicates the model is associated with CogView 4 model architecture."""
ZImage = "z-image"
"""Indicates the model is associated with Z-Image model architecture, including Z-Image-Turbo."""
+ External = "external"
+ """Indicates the model is hosted by an external provider."""
Unknown = "unknown"
"""Indicates the model's base architecture is unknown."""
@@ -76,6 +78,7 @@ class ModelType(str, Enum):
SigLIP = "siglip"
FluxRedux = "flux_redux"
LlavaOnevision = "llava_onevision"
+ ExternalImageGenerator = "external_image_generator"
Unknown = "unknown"
@@ -170,6 +173,7 @@ class ModelFormat(str, Enum):
BnbQuantizedLlmInt8b = "bnb_quantized_int8b"
BnbQuantizednf4b = "bnb_quantized_nf4b"
GGUFQuantized = "gguf_quantized"
+ ExternalApi = "external_api"
Unknown = "unknown"
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index c28df6ee383..9c726b6c938 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -993,6 +993,22 @@
"fileSize": "File Size",
"filterModels": "Filter models",
"fluxRedux": "FLUX Redux",
+ "externalImageGenerator": "External Image Generator",
+ "externalProviders": "External Providers",
+ "externalSetupTitle": "External Providers Setup",
+ "externalSetupDescription": "Connect an API key to enable external image generation and optionally install curated external models.",
+ "externalInstallDefaults": "Auto-install starter models",
+ "externalProvidersUnavailable": "External providers are not available in this build.",
+ "externalSetupFooter": "External providers use remote APIs; usage may incur provider-side costs.",
+ "externalProviderCardDescription": "Configure {{providerId}} credentials for external image generation.",
+ "externalApiKey": "API Key",
+ "externalApiKeyPlaceholder": "Paste your API key",
+ "externalApiKeyPlaceholderSet": "API key configured",
+ "externalApiKeyHelper": "Stored in your InvokeAI config file.",
+ "externalBaseUrl": "Base URL (optional)",
+ "externalBaseUrlPlaceholder": "https://...",
+ "externalBaseUrlHelper": "Override the default API base URL if needed.",
+ "externalResetHelper": "Clear API key and base URL.",
"height": "Height",
"huggingFace": "HuggingFace",
"huggingFacePlaceholder": "owner/model-name",
@@ -1060,6 +1076,21 @@
"modelUpdated": "Model Updated",
"modelUpdateFailed": "Model Update Failed",
"name": "Name",
+ "externalProvider": "External Provider",
+ "externalCapabilities": "External Capabilities",
+ "externalDefaults": "External Defaults",
+ "providerId": "Provider ID",
+ "providerModelId": "Provider Model ID",
+ "supportedModes": "Supported Modes",
+ "supportsNegativePrompt": "Supports Negative Prompt",
+ "supportsReferenceImages": "Supports Reference Images",
+ "supportsSeed": "Supports Seed",
+ "supportsGuidance": "Supports Guidance",
+ "maxImagesPerRequest": "Max Images Per Request",
+ "maxReferenceImages": "Max Reference Images",
+ "maxImageWidth": "Max Image Width",
+ "maxImageHeight": "Max Image Height",
+ "numImages": "Num Images",
"modelPickerFallbackNoModelsInstalled": "No models installed.",
"modelPickerFallbackNoModelsInstalled2": "Visit the Model Manager to install models.",
"noModelsInstalledDesc1": "Install models with the",
@@ -1102,6 +1133,7 @@
"urlDescription": "Install models from a URL or local file path. Perfect for specific models you want to add.",
"huggingFaceDescription": "Browse and install models directly from HuggingFace repositories.",
"scanFolderDescription": "Scan a local folder to automatically detect and install models.",
+ "externalDescription": "Connect to Gemini or OpenAI to generate images with external APIs.",
"recommendedModels": "Recommended Models",
"exploreStarter": "Or browse all available starter models",
"quickStart": "Quick Start Bundles",
@@ -1575,7 +1607,11 @@
"intermediatesCleared_one": "Cleared {{count}} Intermediate",
"intermediatesCleared_other": "Cleared {{count}} Intermediates",
"intermediatesClearedFailed": "Problem Clearing Intermediates",
- "reloadingIn": "Reloading in"
+ "reloadingIn": "Reloading in",
+ "externalProviders": "External Providers",
+ "externalProviderConfigured": "Configured",
+ "externalProviderNotConfigured": "API Key Required",
+ "externalProviderNotConfiguredHint": "Add your API key in Model Manager or the server config to enable this provider."
},
"toast": {
"addedToBoard": "Added to board {{name}}'s assets",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
index 6b8da8dc4da..c8333600c56 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx
@@ -72,10 +72,17 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
onAccept: (item, imageDTO) => {
const bboxRect = selectBboxRect(store.getState());
const { x, y } = bboxRect;
- const imageObject = imageDTOToImageObject(imageDTO);
+ const imageObject = imageDTOToImageObject(imageDTO, { usePixelBbox: false });
+ const scale = Math.min(bboxRect.width / imageDTO.width, bboxRect.height / imageDTO.height);
+ const scaledWidth = Math.round(imageDTO.width * scale);
+ const scaledHeight = Math.round(imageDTO.height * scale);
+ const position = {
+ x: x + Math.round((bboxRect.width - scaledWidth) / 2),
+ y: y + Math.round((bboxRect.height - scaledHeight) / 2),
+ };
const selectedEntityIdentifier = selectSelectedEntityIdentifier(store.getState());
const overrides: Partial = {
- position: { x, y },
+ position,
objects: [imageObject],
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
index f16b9023164..9268fc7570f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.test.ts
@@ -181,6 +181,33 @@ describe('StagingAreaApi Utility Functions', () => {
expect(result).toBe('first-image.png');
});
+ it('should return first image from image collections', () => {
+ const queueItem: S['SessionQueueItem'] = {
+ item_id: 1,
+ status: 'completed',
+ priority: 0,
+ destination: 'test-session',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ started_at: '2024-01-01T00:00:01Z',
+ completed_at: '2024-01-01T00:01:00Z',
+ error: null,
+ session: {
+ id: 'test-session',
+ source_prepared_mapping: {
+ canvas_output: ['output-node-id'],
+ },
+ results: {
+ 'output-node-id': {
+ images: [{ image_name: 'first.png' }, { image_name: 'second.png' }],
+ },
+ },
+ },
+ } as unknown as S['SessionQueueItem'];
+
+ expect(getOutputImageName(queueItem)).toBe('first.png');
+ });
+
it('should handle empty session mapping', () => {
const queueItem: S['SessionQueueItem'] = {
item_id: 1,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
index fe98408df58..1fe461e9993 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/shared.ts
@@ -1,4 +1,4 @@
-import { isImageField } from 'features/nodes/types/common';
+import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { isCanvasOutputNodeId } from 'features/nodes/util/graph/graphBuilderUtils';
import type { S } from 'services/api/types';
import { formatProgressMessage } from 'services/events/stores';
@@ -29,6 +29,9 @@ export const getOutputImageName = (item: S['SessionQueueItem']) => {
if (isImageField(value)) {
return value.image_name;
}
+ if (isImageFieldCollection(value)) {
+ return value[0]?.image_name ?? null;
+ }
}
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
new file mode 100644
index 00000000000..03de58908f0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts
@@ -0,0 +1,61 @@
+import { zModelIdentifierField } from 'features/nodes/types/common';
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+} from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { selectModelSupportsNegativePrompt, selectModelSupportsRefImages } from './paramsSlice';
+
+const createExternalConfig = (capabilities: ExternalModelCapabilities): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities: { ...capabilities, max_image_size: maxImageSize },
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ };
+};
+
+describe('paramsSlice selectors for external models', () => {
+ it('uses external capabilities for negative prompt support', () => {
+ const config = createExternalConfig({
+ modes: ['txt2img'],
+ supports_negative_prompt: true,
+ supports_reference_images: false,
+ });
+ const model = zModelIdentifierField.parse(config);
+
+ expect(selectModelSupportsNegativePrompt.resultFunc(model, config)).toBe(true);
+ });
+
+ it('uses external capabilities for ref image support', () => {
+ const config = createExternalConfig({
+ modes: ['txt2img'],
+ supports_negative_prompt: false,
+ supports_reference_images: false,
+ });
+ const model = zModelIdentifierField.parse(config);
+
+ expect(selectModelSupportsRefImages.resultFunc(model, config)).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index 8dcd93cc5de..41afb2ac19c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -43,7 +43,7 @@ import type {
} from 'features/parameters/types/parameterSchemas';
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
-import { isNonRefinerMainModelConfig } from 'services/api/types';
+import { isExternalApiModelConfig, isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const slice = createSlice({
@@ -638,15 +638,42 @@ export const selectOptimizedDenoisingEnabled = createParamsSelector((params) =>
export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt);
export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt);
export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? '');
+export const selectModelConfig = createSelector(
+ selectModelConfigsQuery,
+ selectParamsSlice,
+ (modelConfigs, { model }) => {
+ if (!modelConfigs.data) {
+ return null;
+ }
+ if (!model) {
+ return null;
+ }
+ return modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key) ?? null;
+ }
+);
export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null);
export const selectModelSupportsNegativePrompt = createSelector(
selectModel,
- (model) => !!model && SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base)
-);
-export const selectModelSupportsRefImages = createSelector(
- selectModel,
- (model) => !!model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)
+ selectModelConfig,
+ (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_negative_prompt ?? false;
+ }
+ return SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base);
+ }
);
+export const selectModelSupportsRefImages = createSelector(selectModel, selectModelConfig, (model, modelConfig) => {
+ if (!model) {
+ return false;
+ }
+ if (modelConfig && isExternalApiModelConfig(modelConfig)) {
+ return modelConfig.capabilities.supports_reference_images ?? false;
+ }
+ return SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base);
+});
export const selectModelSupportsOptimizedDenoising = createSelector(
selectModel,
(model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base)
@@ -693,24 +720,23 @@ export const selectHeight = createParamsSelector((params) => params.dimensions.h
export const selectAspectRatioID = createParamsSelector((params) => params.dimensions.aspectRatio.id);
export const selectAspectRatioValue = createParamsSelector((params) => params.dimensions.aspectRatio.value);
export const selectAspectRatioIsLocked = createParamsSelector((params) => params.dimensions.aspectRatio.isLocked);
+export const selectAllowedAspectRatioIDs = createSelector(selectModelConfig, (modelConfig) => {
+ if (!modelConfig || !isExternalApiModelConfig(modelConfig)) {
+ return null;
+ }
+ const allowed = modelConfig.capabilities.allowed_aspect_ratios;
+ return allowed?.length ? allowed : null;
+});
-export const selectMainModelConfig = createSelector(
- selectModelConfigsQuery,
- selectParamsSlice,
- (modelConfigs, { model }) => {
- if (!modelConfigs.data) {
- return null;
- }
- if (!model) {
- return null;
- }
- const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs.data, model.key);
- if (!modelConfig) {
- return null;
- }
- if (!isNonRefinerMainModelConfig(modelConfig)) {
- return null;
- }
+export const selectMainModelConfig = createSelector(selectModelConfig, (modelConfig) => {
+ if (!modelConfig) {
+ return null;
+ }
+ if (isExternalApiModelConfig(modelConfig)) {
return modelConfig;
}
-);
+ if (!isNonRefinerMainModelConfig(modelConfig)) {
+ return null;
+ }
+ return modelConfig;
+});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
index 3406e9e7ee6..e431c10558f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
@@ -6,7 +6,7 @@ import type {
RefImageState,
} from 'features/controlLayers/store/types';
import type { ModelIdentifierField } from 'features/nodes/types/common';
-import type { AnyModelConfig, MainModelConfig } from 'services/api/types';
+import type { AnyModelConfig, MainOrExternalModelConfig } from 'services/api/types';
const WARNINGS = {
UNSUPPORTED_MODEL: 'controlLayers.warnings.unsupportedModel',
@@ -28,7 +28,7 @@ type WarningTKey = (typeof WARNINGS)[keyof typeof WARNINGS];
export const getRegionalGuidanceWarnings = (
entity: CanvasRegionalGuidanceState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -112,7 +112,7 @@ export const areBasesCompatibleForRefImage = (
export const getGlobalReferenceImageWarnings = (
entity: RefImageState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -147,7 +147,7 @@ export const getGlobalReferenceImageWarnings = (
export const getControlLayerWarnings = (
entity: CanvasControlLayerState,
- model: MainModelConfig | null | undefined
+ model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -181,7 +181,7 @@ export const getControlLayerWarnings = (
export const getRasterLayerWarnings = (
_entity: CanvasRasterLayerState,
- _model: MainModelConfig | null | undefined
+ _model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
@@ -192,7 +192,7 @@ export const getRasterLayerWarnings = (
export const getInpaintMaskWarnings = (
_entity: CanvasInpaintMaskState,
- _model: MainModelConfig | null | undefined
+ _model: MainOrExternalModelConfig | null | undefined
): WarningTKey[] => {
const warnings: WarningTKey[] = [];
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/models.ts b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
index 7b5a08adfe2..99cd2e8a573 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/models.ts
+++ b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
@@ -1,10 +1,11 @@
import type { AnyModelVariant, BaseModelType, ModelFormat, ModelType } from 'features/nodes/types/common';
-import type { AnyModelConfig } from 'services/api/types';
import {
+ type AnyModelConfig,
isCLIPEmbedModelConfig,
isCLIPVisionModelConfig,
isControlLoRAModelConfig,
isControlNetModelConfig,
+ isExternalApiModelConfig,
isFluxReduxModelConfig,
isIPAdapterModelConfig,
isLLaVAModelConfig,
@@ -121,6 +122,11 @@ export const MODEL_CATEGORIES: Record = {
i18nKey: 'modelManager.llavaOnevision',
filter: isLLaVAModelConfig,
},
+ external_image_generator: {
+ category: 'external_image_generator',
+ i18nKey: 'modelManager.externalImageGenerator',
+ filter: isExternalApiModelConfig,
+ },
};
export const MODEL_CATEGORIES_AS_LIST = objectEntries(MODEL_CATEGORIES).map(([category, { i18nKey, filter }]) => ({
@@ -143,6 +149,7 @@ export const MODEL_BASE_TO_COLOR: Record = {
flux2: 'gold',
cogview4: 'red',
'z-image': 'cyan',
+ external: 'orange',
unknown: 'red',
};
@@ -167,6 +174,7 @@ export const MODEL_TYPE_TO_LONG_NAME: Record = {
clip_embed: 'CLIP Embed',
siglip: 'SigLIP',
flux_redux: 'FLUX Redux',
+ external_image_generator: 'External Image Generator',
unknown: 'Unknown',
};
@@ -184,6 +192,7 @@ export const MODEL_BASE_TO_LONG_NAME: Record = {
flux2: 'FLUX.2',
cogview4: 'CogView4',
'z-image': 'Z-Image',
+ external: 'External',
unknown: 'Unknown',
};
@@ -201,6 +210,7 @@ export const MODEL_BASE_TO_SHORT_NAME: Record = {
flux2: 'FLUX.2',
cogview4: 'CogView4',
'z-image': 'Z-Image',
+ external: 'External',
unknown: 'Unknown',
};
@@ -228,6 +238,7 @@ export const MODEL_FORMAT_TO_LONG_NAME: Record = {
checkpoint: 'Checkpoint',
lycoris: 'LyCORIS',
onnx: 'ONNX',
+ external_api: 'External API',
olive: 'Olive',
embedding_file: 'Embedding (file)',
embedding_folder: 'Embedding (folder)',
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
index b99a1212fec..79b7bfe31a7 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
+++ b/invokeai/frontend/web/src/features/modelManagerV2/store/installModelsStore.ts
@@ -1,13 +1,14 @@
import { atom } from 'nanostores';
-type InstallModelsTabName = 'launchpad' | 'urlOrLocal' | 'huggingface' | 'scanFolder' | 'starterModels';
+type InstallModelsTabName = 'launchpad' | 'urlOrLocal' | 'huggingface' | 'external' | 'scanFolder' | 'starterModels';
const TAB_TO_INDEX_MAP: Record = {
launchpad: 0,
urlOrLocal: 1,
huggingface: 2,
- scanFolder: 3,
- starterModels: 4,
+ external: 3,
+ scanFolder: 4,
+ starterModels: 5,
};
export const setInstallModelsTabByName = (tab: InstallModelsTabName) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
new file mode 100644
index 00000000000..26820cb0e29
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx
@@ -0,0 +1,281 @@
+import {
+ Badge,
+ Button,
+ Card,
+ Flex,
+ FormControl,
+ FormHelperText,
+ FormLabel,
+ Heading,
+ Input,
+ Switch,
+ Text,
+ Tooltip,
+} from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { useBuildModelInstallArg } from 'features/modelManagerV2/hooks/useBuildModelsToInstall';
+import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
+import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCheckBold, PiWarningBold } from 'react-icons/pi';
+import {
+ useGetExternalProviderConfigsQuery,
+ useResetExternalProviderConfigMutation,
+ useSetExternalProviderConfigMutation,
+} from 'services/api/endpoints/appInfo';
+import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
+import type { ExternalProviderConfig, StarterModel } from 'services/api/types';
+
+const PROVIDER_SORT_ORDER = ['gemini', 'openai'];
+
+type ProviderCardProps = {
+ provider: ExternalProviderConfig;
+ onInstallModels: (providerId: string) => void;
+};
+
+type UpdatePayload = {
+ provider_id: string;
+ api_key?: string;
+ base_url?: string;
+};
+
+export const ExternalProvidersForm = memo(() => {
+ const { t } = useTranslation();
+ const { data, isLoading } = useGetExternalProviderConfigsQuery();
+ const { data: starterModels } = useGetStarterModelsQuery();
+ const [installModel] = useInstallModel();
+ const { getIsInstalled, buildModelInstallArg } = useBuildModelInstallArg();
+ const [installDefaults, setInstallDefaults] = useState(true);
+ const tabIndex = useStore($installModelsTabIndex);
+
+ const toggleInstallDefaults = useCallback((event: ChangeEvent) => {
+ setInstallDefaults(event.target.checked);
+ }, []);
+
+ const externalModelsByProvider = useMemo(() => {
+ const groups = new Map();
+ for (const model of starterModels?.starter_models ?? []) {
+ if (!model.source.startsWith('external://')) {
+ continue;
+ }
+ const providerId = model.source.replace('external://', '').split('/')[0];
+ if (!providerId) {
+ continue;
+ }
+ const models = groups.get(providerId) ?? [];
+ models.push(model);
+ groups.set(providerId, models);
+ }
+
+ for (const [providerId, models] of groups.entries()) {
+ models.sort((a, b) => a.name.localeCompare(b.name));
+ groups.set(providerId, models);
+ }
+
+ return groups;
+ }, [starterModels]);
+
+ const handleInstallProviderModels = useCallback(
+ (providerId: string) => {
+ if (!installDefaults) {
+ return;
+ }
+ const models = externalModelsByProvider.get(providerId);
+ if (!models?.length) {
+ return;
+ }
+ const modelsToInstall = models.filter((model) => !getIsInstalled(model)).map(buildModelInstallArg);
+ modelsToInstall.forEach((model) => installModel(model));
+ },
+ [buildModelInstallArg, externalModelsByProvider, getIsInstalled, installDefaults, installModel]
+ );
+
+ const sortedProviders = useMemo(() => {
+ if (!data) {
+ return [];
+ }
+ return [...data].sort((a, b) => {
+ const aIndex = PROVIDER_SORT_ORDER.indexOf(a.provider_id);
+ const bIndex = PROVIDER_SORT_ORDER.indexOf(b.provider_id);
+ if (aIndex === -1 && bIndex === -1) {
+ return a.provider_id.localeCompare(b.provider_id);
+ }
+ if (aIndex === -1) {
+ return 1;
+ }
+ if (bIndex === -1) {
+ return -1;
+ }
+ return aIndex - bIndex;
+ });
+ }, [data]);
+
+ return (
+
+
+
+ {t('modelManager.externalSetupTitle')}
+ {t('modelManager.externalSetupDescription')}
+
+
+ {t('modelManager.externalInstallDefaults')}
+
+
+
+
+
+ {isLoading && {t('common.loading')}}
+ {!isLoading && sortedProviders.length === 0 && (
+ {t('modelManager.externalProvidersUnavailable')}
+ )}
+ {sortedProviders.map((provider) => (
+
+ ))}
+
+
+ {tabIndex === 3 && (
+
+ {t('modelManager.externalSetupFooter')}
+
+ )}
+
+ );
+});
+
+ExternalProvidersForm.displayName = 'ExternalProvidersForm';
+
+const ProviderCard = memo(({ provider, onInstallModels }: ProviderCardProps) => {
+ const { t } = useTranslation();
+ const [apiKey, setApiKey] = useState('');
+ const [baseUrl, setBaseUrl] = useState(provider.base_url ?? '');
+ const [saveConfig, { isLoading }] = useSetExternalProviderConfigMutation();
+ const [resetConfig, { isLoading: isResetting }] = useResetExternalProviderConfigMutation();
+
+ useEffect(() => {
+ setBaseUrl(provider.base_url ?? '');
+ }, [provider.base_url]);
+
+ const handleSave = useCallback(() => {
+ const trimmedApiKey = apiKey.trim();
+ const trimmedBaseUrl = baseUrl.trim();
+ const updatePayload: UpdatePayload = {
+ provider_id: provider.provider_id,
+ };
+ if (trimmedApiKey) {
+ updatePayload.api_key = trimmedApiKey;
+ }
+ if (trimmedBaseUrl !== (provider.base_url ?? '')) {
+ updatePayload.base_url = trimmedBaseUrl;
+ }
+
+ if (!updatePayload.api_key && updatePayload.base_url === undefined) {
+ return;
+ }
+
+ saveConfig(updatePayload)
+ .unwrap()
+ .then((result) => {
+ if (result.api_key_configured) {
+ setApiKey('');
+ onInstallModels(provider.provider_id);
+ }
+ if (result.base_url !== undefined) {
+ setBaseUrl(result.base_url ?? '');
+ }
+ });
+ }, [apiKey, baseUrl, onInstallModels, provider.base_url, provider.provider_id, saveConfig]);
+
+ const handleReset = useCallback(() => {
+ resetConfig(provider.provider_id)
+ .unwrap()
+ .then((result) => {
+ setApiKey('');
+ setBaseUrl(result.base_url ?? '');
+ });
+ }, [provider.provider_id, resetConfig]);
+
+ const handleApiKeyChange = useCallback((event: ChangeEvent) => {
+ setApiKey(event.target.value);
+ }, []);
+
+ const handleBaseUrlChange = useCallback((event: ChangeEvent) => {
+ setBaseUrl(event.target.value);
+ }, []);
+
+ const statusBadge = provider.api_key_configured ? (
+
+
+ {t('settings.externalProviderConfigured')}
+
+ ) : (
+
+
+ {t('settings.externalProviderNotConfigured')}
+
+ );
+
+ return (
+
+
+
+
+ {provider.provider_id}
+
+
+ {t('modelManager.externalProviderCardDescription', { providerId: provider.provider_id })}
+
+
+ {statusBadge}
+
+
+
+ {t('modelManager.externalApiKey')}
+
+ {t('modelManager.externalApiKeyHelper')}
+
+
+ {t('modelManager.externalBaseUrl')}
+
+ {t('modelManager.externalBaseUrlHelper')}
+
+
+
+
+
+
+
+
+
+ );
+});
+
+ProviderCard.displayName = 'ProviderCard';
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
index fc99bcec7bf..591c61a4b23 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm.tsx
@@ -6,7 +6,7 @@ import { StarterBundleButton } from 'features/modelManagerV2/subpanels/AddModelP
import { StarterBundleTooltipContentCompact } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterBundleTooltipContentCompact';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiFolderOpenBold, PiLinkBold, PiStarBold } from 'react-icons/pi';
+import { PiFolderOpenBold, PiLinkBold, PiPlugBold, PiStarBold } from 'react-icons/pi';
import { SiHuggingface } from 'react-icons/si';
import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
@@ -28,6 +28,10 @@ export const LaunchpadForm = memo(() => {
setInstallModelsTabByName('scanFolder');
}, []);
+ const navigateToExternalTab = useCallback(() => {
+ setInstallModelsTabByName('external');
+ }, []);
+
const navigateToStarterModelsTab = useCallback(() => {
setInstallModelsTabByName('starterModels');
}, []);
@@ -63,6 +67,12 @@ export const LaunchpadForm = memo(() => {
title={t('modelManager.scanFolder')}
description={t('modelManager.launchpad.scanFolderDescription')}
/>
+
{/* Recommended Section */}
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
index 9039c0f85f4..5bc4c9713fc 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx
@@ -2,18 +2,18 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore';
+import { ExternalProvidersForm } from 'features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm';
+import { HuggingFaceForm } from 'features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
+import { InstallModelForm } from 'features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm';
+import { LaunchpadForm } from 'features/modelManagerV2/subpanels/AddModelPanel/LaunchpadForm/LaunchpadForm';
+import { ModelInstallQueue } from 'features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue';
+import { ScanModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm';
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiCubeBold, PiFolderOpenBold, PiLinkSimpleBold, PiShootingStarBold } from 'react-icons/pi';
+import { PiCubeBold, PiFolderOpenBold, PiLinkSimpleBold, PiPlugBold, PiShootingStarBold } from 'react-icons/pi';
import { SiHuggingface } from 'react-icons/si';
-import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
-import { InstallModelForm } from './AddModelPanel/InstallModelForm';
-import { LaunchpadForm } from './AddModelPanel/LaunchpadForm/LaunchpadForm';
-import { ModelInstallQueue } from './AddModelPanel/ModelInstallQueue/ModelInstallQueue';
-import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
-
const installModelsTabSx: SystemStyleObject = {
display: 'flex',
gap: 2,
@@ -61,6 +61,10 @@ export const InstallModels = memo(() => {
{t('modelManager.huggingFace')}
+
+
+ {t('modelManager.externalProviders')}
+
{t('modelManager.scanFolder')}
@@ -80,6 +84,9 @@ export const InstallModels = memo(() => {
+
+
+
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
index 7d44ee54637..ff4dbe88fc8 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
@@ -19,6 +19,7 @@ const FORMAT_NAME_MAP: Record = {
bnb_quantized_nf4b: 'quantized',
gguf_quantized: 'gguf',
omi: 'omi',
+ external_api: 'external_api',
unknown: 'unknown',
olive: 'olive',
onnx: 'onnx',
@@ -40,6 +41,7 @@ const FORMAT_COLOR_MAP: Record = {
unknown: 'red',
olive: 'base',
onnx: 'base',
+ external_api: 'base',
};
const ModelFormatBadge = ({ format }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
index 8235d26efef..e4c8752e569 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_BASE_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_BASE_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_BASE_TO_LONG_NAME).map(([v
}));
type Props = {
- control: Control;
+ control: Control;
};
const BaseModelSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
index 1057ab7784c..2bd3eb954e5 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_FORMAT_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_FORMAT_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_FORMAT_TO_LONG_NAME).map((
}));
type Props = {
- control: Control;
+ control: Control;
};
const ModelFormatSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
index 44b41f01518..b35ce7f96df 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx
@@ -5,7 +5,7 @@ import { MODEL_TYPE_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_TYPE_TO_LONG_NAME).map(([value, label]) => ({
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = objectEntries(MODEL_TYPE_TO_LONG_NAME).map(([v
}));
type Props = {
- control: Control;
+ control: Control;
};
const ModelTypeSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
index 52eb2a4749d..d8e8c6a5b8a 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx
@@ -5,13 +5,13 @@ import { MODEL_VARIANT_TO_LONG_NAME } from 'features/modelManagerV2/models';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
import { objectEntries } from 'tsafe';
const options: ComboboxOption[] = objectEntries(MODEL_VARIANT_TO_LONG_NAME).map(([value, label]) => ({ label, value }));
type Props = {
- control: Control;
+ control: Control;
};
const ModelVariantSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
index dcef95b4243..593bc4c4136 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx
@@ -4,7 +4,7 @@ import { typedMemo } from 'common/util/typedMemo';
import { useCallback, useMemo } from 'react';
import type { Control } from 'react-hook-form';
import { useController } from 'react-hook-form';
-import type { UpdateModelArg } from 'services/api/endpoints/models';
+import type { UpdateModelBody } from 'services/api/types';
const options: ComboboxOption[] = [
{ value: 'none', label: '-' },
@@ -14,7 +14,7 @@ const options: ComboboxOption[] = [
];
type Props = {
- control: Control;
+ control: Control;
};
const PredictionTypeSelect = ({ control }: Props) => {
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
index d845eca3eec..7cde65bf072 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx
@@ -15,12 +15,17 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader';
import { toast } from 'features/toast/toast';
-import { memo, useCallback } from 'react';
-import { type SubmitHandler, useForm } from 'react-hook-form';
+import { memo, useCallback, useMemo } from 'react';
+import { type SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { PiCheckBold, PiXBold } from 'react-icons/pi';
import { type UpdateModelArg, useUpdateModelMutation } from 'services/api/endpoints/models';
-import type { AnyModelConfig } from 'services/api/types';
+import {
+ type AnyModelConfig,
+ type ExternalModelCapabilities,
+ isExternalApiModelConfig,
+ type UpdateModelBody,
+} from 'services/api/types';
import BaseModelSelect from './Fields/BaseModelSelect';
import ModelFormatSelect from './Fields/ModelFormatSelect';
@@ -33,6 +38,8 @@ type Props = {
modelConfig: AnyModelConfig;
};
+type ModelEditFormValues = UpdateModelBody;
+
const stringFieldOptions = {
validate: (value?: string | null) => (value && value.trim().length > 3) || 'Must be at least 3 characters',
};
@@ -41,19 +48,54 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation();
const dispatch = useAppDispatch();
+ const isExternal = useMemo(() => isExternalApiModelConfig(modelConfig), [modelConfig]);
- const form = useForm({
+ const form = useForm({
defaultValues: modelConfig,
mode: 'onChange',
});
- const onSubmit = useCallback>(
+ const externalModes = useWatch({
+ control: form.control,
+ name: 'capabilities.modes',
+ }) as ExternalModelCapabilities['modes'] | undefined;
+
+ const modeSet = useMemo(() => new Set(externalModes ?? []), [externalModes]);
+
+ const toggleMode = useCallback(
+ (mode: ExternalModelCapabilities['modes'][number]) => {
+ const nextModes = modeSet.has(mode)
+ ? externalModes?.filter((value) => value !== mode)
+ : [...(externalModes ?? []), mode];
+ form.setValue('capabilities.modes', nextModes ?? [], { shouldDirty: true, shouldValidate: true });
+ },
+ [externalModes, form, modeSet]
+ );
+
+ const handleToggleTxt2Img = useCallback(() => toggleMode('txt2img'), [toggleMode]);
+ const handleToggleImg2Img = useCallback(() => toggleMode('img2img'), [toggleMode]);
+ const handleToggleInpaint = useCallback(() => toggleMode('inpaint'), [toggleMode]);
+
+ const parseOptionalNumber = useCallback((value: string | null | undefined) => {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ if (typeof value !== 'string') {
+ return Number.isNaN(Number(value)) ? null : Number(value);
+ }
+ if (value.trim() === '') {
+ return null;
+ }
+ const parsed = Number(value);
+ return Number.isNaN(parsed) ? null : parsed;
+ }, []);
+
+ const onSubmit = useCallback>(
(values) => {
const responseBody: UpdateModelArg = {
key: modelConfig.key,
body: values,
};
-
updateModel(responseBody)
.unwrap()
.then((payload) => {
@@ -160,6 +202,144 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
+ {isExternal && (
+ <>
+
+ {t('modelManager.externalProvider')}
+
+
+
+ {t('modelManager.providerId')}
+
+
+
+ {t('modelManager.providerModelId')}
+
+
+
+
+ {t('modelManager.externalCapabilities')}
+
+
+
+ {t('modelManager.supportedModes')}
+
+
+ txt2img
+
+
+ img2img
+
+
+ inpaint
+
+
+
+
+ {t('modelManager.supportsNegativePrompt')}
+
+
+
+ {t('modelManager.supportsReferenceImages')}
+
+
+
+ {t('modelManager.supportsSeed')}
+
+
+
+ {t('modelManager.supportsGuidance')}
+
+
+
+ {t('modelManager.maxImagesPerRequest')}
+
+
+
+ {t('modelManager.maxReferenceImages')}
+
+
+
+ {t('modelManager.maxImageWidth')}
+
+
+
+ {t('modelManager.maxImageHeight')}
+
+
+
+
+ {t('modelManager.externalDefaults')}
+
+
+
+ {t('modelManager.width')}
+
+
+
+ {t('modelManager.height')}
+
+
+
+ {t('parameters.steps')}
+
+
+
+ {t('parameters.guidance')}
+
+
+
+ {t('modelManager.numImages')}
+
+
+
+ >
+ )}
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index 6e114bb252d..c54523e0fad 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -10,14 +10,15 @@ import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/Tri
import { filesize } from 'filesize';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import type {
- AnyModelConfig,
- CLIPEmbedModelConfig,
- CLIPVisionModelConfig,
- LlavaOnevisionModelConfig,
- Qwen3EncoderModelConfig,
- SigLIPModelConfig,
- T5EncoderModelConfig,
+import {
+ isExternalApiModelConfig,
+ type AnyModelConfig,
+ type CLIPEmbedModelConfig,
+ type CLIPVisionModelConfig,
+ type LlavaOnevisionModelConfig,
+ type Qwen3EncoderModelConfig,
+ type SigLIPModelConfig,
+ type T5EncoderModelConfig,
} from 'services/api/types';
import { isExternalModel } from './isExternalModel';
@@ -100,6 +101,12 @@ export const ModelView = memo(({ modelConfig }: Props) => {
+ {isExternalApiModelConfig(modelConfig) && (
+ <>
+
+
+ >
+ )}
{'variant' in modelConfig && modelConfig.variant && (
)}
diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts
index 36805c022d8..e5a4e32701d 100644
--- a/invokeai/frontend/web/src/features/nodes/types/common.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/common.ts
@@ -94,6 +94,7 @@ export const zBaseModelType = z.enum([
'flux2',
'cogview4',
'z-image',
+ 'external',
'unknown',
]);
export type BaseModelType = z.infer;
@@ -118,6 +119,7 @@ export const zModelType = z.enum([
'clip_embed',
'siglip',
'flux_redux',
+ 'external_image_generator',
'unknown',
]);
export type ModelType = z.infer;
@@ -167,6 +169,7 @@ export const zModelFormat = z.enum([
'bnb_quantized_int8b',
'bnb_quantized_nf4b',
'gguf_quantized',
+ 'external_api',
'unknown',
]);
export type ModelFormat = z.infer;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
new file mode 100644
index 00000000000..fd787456381
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.test.ts
@@ -0,0 +1,154 @@
+import type { RootState } from 'app/store/store';
+import type { ParamsState, RefImagesState } from 'features/controlLayers/store/types';
+import { imageDTOToCroppableImage, initialIPAdapter } from 'features/controlLayers/store/util';
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+ ImageDTO,
+ Invocation,
+} from 'services/api/types';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { buildExternalGraph } from './buildExternalGraph';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+ const capabilities: ExternalModelCapabilities = {
+ modes: ['txt2img'],
+ supports_negative_prompt: true,
+ supports_reference_images: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_image_size: maxImageSize,
+ };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities,
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ ...overrides,
+ };
+};
+
+let mockModelConfig: ExternalApiModelConfig | null = null;
+let mockParams: ParamsState;
+let mockRefImages: RefImagesState;
+let mockPrompts: { positive: string; negative: string };
+let mockSizes: { scaledSize: { width: number; height: number } };
+
+const mockOutputFields = {
+ id: 'external_output',
+ use_cache: false,
+ is_intermediate: false,
+ board: undefined,
+};
+
+vi.mock('features/controlLayers/store/paramsSlice', () => ({
+ selectModelConfig: () => mockModelConfig,
+ selectParamsSlice: () => mockParams,
+}));
+
+vi.mock('features/controlLayers/store/refImagesSlice', () => ({
+ selectRefImagesSlice: () => mockRefImages,
+}));
+
+vi.mock('features/nodes/util/graph/graphBuilderUtils', () => ({
+ getOriginalAndScaledSizesForTextToImage: () => mockSizes,
+ getOriginalAndScaledSizesForOtherModes: () => ({
+ scaledSize: { width: 512, height: 512 },
+ rect: { x: 0, y: 0, width: 512, height: 512 },
+ }),
+ selectCanvasOutputFields: () => mockOutputFields,
+ selectPresetModifiedPrompts: () => mockPrompts,
+}));
+
+beforeEach(() => {
+ mockParams = {
+ steps: 20,
+ guidance: 4.5,
+ } as ParamsState;
+ mockPrompts = { positive: 'a test prompt', negative: 'bad prompt' };
+ mockSizes = { scaledSize: { width: 768, height: 512 } };
+
+ const imageDTO = { image_name: 'ref.png', width: 64, height: 64 } as ImageDTO;
+ mockRefImages = {
+ selectedEntityId: null,
+ isPanelOpen: false,
+ entities: [
+ {
+ id: 'ref-image-1',
+ isEnabled: true,
+ config: {
+ ...initialIPAdapter,
+ weight: 0.5,
+ image: imageDTOToCroppableImage(imageDTO),
+ },
+ },
+ ],
+ };
+});
+
+describe('buildExternalGraph', () => {
+ it('builds txt2img graph with reference images and seed', async () => {
+ const modelConfig = createExternalModel();
+ mockModelConfig = modelConfig;
+
+ const { g } = await buildExternalGraph({
+ generationMode: 'txt2img',
+ state: {} as RootState,
+ manager: null,
+ });
+ const graph = g.getGraph();
+ const externalNode = Object.values(graph.nodes).find(
+ (node) => node.type === 'external_image_generation'
+ ) as Invocation<'external_image_generation'>;
+
+ expect(externalNode).toBeDefined();
+ expect(externalNode.mode).toBe('txt2img');
+ expect(externalNode.width).toBe(768);
+ expect(externalNode.height).toBe(512);
+ expect(externalNode.negative_prompt).toBe('bad prompt');
+ expect(externalNode.guidance).toBe(4.5);
+ expect(externalNode.reference_images?.[0]).toEqual({ image_name: 'ref.png' });
+ expect(externalNode.reference_image_weights).toEqual([0.5]);
+
+ const seedEdge = graph.edges.find((edge) => edge.destination.field === 'seed');
+ expect(seedEdge).toBeDefined();
+ });
+
+ it('throws when mode is unsupported', async () => {
+ const modelConfig = createExternalModel({
+ capabilities: {
+ modes: ['img2img'],
+ },
+ });
+ mockModelConfig = modelConfig;
+
+ await expect(
+ buildExternalGraph({
+ generationMode: 'txt2img',
+ state: {} as RootState,
+ manager: null,
+ })
+ ).rejects.toThrow('does not support txt2img');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
new file mode 100644
index 00000000000..02c030aa3b8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
@@ -0,0 +1,129 @@
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import { zImageField } from 'features/nodes/types/common';
+import { Graph } from 'features/nodes/util/graph/generation/Graph';
+import {
+ getOriginalAndScaledSizesForOtherModes,
+ getOriginalAndScaledSizesForTextToImage,
+ selectCanvasOutputFields,
+ selectPresetModifiedPrompts,
+} from 'features/nodes/util/graph/graphBuilderUtils';
+import {
+ type GraphBuilderArg,
+ type GraphBuilderReturn,
+ UnsupportedGenerationModeError,
+} from 'features/nodes/util/graph/types';
+import { type Invocation, isExternalApiModelConfig } from 'services/api/types';
+import { assert } from 'tsafe';
+
+export const buildExternalGraph = async (arg: GraphBuilderArg): Promise => {
+ const { generationMode, state, manager } = arg;
+
+ const model = selectModelConfig(state);
+ assert(model, 'No model selected');
+ assert(isExternalApiModelConfig(model), 'Selected model is not an external API model');
+
+ const requestedMode = generationMode === 'outpaint' ? 'inpaint' : generationMode;
+ if (!model.capabilities.modes.includes(requestedMode)) {
+ throw new UnsupportedGenerationModeError(`${model.name} does not support ${requestedMode} mode`);
+ }
+
+ const params = selectParamsSlice(state);
+ const refImages = selectRefImagesSlice(state);
+ const prompts = selectPresetModifiedPrompts(state);
+
+ const g = new Graph(getPrefixedId('external_graph'));
+
+ const seed = model.capabilities.supports_seed
+ ? g.addNode({
+ id: getPrefixedId('seed'),
+ type: 'integer',
+ })
+ : null;
+
+ const positivePrompt = g.addNode({
+ id: getPrefixedId('positive_prompt'),
+ type: 'string',
+ });
+
+ const externalNode = g.addNode({
+ id: getPrefixedId('external_image_generation'),
+ type: 'external_image_generation',
+ model,
+ mode: requestedMode,
+ negative_prompt: model.capabilities.supports_negative_prompt ? prompts.negative : null,
+ steps: params.steps,
+ guidance: model.capabilities.supports_guidance ? params.guidance : null,
+ num_images: 1,
+ });
+
+ if (seed) {
+ g.addEdge(seed, 'value', externalNode, 'seed');
+ }
+ g.addEdge(positivePrompt, 'value', externalNode, 'prompt');
+
+ if (model.capabilities.supports_reference_images) {
+ const referenceImages = refImages.entities
+ .filter((entity) => entity.isEnabled)
+ .map((entity) => entity.config)
+ .filter((config) => config.image)
+ .map((config) => zImageField.parse(config.image?.crop?.image ?? config.image?.original.image));
+
+ const referenceWeights = refImages.entities
+ .filter((entity) => entity.isEnabled)
+ .map((entity) => entity.config)
+ .filter((config) => config.image)
+ .map((config) => (config.type === 'ip_adapter' ? config.weight : null));
+
+ if (referenceImages.length > 0) {
+ externalNode.reference_images = referenceImages;
+ if (referenceWeights.every((weight): weight is number => weight !== null)) {
+ externalNode.reference_image_weights = referenceWeights;
+ }
+ }
+ }
+
+ if (generationMode === 'txt2img') {
+ const { scaledSize } = getOriginalAndScaledSizesForTextToImage(state);
+ externalNode.width = scaledSize.width;
+ externalNode.height = scaledSize.height;
+ } else {
+ assert(manager, 'Canvas manager is required for img2img/inpaint');
+ const canvasSettings = selectCanvasSettingsSlice(state);
+ const { scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state);
+ externalNode.width = scaledSize.width;
+ externalNode.height = scaledSize.height;
+
+ const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
+ const initImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
+ is_intermediate: true,
+ silent: true,
+ });
+ externalNode.init_image = { image_name: initImage.image_name };
+
+ if (generationMode === 'inpaint' || generationMode === 'outpaint') {
+ const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
+ const maskImage = await manager.compositor.getGrayscaleMaskCompositeImageDTO(
+ inpaintMaskAdapters,
+ rect,
+ 'denoiseLimit',
+ canvasSettings.preserveMask,
+ {
+ is_intermediate: true,
+ silent: true,
+ }
+ );
+ externalNode.mask_image = { image_name: maskImage.image_name };
+ }
+ }
+
+ g.updateNode(externalNode, selectCanvasOutputFields(state));
+
+ return {
+ g,
+ seed: seed ?? undefined,
+ positivePrompt: positivePrompt as Invocation<'string'>,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
new file mode 100644
index 00000000000..1ae1dcdc3a8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
@@ -0,0 +1,44 @@
+import type { ExternalApiModelConfig } from 'services/api/types';
+import { describe, expect, test } from 'vitest';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => ({
+ key: 'external-test',
+ name: 'External Test',
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'gemini',
+ provider_model_id: 'gemini-2.5-flash-image',
+ description: 'Test model',
+ source: 'external://gemini/gemini-2.5-flash-image',
+ source_type: 'url',
+ source_api_response: null,
+ path: '',
+ file_size: 0,
+ hash: 'external:gemini:gemini-2.5-flash-image',
+ cover_image: null,
+ is_default: false,
+ tags: ['external'],
+ capabilities: {
+ modes: ['txt2img'],
+ supports_reference_images: false,
+ supports_negative_prompt: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_images_per_request: 1,
+ max_image_size: null,
+ allowed_aspect_ratios: ['1:1', '16:9'],
+ max_reference_images: null,
+ mask_format: 'none',
+ input_image_required_for: null,
+ },
+ default_settings: null,
+ ...overrides,
+});
+
+describe('external model aspect ratios (bbox)', () => {
+ test('uses allowed aspect ratios for external models', () => {
+ const model = createExternalModel();
+ expect(model.capabilities.allowed_aspect_ratios).toEqual(['1:1', '16:9']);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
index a237896c676..28dcb54cd7b 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx
@@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectAllowedAspectRatioIDs } from 'features/controlLayers/store/paramsSlice';
import { selectAspectRatioID } from 'features/controlLayers/store/selectors';
import { isAspectRatioID, zAspectRatioID } from 'features/controlLayers/store/types';
import type { ChangeEventHandler } from 'react';
@@ -15,6 +16,8 @@ export const BboxAspectRatioSelect = memo(() => {
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
const isStaging = useCanvasIsStaging();
+ const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs);
+ const options = allowedAspectRatios ?? zAspectRatioID.options;
const onChange = useCallback>(
(e) => {
@@ -32,7 +35,7 @@ export const BboxAspectRatioSelect = memo(() => {
{t('parameters.aspect')}
}>
- {zAspectRatioID.options.map((ratio) => (
+ {options.map((ratio) => (
diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
new file mode 100644
index 00000000000..636260d1d25
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
@@ -0,0 +1,44 @@
+import type { ExternalApiModelConfig } from 'services/api/types';
+import { describe, expect, test } from 'vitest';
+
+const createExternalModel = (overrides: Partial = {}): ExternalApiModelConfig => ({
+ key: 'external-test',
+ name: 'External Test',
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'gemini',
+ provider_model_id: 'gemini-2.5-flash-image',
+ description: 'Test model',
+ source: 'external://gemini/gemini-2.5-flash-image',
+ source_type: 'url',
+ source_api_response: null,
+ path: '',
+ file_size: 0,
+ hash: 'external:gemini:gemini-2.5-flash-image',
+ cover_image: null,
+ is_default: false,
+ tags: ['external'],
+ capabilities: {
+ modes: ['txt2img'],
+ supports_reference_images: false,
+ supports_negative_prompt: true,
+ supports_seed: true,
+ supports_guidance: true,
+ max_images_per_request: 1,
+ max_image_size: null,
+ allowed_aspect_ratios: ['1:1', '16:9'],
+ max_reference_images: null,
+ mask_format: 'none',
+ input_image_required_for: null,
+ },
+ default_settings: null,
+ ...overrides,
+});
+
+describe('external model aspect ratios', () => {
+ test('uses allowed aspect ratios for external models', () => {
+ const model = createExternalModel();
+ expect(model.capabilities.allowed_aspect_ratios).toEqual(['1:1', '16:9']);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
index 4d3edc6e4bd..5e2952552c9 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx
@@ -1,7 +1,11 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
-import { aspectRatioIdChanged, selectAspectRatioID } from 'features/controlLayers/store/paramsSlice';
+import {
+ aspectRatioIdChanged,
+ selectAllowedAspectRatioIDs,
+ selectAspectRatioID,
+} from 'features/controlLayers/store/paramsSlice';
import { isAspectRatioID, zAspectRatioID } from 'features/controlLayers/store/types';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback } from 'react';
@@ -12,6 +16,8 @@ export const DimensionsAspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useAppSelector(selectAspectRatioID);
+ const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs);
+ const options = allowedAspectRatios ?? zAspectRatioID.options;
const onChange = useCallback>(
(e) => {
@@ -29,7 +35,7 @@ export const DimensionsAspectRatioSelect = memo(() => {
{t('parameters.aspect')}
}>
- {zAspectRatioID.options.map((ratio) => (
+ {options.map((ratio) => (
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
new file mode 100644
index 00000000000..b908efa096e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
@@ -0,0 +1,61 @@
+import type {
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+} from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { isExternalModelUnsupportedForTab } from './mainModelPickerUtils';
+
+const createExternalConfig = (modes: ExternalModelCapabilities['modes']): ExternalApiModelConfig => {
+ const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+
+ return {
+ key: 'external-test',
+ hash: 'external:openai:gpt-image-1',
+ path: 'external://openai/gpt-image-1',
+ file_size: 0,
+ name: 'External Test',
+ description: null,
+ source: 'external://openai/gpt-image-1',
+ source_type: 'url',
+ source_api_response: null,
+ cover_image: null,
+ base: 'external',
+ type: 'external_image_generator',
+ format: 'external_api',
+ provider_id: 'openai',
+ provider_model_id: 'gpt-image-1',
+ capabilities: {
+ modes,
+ supports_negative_prompt: true,
+ supports_reference_images: false,
+ max_image_size: maxImageSize,
+ },
+ default_settings: defaultSettings,
+ tags: ['external'],
+ is_default: false,
+ };
+};
+
+describe('isExternalModelUnsupportedForTab', () => {
+ it('disables external models without txt2img for generate', () => {
+ const model = createExternalConfig(['img2img', 'inpaint']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'generate')).toBe(true);
+ });
+
+ it('allows external models with txt2img for generate', () => {
+ const model = createExternalConfig(['txt2img']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'generate')).toBe(false);
+ });
+
+ it('allows external models on canvas', () => {
+ const model = createExternalConfig(['inpaint']);
+
+ expect(isExternalModelUnsupportedForTab(model, 'canvas')).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
new file mode 100644
index 00000000000..bc20c1a1184
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.ts
@@ -0,0 +1,14 @@
+import type { TabName } from 'features/ui/store/uiTypes';
+import { type AnyModelConfig, isExternalApiModelConfig } from 'services/api/types';
+
+export const isExternalModelUnsupportedForTab = (model: AnyModelConfig, tab: TabName): boolean => {
+ if (!isExternalApiModelConfig(model)) {
+ return false;
+ }
+
+ if (tab === 'generate') {
+ return !model.capabilities.modes.includes('txt2img');
+ }
+
+ return false;
+};
diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
index c5397791b84..b8631f5f742 100644
--- a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx
@@ -1,5 +1,6 @@
import type { BoxProps, ButtonProps, SystemStyleObject } from '@invoke-ai/ui-library';
import {
+ Badge,
Button,
Flex,
Icon,
@@ -33,7 +34,7 @@ import { memo, useCallback, useMemo, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiLinkSimple } from 'react-icons/pi';
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
-import type { AnyModelConfig } from 'services/api/types';
+import { type AnyModelConfig, type ExternalApiModelConfig, isExternalApiModelConfig } from 'services/api/types';
const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => {
const keys: string[] = [];
@@ -94,9 +95,7 @@ const NoOptionsFallback = memo(({ noOptionsText }: { noOptionsText?: string }) =
});
NoOptionsFallback.displayName = 'NoOptionsFallback';
-const getGroupIDFromModelConfig = (modelConfig: AnyModelConfig): string => {
- return modelConfig.base;
-};
+const getGroupIDFromModelConfig = (modelConfig: AnyModelConfig): string => modelConfig.base;
const getGroupNameFromModelConfig = (modelConfig: AnyModelConfig): string => {
return MODEL_BASE_TO_LONG_NAME[modelConfig.base];
@@ -387,6 +386,10 @@ const optionNameSx: SystemStyleObject = {
const PickerOptionComponent = typedMemo(
({ option, ...rest }: { option: WithStarred } & BoxProps) => {
const { isCompactView } = usePickerContext>();
+ const externalOption = isExternalApiModelConfig(option as AnyModelConfig)
+ ? (option as ExternalApiModelConfig)
+ : null;
+ const providerLabel = externalOption ? externalOption.provider_id.toUpperCase() : null;
return (
@@ -397,6 +400,15 @@ const PickerOptionComponent = typedMemo(
{option.name}
+ {!isCompactView && externalOption && (
+
+ {providerLabel}
+
+ )}
{option.file_size > 0 && (
(model: WithStarred, searchTerm: string) => {
const regex = getRegex(searchTerm);
const bases = BASE_KEYWORDS[model.base] ?? [model.base];
+ const externalModel = isExternalApiModelConfig(model as AnyModelConfig) ? (model as ExternalApiModelConfig) : null;
+ const externalSearch = externalModel ? ` ${externalModel.provider_id} ${externalModel.provider_model_id}` : '';
const testString =
- `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}`.toLowerCase();
+ `${model.name} ${bases.join(' ')} ${model.type} ${model.description ?? ''} ${model.format}${externalSearch}`.toLowerCase();
if (testString.includes(searchTerm) || regex.test(testString)) {
return true;
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
index 652cf4c5b24..5bfc31d10fd 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
@@ -9,6 +9,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
+import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildExternalGraph';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
@@ -59,6 +60,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
return await buildCogView4Graph(graphBuilderArg);
case 'z-image':
return await buildZImageGraph(graphBuilderArg);
+ case 'external':
+ return await buildExternalGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
index cf00a12ee5f..c50f833ba85 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
@@ -7,6 +7,7 @@ import { withResult, withResultAsync } from 'common/util/result';
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
+import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildExternalGraph';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
@@ -52,6 +53,8 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
return await buildCogView4Graph(graphBuilderArg);
case 'z-image':
return await buildZImageGraph(graphBuilderArg);
+ case 'external':
+ return await buildExternalGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index 8fa97eff4a9..61955e82c92 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -39,7 +39,8 @@ import type { TabName } from 'features/ui/store/uiTypes';
import i18n from 'i18next';
import { atom, computed } from 'nanostores';
import { useEffect } from 'react';
-import type { MainModelConfig } from 'services/api/types';
+import type { MainOrExternalModelConfig } from 'services/api/types';
+import { isExternalApiModelConfig } from 'services/api/types';
import { $isConnected } from 'services/events/stores';
/**
@@ -221,7 +222,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok
const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
isConnected: boolean;
- model: MainModelConfig | null | undefined;
+ model: MainOrExternalModelConfig | null | undefined;
params: ParamsState;
refImages: RefImagesState;
loras: LoRA[];
@@ -243,7 +244,11 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
- if (model?.base === 'flux') {
+ if (!model) {
+ // nothing else to validate
+ } else if (isExternalApiModelConfig(model)) {
+ // external models don't require local sub-models
+ } else if (model.base === 'flux') {
if (!params.t5EncoderModel) {
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
}
@@ -280,7 +285,7 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
}
}
- if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
+ if (model && !isExternalApiModelConfig(model) && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
enabledRefImages.forEach((entity, i) => {
@@ -431,7 +436,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: {
const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
isConnected: boolean;
- model: MainModelConfig | null | undefined;
+ model: MainOrExternalModelConfig | null | undefined;
canvas: CanvasState;
params: ParamsState;
refImages: RefImagesState;
@@ -488,7 +493,11 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
- if (model?.base === 'flux') {
+ if (!model) {
+ // nothing else to validate
+ } else if (isExternalApiModelConfig(model)) {
+ // external models don't require local sub-models
+ } else if (model.base === 'flux') {
if (!params.t5EncoderModel) {
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
}
@@ -682,7 +691,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
}
});
- if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
+ if (model && !isExternalApiModelConfig(model) && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
enabledRefImages.forEach((entity, i) => {
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
index 773b67e39bb..91f5f1efd0a 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
@@ -1,9 +1,11 @@
import { Flex, FormLabel, Icon } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import { isExternalModelUnsupportedForTab } from 'features/parameters/components/MainModel/mainModelPickerUtils';
import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
import { ModelPicker } from 'features/parameters/components/ModelPicker';
import { modelSelected } from 'features/parameters/store/actions';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdMoneyOff } from 'react-icons/md';
@@ -14,6 +16,7 @@ import { type AnyModelConfig, isNonCommercialMainModelConfig } from 'services/ap
export const MainModelPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
+ const activeTab = useAppSelector(selectActiveTab);
const [modelConfigs] = useMainModels();
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
@@ -28,6 +31,11 @@ export const MainModelPicker = memo(() => {
[selectedModelConfig]
);
+ const getIsOptionDisabled = useCallback(
+ (modelConfig: AnyModelConfig) => isExternalModelUnsupportedForTab(modelConfig, activeTab),
+ [activeTab]
+ );
+
return (
@@ -46,6 +54,7 @@ export const MainModelPicker = memo(() => {
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
+ getIsOptionDisabled={getIsOptionDisabled}
/>
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
new file mode 100644
index 00000000000..ea36cf4c65a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/ExternalProviderStatusList.tsx
@@ -0,0 +1,39 @@
+import { Badge, Flex, FormControl, FormLabel, Text, Tooltip } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetExternalProviderStatusesQuery } from 'services/api/endpoints/appInfo';
+
+import { getExternalProviderStatusBadgeInfo } from './externalProviderStatusUtils';
+
+export const ExternalProviderStatusList = memo(() => {
+ const { t } = useTranslation();
+ const { data } = useGetExternalProviderStatusesQuery();
+
+ if (!data || data.length === 0) {
+ return null;
+ }
+
+ const sortedProviders = [...data].sort((a, b) => a.provider_id.localeCompare(b.provider_id));
+
+ return (
+
+ {t('settings.externalProviders')}
+
+ {sortedProviders.map((status) => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(status);
+ const tooltip = badgeInfo.tooltipMessage ?? (badgeInfo.tooltipKey ? t(badgeInfo.tooltipKey) : null);
+ return (
+
+ {status.provider_id}
+
+ {t(badgeInfo.labelKey)}
+
+
+ );
+ })}
+
+
+ );
+});
+
+ExternalProviderStatusList.displayName = 'ExternalProviderStatusList';
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
index 6c7ebada8f0..b94669e92f0 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
@@ -20,6 +20,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice';
+import { ExternalProviderStatusList } from 'features/system/components/SettingsModal/ExternalProviderStatusList';
import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
@@ -48,8 +49,7 @@ import {
} from 'features/system/store/systemSlice';
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
-import type { ChangeEvent, ReactElement } from 'react';
-import { cloneElement, memo, useCallback, useEffect } from 'react';
+import { type ChangeEvent, cloneElement, memo, type ReactElement, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingsLanguageSelect } from './SettingsLanguageSelect';
@@ -198,6 +198,10 @@ const SettingsModal = (props: { children: ReactElement }) => {
+
+
+
+
{t('settings.showProgressInViewer')}
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
new file mode 100644
index 00000000000..98ae63004c3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.test.ts
@@ -0,0 +1,38 @@
+import type { ExternalProviderStatus } from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { getExternalProviderStatusBadgeInfo } from './externalProviderStatusUtils';
+
+const buildStatus = (overrides: Partial = {}): ExternalProviderStatus => ({
+ provider_id: 'openai',
+ configured: false,
+ message: null,
+ ...overrides,
+});
+
+describe('getExternalProviderStatusBadgeInfo', () => {
+ it('marks configured providers as configured', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus({ configured: true }));
+
+ expect(badgeInfo.labelKey).toBe('settings.externalProviderConfigured');
+ expect(badgeInfo.tooltipKey).toBeNull();
+ expect(badgeInfo.tooltipMessage).toBeNull();
+ expect(badgeInfo.colorScheme).toBe('green');
+ });
+
+ it('adds hint when provider is not configured', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus());
+
+ expect(badgeInfo.labelKey).toBe('settings.externalProviderNotConfigured');
+ expect(badgeInfo.tooltipKey).toBe('settings.externalProviderNotConfiguredHint');
+ expect(badgeInfo.tooltipMessage).toBeNull();
+ expect(badgeInfo.colorScheme).toBe('warning');
+ });
+
+ it('prefers status messages when present', () => {
+ const badgeInfo = getExternalProviderStatusBadgeInfo(buildStatus({ message: 'Missing key' }));
+
+ expect(badgeInfo.tooltipKey).toBeNull();
+ expect(badgeInfo.tooltipMessage).toBe('Missing key');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
new file mode 100644
index 00000000000..fb1f764e2a4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/externalProviderStatusUtils.ts
@@ -0,0 +1,26 @@
+import type { ExternalProviderStatus } from 'services/api/types';
+
+type ExternalProviderStatusBadgeInfo = {
+ labelKey: 'settings.externalProviderConfigured' | 'settings.externalProviderNotConfigured';
+ tooltipKey: 'settings.externalProviderNotConfiguredHint' | null;
+ tooltipMessage: string | null;
+ colorScheme: 'green' | 'warning';
+};
+
+export const getExternalProviderStatusBadgeInfo = (status: ExternalProviderStatus): ExternalProviderStatusBadgeInfo => {
+ if (status.configured) {
+ return {
+ labelKey: 'settings.externalProviderConfigured',
+ tooltipKey: null,
+ tooltipMessage: status.message ?? null,
+ colorScheme: 'green',
+ };
+ }
+
+ return {
+ labelKey: 'settings.externalProviderNotConfigured',
+ tooltipKey: status.message ? null : 'settings.externalProviderNotConfiguredHint',
+ tooltipMessage: status.message ?? null,
+ colorScheme: 'warning',
+ };
+};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
index 0d71b621734..07da9e6e18b 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/ui/layouts/InitialStateMainModelPicker.tsx
@@ -1,8 +1,10 @@
import { Flex, FormControl, FormLabel, Icon } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
+import { isExternalModelUnsupportedForTab } from 'features/parameters/components/MainModel/mainModelPickerUtils';
import { ModelPicker } from 'features/parameters/components/ModelPicker';
import { modelSelected } from 'features/parameters/store/actions';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdMoneyOff } from 'react-icons/md';
@@ -13,6 +15,7 @@ import { type AnyModelConfig, isNonCommercialMainModelConfig } from 'services/ap
export const InitialStateMainModelPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
+ const activeTab = useAppSelector(selectActiveTab);
const [modelConfigs] = useMainModels();
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
@@ -27,6 +30,11 @@ export const InitialStateMainModelPicker = memo(() => {
[selectedModelConfig]
);
+ const getIsOptionDisabled = useCallback(
+ (modelConfig: AnyModelConfig) => isExternalModelUnsupportedForTab(modelConfig, activeTab),
+ [activeTab]
+ );
+
return (
@@ -45,6 +53,7 @@ export const InitialStateMainModelPicker = memo(() => {
selectedModelConfig={selectedModelConfig}
onChange={onChange}
grouped
+ getIsOptionDisabled={getIsOptionDisabled}
/>
);
diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
index 8fe85125e6a..9f01c717108 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts
@@ -1,7 +1,7 @@
import type { OpenAPIV3_1 } from 'openapi-types';
import type { stringify } from 'querystring';
import type { paths } from 'services/api/schema';
-import type { AppVersion } from 'services/api/types';
+import type { AppVersion, ExternalProviderConfig, ExternalProviderStatus } from 'services/api/types';
import { api, buildV1Url } from '..';
@@ -52,6 +52,35 @@ export const appInfoApi = api.injectEndpoints({
method: 'GET',
}),
}),
+ getExternalProviderStatuses: build.query({
+ query: () => ({
+ url: buildAppInfoUrl('external_providers/status'),
+ method: 'GET',
+ }),
+ providesTags: ['FetchOnReconnect'],
+ }),
+ getExternalProviderConfigs: build.query({
+ query: () => ({
+ url: buildAppInfoUrl('external_providers/config'),
+ method: 'GET',
+ }),
+ providesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
+ setExternalProviderConfig: build.mutation({
+ query: ({ provider_id, ...body }) => ({
+ url: buildAppInfoUrl(`external_providers/config/${provider_id}`),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
+ resetExternalProviderConfig: build.mutation({
+ query: (provider_id) => ({
+ url: buildAppInfoUrl(`external_providers/config/${provider_id}`),
+ method: 'DELETE',
+ }),
+ invalidatesTags: ['AppConfig', 'FetchOnReconnect'],
+ }),
getInvocationCacheStatus: build.query<
paths['/api/v1/app/invocation_cache/status']['get']['responses']['200']['content']['application/json'],
void
@@ -95,6 +124,10 @@ export const {
useGetAppDepsQuery,
useGetPatchmatchStatusQuery,
useGetRuntimeConfigQuery,
+ useGetExternalProviderStatusesQuery,
+ useGetExternalProviderConfigsQuery,
+ useSetExternalProviderConfigMutation,
+ useResetExternalProviderConfigMutation,
useClearInvocationCacheMutation,
useDisableInvocationCacheMutation,
useEnableInvocationCacheMutation,
@@ -102,3 +135,8 @@ export const {
useGetOpenAPISchemaQuery,
useLazyGetOpenAPISchemaQuery,
} = appInfoApi;
+
+type SetExternalProviderConfigArg =
+ paths['/api/v1/app/external_providers/config/{provider_id}']['post']['requestBody']['content']['application/json'] & {
+ provider_id: paths['/api/v1/app/external_providers/config/{provider_id}']['post']['parameters']['path']['provider_id'];
+ };
diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index 98d7dd1e8df..fa3218d400e 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -9,11 +9,12 @@ import {
useGetMissingModelsQuery,
useGetModelConfigsQuery,
} from 'services/api/endpoints/models';
-import type { AnyModelConfig } from 'services/api/types';
+import type { AnyModelConfig, MainOrExternalModelConfig } from 'services/api/types';
import {
isCLIPEmbedModelConfigOrSubmodel,
isControlLayerModelConfig,
isControlNetModelConfig,
+ isExternalApiModelConfig,
isFlux1VAEModelConfig,
isFlux2VAEModelConfig,
isFluxKontextModelConfig,
@@ -21,7 +22,7 @@ import {
isFluxVAEModelConfig,
isIPAdapterModelConfig,
isLoRAModelConfig,
- isNonRefinerMainModelConfig,
+ isMainOrExternalModelConfig,
isQwen3EncoderModelConfig,
isRefinerMainModelModelConfig,
isSpandrelImageToImageModelConfig,
@@ -50,13 +51,13 @@ const buildModelsHook =
return modelConfigsAdapterSelectors
.selectAll(result.data)
.filter((config) => typeGuard(config))
- .filter((config) => !missingModelKeys.has(config.key))
+ .filter((config) => !missingModelKeys.has(config.key) || isExternalApiModelConfig(config))
.filter(filter);
}, [filter, result.data, missingModelsData]);
return [modelConfigs, result] as const;
};
-export const useMainModels = buildModelsHook(isNonRefinerMainModelConfig);
+export const useMainModels = buildModelsHook(isMainOrExternalModelConfig);
export const useRefinerModels = buildModelsHook(isRefinerMainModelModelConfig);
export const useLoRAModels = buildModelsHook(isLoRAModelConfig);
export const useControlLayerModels = buildModelsHook(isControlLayerModelConfig);
@@ -94,7 +95,7 @@ const buildModelsSelector =
return modelConfigsAdapterSelectors
.selectAll(result.data)
.filter(typeGuard)
- .filter((config) => !missingModelKeys.has(config.key));
+ .filter((config) => !missingModelKeys.has(config.key) || isExternalApiModelConfig(config));
};
export const selectIPAdapterModels = buildModelsSelector(isIPAdapterModelConfig);
export const selectGlobalRefImageModels = buildModelsSelector(
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index b605413787b..392723bef19 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -10434,7 +10434,7 @@ export type components = {
* @description The nodes in this graph
*/
nodes?: {
- [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
};
/**
* Edges
@@ -13656,7 +13656,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -13720,7 +13720,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -14026,7 +14026,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -14101,7 +14101,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExternalImageGenerationInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 5d56c346f87..0c06e04dcd6 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -43,6 +43,9 @@ export type InvocationJSONSchemaExtra = S['UIConfigBase'];
// App Info
export type AppVersion = S['AppVersion'];
+export type ExternalProviderStatus = S['ExternalProviderStatusModel'];
+export type ExternalProviderConfig = S['ExternalProviderConfigModel'];
+export type UpdateModelBody = paths['/api/v2/models/i/{key}']['patch']['requestBody']['content']['application/json'];
const zResourceOrigin = z.enum(['internal', 'external']);
type ResourceOrigin = z.infer;
@@ -110,6 +113,46 @@ export type ChatGPT4oModelConfig = ApiModelConfig;
export type Gemini2_5ModelConfig = ApiModelConfig;
type SubmodelDefinition = S['SubmodelDefinition'];
+export type ExternalImageSize = {
+ width: number;
+ height: number;
+};
+
+export type ExternalModelCapabilities = {
+ modes: ('txt2img' | 'img2img' | 'inpaint')[];
+ supports_reference_images?: boolean;
+ supports_negative_prompt?: boolean;
+ supports_seed?: boolean;
+ supports_guidance?: boolean;
+ max_images_per_request?: number | null;
+ max_image_size?: ExternalImageSize | null;
+ allowed_aspect_ratios?: string[] | null;
+ max_reference_images?: number | null;
+ mask_format?: 'alpha' | 'binary' | 'none';
+ input_image_required_for?: ('txt2img' | 'img2img' | 'inpaint')[] | null;
+};
+
+export type ExternalApiModelDefaultSettings = {
+ width?: number | null;
+ height?: number | null;
+ steps?: number | null;
+ guidance?: number | null;
+ num_images?: number | null;
+};
+
+export type ExternalApiModelConfig = AnyModelConfig & {
+ base: 'external';
+ type: 'external_image_generator';
+ format: 'external_api';
+ provider_id: string;
+ provider_model_id: string;
+ capabilities: ExternalModelCapabilities;
+ default_settings?: ExternalApiModelDefaultSettings | null;
+ tags?: string[] | null;
+ is_default?: boolean;
+};
+export type MainOrExternalModelConfig = MainModelConfig | ExternalApiModelConfig;
+
/**
* Checks if a list of submodels contains any that match a given variant or type
* @param submodels The list of submodels to check
@@ -290,6 +333,10 @@ export const isFluxReduxModelConfig = (config: AnyModelConfig): config is FLUXRe
return config.type === 'flux_redux';
};
+export const isExternalApiModelConfig = (config: AnyModelConfig): config is ExternalApiModelConfig => {
+ return (config as { format?: string }).format === 'external_api';
+};
+
export const isUnknownModelConfig = (config: AnyModelConfig): config is UnknownModelConfig => {
return config.type === 'unknown';
};
@@ -302,6 +349,10 @@ export const isNonRefinerMainModelConfig = (config: AnyModelConfig): config is M
return config.type === 'main' && config.base !== 'sdxl-refiner';
};
+export const isMainOrExternalModelConfig = (config: AnyModelConfig): config is MainOrExternalModelConfig => {
+ return isNonRefinerMainModelConfig(config) || isExternalApiModelConfig(config);
+};
+
export const isRefinerMainModelModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
return config.type === 'main' && config.base === 'sdxl-refiner';
};
diff --git a/tests/app/invocations/test_external_image_generation.py b/tests/app/invocations/test_external_image_generation.py
new file mode 100644
index 00000000000..3ede7aef421
--- /dev/null
+++ b/tests/app/invocations/test_external_image_generation.py
@@ -0,0 +1,120 @@
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+from PIL import Image
+
+from invokeai.app.invocations.external_image_generation import ExternalImageGenerationInvocation
+from invokeai.app.invocations.fields import ImageField
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.shared.graph import Graph, GraphExecutionState
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+
+
+def _build_model() -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img"],
+ supports_reference_images=True,
+ supports_negative_prompt=True,
+ supports_seed=True,
+ ),
+ )
+
+
+def _build_context(model_config: ExternalApiModelConfig, generated_image: Image.Image) -> MagicMock:
+ context = MagicMock()
+ context.models.get_config.return_value = model_config
+ context.images.get_pil.return_value = generated_image
+ context.images.save.return_value = SimpleNamespace(image_name="result.png")
+ context._services.external_generation.generate.return_value = ExternalGenerationResult(
+ images=[ExternalGeneratedImage(image=generated_image, seed=42)],
+ provider_request_id="req-123",
+ provider_metadata={"model": model_config.provider_model_id},
+ )
+ return context
+
+
+def test_external_invocation_builds_request_and_outputs() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ negative_prompt="bad",
+ seed=123,
+ num_images=1,
+ width=512,
+ height=512,
+ steps=10,
+ guidance=4.5,
+ reference_images=[ImageField(image_name="ref.png")],
+ reference_image_weights=[0.6],
+ )
+
+ output = invocation.invoke(context)
+
+ request = context._services.external_generation.generate.call_args[0][0]
+ assert request.prompt == "A prompt"
+ assert request.negative_prompt == "bad"
+ assert request.seed == 123
+ assert len(request.reference_images) == 1
+ assert request.reference_images[0].weight == 0.6
+ assert output.collection[0].image_name == "result.png"
+
+
+def test_external_invocation_rejects_mismatched_reference_weights() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ reference_images=[ImageField(image_name="ref.png")],
+ reference_image_weights=[0.1, 0.2],
+ )
+
+ with pytest.raises(ValueError, match="reference_image_weights"):
+ invocation.invoke(context)
+
+
+def test_external_graph_execution_state_runs_node() -> None:
+ model_config = _build_model()
+ model_field = ModelIdentifierField.from_config(model_config)
+ generated_image = Image.new("RGB", (16, 16), color="black")
+ context = _build_context(model_config, generated_image)
+
+ invocation = ExternalImageGenerationInvocation(
+ id="external_node",
+ model=model_field,
+ mode="txt2img",
+ prompt="A prompt",
+ )
+
+ graph = Graph()
+ graph.add_node(invocation)
+
+ session = GraphExecutionState(graph=graph)
+ node = session.next()
+ assert node is not None
+ output = node.invoke(context)
+ session.complete(node.id, output)
+
+ assert session.results[node.id] == output
diff --git a/tests/app/routers/test_app_info.py b/tests/app/routers/test_app_info.py
new file mode 100644
index 00000000000..12201249ef4
--- /dev/null
+++ b/tests/app/routers/test_app_info.py
@@ -0,0 +1,93 @@
+import os
+import os
+from pathlib import Path
+from typing import Any
+
+import pytest
+from fastapi.testclient import TestClient
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api_app import app
+from invokeai.app.services.config.config_default import get_config, load_and_migrate_config
+from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
+from invokeai.app.services.invoker import Invoker
+
+
+@pytest.fixture(autouse=True, scope="module")
+def client(invokeai_root_dir: Path) -> TestClient:
+ os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
+ return TestClient(app)
+
+
+class MockApiDependencies(ApiDependencies):
+ invoker: Invoker
+
+ def __init__(self, invoker: Invoker) -> None:
+ self.invoker = invoker
+
+
+def test_get_external_provider_statuses(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ statuses = {
+ "gemini": ExternalProviderStatus(provider_id="gemini", configured=True, message=None),
+ "openai": ExternalProviderStatus(provider_id="openai", configured=False, message="Missing key"),
+ }
+
+ monkeypatch.setattr("invokeai.app.api.routers.app_info.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr(mock_invoker.services.external_generation, "get_provider_statuses", lambda: statuses)
+
+ response = client.get("/api/v1/app/external_providers/status")
+
+ assert response.status_code == 200
+ payload = sorted(response.json(), key=lambda item: item["provider_id"])
+ assert payload == [
+ {"provider_id": "gemini", "configured": True, "message": None},
+ {"provider_id": "openai", "configured": False, "message": "Missing key"},
+ ]
+
+
+def test_external_provider_config_update_and_reset(client: TestClient) -> None:
+ for provider_id in ("gemini", "openai"):
+ response = client.delete(f"/api/v1/app/external_providers/config/{provider_id}")
+ assert response.status_code == 200
+
+ response = client.get("/api/v1/app/external_providers/config")
+ assert response.status_code == 200
+ payload = response.json()
+ openai_config = _get_provider_config(payload, "openai")
+ assert openai_config["api_key_configured"] is False
+ assert openai_config["base_url"] is None
+
+ response = client.post(
+ "/api/v1/app/external_providers/config/openai",
+ json={"api_key": "openai-key", "base_url": "https://api.openai.test"},
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["api_key_configured"] is True
+ assert payload["base_url"] == "https://api.openai.test"
+
+ response = client.get("/api/v1/app/external_providers/config")
+ assert response.status_code == 200
+ payload = response.json()
+ openai_config = _get_provider_config(payload, "openai")
+ assert openai_config["api_key_configured"] is True
+ assert openai_config["base_url"] == "https://api.openai.test"
+
+ config_path = get_config().config_file_path
+ file_config = load_and_migrate_config(config_path)
+ assert file_config.external_openai_api_key == "openai-key"
+ assert file_config.external_openai_base_url == "https://api.openai.test"
+
+ response = client.delete("/api/v1/app/external_providers/config/openai")
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["api_key_configured"] is False
+ assert payload["base_url"] is None
+
+ file_config = load_and_migrate_config(config_path)
+ assert file_config.external_openai_api_key is None
+ assert file_config.external_openai_base_url is None
+
+
+def _get_provider_config(payload: list[dict[str, Any]], provider_id: str) -> dict[str, Any]:
+ return next(item for item in payload if item["provider_id"] == provider_id)
diff --git a/tests/app/routers/test_model_manager.py b/tests/app/routers/test_model_manager.py
new file mode 100644
index 00000000000..8f69ffce371
--- /dev/null
+++ b/tests/app/routers/test_model_manager.py
@@ -0,0 +1,71 @@
+import os
+from pathlib import Path
+from typing import Any
+
+import pytest
+from fastapi.testclient import TestClient
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api_app import app
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+from invokeai.backend.model_manager.taxonomy import ModelType
+
+
+@pytest.fixture(autouse=True, scope="module")
+def client(invokeai_root_dir: Path) -> TestClient:
+ os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
+ return TestClient(app)
+
+
+class DummyModelImages:
+ def get_url(self, key: str) -> str:
+ return f"https://example.com/models/{key}.png"
+
+
+class DummyInvoker:
+ def __init__(self, services: Any) -> None:
+ self.services = services
+
+
+class MockApiDependencies(ApiDependencies):
+ invoker: DummyInvoker
+
+ def __init__(self, invoker: DummyInvoker) -> None:
+ self.invoker = invoker
+
+
+def test_model_manager_external_config_round_trip(
+ monkeypatch: Any, client: TestClient, mm2_model_manager: Any, mm2_app_config: Any
+) -> None:
+ config = ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(modes=["txt2img"]),
+ )
+ mm2_model_manager.store.add_model(config)
+
+ services = type("Services", (), {})()
+ services.model_manager = mm2_model_manager
+ services.model_images = DummyModelImages()
+ services.configuration = mm2_app_config
+
+ invoker = DummyInvoker(services)
+ monkeypatch.setattr("invokeai.app.api.routers.model_manager.ApiDependencies", MockApiDependencies(invoker))
+
+ response = client.get("/api/v2/models/", params={"model_type": ModelType.ExternalImageGenerator.value})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert len(payload["models"]) == 1
+ assert payload["models"][0]["key"] == "external_test"
+ assert payload["models"][0]["provider_id"] == "openai"
+ assert payload["models"][0]["cover_image"] == "https://example.com/models/external_test.png"
+
+ get_response = client.get("/api/v2/models/i/external_test")
+
+ assert get_response.status_code == 200
+ model_payload = get_response.json()
+ assert model_payload["provider_model_id"] == "gpt-image-1"
+ assert model_payload["cover_image"] == "https://example.com/models/external_test.png"
diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py
new file mode 100644
index 00000000000..8379b8b754c
--- /dev/null
+++ b/tests/app/services/external_generation/test_external_generation_service.py
@@ -0,0 +1,243 @@
+import logging
+
+import pytest
+from PIL import Image
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderNotConfiguredError,
+ ExternalProviderNotFoundError,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
+
+
+class DummyProvider(ExternalProvider):
+ def __init__(self, provider_id: str, configured: bool, result: ExternalGenerationResult | None = None) -> None:
+ super().__init__(InvokeAIAppConfig(), logging.getLogger("test"))
+ self.provider_id = provider_id
+ self._configured = configured
+ self._result = result
+ self.last_request: ExternalGenerationRequest | None = None
+
+ def is_configured(self) -> bool:
+ return self._configured
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ self.last_request = request
+ assert self._result is not None
+ return self._result
+
+
+def _build_model(capabilities: ExternalModelCapabilities) -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=capabilities,
+ )
+
+
+def _build_request(
+ *,
+ model: ExternalApiModelConfig,
+ mode: str = "txt2img",
+ negative_prompt: str | None = None,
+ seed: int | None = None,
+ num_images: int = 1,
+ guidance: float | None = None,
+ width: int = 64,
+ height: int = 64,
+ init_image: Image.Image | None = None,
+ mask_image: Image.Image | None = None,
+ reference_images: list[ExternalReferenceImage] | None = None,
+) -> ExternalGenerationRequest:
+ return ExternalGenerationRequest(
+ model=model,
+ mode=mode, # type: ignore[arg-type]
+ prompt="A test prompt",
+ negative_prompt=negative_prompt,
+ seed=seed,
+ num_images=num_images,
+ width=width,
+ height=height,
+ steps=10,
+ guidance=guidance,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images or [],
+ metadata=None,
+ )
+
+
+def _make_image() -> Image.Image:
+ return Image.new("RGB", (64, 64), color="black")
+
+
+def test_generate_requires_registered_provider() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model)
+ service = ExternalGenerationService({}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderNotFoundError):
+ service.generate(request)
+
+
+def test_generate_requires_configured_provider() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model)
+ provider = DummyProvider("openai", configured=False)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderNotConfiguredError):
+ service.generate(request)
+
+
+def test_generate_validates_mode_support() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"]))
+ request = _build_request(model=model, mode="img2img", init_image=_make_image())
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Mode 'img2img'"):
+ service.generate(request)
+
+
+def test_generate_validates_negative_prompt_support() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=False))
+ request = _build_request(model=model, negative_prompt="bad")
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Negative prompts"):
+ service.generate(request)
+
+
+def test_generate_requires_init_image_for_img2img() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["img2img"]))
+ request = _build_request(model=model, mode="img2img")
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="requires an init image"):
+ service.generate(request)
+
+
+def test_generate_requires_mask_for_inpaint() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["inpaint"]))
+ request = _build_request(model=model, mode="inpaint", init_image=_make_image())
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="requires a mask"):
+ service.generate(request)
+
+
+def test_generate_validates_reference_images() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_reference_images=False))
+ request = _build_request(
+ model=model,
+ reference_images=[ExternalReferenceImage(image=_make_image(), weight=0.8)],
+ )
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="Reference images"):
+ service.generate(request)
+
+
+def test_generate_validates_limits() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ supports_reference_images=True,
+ max_reference_images=1,
+ max_images_per_request=1,
+ )
+ )
+ request = _build_request(
+ model=model,
+ num_images=2,
+ reference_images=[
+ ExternalReferenceImage(image=_make_image()),
+ ExternalReferenceImage(image=_make_image()),
+ ],
+ )
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ with pytest.raises(ExternalProviderCapabilityError, match="supports at most"):
+ service.generate(request)
+
+
+def test_generate_validates_allowed_aspect_ratios() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1", "16:9"],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ },
+ )
+ )
+ request = _build_request(model=model)
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+ assert response.images == []
+ assert provider.last_request is not None
+ assert provider.last_request.width == 1024
+ assert provider.last_request.height == 1024
+
+
+def test_generate_validates_allowed_aspect_ratios_with_bucket_sizes() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1", "16:9"],
+ aspect_ratio_sizes={
+ "1:1": ExternalImageSize(width=1024, height=1024),
+ "16:9": ExternalImageSize(width=1344, height=768),
+ },
+ )
+ )
+ request = _build_request(model=model, width=160, height=90)
+ provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert response.images == []
+ assert provider.last_request is not None
+ assert provider.last_request.width == 1344
+ assert provider.last_request.height == 768
+
+
+def test_generate_happy_path() -> None:
+ model = _build_model(
+ ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True, supports_seed=True)
+ )
+ request = _build_request(model=model, negative_prompt="", seed=42)
+ result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=_make_image(), seed=42)])
+ provider = DummyProvider("openai", configured=True, result=result)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert response is result
+ assert provider.last_request == request
diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py
new file mode 100644
index 00000000000..38f4c9e3d52
--- /dev/null
+++ b/tests/app/services/external_generation/test_external_provider_adapters.py
@@ -0,0 +1,346 @@
+import io
+import logging
+
+import pytest
+from PIL import Image
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
+from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
+
+
+class DummyResponse:
+ def __init__(self, ok: bool, status_code: int = 200, json_data: dict | None = None, text: str = "") -> None:
+ self.ok = ok
+ self.status_code = status_code
+ self._json_data = json_data or {}
+ self.text = text
+ self.headers: dict[str, str] = {}
+
+ def json(self) -> dict:
+ return self._json_data
+
+
+def _make_image(color: str = "black") -> Image.Image:
+ return Image.new("RGB", (32, 32), color=color)
+
+
+def _build_model(provider_id: str, provider_model_id: str) -> ExternalApiModelConfig:
+ return ExternalApiModelConfig(
+ key=f"{provider_id}_test",
+ name=f"{provider_id.title()} Test",
+ provider_id=provider_id,
+ provider_model_id=provider_model_id,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=True,
+ supports_reference_images=True,
+ supports_seed=True,
+ supports_guidance=True,
+ ),
+ )
+
+
+def _build_request(
+ model: ExternalApiModelConfig,
+ mode: str = "txt2img",
+ init_image: Image.Image | None = None,
+ mask_image: Image.Image | None = None,
+ reference_images: list[ExternalReferenceImage] | None = None,
+) -> ExternalGenerationRequest:
+ return ExternalGenerationRequest(
+ model=model,
+ mode=mode, # type: ignore[arg-type]
+ prompt="A test prompt",
+ negative_prompt="",
+ seed=123,
+ num_images=1,
+ width=256,
+ height=256,
+ steps=20,
+ guidance=5.5,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images or [],
+ metadata=None,
+ )
+
+
+def test_gemini_generate_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ api_key = "gemini-key"
+ config = InvokeAIAppConfig(external_gemini_api_key=api_key)
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ init_image = _make_image("blue")
+ ref_image = _make_image("red")
+ request = _build_request(
+ model,
+ init_image=init_image,
+ reference_images=[ExternalReferenceImage(image=ref_image, weight=0.6)],
+ )
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["params"] = params
+ captured["json"] = json
+ captured["timeout"] = timeout
+ return DummyResponse(
+ ok=True,
+ json_data={
+ "candidates": [
+ {"content": {"parts": [{"inlineData": {"data": encoded}}]}},
+ ]
+ },
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert (
+ captured["url"]
+ == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent"
+ )
+ assert captured["params"] == {"key": api_key}
+ payload = captured["json"]
+ assert isinstance(payload, dict)
+ system_instruction = payload.get("systemInstruction")
+ assert isinstance(system_instruction, dict)
+ system_parts = system_instruction.get("parts")
+ assert isinstance(system_parts, list)
+ system_text = str(system_parts[0]).lower()
+ assert "image" in system_text
+ generation_config = payload.get("generationConfig")
+ assert isinstance(generation_config, dict)
+ assert generation_config["candidateCount"] == 1
+ assert generation_config["responseModalities"] == ["IMAGE"]
+ contents = payload.get("contents")
+ assert isinstance(contents, list)
+ first_content = contents[0]
+ assert isinstance(first_content, dict)
+ parts = first_content.get("parts")
+ assert isinstance(parts, list)
+ assert len(parts) >= 3
+ part0 = parts[0]
+ part1 = parts[1]
+ part2 = parts[2]
+ assert isinstance(part0, dict)
+ assert isinstance(part1, dict)
+ assert isinstance(part2, dict)
+ inline0 = part0.get("inlineData")
+ assert isinstance(inline0, dict)
+ assert part1["text"] == request.prompt
+ inline1 = part2.get("inlineData")
+ assert isinstance(inline1, dict)
+ assert inline0["data"] == encode_image_base64(init_image)
+ assert inline1["data"] == encode_image_base64(ref_image)
+ assert result.images[0].seed == request.seed
+ assert result.provider_metadata == {"model": request.model.provider_model_id}
+
+
+def test_gemini_generate_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_gemini_api_key="gemini-key")
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(ok=False, status_code=400, text="bad request")
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ with pytest.raises(ExternalProviderRequestError, match="Gemini request failed"):
+ provider.generate(request)
+
+
+def test_gemini_generate_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_gemini_api_key="gemini-key",
+ external_gemini_base_url="https://proxy.gemini",
+ )
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.gemini/v1beta/models/gemini-2.5-flash-image:generateContent"
+
+
+def test_gemini_generate_keeps_base_url_version(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_gemini_api_key="gemini-key",
+ external_gemini_base_url="https://proxy.gemini/v1",
+ )
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.gemini/v1/models/gemini-2.5-flash-image:generateContent"
+
+
+def test_gemini_generate_strips_models_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_gemini_api_key="gemini-key")
+ provider = GeminiProvider(config, logging.getLogger("test"))
+ model = _build_model("gemini", "models/gemini-2.5-flash-image")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("green"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, params: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(
+ ok=True,
+ json_data={"candidates": [{"content": {"parts": [{"inlineData": {"data": encoded}}]}}]},
+ )
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert (
+ captured["url"]
+ == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent"
+ )
+
+
+def test_openai_generate_txt2img_success(monkeypatch: pytest.MonkeyPatch) -> None:
+ api_key = "openai-key"
+ config = InvokeAIAppConfig(external_openai_api_key=api_key)
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("purple"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["headers"] = headers
+ captured["json"] = json
+ response = DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+ response.headers["x-request-id"] = "req-123"
+ return response
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/generations"
+ headers = captured["headers"]
+ assert isinstance(headers, dict)
+ assert headers["Authorization"] == f"Bearer {api_key}"
+ json_payload = captured["json"]
+ assert isinstance(json_payload, dict)
+ assert json_payload["prompt"] == request.prompt
+ assert result.provider_request_id == "req-123"
+ assert result.images[0].seed == request.seed
+ assert decode_image_base64(encoded).size == result.images[0].image.size
+
+
+def test_openai_generate_uses_base_url(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(
+ external_openai_api_key="openai-key",
+ external_openai_base_url="https://proxy.openai/",
+ )
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+ encoded = encode_image_base64(_make_image("purple"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ provider.generate(request)
+
+ assert captured["url"] == "https://proxy.openai/v1/images/generations"
+
+
+def test_openai_generate_txt2img_error_response(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(model)
+
+ def fake_post(url: str, headers: dict, json: dict, timeout: int) -> DummyResponse:
+ return DummyResponse(ok=False, status_code=500, text="server error")
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ with pytest.raises(ExternalProviderRequestError, match="OpenAI request failed"):
+ provider.generate(request)
+
+
+def test_openai_generate_inpaint_uses_edit_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(
+ model,
+ mode="inpaint",
+ init_image=_make_image("white"),
+ mask_image=_make_image("black"),
+ )
+ encoded = encode_image_base64(_make_image("orange"))
+ captured: dict[str, object] = {}
+
+ def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) -> DummyResponse:
+ captured["url"] = url
+ captured["data"] = data
+ captured["files"] = files
+ response = DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+ return response
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/edits"
+ data_payload = captured["data"]
+ assert isinstance(data_payload, dict)
+ assert data_payload["prompt"] == request.prompt
+ files = captured["files"]
+ assert isinstance(files, dict)
+ assert "image" in files
+ assert "mask" in files
+ image_tuple = files["image"]
+ assert isinstance(image_tuple, tuple)
+ assert image_tuple[0] == "image.png"
+ assert isinstance(image_tuple[1], io.BytesIO)
+ assert result.images
diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py
index d19eb95a8c2..c3d5d18e06c 100644
--- a/tests/app/services/model_install/test_model_install.py
+++ b/tests/app/services/model_install/test_model_install.py
@@ -33,6 +33,7 @@
URLModelSource,
)
from invokeai.app.services.model_records import ModelRecordChanges, UnknownModelException
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ModelFormat,
@@ -213,6 +214,21 @@ def test_inplace_install(
assert Path(job.config_out.path).exists()
+def test_external_install(mm2_installer: ModelInstallServiceBase) -> None:
+ config = ModelRecordChanges(name="ChatGPT Image", description="External model", key="chatgpt_image")
+ job = mm2_installer.heuristic_import("external://openai/gpt-image-1", config=config)
+
+ mm2_installer.wait_for_installs()
+
+ assert job.status == InstallStatus.COMPLETED
+ assert job.config_out is not None
+ assert isinstance(job.config_out, ExternalApiModelConfig)
+ assert job.config_out.provider_id == "openai"
+ assert job.config_out.provider_model_id == "gpt-image-1"
+ assert job.config_out.base == BaseModelType.External
+ assert job.config_out.type == ModelType.ExternalImageGenerator
+
+
def test_delete_install(
mm2_installer: ModelInstallServiceBase, embedding_file: Path, mm2_app_config: InvokeAIAppConfig
) -> None:
diff --git a/tests/app/services/model_load/test_load_api.py b/tests/app/services/model_load/test_load_api.py
index c0760cd3cad..8f7f8449723 100644
--- a/tests/app/services/model_load/test_load_api.py
+++ b/tests/app/services/model_load/test_load_api.py
@@ -4,6 +4,7 @@
import torch
from diffusers import AutoencoderTiny
+from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.model_manager import ModelManagerServiceBase
from invokeai.app.services.shared.invocation_context import (
@@ -11,6 +12,7 @@
InvocationContextData,
build_invocation_context,
)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalModelCapabilities
from invokeai.backend.model_manager.load.load_base import LoadedModelWithoutConfig
from tests.backend.model_manager.model_manager_fixtures import * # noqa F403
@@ -78,6 +80,27 @@ def test_download_and_load(mock_context: InvocationContext) -> None:
assert loaded_model_1.model is loaded_model_2.model # should be cached copy
+def test_external_model_load_raises(
+ mock_context: InvocationContext, mm2_model_manager: ModelManagerServiceBase
+) -> None:
+ config = ExternalApiModelConfig(
+ key="external_test",
+ name="External Test",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=ExternalModelCapabilities(modes=["txt2img"]),
+ )
+ mm2_model_manager.store.add_model(config)
+
+ model_field = ModelIdentifierField.from_config(config)
+
+ with pytest.raises(ValueError, match="External API models"):
+ mock_context.models.load(model_field)
+
+ with pytest.raises(ValueError, match="External API models"):
+ mock_context.models.load_by_attrs(name=config.name, base=config.base, type=config.type)
+
+
def test_download_diffusers(mock_context: InvocationContext) -> None:
model_path = mock_context.models.download_and_cache_model("stabilityai/sdxl-turbo")
assert (model_path / "model_index.json").exists()
diff --git a/tests/backend/model_manager/test_external_api_config.py b/tests/backend/model_manager/test_external_api_config.py
new file mode 100644
index 00000000000..943a5a79918
--- /dev/null
+++ b/tests/backend/model_manager/test_external_api_config.py
@@ -0,0 +1,54 @@
+import pytest
+from pydantic import ValidationError
+
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalImageSize,
+ ExternalModelCapabilities,
+)
+
+
+def test_external_api_model_config_defaults() -> None:
+ capabilities = ExternalModelCapabilities(modes=["txt2img"], supports_seed=True)
+
+ config = ExternalApiModelConfig(
+ name="Test External",
+ provider_id="openai",
+ provider_model_id="gpt-image-1",
+ capabilities=capabilities,
+ )
+
+ assert config.path == "external://openai/gpt-image-1"
+ assert config.source == "external://openai/gpt-image-1"
+ assert config.hash == "external:openai:gpt-image-1"
+ assert config.file_size == 0
+ assert config.default_settings is None
+ assert config.capabilities.supports_seed is True
+
+
+def test_external_api_model_capabilities_allows_aspect_ratio_sizes() -> None:
+ capabilities = ExternalModelCapabilities(
+ modes=["txt2img"],
+ allowed_aspect_ratios=["1:1"],
+ aspect_ratio_sizes={"1:1": ExternalImageSize(width=1024, height=1024)},
+ )
+
+ assert capabilities.aspect_ratio_sizes is not None
+ assert capabilities.aspect_ratio_sizes["1:1"].width == 1024
+
+
+def test_external_api_model_config_rejects_extra_fields() -> None:
+ with pytest.raises(ValidationError):
+ ExternalModelCapabilities(modes=["txt2img"], supports_seed=True, extra_field=True) # type: ignore
+
+ with pytest.raises(ValidationError):
+ ExternalApiModelDefaultSettings(width=512, extra_field=True) # type: ignore
+
+
+def test_external_api_model_config_validates_limits() -> None:
+ with pytest.raises(ValidationError):
+ ExternalModelCapabilities(modes=["txt2img"], max_images_per_request=0)
+
+ with pytest.raises(ValidationError):
+ ExternalApiModelDefaultSettings(width=0)
diff --git a/tests/conftest.py b/tests/conftest.py
index 980a99611ab..bfd9f070df4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,6 +17,7 @@
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
from invokeai.app.services.images.images_default import ImageService
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_services import InvocationServices
@@ -52,6 +53,7 @@ def mock_services() -> InvocationServices:
model_images=None, # type: ignore
model_manager=None, # type: ignore
download_queue=None, # type: ignore
+ external_generation=ExternalGenerationService({}, logger),
names=None, # type: ignore
performance_statistics=InvocationStatsService(),
session_processor=None, # type: ignore
From 74ecc461b979fb9c212586a307f2c164b4573e15 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:13:50 +0100
Subject: [PATCH 06/50] feat: support reference images for external models
---
.../external_generation_default.py | 35 ++++++++++-
.../external_generation/providers/openai.py | 31 ++++++---
.../backend/model_manager/starter_models.py | 2 +-
.../components/RefImage/RefImagePreview.tsx | 12 ++--
.../components/RefImage/RefImageSettings.tsx | 13 ++--
.../controlLayers/store/validators.ts | 10 ++-
.../test_external_generation_service.py | 23 +++++++
.../test_external_provider_adapters.py | 63 +++++++++++++++++--
8 files changed, 160 insertions(+), 29 deletions(-)
diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py
index c72e16cde8d..c96b5af711e 100644
--- a/invokeai/app/services/external_generation/external_generation_default.py
+++ b/invokeai/app/services/external_generation/external_generation_default.py
@@ -15,6 +15,7 @@
ExternalProvider,
)
from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
ExternalGenerationRequest,
ExternalGenerationResult,
ExternalProviderStatus,
@@ -37,10 +38,17 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
raise ExternalProviderNotConfiguredError(f"Provider '{request.model.provider_id}' is missing credentials")
request = self._refresh_model_capabilities(request)
+ resize_to_original_inpaint_size = _get_resize_target_for_inpaint(request)
request = self._bucket_request(request)
self._validate_request(request)
- return provider.generate(request)
+ result = provider.generate(request)
+
+ if resize_to_original_inpaint_size is None:
+ return result
+
+ width, height = resize_to_original_inpaint_size
+ return _resize_result_images(result, width, height)
def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()}
@@ -276,6 +284,31 @@ def _resize_image(image: PILImageType | None, width: int, height: int, mode: str
return image.convert(mode).resize((width, height), Image.Resampling.LANCZOS)
+def _get_resize_target_for_inpaint(request: ExternalGenerationRequest) -> tuple[int, int] | None:
+ if request.mode != "inpaint" or request.init_image is None:
+ return None
+ return request.init_image.width, request.init_image.height
+
+
+def _resize_result_images(result: ExternalGenerationResult, width: int, height: int) -> ExternalGenerationResult:
+ resized_images = [
+ ExternalGeneratedImage(
+ image=generated.image
+ if generated.image.width == width and generated.image.height == height
+ else generated.image.resize((width, height), Image.Resampling.LANCZOS),
+ seed=generated.seed,
+ )
+ for generated in result.images
+ ]
+ return ExternalGenerationResult(
+ images=resized_images,
+ seed_used=result.seed_used,
+ provider_request_id=result.provider_request_id,
+ provider_metadata=result.provider_metadata,
+ content_filters=result.content_filters,
+ )
+
+
def _apply_starter_overrides(model: ExternalApiModelConfig) -> ExternalApiModelConfig:
source = model.source or f"external://{model.provider_id}/{model.provider_model_id}"
starter_match = next((starter for starter in STARTER_MODELS if starter.source == source), None)
diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py
index e31a493b7a1..f06491a225b 100644
--- a/invokeai/app/services/external_generation/providers/openai.py
+++ b/invokeai/app/services/external_generation/providers/openai.py
@@ -3,6 +3,7 @@
import io
import requests
+from PIL.Image import Image as PILImageType
from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
@@ -29,7 +30,9 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/")
headers = {"Authorization": f"Bearer {api_key}"}
- if request.mode == "txt2img":
+ use_edits_endpoint = request.mode != "txt2img" or bool(request.reference_images)
+
+ if not use_edits_endpoint:
payload: dict[str, object] = {
"prompt": request.prompt,
"n": request.num_images,
@@ -45,20 +48,28 @@ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResu
timeout=120,
)
else:
- files: dict[str, tuple[str, io.BytesIO, str]] = {}
- if request.init_image is None:
- raise ExternalProviderRequestError("OpenAI img2img/inpaint requires an init image")
-
- image_buffer = io.BytesIO()
- request.init_image.save(image_buffer, format="PNG")
- image_buffer.seek(0)
- files["image"] = ("image.png", image_buffer, "image/png")
+ images: list[PILImageType] = []
+ if request.init_image is not None:
+ images.append(request.init_image)
+ images.extend(reference.image for reference in request.reference_images)
+ if not images:
+ raise ExternalProviderRequestError(
+ "OpenAI image edits require at least one image (init image or reference image)"
+ )
+
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]] = []
+ image_field_name = "image" if len(images) == 1 else "image[]"
+ for index, image in enumerate(images):
+ image_buffer = io.BytesIO()
+ image.save(image_buffer, format="PNG")
+ image_buffer.seek(0)
+ files.append((image_field_name, (f"image_{index}.png", image_buffer, "image/png")))
if request.mask_image is not None:
mask_buffer = io.BytesIO()
request.mask_image.save(mask_buffer, format="PNG")
mask_buffer.seek(0)
- files["mask"] = ("mask.png", mask_buffer, "image/png")
+ files.append(("mask", ("mask.png", mask_buffer, "image/png")))
data: dict[str, object] = {
"prompt": request.prompt,
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index 59d7ceba205..183edc04ba7 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -964,7 +964,7 @@ class StarterModelBundle(BaseModel):
supports_negative_prompt=True,
supports_seed=True,
supports_guidance=True,
- supports_reference_images=False,
+ supports_reference_images=True,
max_images_per_request=1,
),
default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1),
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
index 84c1b2fc37b..ddbdb8b131c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
@@ -15,6 +15,7 @@ import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/va
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { PiExclamationMarkBold, PiEyeSlashBold, PiImageBold } from 'react-icons/pi';
import { useImageDTOFromCroppableImage } from 'services/api/endpoints/images';
+import { isExternalApiModelConfig } from 'services/api/types';
import { RefImageWarningTooltipContent } from './RefImageWarningTooltipContent';
@@ -71,18 +72,19 @@ export const RefImagePreview = memo(() => {
const selectedEntityId = useAppSelector(selectSelectedRefEntityId);
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
+ const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
const imageDTO = useImageDTOFromCroppableImage(entity.config.image);
const sx = useMemo(() => {
- if (!isIPAdapterConfig(entity.config)) {
+ if (!isIPAdapterConfig(entity.config) || isExternalModel) {
return baseSx;
}
return getImageSxWithWeight(entity.config.weight);
- }, [entity.config]);
+ }, [entity.config, isExternalModel]);
useEffect(() => {
- if (!isIPAdapterConfig(entity.config)) {
+ if (!isIPAdapterConfig(entity.config) || isExternalModel) {
return;
}
setShowWeightDisplay(true);
@@ -92,7 +94,7 @@ export const RefImagePreview = memo(() => {
return () => {
window.clearTimeout(timeout);
};
- }, [entity.config]);
+ }, [entity.config, isExternalModel]);
const warnings = useMemo(() => {
return getGlobalReferenceImageWarnings(entity, mainModelConfig);
@@ -154,7 +156,7 @@ export const RefImagePreview = memo(() => {
) : (
)}
- {isIPAdapterConfig(entity.config) && (
+ {isIPAdapterConfig(entity.config) && !isExternalModel && (
{
const selectConfig = useMemo(() => buildSelectConfig(id), [id]);
const config = useAppSelector(selectConfig);
const tab = useAppSelector(selectActiveTab);
+ const mainModelConfig = useAppSelector(selectMainModelConfig);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
@@ -120,9 +122,10 @@ const RefImageSettingsContent = memo(() => {
);
const isFLUX = useAppSelector(selectIsFLUX);
+ const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
- // FLUX.2 Klein has built-in reference image support - no model selector needed
- const showModelSelector = !isFlux2ReferenceImageConfig(config);
+ // FLUX.2 Klein and external API models do not require a ref image model selection.
+ const showModelSelector = !isFlux2ReferenceImageConfig(config) && !isExternalModel;
return (
@@ -150,14 +153,14 @@ const RefImageSettingsContent = memo(() => {
)}
- {isIPAdapterConfig(config) && (
+ {isIPAdapterConfig(config) && !isExternalModel && (
{!isFLUX && }
)}
- {isFLUXReduxConfig(config) && (
+ {isFLUXReduxConfig(config) && !isExternalModel && (
None:
assert response is result
assert provider.last_request == request
+
+
+def test_generate_resizes_inpaint_result_to_original_init_size() -> None:
+ model = _build_model(ExternalModelCapabilities(modes=["inpaint"]))
+ request = _build_request(
+ model=model,
+ mode="inpaint",
+ width=128,
+ height=128,
+ init_image=_make_image(),
+ mask_image=_make_image(),
+ )
+ generated_large = Image.new("RGB", (128, 128), color="black")
+ result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=generated_large, seed=1)])
+ provider = DummyProvider("openai", configured=True, result=result)
+ service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
+
+ response = service.generate(request)
+
+ assert request.init_image is not None
+ assert response.images[0].image.width == request.init_image.width
+ assert response.images[0].image.height == request.init_image.height
+ assert response.images[0].seed == 1
diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py
index 38f4c9e3d52..c4da4c913be 100644
--- a/tests/app/services/external_generation/test_external_provider_adapters.py
+++ b/tests/app/services/external_generation/test_external_provider_adapters.py
@@ -320,7 +320,13 @@ def test_openai_generate_inpaint_uses_edit_endpoint(monkeypatch: pytest.MonkeyPa
encoded = encode_image_base64(_make_image("orange"))
captured: dict[str, object] = {}
- def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) -> DummyResponse:
+ def fake_post(
+ url: str,
+ headers: dict,
+ data: dict,
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]],
+ timeout: int,
+ ) -> DummyResponse:
captured["url"] = url
captured["data"] = data
captured["files"] = files
@@ -336,11 +342,56 @@ def fake_post(url: str, headers: dict, data: dict, files: dict, timeout: int) ->
assert isinstance(data_payload, dict)
assert data_payload["prompt"] == request.prompt
files = captured["files"]
- assert isinstance(files, dict)
- assert "image" in files
- assert "mask" in files
- image_tuple = files["image"]
+ assert isinstance(files, list)
+ image_file = next((file for file in files if file[0] == "image"), None)
+ mask_file = next((file for file in files if file[0] == "mask"), None)
+ assert image_file is not None
+ assert mask_file is not None
+ image_tuple = image_file[1]
assert isinstance(image_tuple, tuple)
- assert image_tuple[0] == "image.png"
+ assert image_tuple[0] == "image_0.png"
assert isinstance(image_tuple[1], io.BytesIO)
assert result.images
+
+
+def test_openai_generate_txt2img_with_references_uses_edit_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
+ config = InvokeAIAppConfig(external_openai_api_key="openai-key")
+ provider = OpenAIProvider(config, logging.getLogger("test"))
+ model = _build_model("openai", "gpt-image-1")
+ request = _build_request(
+ model,
+ reference_images=[
+ ExternalReferenceImage(image=_make_image("red")),
+ ExternalReferenceImage(image=_make_image("blue")),
+ ],
+ )
+ encoded = encode_image_base64(_make_image("orange"))
+ captured: dict[str, object] = {}
+
+ def fake_post(
+ url: str,
+ headers: dict,
+ data: dict,
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]],
+ timeout: int,
+ ) -> DummyResponse:
+ captured["url"] = url
+ captured["data"] = data
+ captured["files"] = files
+ return DummyResponse(ok=True, json_data={"data": [{"b64_json": encoded}]})
+
+ monkeypatch.setattr("requests.post", fake_post)
+
+ result = provider.generate(request)
+
+ assert captured["url"] == "https://api.openai.com/v1/images/edits"
+ data_payload = captured["data"]
+ assert isinstance(data_payload, dict)
+ assert data_payload["prompt"] == request.prompt
+ files = captured["files"]
+ assert isinstance(files, list)
+ image_files = [file for file in files if file[0] == "image[]"]
+ assert len(image_files) == 2
+ assert image_files[0][1][0] == "image_0.png"
+ assert image_files[1][1][0] == "image_1.png"
+ assert result.images
From a9d3b4e17c2440c89b629d1089872fe3ed0edb61 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:23:16 +0100
Subject: [PATCH 07/50] fix: sorting lint error
---
.../features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index c54523e0fad..a35d865e752 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -11,10 +11,10 @@ import { filesize } from 'filesize';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
- isExternalApiModelConfig,
type AnyModelConfig,
type CLIPEmbedModelConfig,
type CLIPVisionModelConfig,
+ isExternalApiModelConfig,
type LlavaOnevisionModelConfig,
type Qwen3EncoderModelConfig,
type SigLIPModelConfig,
From 1b43769b951b1244604d391df198ce04e5b5a238 Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Fri, 27 Feb 2026 09:12:35 +0100
Subject: [PATCH 08/50] chore: hide Reidentify button for external models
---
.../subpanels/ModelPanel/ModelReidentifyButton.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
index 31334c0510d..8ea5df310a7 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
@@ -4,7 +4,9 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSparkleFill } from 'react-icons/pi';
import { useReidentifyModelMutation } from 'services/api/endpoints/models';
-import type { AnyModelConfig } from 'services/api/types';
+import { type AnyModelConfig, isExternalApiModelConfig } from 'services/api/types';
+
+import { isExternalModel } from './isExternalModel';
interface Props {
modelConfig: AnyModelConfig;
@@ -13,6 +15,7 @@ interface Props {
export const ModelReidentifyButton = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
const [reidentifyModel, { isLoading }] = useReidentifyModelMutation();
+ const isExternal = isExternalApiModelConfig(modelConfig) || isExternalModel(modelConfig.path);
const onClick = useCallback(() => {
reidentifyModel({ key: modelConfig.key })
@@ -40,6 +43,10 @@ export const ModelReidentifyButton = memo(({ modelConfig }: Props) => {
});
}, [modelConfig.key, reidentifyModel, t]);
+ if (isExternal) {
+ return null;
+ }
+
return (
-
- {t('modelManager.supportsNegativePrompt')}
-
-
{t('modelManager.supportsReferenceImages')}
@@ -253,10 +249,6 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
{t('modelManager.supportsSeed')}
-
- {t('modelManager.supportsGuidance')}
-
-
{t('modelManager.maxImagesPerRequest')}
= {}): ExternalApiModelConfig => {
const maxImageSize: ExternalImageSize = { width: 1024, height: 1024 };
- const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024, steps: 30 };
+ const defaultSettings: ExternalApiModelDefaultSettings = { width: 1024, height: 1024 };
const capabilities: ExternalModelCapabilities = {
modes: ['txt2img'],
- supports_negative_prompt: true,
supports_reference_images: true,
supports_seed: true,
- supports_guidance: true,
- supports_steps: true,
max_image_size: maxImageSize,
};
@@ -54,7 +51,6 @@ const createExternalModel = (overrides: Partial = {}): E
let mockModelConfig: ExternalApiModelConfig | null = null;
let mockParams: ParamsState;
let mockRefImages: RefImagesState;
-let mockPrompts: { positive: string; negative: string };
let mockSizes: { scaledSize: { width: number; height: number } };
const mockOutputFields = {
@@ -80,7 +76,6 @@ vi.mock('features/nodes/util/graph/graphBuilderUtils', () => ({
rect: { x: 0, y: 0, width: 512, height: 512 },
}),
selectCanvasOutputFields: () => mockOutputFields,
- selectPresetModifiedPrompts: () => mockPrompts,
}));
beforeEach(() => {
@@ -88,7 +83,6 @@ beforeEach(() => {
steps: 20,
guidance: 4.5,
} as ParamsState;
- mockPrompts = { positive: 'a test prompt', negative: 'bad prompt' };
mockSizes = { scaledSize: { width: 768, height: 512 } };
const imageDTO = { image_name: 'ref.png', width: 64, height: 64 } as ImageDTO;
@@ -129,44 +123,14 @@ describe('buildExternalGraph', () => {
expect(externalNode?.mode).toBe('txt2img');
expect(externalNode?.width).toBe(768);
expect(externalNode?.height).toBe(512);
- expect(externalNode?.negative_prompt).toBe('bad prompt');
- expect(externalNode?.steps).toBe(20);
- expect(externalNode?.guidance).toBe(4.5);
expect((externalNode?.reference_images as Array<{ image_name: string }> | undefined)?.[0]).toEqual({
image_name: 'ref.png',
});
- expect(externalNode?.reference_image_weights).toEqual([0.5]);
const seedEdge = graph.edges.find((edge) => edge.destination.field === 'seed');
expect(seedEdge).toBeDefined();
});
- it('does not include steps when model does not support them', async () => {
- const modelConfig = createExternalModel({
- capabilities: {
- modes: ['txt2img'],
- supports_negative_prompt: true,
- supports_reference_images: true,
- supports_seed: true,
- supports_guidance: true,
- supports_steps: false,
- },
- });
- mockModelConfig = modelConfig;
-
- const { g } = await buildExternalGraph({
- generationMode: 'txt2img',
- state: {} as RootState,
- manager: null,
- });
- const graph = g.getGraph();
- const externalNode = Object.values(graph.nodes).find((node) => node.type === 'openai_image_generation') as
- | Record
- | undefined;
-
- expect(externalNode?.steps).toBeNull();
- });
-
it('prefers panel schema over capabilities when building node inputs', async () => {
const panelSchema: ExternalModelPanelSchema = {
prompts: [{ name: 'reference_images' }],
@@ -187,9 +151,6 @@ describe('buildExternalGraph', () => {
| Record
| undefined;
- expect(externalNode?.negative_prompt).toBeNull();
- expect(externalNode?.steps).toBeNull();
- expect(externalNode?.guidance).toBeNull();
expect((externalNode?.reference_images as Array<{ image_name: string }> | undefined)?.[0]).toEqual({
image_name: 'ref.png',
});
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
index 4f686a59868..0ba82234a66 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
@@ -8,7 +8,6 @@ import {
getOriginalAndScaledSizesForOtherModes,
getOriginalAndScaledSizesForTextToImage,
selectCanvasOutputFields,
- selectPresetModifiedPrompts,
} from 'features/nodes/util/graph/graphBuilderUtils';
import {
type GraphBuilderArg,
@@ -44,13 +43,9 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise config.image)
.map((config) => zImageField.parse(config.image?.crop?.image ?? config.image?.original.image));
- const referenceWeights = refImages.entities
- .filter((entity) => entity.isEnabled)
- .map((entity) => entity.config)
- .filter((config) => config.image)
- .map((config) => (config.type === 'ip_adapter' ? config.weight : null));
-
if (referenceImages.length > 0) {
externalNode.reference_images = referenceImages;
- if (referenceWeights.every((weight): weight is number => weight !== null)) {
- externalNode.reference_image_weights = referenceWeights;
- }
}
}
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
index 1ae1dcdc3a8..8fb4e81b59d 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.test.tsx
@@ -22,9 +22,7 @@ const createExternalModel = (overrides: Partial = {}): E
capabilities: {
modes: ['txt2img'],
supports_reference_images: false,
- supports_negative_prompt: true,
supports_seed: true,
- supports_guidance: true,
max_images_per_request: 1,
max_image_size: null,
allowed_aspect_ratios: ['1:1', '16:9'],
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx
index 80e9d188b30..62fc5aa826c 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx
@@ -1,8 +1,8 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
-import { selectGuidance, selectGuidanceControl, setGuidance } from 'features/controlLayers/store/paramsSlice';
-import { memo, useCallback, useMemo } from 'react';
+import { selectGuidance, setGuidance } from 'features/controlLayers/store/paramsSlice';
+import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CONSTRAINTS = {
@@ -23,22 +23,10 @@ export const MARKS = [
const ParamGuidance = () => {
const guidance = useAppSelector(selectGuidance);
- const externalControl = useAppSelector(selectGuidanceControl);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const onChange = useCallback((v: number) => dispatch(setGuidance(v)), [dispatch]);
- const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin;
- const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax;
- const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin;
- const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax;
- const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep;
- const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep;
- const marks = useMemo(
- () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax - (sliderMax - sliderMin) / 2), sliderMax],
- [externalControl?.marks, sliderMin, sliderMax]
- );
-
return (
@@ -47,20 +35,20 @@ const ParamGuidance = () => {
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx
index b6d810dd2e2..31efe5d0a6f 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx
@@ -1,8 +1,8 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
-import { selectSteps, selectStepsControl, setSteps } from 'features/controlLayers/store/paramsSlice';
-import { memo, useCallback, useMemo } from 'react';
+import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice';
+import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CONSTRAINTS = {
@@ -19,7 +19,6 @@ export const MARKS = [CONSTRAINTS.sliderMin, Math.floor(CONSTRAINTS.sliderMax /
const ParamSteps = () => {
const steps = useAppSelector(selectSteps);
- const externalControl = useAppSelector(selectStepsControl);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const onChange = useCallback(
@@ -29,17 +28,6 @@ const ParamSteps = () => {
[dispatch]
);
- const sliderMin = externalControl?.slider_min ?? CONSTRAINTS.sliderMin;
- const sliderMax = externalControl?.slider_max ?? CONSTRAINTS.sliderMax;
- const numberInputMin = externalControl?.number_input_min ?? CONSTRAINTS.numberInputMin;
- const numberInputMax = externalControl?.number_input_max ?? CONSTRAINTS.numberInputMax;
- const fineStep = externalControl?.fine_step ?? CONSTRAINTS.fineStep;
- const coarseStep = externalControl?.coarse_step ?? CONSTRAINTS.coarseStep;
- const marks = useMemo(
- () => externalControl?.marks ?? [sliderMin, Math.floor(sliderMax / 2), sliderMax],
- [externalControl?.marks, sliderMin, sliderMax]
- );
-
return (
@@ -48,20 +36,20 @@ const ParamSteps = () => {
diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
index 636260d1d25..0651c47863f 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.test.tsx
@@ -22,9 +22,7 @@ const createExternalModel = (overrides: Partial = {}): E
capabilities: {
modes: ['txt2img'],
supports_reference_images: false,
- supports_negative_prompt: true,
supports_seed: true,
- supports_guidance: true,
max_images_per_request: 1,
max_image_size: null,
allowed_aspect_ratios: ['1:1', '16:9'],
diff --git a/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx
new file mode 100644
index 00000000000..1ec27ef809d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/External/GeminiProviderOptions.tsx
@@ -0,0 +1,74 @@
+import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import {
+ geminiTemperatureChanged,
+ geminiThinkingLevelChanged,
+ selectGeminiTemperature,
+ selectGeminiThinkingLevel,
+} from 'features/controlLayers/store/paramsSlice';
+import type { ChangeEventHandler } from 'react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCaretDownBold } from 'react-icons/pi';
+
+const TEMPERATURE_MARKS = [0, 1, 2];
+
+export const GeminiProviderOptions = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const temperature = useAppSelector(selectGeminiTemperature);
+ const thinkingLevel = useAppSelector(selectGeminiThinkingLevel);
+
+ const onTemperatureChange = useCallback((v: number) => dispatch(geminiTemperatureChanged(v)), [dispatch]);
+
+ const onThinkingLevelChange = useCallback>(
+ (e) => {
+ const value = e.target.value;
+ dispatch(geminiThinkingLevelChanged(value === '' ? null : (value as 'minimal' | 'high')));
+ },
+ [dispatch]
+ );
+
+ return (
+ <>
+
+ {t('parameters.temperature', 'Temperature')}
+
+
+
+
+ {t('parameters.thinkingLevel', 'Thinking Level')}
+ }
+ iconSize="0.75rem"
+ >
+
+
+
+
+
+ >
+ );
+});
+
+GeminiProviderOptions.displayName = 'GeminiProviderOptions';
diff --git a/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx
new file mode 100644
index 00000000000..f4eba05d84b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/External/OpenAIProviderOptions.tsx
@@ -0,0 +1,84 @@
+import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import {
+ openaiBackgroundChanged,
+ openaiInputFidelityChanged,
+ openaiQualityChanged,
+ selectOpenaiBackground,
+ selectOpenaiInputFidelity,
+ selectOpenaiQuality,
+} from 'features/controlLayers/store/paramsSlice';
+import type { ChangeEventHandler } from 'react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCaretDownBold } from 'react-icons/pi';
+
+export const OpenAIProviderOptions = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const quality = useAppSelector(selectOpenaiQuality);
+ const background = useAppSelector(selectOpenaiBackground);
+ const inputFidelity = useAppSelector(selectOpenaiInputFidelity);
+
+ const onQualityChange = useCallback>(
+ (e) => dispatch(openaiQualityChanged(e.target.value as 'auto' | 'high' | 'medium' | 'low')),
+ [dispatch]
+ );
+
+ const onBackgroundChange = useCallback>(
+ (e) => dispatch(openaiBackgroundChanged(e.target.value as 'auto' | 'transparent' | 'opaque')),
+ [dispatch]
+ );
+
+ const onInputFidelityChange = useCallback>(
+ (e) => {
+ const value = e.target.value;
+ dispatch(openaiInputFidelityChanged(value === '' ? null : (value as 'low' | 'high')));
+ },
+ [dispatch]
+ );
+
+ return (
+ <>
+
+ {t('parameters.quality', 'Quality')}
+ } iconSize="0.75rem">
+
+
+
+
+
+
+
+ {t('parameters.background', 'Background')}
+ }
+ iconSize="0.75rem"
+ >
+
+
+
+
+
+
+ {t('parameters.inputFidelity', 'Input Fidelity')}
+ }
+ iconSize="0.75rem"
+ >
+
+
+
+
+
+ >
+ );
+});
+
+OpenAIProviderOptions.displayName = 'OpenAIProviderOptions';
diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
index b908efa096e..7d72b529c2c 100644
--- a/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
+++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/mainModelPickerUtils.test.ts
@@ -30,7 +30,6 @@ const createExternalConfig = (modes: ExternalModelCapabilities['modes']): Extern
provider_model_id: 'gpt-image-1',
capabilities: {
modes,
- supports_negative_prompt: true,
supports_reference_images: false,
max_image_size: maxImageSize,
},
diff --git a/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts b/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts
index 9c1296b1a54..c49190b225b 100644
--- a/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts
+++ b/invokeai/frontend/web/src/features/parameters/util/externalPanelSchema.ts
@@ -11,15 +11,9 @@ type ExternalPanelName = keyof ExternalModelPanelSchema;
const buildExternalPanelSchemaFromCapabilities = (
capabilities: ExternalModelCapabilities
): ExternalModelPanelSchema => ({
- prompts: [
- ...(capabilities.supports_negative_prompt ? [{ name: 'negative_prompt' as const }] : []),
- ...(capabilities.supports_reference_images ? [{ name: 'reference_images' as const }] : []),
- ],
+ prompts: [...(capabilities.supports_reference_images ? [{ name: 'reference_images' as const }] : [])],
image: [{ name: 'dimensions' }, ...(capabilities.supports_seed ? [{ name: 'seed' as const }] : [])],
- generation: [
- ...(capabilities.supports_steps ? [{ name: 'steps' as const }] : []),
- ...(capabilities.supports_guidance ? [{ name: 'guidance' as const }] : []),
- ],
+ generation: [],
});
const getExternalPanelSchema = (modelConfig: ExternalApiModelConfig): ExternalModelPanelSchema =>
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx
index deb3ea2c83f..2988acbd198 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ExternalSettingsAccordion/ExternalSettingsAccordion.tsx
@@ -1,9 +1,11 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectIsExternal } from 'features/controlLayers/store/paramsSlice';
+import { selectExternalProviderId, selectIsExternal } from 'features/controlLayers/store/paramsSlice';
import { ExternalModelImageSizeSelect } from 'features/parameters/components/Dimensions/ExternalModelImageSizeSelect';
import { ExternalModelResolutionSelect } from 'features/parameters/components/Dimensions/ExternalModelResolutionSelect';
+import { GeminiProviderOptions } from 'features/parameters/components/External/GeminiProviderOptions';
+import { OpenAIProviderOptions } from 'features/parameters/components/External/OpenAIProviderOptions';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,6 +17,7 @@ const formLabelProps: FormLabelProps = {
export const ExternalSettingsAccordion = memo(() => {
const { t } = useTranslation();
const isExternal = useAppSelector(selectIsExternal);
+ const providerId = useAppSelector(selectExternalProviderId);
const { isOpen, onToggle } = useStandaloneAccordionToggle({
id: 'external-settings',
defaultIsOpen: true,
@@ -35,6 +38,8 @@ export const ExternalSettingsAccordion = memo(() => {
+ {providerId === 'openai' && }
+ {providerId === 'gemini' && }
diff --git a/tests/app/invocations/test_external_image_generation.py b/tests/app/invocations/test_external_image_generation.py
index 34a2d921625..9cd2919d46d 100644
--- a/tests/app/invocations/test_external_image_generation.py
+++ b/tests/app/invocations/test_external_image_generation.py
@@ -27,7 +27,6 @@ def _build_model() -> ExternalApiModelConfig:
capabilities=ExternalModelCapabilities(
modes=["txt2img"],
supports_reference_images=True,
- supports_negative_prompt=True,
supports_seed=True,
),
)
@@ -57,47 +56,22 @@ def test_external_invocation_builds_request_and_outputs() -> None:
model=model_field,
mode="txt2img",
prompt="A prompt",
- negative_prompt="bad",
seed=123,
num_images=1,
width=512,
height=512,
- steps=10,
- guidance=4.5,
reference_images=[ImageField(image_name="ref.png")],
- reference_image_weights=[0.6],
)
output = invocation.invoke(context)
request = context._services.external_generation.generate.call_args[0][0]
assert request.prompt == "A prompt"
- assert request.negative_prompt == "bad"
assert request.seed == 123
assert len(request.reference_images) == 1
- assert request.reference_images[0].weight == 0.6
assert output.collection[0].image_name == "result.png"
-def test_external_invocation_rejects_mismatched_reference_weights() -> None:
- model_config = _build_model()
- model_field = ModelIdentifierField.from_config(model_config)
- generated_image = Image.new("RGB", (16, 16), color="black")
- context = _build_context(model_config, generated_image)
-
- invocation = ExternalImageGenerationInvocation(
- id="external_node",
- model=model_field,
- mode="txt2img",
- prompt="A prompt",
- reference_images=[ImageField(image_name="ref.png")],
- reference_image_weights=[0.1, 0.2],
- )
-
- with pytest.raises(ValueError, match="reference_image_weights"):
- invocation.invoke(context)
-
-
def test_provider_specific_external_invocation_rejects_wrong_provider() -> None:
model_config = _build_model().model_copy(update={"provider_id": "gemini"})
model_field = ModelIdentifierField.from_config(model_config)
diff --git a/tests/app/routers/test_model_manager.py b/tests/app/routers/test_model_manager.py
index 771790ee9ca..3260b539bb3 100644
--- a/tests/app/routers/test_model_manager.py
+++ b/tests/app/routers/test_model_manager.py
@@ -83,9 +83,9 @@ def test_model_manager_external_config_preserves_custom_panel_schema(
name="External Custom Schema",
provider_id="custom",
provider_model_id="custom-model",
- capabilities=ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True),
+ capabilities=ExternalModelCapabilities(modes=["txt2img"]),
panel_schema=ExternalModelPanelSchema(
- prompts=[{"name": "negative_prompt"}],
+ prompts=[{"name": "reference_images"}],
image=[{"name": "dimensions"}],
),
source="external://custom/custom-model",
@@ -104,7 +104,7 @@ def test_model_manager_external_config_preserves_custom_panel_schema(
assert response.status_code == 200
payload = response.json()
- assert [control["name"] for control in payload["panel_schema"]["prompts"]] == ["negative_prompt"]
+ assert [control["name"] for control in payload["panel_schema"]["prompts"]] == ["reference_images"]
assert [control["name"] for control in payload["panel_schema"]["image"]] == ["dimensions"]
@@ -118,10 +118,7 @@ def test_model_manager_external_starter_model_applies_panel_schema_overrides(
provider_model_id="gpt-image-1",
capabilities=ExternalModelCapabilities(
modes=["txt2img"],
- supports_negative_prompt=True,
supports_reference_images=False,
- supports_guidance=True,
- supports_steps=True,
),
)
mm2_model_manager.store.add_model(config)
diff --git a/tests/app/services/external_generation/test_external_generation_service.py b/tests/app/services/external_generation/test_external_generation_service.py
index 4ad0899c9c1..927f60b6eef 100644
--- a/tests/app/services/external_generation/test_external_generation_service.py
+++ b/tests/app/services/external_generation/test_external_generation_service.py
@@ -55,10 +55,8 @@ def _build_request(
*,
model: ExternalApiModelConfig,
mode: str = "txt2img",
- negative_prompt: str | None = None,
seed: int | None = None,
num_images: int = 1,
- guidance: float | None = None,
width: int = 64,
height: int = 64,
init_image: Image.Image | None = None,
@@ -69,14 +67,11 @@ def _build_request(
model=model,
mode=mode, # type: ignore[arg-type]
prompt="A test prompt",
- negative_prompt=negative_prompt,
seed=seed,
num_images=num_images,
width=width,
height=height,
image_size=None,
- steps=10,
- guidance=guidance,
init_image=init_image,
mask_image=mask_image,
reference_images=reference_images or [],
@@ -117,16 +112,6 @@ def test_generate_validates_mode_support() -> None:
service.generate(request)
-def test_generate_validates_negative_prompt_support() -> None:
- model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=False))
- request = _build_request(model=model, negative_prompt="bad")
- provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
- service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
-
- with pytest.raises(ExternalProviderCapabilityError, match="Negative prompts"):
- service.generate(request)
-
-
def test_generate_requires_init_image_for_img2img() -> None:
model = _build_model(ExternalModelCapabilities(modes=["img2img"]))
request = _build_request(model=model, mode="img2img")
@@ -151,7 +136,7 @@ def test_generate_validates_reference_images() -> None:
model = _build_model(ExternalModelCapabilities(modes=["txt2img"], supports_reference_images=False))
request = _build_request(
model=model,
- reference_images=[ExternalReferenceImage(image=_make_image(), weight=0.8)],
+ reference_images=[ExternalReferenceImage(image=_make_image())],
)
provider = DummyProvider("openai", configured=True, result=ExternalGenerationResult(images=[]))
service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
@@ -231,9 +216,9 @@ def test_generate_validates_allowed_aspect_ratios_with_bucket_sizes() -> None:
def test_generate_happy_path() -> None:
model = _build_model(
- ExternalModelCapabilities(modes=["txt2img"], supports_negative_prompt=True, supports_seed=True)
+ ExternalModelCapabilities(modes=["txt2img"], supports_seed=True)
)
- request = _build_request(model=model, negative_prompt="", seed=42)
+ request = _build_request(model=model, seed=42)
result = ExternalGenerationResult(images=[ExternalGeneratedImage(image=_make_image(), seed=42)])
provider = DummyProvider("openai", configured=True, result=result)
service = ExternalGenerationService({"openai": provider}, logging.getLogger("test"))
diff --git a/tests/app/services/external_generation/test_external_provider_adapters.py b/tests/app/services/external_generation/test_external_provider_adapters.py
index a7493ec0567..a64f197b4dd 100644
--- a/tests/app/services/external_generation/test_external_provider_adapters.py
+++ b/tests/app/services/external_generation/test_external_provider_adapters.py
@@ -40,10 +40,8 @@ def _build_model(provider_id: str, provider_model_id: str) -> ExternalApiModelCo
provider_model_id=provider_model_id,
capabilities=ExternalModelCapabilities(
modes=["txt2img", "img2img", "inpaint"],
- supports_negative_prompt=True,
supports_reference_images=True,
supports_seed=True,
- supports_guidance=True,
),
)
@@ -59,14 +57,11 @@ def _build_request(
model=model,
mode=mode, # type: ignore[arg-type]
prompt="A test prompt",
- negative_prompt="",
seed=123,
num_images=1,
width=256,
height=256,
image_size=None,
- steps=20,
- guidance=5.5,
init_image=init_image,
mask_image=mask_image,
reference_images=reference_images or [],
@@ -84,7 +79,7 @@ def test_gemini_generate_success(monkeypatch: pytest.MonkeyPatch) -> None:
request = _build_request(
model,
init_image=init_image,
- reference_images=[ExternalReferenceImage(image=ref_image, weight=0.6)],
+ reference_images=[ExternalReferenceImage(image=ref_image)],
)
encoded = encode_image_base64(_make_image("green"))
captured: dict[str, object] = {}
From b83b5e88ffa3dbb17603acee9bf36d45aee5aace Mon Sep 17 00:00:00 2001
From: CypherNaught-0x <9931495+CypherNaught-0x@users.noreply.github.com>
Date: Wed, 4 Feb 2026 16:57:59 +0100
Subject: [PATCH 28/50] feat: initial external model support
---
3-LAYER-FLATTENING.md | 199 ++++++++++++
FP8_IMPLEMENTATION_PLAN.md | 293 +++++++++++++++++
.../external_generation/providers/__init__.py | 3 +-
.../external_generation/providers/seedream.py | 154 +++++++++
nul | 0
pr-8822-review.md | 108 +++++++
pr-8884-review.md | 113 +++++++
.../test_seedream_provider.py | 305 ++++++++++++++++++
8 files changed, 1174 insertions(+), 1 deletion(-)
create mode 100644 3-LAYER-FLATTENING.md
create mode 100644 FP8_IMPLEMENTATION_PLAN.md
create mode 100644 invokeai/app/services/external_generation/providers/seedream.py
create mode 100644 nul
create mode 100644 pr-8822-review.md
create mode 100644 pr-8884-review.md
create mode 100644 tests/app/services/external_generation/test_seedream_provider.py
diff --git a/3-LAYER-FLATTENING.md b/3-LAYER-FLATTENING.md
new file mode 100644
index 00000000000..552156130db
--- /dev/null
+++ b/3-LAYER-FLATTENING.md
@@ -0,0 +1,199 @@
+# 3-Layer Flattening System — Design Document
+
+## Problem
+
+Each canvas entity (raster layer, control layer, inpaint mask, regional guidance) creates its own `Konva.Layer`, which in turn creates a separate HTML `