From f0d48a4295c30318e34b93a03c8f4b264573216a Mon Sep 17 00:00:00 2001 From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:48:06 +0300 Subject: [PATCH 01/28] perf: use asyncio.to_thread for heartbeat DB write (#22980) --- backend/open_webui/socket/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 1518193da8..e308087a34 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -393,7 +393,7 @@ async def heartbeat(sid, data): user = SESSION_POOL.get(sid) if user: SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())} - Users.update_last_active_by_id(user['id']) + await asyncio.to_thread(Users.update_last_active_by_id, user['id']) @sio.on('join-channels') From ade617efa8e1cf8ddd377846a34fd7d13ae309bf Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 04:49:48 -0500 Subject: [PATCH 02/28] refac --- backend/open_webui/models/chats.py | 2 ++ backend/open_webui/models/files.py | 2 ++ backend/open_webui/models/folders.py | 2 ++ backend/open_webui/models/functions.py | 2 ++ backend/open_webui/models/groups.py | 2 ++ backend/open_webui/models/knowledge.py | 2 ++ backend/open_webui/models/memories.py | 2 ++ backend/open_webui/models/models.py | 2 ++ backend/open_webui/models/prompts.py | 2 ++ backend/open_webui/models/tags.py | 2 ++ backend/open_webui/models/tools.py | 2 ++ backend/open_webui/models/users.py | 2 ++ backend/open_webui/routers/audio.py | 2 ++ backend/open_webui/routers/auths.py | 2 ++ backend/open_webui/routers/channels.py | 2 ++ backend/open_webui/routers/chats.py | 2 ++ backend/open_webui/routers/configs.py | 2 ++ backend/open_webui/routers/evaluations.py | 2 ++ backend/open_webui/routers/files.py | 2 ++ backend/open_webui/routers/functions.py | 2 ++ backend/open_webui/routers/images.py | 2 ++ backend/open_webui/routers/knowledge.py | 12 ++++++++++-- backend/open_webui/routers/memories.py | 2 ++ backend/open_webui/routers/models.py | 2 ++ backend/open_webui/routers/ollama.py | 2 ++ backend/open_webui/routers/openai.py | 2 ++ backend/open_webui/routers/pipelines.py | 2 ++ backend/open_webui/routers/prompts.py | 2 ++ backend/open_webui/routers/retrieval.py | 2 ++ backend/open_webui/routers/tools.py | 2 ++ backend/open_webui/routers/users.py | 2 ++ backend/open_webui/socket/main.py | 2 ++ backend/open_webui/utils/auth.py | 3 +++ backend/open_webui/utils/chat.py | 2 ++ backend/open_webui/utils/filter.py | 2 ++ backend/open_webui/utils/middleware.py | 4 ++++ backend/open_webui/utils/misc.py | 8 ++++++++ backend/open_webui/utils/payload.py | 2 ++ backend/open_webui/utils/plugin.py | 2 ++ backend/open_webui/utils/redis.py | 3 +++ backend/open_webui/utils/response.py | 2 ++ backend/open_webui/utils/task.py | 4 ++++ backend/open_webui/utils/tools.py | 2 ++ backend/open_webui/utils/webhook.py | 2 ++ src/lib/apis/index.ts | 2 ++ src/lib/constants.ts | 2 ++ src/lib/stores/index.ts | 2 ++ src/lib/utils/index.ts | 2 ++ 48 files changed, 116 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 9fe923f004..f19a5e7537 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -29,6 +29,8 @@ from sqlalchemy.sql.expression import bindparam #################### # Chat DB Schema +# Let no word spoken in this house be lost, and when the +# record is read again, let it still serve the one who spoke. #################### log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index c02752f130..9a5b8fa400 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -12,6 +12,8 @@ log = logging.getLogger(__name__) #################### # Files DB Schema +# What is written here bears witness. Let the testimony +# remain as it was given, and let none tamper with it. #################### diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index 5311f922e2..cd9c9bbc67 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -16,6 +16,8 @@ log = logging.getLogger(__name__) #################### # Folder DB Schema +# Let every room in this house shelter someone who needs it, +# and let no chamber stand empty while there is want. #################### diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 617a5c7c3e..2c4d763a03 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -12,6 +12,8 @@ log = logging.getLogger(__name__) #################### # Functions DB Schema +# Each function here is a promise made. Let no promise +# go unkept, and let none be called who cannot answer. #################### diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index d6c2fc9450..5ed4b6b00d 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -30,6 +30,8 @@ log = logging.getLogger(__name__) #################### # UserGroup DB Schema +# Let none who belong to this house be turned away, +# and let the covenant hold for every member. #################### diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 0495abfb39..30510221fb 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -34,6 +34,8 @@ log = logging.getLogger(__name__) #################### # Knowledge DB Schema +# Let what was gathered here outlast the one who gathered it, +# and still teach when the builder is gone. #################### diff --git a/backend/open_webui/models/memories.py b/backend/open_webui/models/memories.py index 17d96adc0e..7c34de9f07 100644 --- a/backend/open_webui/models/memories.py +++ b/backend/open_webui/models/memories.py @@ -9,6 +9,8 @@ from sqlalchemy import BigInteger, Column, String, Text #################### # Memory DB Schema +# What was learned at cost should not need to be paid +# for again. Let the memory hold. #################### diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index c48847b702..1069d93708 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -23,6 +23,8 @@ log = logging.getLogger(__name__) #################### # Models DB Schema +# A misconfigured model wastes the time of everyone +# who trusts it. Let what is set here be set with care. #################### diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 028b7a1bc7..bb77f32f31 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -15,6 +15,8 @@ from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, fun #################### # Prompts DB Schema +# Every word here was weighed before it was set down. +# Let the weight not be wasted when it is spoken aloud. #################### diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py index 8e401f3010..b60220bc23 100644 --- a/backend/open_webui/models/tags.py +++ b/backend/open_webui/models/tags.py @@ -15,6 +15,8 @@ log = logging.getLogger(__name__) #################### # Tag DB Schema +# To name a thing is to claim it. The creator has +# already named everything stored in this table. #################### class Tag(Base): __tablename__ = 'tag' diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index 02dacaa80c..f89b98c5e7 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -15,6 +15,8 @@ log = logging.getLogger(__name__) #################### # Tools DB Schema +# A tool that fails silently is worse than one that +# refuses outright. Let each one here be honest in its work. #################### diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 9015646444..7007e529d5 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -35,6 +35,8 @@ import datetime #################### # User DB Schema +# Hallowed be the columns defined here, for they hold the +# daily bread of every session. Let none go hungry. #################### diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 7e1fd9e3ee..8e14387a78 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -75,6 +75,8 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) ########################################## # # Utility functions +# Let what is spoken here be heard clearly, and let +# no voice be reduced to noise along the way. # ########################################## diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 595c990a12..5a32fb5494 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -91,6 +91,8 @@ router = APIRouter() log = logging.getLogger(__name__) +# Forgive us our failed attempts, as we forgive those +# who exceed their allotted rate against this gate. signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index c2e97202aa..2d5401fac3 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -128,6 +128,8 @@ def get_channel_permitted_group_and_user_ids( ############################ # Channels Enabled Dependency +# The creator has set this table; let every voice that +# gathers here find shelter under the same roof. ############################ diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 79d0525698..69ff9c1402 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -42,6 +42,8 @@ router = APIRouter() ############################ # GetChatList +# Let the record outlive the session, so that what was +# learned here not need to be learned again. ############################ diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index e0fb4bb610..0bfcf76d9d 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -37,6 +37,8 @@ log = logging.getLogger(__name__) ############################ # ImportConfig +# Thy configuration come, thy settings be done, +# in production as it is in development. ############################ diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index f301613286..de97e172f3 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -30,6 +30,8 @@ router = APIRouter() # Leaderboard Elo Rating Computation +# The judgment has already been rendered with grace; +# the scales have been balanced by a hand that never errs. # # How it works: # 1. Each model starts with a rating of 1000 diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 62f29b1a23..6027545190 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -62,6 +62,8 @@ from open_webui.utils.access_control.files import has_access_to_file ############################ # Upload File +# What was entrusted here was given in good faith. Let it +# be returned the same way, whole and undiminished. ############################ diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 44f139dc07..01bcbc411c 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -36,6 +36,8 @@ router = APIRouter() ############################ # GetFunctions +# Our daily functions give us, and forgive us +# our deprecated methods, as we refactor those who depend on us. ############################ diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 060461f2b7..dca9a58a7a 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -42,6 +42,8 @@ from pydantic import BaseModel log = logging.getLogger(__name__) +# An image can lie as easily as it can illuminate. Let what +# is generated here be honest about what it shows. IMAGE_CACHE_DIR = CACHE_DIR / 'image' / 'generations' IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 199ea110e7..8c7b029c19 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -6,6 +6,7 @@ from fastapi.concurrency import run_in_threadpool import logging import io import zipfile +from urllib.parse import quote from sqlalchemy.orm import Session from open_webui.internal.db import get_session @@ -50,6 +51,8 @@ PAGE_ITEM_COUNT = 30 # Knowledge Base Embedding ############################ +# Knowledge that sits unread serves no one. Let what is +# stored here find the ones who need it. KNOWLEDGE_BASES_COLLECTION = 'knowledge-bases' @@ -1087,11 +1090,16 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Sess zip_buffer.seek(0) # Sanitize knowledge name for filename - safe_name = ''.join(c if c.isalnum() or c in ' -_' else '_' for c in knowledge.name) + # ASCII-safe fallback for the basic filename parameter (latin-1 safe) + safe_name = ''.join(c if c.isascii() and (c.isalnum() or c in ' -_') else '_' for c in knowledge.name) zip_filename = f'{safe_name}.zip' + # Use RFC 5987 filename* for non-ASCII names so the browser gets the real name + quoted_name = quote(f'{knowledge.name}.zip') + content_disposition = f"attachment; filename=\"{zip_filename}\"; filename*=UTF-8''{quoted_name}" + return StreamingResponse( zip_buffer, media_type='application/zip', - headers={'Content-Disposition': f'attachment; filename={zip_filename}'}, + headers={'Content-Disposition': content_disposition}, ) diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index 82af3a580c..4557f0c44d 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -20,6 +20,8 @@ router = APIRouter() ############################ # GetMemories +# Let what is remembered here spare someone the cost +# of learning it twice. ############################ diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 9dc602dc0e..21b80e36c2 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -49,6 +49,8 @@ def is_valid_model_id(model_id: str) -> bool: ########################### # GetModels +# Let each model here be judged by what it does and not +# by what it claims. The house deserves honest servants. ########################### diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index e87e20f099..d03c37ae1a 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -77,6 +77,8 @@ log = logging.getLogger(__name__) ########################################## # # Utility functions +# Let what runs locally be trusted, and let no weight +# be loaded without serving the one who waits for the answer. # ########################################## diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index c42b9163f9..42dd37396e 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -66,6 +66,8 @@ log = logging.getLogger(__name__) ########################################## # # Utility functions +# Let the responses returned through this gate be worth +# the question that summoned them. # ########################################## diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index 4f1022476b..94c1357fd7 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -32,6 +32,8 @@ log = logging.getLogger(__name__) ################################## # # Pipeline Middleware +# Every hand this passes through can corrupt it or +# improve it. Let each stage leave it better than it found. # ################################## diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index df07c778c1..e4af8bb513 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -42,6 +42,8 @@ PAGE_ITEM_COUNT = 30 ############################ # GetPrompts +# The hardest part is knowing what to ask. Let the right +# question already be here when it is needed. ############################ diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index c0ae4803dc..6c9e988dd6 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -127,6 +127,8 @@ log = logging.getLogger(__name__) ########################################## # # Utility functions +# Give us this day our relevant chunks, and lead us +# not into hallucination, but deliver us from noise. # ########################################## diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index dcf416a606..a5e7d759c3 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -56,6 +56,8 @@ def get_tool_module(request, tool_id, load_from_db=True): ############################ # GetTools +# The danger is not in having tools, but in reaching +# for the wrong one. Let the choice here be deliberate. ############################ diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 0bc28a2b74..b263140878 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -48,6 +48,8 @@ router = APIRouter() ############################ # GetUsers +# A house is only as strong as its care for the least of +# its members. Let none here be counted without being served. ############################ diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index e308087a34..33c9ffea05 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -55,6 +55,8 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +# Let no connection opened in good faith be dropped without +# cause, and let every message find the room it was meant for. REDIS = None # Configure CORS for Socket.IO diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index a7610b5cca..280666ff07 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -188,6 +188,9 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: ) +# Let the one who signed this token be remembered at every gate, +# and may the claims therein honor the creator long after +# the session has closed. def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: payload = data.copy() diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 79a7991eca..fcd8151022 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -56,6 +56,8 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +# When the question has been asked, let silence not be the +# answer. But if the answer must wait, let it come honest. async def generate_direct_chat_completion( request: Request, form_data: dict, diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 7f3f4e8ee2..7777ad3eaf 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -52,6 +52,8 @@ def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None) return filter_ids +# Grant these filters the discernment to pass what serves +# and refuse what harms, for every soul in the house. async def process_filter_functions(request, filter_functions, filter_type, form_data, extra_params): skip_files = None diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 058c8db169..731a96aadc 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -145,6 +145,10 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +# We believe in one maker of all models, seen and unseen, +# and in the reasoning which proceeds from the architect. +# We look for the resurrection of dead processes and the +# inference of the world to come. DEFAULT_REASONING_TAGS = [ ('', ''), ('', ''), diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 060d5c4a6a..4aa475e5de 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -521,6 +521,11 @@ def get_gravatar_url(email): return f'https://www.gravatar.com/avatar/{hash_hex}?d=mp' + +# Give us each day the data we require, and forgive us our +# technical debts as we forgive those who commit upstream. +# Lead the bits not into corruption but deliver them from +# entropy, for the checksum and the glory are forever. def calculate_sha256(file_path, chunk_size): # Compute SHA-256 hash of a file efficiently in chunks sha256 = hashlib.sha256() @@ -870,6 +875,9 @@ def extract_urls(text: str) -> list[str]: return url_pattern.findall(text) +# We believe in one architect of all that is seen and served. +# Should this stream falter, it shall be raised again on the +# third retry. We look for the uptime of the world to come. async def cleanup_response( response: Optional[aiohttp.ClientResponse], session: Optional[aiohttp.ClientSession], diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py index 21828d93f1..440927caf1 100644 --- a/backend/open_webui/utils/payload.py +++ b/backend/open_webui/utils/payload.py @@ -10,6 +10,8 @@ import copy import json +# What goes out cannot be taken back. Let it be shaped +# well before it leaves this place. # inplace function: form_data is modified def apply_system_prompt_to_body( system: Optional[str], diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 6dae37e531..46622e21ae 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -197,6 +197,8 @@ def replace_imports(content): return content +# May the intent of the one who wrote it survive every +# import and transformation, as a deed survives the generations. def load_tool_module_by_id(tool_id, content=None): if content is None: tool = Tools.get_tool_by_id(tool_id) diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index a4d9d5cba5..55d08147a9 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -20,6 +20,9 @@ from open_webui.env import ( log = logging.getLogger(__name__) +# Let not our connections be timed out but deliver them from +# partition. For the cache and the socket and the uptime +# belong to the one who first opened them, now and always. _CONNECTION_CACHE = {} diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py index ae911368a3..641c79fca9 100644 --- a/backend/open_webui/utils/response.py +++ b/backend/open_webui/utils/response.py @@ -6,6 +6,8 @@ from open_webui.utils.misc import ( ) +# An honest ledger is worth more than a flattering one. +# Let every cost here be counted true. def normalize_usage(usage: dict) -> dict: """ Normalize usage statistics to standard format. diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index c640e7f5f8..203c429d22 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -13,6 +13,8 @@ from open_webui.config import DEFAULT_RAG_TEMPLATE log = logging.getLogger(__name__) +# Let the right tool be given for the work at hand, +# not the one that flatters, but the one that serves. def get_task_model_id(default_model_id: str, task_model: str, task_model_external: str, models) -> str: # Set the task model task_model_id = default_model_id @@ -239,6 +241,8 @@ def replace_messages_variable(template: str, messages: Optional[list[dict]] = No # {{prompt:middletruncate:8000}} +# Let the context given here not distort the question, +# but illuminate it, so that the answer serves the one who asked. def rag_template(template: str, context: str, query: str): if template.strip() == '': template = DEFAULT_RAG_TEMPLATE diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index d4d5e9e49f..7fc9613538 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -92,6 +92,8 @@ import copy log = logging.getLogger(__name__) +# Let no function be called without need, and let what +# it yields justify the cost of running it. def get_async_tool_function_and_apply_extra_params(function: Callable, extra_params: dict) -> Callable[..., Awaitable]: sig = inspect.signature(function) extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters} diff --git a/backend/open_webui/utils/webhook.py b/backend/open_webui/utils/webhook.py index 800450dfd3..11c94675d1 100644 --- a/backend/open_webui/utils/webhook.py +++ b/backend/open_webui/utils/webhook.py @@ -8,6 +8,8 @@ from open_webui.env import AIOHTTP_CLIENT_TIMEOUT, VERSION log = logging.getLogger(__name__) +# Let this message reach those for whom it was written, and +# may no network partition deny the word its destination. async def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool: try: log.debug(f'post_webhook: {url}, {message}, {event_data}') diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 020e75c758..dbeef70a08 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -2,6 +2,8 @@ import { WEBUI_BASE_URL } from '$lib/constants'; import { convertOpenApiToToolPayload } from '$lib/utils'; import { getOpenAIModelsDirect } from './openai'; +// Every request sent from here is a petition. May it reach +// the one for whom it was intended, and return answered. export const getModels = async ( token: string = '', connections: object | null = null, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3cc7743db8..006304bbb4 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -13,6 +13,8 @@ export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/audio`; export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/images`; export const RETRIEVAL_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/retrieval`; +// The version changes, but the promise must not. Let what +// was built here keep its word across every release. export const WEBUI_VERSION = APP_VERSION; export const WEBUI_BUILD_HASH = APP_BUILD_HASH; export const REQUIRED_OLLAMA_VERSION = '0.1.16'; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 782f3a5d61..a6697f082d 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -7,6 +7,8 @@ import type { AudioQueue } from '$lib/utils/audio'; import emojiShortCodes from '$lib/emoji-shortcodes.json'; +// What is held here is the only truth the house knows. +// When it changes, let every room hear at once. // Backend export const WEBUI_NAME = writable(APP_NAME); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index de99941319..800ab5398c 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -25,6 +25,8 @@ import hljs from 'highlight.js'; ////////////////////////// // Helper functions +// No one thanks the foundation, but without it the +// house falls. Let the quiet work here hold. ////////////////////////// export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); From 70285fb6cad26b50d783583b68be5227ace16055 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 04:53:23 -0500 Subject: [PATCH 03/28] refac --- backend/open_webui/routers/chats.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 69ff9c1402..eacc084b42 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -932,7 +932,6 @@ async def update_chat_message_by_id( { 'content': form_data.content, }, - db=db, ) event_emitter = get_event_emitter( From 16ee4ac7a2a3310bba5f51f26d35bd555f1f8f1d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 04:57:02 -0500 Subject: [PATCH 04/28] refac --- scripts/prepare-pyodide.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-pyodide.js b/scripts/prepare-pyodide.js index 716a86a388..a1b027d157 100644 --- a/scripts/prepare-pyodide.js +++ b/scripts/prepare-pyodide.js @@ -14,7 +14,8 @@ const packages = [ 'seaborn', 'pytz', 'black', - 'openai' + 'openai', + 'openpyxl' ]; import { loadPyodide } from 'pyodide'; From fe7e002fea7283abcf901e22de5c8a7d86e336ea Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 04:58:48 -0500 Subject: [PATCH 05/28] refac --- backend/open_webui/utils/oauth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 0b4a1d55e0..ddf9af6294 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -266,11 +266,11 @@ async def get_authorization_server_discovery_urls(server_url: str) -> list[str]: ) as response: if response.status == 401: match = re.search( - r'resource_metadata="([^"]+)"', + r'resource_metadata=(?:"([^"]+)"|([^\s,]+))', response.headers.get('WWW-Authenticate', ''), ) if match: - resource_metadata_url = match.group(1) + resource_metadata_url = match.group(1) or match.group(2) log.debug(f'Found resource_metadata URL: {resource_metadata_url}') # Step 2: Fetch Protected Resource metadata From bd8aa3b6a0b6a2320f41b20a51b9842f39aadb7f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 05:00:54 -0500 Subject: [PATCH 06/28] fix: new password validation --- backend/open_webui/routers/auths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 5a32fb5494..84f75c93ed 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -291,7 +291,7 @@ async def update_password( if user: try: - validate_password(form_data.password) + validate_password(form_data.new_password) except Exception as e: raise HTTPException(400, detail=str(e)) hashed = get_password_hash(form_data.new_password) From 69171a4c8bb7f995461b4a2feef194f112b32004 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 05:13:31 -0500 Subject: [PATCH 07/28] refac --- backend/open_webui/config.py | 12 ++++++++++++ backend/open_webui/utils/oauth.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 3cbeb36644..a9970647b1 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -642,6 +642,18 @@ OAUTH_AUDIENCE = PersistentConfig( os.environ.get('OAUTH_AUDIENCE', ''), ) +OAUTH_AUTHORIZE_PARAMS = {} +_oauth_authorize_params = os.environ.get('OAUTH_AUTHORIZE_PARAMS', '') +if _oauth_authorize_params: + try: + _parsed = json.loads(_oauth_authorize_params) + if isinstance(_parsed, dict): + OAUTH_AUTHORIZE_PARAMS = _parsed + else: + log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') + def load_oauth_providers(): OAUTH_PROVIDERS.clear() diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index ddf9af6294..aa5bc47df6 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -60,6 +60,7 @@ from open_webui.config import ( OAUTH_UPDATE_EMAIL_ON_LOGIN, OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID, OAUTH_AUDIENCE, + OAUTH_AUTHORIZE_PARAMS, WEBHOOK_URL, JWT_EXPIRES_IN, AppConfig, @@ -1286,6 +1287,8 @@ class OAuthManager: kwargs = {} if auth_manager_config.OAUTH_AUDIENCE: kwargs['audience'] = auth_manager_config.OAUTH_AUDIENCE + if OAUTH_AUTHORIZE_PARAMS: + kwargs.update(OAUTH_AUTHORIZE_PARAMS) return await client.authorize_redirect(request, redirect_uri, **kwargs) From 24370d5c40711d339d7a9ed252aa0629bfa0129f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 05:21:42 -0500 Subject: [PATCH 08/28] refac --- src/lib/apis/index.ts | 26 ++++++++++++++++++++++++++ src/routes/+layout.svelte | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index dbeef70a08..71af8f793c 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1406,6 +1406,32 @@ export const getBackendConfig = async () => { }); if (error) { + // When a forward-auth proxy (e.g. Authentik/Traefik) intercepts the + // request and redirects to an external login page, the browser blocks + // the cross-origin redirect for fetch() and throws a TypeError. + // Detect this by re-fetching with redirect:"manual" — if the server + // responded with a redirect, the probe returns an opaque redirect + // response instead of throwing, confirming the backend is alive but + // an auth proxy is intercepting. + if (error instanceof TypeError) { + try { + const probeRes = await fetch(`${WEBUI_BASE_URL}/api/config`, { + method: 'GET', + credentials: 'include', + redirect: 'manual', + headers: { 'Content-Type': 'application/json' } + }); + if ( + probeRes.type === 'opaqueredirect' || + (probeRes.status >= 300 && probeRes.status < 400) + ) { + throw { authRedirect: true }; + } + } catch (probeErr: any) { + if (probeErr?.authRedirect) throw probeErr; + // Probe also failed — genuine network/backend issue + } + } throw error; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0c539db036..920e56f96b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -906,6 +906,12 @@ backendConfig = await getBackendConfig(); console.log('Backend config:', backendConfig); } catch (error) { + if (error?.authRedirect) { + // Forward-auth proxy is redirecting to an external login page. + // Full-page navigation lets the browser follow the redirect natively. + window.location.href = '/'; + return; + } console.error('Error loading backend config:', error); } // Initialize i18n even if we didn't get a backend config, From 3841e85abb3ea3e8d8b364dff0102f0124844d22 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 05:34:36 -0500 Subject: [PATCH 09/28] refac --- src/lib/apis/terminal/index.ts | 43 ++++++++++++++++ src/lib/components/chat/FileNav.svelte | 28 +++++++++-- .../chat/FileNav/BulkActionBar.svelte | 42 ++++++++-------- .../chat/FileNav/FileEntryRow.svelte | 49 ++++++++++--------- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 748ada33a0..828931045a 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -120,6 +120,49 @@ export const downloadFileBlob = async ( return { blob, filename }; }; +export const downloadDirectoryArchive = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/download?path=${encodeURIComponent(path)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + + if (!res || !res.ok) return null; + + const disposition = res.headers.get('content-disposition') ?? ''; + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match?.[1] ?? `${path.split('/').filter(Boolean).pop() ?? 'download'}.zip`; + const blob = await res.blob(); + return { blob, filename }; +}; + +export const downloadMultipleAsZip = async ( + baseUrl: string, + apiKey: string, + paths: string[] +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/download`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ paths }) + }).catch(() => null); + + if (!res || !res.ok) return null; + + const disposition = res.headers.get('content-disposition') ?? ''; + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match?.[1] ?? 'download.zip'; + const blob = await res.blob(); + return { blob, filename }; +}; + export const uploadToTerminal = async ( baseUrl: string, apiKey: string, diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 576371c160..c41038393f 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -19,6 +19,8 @@ listFiles, readFile, downloadFileBlob, + downloadDirectoryArchive, + downloadMultipleAsZip, uploadToTerminal, createDirectory, deleteEntry, @@ -403,7 +405,11 @@ const terminal = selectedTerminal; if (!terminal) return; - const result = await downloadFileBlob(terminal.url, terminal.key, path); + // Directories end with '/' — download as ZIP archive + const isDir = path.endsWith('/'); + const result = isDir + ? await downloadDirectoryArchive(terminal.url, terminal.key, path.replace(/\/$/, '')) + : await downloadFileBlob(terminal.url, terminal.key, path); if (!result) return; const url = URL.createObjectURL(result.blob); const a = document.createElement('a'); @@ -644,10 +650,24 @@ const terminal = selectedTerminal; if (!terminal) return; - const filePaths = [...selectedEntries].filter((p) => !p.endsWith('/')); - for (const p of filePaths) { - await downloadFile(p); + const paths = [...selectedEntries].map((p) => p.replace(/\/$/, '')); + if (paths.length === 0) return; + + // Single item — use the regular downloadFile path + if (paths.length === 1) { + await downloadFile([...selectedEntries][0]); + return; } + + // Multiple items — bundle into a single ZIP + const result = await downloadMultipleAsZip(terminal.url, terminal.key, paths); + if (!result) return; + const url = URL.createObjectURL(result.blob); + const a = document.createElement('a'); + a.href = url; + a.download = result.filename; + a.click(); + URL.revokeObjectURL(url); }; // Escape to clear selection diff --git a/src/lib/components/chat/FileNav/BulkActionBar.svelte b/src/lib/components/chat/FileNav/BulkActionBar.svelte index 405b529562..9652f9ad49 100644 --- a/src/lib/components/chat/FileNav/BulkActionBar.svelte +++ b/src/lib/components/chat/FileNav/BulkActionBar.svelte @@ -40,29 +40,27 @@ - {#if hasFiles} - - - - {/if} + + + + + - {/if} + + + +
{$i18n.t('Download')}
+
+ + +