Skip to content

Commit d72a2ad

Browse files
committed
feat: add Open WebUI Files API integration
Upload images to Open WebUI's file storage for better accessibility. When OPENWEBUI_API_URL and OPENWEBUI_API_KEY are configured, images are automatically uploaded to Open WebUI and URLs point there. This solves the issue where IMAGE_BASE_URL needs to be accessible from the browser - now images are served by Open WebUI itself.
1 parent 606384d commit d72a2ad

3 files changed

Lines changed: 115 additions & 4 deletions

File tree

app/core/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class Settings(BaseSettings):
5252
FILTER_IMAGE_MODELS: bool = True # Only return image generation models from LiteLLM
5353
DEFAULT_MODEL: str | None = None # Default model for image generation
5454

55+
# Open WebUI Integration (optional)
56+
OPENWEBUI_API_URL: str | None = None # e.g. https://chat.mydomain.com
57+
OPENWEBUI_API_KEY: str | None = None # API key from Open WebUI settings
58+
5559
# Server Configuration
5660
HOST: str = "0.0.0.0"
5761
PORT: int = 8000
@@ -71,5 +75,10 @@ def gemini_available(self) -> bool:
7175
"""Check if Gemini direct access is available."""
7276
return bool(self.GEMINI_API_KEY)
7377

78+
@property
79+
def openwebui_available(self) -> bool:
80+
"""Check if Open WebUI integration is configured."""
81+
return bool(self.OPENWEBUI_API_URL and self.OPENWEBUI_API_KEY)
82+
7483

7584
settings = Settings()

app/services/openwebui_service.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import logging
2+
3+
import httpx
4+
5+
from app.core.config import settings
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class OpenWebUIService:
11+
"""
12+
Service for uploading images to Open WebUI's Files API.
13+
14+
When configured, images are uploaded to Open WebUI's storage,
15+
making them accessible regardless of network topology.
16+
"""
17+
18+
def __init__(self):
19+
if not settings.OPENWEBUI_API_URL:
20+
raise ValueError("OPENWEBUI_API_URL not configured")
21+
if not settings.OPENWEBUI_API_KEY:
22+
raise ValueError("OPENWEBUI_API_KEY not configured")
23+
24+
self.base_url = settings.OPENWEBUI_API_URL.rstrip("/")
25+
self.api_key = settings.OPENWEBUI_API_KEY
26+
27+
async def upload_image(self, image_data: bytes, filename: str) -> str:
28+
"""
29+
Upload image to Open WebUI Files API.
30+
31+
Args:
32+
image_data: Image bytes
33+
filename: Filename with extension (e.g., "image.png")
34+
35+
Returns:
36+
URL to access the uploaded file
37+
"""
38+
url = f"{self.base_url}/api/v1/files/"
39+
40+
# Determine content type from filename
41+
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "png"
42+
content_types = {
43+
"png": "image/png",
44+
"jpg": "image/jpeg",
45+
"jpeg": "image/jpeg",
46+
"webp": "image/webp",
47+
"gif": "image/gif",
48+
}
49+
content_type = content_types.get(ext, "image/png")
50+
51+
headers = {
52+
"Authorization": f"Bearer {self.api_key}",
53+
}
54+
55+
# Multipart file upload
56+
files = {
57+
"file": (filename, image_data, content_type),
58+
}
59+
60+
async with httpx.AsyncClient(timeout=30.0) as client:
61+
response = await client.post(url, headers=headers, files=files)
62+
response.raise_for_status()
63+
64+
data = response.json()
65+
file_id = data.get("id")
66+
67+
if not file_id:
68+
raise ValueError(f"No file ID in response: {data}")
69+
70+
# Return URL to access the file
71+
file_url = f"{self.base_url}/api/v1/files/{file_id}/content"
72+
logger.info(f"Uploaded image to Open WebUI: {file_url}")
73+
74+
return file_url
75+
76+
77+
# Singleton instance (lazy initialization)
78+
_openwebui_service: OpenWebUIService | None = None
79+
80+
81+
def get_openwebui_service() -> OpenWebUIService:
82+
"""Get Open WebUI service instance."""
83+
global _openwebui_service
84+
if _openwebui_service is None:
85+
_openwebui_service = OpenWebUIService()
86+
return _openwebui_service

app/services/storage_service.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import uuid
23
from pathlib import Path
34

@@ -6,10 +7,13 @@
67

78
from app.core.config import settings
89

10+
logger = logging.getLogger(__name__)
11+
912

1013
class StorageService:
1114
"""
1215
Handles local file storage for generated images.
16+
Optionally uploads to Open WebUI for better accessibility.
1317
"""
1418

1519
def __init__(self):
@@ -18,24 +22,36 @@ def __init__(self):
1822

1923
async def save_image(self, image_data: bytes, extension: str = "png") -> str:
2024
"""
21-
Save image bytes to local storage and return public URL.
25+
Save image bytes to local storage and optionally upload to Open WebUI.
2226
2327
Args:
2428
image_data: Raw image bytes
2529
extension: File extension (png, jpg, webp)
2630
2731
Returns:
28-
Public URL to access the image
32+
Public URL to access the image (Open WebUI URL if configured, else local)
2933
"""
3034
filename = f"{uuid.uuid4()}.{extension}"
3135
filepath = self.storage_path / filename
3236

37+
# Always save locally first
3338
async with aiofiles.open(filepath, "wb") as f:
3439
await f.write(image_data)
3540

36-
relative_url = f"/images/{filename}"
41+
local_url = f"{settings.IMAGE_BASE_URL.rstrip('/')}/images/{filename}"
42+
43+
# Upload to Open WebUI if configured
44+
if settings.openwebui_available:
45+
try:
46+
from app.services.openwebui_service import get_openwebui_service
47+
48+
webui_service = get_openwebui_service()
49+
webui_url = await webui_service.upload_image(image_data, filename)
50+
return webui_url
51+
except Exception as e:
52+
logger.warning(f"Open WebUI upload failed, using local URL: {e}")
3753

38-
return f"{settings.IMAGE_BASE_URL.rstrip('/')}{relative_url}"
54+
return local_url
3955

4056
async def get_image(self, url: str) -> bytes:
4157
"""

0 commit comments

Comments
 (0)