diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py index dd0120d7b9..43dcf70a7b 100644 --- a/backend/open_webui/socket/utils.py +++ b/backend/open_webui/socket/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import uuid @@ -50,6 +51,10 @@ class RedisLock: class RedisDict: def __init__(self, name, redis_url, redis_sentinels=[], redis_cluster=False): self.name = name + # Per-process cache of the last payload fingerprint written by set(). + # Used to skip redundant HSET round-trips when the model list hasn't + # changed — the dominant Redis write source on busy multi-pod setups. + self._last_signature: str | None = None self.redis = get_redis_connection( redis_url, redis_sentinels, @@ -90,6 +95,20 @@ class RedisDict: def set(self, mapping: dict): if not mapping: self.redis.delete(self.name) + self._last_signature = None + return + + # Serialize values once — reused for both the fingerprint and the write. + serialized = {k: json.dumps(v) for k, v in mapping.items()} + + # Skip the write when the prepared mapping is identical to the last one + # this process wrote. The check is per-instance (not distributed), but + # still eliminates the majority of redundant writes because each pod + # typically produces the same model list on consecutive refreshes. + signature = hashlib.sha256( + json.dumps(serialized, sort_keys=True).encode() + ).hexdigest() + if signature == self._last_signature: return # Fetch existing keys before writing so we know which ones to remove. @@ -101,10 +120,12 @@ class RedisDict: # HSET first (add/update all new values), then HDEL (remove stale keys). # We never DELETE the whole hash — this eliminates the race window # where concurrent readers would see an empty models dict. - self.redis.hset(self.name, mapping={k: json.dumps(v) for k, v in mapping.items()}) + self.redis.hset(self.name, mapping=serialized) if keys_to_remove: self.redis.hdel(self.name, *keys_to_remove) + self._last_signature = signature + def get(self, key, default=None): try: return self[key] @@ -113,6 +134,7 @@ class RedisDict: def clear(self): self.redis.delete(self.name) + self._last_signature = None def update(self, other=None, **kwargs): if other is not None: diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index c6d87dfc54..60c5836637 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -2,7 +2,6 @@ import asyncio import copy import logging import sys -import time from aiocache import cached from fastapi import Request @@ -36,7 +35,7 @@ async def fetch_ollama_models(request: Request, user: UserModel = None): 'id': model['model'], 'name': model['name'], 'object': 'model', - 'created': int(time.time()), + 'created': 0, 'owned_by': 'ollama', 'ollama': model, 'loaded': 'expires_at' in model, @@ -100,7 +99,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) 'meta': model['meta'], }, 'object': 'model', - 'created': int(time.time()), + 'created': 0, 'owned_by': 'arena', 'arena': True, } @@ -116,7 +115,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) 'meta': DEFAULT_ARENA_MODEL['meta'], }, 'object': 'model', - 'created': int(time.time()), + 'created': 0, 'owned_by': 'arena', 'arena': True, }