From 243a39dc9d1f2daea163047df87165227fb2a763 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:25:53 +0200 Subject: [PATCH] perf: read the model pool with one HGETALL instead of one HGET per model (#27821) `request.app.state.MODELS` is a `RedisDict` when Redis is configured. Unpacking it with `{**pool}` makes Python call `keys()` and then `__getitem__` once per key, which is one HKEYS plus one HGET per model, issued sequentially through a synchronous client. At 200 models that is 201 blocking Redis round trips per call. `RedisDict.items()` is a single HGETALL, so `dict(pool.items())` fetches the same data in one round trip. `utils/chat.py:184` already does exactly this and carries a comment explaining why; these ten call sites were missed. They are on the direct-connection branch of the task endpoints (title, tags, follow-up, autocomplete, query generation and the rest), of `chat_completed`, and of context compaction, so they run for background tasks fired on ordinary chat turns. Behaviour is unchanged. The merged mapping is identical, the explicitly added direct model still overrides any pool entry with the same id, and when Redis is not configured the pool is a plain dict where `dict(d.items())` and `{**d}` are equivalent. It also closes a race. `RedisDict.set` writes with HSET and then HDELs the stale keys, so a key returned by HKEYS could be deleted before its HGET arrived, raising `KeyError` out of the dict literal and failing the request mid model refresh. The old path could likewise observe a mix of pre- and post-refresh entries. HGETALL is atomic, so the caller now always sees one coherent snapshot. --- backend/open_webui/routers/tasks.py | 16 ++++++++-------- backend/open_webui/utils/chat.py | 2 +- backend/open_webui/utils/middleware.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 4e377e2b78..bce50b744e 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -117,7 +117,7 @@ async def generate_title(request: Request, form_data: dict, user=Depends(get_ver if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -201,7 +201,7 @@ async def generate_follow_ups(request: Request, form_data: dict, user=Depends(ge if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -271,7 +271,7 @@ async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -335,7 +335,7 @@ async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get async def generate_image_prompt(request: Request, form_data: dict, user=Depends(get_verified_user)): if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -417,7 +417,7 @@ async def generate_queries(request: Request, form_data: dict, user=Depends(get_v if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -498,7 +498,7 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -562,7 +562,7 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend async def generate_emoji(request: Request, form_data: dict, user=Depends(get_verified_user)): if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: @@ -628,7 +628,7 @@ async def generate_emoji(request: Request, form_data: dict, user=Depends(get_ver async def generate_moa_response(request: Request, form_data: dict, user=Depends(get_verified_user)): if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index fda6320749..db5793c7e4 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -316,7 +316,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any): if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 271d072992..86cb3cd3eb 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2347,7 +2347,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): if is_saved_chat_id(chat_id) and user_message_id: if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): compaction_models = { - **request.app.state.MODELS, + **dict(request.app.state.MODELS.items()), request.state.model['id']: request.state.model, } else: