diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py
index acb70e17e2..be25227d36 100644
--- a/backend/open_webui/__init__.py
+++ b/backend/open_webui/__init__.py
@@ -2,18 +2,17 @@ import base64
import os
import random
from pathlib import Path
+from typing import Annotated
import typer
import uvicorn
-from typing import Optional
-from typing_extensions import Annotated
app = typer.Typer()
KEY_FILE = Path.cwd() / '.webui_secret_key'
-def version_callback(value: bool):
+def version_callback(value: bool) -> None:
if value:
from open_webui.env import VERSION
@@ -23,7 +22,7 @@ def version_callback(value: bool):
@app.command()
def main(
- version: Annotated[Optional[bool], typer.Option('--version', callback=version_callback)] = None,
+ version: Annotated[bool | None, typer.Option('--version', callback=version_callback)] = None,
):
pass
@@ -66,7 +65,7 @@ def serve(
os.environ['USE_CUDA_DOCKER'] = 'false'
os.environ['LD_LIBRARY_PATH'] = ':'.join(LD_LIBRARY_PATH)
- import open_webui.main # we need set environment variables before importing main
+ import open_webui.main # noqa: F401
from open_webui.env import UVICORN_WORKERS # Import the workers setting
uvicorn.run(
diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py
index 31a77988f0..19778dd81f 100644
--- a/backend/open_webui/config.py
+++ b/backend/open_webui/config.py
@@ -362,6 +362,18 @@ GOOGLE_REDIRECT_URI = PersistentConfig(
os.environ.get('GOOGLE_REDIRECT_URI', ''),
)
+GOOGLE_OAUTH_AUTHORIZE_PARAMS = {}
+_google_oauth_authorize_params = os.environ.get('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '')
+if _google_oauth_authorize_params:
+ try:
+ _parsed = json.loads(_google_oauth_authorize_params)
+ if isinstance(_parsed, dict):
+ GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed
+ else:
+ log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring')
+ except (json.JSONDecodeError, TypeError):
+ log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring')
+
MICROSOFT_CLIENT_ID = PersistentConfig(
'MICROSOFT_CLIENT_ID',
'oauth.microsoft.client_id',
@@ -642,6 +654,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()
@@ -658,6 +682,9 @@ def load_oauth_providers():
**({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}),
},
redirect_uri=GOOGLE_REDIRECT_URI.value,
+ **({
+ 'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS
+ } if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}),
)
return client
@@ -1577,7 +1604,7 @@ ENABLE_MESSAGE_RATING = PersistentConfig(
ENABLE_USER_WEBHOOKS = PersistentConfig(
'ENABLE_USER_WEBHOOKS',
'ui.enable_user_webhooks',
- os.environ.get('ENABLE_USER_WEBHOOKS', 'True').lower() == 'true',
+ os.environ.get('ENABLE_USER_WEBHOOKS', 'False').lower() == 'true',
)
# FastAPI / AnyIO settings
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..f9761e947a 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.
####################
@@ -226,12 +228,7 @@ class FunctionsTable:
def get_function_list(self, db: Optional[Session] = None) -> list[FunctionUserResponse]:
with get_db_context(db) as db:
- functions = (
- db.query(Function)
- .options(defer(Function.content))
- .order_by(Function.updated_at.desc())
- .all()
- )
+ functions = db.query(Function).options(defer(Function.content)).order_by(Function.updated_at.desc()).all()
user_ids = list(set(func.user_id for func in functions))
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
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..367ea4478c 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)
@@ -289,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)
@@ -580,9 +582,7 @@ async def signin(
if user.role != trusted_role:
Users.update_user_role_by_id(user.id, trusted_role, db=db)
elif trusted_role:
- log.warning(
- f'Ignoring invalid trusted role header value: {trusted_role}'
- )
+ log.warning(f'Ignoring invalid trusted role header value: {trusted_role}')
elif WEBUI_AUTH == False:
admin_email = 'admin@localhost'
diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py
index c2e97202aa..68ea5ff7f8 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.
############################
@@ -813,12 +815,16 @@ async def get_pinned_channel_messages(
############################
-async def send_notification(name, webui_url, channel, message, active_user_ids, db=None):
+async def send_notification(request, channel, message, active_user_ids, db=None):
+ name = request.app.state.WEBUI_NAME
+ webui_url = request.app.state.config.WEBUI_URL
+ enable_user_webhooks = request.app.state.config.ENABLE_USER_WEBHOOKS
+
users = get_channel_users_with_access(channel, 'read', db=db)
for user in users:
if (user.id not in active_user_ids) and Channels.is_user_channel_member(channel.id, user.id, db=db):
- if user.settings:
+ if enable_user_webhooks and user.settings:
webhook_url = user.settings.ui.get('notifications', {}).get('webhook_url', None)
if webhook_url:
await post_webhook(
@@ -1107,8 +1113,7 @@ async def post_new_message(
async def background_handler():
await model_response_handler(request, channel, message, user)
await send_notification(
- request.app.state.WEBUI_NAME,
- request.app.state.config.WEBUI_URL,
+ request,
channel,
message,
active_user_ids,
diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py
index 79d0525698..eacc084b42 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.
############################
@@ -930,7 +932,6 @@ async def update_chat_message_by_id(
{
'content': form_data.content,
},
- db=db,
)
event_emitter = get_event_emitter(
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..ead782cdbf 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..e7d2b0593f 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.
#
##########################################
@@ -837,32 +839,41 @@ def convert_to_responses_payload(payload: dict) -> dict:
if role == 'assistant' and msg.get('tool_calls'):
# Add text content as message if present
if content:
- text = content if isinstance(content, str) else '\n'.join(
- p.get('text', '') for p in content if p.get('type') == 'text'
+ text = (
+ content
+ if isinstance(content, str)
+ else '\n'.join(p.get('text', '') for p in content if p.get('type') == 'text')
)
if text.strip():
- input_items.append({
- 'type': 'message', 'role': 'assistant',
- 'content': [{'type': 'output_text', 'text': text}],
- })
+ input_items.append(
+ {
+ 'type': 'message',
+ 'role': 'assistant',
+ 'content': [{'type': 'output_text', 'text': text}],
+ }
+ )
# Convert each tool_call to a function_call input item
for tool_call in msg['tool_calls']:
func = tool_call.get('function', {})
- input_items.append({
- 'type': 'function_call',
- 'call_id': tool_call.get('id', ''),
- 'name': func.get('name', ''),
- 'arguments': func.get('arguments', '{}'),
- })
+ input_items.append(
+ {
+ 'type': 'function_call',
+ 'call_id': tool_call.get('id', ''),
+ 'name': func.get('name', ''),
+ 'arguments': func.get('arguments', '{}'),
+ }
+ )
continue
# Handle tool result messages
if role == 'tool':
- input_items.append({
- 'type': 'function_call_output',
- 'call_id': msg.get('tool_call_id', ''),
- 'output': msg.get('content', ''),
- })
+ input_items.append(
+ {
+ 'type': 'function_call_output',
+ 'call_id': msg.get('tool_call_id', ''),
+ 'output': msg.get('content', ''),
+ }
+ )
continue
# Convert content format
@@ -1130,8 +1141,7 @@ async def generate_chat_completion(
for message in payload['messages']:
if message.get('role') == 'tool' and isinstance(message.get('content'), list):
message['content'] = ''.join(
- part.get('text', '') for part in message['content']
- if part.get('type') in ('input_text', 'text')
+ part.get('text', '') for part in message['content'] if part.get('type') in ('input_text', 'text')
)
payload = json.dumps(payload)
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 deadea2a41..eac21f0420 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..a0b8bccd44 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.
############################
@@ -760,19 +762,35 @@ async def update_tools_valves_by_id(
@router.get('/id/{id}/valves/user', response_model=Optional[dict])
async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
tools = Tools.get_tool_by_id(id, db=db)
- if tools:
- try:
- user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db)
- return user_valves
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=ERROR_MESSAGES.DEFAULT(str(e)),
- )
- else:
+ if not tools:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=ERROR_MESSAGES.NOT_FOUND,
+ )
+
+ if (
+ tools.user_id != user.id
+ and not AccessGrants.has_access(
+ user_id=user.id,
+ resource_type='tool',
+ resource_id=tools.id,
+ permission='read',
+ db=db,
+ )
+ and user.role != 'admin'
+ ):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail=ERROR_MESSAGES.NOT_FOUND,
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+ )
+
+ try:
+ user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db)
+ return user_valves
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
@@ -784,26 +802,42 @@ async def get_tools_user_valves_spec_by_id(
db: Session = Depends(get_session),
):
tools = Tools.get_tool_by_id(id, db=db)
- if tools:
- if id in request.app.state.TOOLS:
- tools_module = request.app.state.TOOLS[id]
- else:
- tools_module, _ = load_tool_module_by_id(id)
- request.app.state.TOOLS[id] = tools_module
-
- if hasattr(tools_module, 'UserValves'):
- UserValves = tools_module.UserValves
- schema = UserValves.schema()
- # Resolve dynamic options for select dropdowns
- schema = resolve_valves_schema_options(UserValves, schema, user)
- return schema
- return None
- else:
+ if not tools:
raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
+ status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
+ if (
+ tools.user_id != user.id
+ and not AccessGrants.has_access(
+ user_id=user.id,
+ resource_type='tool',
+ resource_id=tools.id,
+ permission='read',
+ db=db,
+ )
+ and user.role != 'admin'
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+ )
+
+ if id in request.app.state.TOOLS:
+ tools_module = request.app.state.TOOLS[id]
+ else:
+ tools_module, _ = load_tool_module_by_id(id)
+ request.app.state.TOOLS[id] = tools_module
+
+ if hasattr(tools_module, 'UserValves'):
+ UserValves = tools_module.UserValves
+ schema = UserValves.schema()
+ # Resolve dynamic options for select dropdowns
+ schema = resolve_valves_schema_options(UserValves, schema, user)
+ return schema
+ return None
+
@router.post('/id/{id}/valves/user/update', response_model=Optional[dict])
async def update_tools_user_valves_by_id(
@@ -814,33 +848,48 @@ async def update_tools_user_valves_by_id(
db: Session = Depends(get_session),
):
tools = Tools.get_tool_by_id(id, db=db)
+ if not tools:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=ERROR_MESSAGES.NOT_FOUND,
+ )
- if tools:
- if id in request.app.state.TOOLS:
- tools_module = request.app.state.TOOLS[id]
- else:
- tools_module, _ = load_tool_module_by_id(id)
- request.app.state.TOOLS[id] = tools_module
+ if (
+ tools.user_id != user.id
+ and not AccessGrants.has_access(
+ user_id=user.id,
+ resource_type='tool',
+ resource_id=tools.id,
+ permission='read',
+ db=db,
+ )
+ and user.role != 'admin'
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+ )
- if hasattr(tools_module, 'UserValves'):
- UserValves = tools_module.UserValves
+ if id in request.app.state.TOOLS:
+ tools_module = request.app.state.TOOLS[id]
+ else:
+ tools_module, _ = load_tool_module_by_id(id)
+ request.app.state.TOOLS[id] = tools_module
- try:
- form_data = {k: v for k, v in form_data.items() if v is not None}
- user_valves = UserValves(**form_data)
- user_valves_dict = user_valves.model_dump(exclude_unset=True)
- Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db)
- return user_valves_dict
- except Exception as e:
- log.exception(f'Failed to update user valves by id {id}: {e}')
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=ERROR_MESSAGES.DEFAULT(str(e)),
- )
- else:
+ if hasattr(tools_module, 'UserValves'):
+ UserValves = tools_module.UserValves
+
+ try:
+ form_data = {k: v for k, v in form_data.items() if v is not None}
+ user_valves = UserValves(**form_data)
+ user_valves_dict = user_valves.model_dump(exclude_unset=True)
+ Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db)
+ return user_valves_dict
+ except Exception as e:
+ log.exception(f'Failed to update user valves by id {id}: {e}')
raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail=ERROR_MESSAGES.NOT_FOUND,
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
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 1518193da8..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
@@ -393,7 +395,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')
diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py
index 9fbe5f75f1..f02a082c42 100644
--- a/backend/open_webui/tools/builtin.py
+++ b/backend/open_webui/tools/builtin.py
@@ -1604,13 +1604,15 @@ async def search_knowledge_files(
)
for file in result.items:
- all_files.append({
- 'id': file.id,
- 'filename': file.filename,
- 'knowledge_id': knowledge.id,
- 'knowledge_name': knowledge.name,
- 'updated_at': file.updated_at,
- })
+ all_files.append(
+ {
+ 'id': file.id,
+ 'filename': file.filename,
+ 'knowledge_id': knowledge.id,
+ 'knowledge_name': knowledge.name,
+ 'updated_at': file.updated_at,
+ }
+ )
# Search within directly attached files (filename match)
if not knowledge_id and attached_file_ids:
@@ -1618,14 +1620,16 @@ async def search_knowledge_files(
for file_id in attached_file_ids:
file = Files.get_file_by_id(file_id)
if file and (not query_lower or query_lower in file.filename.lower()):
- all_files.append({
- 'id': file.id,
- 'filename': file.filename,
- 'updated_at': file.updated_at,
- })
+ all_files.append(
+ {
+ 'id': file.id,
+ 'filename': file.filename,
+ 'updated_at': file.updated_at,
+ }
+ )
# Apply pagination across combined results
- all_files = all_files[skip:skip + count]
+ all_files = all_files[skip : skip + count]
return json.dumps(all_files, ensure_ascii=False)
# No attached knowledge - search all accessible KBs
@@ -1739,7 +1743,7 @@ async def view_file(
content = file.data.get('content', '')
total_chars = len(content)
- sliced = content[offset:offset + max_chars]
+ sliced = content[offset : offset + max_chars]
is_truncated = (offset + len(sliced)) < total_chars
result = {
@@ -1844,7 +1848,7 @@ async def view_knowledge_file(
content = file.data.get('content', '')
total_chars = len(content)
- sliced = content[offset:offset + max_chars]
+ sliced = content[offset : offset + max_chars]
is_truncated = (offset + len(sliced)) < total_chars
result = {
@@ -1935,21 +1939,20 @@ async def list_knowledge(
# Include file listing for each KB
if kb_files:
- kb_entry['files'] = [
- {'id': f.id, 'filename': f.filename}
- for f in kb_files
- ]
+ kb_entry['files'] = [{'id': f.id, 'filename': f.filename} for f in kb_files]
knowledge_bases.append(kb_entry)
elif item_type == 'file':
file = Files.get_file_by_id(item_id)
if file:
- files.append({
- 'id': file.id,
- 'filename': file.filename,
- 'updated_at': file.updated_at,
- })
+ files.append(
+ {
+ 'id': file.id,
+ 'filename': file.filename,
+ 'updated_at': file.updated_at,
+ }
+ )
elif item_type == 'note':
note = Notes.get_note_by_id(item_id)
@@ -1963,16 +1966,21 @@ async def list_knowledge(
permission='read',
)
):
- notes.append({
- 'id': note.id,
- 'title': note.title,
- })
+ notes.append(
+ {
+ 'id': note.id,
+ 'title': note.title,
+ }
+ )
- return json.dumps({
- 'knowledge_bases': knowledge_bases,
- 'files': files,
- 'notes': notes,
- }, ensure_ascii=False)
+ return json.dumps(
+ {
+ 'knowledge_bases': knowledge_bases,
+ 'files': files,
+ 'notes': notes,
+ },
+ ensure_ascii=False,
+ )
except Exception as e:
log.exception(f'list_knowledge error: {e}')
return json.dumps({'error': str(e)})
diff --git a/backend/open_webui/utils/access_control/__init__.py b/backend/open_webui/utils/access_control/__init__.py
index 5d357bcb22..f31c59e158 100644
--- a/backend/open_webui/utils/access_control/__init__.py
+++ b/backend/open_webui/utils/access_control/__init__.py
@@ -226,7 +226,9 @@ def filter_allowed_access_grants(
return access_grants
# Check if user can share publicly
- if (has_public_read_access_grant(access_grants) or has_public_write_access_grant(access_grants)) and not has_permission(
+ if (
+ has_public_read_access_grant(access_grants) or has_public_write_access_grant(access_grants)
+ ) and not has_permission(
user_id,
public_permission_key,
default_permissions,
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..df07dea4a1 100644
--- a/backend/open_webui/utils/filter.py
+++ b/backend/open_webui/utils/filter.py
@@ -34,17 +34,17 @@ def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None)
if 'info' in model and 'meta' in model['info']:
filter_ids.extend(model['info']['meta'].get('filterIds', []))
filter_ids = list(set(filter_ids))
- active_filter_ids = [function.id for function in Functions.get_functions_by_type('filter', active_only=True)]
+ active_filter_ids = {function.id for function in Functions.get_functions_by_type('filter', active_only=True)}
def get_active_status(filter_id):
function_module = get_function_module(request, filter_id)
if getattr(function_module, 'toggle', None):
- return filter_id in (enabled_filter_ids or [])
+ return filter_id in (enabled_filter_ids or set())
return True
- active_filter_ids = [filter_id for filter_id in active_filter_ids if get_active_status(filter_id)]
+ active_filter_ids = {filter_id for filter_id in active_filter_ids if get_active_status(filter_id)}
filter_ids = [fid for fid in filter_ids if fid in active_filter_ids]
filter_ids.sort(key=lambda fid: (get_priority(fid), fid))
@@ -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..f6582bf239 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 = [
('