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/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/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 (