Skip to content

Commit 6d9f408

Browse files
committed
feat(owui): add native image display via OWUI file upload
Upload images directly to Open WebUI's file storage (/api/v1/files/) when OPENWEBUI_BASE_URL and OPENWEBUI_API_KEY are configured. Images display natively in chat with download/save, replacing the base64 HTMLResponse iframe workaround. Falls back gracefully to the existing approach when OWUI upload is not configured or fails.
1 parent dd45f30 commit 6d9f408

10 files changed

Lines changed: 311 additions & 44 deletions

File tree

.env.example

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,25 @@ IMAGE_BASE_URL=http://localhost:8000
3838
# (images will be stored by Open WebUI instead)
3939
SAVE_IMAGES_LOCALLY=true
4040

41+
# -----------------------------------------------------------------------------
42+
# Open WebUI Integration
43+
# -----------------------------------------------------------------------------
44+
# Enable Open WebUI tool integration mode (default: false)
45+
OPENWEBUI_MODE=false
46+
47+
# Open WebUI server URL for native image display (uploads images to OWUI file storage)
48+
# Example: http://open-webui:3000 or http://localhost:3000
49+
OPENWEBUI_BASE_URL=
50+
51+
# Open WebUI API key (generate in OWUI: Settings > Account > API Keys)
52+
OPENWEBUI_API_KEY=
53+
4154
# -----------------------------------------------------------------------------
4255
# Response Configuration
4356
# -----------------------------------------------------------------------------
4457
# Embed images as base64 data URI in markdown responses (default: false)
4558
# When true: Returns ![image](data:image/png;base64,...)
4659
# When false: Returns ![image](http://...)
47-
# Set to true for Open WebUI integration with ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION
4860
MARKDOWN_EMBED_IMAGES=false
4961

5062
# -----------------------------------------------------------------------------

CONFIGURATION.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,29 @@ Complete configuration reference for the Image Generation API.
6161
**`OPENWEBUI_MODE`**
6262
- Enable Open WebUI tool integration mode
6363
- Default: `false`
64-
- When `true`:
65-
- Returns `["data:image/png;base64,..."]` (list with data URI)
66-
- OpenWebUI extracts this as a file and displays the image
67-
- Overrides `response_format` parameter and `MARKDOWN_EMBED_IMAGES`
68-
- Works with OpenWebUI's tool response handling
64+
- When `true` and `OPENWEBUI_BASE_URL` + `OPENWEBUI_API_KEY` are set:
65+
- Uploads images to Open WebUI's file storage via `/api/v1/files/`
66+
- Returns JSON with the OWUI file URL and markdown
67+
- Images display natively in Open WebUI (with download/save)
68+
- Falls back to base64 HTMLResponse if upload fails
69+
- When `true` without OWUI upload configured:
70+
- Returns HTML with base64-encoded `<img>` tag (legacy iframe mode)
6971
- When `false`:
7072
- Normal API behavior based on `response_format` parameter
71-
- Requires: OpenWebUI with `ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION=true`
73+
74+
### Open WebUI Integration
75+
76+
**`OPENWEBUI_BASE_URL`**
77+
- Open WebUI server URL for file upload integration
78+
- Example: `http://open-webui:3000` or `http://localhost:3000`
79+
- Optional - leave empty to use legacy base64 HTMLResponse mode
80+
- Used with `OPENWEBUI_MODE=true` to upload images directly to OWUI
81+
82+
**`OPENWEBUI_API_KEY`**
83+
- API key for Open WebUI authentication
84+
- Required when `OPENWEBUI_BASE_URL` is set
85+
- Generate in Open WebUI: Settings > Account > API Keys
86+
- Used for `POST /api/v1/files/` uploads
7287

7388
**`MARKDOWN_EMBED_IMAGES`**
7489
- Embed images as base64 data URI in markdown responses
@@ -386,10 +401,11 @@ Use after LiteLLM configuration changes.
386401

387402
## Response Format Reference
388403

