From f2703ec0034f0334ed4d3c0072b7e5f2b0232868 Mon Sep 17 00:00:00 2001 From: Christopher Quenneville Date: Sun, 5 Apr 2026 12:27:45 -0500 Subject: [PATCH] feat: add Open WebUI as image generation provider Add native support for Open WebUI's image generation API as a new image provider option. Open WebUI exposes an OpenAI-like /v1/images/generations endpoint but with key differences that require special handling: - Response is a bare JSON array instead of {"data": [...]} - Image URLs are relative paths (e.g. /api/v1/files/.../content) - File downloads require the same Bearer auth token The implementation uses raw HTTP calls via aiohttp rather than the OpenAI SDK to handle these differences. No model parameter is sent since Open WebUI manages the image model in its own admin settings. Backend changes: - New OPEN_WEBUI enum value in ImageProvider - generate_image_open_webui() method in ImageGenerationService - Environment getters/setters for OPEN_WEBUI_IMAGE_URL and OPEN_WEBUI_IMAGE_API_KEY - UserConfig model and config loading/saving pipeline updated Frontend changes: - New "Open WebUI" option in image provider dropdown - Settings UI with URL and optional API key fields - Validation, field mappings, and config persistence Docker: - OPEN_WEBUI_IMAGE_URL and OPEN_WEBUI_IMAGE_API_KEY added to all docker-compose service definitions Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 10 ++- servers/fastapi/enums/image_provider.py | 1 + servers/fastapi/models/user_config.py | 4 + .../services/image_generation_service.py | 87 +++++++++++++++++++ servers/fastapi/utils/get_env.py | 9 ++ servers/fastapi/utils/image_provider.py | 4 + servers/fastapi/utils/set_env.py | 9 ++ servers/fastapi/utils/user_config.py | 10 +++ .../(dashboard)/settings/ImageProvider.tsx | 52 +++++++++++ servers/nextjs/app/api/user-config/route.ts | 4 + .../components/ImageSelectionConfig.tsx | 50 +++++++++++ servers/nextjs/types/llm_config.ts | 4 + servers/nextjs/utils/providerConstants.ts | 9 ++ servers/nextjs/utils/providerUtils.ts | 2 + servers/nextjs/utils/storeHelpers.ts | 5 ++ 15 files changed, 259 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05c189f8..9698ed29 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} - COMFYUI_URL=${COMFYUI_URL} - COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW} + - OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL} + - OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY} production-gpu: # image: ghcr.io/presenton/presenton:latest @@ -81,7 +83,9 @@ services: - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} - COMFYUI_URL=${COMFYUI_URL} - COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW} - + - OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL} + - OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY} + development: build: context: . @@ -118,6 +122,8 @@ services: - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} - COMFYUI_URL=${COMFYUI_URL} - COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW} + - OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL} + - OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY} development-gpu: build: @@ -162,3 +168,5 @@ services: - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} - COMFYUI_URL=${COMFYUI_URL} - COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW} + - OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL} + - OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY} diff --git a/servers/fastapi/enums/image_provider.py b/servers/fastapi/enums/image_provider.py index 9d773ad5..76312b73 100644 --- a/servers/fastapi/enums/image_provider.py +++ b/servers/fastapi/enums/image_provider.py @@ -9,3 +9,4 @@ class ImageProvider(Enum): DALLE3 = "dall-e-3" GPT_IMAGE_1_5 = "gpt-image-1.5" COMFYUI = "comfyui" + OPEN_WEBUI = "open_webui" diff --git a/servers/fastapi/models/user_config.py b/servers/fastapi/models/user_config.py index c26a6cb0..8a1b4249 100644 --- a/servers/fastapi/models/user_config.py +++ b/servers/fastapi/models/user_config.py @@ -36,6 +36,10 @@ class UserConfig(BaseModel): COMFYUI_URL: Optional[str] = None COMFYUI_WORKFLOW: Optional[str] = None + # Open WebUI Image Provider + OPEN_WEBUI_IMAGE_URL: Optional[str] = None + OPEN_WEBUI_IMAGE_API_KEY: Optional[str] = None + # Dalle 3 Quality DALL_E_3_QUALITY: Optional[str] = None # Gpt Image 1.5 Quality diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index f9ec1202..93255e36 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -12,6 +12,8 @@ from utils.get_env import ( get_dall_e_3_quality_env, get_gpt_image_1_5_quality_env, get_pexels_api_key_env, + get_open_webui_image_url_env, + get_open_webui_image_api_key_env, ) from utils.get_env import get_pixabay_api_key_env from utils.get_env import get_comfyui_url_env @@ -25,6 +27,7 @@ from utils.image_provider import ( is_nanobanana_pro_selected, is_dalle3_selected, is_comfyui_selected, + is_open_webui_selected, ) import uuid @@ -53,6 +56,8 @@ class ImageGenerationService: return self.generate_image_openai_gpt_image_1_5 elif is_comfyui_selected(): return self.generate_image_comfyui + elif is_open_webui_selected(): + return self.generate_image_open_webui return None def is_stock_provider_selected(self): @@ -141,6 +146,88 @@ class ImageGenerationService: get_gpt_image_1_5_quality_env() or "medium", ) + async def generate_image_open_webui( + self, prompt: str, output_directory: str + ) -> str: + base_url = get_open_webui_image_url_env() + if not base_url: + raise ValueError("OPEN_WEBUI_IMAGE_URL environment variable is not set") + + base_url = base_url.rstrip("/") + api_key = get_open_webui_image_api_key_env() or "" + + from urllib.parse import urlparse + + parsed = urlparse(base_url) + origin = f"{parsed.scheme}://{parsed.netloc}" + + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + payload = { + "prompt": prompt, + "n": 1, + "size": "1024x1024", + } + + async with aiohttp.ClientSession(trust_env=True) as session: + resp = await session.post( + f"{base_url}/images/generations", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=300), + ) + + if resp.status != 200: + error_text = await resp.text() + raise Exception( + f"Open WebUI image generation returned {resp.status}: {error_text}" + ) + + body = await resp.json() + + # Open WebUI returns a bare [...] array instead of {"data": [...]}. + if isinstance(body, list): + items = body + elif isinstance(body, dict) and "data" in body: + items = body["data"] + else: + raise Exception(f"Unexpected response format: {type(body)}") + + if not items: + raise Exception("Open WebUI returned empty results") + + item = items[0] + image_path = os.path.join(output_directory, f"{uuid.uuid4()}.png") + + if item.get("b64_json"): + with open(image_path, "wb") as f: + f.write(base64.b64decode(item["b64_json"])) + elif item.get("url"): + image_url = item["url"] + # Open WebUI returns relative URLs like /api/v1/files/.../content + if image_url.startswith("/"): + image_url = origin + image_url + dl_headers = {} + if api_key: + dl_headers["Authorization"] = f"Bearer {api_key}" + dl_resp = await session.get( + image_url, + headers=dl_headers, + timeout=aiohttp.ClientTimeout(total=120), + ) + if dl_resp.status != 200: + raise Exception( + f"Failed to download image: {dl_resp.status}" + ) + with open(image_path, "wb") as f: + f.write(await dl_resp.read()) + else: + raise Exception("Open WebUI returned no image data") + + return image_path + async def _generate_image_google( self, prompt: str, output_directory: str, model: str ) -> str: diff --git a/servers/fastapi/utils/get_env.py b/servers/fastapi/utils/get_env.py index 74cf2e1f..be8d31b4 100644 --- a/servers/fastapi/utils/get_env.py +++ b/servers/fastapi/utils/get_env.py @@ -142,3 +142,12 @@ def get_codex_model_env(): def get_migrate_database_on_startup_env(): return os.getenv("MIGRATE_DATABASE_ON_STARTUP") + + +# Open WebUI Image Provider +def get_open_webui_image_url_env(): + return os.getenv("OPEN_WEBUI_IMAGE_URL") + + +def get_open_webui_image_api_key_env(): + return os.getenv("OPEN_WEBUI_IMAGE_API_KEY") diff --git a/servers/fastapi/utils/image_provider.py b/servers/fastapi/utils/image_provider.py index 15469709..cb3525e0 100644 --- a/servers/fastapi/utils/image_provider.py +++ b/servers/fastapi/utils/image_provider.py @@ -38,6 +38,10 @@ def is_comfyui_selected() -> bool: return ImageProvider.COMFYUI == get_selected_image_provider() +def is_open_webui_selected() -> bool: + return ImageProvider.OPEN_WEBUI == get_selected_image_provider() + + def get_selected_image_provider() -> ImageProvider | None: """ Get the selected image provider from environment variables. diff --git a/servers/fastapi/utils/set_env.py b/servers/fastapi/utils/set_env.py index 6f26b34f..52c1278d 100644 --- a/servers/fastapi/utils/set_env.py +++ b/servers/fastapi/utils/set_env.py @@ -124,3 +124,12 @@ def set_codex_account_id_env(value: str): def set_codex_model_env(value: str): os.environ["CODEX_MODEL"] = value + + +# Open WebUI Image Provider +def set_open_webui_image_url_env(value: str): + os.environ["OPEN_WEBUI_IMAGE_URL"] = value + + +def set_open_webui_image_api_key_env(value: str): + os.environ["OPEN_WEBUI_IMAGE_API_KEY"] = value diff --git a/servers/fastapi/utils/user_config.py b/servers/fastapi/utils/user_config.py index f83d3047..a050163d 100644 --- a/servers/fastapi/utils/user_config.py +++ b/servers/fastapi/utils/user_config.py @@ -33,6 +33,8 @@ from utils.get_env import ( get_codex_token_expires_env, get_codex_account_id_env, get_codex_model_env, + get_open_webui_image_url_env, + get_open_webui_image_api_key_env, ) from utils.parsers import parse_bool_or_none from utils.set_env import ( @@ -65,6 +67,8 @@ from utils.set_env import ( set_codex_token_expires_env, set_codex_account_id_env, set_codex_model_env, + set_open_webui_image_url_env, + set_open_webui_image_api_key_env, ) @@ -133,6 +137,8 @@ def get_user_config(): CODEX_REFRESH_TOKEN=existing_config.CODEX_REFRESH_TOKEN or get_codex_refresh_token_env(), CODEX_TOKEN_EXPIRES=existing_config.CODEX_TOKEN_EXPIRES or get_codex_token_expires_env(), CODEX_ACCOUNT_ID=existing_config.CODEX_ACCOUNT_ID or get_codex_account_id_env(), + OPEN_WEBUI_IMAGE_URL=existing_config.OPEN_WEBUI_IMAGE_URL or get_open_webui_image_url_env(), + OPEN_WEBUI_IMAGE_API_KEY=existing_config.OPEN_WEBUI_IMAGE_API_KEY or get_open_webui_image_api_key_env(), ) @@ -196,6 +202,10 @@ def update_env_with_user_config(): set_codex_token_expires_env(user_config.CODEX_TOKEN_EXPIRES) if user_config.CODEX_ACCOUNT_ID: set_codex_account_id_env(user_config.CODEX_ACCOUNT_ID) + if user_config.OPEN_WEBUI_IMAGE_URL: + set_open_webui_image_url_env(user_config.OPEN_WEBUI_IMAGE_URL) + if user_config.OPEN_WEBUI_IMAGE_API_KEY: + set_open_webui_image_api_key_env(user_config.OPEN_WEBUI_IMAGE_API_KEY) def save_codex_tokens_to_user_config() -> None: diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx index 31bc11ea..59e8a21c 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx @@ -262,6 +262,33 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL ); } + // Show Open WebUI configuration + if (provider.value === "open_webui") { + return ( +
+
+ +
+ { + input_field_changed( + e.target.value, + "OPEN_WEBUI_IMAGE_URL" + ); + }} + /> +
+
+
+ ); + } + // Show API key input for other providers return (
@@ -300,6 +327,31 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL {!isImageGenerationDisabled &&
{renderQualitySelector(llmConfig, input_field_changed)} + {llmConfig.IMAGE_PROVIDER === "open_webui" && ( +
+ +
+ { + input_field_changed(e.target.value, "OPEN_WEBUI_IMAGE_API_KEY"); + }} + /> + +
+
+ )} {llmConfig.IMAGE_PROVIDER === "comfyui" &&