From 0fc630b34b2899599dabffffa012afd47599aa75 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:51:02 +0200 Subject: [PATCH] fix: per-user model cache used static key= (cross-user model exposure) (#25783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routers/openai.py and routers/ollama.py decorated get_all_models with `@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC cache key: get_cache_key returns `self.key` verbatim (the lambda object) without calling it, so every caller collides to ONE shared entry within the TTL. The intended per-user namespacing never happened — one user's permission-filtered model list could be served to another user (or an anonymous caller) during the cache window. The per-call hook is `key_builder=` (called as key_builder(func, *args, **kwargs)). Switch both sites to key_builder with a (func, request, user=None) signature so the key is built per call. user=None mirrors ollama's optional-user signature and stays correct whether user is passed positionally, as a kwarg, or omitted. Verified offline: old form returns the same key object for distinct users (collision); new form yields distinct openai_all_models_ / ollama_all_models_ keys, and the unauthenticated base key when no user. These two were the only @cached(key=lambda ...) sites in the backend. --- backend/open_webui/routers/ollama.py | 4 +++- backend/open_webui/routers/openai.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 238a267e7e..da3060ae7b 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -336,7 +336,9 @@ def resolve_api_config(api_configs: dict, idx: int, url: str) -> dict: @cached( ttl=MODELS_CACHE_TTL, - key=lambda _, user: f'ollama_all_models_{user.id}' if user else 'ollama_all_models', + # key_builder (not key) is the per-call hook in aiocache 0.12; `key=` is a + # static key, so a `key=lambda` collapsed every caller to one shared entry. + key_builder=lambda _func, request, user=None: f'ollama_all_models_{user.id}' if user else 'ollama_all_models', ) async def get_all_models(request: Request, user: UserModel | None = None): """Aggregate model tags from every enabled Ollama backend.""" diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 2631011a5b..a30e24d8e4 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -522,7 +522,9 @@ async def get_filtered_models(models, user, db=None): @cached( ttl=MODELS_CACHE_TTL, - key=lambda _, user: f'openai_all_models_{user.id}' if user else 'openai_all_models', + # key_builder (not key) is the per-call hook in aiocache 0.12; `key=` is a + # static key, so a `key=lambda` collapsed every caller to one shared entry. + key_builder=lambda _func, request, user=None: f'openai_all_models_{user.id}' if user else 'openai_all_models', ) async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: log.info('get_all_models()')