Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/src/generated/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,28 @@
"type": "typing.Optional[str]",
"validation": {}
},
{
"category": "EXTERNAL PROVIDERS",
"default": null,
"description": "API key for a custom OpenAI Images-compatible provider.",
"env_var": "INVOKEAI_EXTERNAL_CUSTOM_OPENAI_IMAGES_API_KEY",
"literal_values": [],
"name": "external_custom_openai_images_api_key",
"required": false,
"type": "typing.Optional[str]",
"validation": {}
},
{
"category": "EXTERNAL PROVIDERS",
"default": null,
"description": "Base URL for a custom OpenAI Images-compatible provider.",
"env_var": "INVOKEAI_EXTERNAL_CUSTOM_OPENAI_IMAGES_BASE_URL",
"literal_values": [],
"name": "external_custom_openai_images_base_url",
"required": false,
"type": "typing.Optional[str]",
"validation": {}
},
{
"category": "EXTERNAL PROVIDERS",
"default": null,
Expand Down
4 changes: 4 additions & 0 deletions invokeai/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
from invokeai.app.services.external_generation.providers import (
AlibabaCloudProvider,
CustomOpenAIImagesProvider,
GeminiProvider,
OpenAIProvider,
SeedreamProvider,
Expand Down Expand Up @@ -167,6 +168,9 @@ def initialize(
external_generation = ExternalGenerationService(
providers={
AlibabaCloudProvider.provider_id: AlibabaCloudProvider(app_config=configuration, logger=logger),
CustomOpenAIImagesProvider.provider_id: CustomOpenAIImagesProvider(
app_config=configuration, logger=logger
),
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),
Expand Down
126 changes: 124 additions & 2 deletions invokeai/app/api/routers/app_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@
load_external_api_keys,
)
from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
from invokeai.app.services.external_generation.providers.custom_openai_images import (
CUSTOM_OPENAI_IMAGES_CAPABILITIES,
CUSTOM_OPENAI_IMAGES_DEFAULT_SETTINGS,
CUSTOM_OPENAI_IMAGES_PROVIDER_ID,
)
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.app.services.model_records.model_records_base import DuplicateModelException, UnknownModelException
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelSourceType, ModelType
from invokeai.backend.util.logging import logging
from invokeai.backend.util.util import slugify
from invokeai.version import __version__


Expand Down Expand Up @@ -98,8 +105,17 @@ class ExternalProviderConfigModel(BaseModel):
base_url: str | None = Field(default=None, description="Optional base URL override")


class CustomOpenAIImagesModelCreate(BaseModel):
provider_model_id: str = Field(min_length=1, description="Provider-specific model ID")
name: str | None = Field(default=None, description="Optional display name")


EXTERNAL_PROVIDER_FIELDS: dict[str, tuple[str, str]] = {
"alibabacloud": ("external_alibabacloud_api_key", "external_alibabacloud_base_url"),
CUSTOM_OPENAI_IMAGES_PROVIDER_ID: (
"external_custom_openai_images_api_key",
"external_custom_openai_images_base_url",
),
"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"),
Expand Down Expand Up @@ -239,6 +255,87 @@ async def reset_external_provider_config(
return _build_external_provider_config(provider_id, get_config())


@app_router.get(
"/external_providers/custom_openai_images/models",
operation_id="list_custom_openai_images_models",
status_code=200,
response_model=list[ExternalApiModelConfig],
)
async def list_custom_openai_images_models() -> list[ExternalApiModelConfig]:
return _list_external_models_for_provider(CUSTOM_OPENAI_IMAGES_PROVIDER_ID)


@app_router.post(
"/external_providers/custom_openai_images/models",
operation_id="create_custom_openai_images_model",
status_code=200,
response_model=ExternalApiModelConfig,
)
async def create_custom_openai_images_model(
model: CustomOpenAIImagesModelCreate = Body(description="Custom OpenAI Images-compatible model settings"),
) -> ExternalApiModelConfig:
provider_model_id = model.provider_model_id.strip()
if not provider_model_id:
raise HTTPException(status_code=400, detail="Provider model ID is required")
name = (model.name or provider_model_id).strip() or provider_model_id

model_store = ApiDependencies.invoker.services.model_manager.store
existing = next(
(
external_model
for external_model in _list_external_models_for_provider(CUSTOM_OPENAI_IMAGES_PROVIDER_ID)
if external_model.provider_model_id == provider_model_id
),
None,
)
key = existing.key if existing is not None else _build_unique_external_model_key(provider_model_id)
source = f"external://{CUSTOM_OPENAI_IMAGES_PROVIDER_ID}/{provider_model_id}"
config = ExternalApiModelConfig(
key=key,
name=name,
description="Custom OpenAI Images-compatible external API model.",
provider_id=CUSTOM_OPENAI_IMAGES_PROVIDER_ID,
provider_model_id=provider_model_id,
capabilities=CUSTOM_OPENAI_IMAGES_CAPABILITIES,
default_settings=CUSTOM_OPENAI_IMAGES_DEFAULT_SETTINGS,
source=source,
source_type=ModelSourceType.External,
path="",
hash="",
file_size=0,
tags=["custom", "openai-compatible"],
)

try:
if existing is not None:
saved_model = model_store.replace_model(existing.key, config)
else:
saved_model = model_store.add_model(config)
except DuplicateModelException as e:
raise HTTPException(status_code=409, detail=str(e)) from e
if not isinstance(saved_model, ExternalApiModelConfig):
raise HTTPException(status_code=500, detail="Saved model was not an external API model")
return saved_model


@app_router.delete(
"/external_providers/custom_openai_images/models/{model_key}",
operation_id="delete_custom_openai_images_model",
status_code=204,
)
async def delete_custom_openai_images_model(
model_key: str = Path(description="Custom OpenAI Images-compatible model key"),
) -> None:
model_store = ApiDependencies.invoker.services.model_manager.store
try:
model = model_store.get_model(model_key)
except UnknownModelException as e:
raise HTTPException(status_code=404, detail=f"Unknown model '{model_key}'") from e
if not isinstance(model, ExternalApiModelConfig) or model.provider_id != CUSTOM_OPENAI_IMAGES_PROVIDER_ID:
raise HTTPException(status_code=404, detail=f"Unknown custom OpenAI Images-compatible model '{model_key}'")
model_store.del_model(model_key)


def status_to_model(status: ExternalProviderStatus) -> ExternalProviderStatusModel:
return ExternalProviderStatusModel(
provider_id=status.provider_id,
Expand Down Expand Up @@ -314,6 +411,31 @@ def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig)
)


def _list_external_models_for_provider(provider_id: str) -> list[ExternalApiModelConfig]:
model_store = ApiDependencies.invoker.services.model_manager.store
external_models = model_store.search_by_attr(
base_model=BaseModelType.External,
model_type=ModelType.ExternalImageGenerator,
)
models = [
model
for model in external_models
if isinstance(model, ExternalApiModelConfig) and model.provider_id == provider_id
]
return sorted(models, key=lambda model: model.name.lower())


def _build_unique_external_model_key(provider_model_id: str) -> str:
model_store = ApiDependencies.invoker.services.model_manager.store
base_key = slugify(f"{CUSTOM_OPENAI_IMAGES_PROVIDER_ID}-{provider_model_id}")
key = base_key
suffix = 2
while model_store.exists(key):
key = f"{base_key}-{suffix}"
suffix += 1
return key


def _remove_external_models_for_provider(provider_id: str) -> None:
model_manager = ApiDependencies.invoker.services.model_manager
external_models = model_manager.store.search_by_attr(
Expand Down
53 changes: 53 additions & 0 deletions invokeai/app/invocations/external_image_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,59 @@ def _build_output_provider_metadata(self) -> dict[str, Any]:
return metadata


@invocation(
"custom_openai_images_generation",
title="Custom OpenAI Images-compatible Generation",
tags=["external", "generation", "openai-compatible"],
category="image",
version="1.0.0",
)
class CustomOpenAIImagesGenerationInvocation(BaseExternalImageGenerationInvocation):
"""Generate images using a custom OpenAI Images-compatible external model."""

provider_id = "custom_openai_images"

model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.External],
ui_model_type=[ModelType.ExternalImageGenerator],
ui_model_format=[ModelFormat.ExternalApi],
ui_model_provider_id=["custom_openai_images"],
)

mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
init_image: ImageField | None = InputField(
default=None, description="Init image (use reference_images instead)", ui_hidden=True
)
mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)

quality: Literal["auto", "high", "medium", "low"] = InputField(default="auto", description="Output image quality")
background: Literal["auto", "transparent", "opaque"] = InputField(
default="auto", description="Background transparency handling"
)
input_fidelity: Literal["low", "high"] | None = InputField(
default=None, description="Fidelity to source images (edits only)"
)

def _build_provider_options(self) -> dict[str, Any]:
options: dict[str, Any] = {
"quality": self.quality,
"background": self.background,
}
if self.input_fidelity is not None:
options["input_fidelity"] = self.input_fidelity
return options

def _build_output_provider_metadata(self) -> dict[str, Any]:
metadata: dict[str, Any] = {
"custom_openai_images_quality": self.quality,
"custom_openai_images_background": self.background,
}
if self.input_fidelity is not None:
metadata["custom_openai_images_input_fidelity"] = self.input_fidelity
return metadata


@invocation(
"gemini_image_generation",
title="Gemini Image Generation",
Expand Down
10 changes: 10 additions & 0 deletions invokeai/app/services/config/config_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
EXTERNAL_PROVIDER_CONFIG_FIELDS = (
"external_alibabacloud_api_key",
"external_alibabacloud_base_url",
"external_custom_openai_images_api_key",
"external_custom_openai_images_base_url",
"external_gemini_api_key",
"external_gemini_base_url",
"external_openai_api_key",
Expand Down Expand Up @@ -128,6 +130,8 @@ class InvokeAIAppConfig(BaseSettings):
strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.
external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.
external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.
external_custom_openai_images_api_key: API key for a custom OpenAI Images-compatible provider.
external_custom_openai_images_base_url: Base URL for a custom OpenAI Images-compatible provider.
external_gemini_api_key: API key for Gemini image generation.
external_openai_api_key: API key for OpenAI image generation.
external_gemini_base_url: Base URL override for Gemini image generation.
Expand Down Expand Up @@ -238,6 +242,12 @@ class InvokeAIAppConfig(BaseSettings):
external_alibabacloud_base_url: Optional[str] = Field(
default=None, description="Base URL override for Alibaba Cloud DashScope image generation."
)
external_custom_openai_images_api_key: Optional[str] = Field(
default=None, description="API key for a custom OpenAI Images-compatible provider."
)
external_custom_openai_images_base_url: Optional[str] = Field(
default=None, description="Base URL for a custom OpenAI Images-compatible provider."
)
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from invokeai.app.services.external_generation.providers.alibabacloud import AlibabaCloudProvider
from invokeai.app.services.external_generation.providers.custom_openai_images import CustomOpenAIImagesProvider
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__ = ["AlibabaCloudProvider", "GeminiProvider", "OpenAIProvider", "SeedreamProvider"]
__all__ = ["AlibabaCloudProvider", "CustomOpenAIImagesProvider", "GeminiProvider", "OpenAIProvider", "SeedreamProvider"]
Loading
Loading