389-
| `OPENWEBUI_MODE` | `MARKDOWN_EMBED_IMAGES` | `response_format` | Response |
390-
|------------------|-------------------------|-------------------|----------|
391-
| `true` | (ignored) | (ignored) | `["data:image/png;base64,..."]` |
392-
| `false` | `false` | `url` (default) | `{"image_url": "http://..."}` |
393-
| `false` | `false` | `base64` | `{"image_base64": "...", "mime_type": "..."}` |
394-
| `false` | `false` | `markdown` | `{"markdown": "![img](http://...)"}` |
395-
| `false` | `true` | `markdown` | `{"markdown": "![img](data:...;base64,...)"}` |
404+
| `OPENWEBUI_MODE` | OWUI Upload | `MARKDOWN_EMBED_IMAGES` | `response_format` | Response |
405+
|------------------|-------------|-------------------------|-------------------|----------|
406+
| `true` | configured | (ignored) | (ignored) | `{"image_url": "http://owui/api/v1/files/.../content", "markdown": "![img](...)"}` |
407+
| `true` | not configured | (ignored) | (ignored) | HTML `<img>` with base64 data URI (iframe) |
408+
| `false` || `false` | `url` (default) | `{"image_url": "http://..."}` |
409+
| `false` || `false` | `base64` | `{"image_base64": "...", "mime_type": "..."}` |
410+
| `false` || `false` | `markdown` | `{"markdown": "![img](http://...)"}` |
411+
| `false` || `true` | `markdown` | `{"markdown": "![img](data:...;base64,...)"}` |

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,23 @@ See [CONFIGURATION.md](CONFIGURATION.md) for details.
171171
172172
For admin/global tools, configure in Admin Settings with server-accessible URL.
173173
174+
### Native Image Display (Recommended)
175+
176+
For the best experience (images display natively in chat with download/save):
177+
178+
1. Generate an API key in Open WebUI: **Settings > Account > API Keys**
179+
2. Configure in `.env`:
180+
181+
```env
182+
OPENWEBUI_MODE=true
183+
OPENWEBUI_BASE_URL=http://open-webui:3000
184+
OPENWEBUI_API_KEY=your-owui-api-key
185+
```
186+
187+
Images are uploaded directly to Open WebUI's file storage and displayed inline, just like the built-in image generation.
188+
189+
Without `OPENWEBUI_BASE_URL`/`OPENWEBUI_API_KEY`, falls back to base64 HTMLResponse (iframe display).
190+
174191
## API Endpoints
175192

176193
### Image Generation

TODO.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@ Add models released since the last update:
2525
- Test with LiteLLM model discovery
2626

2727
## 3. Improve OWUI Image Display
28-
Status: PENDING
28+
Status: DONE
2929

30-
Current approach returns HTMLResponse with base64-encoded `<img>` tag — a workaround
31-
that went through several iterations. OWUI has had updates since; investigate:
32-
- Check current OWUI tool response handling (artifact display, image rendering)
33-
- See if OWUI now natively supports image URLs or base64 in tool responses without HTML hack
34-
- Consider using OWUI's file upload API to store images there directly
35-
- Reduce unnecessary disk I/O (currently writes to disk then reads back for base64)
36-
- Test with latest OWUI version and document the recommended setup
30+
Added Open WebUI file upload integration:
31+
- Images uploaded directly to OWUI via `POST /api/v1/files/` when `OPENWEBUI_BASE_URL` + `OPENWEBUI_API_KEY` configured
32+
- Returns JSON with OWUI file URL — images display natively in chat (with download/save)
33+
- Graceful fallback to base64 HTMLResponse when OWUI upload not configured or fails
34+
- Eliminated dependency on `ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION` setting

