Image tools: migrate to OWUI 0.9.0 async model accessors
Open WebUI 0.9.0 made every model-class accessor (Users.get_user_by_id,
Chats.get_chat_by_id, Files.get_file_by_id, …) a coroutine. Both tools
were still calling them synchronously, so the calls returned coroutines
instead of model objects; the first downstream attribute access threw,
the bare `except Exception: return False` swallowed it, and uploads
silently fell through to the data-URI fallback. The data-URI markdown
rendered during streaming but didn't survive post-stream commit, which
looked like "image flashes in, then disappears."
Add await to the six call sites; promote `_read_file_dict` to async
since it now contains an await; restore `_push_image_to_chat` to the
canonical `files` event so the file-attachment chrome (thumbnail +
download) comes back.
This supersedes commit d034700, which mis-diagnosed the symptom as a
virtualization regression and switched to a `message`-event markdown
workaround. The workaround didn't help (same flash-and-vanish) because
the upload pre-check still failed for the same async-migration reason
and the data-URI fallback path still ran.
smart_image_gen.py 0.7.9 -> 0.7.10
smart_image_pipe.py 0.1.1 -> 0.1.2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: Smart Image Generator & Editor (ComfyUI)
|
||||
author: ai-stack
|
||||
version: 0.7.9
|
||||
version: 0.7.10
|
||||
description: Generate or edit images via ComfyUI with automatic SDXL
|
||||
checkpoint routing. Two methods — generate_image (txt2img) and
|
||||
edit_image (img2img on the user's most recently attached image). The
|
||||
@@ -9,8 +9,8 @@ description: Generate or edit images via ComfyUI with automatic SDXL
|
||||
score-tag, NoobAI/Illustrious furry, etc. — and each style ships
|
||||
with the creator-recommended sampler, scheduler, CFG, steps, CLIP
|
||||
skip, prompt-prefix dialect, and negatives. The image is uploaded
|
||||
to Open WebUI's file store and surfaced as a markdown image
|
||||
appended to the assistant message via a `message` event; the
|
||||
to Open WebUI's file store and surfaced via a `files` event (the
|
||||
canonical pattern used by Open WebUI's own image-gen path); the
|
||||
function return is a short confirmation so the LLM doesn't try to
|
||||
describe or re-emit the image.
|
||||
required_open_webui_version: 0.5.0
|
||||
@@ -405,7 +405,7 @@ def _file_dict_is_image(f: dict) -> bool:
|
||||
_FILE_URL_ID_RE = re.compile(r"/(?:api/v1/)?files/([0-9a-fA-F-]{8,})(?:/content)?")
|
||||
|
||||
|
||||
def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
async def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
"""
|
||||
Try to read raw bytes for one file dict. Tries in order:
|
||||
1. Local filesystem path keys (covers user uploads with `path`).
|
||||
@@ -415,6 +415,12 @@ def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
assistant-emitted files where the message attachment is just
|
||||
{"type":"image","url":"/api/v1/files/<uuid>/content"} —
|
||||
no id field, no path field, but the URL has the id).
|
||||
|
||||
Async because Open WebUI 0.9.0 made every model-class accessor
|
||||
a coroutine (Users / Chats / Files / etc.). Calling the sync
|
||||
way returns a coroutine object instead of the model — silently
|
||||
breaks downstream attribute access. Same reason the callers in
|
||||
_extract_attached_image and _push_image_to_chat must await.
|
||||
"""
|
||||
for path_key in ("path", "filepath", "file_path"):
|
||||
path = f.get(path_key)
|
||||
@@ -437,7 +443,7 @@ def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
if _OPENWEBUI_RUNTIME:
|
||||
for fid in candidate_ids:
|
||||
try:
|
||||
file_model = Files.get_file_by_id(fid)
|
||||
file_model = await Files.get_file_by_id(fid)
|
||||
if file_model is None:
|
||||
continue
|
||||
path = getattr(file_model, "path", None)
|
||||
@@ -506,7 +512,7 @@ async def _extract_attached_image(
|
||||
for f in msg_files:
|
||||
if not isinstance(f, dict) or not _file_dict_is_image(f):
|
||||
continue
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
@@ -514,7 +520,7 @@ async def _extract_attached_image(
|
||||
for f in files or []:
|
||||
if not isinstance(f, dict) or not _file_dict_is_image(f):
|
||||
continue
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
@@ -526,7 +532,7 @@ async def _extract_attached_image(
|
||||
chat_id = metadata.get("chat_id")
|
||||
if chat_id:
|
||||
try:
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
chat = await Chats.get_chat_by_id(chat_id)
|
||||
chat_data = getattr(chat, "chat", None) if chat else None
|
||||
chat_messages = (chat_data or {}).get("messages", []) if isinstance(chat_data, dict) else []
|
||||
for msg in reversed(chat_messages):
|
||||
@@ -536,7 +542,7 @@ async def _extract_attached_image(
|
||||
for f in msg_files:
|
||||
if not isinstance(f, dict) or not _file_dict_is_image(f):
|
||||
continue
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
except Exception:
|
||||
@@ -586,28 +592,20 @@ async def _push_image_to_chat(
|
||||
event_emitter: Optional[Callable[[dict], Awaitable[None]]],
|
||||
) -> bool:
|
||||
"""
|
||||
Surface a generated image in the chat: upload the bytes via the
|
||||
internal file store, then inject a markdown image referencing the
|
||||
served URL into the assistant message via a `message` event.
|
||||
Surface a generated image in the chat using Open WebUI's canonical
|
||||
pattern: upload the bytes via the internal file store, then emit a
|
||||
`files` event referencing the served URL. This is the same path Open
|
||||
WebUI's own image-generation code uses (utils/middleware.py ~1325).
|
||||
|
||||
We deliberately don't use the `files` event (Open WebUI's own
|
||||
image-gen path). Open WebUI 0.9.x added chat-history virtualization
|
||||
that unmounts off-screen messages and reconstructs them from
|
||||
persisted shape — and `files` attached to an assistant message
|
||||
mid-stream by a tool don't survive that round-trip. The image
|
||||
flashes in during streaming and disappears the moment the message
|
||||
commits. Markdown in `message.content` is part of the persisted
|
||||
shape, so it renders reliably on every remount.
|
||||
|
||||
Returns True if the image was uploaded and emitted. Returns False
|
||||
if anything is missing — caller falls back to a data-URI markdown
|
||||
message (same `message` event path, just inline bytes).
|
||||
Returns True if the image was uploaded and emitted via the files
|
||||
event. Returns False if anything is missing — caller should fall
|
||||
back to a data-URI markdown message in that case.
|
||||
"""
|
||||
if not (_OPENWEBUI_RUNTIME and request and user_dict and event_emitter):
|
||||
return False
|
||||
|
||||
try:
|
||||
user = Users.get_user_by_id(user_dict.get("id"))
|
||||
user = await Users.get_user_by_id(user_dict.get("id"))
|
||||
if not user:
|
||||
return False
|
||||
|
||||
@@ -638,23 +636,9 @@ async def _push_image_to_chat(
|
||||
"get_file_content_by_id", id=file_item.id
|
||||
)
|
||||
|
||||
# TODO(open-webui virtualization fix): once chat-history
|
||||
# virtualization survives `files` events from tool calls again,
|
||||
# swap back to the `files` payload below. The `files` event
|
||||
# gives proper file-attachment chrome (thumbnail card + download
|
||||
# button) that the markdown-image path lacks. Verify the fix by
|
||||
# emitting `files`, then refreshing the page AND scrolling the
|
||||
# message off-screen and back — both must keep the image
|
||||
# visible. Track upstream: github.com/open-webui/open-webui
|
||||
# release notes mentioning tool calls, `files` events, or
|
||||
# virtualization. To restore, replace the block below with:
|
||||
# await event_emitter({
|
||||
# "type": "files",
|
||||
# "data": {"files": [{"type": "image", "url": url}]},
|
||||
# })
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {"content": f"\n\n"},
|
||||
"type": "files",
|
||||
"data": {"files": [{"type": "image", "url": url}]},
|
||||
})
|
||||
return True
|
||||
except Exception:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: Smart Image Studio (Pipe)
|
||||
author: ai-stack
|
||||
version: 0.1.1
|
||||
version: 0.1.2
|
||||
description: Deterministic image-gen / edit / inpaint pipe — no LLM in the
|
||||
loop for the routing decision. Registers as a model in the chat-model
|
||||
dropdown ('Image Studio (Pipe)'). Reads the user's message + attached
|
||||
@@ -303,7 +303,7 @@ def _file_dict_is_image(f: dict) -> bool:
|
||||
return "image" in ftype or fname.endswith((".png", ".jpg", ".jpeg", ".webp"))
|
||||
|
||||
|
||||
def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
async def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
for path_key in ("path", "filepath", "file_path"):
|
||||
path = f.get(path_key)
|
||||
if path:
|
||||
@@ -323,7 +323,7 @@ def _read_file_dict(f: dict) -> Optional[bytes]:
|
||||
if _OPENWEBUI_RUNTIME:
|
||||
for fid in candidate_ids:
|
||||
try:
|
||||
file_model = Files.get_file_by_id(fid)
|
||||
file_model = await Files.get_file_by_id(fid)
|
||||
if file_model is None:
|
||||
continue
|
||||
path = getattr(file_model, "path", None)
|
||||
@@ -361,13 +361,13 @@ async def _extract_attached_image(files, messages, metadata, session) -> Optiona
|
||||
continue
|
||||
for f in (msg.get("files") or []):
|
||||
if isinstance(f, dict) and _file_dict_is_image(f):
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
# 3. __files__
|
||||
for f in files or []:
|
||||
if isinstance(f, dict) and _file_dict_is_image(f):
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
# 4. DB lookup (assistant-emitted files often only land here)
|
||||
@@ -375,13 +375,13 @@ async def _extract_attached_image(files, messages, metadata, session) -> Optiona
|
||||
chat_id = metadata.get("chat_id")
|
||||
if chat_id:
|
||||
try:
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
chat = await Chats.get_chat_by_id(chat_id)
|
||||
chat_data = getattr(chat, "chat", None) if chat else None
|
||||
chat_messages = (chat_data or {}).get("messages", []) if isinstance(chat_data, dict) else []
|
||||
for msg in reversed(chat_messages):
|
||||
for f in (msg.get("files") or []) if isinstance(msg, dict) else []:
|
||||
if isinstance(f, dict) and _file_dict_is_image(f):
|
||||
data = _read_file_dict(f)
|
||||
data = await _read_file_dict(f)
|
||||
if data is not None:
|
||||
return data
|
||||
except Exception:
|
||||
@@ -404,7 +404,7 @@ async def _push_image_to_chat(raw, prefix, request, user_dict, metadata, event_e
|
||||
if not (_OPENWEBUI_RUNTIME and request and user_dict and event_emitter):
|
||||
return False
|
||||
try:
|
||||
user = Users.get_user_by_id(user_dict.get("id"))
|
||||
user = await Users.get_user_by_id(user_dict.get("id"))
|
||||
if not user:
|
||||
return False
|
||||
upload = UploadFile(
|
||||
@@ -420,23 +420,9 @@ async def _push_image_to_chat(raw, prefix, request, user_dict, metadata, event_e
|
||||
)
|
||||
file_item = await result if inspect.iscoroutine(result) else result
|
||||
url = request.app.url_path_for("get_file_content_by_id", id=file_item.id)
|
||||
# TODO(open-webui virtualization fix): once chat-history
|
||||
# virtualization survives `files` events from tool calls again,
|
||||
# swap back to the `files` payload below. The `files` event
|
||||
# gives proper file-attachment chrome (thumbnail card + download
|
||||
# button) that the markdown-image path lacks. Verify the fix by
|
||||
# emitting `files`, then refreshing the page AND scrolling the
|
||||
# message off-screen and back — both must keep the image
|
||||
# visible. Track upstream: github.com/open-webui/open-webui
|
||||
# release notes mentioning tool calls, `files` events, or
|
||||
# virtualization. To restore, replace the block below with:
|
||||
# await event_emitter({
|
||||
# "type": "files",
|
||||
# "data": {"files": [{"type": "image", "url": url}]},
|
||||
# })
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {"content": f"\n\n"},
|
||||
"type": "files",
|
||||
"data": {"files": [{"type": "image", "url": url}]},
|
||||
})
|
||||
return True
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user