1818import re
1919import time
2020import traceback
21- from typing import Callable , Generator , List , Optional , Union
21+ from typing import AsyncGenerator , Callable , Generator , List , Optional , Union
2222
2323import requests
2424from pydantic import BaseModel , Field , field_validator
3838# Cache TTL for model list (seconds)
3939_MODELS_CACHE_TTL = 300.0 # 5 minutes
4040
41+ # Provider icons — synced into the Open WebUI Models database by
42+ # _sync_model_icons() so the frontend can serve them via
43+ # /models/model/profile/image. Disable with SYNC_PROVIDER_ICONS = False.
4144_PROVIDER_ICONS = {
4245 "openai" : "https://openrouter.ai/images/models/openai.svg" ,
4346 "anthropic" : "https://openrouter.ai/images/models/anthropic.svg" ,
@@ -203,6 +206,10 @@ class Valves(BaseModel):
203206 == "true" ,
204207 description = "Enable prompt caching for Anthropic models (reduces cost on repeated long prompts). No effect on other providers" ,
205208 )
209+ SYNC_PROVIDER_ICONS : bool = Field (
210+ default = os .getenv ("OPENROUTER_SYNC_ICONS" , "true" ).lower () == "true" ,
211+ description = "Automatically sync provider icons into Open WebUI's model database so they appear in the UI" ,
212+ )
206213 REQUEST_TIMEOUT : int = Field (
207214 default = int (os .getenv ("OPENROUTER_REQUEST_TIMEOUT" , "90" )),
208215 gt = 0 ,
@@ -231,6 +238,8 @@ def __init__(self) -> None:
231238 self ._models_cache : Optional [List [dict ]] = None
232239 self ._models_cache_ts : float = 0.0
233240 self ._models_cache_key : str = ""
241+ # Track which model IDs already have icons synced (avoids repeated DB writes)
242+ self ._icons_synced : set = set ()
234243 if not self .valves .OPENROUTER_API_KEY :
235244 print ("[OpenRouter Pipe] Warning: OPENROUTER_API_KEY not set" )
236245
@@ -338,12 +347,10 @@ def pipes(self) -> List[dict]:
338347 continue
339348
340349 model_name = model .get ("name" , model_id )
341- icon_url = self ._get_provider_icon (provider_key )
342350
343351 model_dict = {
344352 "id" : model_id ,
345353 "name" : f"{ prefix } { model_name } " ,
346- "info" : {"meta" : {"profile_image_url" : icon_url or "" }},
347354 }
348355
349356 models .append (model_dict )
@@ -363,6 +370,10 @@ def pipes(self) -> List[dict]:
363370 self ._models_cache_ts = time .monotonic ()
364371 self ._models_cache_key = f"{ self .valves .OPENROUTER_API_KEY } |{ self .valves .FREE_ONLY } |{ self .valves .MODEL_PROVIDERS } |{ self .valves .INVERT_PROVIDER_LIST } |{ self .valves .MODEL_PREFIX } "
365372
373+ # Sync provider icons into Open WebUI's Models database
374+ if self .valves .SYNC_PROVIDER_ICONS :
375+ self ._sync_model_icons (models )
376+
366377 return models
367378
368379 @staticmethod
@@ -377,11 +388,11 @@ async def pipe(
377388 body : dict ,
378389 __user__ : Optional [dict ] = None ,
379390 __event_emitter__ : Optional [Callable ] = None ,
380- ) -> Union [str , Generator [str , None , None ]]:
391+ ) -> Union [str , Generator [str , None , None ], AsyncGenerator [ str , None ] ]:
381392 """Route a chat completion request to OpenRouter (stream or non-stream).
382393
383- Note: This async method returns a sync generator for streaming ,
384- as required by the Open WebUI pipe protocol .
394+ Returns an async generator for streaming (allows proper status cleanup) ,
395+ or a plain string for non-streaming responses .
385396 """
386397 if not self .valves .OPENROUTER_API_KEY :
387398 return "OpenRouter Error: OPENROUTER_API_KEY not configured. Set it in Settings → Connections."
@@ -414,18 +425,17 @@ async def pipe(
414425 if stream :
415426 gen = self ._stream_response (headers , payload )
416427
417- # Wrap the sync generator to emit status done after streaming finishes
428+ # Wrap in an async generator so we can await the done event
418429 if __event_emitter__ :
419- original_gen = gen
420- def _wrap_stream ():
430+ async def _wrap_stream ():
421431 try :
422- yield from original_gen
432+ for chunk in gen :
433+ yield chunk
423434 finally :
424- pass # done event emitted below
425- gen = _wrap_stream ()
426- # We can't await inside a sync generator, so we emit done
427- # after the generator is fully consumed by the framework.
428- # Open WebUI handles this; we emit here as best-effort.
435+ await __event_emitter__ (
436+ {"type" : "status" , "data" : {"description" : "" , "done" : True }}
437+ )
438+ return _wrap_stream ()
429439
430440 return gen
431441
@@ -438,7 +448,107 @@ def _wrap_stream():
438448
439449 return result
440450
441- def _get_provider_icon (self , provider : str ) -> Optional [str ]:
451+ def _sync_model_icons (self , models : List [dict ]) -> None :
452+ """Write provider icons into Open WebUI's Models DB.
453+
454+ Open WebUI serves model icons from its database, not from the dicts
455+ returned by ``pipes()``. Crucially, Open WebUI prefixes every pipe
456+ model ID with ``{function_id}.`` (e.g. ``openrouter_pipe.openai/gpt-4o``)
457+ and the frontend requests icons using that prefixed ID. This method
458+ discovers the pipe's own *function_id* from ``type(self).__module__``
459+ (set to ``function_{id}`` by Open WebUI's module loader) and writes
460+ DB records keyed by the full prefixed ID.
461+
462+ Models that already have a custom icon in the DB are skipped.
463+ This is a best-effort operation — failures are silently logged.
464+ """
465+ try :
466+ from open_webui .models .models import (
467+ ModelForm ,
468+ ModelMeta ,
469+ ModelParams ,
470+ Models ,
471+ )
472+ except ImportError :
473+ # Running outside Open WebUI (e.g. standalone tests) — skip silently
474+ return
475+
476+ # Discover the pipe's function_id. Open WebUI loads pipe modules as
477+ # ``function_{function_id}`` so type(self).__module__ exposes it.
478+ func_module = type (self ).__module__ or ""
479+ if func_module .startswith ("function_" ):
480+ function_id = func_module [len ("function_" ):]
481+ else :
482+ # Cannot determine the pipe's function_id — skip icon sync
483+ return
484+
485+ for model in models :
486+ model_id = model .get ("id" , "" )
487+ if not model_id or model_id == "error" :
488+ continue
489+
490+ # Skip if already synced this session
491+ if model_id in self ._icons_synced :
492+ continue
493+
494+ # Determine provider icon
495+ parts = model_id .split ("/" , 1 )
496+ provider_key = parts [0 ].lower () if len (parts ) > 1 else ""
497+ icon_url = _PROVIDER_ICONS .get (provider_key )
498+ if not icon_url :
499+ self ._icons_synced .add (model_id )
500+ continue
501+
502+ # Build the prefixed ID that Open WebUI uses in the frontend
503+ db_model_id = f"{ function_id } .{ model_id } "
504+
505+ try :
506+ existing = Models .get_model_by_id (db_model_id )
507+ if existing :
508+ # If model already has a custom icon, don't overwrite it
509+ existing_icon = ""
510+ if hasattr (existing , "meta" ) and existing .meta :
511+ existing_icon = (
512+ getattr (existing .meta , "profile_image_url" , "" ) or ""
513+ )
514+ if existing_icon :
515+ self ._icons_synced .add (model_id )
516+ continue
517+
518+ # Update existing model with icon
519+ Models .update_model_by_id (
520+ db_model_id ,
521+ ModelForm (
522+ id = db_model_id ,
523+ name = (
524+ existing .name
525+ if hasattr (existing , "name" )
526+ else model .get ("name" , model_id )
527+ ),
528+ meta = ModelMeta (profile_image_url = icon_url ),
529+ params = ModelParams (),
530+ ),
531+ )
532+ else :
533+ # Insert new model record with icon
534+ Models .insert_new_model (
535+ ModelForm (
536+ id = db_model_id ,
537+ name = model .get ("name" , model_id ),
538+ meta = ModelMeta (profile_image_url = icon_url ),
539+ params = ModelParams (),
540+ ),
541+ user_id = "pipe:openrouter" ,
542+ )
543+
544+ self ._icons_synced .add (model_id )
545+ except Exception as exc :
546+ # Best-effort — don't let icon sync break model listing
547+ print (f"[OpenRouter Pipe] Icon sync failed for { db_model_id } : { exc } " )
548+ self ._icons_synced .add (model_id ) # Don't retry on every refresh
549+
550+ @staticmethod
551+ def get_provider_icon (provider : str ) -> Optional [str ]:
442552 """Return icon URL for the given provider."""
443553 return _PROVIDER_ICONS .get (provider .lower ())
444554
0 commit comments