app/api/routes/edit.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,24 @@ async def edit_image_json(
207207
if not urls:
208208
raise HTTPException(status_code=500, detail="No images generated")
209209

210-
# OpenWebUI mode: return HTML with embedded image for iframe display
210+
# OpenWebUI mode
211211
if settings.OPENWEBUI_MODE:
212+
if settings.openwebui_upload_available:
213+
# OWUI upload succeeded — URL is already an OWUI file URL.
214+
return ImageResponse(
215+
image_url=urls[0],
216+
markdown=f"![Edited image]({urls[0]})",
217+
prompt=request.prompt,
218+
model=model,
219+
provider=request.provider,
220+
metadata={
221+
"n": len(urls),
222+
"edit": True,
223+
"source_image": request.image_url,
224+
"all_urls": urls if len(urls) > 1 else None,
225+
},
226+
)
227+
# Fallback: HTMLResponse with base64 (iframe display)
212228
try:
213229
image_data, mime_type = await read_image_as_base64(urls[0])
214230
except FileNotFoundError:

app/api/routes/generate.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,25 @@ async def generate_image(request: ImageRequest, _: None = Depends(verify_token))
6666
if not urls:
6767
raise HTTPException(status_code=500, detail="No images generated")
6868

69-
# OpenWebUI mode: return HTML with embedded image for iframe display
69+
# OpenWebUI mode
7070
if settings.OPENWEBUI_MODE:
71+
if settings.openwebui_upload_available:
72+
# OWUI upload succeeded — URL is already an OWUI file URL.
73+
# Return ImageResponse so the LLM can include the URL in its message.
74+
return ImageResponse(
75+
image_url=urls[0],
76+
markdown=f"![Generated image]({urls[0]})",
77+
prompt=request.prompt,
78+
model=model,
79+
provider=request.provider,
80+
metadata={
81+
"aspect_ratio": request.aspect_ratio,
82+
"quality": request.quality,
83+
"n": len(urls),
84+
"all_urls": urls if len(urls) > 1 else None,
85+
},
86+
)
87+
# Fallback: HTMLResponse with base64 (iframe display)
7188
try:
7289
image_data, mime_type = await read_image_as_base64(urls[0])
7390
except FileNotFoundError:

app/core/config.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ class Settings(BaseSettings):
4646
SAVE_IMAGES_LOCALLY: bool = True # Set to false when using OPENWEBUI_MODE or MARKDOWN_EMBED_IMAGES
4747

4848
# Response Configuration
49-
OPENWEBUI_MODE: bool = False # Return ["data:mime;base64,..."] for OpenWebUI tool integration
49+
OPENWEBUI_MODE: bool = False # Enable OpenWebUI tool integration mode
5050
MARKDOWN_EMBED_IMAGES: bool = False # Embed images as base64 data URI in markdown responses
5151

52+
# Open WebUI Integration
53+
OPENWEBUI_BASE_URL: str | None = None # e.g. http://open-webui:3000
54+
OPENWEBUI_API_KEY: str | None = None # API key for OWUI file upload
55+
5256
# Security (Optional)
5357
API_BEARER_TOKEN: str | None = None
5458

@@ -79,5 +83,10 @@ def gemini_available(self) -> bool:
7983
"""Check if Gemini direct access is available."""
8084
return bool(self.GEMINI_API_KEY)
8185

86+
@property
87+
def openwebui_upload_available(self) -> bool:
88+
"""Check if Open WebUI file upload is configured."""
89+
return bool(self.OPENWEBUI_BASE_URL and self.OPENWEBUI_API_KEY)
90+
8291

8392
settings = Settings()

app/services/storage_service.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class StorageService:
1717
"""
18-
Handles local file storage for generated images.
18+
Handles image storage — local filesystem and Open WebUI file upload.
1919
"""
2020

2121
def __init__(self):
@@ -24,15 +24,35 @@ def __init__(self):
2424

2525
async def save_image(self, image_data: bytes, extension: str = "png") -> str:
2626
"""
27-
Save image bytes to local storage.
27+
Save image bytes to the best available storage.
28+
29+
When OPENWEBUI_MODE is enabled and OWUI upload is configured, uploads
30+
to Open WebUI's file storage and returns an OWUI file URL. Falls back
31+
to local storage on failure or when OWUI upload is not configured.
2832
2933
Args:
3034
image_data: Raw image bytes
3135
extension: File extension (png, jpg, webp)
3236
3337
Returns:
34-
Public URL to access the image
38+
Public URL to access the image (OWUI URL or local URL)
3539
"""
40+
# Try OWUI upload when in OWUI mode with upload configured
41+
if settings.OPENWEBUI_MODE and settings.openwebui_upload_available:
42+
try:
43+
owui_url = await self._upload_to_openwebui(image_data, extension)
44+
# Also save locally if configured
45+
if settings.SAVE_IMAGES_LOCALLY:
46+
await self._save_locally(image_data, extension)
47+
return owui_url
48+
except Exception as e:
49+
logger.warning(f"OWUI upload failed, falling back to local storage: {e}")
50+
51+
# Local storage
52+
return await self._save_locally(image_data, extension)
53+
54+
async def _save_locally(self, image_data: bytes, extension: str = "png") -> str:
55+
"""Save image bytes to local filesystem."""
3656
filename = f"{uuid.uuid4()}.{extension}"
3757
filepath = self.storage_path / filename
3858

@@ -41,6 +61,28 @@ async def save_image(self, image_data: bytes, extension: str = "png") -> str:
4161

4262
return f"{settings.IMAGE_BASE_URL.rstrip('/')}/images/{filename}"
4363

64+
async def _upload_to_openwebui(self, image_data: bytes, extension: str = "png") -> str:
65+
"""
66+
Upload image to Open WebUI's file storage.
67+
68+
Returns:
69+
OWUI file URL (e.g. http://open-webui:3000/api/v1/files/{id}/content)
70+
"""
71+
filename = f"{uuid.uuid4()}.{extension}"
72+
mime_type = f"image/{extension}"
73+
base_url = settings.OPENWEBUI_BASE_URL.rstrip("/")
74+
75+
async with httpx.AsyncClient(timeout=30.0) as client:
76+
response = await client.post(
77+
f"{base_url}/api/v1/files/",
78+
files={"file": (filename, image_data, mime_type)},
79+
headers={"Authorization": f"Bearer {settings.OPENWEBUI_API_KEY}"},
80+
)
81+
response.raise_for_status()
82+
file_id = response.json()["id"]
83+
logger.info(f"Uploaded image to OWUI: {file_id}")
84+
return f"{base_url}/api/v1/files/{file_id}/content"
85+
4486
async def get_image(self, url: str) -> bytes:
4587
"""
4688
Retrieve image bytes from URL or local storage.

tests/test_edit.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async def test_storage_get_image_local(temp_storage):
5959
with patch("app.services.storage_service.settings") as mock_settings:
6060
mock_settings.STORAGE_PATH = str(temp_storage)
6161
mock_settings.IMAGE_BASE_URL = "http://localhost:8000"
62+
mock_settings.OPENWEBUI_MODE = False
6263

6364
service = StorageService()
6465

@@ -79,6 +80,7 @@ async def test_storage_get_image_local_relative(temp_storage):
7980
with patch("app.services.storage_service.settings") as mock_settings:
8081
mock_settings.STORAGE_PATH = str(temp_storage)
8182
mock_settings.IMAGE_BASE_URL = "http://localhost:8000"
83+
mock_settings.OPENWEBUI_MODE = False
8284

8385
service = StorageService()
8486

0 commit comments

Comments
 (0)