This commit is contained in:
Timothy Jaeryang Baek 2026-11-02 08:48:51 +09:00
parent 5140da6eb9
commit d99ec1c02c
135 changed files with 606 additions and 1867 deletions

View file

@ -57,9 +57,7 @@ class EndpointFilter(logging.Filter):
logging.getLogger('uvicorn.access').addFilter(EndpointFilter())
####################################
# Initialization
####################################
def run_migrations():
@ -92,9 +90,7 @@ async def import_legacy_config_json():
os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json')
####################################
# Static DIR
####################################
STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')).resolve()
STATIC_SOURCE_DIR = FRONTEND_BUILD_DIR / 'static'
@ -151,7 +147,7 @@ if frontend_loader.exists():
logging.error(f'An error occurred: {e}')
# --- Storage Provider ---
# Storage provider
STORAGE_PROVIDER = os.getenv('STORAGE_PROVIDER', 'local') # defaults to local, s3
STORAGE_LOCAL_CACHE = os.getenv('STORAGE_LOCAL_CACHE', 'true').lower() == 'true'
@ -173,25 +169,19 @@ AZURE_STORAGE_ENDPOINT = os.getenv('AZURE_STORAGE_ENDPOINT', None)
AZURE_STORAGE_CONTAINER_NAME = os.getenv('AZURE_STORAGE_CONTAINER_NAME', None)
AZURE_STORAGE_KEY = os.getenv('AZURE_STORAGE_KEY', None)
####################################
# File Upload DIR
####################################
UPLOAD_DIR = DATA_DIR / 'uploads'
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
####################################
# Cache DIR
####################################
CACHE_DIR = DATA_DIR / 'cache'
CACHE_DIR.mkdir(parents=True, exist_ok=True)
####################################
# CUSTOM_NAME (Legacy)
####################################
# LICENSE covers this legacy Open WebUI branding path.
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -229,16 +219,12 @@ if CUSTOM_NAME:
pass
####################################
# DIRECT CONNECTIONS
####################################
# Direct connections
ENABLE_DIRECT_CONNECTIONS = os.getenv('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true'
ENABLE_DIRECT_INTEGRATIONS = os.getenv('ENABLE_DIRECT_INTEGRATIONS', 'False').lower() == 'true'
####################################
# OLLAMA_BASE_URL
####################################
ENABLE_OLLAMA_API = os.getenv('ENABLE_OLLAMA_API', 'True').lower() == 'true'
@ -246,7 +232,7 @@ OLLAMA_API_BASE_URL = os.getenv('OLLAMA_API_BASE_URL', 'http://localhost:11434/a
OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', '')
if OLLAMA_BASE_URL:
# Remove trailing slash
# Trim the trailing slash; the next path will bring its own.
OLLAMA_BASE_URL = OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith('/') else OLLAMA_BASE_URL
@ -317,9 +303,7 @@ if _ollama_api_configs:
except (JSONCodec.JSONDecodeError, TypeError):
log.warning('OLLAMA_API_CONFIGS is not valid JSON, ignoring')
####################################
# OPENAI_API
####################################
ENABLE_OPENAI_API = os.getenv('ENABLE_OPENAI_API', 'True').lower() == 'true'
@ -373,16 +357,12 @@ except Exception:
OPENAI_API_BASE_URL = 'https://api.openai.com/v1'
####################################
# MODELS
####################################
# Models
ENABLE_BASE_MODELS_CACHE = os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true'
####################################
# TOOL_SERVERS
####################################
try:
tool_server_connections = JSONCodec.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]'))
@ -395,9 +375,7 @@ TOOL_SERVER_CONNECTIONS = tool_server_connections
OAUTH_CLIENT_TIMEOUT = os.getenv('OAUTH_CLIENT_TIMEOUT', '')
####################################
# TERMINAL_SERVER
####################################
terminal_server_connections = JSONCodec.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]'))
@ -408,9 +386,7 @@ try:
except Exception:
TERMINAL_PROXY_HEADERS = {}
####################################
# Code Interpreter
####################################
ENABLE_CODE_EXECUTION = os.getenv('ENABLE_CODE_EXECUTION', 'True').lower() == 'true'
@ -502,9 +478,7 @@ CODE_INTERPRETER_PYODIDE_PROMPT = """
- Use `import os; os.listdir('/mnt/uploads')` to discover available files."""
####################################
# Vector Database
####################################
VECTOR_DB = os.getenv('VECTOR_DB', 'pgvector' if USE_SLIM else 'chroma')
@ -841,9 +815,7 @@ VALKEY_HNSW_M = int(os.getenv('VALKEY_HNSW_M', '16'))
VALKEY_HNSW_EF_CONSTRUCTION = int(os.getenv('VALKEY_HNSW_EF_CONSTRUCTION', '200'))
VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10'))
####################################
# Information Retrieval (RAG)
####################################
# If configured, Google Drive will be available as an upload option.
@ -1182,9 +1154,7 @@ YOUTUBE_LOADER_LANGUAGE = os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(',')
YOUTUBE_LOADER_PROXY_URL = os.getenv('YOUTUBE_LOADER_PROXY_URL', '')
####################################
# Web Search
####################################
ENABLE_WEB_SEARCH = os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true'
@ -1374,9 +1344,7 @@ except JSONCodec.JSONDecodeError:
LINKUP_SEARCH_PARAMS = linkup_search_params
####################################
# Images
####################################
ENABLE_IMAGE_GENERATION = os.getenv('ENABLE_IMAGE_GENERATION', '').lower() == 'true'
@ -1584,9 +1552,7 @@ except JSONCodec.JSONDecodeError:
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = images_edit_comfyui_workflow_nodes
####################################
# Audio
####################################
# Transcription
WHISPER_MODEL = os.getenv('WHISPER_MODEL', 'base')
@ -1702,9 +1668,7 @@ AUDIO_TTS_MISTRAL_API_KEY = os.getenv('AUDIO_TTS_MISTRAL_API_KEY', '')
AUDIO_TTS_MISTRAL_API_BASE_URL = os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1')
####################################
# WEBUI
####################################
# WebUI
WEBUI_URL = os.getenv('WEBUI_URL', '')
@ -2254,9 +2218,7 @@ SHOW_ADMIN_DETAILS = os.getenv('SHOW_ADMIN_DETAILS', 'true').lower() == 'true'
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', None)
####################################
# TASKS
####################################
# Tasks
TASK_MODEL = os.getenv('TASK_MODEL', '')
@ -2560,9 +2522,7 @@ Your task is to synthesize these responses into a single, high-quality response.
Responses from models: {{responses}}"""
####################################
# Auth
####################################
ENABLE_API_KEYS = os.getenv('ENABLE_API_KEYS', 'False').lower() == 'true'
@ -2588,9 +2548,7 @@ if JWT_EXPIRES_IN == '-1':
' See: https://docs.openwebui.com/reference/env-configuration\n'
)
####################################
# OAuth config
####################################
# Master switch for OAuth/OIDC sign-in. Defaults to enabled so existing
# deployments that already have a provider configured keep working; admins can
@ -2898,9 +2856,7 @@ def load_oauth_providers():
load_oauth_providers()
####################################
# LDAP
####################################
ENABLE_LDAP = os.getenv('ENABLE_LDAP', 'false').lower() == 'true'

View file

@ -18,9 +18,7 @@ import markdown
from bs4 import BeautifulSoup
from cryptography.hazmat.primitives import serialization
####################################
# Load .env file
####################################
# Use .resolve() to get the canonical path, removing any '..' or '.' components
ENV_FILE_PATH = Path(__file__).resolve()
@ -72,9 +70,7 @@ if not USE_SLIM and sys.platform == 'darwin' and DEVICE_TYPE == 'cpu':
# Torch MPS inference is not thread-safe and a concurrent call kills the whole process.
MPS_INFERENCE_LOCK = threading.Lock() if DEVICE_TYPE == 'mps' else nullcontext()
####################################
# LOGGING
####################################
# Logging
_LEVEL_MAP = {
'DEBUG': 'debug',
@ -132,9 +128,7 @@ if _cuda_error:
SRC_LOG_LEVELS = {} # Legacy variable, do not remove
####################################
# ENV (dev,test,prod)
####################################
ENV = os.getenv('ENV', 'dev')
@ -221,9 +215,7 @@ for version in soup.find_all('h2'):
CHANGELOG = changelog_json
####################################
# DATA/FRONTEND BUILD DIR
####################################
# Data/frontend build dir
DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve()
@ -256,9 +248,7 @@ FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).r
if FROM_INIT_PY:
FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', OPEN_WEBUI_DIR / 'frontend')).resolve()
####################################
# Database
####################################
# Check if the file exists
if os.path.exists(f'{DATA_DIR}/ollama.db'):
@ -374,9 +364,7 @@ RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
# Empty by default: chunk metadata also holds internal bookkeeping (file hashes, collection names, scores).
RAG_SOURCE_METADATA_KEYS = [key.strip() for key in os.getenv('RAG_SOURCE_METADATA_KEYS', '').split(',') if key.strip()]
####################################
# REDIS
####################################
# Redis
REDIS_URL = os.getenv('REDIS_URL', '')
REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true'
@ -454,9 +442,7 @@ else:
except Exception:
REDIS_RECONNECT_DELAY = None
####################################
# Uvicorn
####################################
try:
UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1)
@ -466,9 +452,7 @@ except (ValueError, TypeError):
# tiny delta-stream frames make per-frame websocket compression CPU-bound, allow opting out (true/false)
UVICORN_WS_PER_MESSAGE_DEFLATE = os.getenv('UVICORN_WS_PER_MESSAGE_DEFLATE', 'True').lower() == 'true'
####################################
# WEBSOCKET SUPPORT
####################################
# Websocket support
ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
@ -728,9 +712,7 @@ except (ValueError, TypeError):
MCP_INITIALIZE_TIMEOUT = 10
####################################
# AIOHTTP Connection Pool
####################################
AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '')
if AIOHTTP_POOL_CONNECTIONS == '':
@ -778,18 +760,14 @@ else:
RAG_EMBEDDING_TIMEOUT = None
####################################
# Auth
####################################
WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true'
ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true'
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true'
####################################
# Secret key & cookies
####################################
# WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead.
# No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below.
@ -823,9 +801,7 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == '':
ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true'
####################################
# Admin Account Runtime Creation
####################################
# Optional env vars for creating an admin account on startup
# Useful for headless/automated deployments
@ -907,9 +883,7 @@ ENABLE_OPENAI_API_PASSTHROUGH = os.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'Fals
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None)
####################################
# OAUTH Configuration
####################################
ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true'
ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true'
@ -944,9 +918,7 @@ OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS = [
# Requires Redis for JWT revocation.
ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true'
####################################
# SCIM Configuration
####################################
ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true'
SCIM_TOKEN = os.getenv('SCIM_TOKEN', '')
@ -959,9 +931,7 @@ if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
'to enable externalId storage.'
)
####################################
# LICENSE_KEY
####################################
LICENSE_KEY = os.getenv('LICENSE_KEY', '')
@ -984,9 +954,7 @@ if LICENSE_PUBLIC_KEY:
)
####################################
# WEBUI Identity
####################################
# LICENSE covers this Open WebUI branding surface, including name, logo,
# visual, textual, symbolic identifiers, metadata, and surrounding UI.
@ -1004,9 +972,7 @@ WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png'
WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build')
TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '')
####################################
# Feature flags
####################################
SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true'
ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
@ -1028,9 +994,7 @@ PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset(
_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip()
PROFILE_IMAGE_MAX_DATA_URI_SIZE = int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None
####################################
# Forward Headers
####################################
ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true'
@ -1053,9 +1017,7 @@ try:
except ValueError:
FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = 300
####################################
# Progressive Web App
####################################
# LICENSE covers this install-time Open WebUI branding surface, including
# names, logos, manifests, metadata, and surrounding UI.
@ -1063,18 +1025,14 @@ except ValueError:
# https://docs.openwebui.com/license.
EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None)
####################################
# GROUP DEFAULTS
####################################
# Group defaults
# Controls the default "Who can share to this group" setting for new groups.
# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
_default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower()
DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true'
####################################
# MODELS
####################################
# Models
ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
@ -1088,9 +1046,7 @@ else:
MODELS_CACHE_TTL = 1
####################################
# CHAT
####################################
# Chat
ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
os.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
@ -1162,9 +1118,7 @@ else:
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
####################################
# SENTENCE TRANSFORMERS
####################################
# Sentence transformers
SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '')
@ -1203,9 +1157,7 @@ SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
)
####################################
# KNOWLEDGE TOOLS
####################################
# Knowledge tools
def _int_env(name: str, default: int) -> int:
@ -1225,9 +1177,7 @@ KNOWLEDGE_GREP_MAX_MATCHES = _int_env('KNOWLEDGE_GREP_MAX_MATCHES', 50)
VIEW_FILE_MAX_CHARS = _int_env('VIEW_FILE_MAX_CHARS', 100_000)
VIEW_FILE_DEFAULT_MAX_CHARS = _int_env('VIEW_FILE_DEFAULT_MAX_CHARS', 10_000)
####################################
# TOOLS/FUNCTIONS PIP OPTIONS
####################################
# Tools/functions pip options
ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true'
# Deployment controls: the master switch always overrides all feature switches.
@ -1243,9 +1193,7 @@ PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split()
####################################
# OFFLINE_MODE
####################################
ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true'
@ -1254,15 +1202,11 @@ if OFFLINE_MODE:
os.environ['HF_HUB_OFFLINE'] = '1'
ENABLE_VERSION_UPDATE_CHECK = False
####################################
# Pyodide file persistence
####################################
ENABLE_PYODIDE_FILE_PERSISTENCE = os.getenv('ENABLE_PYODIDE_FILE_PERSISTENCE', 'false').lower() == 'true'
####################################
# Audit logging
####################################
ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true'
@ -1306,9 +1250,7 @@ AUDIT_INCLUDED_PATHS = [
ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true'
####################################
# OPENTELEMETRY
####################################
# OpenTelemetry
ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true'
ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true'

View file

@ -43,17 +43,7 @@ from typing_extensions import Self
log = logging.getLogger(__name__)
# ── SSL URL normalization (used by sync engine & Alembic migrations) ─
#
# psycopg2 (sync) needs ``sslmode=`` in the connection string (it does
# not recognise the bare ``ssl=`` key that some ORMs emit). The helpers
# below strip all SSL-related query params, normalise them, and
# reattach them in the canonical libpq form.
#
# The **async** engine now uses psycopg (v3), which speaks libpq
# natively, so it needs no translation at all — the DATABASE_URL is
# passed through as-is.
# ─────────────────────────────────────────────────────────────────────
# Normalize SSL query parameters for sync psycopg2 and Alembic; async psycopg uses the URL unchanged.
def _pop_first(params: dict[str, list[str]], key: str) -> str | None:
@ -281,10 +271,8 @@ def _create_async_engine(*args, **kwargs):
return create_async_engine(*args, **_json_codec_kwargs(kwargs))
# ============================================================
# SYNC ENGINE (used only for: startup migrations, config loading,
# Sync engine (used only for: startup migrations, config loading,
# Alembic, peewee migration, health checks)
# ============================================================
# Handle SQLCipher URLs
if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'):
@ -443,9 +431,7 @@ def get_session():
get_db = contextmanager(get_session)
# ============================================================
# ASYNC ENGINE (used for ALL runtime database operations)
# ============================================================
# Async engine (used for all runtime database operations)
# psycopg (v3) speaks libpq natively — the full DATABASE_URL is passed
# through as-is. SSL params, ``options``, ``target_session_attrs``, etc.

View file

@ -587,11 +587,7 @@ app.state.USER_COUNT = None
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
########################################
#
# OPENTELEMETRY
#
########################################
# OpenTelemetry
if ENABLE_OTEL:
from open_webui.utils.telemetry.setup import setup as setup_opentelemetry
@ -599,69 +595,34 @@ if ENABLE_OTEL:
setup_opentelemetry(app=app, db_engine=engine)
########################################
#
# OLLAMA
#
########################################
# Ollama
app.state.OLLAMA_MODELS = {}
########################################
#
# OPENAI
#
########################################
# OpenAI
app.state.OPENAI_MODELS = {}
########################################
#
# TOOL SERVERS
#
########################################
# Tool servers
app.state.TOOL_SERVERS = []
########################################
#
# TERMINAL SERVER
#
########################################
# Terminal server
app.state.TERMINAL_SERVERS = []
########################################
#
# DIRECT CONNECTIONS
#
########################################
########################################
#
# SCIM
#
########################################
app.state.ENABLE_SCIM = ENABLE_SCIM
app.state.SCIM_TOKEN = SCIM_TOKEN
########################################
#
# MODELS
#
########################################
# Models
app.state.BASE_MODELS = []
########################################
#
# WEBUI
#
########################################
# WebUI
async def initialize_runtime_config(app: FastAPI):
@ -796,25 +757,7 @@ async def initialize_runtime_config(app: FastAPI):
)
########################################
#
# CODE EXECUTION
#
########################################
########################################
#
# IMAGES
#
########################################
########################################
#
# AUDIO
#
########################################
# Audio
app.state.faster_whisper_model = None
@ -822,18 +765,7 @@ app.state.speech_synthesiser = None
app.state.speech_speaker_embeddings_dataset = None
########################################
#
# TASKS
#
########################################
########################################
#
# WEBUI
#
########################################
# WebUI
app.state.MODELS = MODELS
@ -861,14 +793,8 @@ if ENABLE_COMPRESSION_MIDDLEWARE:
app.add_middleware(CompressMiddleware)
# All HTTP middlewares below are pure-ASGI implementations. The previous
# `BaseHTTPMiddleware` / `@app.middleware('http')` versions wrapped the
# downstream app in an anyio task group whose cancel scope cancelled
# in-flight DB calls (and any other awaits) on client disconnect /
# response completion — which surfaced as noisy SQLAlchemy
# `terminate_force_close` tracebacks under aiosqlite and as random
# CancelledError storms across the request path. See
# `open_webui.utils.asgi_middleware` for the rationale.
# Use pure ASGI middleware to avoid BaseHTTPMiddleware cancellation of in-flight DB work.
# See open_webui.utils.asgi_middleware for the disconnect and cleanup rationale.
app.add_middleware(AppHTTPMiddleware)
@ -935,11 +861,7 @@ if ENABLE_SCIM:
app.include_router(scim.router, prefix='/api/v1/scim/v2', tags=['scim'])
##################################
#
# Chat Endpoints
#
##################################
@app.get('/api/models')
@ -947,7 +869,7 @@ if ENABLE_SCIM:
async def get_models(request: Request, refresh: bool = False, user=Depends(get_verified_user)):
all_models = await get_all_models(request, refresh=refresh, user=user)
# Filter out filter pipelines
# Keep filter pipelines off the model menu; they work backstage.
models = [
model for model in all_models if not ('pipeline' in model and model['pipeline'].get('type', None) == 'filter')
]
@ -964,7 +886,7 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v
info = model.get('info') if isinstance(model.get('info'), dict) else {}
meta = info.get('meta') if isinstance(info.get('meta'), dict) else {}
# Remove profile image URL to reduce payload size
# Leave profile images out; this model list is traveling light.
meta.pop('profile_image_url', None)
if 'tags' in meta:
@ -976,7 +898,7 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v
model_order_list = await Config.get('ui.model_order_list')
if model_order_list:
model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)}
# Sort models by order list priority, with fallback for those not in the list
# Configured models get the front seats; the rest line up by name.
models.sort(
key=lambda model: (
model_order_dict.get(model.get('id', ''), float('inf')),
@ -1021,7 +943,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend
break
model_id = model_info.base_model_id
# --- Ollama provider ---
# Ollama provider
if model_id in ollama_models:
ollama_config = await Config.get_many('ollama.base_urls', 'ollama.api_configs')
ollama_base_urls = ollama_config.get('ollama.base_urls') or []
@ -1067,7 +989,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend
)
return {'status': True}
# --- OpenAI-compatible providers ---
# OpenAI-compatible providers
if model_id in openai_models:
openai_config = await Config.get_many('openai.api_configs', 'openai.api_base_urls', 'openai.api_keys')
openai_api_configs = openai_config.get('openai.api_configs') or {}
@ -1114,9 +1036,7 @@ async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depend
raise HTTPException(status_code=404, detail=f'Model "{model_id}" not found')
##################################
# Embeddings
##################################
@app.post('/api/embeddings')
@ -1975,11 +1895,7 @@ async def resolve_chat_message_tool_call(
app.state.CHAT_COMPLETION_HANDLER = chat_completion
##################################
#
# Anthropic Messages API Compatible Endpoint
#
##################################
from open_webui.utils.anthropic import (
@ -2108,13 +2024,13 @@ async def generate_messages(
return await passthrough_anthropic_messages(request, form_data, user)
passthrough_params = api_config.get('passthrough_params') or []
# Convert Anthropic payload to OpenAI format
# Translate Anthropic into OpenAI before sending it through the house plumbing.
openai_payload = convert_anthropic_to_openai_payload(form_data, passthrough_params)
# Route through the existing chat_completion handler
response = await chat_completion(request, openai_payload, user)
# Convert response back to Anthropic format
# Translate back to Anthropic; the caller ordered that dialect.
if isinstance(response, StreamingResponse):
# Streaming response: wrap the generator to convert SSE format
return StreamingResponse(
@ -2291,11 +2207,7 @@ async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
return result
##################################
#
# Config Endpoints
#
##################################
@app.get('/api/config')
@ -2434,7 +2346,7 @@ async def get_app_config(request: Request):
},
'features': {
'slim': USE_SLIM,
# --- Public: required by login/signup page pre-auth ---
# Public: required by login/signup page pre-auth
'auth': WEBUI_AUTH,
'auth_trusted_header': bool(WEBUI_AUTH_TRUSTED_EMAIL_HEADER),
'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
@ -2447,7 +2359,7 @@ async def get_app_config(request: Request):
if WEBSOCKET_HEARTBEAT_INTERVAL is not None
else {}
),
# --- Authenticated: only consumed by logged-in frontend ---
# Authenticated: only consumed by logged-in frontend
**(
{
'enable_api_keys': config.get('auth.enable_api_keys'),
@ -2776,7 +2688,7 @@ async def get_current_usage(user=Depends(get_verified_user)):
raise HTTPException(status_code=500, detail='Internal Server Error')
# --- OAuth Login & Callback ---
# OAuth login & callback
try:
@ -2992,9 +2904,7 @@ async def oauth_login_callback(
return await oauth_manager.handle_callback(request, provider, response, db=db)
############################
# OIDC Back-Channel Logout
############################
@app.post('/oauth/backchannel-logout')
@ -3155,7 +3065,7 @@ async def check_db_health():
return {'status': True}
# --- static assets & files ---
# Static assets & files
# Windows registry entries can override these with text/plain, which breaks module and wasm loading
mimetypes.add_type('text/javascript', '.js')
mimetypes.add_type('text/javascript', '.mjs')

View file

@ -6,7 +6,7 @@ from alembic import op # noqa: E402 — alembic runtime context
from sqlalchemy import inspect # metadata inspection
# --- database helper functions ---
# Database helper functions
def get_existing_tables() -> set[str]:
"""Return table names already present in the database."""
conn = op.get_bind()

View file

@ -22,7 +22,7 @@ def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
existing_tables = set(inspector.get_table_names())
# ── Create oauth_session table (idempotent) ───────────────────────
# Create oauth_session table (idempotent)
if 'oauth_session' not in existing_tables:
op.create_table(
'oauth_session',

View file

@ -178,7 +178,7 @@ _INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
]
# --- migration execution ---
# Migration execution
def upgrade() -> None: # deploy initial schema tables
existing_tables = set(get_existing_tables())
for name, columns, constraints in _INITIAL_TABLES:
@ -186,7 +186,7 @@ def upgrade() -> None: # deploy initial schema tables
op.create_table(name, *columns, *constraints)
# --- rollback function ---
# Rollback function
def downgrade() -> None: # rollback initial schema tables
for table_name, _, _ in reversed(_INITIAL_TABLES):
op.drop_table(table_name)

View file

@ -22,7 +22,7 @@ def upgrade():
existing_tables = set(inspector.get_table_names())
if 'feedback' not in existing_tables:
# ### Create feedback table ###
# Create feedback table
op.create_table(
'feedback',
sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type)
@ -42,5 +42,5 @@ def upgrade():
def downgrade():
# ### Drop feedback table ###
# Drop feedback table
op.drop_table('feedback')

View file

@ -20,7 +20,7 @@ down_revision: Union[str, None] = '2f1211949ecc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# ── Ad-hoc table references for Core DML ─────────────────────────────────
# Ad-hoc table references for Core DML
# These are lightweight table() / column() references used only inside this
# migration for SELECT / UPDATE / INSERT — they do NOT create or alter
# anything on disk.
@ -143,7 +143,7 @@ def upgrade() -> None:
existing_tables = set(inspector.get_table_names())
user_columns = {c['name'] for c in inspector.get_columns('user')}
# ── Add new columns (idempotent) ──────────────────────────────────
# Add new columns (idempotent)
for col_name, col_type in [
('profile_banner_image_url', sa.Text()),
('timezone', sa.String()),
@ -164,7 +164,7 @@ def upgrade() -> None:
if isinstance(user_col_types.get('settings'), sa.Text):
_convert_column_to_json('user', 'settings')
# ── Create api_key table (idempotent) ─────────────────────────────
# Create api_key table (idempotent)
if 'api_key' not in existing_tables:
op.create_table(
'api_key',
@ -178,7 +178,7 @@ def upgrade() -> None:
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
# ── Migrate oauth_sub → oauth JSON (only if old column still exists)
# Migrate oauth_sub → OAuth JSON (only if old column still exists)
if 'oauth_sub' in user_columns:
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth_sub).where(_user.c.oauth_sub.is_not(None))).fetchall()
@ -187,7 +187,7 @@ def upgrade() -> None:
provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub)
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth={provider: {'sub': sub}}))
# ── Migrate api_key column → api_key table (only if old column still exists)
# Migrate api_key column → api_key table (only if old column still exists)
if 'api_key' in user_columns:
rows = conn.execute(sa.select(_user.c.id, _user.c.api_key).where(_user.c.api_key.is_not(None))).fetchall()
now = int(time.time())
@ -204,7 +204,7 @@ def upgrade() -> None:
)
)
# ── Drop legacy columns (idempotent) ──────────────────────────────
# Drop legacy columns (idempotent)
cols_to_drop = {'api_key', 'oauth_sub'} & user_columns
if cols_to_drop:
if conn.dialect.name == 'sqlite':
@ -235,7 +235,7 @@ def downgrade() -> None:
op.drop_column('user', 'oauth')
# --- Restore api_key field ---
# Restore api_key field
op.add_column('user', sa.Column('api_key', sa.String(), nullable=True))
keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall()

View file

@ -23,7 +23,7 @@ def upgrade():
inspector = sa.inspect(conn)
file_cols = {c['name'] for c in inspector.get_columns('file')}
# ### commands auto generated by Alembic - please adjust! ###
# Commands auto generated by alembic - please adjust!
if 'hash' not in file_cols:
op.add_column('file', sa.Column('hash', sa.Text(), nullable=True))
if 'data' not in file_cols:
@ -33,7 +33,7 @@ def upgrade():
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# Commands auto generated by alembic - please adjust!
op.drop_column('file', 'updated_at')
op.drop_column('file', 'data')
op.drop_column('file', 'hash')

View file

@ -17,9 +17,7 @@ PRINCIPAL_TYPE_USER = 'user'
WILDCARD_PRINCIPAL_ID = '*'
####################
# AccessGrant DB Schema
####################
class AccessGrant(Base):
@ -75,9 +73,7 @@ class AccessGrantResponse(BaseModel):
)
####################
# Conversion utilities
####################
def access_control_to_grants(
@ -318,9 +314,7 @@ def grants_to_access_control(grants: list) -> Optional[dict]:
return result
####################
# Table Operations
####################
class AccessGrantsTable:

View file

@ -205,9 +205,7 @@ class AppInteractionForm(StrictModel):
action_id: str = Field(min_length=1, max_length=100)
####################
# Forms
####################
class AppManifestForm(StrictModel):
@ -244,9 +242,7 @@ class AppConnectionForm(StrictModel):
api_key: SecretStr | None = Field(default=None, min_length=1, max_length=4096)
####################
# Responses
####################
class AppResponse(BaseModel):

View file

@ -207,7 +207,7 @@ class AddUserForm(SignupForm):
role: str | None = 'pending'
# --- data-access layer ---
# Data-access layer
class AuthsTable:
@ -393,7 +393,7 @@ class AuthsTable:
await session.commit()
await Users.update_user_by_id(user_id, {'email': email}, db=session)
return True
# --- password modification ---
# Password modification
async def update_user_password_by_id(
self,

View file

@ -13,9 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
####################
# Automation DB Schema
####################
class Automation(Base):
@ -56,9 +54,7 @@ class AutomationRun(Base):
)
####################
# Pydantic Models
####################
class AutomationTerminalConfig(BaseModel):
@ -125,9 +121,7 @@ class AutomationListResponse(BaseModel):
total: int
####################
# AutomationTable
####################
# Automation Table
class AutomationTable:
@ -354,9 +348,7 @@ class AutomationTable:
return [AutomationModel.model_validate(r) for r in claimed]
####################
# AutomationRunTable
####################
# Automation Run Table
class AutomationRunTable:

View file

@ -31,9 +31,7 @@ log = logging.getLogger(__name__)
MIN_CALENDAR_RRULE_INTERVAL_SECONDS = 24 * 60 * 60
####################
# Calendar DB Schema
####################
class Calendar(Base):
@ -98,9 +96,7 @@ class CalendarEventAttendee(Base):
)
####################
# Pydantic Models
####################
class CalendarModel(BaseModel):
@ -159,9 +155,7 @@ class CalendarEventAttendeeModel(BaseModel):
updated_at: int
####################
# Forms
####################
class CalendarForm(BaseModel):
@ -228,9 +222,7 @@ class RSVPForm(BaseModel):
status: str # 'accepted' | 'declined' | 'tentative' | 'pending'
####################
# Response Models
####################
class CalendarEventUserResponse(CalendarEventModel):
@ -242,9 +234,7 @@ class CalendarEventListResponse(BaseModel):
total: int
####################
# Table Operations
####################
class CalendarTable:

View file

@ -34,9 +34,7 @@ from sqlalchemy import (
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession
####################
# Channel DB Schema
####################
class Channel(Base):
@ -226,9 +224,7 @@ class ChannelWebhookModel(BaseModel):
updated_at: int # timestamp in epoch (time_ns)
####################
# Forms
####################
class ChannelResponse(ChannelModel):
@ -794,7 +790,7 @@ class ChannelTable:
allowed_channels = []
for channel in channels:
# --- Case A: group or dm => user must be an active member ---
# Case a: group or dm => user must be an active member
if channel.type in ['group', 'dm']:
result = await db.execute(
select(ChannelMember)
@ -810,7 +806,7 @@ class ChannelTable:
allowed_channels.append(await self._to_channel_model(channel, db=db))
continue
# --- Case B: standard channel => rely on ACL permissions ---
# Case b: standard channel => rely on acl permissions
stmt = select(Channel).filter(Channel.id == channel.id)
stmt = self._has_permission(
@ -972,9 +968,7 @@ class ChannelTable:
await emit_changes(recipients)
return True
####################
# Webhook Methods
####################
async def insert_webhook(
self,

View file

@ -32,9 +32,7 @@ from sqlalchemy import (
)
from sqlalchemy.ext.asyncio import AsyncSession
####################
# Helpers
####################
def _normalize_timestamp(timestamp: Optional[int]) -> float:
@ -43,11 +41,11 @@ def _normalize_timestamp(timestamp: Optional[int]) -> float:
if timestamp is None:
return now
# Convert milliseconds to seconds if needed
# Treat values above the seconds-range cutoff as milliseconds.
if timestamp > 10_000_000_000:
timestamp = timestamp / 1000
# Validate: must be after 2020 and not in the future (with 1 day tolerance)
# Accept timestamps from 2020-01-01 through one day ahead of now, inclusive.
min_valid = 1577836800 # 2020-01-01 00:00:00 UTC
max_valid = now + 86400 # 1 day in the future (clock skew tolerance)
@ -157,9 +155,7 @@ def _extract_tool_names(value: Any) -> list[str]:
return names
####################
# ChatMessage DB Schema
####################
class ChatMessage(Base):
@ -210,9 +206,7 @@ class ChatMessage(Base):
)
####################
# Pydantic Models
####################
class ChatMessageModel(BaseModel):
@ -239,9 +233,7 @@ class ChatMessageModel(BaseModel):
updated_at: int
####################
# Table Operations
####################
class ChatMessageTable:

View file

@ -297,9 +297,7 @@ class ChatFileModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class ChatForm(BaseModel):

View file

@ -98,7 +98,7 @@ def _json_value(value: Any) -> Any:
return jsonable_encoder(value)
# ── Model ────────────────────────────────────────────────────────────────────
# Model
class Config(Base):
@ -114,7 +114,7 @@ class Config(Base):
PERSISTENT_ENABLED: ClassVar[bool] = True
OAUTH_PERSISTENT_ENABLED: ClassVar[bool] = False
# ── Class methods ────────────────────────────────────────
# Class methods
@classmethod
def configure(

View file

@ -12,9 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
####################
# Feedback DB Schema
####################
class Feedback(Base):
@ -44,9 +42,7 @@ class FeedbackModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FeedbackResponse(BaseModel):

View file

@ -48,7 +48,7 @@ class FileModel(BaseModel):
updated_at: int | None # timestamp in epoch
# --- metadata structures ---
# Metadata structures
class FileMeta(BaseModel):
name: str | None = None
content_type: str | None = None

View file

@ -12,11 +12,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
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.
####################
class Folder(Base):
@ -78,9 +76,7 @@ class SharedFolderResponse(BaseModel):
updated_at: int
####################
# Forms
####################
class FolderForm(BaseModel):

View file

@ -59,7 +59,7 @@ class FunctionModel(BaseModel):
model_config = ConfigDict(from_attributes=True) # allows ORM model binding
# --- form / schema definitions ---
# Form / schema definitions
class FunctionWithValvesModel(BaseModel):
version_id: str | None = None
id: str
@ -77,9 +77,7 @@ class FunctionWithValvesModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FunctionResponse(BaseModel):

View file

@ -30,11 +30,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
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.
####################
class Group(Base):
@ -102,9 +98,7 @@ class GroupMemberModel(BaseModel):
updated_at: Optional[int] = None # timestamp in epoch
####################
# Forms
####################
class GroupResponse(GroupModel):

View file

@ -41,11 +41,9 @@ log = logging.getLogger(__name__)
# Columns the knowledge base list may be ordered by; anything else falls back to the default.
KNOWLEDGE_SORTABLE_FIELDS = {'name', 'created_at', 'updated_at'}
####################
# Knowledge DB Schema
# Let what was gathered here outlast the one who gathered it,
# and still teach when the builder is gone.
####################
class Knowledge(Base):
@ -149,9 +147,7 @@ class KnowledgeDirectoryForm(BaseModel):
parent_id: Optional[str] = None
####################
# Forms
####################
class KnowledgeUserModel(KnowledgeModel):
user: Optional[UserResponse] = None
file_count: int | None = None
@ -554,12 +550,12 @@ class KnowledgeTable:
.filter(KnowledgeFile.knowledge_id == knowledge_id)
)
# Filter by directory_id (None = root level)
# Omit directory_id to include all directories; supply it to restrict the listing.
directory_id = filter.get('directory_id') if filter else None
if directory_id:
stmt = stmt.filter(KnowledgeFile.directory_id == directory_id)
elif filter and 'directory_id' in filter:
# Explicit None = root level only
# A supplied but falsy directory_id selects root-level files only.
stmt = stmt.filter(KnowledgeFile.directory_id.is_(None))
# Default sort: updated_at descending
@ -885,7 +881,7 @@ class KnowledgeTable:
except Exception:
return False
# ── Directory CRUD ────────────────────────────────────────────────
# Directory CRUD
async def create_directory(
self,

View file

@ -12,9 +12,7 @@ from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, and_, de
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import exists
####################
# Message DB Schema
####################
class MessageReaction(Base):
@ -89,9 +87,7 @@ class MessageModel(BaseModel):
return str(self.updated_at)
####################
# Forms
####################
class MessageForm(BaseModel):

View file

@ -67,7 +67,7 @@ def strip_extracted_content_from_model_knowledge(knowledge: Any) -> Any:
return sanitized
# --- Models DB Schema ---
# Models DB schema
ModelControlKey = Annotated[str, Field(pattern=re.compile(r'^(?!(?:constructor|prototype)\Z)[a-zA-Z][a-zA-Z0-9_-]*\Z'))]

View file

@ -386,9 +386,7 @@ def matching_page(data: dict, query: str = '') -> dict:
return pages[0]
####################
# Note DB Schema
####################
class Note(Base):
@ -462,9 +460,7 @@ class PinnedNote(Base):
created_at = Column(BigInteger, nullable=False)
####################
# Forms
####################
class NoteForm(BaseModel):

View file

@ -15,9 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
####################
# DB MODEL
####################
# DB model
class OAuthSession(Base):
@ -51,9 +49,7 @@ class OAuthSessionModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class OAuthSessionResponse(BaseModel):

View file

@ -11,9 +11,7 @@ from pydantic import BaseModel, ConfigDict
from sqlalchemy import JSON, BigInteger, Column, Index, Text, delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
####################
# PromptHistory DB Schema
####################
class PromptHistory(Base):

View file

@ -59,9 +59,8 @@ class PromptModel(BaseModel):
model_config = ConfigDict(from_attributes=True) # allows ORM model binding
# --- form / schema definitions ---
# Form / schema definitions
# Forms
####################
class PromptUserResponse(PromptModel):
@ -196,7 +195,7 @@ class PromptsTable:
if match is None:
return
return await self._to_prompt_model(match, db=session)
# --- context manager always returns above ---
# Context manager always returns above
return
async def get_prompts(self, db: AsyncSession | None = None) -> list[PromptUserResponse]:

View file

@ -17,9 +17,7 @@ class ShareChatForm(BaseModel):
share_mode: ChatShareMode = None
####################
# SharedChat DB Schema
####################
class SharedChat(Base):
@ -59,9 +57,7 @@ class SharedChatResponse(BaseModel):
created_at: int
####################
# Table Operations
####################
class SharedChatsTable:

View file

@ -18,9 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
####################
# Skills DB Schema
####################
class Skill(Base):
@ -63,9 +61,7 @@ class SkillModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class SkillUserModel(SkillModel):

View file

@ -39,9 +39,8 @@ class TagModel(BaseModel):
model_config = ConfigDict(from_attributes=True) # allows ORM model binding
# --- tag schema forms ---
# Tag schema forms
# Forms
####################
class TagChatIdForm(BaseModel):

View file

@ -66,9 +66,8 @@ class ToolModel(BaseModel):
model_config = ConfigDict(from_attributes=True) # enables ORM mapping
# --- tool request forms ---
# Tool request forms
# Forms
####################
class ToolUserModel(ToolModel):

View file

@ -35,11 +35,9 @@ from sqlalchemy import (
)
from sqlalchemy.ext.asyncio import AsyncSession
####################
# User DB Schema
# Hallowed be the columns defined here, for they hold the
# daily bread of every session. Let none go hungry.
####################
# Let every name entrusted to these records find its place,
# and let no one be forgotten among the many.
class InterfaceTitleSettings(BaseModel):
@ -217,7 +215,7 @@ class UserModel(BaseModel):
)
# validation schema logic
# --- model validators ---
# Model validators
@model_validator(mode='after')
def _ensure_profile_image(self) -> 'UserModel':
"""Assign a generated avatar when no profile image is provided."""
@ -262,9 +260,7 @@ class ApiKeyModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class UpdateProfileForm(BaseModel):
@ -410,7 +406,7 @@ class UsersTable:
return user if result else None
# database read methods
# --- read / lookup operations ---
# Read / lookup operations
async def get_user_by_id(
self,
id: str,
@ -448,10 +444,10 @@ class UsersTable:
if match is None:
return
return UserModel.model_validate(match)
# --- context manager above always returns ---
# Context manager above always returns
return
# --- oauth & integrations ---
# OAuth & integrations
async def get_user_by_oauth_sub(
self,
provider: str,

View file

@ -65,7 +65,7 @@ class MistralLoader:
self.params = params or {}
self.user = user
# PERFORMANCE OPTIMIZATION: Differentiated timeouts for different operations
# Use separate timeouts for different operations.
# This prevents long-running OCR operations from affecting quick operations
# and improves user experience by failing fast on operations that should be quick
self.upload_timeout = min(timeout, 120) # Cap upload at 2 minutes - prevents hanging on large files
@ -73,12 +73,12 @@ class MistralLoader:
self.ocr_timeout = timeout # OCR can take the full timeout - this is the heavy operation
self.cleanup_timeout = 30 # Cleanup should be quick - don't hang on file deletion
# PERFORMANCE OPTIMIZATION: Pre-compute file info to avoid repeated filesystem calls
# Precompute file info to avoid repeated filesystem calls.
# This avoids multiple os.path.basename() and os.path.getsize() calls during processing
self.file_name = os.path.basename(file_path)
self.file_size = os.path.getsize(file_path)
# ENHANCEMENT: Added User-Agent for better API tracking and debugging
# Include User-Agent for API tracking and debugging.
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'User-Agent': 'OpenWebUI-MistralLoader/2.0', # Helps API provider track usage
@ -161,7 +161,7 @@ class MistralLoader:
if attempt == self.max_retries - 1 or not self._is_retryable_error(e):
raise
# PERFORMANCE OPTIMIZATION: Exponential backoff with cap
# Cap exponential backoff between retries.
# Prevents overwhelming the server while ensuring reasonable retry delays
wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds
log.warning(
@ -182,7 +182,7 @@ class MistralLoader:
url = f'{self.base_url}/files'
def upload_request():
# MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime
# Limit the file handle's lifetime with a context manager.
# This ensures the file is closed immediately after reading, reducing memory usage
with open(self.file_path, 'rb') as f:
files = {'file': (self.file_name, f, 'application/pdf')}

View file

@ -34,10 +34,7 @@ class ColBERT(BaseReranker):
DOCKER = kwargs.get('env') == 'docker'
if DOCKER:
# This is a workaround for the issue with the docker container
# where the torch extension is not loaded properly
# and the following error is thrown:
# /root/.cache/torch_extensions/py312_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
# Remove the stale build lock so Docker can load the segmented_maxsim_cpp extension.
lock_file = '/root/.cache/torch_extensions/py312_cpu/segmented_maxsim_cpp/lock'
if os.path.exists(lock_file):

View file

@ -243,12 +243,7 @@ def _get_content_from_url_sync(request, url: str, config: dict):
# Validate URL before making any request (blocks private IPs, non-HTTP, filter list)
validate_url(url)
# YouTube URLs (including youtu.be short links) should go straight to
# YoutubeLoader, which uses youtube-transcript-api and never needs the
# HTTP response body. Probing the URL first is harmful for short URLs:
# youtu.be returns a 303 redirect with Content-Type: application/binary
# when allow_redirects=False, causing the binary-content path to run
# and produce empty docs → HTTP 400.
# Send YouTube URLs directly to YoutubeLoader; probing youtu.be can misclassify its 303 as binary.
if is_youtube_url(url):
loader = get_loader(request, url, config)
docs = loader.load()
@ -814,11 +809,7 @@ async def query_collection_with_hybrid_search(
if native_task_results and all(result is not None for result in native_task_results):
return merge_and_sort_query_results(native_task_results, k=k)
# Fetch every collection's contents once up front so the
# per-query/per-document loop below can reuse them. Each fetch
# offloads to a worker thread, so run them concurrently with
# `asyncio.gather` instead of awaiting them serially — otherwise
# latency scales linearly with `len(collection_names)`.
# Fetch collections concurrently once, then reuse the results across queries and documents.
log.debug(
'query_collection_with_hybrid_search: prefetching %d collections',
len(collection_names),

View file

@ -237,7 +237,7 @@ class ValkeyClient(VectorDBBase):
except Exception:
pass
# ----- version checks ----------------------------------------------------
# Version checks
@staticmethod
def _parse_semver(version_str: str) -> tuple[int, int, int] | None:

View file

@ -20,9 +20,7 @@ log = logging.getLogger(__name__)
router = APIRouter()
####################
# Response Models
####################
class ModelAnalyticsEntry(BaseModel):
@ -51,9 +49,7 @@ class UserAnalyticsResponse(BaseModel):
total: int
####################
# Endpoints
####################
@router.get('/models', response_model=ModelAnalyticsResponse)
@ -283,9 +279,7 @@ async def get_token_usage(
)
####################
# Model Chats Browser
####################
class ModelChatEntry(BaseModel):
@ -344,9 +338,7 @@ async def get_model_chats(
)
####################
# Model Overview
####################
class HistoryEntry(BaseModel):
@ -446,9 +438,7 @@ async def get_model_overview(
return ModelOverviewResponse(history=history, tags=tags)
####################
# Session Explorer
####################
def require_session_access(user=Depends(get_admin_user)):

View file

@ -38,9 +38,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
############################
# GetApps
############################
# Get Apps
@router.get('', response_model=list[AppResponse])
@ -57,9 +55,7 @@ async def get_apps(user=Depends(get_verified_user), db: AsyncSession = Depends(g
]
############################
# PreviewAppManifest
############################
# Preview App Manifest
@router.post('/manifest/preview', response_model=Manifest)
@ -96,9 +92,7 @@ async def preview_app_manifest(
) from None
############################
# CreateNewApp
############################
# Create New App
@router.post('', response_model=AppResponse)
@ -140,9 +134,7 @@ async def create_new_app(
return get_app_response(app)
############################
# GetAppDetailsById
############################
# Get App Details By Id
@router.get('/{app_id}', response_model=AppAccessResponse)
@ -164,9 +156,7 @@ async def get_app_details_by_id(
return result
############################
# UpdateAppById
############################
# Update App By Id
@router.patch('/{app_id}', response_model=AppResponse)
@ -207,9 +197,7 @@ async def update_app_by_id(
return get_app_response(app)
############################
# ConnectAppById
############################
# Connect App By Id
@router.post('/{app_id}/connect', response_model=AppResponse)
@ -245,9 +233,7 @@ async def connect_app_by_id(
return {**get_app_response(app), 'connection_status': 'connected'}
############################
# UninstallAppById
############################
# Uninstall App By Id
@router.delete('/{app_id}', response_model=AppResponse)
@ -257,9 +243,7 @@ async def uninstall_app_by_id(app_id: str, user=Depends(get_admin_user), db: Asy
return get_app_response(app)
############################
# CreateAppInteraction
############################
# Create App Interaction
@router.post('/{app_id}/interactions')

View file

@ -72,7 +72,7 @@ log = logging.getLogger(__name__)
router = APIRouter()
router.include_router(realtime.router)
# --- Constants ---
# Constants
MAX_FILE_SIZE_MB: int = 20
MAX_FILE_SIZE: int = MAX_FILE_SIZE_MB * 1024 * 1024

View file

@ -90,8 +90,8 @@ router = APIRouter(route_class=MfaRoute)
log = logging.getLogger(__name__)
# Forgive us our failed attempts, as we forgive those
# who exceed their allotted rate against this gate.
# Let patience meet those who stumble at the gate,
# and let those within rest safely through the night.
signin_rate_limiter = RateLimiter(limit=5 * 3, window=60 * 3)
# Best-effort throttle only: there is no caller identity before the provider answers,
# and deployments may derive request.client from proxy headers.
@ -242,9 +242,7 @@ async def get_session_user(
return response_data
############################
# Update Profile
############################
@router.post('/update/profile', response_model=UserProfileImageResponse)
@ -275,9 +273,7 @@ async def update_profile(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
############################
# Update Timezone
############################
class UpdateTimezoneForm(BaseModel):
@ -309,9 +305,7 @@ async def update_timezone(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
############################
# Update Password
############################
@router.post('/update/password', response_model=bool)
@ -406,9 +400,7 @@ def extract_group_cn_from_dn(group_dn: str) -> str | None:
return None
############################
# LDAP Authentication
############################
@router.post('/ldap', response_model=SigninResult)
async def ldap_auth(
request: Request,
@ -657,9 +649,7 @@ async def ldap_auth(
raise HTTPException(400, detail='LDAP authentication failed.')
############################
# SignIn
############################
# Sign In
@router.post('/signin', response_model=SigninResult)
@ -790,9 +780,7 @@ async def signin(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
############################
# SignUp
############################
# Sign Up
async def signup_handler(
@ -1016,9 +1004,7 @@ async def signout(request: Request, response: Response, db: AsyncSession = Depen
return JSONResponse(status_code=200, content={'status': True}, headers=response.headers)
############################
# OAuth Session Management
############################
@router.delete('/oauth/sessions/{provider:path}', response_model=bool)
@ -1050,9 +1036,7 @@ async def delete_oauth_session_by_provider(
return True
############################
# AddUser
############################
# Add User
@router.post('/add', response_model=AddUserResponse, response_model_exclude_none=True)
@ -1125,9 +1109,7 @@ async def add_user(
raise HTTPException(500, detail='An internal error occurred while adding the user.')
############################
# GetAdminDetails
############################
# Get Admin Details
@router.get('/admin/details')
@ -1158,9 +1140,7 @@ async def get_admin_details(
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
############################
# ToggleSignUp
############################
# Toggle Sign Up
@router.get('/admin/config')
@ -1348,9 +1328,7 @@ async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=D
return {'ENABLE_LDAP': await Config.get('ldap.enable')}
############################
# API Key
############################
class OAuthConfigForm(BaseModel):
@ -1568,9 +1546,7 @@ async def get_api_key(request: Request, user=Depends(get_current_user), db: Asyn
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
############################
# Token Exchange
############################
class TokenExchangeForm(BaseModel):

View file

@ -37,9 +37,7 @@ router = APIRouter()
PAGE_ITEM_COUNT = 30
############################
# Helpers
############################
async def check_automations_permission(request, user):
@ -154,9 +152,7 @@ async def enrich_automation(automation: AutomationModel, db: AsyncSession, tz: s
)
############################
# GetAutomationItems (paginated)
############################
@router.get('/list')
@ -200,9 +196,7 @@ async def get_automation_items(
}
############################
# CreateNewAutomation
############################
# Create New Automation
@router.post('/create', response_model=AutomationResponse)
@ -238,9 +232,7 @@ async def create_new_automation(
return response
############################
# GetAutomationById
############################
# Get Automation By Id
@router.get('/{id}', response_model=AutomationResponse)
@ -256,9 +248,7 @@ async def get_automation_by_id(
return await enrich_automation(automation, db, tz=user.timezone)
############################
# UpdateAutomationById
############################
# Update Automation By Id
@router.post('/{id}/update', response_model=AutomationResponse)
@ -298,9 +288,7 @@ async def update_automation_by_id(
return response
############################
# ToggleAutomationById
############################
# Toggle Automation By Id
@router.post('/{id}/toggle', response_model=AutomationResponse)
@ -326,9 +314,7 @@ async def toggle_automation_by_id(
return response
############################
# RunAutomationById
############################
# Run Automation By Id
@router.post('/{id}/run')
@ -353,9 +339,7 @@ async def run_automation_by_id(
return await enrich_automation(automation, db, tz=user.timezone)
############################
# DeleteAutomationById
############################
# Delete Automation By Id
@router.delete('/{id}/delete')
@ -381,9 +365,7 @@ async def delete_automation_by_id(
return result
############################
# GetAutomationRuns
############################
# Get Automation Runs
@router.get('/{id}/runs', response_model=list[AutomationRunModel])

View file

@ -81,9 +81,7 @@ async def _check_calendar_access(calendar_id: str, user: UserModel, permission:
raise HTTPException(status_code=403, detail='Access denied')
####################
# Calendar CRUD (static paths first)
####################
@router.get('/', response_model=list[CalendarModel])
@ -137,9 +135,7 @@ async def create_calendar(request: Request, form_data: CalendarForm, user: UserM
return calendar
####################
# Event CRUD (before /{calendar_id} to avoid route conflicts)
####################
@router.get('/events')
@ -399,9 +395,7 @@ async def rsvp_event(
return {'status': True, 'rsvp': result.status}
####################
# Calendar by ID (dynamic path — MUST come after /events* routes)
####################
@router.get('/{calendar_id}', response_model=CalendarModel)

View file

@ -149,11 +149,9 @@ async def get_channel_member_user_ids(
return list(dict.fromkeys([*user_ids, channel.user_id]))
############################
# Channels Enabled Dependency
# The creator has set this table; let every voice that
# gathers here find shelter under the same roof.
############################
# Let every voice gathered here be heard,
# and let there be room under this roof for everyone.
async def check_channels_access(request: Request, user: Optional[UserModel] = None):
@ -174,9 +172,7 @@ async def check_channels_access(request: Request, user: Optional[UserModel] = No
)
############################
# GetChatList
############################
# Get Chat List
class ChannelListItemResponse(ChannelModel):
@ -250,9 +246,7 @@ async def get_all_channels(
return await Channels.get_channels_by_user_id(user.id, db=db)
############################
# GetDMChannelByUserId
############################
# Get DM Channel By User Id
@router.get('/users/{user_id}', response_model=Optional[ChannelModel])
@ -308,9 +302,7 @@ async def get_dm_channel_by_user_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# CreateNewChannel
############################
# Create New Channel
@router.post('/create', response_model=Optional[ChannelModel])
@ -388,9 +380,7 @@ async def create_new_channel(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChannelById
############################
# Get Channel By Id
class ChannelFullResponse(ChannelResponse):
@ -492,9 +482,7 @@ async def get_channel_by_id(
)
############################
# GetChannelMembersById
############################
# Get Channel Members By Id
PAGE_ITEM_COUNT = 30
@ -602,9 +590,7 @@ async def get_channel_members_by_id(
}
#################################################
# UpdateIsActiveMemberByIdAndUserId
#################################################
# Update Is Active Member By Id And User Id
class UpdateActiveMemberForm(BaseModel):
@ -670,9 +656,7 @@ async def update_is_active_member_by_id_and_user_id(
return True
#################################################
# AddMembersById
#################################################
# Add Members By Id
class UpdateMembersForm(BaseModel):
@ -725,9 +709,6 @@ async def add_members_by_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
#################################################
#
#################################################
class RemoveMembersForm(BaseModel):
@ -773,9 +754,7 @@ async def remove_members_by_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# UpdateChannelById
############################
# Update Channel By Id
@router.post('/{id}/update', response_model=Optional[ChannelModel])
@ -853,9 +832,7 @@ async def update_channel_access_by_id(
return channel
############################
# DeleteChannelById
############################
# Delete Channel By Id
@router.delete('/{id}/delete', response_model=bool)
@ -891,9 +868,7 @@ async def delete_channel_by_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChannelMessages
############################
# Get Channel Messages
@router.get('/{channel_id}/apps')
@ -1046,9 +1021,7 @@ async def get_channel_messages(
return messages
############################
# GetPinnedChannelMessages
############################
# Get Pinned Channel Messages
PAGE_ITEM_COUNT_PINNED = 20
@ -1118,9 +1091,7 @@ async def get_pinned_channel_messages(
return messages
############################
# PostNewMessage
############################
# Post New Message
async def send_notification(request, channel, message, active_user_ids, db=None):
@ -1370,10 +1341,7 @@ async def model_response_handler(request, channel, message, user, db=None):
# Resolve model config (same path automations use)
from open_webui.utils.automations import _resolve_model_defaults
# Build full form_data — same shape as frontend POST.
# The channel: prefix routes pipeline events to the
# channel emitter in socket/main.py instead of the
# default chat emitter.
# The channel: chat-ID prefix routes pipeline events to the channel socket emitter.
form_data = {
**await _resolve_model_defaults(request.app, model_id),
'model': model_id,
@ -1390,10 +1358,7 @@ async def model_response_handler(request, channel, message, user, db=None):
if files:
form_data['files'] = files
# Call the full chat completion pipeline — streaming,
# tools, filters, RAG — everything. The pipeline runs as
# an async task; the channel emitter handles progressive
# message updates via socket events.
# Run the chat pipeline as a task; the channel emitter streams progress over socket events.
await request.app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user)
except Exception as e:
@ -1532,9 +1497,7 @@ async def post_new_message(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChannelMessage
############################
# Get Channel Message
@router.get('/{id}/messages/{message_id}', response_model=Optional[MessageResponse])
@ -1573,9 +1536,7 @@ async def get_channel_message(
)
############################
# GetChannelMessageData
############################
# Get Channel Message Data
@router.get('/{id}/messages/{message_id}/data', response_model=Optional[dict])
@ -1608,9 +1569,7 @@ async def get_channel_message_data(
return message.data
############################
# PinChannelMessage
############################
# Pin Channel Message
class PinMessageForm(BaseModel):
@ -1688,9 +1647,7 @@ async def pin_channel_message(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChannelThreadMessages
############################
# Get Channel Thread Messages
@router.get('/{id}/messages/{message_id}/thread', response_model=list[MessageUserResponse])
@ -1754,9 +1711,7 @@ async def get_channel_thread_messages(
return messages
############################
# UpdateMessageById
############################
# Update Message By Id
@router.post('/{id}/messages/{message_id}/update', response_model=Optional[MessageModel])
@ -1829,9 +1784,7 @@ async def update_message_by_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# AddReactionToMessage
############################
# Add Reaction To Message
class ReactionForm(BaseModel):
@ -1907,9 +1860,7 @@ async def add_reaction_to_message(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# RemoveReactionById
############################
# Remove Reaction By Id
@router.post('/{id}/messages/{message_id}/reactions/remove', response_model=bool)
@ -1982,9 +1933,7 @@ async def remove_reaction_by_id_and_user_id_and_name(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# DeleteMessageById
############################
# Delete Message By Id
@router.delete('/{id}/messages/{message_id}/delete', response_model=bool)
@ -2080,9 +2029,7 @@ async def delete_message_by_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# Webhooks
############################
@router.get('/webhooks/{webhook_id}/profile/image')
@ -2263,9 +2210,7 @@ async def delete_channel_webhook(
return deleted
############################
# Public Webhook Endpoint
############################
class WebhookMessageForm(BaseModel):

View file

@ -251,11 +251,9 @@ async def require_chat_import_permission(request: Request, user, db: AsyncSessio
)
############################
# GetChatList
# Get Chat List
# Let the record outlive the session, so that what was
# learned here not need to be learned again.
############################
@router.get('/', response_model=list[ChatTitleIdResponse])
@ -311,10 +309,8 @@ async def mark_chats_read_by_user_id(
}
############################
# GetChatUsageStats
# Get Chat Usage Stats
# EXPERIMENTAL: may be removed in future releases
############################
@router.get('/stats/usage', response_model=ChatUsageStatsListResponse)
@ -422,9 +418,7 @@ async def get_session_user_chat_usage_stats(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChatStatsExport
############################
# Get Chat Stats Export
CHAT_EXPORT_PAGE_ITEM_COUNT = 10
@ -478,7 +472,7 @@ def _process_chat_for_export(chat) -> ChatStatsExport | None:
export_messages[key] = message_stat
# --- Aggregation Logic (copied/adapted from usage stats) ---
# Aggregation logic (copied/adapted from usage stats)
role = message.get('role', '')
if role == 'user':
history_user_messages.append(message)
@ -660,9 +654,7 @@ async def export_chat_stats(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetSingleChatStatsExport
############################
# Get Single Chat Stats Export
@router.get('/stats/export/{chat_id}', response_model=ChatStatsExport | None)
@ -743,9 +735,7 @@ async def delete_all_user_chats(
return result
############################
# GetUserChatList
############################
# Get User Chat List
@router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse])
@ -781,9 +771,7 @@ async def get_user_chat_list_by_user_id(
return await add_active_state_to_chat_list(request, chats)
############################
# CreateNewChat
############################
# Create New Chat
@router.post('/new', response_model=ChatResponse | None)
@ -817,9 +805,7 @@ async def create_new_chat(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# ImportChats
############################
# Import Chats
@router.post('/import', response_model=list[ChatResponse])
@ -853,9 +839,7 @@ async def import_chats(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# ChatConfig
############################
# Chat Config
@router.get('/config', response_model=ChatConfigForm)
@ -892,9 +876,7 @@ async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user
return await get_chat_config_values()
############################
# GetChats
############################
# Get Chats
@router.get('/search', response_model=list[ChatTitleIdResponse])
@ -936,9 +918,7 @@ async def get_chat_search_snippet(
return snippets.get(id)
############################
# GetChatsByFolderId
############################
# Get Chats By Folder Id
@router.get('/folder/{folder_id}', response_model=list[ChatResponse])
@ -986,9 +966,7 @@ async def get_chat_list_by_folder_id(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetPinnedChats
############################
# Get Pinned Chats
@router.get('/pinned', response_model=list[ChatTitleIdResponse])
@ -999,9 +977,7 @@ async def get_user_pinned_chats(
return await add_active_state_to_chat_list(request, chats)
############################
# GetChats
############################
# Get Chats
CHAT_EXPORT_BATCH_SIZE = 100
@ -1045,9 +1021,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
)
############################
# GetArchivedChats
############################
# Get Archived Chats
@router.get('/all/archived', response_model=list[ChatResponse])
@ -1058,9 +1032,7 @@ async def get_user_archived_chats(user=Depends(get_verified_user), db: AsyncSess
]
############################
# GetAllTags
############################
# Get All Tags
@router.get('/all/tags', response_model=list[TagModel])
@ -1073,9 +1045,7 @@ async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession =
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetAllChatsInDB
############################
# Get All Chats In DB
@router.get('/all/db', response_model=list[ChatResponse])
@ -1085,9 +1055,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSessio
return [ChatResponse.model_validate(chat, from_attributes=True) for chat in await Chats.get_chats(db=db)]
############################
# GetArchivedChats
############################
# Get Archived Chats
@router.get('/archived', response_model=list[ChatTitleIdResponse])
@ -1124,9 +1092,7 @@ async def get_archived_session_user_chat_list(
return await add_active_state_to_chat_list(request, chats)
############################
# GetArchivedChatsCount
############################
# Get Archived Chats Count
@router.get('/archived/count', response_model=int)
@ -1137,9 +1103,7 @@ async def get_archived_session_user_chat_count(
return await Chats.count_archived_chats_by_user_id(user.id, db=db)
############################
# ArchiveAllChats
############################
# Archive All Chats
@router.post('/archive/all', response_model=bool)
@ -1152,9 +1116,7 @@ async def archive_all_chats(
return result
############################
# UnarchiveAllChats
############################
# Unarchive All Chats
@router.post('/unarchive/all', response_model=bool)
@ -1173,9 +1135,7 @@ async def unarchive_all_chats(
return result
############################
# UnshareAllChats
############################
# Unshare All Chats
@router.delete('/share/all', response_model=bool)
@ -1238,9 +1198,7 @@ async def get_shared_session_user_chat_list(
)
############################
# GetSharedChatById
############################
# Get Shared Chat By Id
@router.get('/share/{share_id}', response_model=ChatResponse | None)
@ -1288,9 +1246,7 @@ async def get_shared_chat_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
############################
# GetChatsByTags
############################
# Get Chats By Tags
class TagForm(BaseModel):
@ -1318,9 +1274,7 @@ async def get_user_chat_list_by_tag_name(
return await add_active_state_to_chat_list(request, chats)
############################
# CompactChat
############################
# Compact Chat
@router.post('/{id}/compact')
@ -1372,9 +1326,7 @@ async def compact_chat_by_id(
return result
############################
# GetChatById
############################
# Get Chat By Id
@router.get('/{id}', response_model=ChatResponse | None)
@ -1407,9 +1359,7 @@ async def get_chat_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
############################
# UpdateChatById
############################
# Update Chat By Id
@router.post('/{id}', response_model=ChatResponse | None)
@ -1466,9 +1416,7 @@ async def update_chat_by_id(
)
############################
# UpdateChatParams
############################
# Update Chat Params
@router.patch('/{id}/params', response_model=dict)
@ -1499,9 +1447,7 @@ async def update_chat_params(
return chat.chat['params']
############################
# UpdateChatMessageById
############################
# Update Chat Message By Id
class MessageForm(BaseModel):
content: str | None = None
voice: dict | None = None
@ -1613,9 +1559,7 @@ async def delete_chat_message_by_id(
return ChatResponse.model_validate(chat, from_attributes=True)
############################
# SendChatMessageEventById
############################
# Send Chat Message Event By Id
class EventForm(BaseModel):
type: str
data: dict
@ -1669,9 +1613,7 @@ async def send_chat_message_event_by_id(
return False
############################
# DeleteChatById
############################
# Delete Chat By Id
@router.delete('/{id}', response_model=bool)
@ -1727,9 +1669,7 @@ async def delete_chat_by_id(
return result
############################
# GetPinnedStatusById
############################
# Get Pinned Status By Id
@router.get('/{id}/pinned', response_model=bool | None)
@ -1743,9 +1683,7 @@ async def get_pinned_status_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
############################
# PinChatById
############################
# Pin Chat By Id
@router.post('/{id}/pin', response_model=ChatResponse | None)
@ -1767,9 +1705,7 @@ async def pin_chat_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
############################
# CloneChat
############################
# Clone Chat
class CloneForm(BaseModel):
@ -1956,9 +1892,7 @@ async def clone_chat_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
############################
# CloneSharedChatById
############################
# Clone Shared Chat By Id
@router.post('/{id}/clone/shared', response_model=ChatResponse | None)
@ -2040,9 +1974,7 @@ async def clone_shared_chat_by_id(
)
############################
# ArchiveChat
############################
# Archive Chat
@router.post('/{id}/archive', response_model=ChatResponse | None)
@ -2076,7 +2008,7 @@ async def archive_chat_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
# --- Share Chat ---
# Share chat
@router.post('/{id}/share', response_model=ChatResponse | None)
@ -2127,7 +2059,7 @@ async def share_chat_by_id(
return ChatResponse.model_validate(chat, from_attributes=True)
# --- Delete Shared Chat ---
# Delete shared chat
@router.delete('/{id}/share', response_model=bool | None)
@ -2155,9 +2087,7 @@ async def delete_shared_chat_by_id(
return True
############################
# UpdateSharedChatAccessById
############################
# Update Shared Chat Access By Id
class ChatAccessGrantsForm(BaseModel):
@ -2202,9 +2132,7 @@ async def update_shared_chat_access_by_id(
return ChatResponse.model_validate(chat, from_attributes=True)
############################
# GetSharedChatAccessById
############################
# Get Shared Chat Access By Id
@router.get('/shared/{id}/access', response_model=list)
@ -2235,9 +2163,7 @@ async def get_shared_chat_access_by_id(
]
############################
# UpdateChatFolderIdById
############################
# Update Chat Folder Id By Id
class ChatFolderIdForm(BaseModel):
@ -2301,9 +2227,7 @@ async def update_chat_folder_id_by_id(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetChatTagsById
############################
# Get Chat Tags By Id
@router.get('/{id}/tags', response_model=list[TagModel])
@ -2321,9 +2245,7 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user), db: Asyn
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
############################
# AddChatTagById
############################
# Add Chat Tag By Id
@router.post('/{id}/tags', response_model=list[TagModel])
@ -2362,9 +2284,7 @@ async def add_tag_by_id_and_tag_name(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
############################
# DeleteChatTagById
############################
# Delete Chat Tag By Id
@router.delete('/{id}/tags', response_model=list[TagModel])

View file

@ -86,11 +86,9 @@ def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
############################
# ImportConfig
# Thy configuration come, thy settings be done,
# in production as it is in development.
############################
# Import Config
# Let what we prepare in quiet hold when the world grows loud,
# and let those who depend on it find steadiness through change.
class ImportConfigForm(BaseModel):
@ -112,9 +110,7 @@ async def import_config(request: Request, form_data: ImportConfigForm, user=Depe
return await Config.get_all()
############################
# ExportConfig
############################
# Export Config
@router.get('/export', response_model=dict)
@ -145,9 +141,7 @@ async def set_interface_visibility(request: Request, form_data: InterfaceVisibil
return form_data
############################
# Connections Config
############################
class ConnectionsConfigForm(BaseModel):
@ -232,9 +226,7 @@ async def register_oauth_client(
)
############################
# ToolServers Config
############################
class ToolServerConnection(BaseModel):
@ -718,9 +710,7 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn
)
############################
# CodeInterpreterConfig
############################
# Code Interpreter Config
class CodeInterpreterConfigForm(BaseModel):
ENABLE_CODE_EXECUTION: bool
CODE_EXECUTION_ENGINE: str
@ -766,9 +756,7 @@ async def set_code_execution_config(
return values
############################
# SetDefaultModels
############################
# Set Default Models
class ModelsConfigForm(BaseModel):
DEFAULT_MODELS: str | None
DEFAULT_PINNED_MODELS: str | None
@ -878,9 +866,7 @@ async def set_default_suggestions(
return {'suggestions': suggestions, 'i18n': suggestions_i18n}
############################
# SetBanners
############################
# Set Banners
class SetBannersForm(BaseModel):

View file

@ -41,27 +41,10 @@ async def get_config_values(key_map: dict[str, str]) -> dict:
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
# 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
# 2. When a user picks a winner between two models, ratings are adjusted:
# - Winner gains points, loser loses points
# - The amount depends on expected outcome (upset = bigger change)
# 3. The Elo formula: new_rating = old_rating + K * (actual - expected)
# - K=32 controls how much ratings can change per match
# - expected = probability of winning based on current ratings
#
# Query-based re-ranking (optional):
# When a user searches for a topic (e.g., "coding"), we want to show
# which models perform best FOR THAT TOPIC. We do this by:
# 1. Computing semantic similarity between the query and each feedback's tags
# 2. Using that similarity as a weight in the Elo calculation
# 3. Feedbacks about "coding" contribute more to the final ranking
# 4. Feedbacks about unrelated topics (e.g., "cooking") contribute less
# This gives topic-specific leaderboards without needing separate data.
# Let the scales be fair even when our favorite loses,
# and let a better score never go straight to our heads.
# Elo starts at 1000 with K=32; query similarity optionally weights each match.
import os
@ -277,9 +260,7 @@ async def get_model_history(
return ModelHistoryResponse(model_id=model_id, history=history)
############################
# GetConfig
############################
# Get Config
@router.get('/config')
@ -287,9 +268,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
return await get_config_values(EVALUATION_CONFIG_KEYS)
############################
# UpdateConfig
############################
# Update Config
class UpdateConfigForm(BaseModel):

View file

@ -57,11 +57,9 @@ router = APIRouter()
from open_webui.utils.access_control.files import has_access_to_file
from open_webui.utils.json_codec import JSONCodec
############################
# Upload File
# What was entrusted here was given in good faith. Let it
# be returned the same way, whole and undiminished.
############################
def _is_text_file(file_path: str, chunk_size: int = 8192) -> bool:
@ -183,10 +181,7 @@ async def process_uploaded_file(
)
):
if content_type.startswith('video/'):
# Videos are stored as-is for downstream multimodal
# processing (Tools, vision models). Attempting text
# extraction causes "Timeout reached while detecting
# encoding" errors.
# Keep videos for multimodal processing; text extraction can time out detecting their encoding.
log.info('Video file detected (%s), skipping text extraction', content_type)
else:
raise Exception(f'File type {content_type} is not supported for processing')
@ -507,9 +502,7 @@ async def upload_file_handler(
)
############################
# List Files
############################
PAGE_SIZE = 50
@ -535,9 +528,7 @@ async def list_files(
return result
############################
# Search Files
############################
async def _search_files(filename: str, content: bool, skip: int, limit: int, user_id: str | None, db: AsyncSession):
@ -583,9 +574,7 @@ async def search_user_files(
return await _search_files(filename, content, skip, limit, user.id, db)
############################
# Count Files
############################
@router.get('/count', response_model=int)
@ -605,9 +594,7 @@ async def count_user_files(
return await Files.count_files_by_user_id(user_id=user.id, db=db)
############################
# Delete All Files
############################
@router.delete('/all')
@ -635,9 +622,7 @@ async def delete_all_files(
)
############################
# Get File By Id
############################
@router.get('/{id}', response_model=Optional[FileModel])
@ -665,10 +650,7 @@ async def get_file_process_status(
stream: bool = Query(False),
user=Depends(get_verified_user),
):
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations manage their own short-lived sessions internally.
# Holding a session here would keep a connection for the entire stream
# (up to two hours) and exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
file = await Files.get_file_by_id(id)
if not file:
@ -718,9 +700,7 @@ async def get_file_process_status(
)
############################
# Get File Data Content By Id
############################
@router.get('/{id}/data/content')
@ -744,9 +724,7 @@ async def get_file_data_content_by_id(
)
############################
# Update File Data Content By Id
############################
class ContentForm(BaseModel):
@ -838,9 +816,7 @@ async def update_file_data_content_by_id(
)
############################
# Get File Content By Id
############################
@router.get('/{id}/content')
@ -1008,9 +984,7 @@ async def get_file_content_by_id(
)
############################
# Rename File By Id
############################
class FileRenameForm(BaseModel):
@ -1056,9 +1030,7 @@ async def rename_file_by_id(
)
############################
# Delete File By Id
############################
@router.delete('/{id}')

View file

@ -84,9 +84,7 @@ async def check_folders_permission(request: Request, user, db=None):
)
############################
# Get Folders
############################
@router.get('/', response_model=list[FolderNameIdResponse])
@ -142,9 +140,7 @@ async def get_folders(
]
############################
# Create Folder
############################
@router.post('/')
@ -234,9 +230,7 @@ async def create_folder(
)
############################
# Get Shared Folders
############################
@router.get('/shared')
@ -287,9 +281,7 @@ async def get_shared_folders(
return results
############################
# Get Folders By Id
############################
class FolderResponse(FolderModel):
@ -327,9 +319,7 @@ async def get_folder_by_id(
)
############################
# Update Folder Name By Id
############################
@router.post('/{id}/update')
@ -416,9 +406,7 @@ async def update_folder_name_by_id(
)
############################
# Update Folder Parent Id By Id
############################
class FolderParentIdForm(BaseModel):
@ -478,9 +466,7 @@ async def update_folder_parent_id_by_id(
)
############################
# Update Folder Is Expanded By Id
############################
class FolderIsExpandedForm(BaseModel):
@ -522,9 +508,7 @@ async def update_folder_is_expanded_by_id(
)
############################
# Update Folder Access By Id
############################
class FolderAccessGrantsForm(BaseModel):
@ -584,9 +568,7 @@ async def update_folder_access_by_id(
}
############################
# Get Shared Folder Chats
############################
@router.get('/{id}/shared/chats')
@ -702,9 +684,7 @@ async def mark_folder_chats_read_by_id(
}
############################
# Delete Folder By Id
############################
@router.delete('/{id}')

View file

@ -40,11 +40,9 @@ log = logging.getLogger(__name__)
router = APIRouter()
############################
# GetFunctions
# Our daily functions give us, and forgive us
# our deprecated methods, as we refactor those who depend on us.
############################
# Get Functions
# Let the work begun here be faithful to its promise,
# and let it serve those who call upon it, quietly and well.
@router.get('/', response_model=list[FunctionResponse])
@ -63,9 +61,7 @@ async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Dep
return await Functions.get_function_list(db=db)
############################
# ExportFunctions
############################
# Export Functions
@router.get('/export', response_model=list[FunctionModel | FunctionWithValvesModel])
@ -80,9 +76,7 @@ async def get_functions(
return await Functions.get_functions(include_valves=include_valves, db=db)
############################
# LoadFunctionFromLink
############################
# Load Function From Link
class LoadUrlForm(BaseModel):
@ -153,9 +147,7 @@ async def load_function_from_url(request: Request, form_data: LoadUrlForm, user=
)
############################
# SyncFunctions
############################
# Sync Functions
class SyncFunctionsForm(BaseModel):
@ -199,9 +191,7 @@ async def sync_functions(
)
############################
# CreateNewFunction
############################
# Create New Function
@router.post('/create', response_model=FunctionResponse | None)
@ -267,9 +257,7 @@ async def create_new_function(
)
############################
# GetFunctionById
############################
# Get Function By Id
@router.get('/id/{id}', response_model=FunctionModel | None)
@ -285,9 +273,7 @@ async def get_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes
)
############################
# ToggleFunctionById
############################
# Toggle Function By Id
@router.post('/id/{id}/toggle', response_model=FunctionModel | None)
@ -338,9 +324,7 @@ async def toggle_function_by_id(
)
############################
# ToggleGlobalById
############################
# Toggle Global By Id
@router.post('/id/{id}/toggle/global', response_model=FunctionModel | None)
@ -375,9 +359,7 @@ async def toggle_global_by_id(
)
############################
# UpdateFunctionById
############################
# Update Function By Id
@router.post('/id/{id}/update', response_model=FunctionModel | None)
@ -433,9 +415,7 @@ async def _update_function(request, id, form_data, user, db, version_id=None):
)
############################
# DeleteFunctionById
############################
# Delete Function By Id
@router.delete('/id/{id}/delete', response_model=bool)
@ -462,9 +442,7 @@ async def delete_function_by_id(
return result
############################
# GetFunctionValves
############################
# Get Function Valves
@router.get('/id/{id}/valves', response_model=dict | None)
@ -488,9 +466,7 @@ async def get_function_valves_by_id(
)
############################
# GetFunctionValvesSpec
############################
# Get Function Valves Spec
@router.get('/id/{id}/valves/spec', response_model=dict | None)
@ -518,9 +494,7 @@ async def get_function_valves_spec_by_id(
)
############################
# UpdateFunctionValves
############################
# Update Function Valves
@router.post('/id/{id}/valves/update', response_model=dict | None)
@ -570,9 +544,7 @@ async def update_function_valves_by_id(
)
############################
# FunctionUserValves
############################
# Function User Valves
@router.get('/id/{id}/valves/user', response_model=dict | None)

View file

@ -40,9 +40,7 @@ log = logging.getLogger(__name__)
router = APIRouter()
############################
# GetFunctions
############################
# Get Functions
@router.get('/', response_model=list[GroupResponse])
@ -64,9 +62,7 @@ async def get_groups(
return groups
############################
# CreateNewGroup
############################
# Create New Group
@router.post('/create', response_model=Optional[GroupResponse])
@ -107,9 +103,7 @@ async def create_new_group(
)
############################
# GetGroupById
############################
# Get Group By Id
@router.get('/id/{id}', response_model=Optional[GroupResponse])
@ -142,9 +136,7 @@ async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: Asy
)
############################
# ExportGroupById
############################
# Export Group By Id
class GroupExportResponse(GroupResponse):
@ -168,9 +160,7 @@ async def export_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes
)
############################
# GetUsersInGroupById
############################
# Get Users In Group By Id
@router.post('/id/{id}/users', response_model=list[UserInfoResponse])
@ -186,9 +176,7 @@ async def get_users_in_group(id: str, user=Depends(get_admin_user), db: AsyncSes
)
############################
# UpdateGroupById
############################
# Update Group By Id
@router.post('/id/{id}/update', response_model=Optional[GroupResponse])
@ -231,9 +219,7 @@ async def update_group_by_id(
)
############################
# AddUserToGroupByUserIdAndGroupId
############################
# Add User To Group By User Id And Group Id
@router.post('/id/{id}/users/add', response_model=Optional[GroupResponse])
@ -317,9 +303,7 @@ async def remove_users_from_group(
)
############################
# DeleteGroupById
############################
# Delete Group By Id
@router.delete('/id/{id}/delete', response_model=bool)
@ -363,9 +347,7 @@ async def delete_group_by_id(
)
############################
# PreviewGroupAccess
############################
# Preview Group Access
@router.get('/id/{id}/preview')

View file

@ -53,9 +53,7 @@ log = logging.getLogger(__name__)
router = APIRouter()
############################
# getKnowledgeBases
############################
PAGE_ITEM_COUNT = 30
@ -79,9 +77,7 @@ async def delete_file_resource(file: FileModel, db: AsyncSession) -> bool:
return result
############################
# Knowledge Base Embedding
############################
# Knowledge that sits unread serves no one. Let what is
# stored here find the ones who need it.
@ -311,9 +307,7 @@ async def search_knowledge_files(
return await Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit, db=db)
############################
# CreateNewKnowledge
############################
# Create New Knowledge
@router.post('/create', response_model=KnowledgeResponse | None)
@ -322,10 +316,7 @@ async def create_new_knowledge(
form_data: KnowledgeForm,
user=Depends(get_verified_user),
):
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (has_permission, filter_allowed_access_grants, insert_new_knowledge) manage their own sessions.
# This prevents holding a connection during embed_knowledge_base_metadata()
# which makes external embedding API calls (1-5+ seconds).
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
if user.role != 'admin' and not await has_permission(
user.id, 'workspace.knowledge', await Config.get('user.permissions')
):
@ -367,9 +358,7 @@ async def create_new_knowledge(
)
############################
# ReindexKnowledgeFiles
############################
# Reindex Knowledge Files
@router.post('/reindex', response_model=bool)
@ -462,9 +451,7 @@ async def reindex_knowledge_files(
return True
############################
# ReindexKnowledgeBases
############################
# Reindex Knowledge Bases
@router.post('/metadata/reindex', response_model=dict)
@ -495,9 +482,7 @@ async def reindex_knowledge_base_metadata_embeddings(
return {'total': len(knowledge_bases), 'success': success_count}
############################
# External Knowledge Sources
############################
class ExternalKnowledgeSourceForm(BaseModel):
@ -1079,9 +1064,7 @@ async def update_external_knowledge_source(
return updated
############################
# GetKnowledgeById
############################
# Get Knowledge By Id
class KnowledgeFilesResponse(KnowledgeResponse):
@ -1131,9 +1114,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Asyn
)
############################
# UpdateKnowledgeById
############################
# Update Knowledge By Id
@router.post('/{id}/update', response_model=KnowledgeFilesResponse | None)
@ -1143,10 +1124,7 @@ async def update_knowledge_by_id(
form_data: KnowledgeForm,
user=Depends(get_verified_user),
):
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations manage their own short-lived sessions internally.
# This prevents holding a connection during embed_knowledge_base_metadata()
# which makes external embedding API calls (1-5+ seconds).
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
knowledge = await Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
@ -1203,9 +1181,7 @@ async def update_knowledge_by_id(
)
############################
# UpdateKnowledgeAccessById
############################
# Update Knowledge Access By Id
class KnowledgeAccessGrantsForm(BaseModel):
@ -1267,9 +1243,7 @@ async def update_knowledge_access_by_id(
return response
############################
# GetPendingKnowledgeFiles
############################
# Get Pending Knowledge Files
@router.get('/{id}/files/pending')
@ -1289,10 +1263,7 @@ async def get_pending_knowledge_files(
When ``stream=true``, returns an SSE stream that polls every 3 seconds
and emits the current pending file list. Closes when no files remain.
"""
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations manage their own short-lived sessions internally.
# Holding a session here would keep a connection for the entire stream
# (up to an hour) and exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
knowledge = await Knowledges.get_knowledge_by_id(id=id)
if not knowledge:
raise HTTPException(
@ -1350,9 +1321,7 @@ async def get_pending_knowledge_files(
)
############################
# GetKnowledgeFilesById
############################
# Get Knowledge Files By Id
@router.get('/{id}/files', response_model=KnowledgeFileListResponse)
@ -1419,9 +1388,7 @@ async def get_knowledge_files_by_id(
return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db)
############################
# AddFileToKnowledge
############################
# Add File To Knowledge
async def check_knowledge_upload_limits(
@ -1681,9 +1648,7 @@ async def update_file_from_knowledge_by_id(
)
############################
# RemoveFileFromKnowledge
############################
# Remove File From Knowledge
@router.post('/{id}/file/remove', response_model=KnowledgeFilesResponse | None)
@ -1770,9 +1735,7 @@ async def remove_file_from_knowledge_by_id(
)
############################
# DeleteKnowledgeById
############################
# Delete Knowledge By Id
@router.delete('/{id}/delete', response_model=bool)
@ -1860,9 +1823,7 @@ async def delete_knowledge_by_id(
return result
############################
# ResetKnowledgeById
############################
# Reset Knowledge By Id
@router.post('/{id}/reset', response_model=KnowledgeResponse | None)
@ -1922,9 +1883,7 @@ async def reset_knowledge_by_id(
return knowledge
############################
# SyncKnowledgeDiff
############################
# Sync Knowledge Diff
class FileManifestEntry(BaseModel):
@ -1961,7 +1920,7 @@ async def sync_knowledge_diff(
"""
await _verify_knowledge_write_access(id, user, db)
# ── Index existing state ──
# Index existing state
existing_directories = await Knowledges.get_all_directories(id, db=db)
# Build directory path lookups
@ -2002,7 +1961,7 @@ async def sync_knowledge_diff(
'checksum': stored_checksum,
}
# ── Diff files ──
# Diff files
added: list[dict] = []
modified: list[dict] = []
deleted: list[dict] = []
@ -2046,7 +2005,7 @@ async def sync_knowledge_diff(
db=db,
)
# ── Diff directories ──
# Diff directories
required_directory_paths: set[str] = set()
for entry in form_data.manifest:
if entry.path:
@ -2070,9 +2029,7 @@ async def sync_knowledge_diff(
)
############################
# SyncKnowledgeCleanup
############################
# Sync Knowledge Cleanup
class SyncCleanupForm(BaseModel):
@ -2093,7 +2050,7 @@ async def sync_knowledge_cleanup(
"""
await _verify_knowledge_write_access(id, user, db)
# ── Remove deleted files ──
# Remove deleted files
for file_id in form_data.file_ids:
file = await Files.get_file_by_id(file_id, db=db)
if not file:
@ -2120,7 +2077,7 @@ async def sync_knowledge_cleanup(
):
await delete_file_resource(file, db)
# ── Remove orphaned directories (children before parents) ──
# Remove orphaned directories (children before parents)
for dir_id in reversed(form_data.dir_ids):
# Only delete directories that belong to this knowledge base.
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
@ -2131,9 +2088,7 @@ async def sync_knowledge_cleanup(
return {'status': True}
############################
# AddFilesToKnowledge
############################
# Add Files To Knowledge
@router.post('/{id}/files/batch/add', response_model=KnowledgeFilesResponse | None)
@ -2276,9 +2231,7 @@ async def add_files_to_knowledge_batch(
)
############################
# ExportKnowledgeById
############################
# Export Knowledge By Id
@router.get('/{id}/export')
@ -2324,9 +2277,7 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Asyn
)
############################
# Directory endpoints
############################
class KnowledgeDirectoryCreateForm(BaseModel):

View file

@ -45,11 +45,9 @@ async def check_memories_permission(user):
)
############################
# GetMemories
# Get Memories
# Let what is remembered here spare someone the cost
# of learning it twice.
############################
@router.get('/', response_model=list[MemoryModel])
@ -63,9 +61,7 @@ async def get_memories(
return await Memories.get_memories_by_user_id(user.id, db=db)
############################
# AddMemory
############################
# Add Memory
class AddMemoryForm(BaseModel):
@ -316,9 +312,7 @@ async def update_memories(
return response
############################
# QueryMemory
############################
# Query Memory
class QueryMemoryForm(BaseModel):
@ -332,10 +326,7 @@ async def query_memory(
form_data: QueryMemoryForm,
user=Depends(get_verified_user),
):
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (get_memories_by_user_id) manage their own short-lived sessions.
# This prevents holding a connection during EMBEDDING_FUNCTION()
# which makes external embedding API calls (1-5+ seconds).
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
await check_memories_permission(user)
memories = await Memories.get_memories_by_user_id(user.id)
@ -352,12 +343,7 @@ async def query_memory(
limit=form_data.k,
)
# Filter results by relevance threshold to avoid returning unrelated
# memories. Vector similarity search always returns the top-K nearest
# neighbours even when they are completely irrelevant; applying the
# same RELEVANCE_THRESHOLD used by RAG ensures only genuinely matching
# memories are surfaced (distances are normalised to 0→1, higher is
# better).
# Apply the RAG relevance threshold; top-K results can include unrelated memories.
relevance_threshold = await Config.get('rag.relevance_threshold', 0.0)
if results and relevance_threshold > 0.0 and results.distances and results.distances[0]:
from open_webui.retrieval.vector.main import SearchResult
@ -442,9 +428,7 @@ async def read_memory_path(
}
############################
# ReindexMemoryVectorDB
############################
# Reindex Memory Vector DB
@router.post('/reindex')
async def reindex_memories_from_vector_db(
request: Request,
@ -506,9 +490,7 @@ async def reset_memory_from_vector_db(
return True
############################
# DeleteMemoriesByUserId
############################
# Delete Memories By User Id
@router.delete('/delete/user', response_model=bool)
@ -538,9 +520,7 @@ async def delete_memory_by_user_id(
return False
############################
# UpdateMemoryById
############################
# Update Memory By Id
@router.post('/{memory_id}/update', response_model=MemoryModel | None)
@ -550,10 +530,7 @@ async def update_memory_by_id(
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (update_memory_by_id_and_user_id) manage their own
# short-lived sessions. This prevents holding a connection during
# EMBEDDING_FUNCTION() which makes external API calls (1-5+ seconds).
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
await check_memories_permission(user)
content = clean_memory_content(form_data.content) if form_data.content is not None else None
@ -602,9 +579,7 @@ async def update_memory_by_id(
return memory
############################
# DeleteMemoryById
############################
# Delete Memory By Id
@router.delete('/{memory_id}', response_model=bool)

View file

@ -214,11 +214,7 @@ async def _verify_knowledge_file_access(
)
###########################
# GetModels
# Let each model here be judged by what it does and not
# by what it claims. The house deserves honest servants.
###########################
# Get Models
PAGE_ITEM_COUNT = 30
@ -302,9 +298,7 @@ async def get_models(
)
###########################
# GetBaseModels
###########################
# Get Base Models
@router.get('/all', response_model=list[ModelResponse])
@ -327,9 +321,7 @@ async def get_base_models(
return await Models.get_base_models(tag=tag, db=db)
###########################
# GetModelTags
###########################
# Get Model Tags
@router.get('/tags', response_model=list[str])
@ -342,9 +334,7 @@ async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Dep
return sorted(tags)
############################
# CreateNewModel
############################
# Create New Model
@router.post('/create', response_model=ModelModel | None)
@ -436,9 +426,7 @@ async def create_new_model(
return model_response(model)
############################
# ExportModels
############################
# Export Models
class ModelExportResponse(ModelModel):
@ -496,9 +484,7 @@ async def export_models(
return exported
############################
# ImportModels
############################
# Import Models
class ModelsImportForm(BaseModel):
@ -737,9 +723,7 @@ async def import_models(
raise HTTPException(status_code=500, detail=str(e))
############################
# SyncModels
############################
# Sync Models
class SyncModelsForm(BaseModel):
@ -776,9 +760,7 @@ async def sync_models(
return models
###########################
# GetModelById
###########################
# Get Model By Id
class ModelIdForm(BaseModel):
@ -835,9 +817,7 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes
)
###########################
# GetModelById
###########################
# Get Model By Id
async def authorized_model_history(id, user, db):
@ -1084,9 +1064,7 @@ async def get_model_profile_image(
)
############################
# ToggleModelById
############################
# Toggle Model By Id
@router.post('/model/toggle', response_model=ModelResponse | None)
@ -1135,9 +1113,7 @@ async def toggle_model_by_id(
)
############################
# UpdateModelById
############################
# Update Model By Id
@router.post('/model/update', response_model=ModelModel | None)
@ -1212,9 +1188,7 @@ async def update_model_by_id(
return model_response(model)
############################
# UpdateModelAccessById
############################
# Update Model Access By Id
class ModelAccessGrantsForm(BaseModel):
@ -1294,9 +1268,7 @@ async def update_model_access_by_id(
return model_response(model)
############################
# DeleteModelById
############################
# Delete Model By Id
@router.post('/model/delete', response_model=bool)

View file

@ -95,9 +95,7 @@ async def leave_note_rooms_for_revoked_users(
await leave_room_for_users(room, user_ids)
############################
# GetNotes
############################
# Get Notes
class NoteItemResponse(BaseModel):
@ -148,9 +146,7 @@ async def get_notes(
]
############################
# GetPinnedNotes
############################
# Get Pinned Notes
@router.get('/pinned', response_model=list[NoteItemResponse])
@ -229,9 +225,7 @@ async def search_notes(
return result
############################
# CreateNewNote
############################
# Create New Note
@router.post('/create', response_model=Optional[NoteModel])
@ -270,9 +264,7 @@ async def create_new_note(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
# GetNoteById
############################
# Get Note By Id
class NoteResponse(NoteModel):
@ -521,9 +513,7 @@ async def create_note_chat_by_id(
return chat
############################
# UpdateNoteById
############################
# Update Note By Id
@router.post('/{id}/update', response_model=Optional[NoteModel])
@ -609,9 +599,7 @@ async def update_note_pages(
return await update_note_by_id(request, id, NoteUpdateForm(page_operation=form_data), user, db)
############################
# UpdateNoteAccessById
############################
# Update Note Access By Id
class NoteAccessGrantsForm(BaseModel):
@ -667,9 +655,7 @@ async def update_note_access_by_id(
return note
############################
# PinNoteById
############################
# Pin Note By Id
@router.post('/{id}/pin', response_model=Optional[NoteModel])
@ -710,9 +696,7 @@ async def pin_note_by_id(
return note
############################
# DeleteNoteById
############################
# Delete Note By Id
@router.delete('/{id}/delete', response_model=bool)

View file

@ -189,11 +189,7 @@ def get_api_key(idx, url, configs):
return configs.get(str(idx), configs.get(base_url, {})).get('key', None) # Legacy support
##########################################
#
# API routes
#
##########################################
router = APIRouter()
@ -1138,10 +1134,7 @@ async def generate_chat_completion(
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions.
# This prevents holding a connection during the entire LLM call (30-60+ seconds),
# which would exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
# bypass_filter and bypass_system_prompt are read from request.state to prevent
# external clients from setting them via query parameter. Only internal
@ -1254,10 +1247,7 @@ async def generate_openai_completion(
user=Depends(get_verified_user), # noqa: B008
):
"""Forward a text completion request via the OpenAI-compatible proxy."""
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions.
# This prevents holding a connection during the entire LLM call (30-60+ seconds),
# which would exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
metadata = form_data.pop('metadata', None)
try:
@ -1359,10 +1349,7 @@ async def generate_openai_chat_completion(
user=Depends(get_verified_user), # noqa: B008
):
"""Forward a chat completion request via the OpenAI-compatible proxy."""
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions.
# This prevents holding a connection during the entire LLM call (30-60+ seconds),
# which would exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
metadata = form_data.pop('metadata', None)
try:

View file

@ -60,13 +60,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
log = logging.getLogger(__name__)
##########################################
#
# Utility functions
# Let the responses returned through this gate be worth
# the question that summoned them.
#
##########################################
# Headers that become stale after aiohttp auto-decompresses the upstream
# response body. Forwarding them verbatim causes desktop / programmatic
@ -150,11 +146,7 @@ def openai_reasoning_model_handler(payload):
return payload
##########################################
#
# API routes
#
##########################################
router = APIRouter()
@ -1412,10 +1404,7 @@ async def generate_chat_completion(
if not await Config.get('openai.enable'):
raise HTTPException(status_code=503, detail='OpenAI API is disabled')
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
# Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions.
# This prevents holding a connection during the entire LLM call (30-60+ seconds),
# which would exhaust the connection pool under concurrent load.
# Let DB helpers own short-lived sessions so remote calls and streams do not hold pool connections.
# bypass_filter and bypass_system_prompt are read from request.state to prevent
# external clients from setting them via query parameter. Only internal

View file

@ -29,13 +29,9 @@ from starlette.responses import FileResponse
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.
#
##################################
def get_sorted_filters(model_id, models):
@ -192,11 +188,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
return payload
##################################
#
# Pipelines Endpoints
#
##################################
router = APIRouter()

View file

@ -44,11 +44,9 @@ router = APIRouter()
PAGE_ITEM_COUNT = 30
############################
# GetPrompts
# Get Prompts
# The hardest part is knowing what to ask. Let the right
# question already be here when it is needed.
############################
@router.get('/', response_model=list[PromptModel])
@ -135,9 +133,7 @@ async def get_prompt_list(
)
############################
# CreateNewPrompt
############################
# Create New Prompt
@router.post('/create', response_model=PromptModel | None)
@ -197,9 +193,7 @@ async def create_new_prompt(
)
############################
# GetPromptById
############################
# Get Prompt By Id
@router.get('/id/{prompt_id}', response_model=PromptAccessResponse | None)
@ -241,9 +235,7 @@ async def get_prompt_by_id(
)
############################
# UpdatePromptById
############################
# Update Prompt By Id
@router.post('/id/{prompt_id}/update', response_model=PromptModel | None)
@ -307,9 +299,7 @@ async def update_prompt_by_id(
)
############################
# UpdatePromptMetadata
############################
# Update Prompt Metadata
@router.post('/id/{prompt_id}/update/meta', response_model=PromptModel | None)
@ -421,9 +411,7 @@ async def set_prompt_version(
)
############################
# UpdatePromptAccessById
############################
# Update Prompt Access By Id
class PromptAccessGrantsForm(BaseModel):
@ -482,9 +470,7 @@ async def update_prompt_access_by_id(
return updated_prompt
############################
# TogglePromptActiveById
############################
# Toggle Prompt Active By Id
@router.post('/id/{prompt_id}/toggle', response_model=PromptModel | None)
@ -535,9 +521,7 @@ async def toggle_prompt_active(
)
############################
# DeletePromptById
############################
# Delete Prompt By Id
@router.delete('/id/{prompt_id}/delete', response_model=bool)
@ -583,9 +567,7 @@ async def delete_prompt_by_id(
return result
############################
# Prompt History Endpoints
############################
@router.get('/id/{prompt_id}/history', response_model=list[PromptHistoryResponse])

View file

@ -135,13 +135,9 @@ log = logging.getLogger(__name__)
TIKTOKEN_DISALLOWED_SPECIAL = ()
##########################################
#
# Utility functions
# Give us this day our relevant chunks, and lead us
# not into hallucination, but deliver us from noise.
#
##########################################
# Let us find what is true among the many voices,
# and let us not mistake the loudest answer for the wisest.
def get_ef(
@ -240,11 +236,7 @@ def get_rf(
return rf
##########################################
#
# API routes
#
##########################################
RETRIEVAL_CONFIG_KEYS = {
@ -1243,11 +1235,7 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend
}
####################################
#
# Document process and retrieval
#
####################################
def can_merge_chunks(a: Document, b: Document) -> bool:
@ -2964,11 +2952,7 @@ async def query_collection_handler(
)
####################################
#
# Vector DB operations
#
####################################
class DeleteForm(BaseModel):
@ -3006,13 +2990,7 @@ async def delete_entries_from_collection(
detail=ERROR_MESSAGES.DEFAULT('File has no hash; cannot delete vector entries by hash.'),
)
# Pre-existing bug: this used `metadata=` which is not a
# parameter on `VectorDBBase.delete` nor on any backend
# implementation, so the call always raised TypeError that
# was silently swallowed by the surrounding `except
# Exception` and the endpoint reported `{'status': False}`
# for every request. Use `filter` to actually do what the
# endpoint name promises.
# Vector deletion accepts filter, not metadata; passing the wrong keyword raises TypeError.
await ASYNC_VECTOR_DB_CLIENT.delete(
collection_name=form_data.collection_name,
filter={'hash': hash},

View file

@ -48,9 +48,7 @@ PAGE_ITEM_COUNT = 30
router = APIRouter()
############################
# GetSkills
############################
# Get Skills
@router.get('/', response_model=list[SkillUserResponse])
@ -72,9 +70,7 @@ async def get_skills(
return skills
############################
# GetSkillList
############################
# Get Skill List
@router.get('/list', response_model=SkillAccessListResponse)
@ -134,9 +130,7 @@ async def get_skill_list(
)
############################
# ExportSkills
############################
# Export Skills
async def authorized_skill(id, user, permission='read', db=None):
@ -211,9 +205,7 @@ async def export_skills(
raise HTTPException(400, str(error))
############################
# CreateNewSkill
############################
# Create New Skill
@router.post('/create', response_model=Optional[SkillResponse])
@ -290,9 +282,7 @@ async def create_new_skill(
)
############################
# GetSkillById
############################
# Get Skill By Id
@router.get('/id/{id}', response_model=Optional[SkillDetailResponse])
@ -337,9 +327,7 @@ async def get_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes
)
############################
# UpdateSkillById
############################
# Update Skill By Id
@router.post('/id/{id}/update', response_model=Optional[SkillModel])
@ -403,9 +391,7 @@ async def update_skill_by_id(
)
############################
# UpdateSkillAccessById
############################
# Update Skill Access By Id
class SkillAccessGrantsForm(BaseModel):
@ -464,9 +450,7 @@ async def update_skill_access_by_id(
return skill
############################
# ToggleSkillById
############################
# Toggle Skill By Id
@router.post('/id/{id}/toggle', response_model=Optional[SkillModel])
@ -517,9 +501,7 @@ async def toggle_skill_by_id(
)
############################
# DeleteSkillById
############################
# Delete Skill By Id
@router.delete('/id/{id}/delete', response_model=bool)

View file

@ -99,11 +99,7 @@ async def get_task_model_generation_config(default_model_id: str, models) -> tup
)
##################################
#
# Task Endpoints
#
##################################
@router.get('/config')

View file

@ -480,9 +480,7 @@ async def proxy_terminal(
return JSONResponse({'error': f'Terminal proxy error: {error}'}, status_code=502)
# ---------------------------------------------------------------------------
# WebSocket proxy for interactive terminal sessions
# ---------------------------------------------------------------------------
async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):

View file

@ -70,11 +70,9 @@ async def get_tool_module(request, tool_id, load_from_db=True):
return tool_module
############################
# GetTools
# Get Tools
# The danger is not in having tools, but in reaching
# for the wrong one. Let the choice here be deliberate.
############################
@router.get('/', response_model=list[ToolUserResponse])
@ -242,9 +240,7 @@ async def get_tool_specs(request: Request, id: str, user=Depends(get_verified_us
raise HTTPException(status_code=502, detail='Unable to load tools')
############################
# GetToolList
############################
# Get Tool List
class MCPAppRPCForm(BaseModel):
@ -399,9 +395,7 @@ async def toggle_tool_by_id(
return tool
############################
# LoadFunctionFromLink
############################
# Load Function From Link
class LoadUrlForm(BaseModel):
@ -472,9 +466,7 @@ async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depe
)
############################
# ExportTools
############################
# Export Tools
@router.get('/export', response_model=list[ToolModel])
@ -502,9 +494,7 @@ async def export_tools(
)
############################
# CreateNewTools
############################
# Create New Tools
@router.post('/create', response_model=ToolResponse | None)
@ -591,9 +581,7 @@ async def create_new_tools(
)
############################
# GetToolsById
############################
# Get Tools By Id
@router.get('/id/{id}', response_model=ToolAccessResponse | None)
@ -640,9 +628,7 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes
)
############################
# UpdateToolsById
############################
# Update Tools By Id
@router.post('/id/{id}/update', response_model=ToolModel | None)
@ -736,9 +722,7 @@ async def _update_tool(request, id, form_data, user, db, version_id=None):
)
############################
# UpdateToolAccessById
############################
# Update Tool Access By Id
class ToolAccessGrantsForm(BaseModel):
@ -797,9 +781,7 @@ async def update_tool_access_by_id(
return tools
############################
# DeleteToolsById
############################
# Delete Tools By Id
@router.delete('/id/{id}/delete', response_model=bool)
@ -849,9 +831,7 @@ async def delete_tools_by_id(
return result
############################
# GetToolValves
############################
# Get Tool Valves
@router.get('/id/{id}/valves', response_model=dict | None)
@ -891,9 +871,7 @@ async def get_tools_valves_by_id(
)
############################
# GetToolValvesSpec
############################
# Get Tool Valves Spec
@router.get('/id/{id}/valves/spec', response_model=dict | None)
@ -937,9 +915,7 @@ async def get_tools_valves_spec_by_id(
return None
############################
# UpdateToolValves
############################
# Update Tool Valves
@router.post('/id/{id}/valves/update', response_model=dict | None)
@ -1002,9 +978,7 @@ async def update_tools_valves_by_id(
)
############################
# ToolUserValves
############################
# Tool User Valves
@router.get('/id/{id}/valves/user', response_model=dict | None)

View file

@ -72,11 +72,7 @@ def merge_user_ui_settings(defaults: dict, settings: dict) -> dict:
return merged
############################
# GetUsers
# A house is only as strong as its care for the least of
# its members. Let none here be counted without being served.
############################
# Get Users
PAGE_ITEM_COUNT = 30
@ -164,9 +160,7 @@ async def search_users(
)
############################
# User Groups
############################
@router.get('/groups')
@ -176,9 +170,7 @@ async def get_user_groups(
return await user_groups_response(user.id, include_inherited, db)
############################
# User Permissions
############################
@router.get('/permissions')
@ -192,9 +184,7 @@ async def get_user_permissisions(
return user_permissions
############################
# User Default Permissions
############################
class WorkspacePermissions(BaseModel):
models: bool = False
knowledge: bool = False
@ -458,9 +448,7 @@ async def get_default_user_permissions_defaults(user=Depends(get_admin_user)):
}
############################
# GetUserSettingsBySessionUser
############################
# Get User Settings By Session User
@router.get('/user/settings', response_model=UserSettings | None)
@ -483,9 +471,7 @@ async def get_user_settings_by_session_user(
return UserSettings.model_validate(user_settings)
############################
# UpdateUserSettingsBySessionUser
############################
# Update User Settings By Session User
@router.post('/user/settings/update', response_model=UserSettings)
@ -561,9 +547,7 @@ async def update_user_settings_by_session_user(
)
############################
# GetUserStatusBySessionUser
############################
# Get User Status By Session User
@router.get('/user/status')
@ -580,9 +564,7 @@ async def get_user_status_by_session_user(
return user
############################
# UpdateUserStatusBySessionUser
############################
# Update User Status By Session User
@router.post('/user/status/update')
@ -613,9 +595,7 @@ async def update_user_status_by_session_user(
)
############################
# GetUserInfoBySessionUser
############################
# Get User Info By Session User
@router.get('/user/info', response_model=dict | None)
@ -632,9 +612,7 @@ class UserVariablesResponse(BaseModel):
variables: dict[str, str] = Field(default_factory=dict)
############################
# GetUserVariablesBySessionUser
############################
# Get User Variables By Session User
@router.get('/user/variables', response_model=UserVariablesResponse)
@ -642,9 +620,7 @@ async def get_user_variables_by_session_user(user=Depends(get_verified_user)):
return UserVariablesResponse(variables=normalize_user_variables(user.variables))
############################
# UpdateUserVariablesBySessionUser
############################
# Update User Variables By Session User
@router.post('/user/variables/update', response_model=UserVariablesResponse)
@ -670,9 +646,7 @@ async def update_user_variables_by_session_user(
return UserVariablesResponse(variables=variables)
############################
# UpdateUserInfoBySessionUser
############################
# Update User Info By Session User
@router.post('/user/info/update', response_model=dict | None)
@ -697,9 +671,7 @@ async def update_user_info_by_session_user( # PATCH-style merge
return updated.info
############################
# GetUserUsageBySessionUser
############################
# Get User Usage By Session User
@router.get('/usage', response_model=UserUsageResponse)
@ -776,9 +748,7 @@ async def get_user_usage_by_session_user(
)
############################
# GetUserById
############################
# Get User By Id
class UserActiveResponse(UserStatus):
@ -845,9 +815,7 @@ async def get_user_oauth_sessions_by_id(
)
############################
# GetUserProfileImageById
############################
# Get User Profile Image By Id
@router.get('/{user_id}/profile/image')
@ -892,9 +860,7 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u
)
############################
# GetUserActiveStatusById
############################
# Get User Active Status By Id
@router.get('/{user_id}/active', response_model=dict)
@ -906,9 +872,7 @@ async def get_user_active_status_by_id(
}
############################
# UpdateUserById
############################
# Update User By Id
@router.post('/{user_id}/sessions/revoke', response_model=bool)
@ -1114,9 +1078,7 @@ async def update_user_by_id(
)
############################
# DeleteUserById
############################
# Delete User By Id
@router.delete('/{user_id}', response_model=bool)
@ -1164,9 +1126,7 @@ async def delete_user_by_id(
)
############################
# GetUserGroupsById
############################
# Get User Groups By Id
@router.get('/{user_id}/groups')
@ -1179,9 +1139,7 @@ async def get_user_groups_by_id(
return await user_groups_response(user_id, include_inherited, db)
############################
# GetUserPreview
############################
# Get User Preview
@router.get('/{user_id}/preview')

View file

@ -76,7 +76,7 @@ log = logging.getLogger(__name__)
# cause, and let every message find the room it was meant for.
REDIS = None
# Configure CORS for Socket.IO
# Socket.IO expects a wildcard string rather than a one-item wildcard list.
SOCKETIO_CORS_ORIGINS = '*' if CORS_ALLOW_ORIGIN == ['*'] else CORS_ALLOW_ORIGIN
# Large notes outgrow the 1 MB default; match uvicorn's 16 MiB websocket limit

View file

@ -84,9 +84,7 @@ async def redis_task_heartbeat(app):
log.exception('Redis task heartbeat failed')
### ------------------------------
### REDIS-ENABLED HANDLERS
### ------------------------------
# Redis-enabled handlers
async def redis_save_task(redis: Redis, task_id: str, item_id: str | None):
@ -166,11 +164,11 @@ async def create_task(redis, coroutine, id=None, task_id=None):
task_id = task_id or str(uuid4()) # Generate a unique ID for the task
task = asyncio.create_task(coroutine) # Create the task
# Add a done callback for cleanup
# Give the task a checkout routine before handing it the keys.
task.add_done_callback(lambda t: asyncio.create_task(cleanup_task(redis, task_id, id)))
tasks[task_id] = task
# If an ID is provided, associate the task with that ID
# File this task under its item ID, if it came with a return address.
if id:
if item_tasks.get(id):
item_tasks[id].append(task_id)

View file

@ -124,9 +124,7 @@ async def _has_read_access_to_file(
)
# =============================================================================
# TIME UTILITIES
# =============================================================================
# Time utilities
async def notify(
@ -226,7 +224,7 @@ async def calculate_timestamp(
total_days = days_ago + (weeks_ago * 7)
adjusted = now - datetime.timedelta(days=total_days)
# Handle months and years separately (variable length)
# Months and years need calendar arithmetic; February refuses to be 30 days.
if months_ago > 0 or years_ago > 0:
adjusted = adjusted - relativedelta(months=months_ago, years=years_ago)
@ -287,9 +285,7 @@ async def calculate_timestamp(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# WEB SEARCH TOOLS
# =============================================================================
# Web search tools
async def search_web(
@ -363,9 +359,7 @@ async def fetch_url(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# IMAGE GENERATION TOOLS
# =============================================================================
# Image generation tools
async def generate_image(
@ -535,9 +529,7 @@ async def edit_image(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# USER INPUT TOOLS
# =============================================================================
# User input tools
async def ask_user(
@ -652,9 +644,7 @@ async def ask_user(
return JSONCodec.dumps({'status': 'error', 'error': str(e)}, ensure_ascii=False)
# =============================================================================
# CODE INTERPRETER TOOLS
# =============================================================================
# Code interpreter tools
async def execute_code(
@ -819,9 +809,7 @@ async def execute_code(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# MEMORY TOOLS
# =============================================================================
# Memory tools
async def list_memory_paths(
@ -1146,9 +1134,7 @@ async def list_memories(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# NOTES TOOLS
# =============================================================================
# Notes tools
async def search_notes(
@ -1634,9 +1620,7 @@ async def replace_note_content(
return JSONCodec.dumps({'error': str(e), 'code': 'unexpected_error'})
# =============================================================================
# CHATS TOOLS
# =============================================================================
# Chats tools
async def search_chats(
@ -1750,7 +1734,7 @@ async def view_chat(
history = chat.chat.get('history', {})
msg_dict = history.get('messages', {})
# Build message chain from currentId
# Follow parent links from currentId; conversations have family trees too.
current_id = history.get('currentId')
visited = set()
@ -1784,9 +1768,7 @@ async def view_chat(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# SUB-AGENT TOOL
# =============================================================================
# Sub-agent tool
async def delegate_task(
@ -1867,9 +1849,7 @@ async def timer(
)
# =============================================================================
# CHANNELS TOOLS
# =============================================================================
# Channels tools
async def search_channels(
@ -2109,7 +2089,7 @@ async def view_channel_thread(
# Build the response
messages = []
# Add parent message first
# Parent message first; the replies need something to reply to.
messages.append(
{
'id': parent_message.id,
@ -2121,7 +2101,7 @@ async def view_channel_thread(
}
)
# Add thread replies (reverse to get chronological order)
# Reverse the replies into chronological order; spoilers go at the end.
for reply in reversed(thread_replies):
messages.append(
{
@ -2150,9 +2130,7 @@ async def view_channel_thread(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# KNOWLEDGE BASE TOOLS
# =============================================================================
# Knowledge base tools
async def list_knowledge_bases(
@ -3664,9 +3642,7 @@ async def query_knowledge_bases(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# SKILLS TOOLS
# =============================================================================
# Skills tools
async def view_skill(
@ -4040,9 +4016,7 @@ async def update_skill_files(
return JSONCodec.dumps({'error': detail})
# =============================================================================
# TOOL SEARCH
# =============================================================================
# Tool search
async def search_tools(
@ -4072,9 +4046,7 @@ async def search_tools(
return JSONCodec.dumps({'tools': [candidates[name] for name in matches]})
# =============================================================================
# TASK MANAGEMENT TOOLS
# =============================================================================
# Task management tools
from typing import Literal
@ -4217,9 +4189,7 @@ async def update_task(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# AUTOMATION TOOLS
# =============================================================================
# Automation tools
async def _validate_owned_automation_folder(user_id: str, folder_id: Optional[str]) -> Optional[str]:
@ -4612,9 +4582,7 @@ async def delete_automation(
return JSONCodec.dumps({'error': str(e)})
# =============================================================================
# CALENDAR TOOLS
# =============================================================================
# Calendar tools
MAX_CALENDAR_RANGE_END_NS = 2**63 - 1

View file

@ -62,9 +62,7 @@ def match_budget():
_active_budget.reset(token)
# =============================================================================
# SHARED REGEX UTILITIES — also used by builtin.py grep_knowledge_files
# =============================================================================
# Shared regex utilities — also used by builtin.py grep_knowledge_files
def is_regex_pattern(pattern: str) -> bool:
@ -173,9 +171,7 @@ def build_matcher(pattern: str, case_insensitive: bool = False, use_regex: bool
return (lambda line: sp in (line.lower() if case_insensitive else line)), None
# =============================================================================
# COMMAND PARSING
# =============================================================================
# Command parsing
def _parse_pipeline(command: str) -> list[list[str]]:
@ -248,9 +244,7 @@ def _extract_numeric_flag(tokens: list[str]) -> tuple[Optional[int], list[str]]:
return num, remaining
# =============================================================================
# DIRECTORY TREE & PATH RESOLUTION
# =============================================================================
# Directory tree & path resolution
async def _build_directory_tree(knowledge_id: str) -> dict:
@ -344,9 +338,7 @@ def _get_files_under_dir(tree: dict, dir_id: str) -> list[dict]:
return [f for f in tree['files'] if f['directory_id'] in target_ids]
# =============================================================================
# FILE RESOLUTION & ACCESS CONTROL
# =============================================================================
# File resolution & access control
async def _get_accessible_kb_ids(
@ -573,9 +565,7 @@ async def _get_file_content(file_id: str) -> str | None:
return None
# =============================================================================
# COMMAND HANDLERS
# =============================================================================
# Command handlers
async def _kb_ls(args: list[str], flags: set[str], user: dict, model_knowledge: list[dict] | None) -> str:
@ -1063,9 +1053,7 @@ async def _kb_sed(
return result
# =============================================================================
# PIPE EXECUTOR
# =============================================================================
# Pipe executor
async def _kb_tree(args: list[str], flags: set[str], user: dict, model_knowledge: list[dict] | None) -> str:
@ -1177,9 +1165,7 @@ async def _execute_pipeline(
return piped_input or ''
# =============================================================================
# ENTRY POINT
# =============================================================================
# Entry point
async def kb_exec(

View file

@ -101,11 +101,7 @@ async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> di
return {'object': 'list', 'data': all_models}
##############################
#
# Anthropic Messages API Conversion Utilities
#
##############################
def _copy_cache_control(source: dict, target: dict) -> dict:
@ -752,7 +748,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
}
yield f'event: content_block_delta\ndata: {JSONCodec.dumps(block_delta)}\n\n'.encode()
# --- Handle text content ---
# Handle text content
# Anthropic expects text blocks before tool blocks, so skip
# text deltas once any tool call has started.
content = delta.get('content')
@ -782,7 +778,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
}
yield f'event: content_block_delta\ndata: {JSONCodec.dumps(block_delta)}\n\n'.encode()
# --- Handle tool calls ---
# Handle tool calls
# Some providers put tool_calls on the final message object
# instead of the delta; fall back to that when needed.
tool_calls = delta.get('tool_calls') or []
@ -903,7 +899,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
except (JSONCodec.JSONDecodeError, ValueError):
pass
# --- Handle finish reason ---
# Handle finish reason
if finish_reason is not None:
stop_reason_map = {
'stop': 'end_turn',

View file

@ -48,9 +48,7 @@ SESSION_SECRET = WEBUI_SECRET_KEY
ALGORITHM = 'HS256'
PASSWORD_BCRYPT_MAX_BYTES = 72
##############
# Auth Utils
##############
def verify_signature(payload: str, signature: str) -> bool:
@ -222,9 +220,8 @@ async def verify_password(plain_password: str, hashed_password: str) -> bool:
return False
# 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.
# Let this token open only the doors it should,
# and have the good manners to expire before overstaying its welcome.
def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:
payload = data.copy()
@ -391,10 +388,7 @@ async def get_current_user(
response: Response,
background_tasks: BackgroundTasks,
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
# NOTE: We intentionally do NOT use Depends(get_session) here.
# Sessions are managed internally with short-lived context managers.
# This ensures connections are released immediately after auth queries,
# not held for the entire request duration (e.g., during 30+ second LLM calls).
# Use short-lived auth sessions so streaming requests do not retain DB connections.
):
token = None

View file

@ -56,9 +56,7 @@ TIMER_POLL_INTERVAL = int(os.getenv('TIMER_POLL_INTERVAL', '1'))
CALENDAR_ALERT_LOOKAHEAD_MINUTES = int(os.getenv('CALENDAR_ALERT_LOOKAHEAD_MINUTES', '10'))
############################
# Worker Loop
############################
# Keep the old name as an alias so any stale imports still work.
@ -87,7 +85,7 @@ async def scheduler_worker_loop(app) -> None:
while True:
try:
now = time.monotonic()
# ── Timers ──
# Timers
try:
from open_webui.utils.timers import claim_due_timers, execute_due_timer
@ -102,7 +100,7 @@ async def scheduler_worker_loop(app) -> None:
# Jitter to spread automation/calendar load across instances; timers keep a tight poll.
next_scheduler_poll = now + SCHEDULER_POLL_INTERVAL + random.uniform(0, 2)
# ── Automations ──
# Automations
if await Config.get('automations.enable'):
try:
async with get_async_db() as db:
@ -114,7 +112,7 @@ async def scheduler_worker_loop(app) -> None:
except Exception:
log.exception('Scheduler: automation error')
# ── Calendar Alerts ──
# Calendar alerts
if await Config.get('calendar.enable'):
try:
await _check_calendar_alerts(app)
@ -127,9 +125,7 @@ async def scheduler_worker_loop(app) -> None:
await asyncio.sleep(max(1, TIMER_POLL_INTERVAL))
##########################
# Execute
####################
def _build_request(
@ -540,9 +536,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
raise
####################
# Internals
####################
async def _check_calendar_alerts(app) -> None:

View file

@ -207,8 +207,8 @@ async def process_filter_function(
return form_data, valves_by_id, skip_files
# Grant these filters the discernment to pass what serves
# and refuse what harms, for every soul in the house.
# Let these filters pass what helps and catch what harms,
# and spare us the bug report that explains the difference.
async def process_filter_functions(
request,
filter_context,

View file

@ -274,10 +274,8 @@ async def publish_chat_failed_event(request: Request, user: UserModel, metadata:
)
# 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.
# Let every wandering thought find its closing tag,
# and let the answer arrive before we forget the question.
DEFAULT_REASONING_TAGS = [
('<think>', '</think>'),
('<thinking>', '</thinking>'),
@ -2435,12 +2433,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
form_data['messages'],
)
else:
# Native FC: tool docstring can't be dynamic, so inject
# filesystem context into the system message for pyodide
# engine. Appending to the system prompt (instead of the
# user message) keeps it in the stable cached prefix so
# providers with prefix caching don't re-bill the full
# conversation on every turn.
# Put dynamic Pyodide filesystem context in the system prompt to preserve prefix caching.
if engine != 'jupyter':
form_data['messages'] = add_or_update_system_message(
CODE_INTERPRETER_PYODIDE_PROMPT,
@ -2631,10 +2624,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
prompt = get_last_user_message(form_data['messages'])
# Guard against empty user message after skill mention stripping.
# When a user selects a skill ($skill-name) without typing additional text,
# the stripped result is an empty string which causes 400 errors on providers
# that reject empty content blocks (e.g. AWS Bedrock ConverseStream).
# Supply fallback text for skill-only messages; some providers reject empty content.
if not prompt or not prompt.strip():
fallback = ', '.join([s.name for s in available_skills] + [s['name'] for s in terminal_skills])
# Attachment-only messages keep their empty text, same as on models without skills.
@ -4902,11 +4892,7 @@ async def streaming_chat_response_handler(response, ctx):
}
)
# Merge any metadata (usage, etc.)
# Strip 'done' — response.completed emits
# it but we may still need to execute tool
# calls. The outer middleware manages the
# actual completion signal.
# Merge metadata without 'done'; the outer middleware signals completion after tool calls.
if response_metadata:
if ENABLE_RESPONSES_API_STATEFUL:
response_id = response_metadata.pop('response_id', None)
@ -5352,13 +5338,7 @@ async def streaming_chat_response_handler(response, ctx):
# closure-cell str += recopies per chunk; append + join once at read is O(n)
content_parts.append(value)
# Check if we're inside a tag-based block
# (reasoning, code_interpreter, or solution).
# If so, append to the existing in-progress
# item instead of creating a new message —
# otherwise tag_output_handler re-detects the
# start tag on every chunk and fragments the
# output.
# Continue the open tag block so its start tag is not re-detected on each chunk.
last_item = output[-1] if output else None
last_item_type = last_item.get('type', '') if last_item else ''
inside_tag_block = (
@ -6098,12 +6078,7 @@ async def streaming_chat_response_handler(response, ctx):
)
if isinstance(res, StreamingResponse):
# Save accumulated output and start fresh.
# Responses API output_index values are relative
# to the current response — a clean output list
# keeps indices aligned. The display prefix
# ensures the UI shows tool history during
# streaming.
# Reset response-relative output indices; keep prior output as the UI history prefix.
prior_output = list(full_output())
# Trim the trailing empty placeholder message
# so it doesn't persist as a ghost item once
@ -6392,10 +6367,7 @@ async def streaming_chat_response_handler(response, ctx):
except asyncio.CancelledError:
log.warning('Task was cancelled!')
# Close the response body iterator to trigger cleanup
# in stream_wrapper's finally block and release the
# upstream connection. Without this, the async
# generator is orphaned and may spin in anyio internals.
# Close the iterator so stream_wrapper's finally block releases the upstream connection.
if hasattr(response, 'body_iterator') and hasattr(response.body_iterator, 'aclose'):
try:
await asyncio.shield(response.body_iterator.aclose())

View file

@ -192,7 +192,7 @@ def get_message_list(messages_map, message_id):
:return: List of ordered messages starting from the root to the given message
"""
# Handle case where messages is None
# No history, no archaeology. An empty map gets an empty list.
if not messages_map:
return [] # Return empty list instead of None to prevent iteration errors
@ -202,7 +202,7 @@ def get_message_list(messages_map, message_id):
if not current_message:
return [] # Return empty list instead of None to prevent iteration errors
# Reconstruct the chain by following the parentId links
# Follow parentId back through the conversation family tree.
message_list = []
visited_message_ids = set()
@ -840,25 +840,21 @@ def openai_chat_completion_message_template(
def get_gravatar_url(email):
# Trim leading and trailing whitespace from
# an email address and force all characters
# to lower case
# Trim and lowercase the email; the avatar should survive a stuck Shift key.
address = str(email).strip().lower()
# Create a SHA256 hash of the final string
# Hash the normalized email into its Gravatar calling card.
hash_object = hashlib.sha256(address.encode())
hash_hex = hash_object.hexdigest()
# Grab the actual image URL
# Send the hash to Gravatar; the email stays out of the URL.
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.
# Let what has been entrusted to us be kept whole,
# and let nothing be lost in the passing from hand to hand.
def calculate_sha256(file_path, chunk_size):
# Compute SHA-256 hash of a file efficiently in chunks
# Read in chunks so large files do not need to fit in memory.
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
while chunk := f.read(chunk_size):
@ -869,9 +865,9 @@ def calculate_sha256(file_path, chunk_size):
def calculate_sha256_string(string):
# Create a new SHA-256 hash object
sha256_hash = hashlib.sha256()
# Update the hash object with the bytes of the input string
# Feed the hash UTF-8 bytes; it does not take strings on faith.
sha256_hash.update(string.encode('utf-8'))
# Get the hexadecimal representation of the hash
# Return the hex fingerprint, now suitable for polite string comparison.
hashed_string = sha256_hash.hexdigest()
return hashed_string
@ -884,13 +880,13 @@ def validate_email_format(email: str) -> bool:
def sanitize_filename(file_name):
# Convert to lowercase
# Lowercase the filename; shouting does not help us find it.
lower_case_file_name = file_name.lower()
# Remove special characters using regular expression
# Strip punctuation; word characters and whitespace make the guest list.
sanitized_file_name = re.sub(r'[^\w\s]', '', lower_case_file_name)
# Replace spaces with dashes
# Give each whitespace run one dash, regardless of its enthusiasm.
final_file_name = re.sub(r'\s+', '-', sanitized_file_name)
return final_file_name
@ -1009,7 +1005,7 @@ def extract_folders_after_data_docs(path):
except ValueError:
return []
# Exclude the filename and accumulate folder names
# Collect the folder ancestry; the filename is not invited.
tags = []
folders = parts[index_docs:-1]
@ -1245,17 +1241,15 @@ 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.
# Let what falters have another chance,
# and let the third try be kinder than the first two.
async def cleanup_response(
response: aiohttp.ClientResponse | None,
session: aiohttp.ClientSession | None,
):
if response:
if not response.closed:
# aiohttp 3.9+ made ClientResponse.close() synchronous (returns None).
# Older versions returned a coroutine. Handle both gracefully.
# Close directly, and await completion if the implementation returns an awaitable.
result = response.close()
if result is not None:
await result

View file

@ -353,13 +353,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
functions_by_id = {f.id: f for f in await Functions.get_functions_by_ids(list(all_function_ids))}
# Pre-warm the function module cache once per unique function ID.
# This ensures each function's DB freshness check runs exactly once,
# not once per (model × function) pair.
# Only attempt to load functions that actually exist in the local DB;
# imported/custom model configs may reference tools or filters the user
# hasn't installed, and trying to load those would cause persistent
# "Failed to load function module" log spam on every model refresh.
# Warm each installed function once, avoiding per-model freshness checks and errors for missing functions.
for function_id, function in functions_by_id.items():
try:
await get_function_module_from_cache(request, function_id, function=function)

View file

@ -65,7 +65,7 @@ async def resolve_system_prompt(
# What goes out cannot be taken back. Let it be shaped
# well before it leaves this place.
# inplace function: form_data is modified
# Mutates form_data in place.
async def apply_system_prompt_to_body(
system: Optional[str],
form_data: dict,
@ -242,7 +242,7 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
for key, value in name_differences.items():
if (param := params.get(key, None)) is not None:
# Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided
# Move this parameter to the payload root so it is not also sent as a generation option.
params[value] = params[key]
del params[key]
@ -292,7 +292,7 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
form_data[key] = value(param)
del params[key]
# Unlike OpenAI, Ollama does not support params directly in the body
# Map the remaining generation parameters into options; root-level parameters were handled above.
form_data['options'] = apply_model_params_to_body(params, (form_data.get('options', {}) or {}), mappings)
return form_data
@ -301,7 +301,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
ollama_messages = []
for message in messages:
# Initialize the new message structure with the role
# New envelope, same speaker: carry the role across first.
new_message = {'role': message['role']}
# Preserve Ollama-native 'thinking' field (used by reasoning models,
@ -315,12 +315,12 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
tool_calls = message.get('tool_calls', None)
tool_call_id = message.get('tool_call_id', None)
# Check if the content is a string (just a simple message)
# Plain text without tool calls gets the express lane.
if isinstance(content, str) and not tool_calls:
# If the content is a string, it's pure text
new_message['content'] = content
# If message is a tool call, add the tool call id to the message
# Preserve the tool response's link to the call it answers.
if tool_call_id:
new_message['tool_call_id'] = tool_call_id
@ -339,7 +339,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
ollama_tool_calls.append(ollama_tool_call)
new_message['tool_calls'] = ollama_tool_calls
# Put the content to empty string (Ollama requires an empty string for tool calls)
# Tool calls get an empty content string; Ollama insists on the blank name tag.
new_message['content'] = ''
else:
@ -357,7 +357,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
elif item.get('type') == 'image_url':
img_url = item.get('image_url', {}).get('url', '')
if img_url:
# If the image url starts with data:, it's a base64 image and should be trimmed
# Peel the data-URL label off the base64 image before packing it for Ollama.
if img_url.startswith('data:'):
img_url = img_url.split(',')[-1]
images.append(img_url)
@ -370,7 +370,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
if images:
new_message['images'] = images
# Append the new formatted message to the result
# Add the repacked message to the outgoing mail.
ollama_messages.append(new_message)
return ollama_messages
@ -425,7 +425,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
# Ollama's options field can contain parameters that should be at the root level.
for key, value in ollama_root_params.items():
if (param := ollama_options.get(key, None)) is not None:
# Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided
# Move this parameter to the payload root so it is not also sent as a generation option.
ollama_payload[key] = value(param)
del ollama_options[key]
@ -434,8 +434,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
ollama_options['num_predict'] = ollama_options['max_tokens']
del ollama_options['max_tokens']
# Ollama lacks a "system" prompt option. It has to be provided as a direct parameter, so we copy it down.
# Comment: Not sure why this is needed, but we'll keep it for compatibility.
# Preserve legacy options.system input by moving it to the payload root.
if 'system' in ollama_options:
ollama_payload['system'] = ollama_options['system']
del ollama_options['system']

View file

@ -203,8 +203,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.
# Let the care put into this work survive every
# import and transformation, and serve those who carry it forward.
async def load_tool_module_by_id(tool_id, content=None):
"""Prepare a Tool instance, frontmatter, and Python module without changing the live cache."""
if not ENABLE_TOOLS:
@ -229,8 +229,7 @@ async def load_tool_module_by_id(tool_id, content=None):
module = types.ModuleType(module_name)
sys.modules[module_name] = module
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
# Give the module a real __file__; even dynamically loaded code wants an address.
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:
@ -244,7 +243,7 @@ async def load_tool_module_by_id(tool_id, content=None):
frontmatter = extract_frontmatter(content)
log.info('Loaded module: %s', module.__name__)
# Create and return the object if the class 'Tools' is found in the module
# Look for the Tools class; the toolkit still needs someone to answer the door.
if hasattr(module, 'Tools'):
return module.Tools(), frontmatter, module
else:
@ -278,8 +277,7 @@ async def load_function_module_by_id(function_id: str, content: str | None = Non
module = types.ModuleType(module_name)
sys.modules[module_name] = module
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
# Give the module a real __file__; even dynamically loaded code wants an address.
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:

View file

@ -113,9 +113,7 @@ def get_redis_client(async_mode: bool = False) -> Any | None:
return None
# ---------------------------------------------------------------------------
# Sentinel proxy with automatic failover retry
# ---------------------------------------------------------------------------
class SentinelRedisProxy:
@ -178,7 +176,7 @@ class SentinelRedisProxy:
exc,
)
# -- async wrappers -----------------------------------------------------
# Async wrappers
def _wrap_async(self, name: str, attr: Any) -> Any:
if inspect.isasyncgenfunction(attr):
@ -233,7 +231,7 @@ class SentinelRedisProxy:
return wrapper
# -- sync wrapper -------------------------------------------------------
# Sync wrapper
def _wrap_sync(self, name: str) -> Any:
proxy = self
@ -256,9 +254,7 @@ class SentinelRedisProxy:
return wrapper
# ---------------------------------------------------------------------------
# Connection factory
# ---------------------------------------------------------------------------
def _socket_options() -> dict[str, Any]:

View file

@ -15,9 +15,8 @@ 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
# Check if the user has a custom task model and use that model
# Use an available task model override for the local or external connection.
if models.get(task_model_id, {}).get('connection_type') == 'local':
if task_model and task_model in models:
task_model_id = task_model
@ -85,7 +84,7 @@ async def prompt_template(template: str, user: Optional[Any] = None) -> str:
# Get the current date
current_date = datetime.now()
# Format the date to YYYY-MM-DD
# YYYY-MM-DD: one date format, fewer international incidents.
formatted_date = current_date.strftime('%Y-%m-%d')
formatted_time = current_date.strftime('%I:%M:%S %p')
formatted_weekday = current_date.strftime('%A')

View file

@ -63,10 +63,7 @@ def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args,
Redis Request Hook
"""
# In cluster mode, the instance can be of two types:
# - redis.asyncio.cluster.RedisCluster
# - redis.cluster.RedisCluster
# Instead of checking the type, we check if the instance has a nodes_manager attribute.
# Detect both sync and async RedisCluster clients by their nodes_manager attribute.
try:
db = ''
if hasattr(instance, 'nodes_manager'):

View file

@ -55,17 +55,7 @@ from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Sync DB helpers for OTel gauge callbacks
#
# The OTel Python SDK calls observable-instrument callbacks *synchronously*
# from a background collection thread — async callbacks are NOT supported
# (the SDK does not ``await`` the return value).
#
# Rather than bridging into the async event loop, we run plain synchronous
# SQL queries using the sync engine that is already available at setup time.
# This avoids any cross-thread / cross-loop concerns entirely.
# ---------------------------------------------------------------------------
# OTel gauge callbacks run synchronously on a collection thread, so use the sync DB engine.
def _count_total_users(db_engine: Engine) -> Optional[int]:
@ -168,10 +158,7 @@ def setup_metrics(app: FastAPI, resource: Resource, db_engine: Engine) -> None:
unit='ms',
)
# -- Observable gauge callbacks ----------------------------------------
# These are called synchronously by the OTel SDK from a background
# collection thread. They use the sync DB engine directly — no async
# bridging required.
# Gauge callbacks use the sync DB engine on OTel's collection thread.
def observe_total_users(
options: metrics.CallbackOptions,

View file

@ -61,12 +61,12 @@ def validate_image_url(url: str, *, file_only: bool = False) -> str:
return url
raise ValueError('Invalid background image URL: must reference an internal file.')
# --- Relative paths (exact match + anchored regex only) -----------
# Relative paths (exact match + anchored regex only)
if not url or url in _SAFE_STATIC_PATHS or _USER_PROFILE_IMAGE_RE.match(url):
return url
# --- Absolute URLs -------------------------------------------------
# Absolute URLs
# urlparse normalises the scheme to lowercase, giving us
# case-insensitive scheme matching for free.

View file

@ -40,14 +40,14 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict, desc
try:
payload = {}
# Slack and Google Chat Webhooks
# Slack and Google Chat both order their message under "text".
if 'https://hooks.slack.com' in url or 'https://chat.googleapis.com' in url:
payload['text'] = _event_text(message, description, event_data)
# Discord Webhooks
# Discord calls it "content" and keeps the speech under 2,000 characters.
elif 'https://discord.com/api/webhooks' in url:
content = _event_text(message, description, event_data)
payload['content'] = content if len(content) < 2000 else f'{content[: 2000 - 20]}... (truncated)'
# Microsoft Teams Webhooks
# Teams prefers a MessageCard; apparently the message needs stationery.
elif 'webhook.office.com' in url:
action = event_data.get('action', 'undefined')
user_data = event_data.get('user') or event_data.get('actor') or {}
@ -79,7 +79,7 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict, desc
}
],
}
# Default Payload
# Everyone else gets the event data without gift wrapping.
else:
payload = event_data

View file

@ -1,11 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Container entry point for Open WebUI.
# Handles secret key generation, optional Ollama/CUDA/Playwright setup,
# HuggingFace Space deployment, and launches the uvicorn server.
# ---------------------------------------------------------------------------
# Default optional env vars that we test below with bash's `,,` lowercase
# expansion. The two can't be combined inline (`${VAR:-default,,}` makes
@ -16,7 +14,7 @@ set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
cd "$SCRIPT_DIR" || exit 1
# ── Playwright browser installation (if configured) ──────────────────────────
# Playwright browser installation (if configured)
if [[ "${USE_SLIM_DOCKER,,}" != "true" && "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then
if [[ -z "${PLAYWRIGHT_WS_URL:-}" ]]; then
@ -26,7 +24,7 @@ if [[ "${USE_SLIM_DOCKER,,}" != "true" && "${WEB_LOADER_ENGINE,,}" == "playwrigh
fi
fi
# ── Secret key setup ─────────────────────────────────────────────────────────
# Secret key setup
KEY_FILE="${WEBUI_SECRET_KEY_FILE:-.webui_secret_key}"
WEBUI_SECRET_KEY_LENGTH="${WEBUI_SECRET_KEY_LENGTH:-24}"
@ -49,21 +47,21 @@ if [[ -z "${WEBUI_SECRET_KEY:-}" && -z "${WEBUI_JWT_SECRET_KEY:-}" ]]; then
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
fi
# ── Ollama (bundled Docker image) ────────────────────────────────────────────
# Ollama (bundled docker image)
if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then
echo "Starting bundled ollama serve..."
ollama serve &
fi
# ── CUDA library paths ──────────────────────────────────────────────────────
# Cuda library paths
if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
echo "CUDA enabled — extending LD_LIBRARY_PATH for torch/cudnn libraries."
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:/usr/local/lib/python3.12/site-packages/torch/lib:/usr/local/lib/python3.12/site-packages/nvidia/cudnn/lib"
fi
# ── HuggingFace Space deployment ─────────────────────────────────────────────
# HuggingFace space deployment
if [[ -n "${SPACE_ID:-}" ]]; then
echo "Configuring for HuggingFace Space deployment..."
@ -93,7 +91,7 @@ if [[ -n "${SPACE_ID:-}" ]]; then
export WEBUI_URL="${SPACE_HOST}"
fi
# ── Launch uvicorn ───────────────────────────────────────────────────────────
# Launch uvicorn
PYTHON_CMD=$(command -v python3 || command -v python)
UVICORN_WORKERS="${UVICORN_WORKERS:-1}"

View file

@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Tear down the compose project and remove all volumes (including data).
# ---------------------------------------------------------------------------
echo "WARNING: This will stop all containers and delete all volumes (including persistent data)."
read -rp "Are you sure you want to continue? [y/N]: " answer

View file

@ -1,10 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Interactive docker compose launcher for Open WebUI.
# Supports GPU auto-detection, configurable ports, data mounts, and Playwright.
# ---------------------------------------------------------------------------
readonly BOLD='\033[1m'
readonly GREEN='\033[1;32m'
@ -13,7 +11,7 @@ readonly RED='\033[0;31m'
readonly RESET='\033[0m'
readonly CHECK_MARK='\xE2\x9C\x93'
# ── GPU detection ─────────────────────────────────────────────────────────────
# Gpu detection
detect_gpu_driver() {
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then
@ -32,7 +30,7 @@ detect_gpu_driver() {
fi
}
# ── Argument helpers ──────────────────────────────────────────────────────────
# Argument helpers
extract_bracket_value() {
local input="$1" fallback="${2:-}"
@ -66,7 +64,7 @@ Examples:
EOF
}
# ── Defaults ──────────────────────────────────────────────────────────────────
# Defaults
enable_gpu=false
enable_api=false
@ -79,7 +77,7 @@ api_port=11435
webui_port=3000
data_dir=""
# ── Parse arguments ───────────────────────────────────────────────────────────
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
@ -97,7 +95,7 @@ while [[ $# -gt 0 ]]; do
shift
done
# ── Drop mode ─────────────────────────────────────────────────────────────────
# Drop mode
if [[ "$drop_project" == true ]]; then
docker compose down --remove-orphans
@ -105,7 +103,7 @@ if [[ "$drop_project" == true ]]; then
exit 0
fi
# ── Build compose command ─────────────────────────────────────────────────────
# Build compose command
compose_files=("-f" "docker-compose.yaml")
@ -141,7 +139,7 @@ if [[ "$build_image" == true ]]; then
up_args+=("--build")
fi
# ── Confirmation ──────────────────────────────────────────────────────────────
# Confirmation
echo
echo -e "${WHITE}${BOLD}Current Setup:${RESET}"
@ -161,7 +159,7 @@ if [[ "$headless" != true ]]; then
fi
fi
# ── Launch ────────────────────────────────────────────────────────────────────
# Launch
if docker compose "${compose_files[@]}" "${up_args[@]}"; then
echo

View file

@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Pull and run the official Ollama container with optional GPU support.
# ---------------------------------------------------------------------------
readonly CONTAINER="ollama"
readonly HOST_PORT="${OLLAMA_PORT:-11434}"

View file

@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Build and run the Open WebUI Docker container locally.
# ---------------------------------------------------------------------------
readonly IMAGE="open-webui"
readonly CONTAINER="open-webui"

View file

@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Pull the latest version of every model installed in the Ollama container.
# ---------------------------------------------------------------------------
readonly CONTAINER="${OLLAMA_CONTAINER:-ollama}"

View file

@ -60,14 +60,14 @@ generate() {
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' RETURN
# --- Python: resolve all transitive deps without installing ---
# Python: resolve all transitive deps without installing
dim "Resolving Python transitive deps (uv pip compile)..."
uv pip compile "$ROOT_DIR/backend/requirements.txt" \
--python-version 3.12 \
--quiet \
> "$WORK_DIR/requirements-resolved.txt" 2>/dev/null
# --- JavaScript: package-lock.json is already fully resolved ---
# JavaScript: package-lock.JSON is already fully resolved
if [[ -f "$ROOT_DIR/package-lock.json" ]]; then
cp "$ROOT_DIR/package-lock.json" "$WORK_DIR/package-lock.json"
# Syft needs package.json alongside the lockfile
@ -76,7 +76,7 @@ generate() {
warn "package-lock.json not found — JS deps will be skipped"
fi
# --- Scan only the resolved files ---
# Scan only the resolved files
dim "Scanning resolved manifests with Syft..."
syft scan "dir:$WORK_DIR" \
--output "cyclonedx-json=$OUTPUT" \
@ -175,7 +175,7 @@ else:
"
}
# --- Main ---
# Main
cd "$ROOT_DIR"
TARGET="${1:-generate}"

View file

@ -69,7 +69,7 @@ export type CalendarForm = {
access_grants?: { target_type: string; target_id: string; permission: string }[];
};
// ── Calendars ─────────────────────────────────
// Calendars
export const getCalendars = async (token: string): Promise<CalendarModel[]> => {
let error = null;
@ -220,7 +220,7 @@ export const setDefaultCalendar = async (
return res;
};
// ── Events ─────────────────────────────────
// Events
export const getCalendarEvents = async (
token: string,

View file

@ -20,8 +20,8 @@ const OPENAPI_HTTP_METHODS = new Set([
'trace'
]);
// Every request sent from here is a petition. May it reach
// the one for whom it was intended, and return answered.
// Let every request reach its destination,
// and let those who wait receive an answer worthy of their patience.
export const getModels = async (
token: string = '',
connections: object | null = null,

View file

@ -1038,7 +1038,7 @@ export const exportKnowledgeById = async (token: string, id: string) => {
return res;
};
// ── Directory API ───────────────────────────────────────────────────
// Directory API
export const createKnowledgeDirectory = async (
token: string,

View file

@ -511,16 +511,3 @@ export const uploadModel = async (token: string, file: File, urlIdx: string | nu
}
return res;
};
// export const pullModel = async (token: string, tagName: string) => {
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'text/event-stream',
// Authorization: `Bearer ${token}`
// },
// body: JSON.stringify({
// name: tagName
// })
// });
// };

View file

@ -461,9 +461,7 @@ export const updatePromptAccessGrants = async (
return res;
};
////////////////////////////
// Prompt History APIs
////////////////////////////
export const getPromptHistory = async (
token: string,

View file

@ -722,9 +722,7 @@ export const getPortProxyUrl = (baseUrl: string, port: number, path: string = ''
return `${baseUrl.replace(/\/$/, '')}/proxy/${port}/${path}`;
};
// ---------------------------------------------------------------------------
// Notebook execution
// ---------------------------------------------------------------------------
export const createNotebookSession = async (
baseUrl: string,

Some files were not shown because too many files have changed in this diff Show more