Skip to content

Commit a63a796

Browse files
authored
Merge pull request #4 from sena-labs/claude/magical-mccarthy-9fed0e
fix: resolve 5 bugs preventing provider icons from appearing in Open WebUI
2 parents 9f977da + 30b9413 commit a63a796

3 files changed

Lines changed: 147 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **Automatic provider-icon sync** — new `_sync_model_icons()` method writes provider icons directly into Open WebUI's Models database so they appear in the UI; controlled by the `SYNC_PROVIDER_ICONS` valve (default: enabled). Models with a manually-set icon are never overwritten
13+
- **`_is_owui_managed_icon()` helper** — distinguishes OWUI-default icons (`data:` URLs) and our own provider icons from user-set custom icons, enabling safe icon updates without clobbering user customisations
1314

1415
### Fixed
1516

1617
- **Icon sync: correct prefixed model IDs**`_sync_model_icons()` now discovers the pipe's `function_id` via `type(self).__module__` and writes DB records with the full prefixed ID (e.g. `openrouter_pipe.openai/gpt-4o`) matching what Open WebUI's frontend requests at `/models/model/profile/image`
18+
- **Icon sync: icons now actually appear in the UI** — five bugs prevented provider icons from ever showing after the first pipe load:
19+
- *Wrong skip condition*`if existing_icon:` skipped any model with *any* icon (including the generic `data:` SVG that OWUI assigns by default), so provider icons were never applied; fixed to skip only user-set custom URLs
20+
- *Race condition*`_sync_model_icons()` was called before `pipes()` returned, i.e. before OWUI registered the models; OWUI then overwrote the early insert with its own default icon; fixed by also calling `_sync_model_icons()` on cache-hit paths (until all models are confirmed synced)
21+
- *Exception swallowed retry* — DB errors added the model to `_icons_synced` anyway, permanently preventing retry; removed the erroneous add
22+
- *Insert marked as synced prematurely* — after `insert_new_model` the model was marked synced even though OWUI could overwrite it; the insert path no longer updates `_icons_synced`
23+
- *User params clobbered*`update_model_by_id` used an empty `ModelParams()`, erasing user-configured temperature/system-prompt/etc.; now preserves `existing.params`
24+
- **Icon sync: `function_id` cached at init**`type(self).__module__` is evaluated once in `__init__` instead of on every `_sync_model_icons()` call
1725
- **Streaming status event** — the "done" status event is now correctly emitted at the end of streaming responses (async generator wrapper replaces sync generator that could not `await`)
1826
- **Dead provider-icon code removed**`info.meta.profile_image_url` was included in model dicts returned by `pipes()` but Open WebUI ignores all fields except `id` and `name`; the field has been removed in favour of the new DB-sync approach
1927
- **`pipes()` response always closed** — added `finally: response.close()` to guarantee HTTP connections are returned to the session pool in all code paths (auth errors, JSON decode failures, unexpected exceptions)

openrouter_pipe.py

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ def _is_safe_url(url: str) -> bool:
7272
return isinstance(url, str) and url.lower().startswith(("http://", "https://"))
7373

7474

75+
def _is_owui_managed_icon(url: str) -> bool:
76+
"""Return True if the icon URL was set by OWUI or our sync logic.
77+
78+
data: URLs are the pipe's own SVG icon that OWUI assigns as default to all
79+
manifold child models. openrouter.ai/images/models/ URLs are the provider
80+
icons we write. Any other URL is assumed to be a user-set custom icon and
81+
must not be overwritten.
82+
"""
83+
return (
84+
not url
85+
or url.startswith("data:")
86+
or url.startswith("https://openrouter.ai/images/models/")
87+
)
88+
89+
7590
def _insert_citations(text: str, citations: Optional[List[str]]) -> str:
7691
"""Replace [n] references with markdown links (only safe HTTP URLs)."""
7792
if not citations or not text:
@@ -240,6 +255,11 @@ def __init__(self) -> None:
240255
self._models_cache_key: str = ""
241256
# Track which model IDs already have icons synced (avoids repeated DB writes)
242257
self._icons_synced: set = set()
258+
# Cache function_id once: OWUI sets __module__ to "function_{id}" at load time
259+
_fm = type(self).__module__ or ""
260+
self._function_id: Optional[str] = (
261+
_fm[len("function_"):] if _fm.startswith("function_") else None
262+
)
243263
if not self.valves.OPENROUTER_API_KEY:
244264
print("[OpenRouter Pipe] Warning: OPENROUTER_API_KEY not set")
245265

