Merge pull request #497 from hilotechinc/main

feat: add Open WebUI as image generation provider
This commit is contained in:
Saurav Niraula 2026-04-15 18:34:09 +05:45 committed by GitHub
commit 8b63e407e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 259 additions and 1 deletions

View file

@ -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}

View file

@ -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"

View file

@ -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

View file

@ -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:

View file

@ -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")

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -262,6 +262,33 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
);
}
// Show Open WebUI configuration
if (provider.value === "open_webui") {
return (
<div className="space-y-4">
<div className='w-[205px]'>
<label className="block text-sm font-medium text-gray-700 mb-2">
Open WebUI URL
</label>
<div className="relative">
<input
type="text"
placeholder="http://localhost:3000/api/v1"
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={llmConfig.OPEN_WEBUI_IMAGE_URL || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"OPEN_WEBUI_IMAGE_URL"
);
}}
/>
</div>
</div>
</div>
);
}
// Show API key input for other providers
return (
<div className=" w-[205px]">
@ -300,6 +327,31 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
{!isImageGenerationDisabled && <div className='flex justify-end items-center mt-[18px]'>
{renderQualitySelector(llmConfig, input_field_changed)}
{llmConfig.IMAGE_PROVIDER === "open_webui" && (
<div className='w-[205px]'>
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key (optional)
</label>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
placeholder="API key"
className="w-full px-4 py-2.5 h-12 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={llmConfig.OPEN_WEBUI_IMAGE_API_KEY || ""}
onChange={(e) => {
input_field_changed(e.target.value, "OPEN_WEBUI_IMAGE_API_KEY");
}}
/>
<button
type="button"
onClick={() => setShowApiKey((prev) => !prev)}
className='absolute right-2 top-1/2 -translate-y-1/2 bg-white px-2 py-1 cursor-pointer'
>
{showApiKey ? <Eye className='w-4 h-4 text-gray-500' /> : <EyeOff className='w-4 h-4 text-gray-500' />}
</button>
</div>
</div>
)}
{llmConfig.IMAGE_PROVIDER === "comfyui" && <div className='w-full'>
<label className="block text-sm font-medium text-gray-700 mb-2">
Workflow JSON

View file

@ -91,6 +91,10 @@ export async function POST(request: Request) {
userConfig.USE_CUSTOM_URL === undefined
? existingConfig.USE_CUSTOM_URL
: userConfig.USE_CUSTOM_URL,
OPEN_WEBUI_IMAGE_URL:
userConfig.OPEN_WEBUI_IMAGE_URL || existingConfig.OPEN_WEBUI_IMAGE_URL,
OPEN_WEBUI_IMAGE_API_KEY:
userConfig.OPEN_WEBUI_IMAGE_API_KEY || existingConfig.OPEN_WEBUI_IMAGE_API_KEY,
CODEX_MODEL: userConfig.CODEX_MODEL || existingConfig.CODEX_MODEL,
CODEX_ACCESS_TOKEN: existingConfig.CODEX_ACCESS_TOKEN,
CODEX_REFRESH_TOKEN: existingConfig.CODEX_REFRESH_TOKEN,

View file

@ -266,6 +266,56 @@ const ImageSelectionConfig = ({ isImageGenerationDisabled, openImageProviderSele
return <></>;
}
// Show Open WebUI configuration
if (provider.value === "open_webui") {
return (
<div className="space-y-4 w-[295px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Open WebUI URL
</label>
<div className="relative">
<input
type="text"
placeholder="http://localhost:3000/api/v1"
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={llmConfig.OPEN_WEBUI_IMAGE_URL || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"open_webui_image_url"
);
}}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Image model is configured in Open WebUI admin settings
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key (optional)
</label>
<div className="relative">
<input
type="text"
placeholder="Open WebUI API key"
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={llmConfig.OPEN_WEBUI_IMAGE_API_KEY || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"open_webui_image_api_key"
);
}}
/>
</div>
</div>
</div>
);
}
// Show ComfyUI configuration
if (provider.value === "comfyui") {
return (

View file

@ -32,6 +32,10 @@ export interface LLMConfig {
COMFYUI_URL?: string;
COMFYUI_WORKFLOW?: string;
// Open WebUI Image Provider
OPEN_WEBUI_IMAGE_URL?: string;
OPEN_WEBUI_IMAGE_API_KEY?: string;
// Dalle 3 Quality
DALL_E_3_QUALITY?: string;
// GPT Image 1.5 Quality

View file

@ -90,6 +90,15 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
apiKeyField: "COMFYUI_URL",
apiKeyFieldLabel: "ComfyUI Server URL",
},
open_webui: {
value: "open_webui",
label: "Open WebUI",
description: "Use your Open WebUI server for image generation",
icon: "/icons/open-webui.png",
requiresApiKey: false,
apiKeyField: "OPEN_WEBUI_IMAGE_URL",
apiKeyFieldLabel: "Open WebUI URL",
},
};
export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {

View file

@ -52,6 +52,8 @@ export const updateLLMConfig = (
comfyui_workflow: "COMFYUI_WORKFLOW",
dall_e_3_quality: "DALL_E_3_QUALITY",
gpt_image_1_5_quality: "GPT_IMAGE_1_5_QUALITY",
open_webui_image_url: "OPEN_WEBUI_IMAGE_URL",
open_webui_image_api_key: "OPEN_WEBUI_IMAGE_API_KEY",
codex_model: "CODEX_MODEL",
};

View file

@ -102,6 +102,11 @@ export const getLLMConfigValidationError = (
return "ComfyUI server URL is required.";
}
break;
case "open_webui":
if (!isProvided(llmConfig.OPEN_WEBUI_IMAGE_URL)) {
return "Open WebUI URL is required.";
}
break;
default:
return "Select a valid image provider.";
}