@@ -274,6 +294,11 @@ def pipes(self) -> List[dict]:
274294

275295
# Return cached models if still valid
276296
if self._models_cache_valid() and self._models_cache is not None:
297+
# Continue syncing icons on cache hits until all models are confirmed.
298+
# This resolves the race condition where OWUI registers models (and may
299+
# overwrite icons) only after the first pipes() call returns.
300+
if self.valves.SYNC_PROVIDER_ICONS and len(self._icons_synced) < len(self._models_cache):
301+
self._sync_model_icons(self._models_cache)
277302
return self._models_cache
278303

279304
headers = self._build_headers(include_content_type=False)
@@ -456,14 +481,18 @@ def _sync_model_icons(self, models: List[dict]) -> None:
456481
"""Write provider icons into Open WebUI's Models DB.
457482
458483
Open WebUI serves model icons from its database, not from the dicts
459-
returned by ``pipes()``. Crucially, Open WebUI prefixes every pipe
460-
model ID with ``{function_id}.`` (e.g. ``openrouter_pipe.openai/gpt-4o``)
461-
and the frontend requests icons using that prefixed ID. This method
462-
discovers the pipe's own *function_id* from ``type(self).__module__``
463-
(set to ``function_{id}`` by Open WebUI's module loader) and writes
464-
DB records keyed by the full prefixed ID.
465-
466-
Models that already have a custom icon in the DB are skipped.
484+
returned by ``pipes()``. OWUI prefixes every pipe model ID with
485+
``{function_id}.`` (e.g. ``openrouter_pipe.openai/gpt-4o``) and the
486+
frontend requests icons using that prefixed ID.
487+
488+
Called both on cache miss and on subsequent cache hits (until all
489+
models are confirmed synced). The cache-hit path is needed because
490+
OWUI registers models *after* ``pipes()`` returns, potentially
491+
overwriting any early insert with its own default icon; the second
492+
call finds the models already in DB and updates them correctly.
493+
494+
User-set custom icons (any URL that is not a ``data:`` URL and does not
495+
start with ``https://openrouter.ai/images/models/``) are preserved.
467496
This is a best-effort operation — failures are silently logged.
468497
"""
469498
try:
@@ -477,14 +506,10 @@ def _sync_model_icons(self, models: List[dict]) -> None:
477506
# Running outside Open WebUI (e.g. standalone tests) — skip silently
478507
return
479508

480-
# Discover the pipe's function_id. Open WebUI loads pipe modules as
481-
# ``function_{function_id}`` so type(self).__module__ exposes it.
482-
func_module = type(self).__module__ or ""
483-
if func_module.startswith("function_"):
484-
function_id = func_module[len("function_"):]
485-
else:
486-
# Cannot determine the pipe's function_id — skip icon sync
509+
# function_id was resolved once in __init__ from type(self).__module__
510+
if not self._function_id:
487511
return
512+
function_id = self._function_id
488513

489514
for model in models:
490515
model_id = model.get("id", "")
@@ -509,17 +534,28 @@ def _sync_model_icons(self, models: List[dict]) -> None:
509534
try:
510535
existing = Models.get_model_by_id(db_model_id)
511536
if existing:
512-
# If model already has a custom icon, don't overwrite it
513537
existing_icon = ""
514538
if hasattr(existing, "meta") and existing.meta:
515539
existing_icon = (
516540
getattr(existing.meta, "profile_image_url", "") or ""
517541
)
518-
if existing_icon:
542+
543+
# Skip if icon is already the correct provider URL
544+
if existing_icon == icon_url:
545+
self._icons_synced.add(model_id)
546+
continue
547+
548+
# Skip if icon was set by the user (not by OWUI or our sync).
549+
# data: URLs are OWUI defaults; openrouter.ai URLs are ours.
550+
if existing_icon and not _is_owui_managed_icon(existing_icon):
519551
self._icons_synced.add(model_id)
520552
continue
521553

522-
# Update existing model with icon
554+
# Proceed: icon is empty, an OWUI default, or one of our URLs
555+
# Update existing model with icon, preserving user-set params
556+
existing_params = ModelParams()
557+
if hasattr(existing, "params") and existing.params:
558+
existing_params = existing.params
523559
Models.update_model_by_id(
524560
db_model_id,
525561
ModelForm(
@@ -530,26 +566,34 @@ def _sync_model_icons(self, models: List[dict]) -> None:
530566
else model.get("name", model_id)
531567
),
532568
meta=ModelMeta(profile_image_url=icon_url),
533-
params=ModelParams(),
569+
params=existing_params,
534570
),
535571
)
536572
else:
537-
# Insert new model record with icon
538-
Models.insert_new_model(
539-
ModelForm(
540-
id=db_model_id,
541-
name=model.get("name", model_id),
542-
meta=ModelMeta(profile_image_url=icon_url),
543-
params=ModelParams(),
544-
),
545-
user_id="pipe:openrouter",
546-
)
573+
# Model not yet in DB — best-effort early insert.
574+
# OWUI will register models after pipes() returns and may
575+
# overwrite this record, so do NOT mark as synced here.
576+
# The next cache-hit call to _sync_model_icons will find the
577+
# model in DB and update it correctly.
578+
try:
579+
Models.insert_new_model(
580+
ModelForm(
581+
id=db_model_id,
582+
name=model.get("name", model_id),
583+
meta=ModelMeta(profile_image_url=icon_url),
584+
params=ModelParams(),
585+
),
586+
user_id="pipe:openrouter",
587+
)
588+
except Exception:
589+
pass
590+
continue # do not add to _icons_synced yet
547591

548592
self._icons_synced.add(model_id)
549593
except Exception as exc:
550594
# Best-effort — don't let icon sync break model listing
595+
# Do NOT add to _icons_synced: allow retry on next call
551596
print(f"[OpenRouter Pipe] Icon sync failed for {db_model_id}: {exc}")
552-
self._icons_synced.add(model_id) # Don't retry on every refresh
553597

554598
@staticmethod
555599
def get_provider_icon(provider: str) -> Optional[str]:

test_pipe.py

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
_insert_citations = mod._insert_citations
4444
_format_citation_list = mod._format_citation_list
4545
_OWUI_INTERNAL_KEYS = mod._OWUI_INTERNAL_KEYS
46+
_is_owui_managed_icon = mod._is_owui_managed_icon
4647

4748
# ── Helpers ───────────────────────────────────────────────────────────────────
4849
_PASS = 0
@@ -1380,7 +1381,9 @@ async def _test_pipe_no_msgs_key():
13801381
# 25d. Valve default is True
13811382
_assert(Pipe.Valves(OPENROUTER_API_KEY="k").SYNC_PROVIDER_ICONS is True, "SYNC_PROVIDER_ICONS default is True")
13821383

1383-
# 25e. Non-function module → returns early without DB calls
1384+
# 25e. No function_id → returns early without DB calls
1385+
# function_id is cached in __init__; pipes created in tests have _function_id=None
1386+
# because the test module name doesn't start with "function_".
13841387
_pipe_nofunc = Pipe()
13851388
_pipe_nofunc.valves = Pipe.Valves(OPENROUTER_API_KEY="k", SYNC_PROVIDER_ICONS=True)
13861389
_mock_Models_nf = MagicMock()
@@ -1389,25 +1392,25 @@ async def _test_pipe_no_msgs_key():
13891392
_fake_owui_nf.ModelForm = MagicMock()
13901393
_fake_owui_nf.ModelMeta = MagicMock()
13911394
_fake_owui_nf.ModelParams = MagicMock()
1392-
_orig_module_nf = Pipe.__module__
13931395
try:
13941396
sys.modules["open_webui.models.models"] = _fake_owui_nf
1395-
# Module name doesn't start with "function_" → should skip
1396-
Pipe.__module__ = "openrouter_pipe"
1397+
# _pipe_nofunc._function_id is None (no "function_" prefix at init time) → skip
1398+
_assert(_pipe_nofunc._function_id is None, "_sync_model_icons: _function_id is None outside OWUI")
13971399
_pipe_nofunc._sync_model_icons([{"id": "openai/gpt-4o", "name": "GPT-4o"}])
13981400
_assert(
13991401
not _mock_Models_nf.get_model_by_id.called,
1400-
"_sync_model_icons: skips DB when module is not function_*",
1402+
"_sync_model_icons: skips DB when _function_id is None",
14011403
)
14021404
finally:
1403-
Pipe.__module__ = _orig_module_nf
14041405
sys.modules.pop("open_webui.models.models", None)
14051406

1406-
# 25f. With function_ module → uses prefixed IDs for DB operations
1407+
# 25f. With function_id set → uses prefixed IDs; does NOT add to _icons_synced after insert
1408+
# (OWUI may overwrite the record after pipes() returns; confirmed on next call)
14071409
_pipe_func = Pipe()
14081410
_pipe_func.valves = Pipe.Valves(OPENROUTER_API_KEY="k", SYNC_PROVIDER_ICONS=True)
1411+
_pipe_func._function_id = "openrouter_pipe" # simulate OWUI module naming
14091412
_mock_Models_f = MagicMock()
1410-
_mock_Models_f.get_model_by_id.return_value = None # No existing record
1413+
_mock_Models_f.get_model_by_id.return_value = None # No existing record yet
14111414
_mock_ModelForm_f = MagicMock()
14121415
_mock_ModelMeta_f = MagicMock()
14131416
_mock_ModelParams_f = MagicMock()
@@ -1416,10 +1419,8 @@ async def _test_pipe_no_msgs_key():
14161419
_fake_owui_f.ModelForm = _mock_ModelForm_f
14171420
_fake_owui_f.ModelMeta = _mock_ModelMeta_f
14181421
_fake_owui_f.ModelParams = _mock_ModelParams_f
1419-
_orig_module_f = Pipe.__module__
14201422
try:
14211423
sys.modules["open_webui.models.models"] = _fake_owui_f
1422-
Pipe.__module__ = "function_openrouter_pipe"
14231424
_pipe_func._sync_model_icons([
14241425
{"id": "openai/gpt-4o", "name": "GPT-4o"},
14251426
{"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5"},
@@ -1440,9 +1441,6 @@ async def _test_pipe_no_msgs_key():
14401441
"_sync_model_icons: insert_new_model called for new models",
14411442
)
14421443
# Verify the ModelForm ID is prefixed
1443-
_insert_form = _mock_Models_f.insert_new_model.call_args_list[0].args[0]
1444-
_insert_id = _mock_ModelForm_f.call_args_list[0].kwargs.get("id", _mock_ModelForm_f.call_args_list[0].args[0] if _mock_ModelForm_f.call_args_list[0].args else "")
1445-
# ModelForm was called with id=prefixed_id
14461444
_form_calls = _mock_ModelForm_f.call_args_list
14471445
_form_ids = [c.kwargs.get("id", "") for c in _form_calls]
14481446
_assert(
@@ -1453,45 +1451,89 @@ async def _test_pipe_no_msgs_key():
14531451
"openrouter_pipe.anthropic/claude-3.5-sonnet" in _form_ids,
14541452
"_sync_model_icons: ModelForm uses prefixed ID (anthropic)",
14551453
)
1456-
# Verify _icons_synced tracks the RAW model IDs
1454+
# After insert (model not yet registered by OWUI), _icons_synced must NOT be updated.
1455+
# The next cache-hit call will confirm the icon is set correctly.
14571456
_assert(
1458-
"openai/gpt-4o" in _pipe_func._icons_synced,
1459-
"_sync_model_icons: _icons_synced uses raw model ID",
1457+
"openai/gpt-4o" not in _pipe_func._icons_synced,
1458+
"_sync_model_icons: _icons_synced NOT updated after insert (allows retry)",
14601459
)
14611460
finally:
1462-
Pipe.__module__ = _orig_module_f
14631461
sys.modules.pop("open_webui.models.models", None)
14641462

1465-
# 25g. Existing model with icon → skips overwrite
1463+
# 25g. Existing model with user custom icon → skips overwrite
14661464
_pipe_skip = Pipe()
14671465
_pipe_skip.valves = Pipe.Valves(OPENROUTER_API_KEY="k", SYNC_PROVIDER_ICONS=True)
1466+
_pipe_skip._function_id = "openrouter_pipe" # simulate OWUI module naming
14681467
_mock_Models_s = MagicMock()
14691468
_existing_model = MagicMock()
14701469
_existing_model.meta.profile_image_url = "https://custom-icon.example.com/icon.png"
14711470
_existing_model.name = "Custom GPT"
1471+
_existing_model.params = None
14721472
_mock_Models_s.get_model_by_id.return_value = _existing_model
14731473
_fake_owui_s = ModuleType("open_webui.models.models")
14741474
_fake_owui_s.Models = _mock_Models_s
14751475
_fake_owui_s.ModelForm = MagicMock()
14761476
_fake_owui_s.ModelMeta = MagicMock()
14771477
_fake_owui_s.ModelParams = MagicMock()
1478-
_orig_module_s = Pipe.__module__
14791478
try:
14801479
sys.modules["open_webui.models.models"] = _fake_owui_s
1481-
Pipe.__module__ = "function_openrouter_pipe"
14821480
_pipe_skip._sync_model_icons([{"id": "openai/gpt-4o", "name": "GPT-4o"}])
14831481
_assert(
14841482
not _mock_Models_s.update_model_by_id.called,
1485-
"_sync_model_icons: skips update when model has custom icon",
1483+
"_sync_model_icons: skips update when model has user custom icon",
14861484
)
14871485
_assert(
14881486
not _mock_Models_s.insert_new_model.called,
1489-
"_sync_model_icons: skips insert when model has custom icon",
1487+
"_sync_model_icons: skips insert when model has user custom icon",
1488+
)
1489+
_assert(
1490+
"openai/gpt-4o" in _pipe_skip._icons_synced,
1491+
"_sync_model_icons: adds to _icons_synced when skipping custom icon",
14901492
)
14911493
finally:
1492-
Pipe.__module__ = _orig_module_s
14931494
sys.modules.pop("open_webui.models.models", None)
14941495

1496+
# 25h. Existing model with OWUI default (data: URL) icon → updates with provider icon
1497+
_pipe_update = Pipe()
1498+
_pipe_update.valves = Pipe.Valves(OPENROUTER_API_KEY="k", SYNC_PROVIDER_ICONS=True)
1499+
_pipe_update._function_id = "openrouter_pipe" # simulate OWUI module naming
1500+
_mock_Models_u = MagicMock()
1501+
_existing_default = MagicMock()
1502+
_existing_default.name = "GPT-4o"
1503+
_existing_default.meta.profile_image_url = "data:image/svg+xml;base64,ABC123=="
1504+
_existing_default.params = None
1505+
_mock_Models_u.get_model_by_id.return_value = _existing_default
1506+
_fake_owui_u = ModuleType("open_webui.models.models")
1507+
_fake_owui_u.Models = _mock_Models_u
1508+
_fake_owui_u.ModelForm = MagicMock()
1509+
_fake_owui_u.ModelMeta = MagicMock()
1510+
_fake_owui_u.ModelParams = MagicMock()
1511+
try:
1512+
sys.modules["open_webui.models.models"] = _fake_owui_u
1513+
_pipe_update._sync_model_icons([{"id": "openai/gpt-4o", "name": "GPT-4o"}])
1514+
_assert(
1515+
_mock_Models_u.update_model_by_id.called,
1516+
"_sync_model_icons: updates model when icon is OWUI default (data: URL)",
1517+
)
1518+
_assert(
1519+
not _mock_Models_u.insert_new_model.called,
1520+
"_sync_model_icons: does not insert when model already exists",
1521+
)
1522+
_assert(
1523+
"openai/gpt-4o" in _pipe_update._icons_synced,
1524+
"_sync_model_icons: adds to _icons_synced after successful update",
1525+
)
1526+
finally:
1527+
sys.modules.pop("open_webui.models.models", None)
1528+
1529+
# 25i. _is_owui_managed_icon helper
1530+
_is_owui = mod._is_owui_managed_icon
1531+
_assert(_is_owui(""), "_is_owui_managed_icon: empty string → True (no icon)")
1532+
_assert(_is_owui("data:image/svg+xml;base64,ABC"), "_is_owui_managed_icon: data: URL → True")
1533+
_assert(_is_owui("https://openrouter.ai/images/models/openai.svg"), "_is_owui_managed_icon: openrouter.ai URL → True")
1534+
_assert(not _is_owui("https://custom-icon.example.com/icon.png"), "_is_owui_managed_icon: external URL → False")
1535+
_assert(not _is_owui("https://cdn.openai.com/logo.png"), "_is_owui_managed_icon: other https URL → False")
1536+
14951537
# ══════════════════════════════════════════════════════════════════════════════
14961538
# Summary
14971539
# ══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)