mirror of
https://github.com/open-webui/open-webui.git
synced 2026-04-26 01:25:34 +02:00
1053 lines
39 KiB
Python
1053 lines
39 KiB
Python
import importlib.metadata
|
|
import json
|
|
import logging
|
|
import os
|
|
import pkgutil
|
|
import sys
|
|
import shutil
|
|
import traceback
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
from pathlib import Path
|
|
from cryptography.hazmat.primitives import serialization
|
|
import re
|
|
|
|
|
|
import markdown
|
|
from bs4 import BeautifulSoup
|
|
from open_webui.constants import ERROR_MESSAGES
|
|
|
|
####################################
|
|
# Load .env file
|
|
####################################
|
|
|
|
# Use .resolve() to get the canonical path, removing any '..' or '.' components
|
|
ENV_FILE_PATH = Path(__file__).resolve()
|
|
|
|
# OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/)
|
|
OPEN_WEBUI_DIR = ENV_FILE_PATH.parent
|
|
|
|
# BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/)
|
|
BACKEND_DIR = OPEN_WEBUI_DIR.parent
|
|
|
|
# BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/)
|
|
BASE_DIR = BACKEND_DIR.parent
|
|
|
|
try:
|
|
from dotenv import find_dotenv, load_dotenv
|
|
|
|
load_dotenv(find_dotenv(str(BASE_DIR / '.env')))
|
|
except ImportError:
|
|
print('dotenv not installed, skipping...')
|
|
|
|
DOCKER = os.environ.get('DOCKER', 'False').lower() == 'true'
|
|
|
|
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
|
USE_CUDA = os.environ.get('USE_CUDA_DOCKER', 'false')
|
|
|
|
if USE_CUDA.lower() == 'true':
|
|
try:
|
|
import torch
|
|
|
|
assert torch.cuda.is_available(), 'CUDA not available'
|
|
DEVICE_TYPE = 'cuda'
|
|
except Exception as e:
|
|
cuda_error = f'Error when testing CUDA but USE_CUDA_DOCKER is true. Resetting USE_CUDA_DOCKER to false: {e}'
|
|
os.environ['USE_CUDA_DOCKER'] = 'false'
|
|
USE_CUDA = 'false'
|
|
DEVICE_TYPE = 'cpu'
|
|
else:
|
|
DEVICE_TYPE = 'cpu'
|
|
|
|
if sys.platform == 'darwin':
|
|
try:
|
|
import torch
|
|
|
|
if torch.backends.mps.is_available() and torch.backends.mps.is_built():
|
|
DEVICE_TYPE = 'mps'
|
|
except Exception:
|
|
pass
|
|
|
|
####################################
|
|
# LOGGING
|
|
####################################
|
|
|
|
_LEVEL_MAP = {
|
|
'DEBUG': 'debug',
|
|
'INFO': 'info',
|
|
'WARNING': 'warn',
|
|
'ERROR': 'error',
|
|
'CRITICAL': 'fatal',
|
|
}
|
|
|
|
|
|
class JSONFormatter(logging.Formatter):
|
|
"""Format log records as single-line JSON objects for structured logging."""
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
log_entry: dict[str, Any] = {
|
|
'ts': datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(timespec='milliseconds'),
|
|
'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
|
|
'msg': record.getMessage(),
|
|
'caller': record.name,
|
|
}
|
|
|
|
if record.exc_info and record.exc_info[0] is not None:
|
|
log_entry['error'] = ''.join(traceback.format_exception(*record.exc_info)).rstrip()
|
|
elif record.exc_text:
|
|
log_entry['error'] = record.exc_text
|
|
|
|
if record.stack_info:
|
|
log_entry['stacktrace'] = record.stack_info
|
|
|
|
return json.dumps(log_entry, ensure_ascii=False, default=str)
|
|
|
|
|
|
LOG_FORMAT = os.environ.get('LOG_FORMAT', '').lower()
|
|
|
|
GLOBAL_LOG_LEVEL = os.environ.get('GLOBAL_LOG_LEVEL', '').upper()
|
|
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
|
|
if LOG_FORMAT == 'json':
|
|
_handler = logging.StreamHandler(sys.stdout)
|
|
_handler.setFormatter(JSONFormatter())
|
|
logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True)
|
|
else:
|
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
|
else:
|
|
GLOBAL_LOG_LEVEL = 'INFO'
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.info(f'GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}')
|
|
|
|
if 'cuda_error' in locals():
|
|
log.exception(cuda_error)
|
|
del cuda_error
|
|
|
|
SRC_LOG_LEVELS = {} # Legacy variable, do not remove
|
|
|
|
WEBUI_NAME = os.environ.get('WEBUI_NAME', 'Open WebUI')
|
|
if WEBUI_NAME != 'Open WebUI':
|
|
WEBUI_NAME += ' (Open WebUI)'
|
|
|
|
WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png'
|
|
|
|
TRUSTED_SIGNATURE_KEY = os.environ.get('TRUSTED_SIGNATURE_KEY', '')
|
|
|
|
####################################
|
|
# ENV (dev,test,prod)
|
|
####################################
|
|
|
|
ENV = os.environ.get('ENV', 'dev')
|
|
|
|
FROM_INIT_PY = os.environ.get('FROM_INIT_PY', 'False').lower() == 'true'
|
|
|
|
if FROM_INIT_PY:
|
|
PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')}
|
|
else:
|
|
try:
|
|
PACKAGE_DATA = json.loads((BASE_DIR / 'package.json').read_text())
|
|
except Exception:
|
|
PACKAGE_DATA = {'version': '0.0.0'}
|
|
|
|
VERSION = PACKAGE_DATA['version']
|
|
|
|
|
|
DEPLOYMENT_ID = os.environ.get('DEPLOYMENT_ID', '')
|
|
INSTANCE_ID = os.environ.get('INSTANCE_ID', str(uuid4()))
|
|
|
|
ENABLE_DB_MIGRATIONS = os.environ.get('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true'
|
|
|
|
|
|
# Function to parse each section
|
|
def parse_section(section):
|
|
items = []
|
|
for li in section.find_all('li'):
|
|
# Extract raw HTML string
|
|
raw_html = str(li)
|
|
|
|
# Extract text without HTML tags
|
|
text = li.get_text(separator=' ', strip=True)
|
|
|
|
# Split into title and content
|
|
parts = text.split(': ', 1)
|
|
title = parts[0].strip() if len(parts) > 1 else ''
|
|
content = parts[1].strip() if len(parts) > 1 else text
|
|
|
|
items.append({'title': title, 'content': content, 'raw': raw_html})
|
|
return items
|
|
|
|
|
|
try:
|
|
changelog_path = BASE_DIR / 'CHANGELOG.md'
|
|
with open(str(changelog_path.absolute()), 'r', encoding='utf8') as file:
|
|
changelog_content = file.read()
|
|
|
|
except Exception:
|
|
changelog_content = (pkgutil.get_data('open_webui', 'CHANGELOG.md') or b'').decode()
|
|
|
|
# Convert markdown content to HTML
|
|
html_content = markdown.markdown(changelog_content)
|
|
|
|
# Parse the HTML content
|
|
soup = BeautifulSoup(html_content, 'html.parser')
|
|
|
|
# Initialize JSON structure
|
|
changelog_json = {}
|
|
|
|
# Iterate over each version
|
|
for version in soup.find_all('h2'):
|
|
version_number = version.get_text().strip().split(' - ')[0][1:-1] # Remove brackets
|
|
date = version.get_text().strip().split(' - ')[1]
|
|
|
|
version_data = {'date': date}
|
|
|
|
# Find the next sibling that is a h3 tag (section title)
|
|
current = version.find_next_sibling()
|
|
|
|
while current and current.name != 'h2':
|
|
if current.name == 'h3':
|
|
section_title = current.get_text().lower() # e.g., "added", "fixed"
|
|
section_items = parse_section(current.find_next_sibling('ul'))
|
|
version_data[section_title] = section_items
|
|
|
|
# Move to the next element
|
|
current = current.find_next_sibling()
|
|
|
|
changelog_json[version_number] = version_data
|
|
|
|
CHANGELOG = changelog_json
|
|
|
|
####################################
|
|
# SAFE_MODE
|
|
####################################
|
|
|
|
SAFE_MODE = os.environ.get('SAFE_MODE', 'false').lower() == 'true'
|
|
|
|
|
|
####################################
|
|
# ENABLE_FORWARD_USER_INFO_HEADERS
|
|
####################################
|
|
|
|
ENABLE_FORWARD_USER_INFO_HEADERS = os.environ.get('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true'
|
|
|
|
# Header names for user info forwarding (customizable via environment variables)
|
|
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name')
|
|
FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id')
|
|
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email')
|
|
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role')
|
|
|
|
# Header name for chat ID forwarding (customizable via environment variable)
|
|
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.environ.get(
|
|
'FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id'
|
|
)
|
|
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id')
|
|
|
|
# Experimental feature, may be removed in future
|
|
ENABLE_STAR_SESSIONS_MIDDLEWARE = os.environ.get('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true'
|
|
|
|
ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
|
|
|
|
####################################
|
|
# WEBUI_BUILD_HASH
|
|
####################################
|
|
|
|
WEBUI_BUILD_HASH = os.environ.get('WEBUI_BUILD_HASH', 'dev-build')
|
|
|
|
####################################
|
|
# DATA/FRONTEND BUILD DIR
|
|
####################################
|
|
|
|
DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve()
|
|
|
|
if FROM_INIT_PY:
|
|
NEW_DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')).resolve()
|
|
NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Check if the data directory exists in the package directory
|
|
if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
|
|
log.info(f'Moving {DATA_DIR} to {NEW_DATA_DIR}')
|
|
for item in DATA_DIR.iterdir():
|
|
dest = NEW_DATA_DIR / item.name
|
|
if item.is_dir():
|
|
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copy2(item, dest)
|
|
|
|
# Zip the data directory
|
|
shutil.make_archive(DATA_DIR.parent / 'open_webui_data', 'zip', DATA_DIR)
|
|
|
|
# Remove the old data directory
|
|
shutil.rmtree(DATA_DIR)
|
|
|
|
DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data'))
|
|
|
|
STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static'))
|
|
|
|
FONTS_DIR = Path(os.getenv('FONTS_DIR', OPEN_WEBUI_DIR / 'static' / 'fonts'))
|
|
|
|
FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).resolve()
|
|
|
|
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'):
|
|
# Rename the file
|
|
os.rename(f'{DATA_DIR}/ollama.db', f'{DATA_DIR}/webui.db')
|
|
log.info('Database migrated from Ollama-WebUI successfully.')
|
|
else:
|
|
pass
|
|
|
|
DATABASE_URL = os.environ.get('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db')
|
|
|
|
DATABASE_TYPE = os.environ.get('DATABASE_TYPE')
|
|
DATABASE_USER = os.environ.get('DATABASE_USER')
|
|
DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD')
|
|
|
|
DATABASE_CRED = ''
|
|
if DATABASE_USER:
|
|
DATABASE_CRED += f'{DATABASE_USER}'
|
|
if DATABASE_PASSWORD:
|
|
DATABASE_CRED += f':{DATABASE_PASSWORD}'
|
|
|
|
DB_VARS = {
|
|
'db_type': DATABASE_TYPE,
|
|
'db_cred': DATABASE_CRED,
|
|
'db_host': os.environ.get('DATABASE_HOST'),
|
|
'db_port': os.environ.get('DATABASE_PORT'),
|
|
'db_name': os.environ.get('DATABASE_NAME'),
|
|
}
|
|
|
|
if all(DB_VARS.values()):
|
|
DATABASE_URL = (
|
|
f'{DB_VARS["db_type"]}://{DB_VARS["db_cred"]}@{DB_VARS["db_host"]}:{DB_VARS["db_port"]}/{DB_VARS["db_name"]}'
|
|
)
|
|
elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.environ.get('DATABASE_URL'):
|
|
# Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set
|
|
DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db'
|
|
|
|
# Replace the postgres:// with postgresql://
|
|
if 'postgres://' in DATABASE_URL:
|
|
DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://')
|
|
|
|
DATABASE_SCHEMA = os.environ.get('DATABASE_SCHEMA', None)
|
|
|
|
DATABASE_POOL_SIZE = os.environ.get('DATABASE_POOL_SIZE', None)
|
|
|
|
if DATABASE_POOL_SIZE != None:
|
|
try:
|
|
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
|
|
except Exception:
|
|
DATABASE_POOL_SIZE = None
|
|
|
|
DATABASE_POOL_MAX_OVERFLOW = os.environ.get('DATABASE_POOL_MAX_OVERFLOW', 0)
|
|
|
|
if DATABASE_POOL_MAX_OVERFLOW == '':
|
|
DATABASE_POOL_MAX_OVERFLOW = 0
|
|
else:
|
|
try:
|
|
DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
|
|
except Exception:
|
|
DATABASE_POOL_MAX_OVERFLOW = 0
|
|
|
|
DATABASE_POOL_TIMEOUT = os.environ.get('DATABASE_POOL_TIMEOUT', 30)
|
|
|
|
if DATABASE_POOL_TIMEOUT == '':
|
|
DATABASE_POOL_TIMEOUT = 30
|
|
else:
|
|
try:
|
|
DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
|
|
except Exception:
|
|
DATABASE_POOL_TIMEOUT = 30
|
|
|
|
DATABASE_POOL_RECYCLE = os.environ.get('DATABASE_POOL_RECYCLE', 3600)
|
|
|
|
if DATABASE_POOL_RECYCLE == '':
|
|
DATABASE_POOL_RECYCLE = 3600
|
|
else:
|
|
try:
|
|
DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
|
|
except Exception:
|
|
DATABASE_POOL_RECYCLE = 3600
|
|
|
|
DATABASE_ENABLE_SQLITE_WAL = os.environ.get('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true'
|
|
|
|
# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server
|
|
# workloads. Each can be overridden via its environment variable.
|
|
# Set any value to an empty string to skip that PRAGMA entirely.
|
|
|
|
# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per
|
|
# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3).
|
|
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.environ.get('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL')
|
|
|
|
# PRAGMA busy_timeout (ms): how long a connection waits for a write lock
|
|
# before raising SQLITE_BUSY.
|
|
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.environ.get('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000')
|
|
|
|
# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache.
|
|
DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536')
|
|
|
|
# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM.
|
|
# Valid values: DEFAULT (0), FILE (1), MEMORY (2).
|
|
DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.environ.get('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY')
|
|
|
|
# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB.
|
|
# Set to 0 to disable mmap.
|
|
DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456')
|
|
|
|
# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint.
|
|
# Without this the WAL grows unbounded during write bursts and is never
|
|
# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default).
|
|
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.environ.get('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864')
|
|
|
|
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None)
|
|
if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None:
|
|
try:
|
|
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL)
|
|
except Exception:
|
|
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0
|
|
|
|
# When enabled, get_db_context reuses existing sessions; set to False to always create new sessions
|
|
DATABASE_ENABLE_SESSION_SHARING = os.environ.get('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true'
|
|
|
|
# Enable public visibility of active user count (when disabled, only admins can see it)
|
|
ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.environ.get('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true'
|
|
|
|
RESET_CONFIG_ON_START = os.environ.get('RESET_CONFIG_ON_START', 'False').lower() == 'true'
|
|
|
|
ENABLE_REALTIME_CHAT_SAVE = os.environ.get('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true'
|
|
|
|
ENABLE_QUERIES_CACHE = os.environ.get('ENABLE_QUERIES_CACHE', 'False').lower() == 'true'
|
|
|
|
RAG_SYSTEM_CONTEXT = os.environ.get('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
|
|
|
|
####################################
|
|
# REDIS
|
|
####################################
|
|
|
|
REDIS_URL = os.environ.get('REDIS_URL', '')
|
|
REDIS_CLUSTER = os.environ.get('REDIS_CLUSTER', 'False').lower() == 'true'
|
|
|
|
REDIS_KEY_PREFIX = os.environ.get('REDIS_KEY_PREFIX', 'open-webui')
|
|
|
|
REDIS_SENTINEL_HOSTS = os.environ.get('REDIS_SENTINEL_HOSTS', '')
|
|
REDIS_SENTINEL_PORT = os.environ.get('REDIS_SENTINEL_PORT', '26379')
|
|
|
|
# Maximum number of retries for Redis operations when using Sentinel fail-over
|
|
REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get('REDIS_SENTINEL_MAX_RETRY_COUNT', '2')
|
|
try:
|
|
REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
|
|
if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
|
|
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
|
|
except ValueError:
|
|
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
|
|
|
|
|
|
REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get('REDIS_SOCKET_CONNECT_TIMEOUT', '')
|
|
try:
|
|
REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
|
|
except ValueError:
|
|
REDIS_SOCKET_CONNECT_TIMEOUT = None
|
|
|
|
# Whether to enable TCP SO_KEEPALIVE on Redis client sockets. Opt-in:
|
|
# defaults to off so behavior is unchanged for existing deployments. When
|
|
# enabled, the kernel sends TCP keepalive probes on idle connections so
|
|
# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC
|
|
# flap) are detected before the next command lands on them.
|
|
REDIS_SOCKET_KEEPALIVE = os.environ.get('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true'
|
|
|
|
# How often (in seconds) redis-py should PING an idle pooled connection
|
|
# before reusing it. Opt-in: defaults to unset (empty string) so behavior
|
|
# is unchanged for existing deployments. When set, should be shorter than
|
|
# the Redis server `timeout` setting and any firewall/LB idle timeout on
|
|
# the path to Redis, so stale connections are detected before a real
|
|
# command lands on them. Set to 0 or empty to disable.
|
|
REDIS_HEALTH_CHECK_INTERVAL = os.environ.get('REDIS_HEALTH_CHECK_INTERVAL', '')
|
|
try:
|
|
REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL)
|
|
if REDIS_HEALTH_CHECK_INTERVAL <= 0:
|
|
REDIS_HEALTH_CHECK_INTERVAL = None
|
|
except ValueError:
|
|
REDIS_HEALTH_CHECK_INTERVAL = None
|
|
|
|
REDIS_RECONNECT_DELAY = os.environ.get('REDIS_RECONNECT_DELAY', '')
|
|
|
|
if REDIS_RECONNECT_DELAY == '':
|
|
REDIS_RECONNECT_DELAY = None
|
|
else:
|
|
try:
|
|
REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
|
|
if REDIS_RECONNECT_DELAY < 0:
|
|
REDIS_RECONNECT_DELAY = None
|
|
except Exception:
|
|
REDIS_RECONNECT_DELAY = None
|
|
|
|
####################################
|
|
# UVICORN WORKERS
|
|
####################################
|
|
|
|
# Number of uvicorn worker processes for handling requests
|
|
UVICORN_WORKERS = os.environ.get('UVICORN_WORKERS', '1')
|
|
try:
|
|
UVICORN_WORKERS = int(UVICORN_WORKERS)
|
|
if UVICORN_WORKERS < 1:
|
|
UVICORN_WORKERS = 1
|
|
except ValueError:
|
|
UVICORN_WORKERS = 1
|
|
log.info(f'Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}')
|
|
|
|
####################################
|
|
# WEBUI_AUTH (Required for security)
|
|
####################################
|
|
|
|
WEBUI_AUTH = os.environ.get('WEBUI_AUTH', 'True').lower() == 'true'
|
|
|
|
ENABLE_INITIAL_ADMIN_SIGNUP = os.environ.get('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true'
|
|
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.environ.get('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true'
|
|
|
|
####################################
|
|
# Admin Account Runtime Creation
|
|
####################################
|
|
|
|
# Optional env vars for creating an admin account on startup
|
|
# Useful for headless/automated deployments
|
|
WEBUI_ADMIN_EMAIL = os.environ.get('WEBUI_ADMIN_EMAIL', '')
|
|
WEBUI_ADMIN_PASSWORD = os.environ.get('WEBUI_ADMIN_PASSWORD', '')
|
|
WEBUI_ADMIN_NAME = os.environ.get('WEBUI_ADMIN_NAME', 'Admin')
|
|
|
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_NAME_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None)
|
|
|
|
|
|
ENABLE_PASSWORD_VALIDATION = os.environ.get('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
|
|
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
|
|
'PASSWORD_VALIDATION_REGEX_PATTERN',
|
|
r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$',
|
|
)
|
|
|
|
|
|
try:
|
|
PASSWORD_VALIDATION_REGEX_PATTERN = rf'{PASSWORD_VALIDATION_REGEX_PATTERN}'
|
|
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
|
|
except Exception as e:
|
|
log.error(f'Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}')
|
|
PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$')
|
|
|
|
PASSWORD_VALIDATION_HINT = os.environ.get('PASSWORD_VALIDATION_HINT', '')
|
|
|
|
|
|
BYPASS_MODEL_ACCESS_CONTROL = os.environ.get('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true'
|
|
|
|
# When enabled, skips pydub-based preprocessing (format conversion, compression,
|
|
# and chunked splitting) before sending files to processing engines. Useful when
|
|
# the upstream provider handles these steps or when ffmpeg is unavailable.
|
|
BYPASS_PYDUB_PREPROCESSING = os.environ.get('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true'
|
|
|
|
# When disabled (default), the OpenAI catch-all proxy endpoint (/{path:path})
|
|
# is blocked. Enable only if you need direct passthrough to upstream OpenAI-
|
|
# compatible APIs for endpoints not natively handled by Open WebUI.
|
|
ENABLE_OPENAI_API_PASSTHROUGH = os.environ.get('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true'
|
|
|
|
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None)
|
|
|
|
####################################
|
|
# WEBUI_SECRET_KEY
|
|
####################################
|
|
|
|
WEBUI_SECRET_KEY = os.environ.get(
|
|
'WEBUI_SECRET_KEY',
|
|
os.environ.get('WEBUI_JWT_SECRET_KEY', 't0p-s3cr3t'), # DEPRECATED: remove at next major version
|
|
)
|
|
|
|
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax')
|
|
|
|
WEBUI_SESSION_COOKIE_SECURE = os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
|
|
|
WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE)
|
|
|
|
WEBUI_AUTH_COOKIE_SECURE = (
|
|
os.environ.get(
|
|
'WEBUI_AUTH_COOKIE_SECURE',
|
|
os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false'),
|
|
).lower()
|
|
== 'true'
|
|
)
|
|
|
|
if WEBUI_AUTH and WEBUI_SECRET_KEY == '':
|
|
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
|
|
|
ENABLE_COMPRESSION_MIDDLEWARE = os.environ.get('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true'
|
|
|
|
####################################
|
|
# OAUTH Configuration
|
|
####################################
|
|
ENABLE_OAUTH_EMAIL_FALLBACK = os.environ.get('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true'
|
|
|
|
ENABLE_OAUTH_ID_TOKEN_COOKIE = os.environ.get('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true'
|
|
|
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
|
|
|
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
|
|
|
# Maximum number of concurrent OAuth sessions per user per provider
|
|
# This prevents unbounded session growth while allowing multi-device usage
|
|
OAUTH_MAX_SESSIONS_PER_USER = int(os.environ.get('OAUTH_MAX_SESSIONS_PER_USER', '10'))
|
|
|
|
# Token Exchange Configuration
|
|
# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
|
|
ENABLE_OAUTH_TOKEN_EXCHANGE = os.environ.get('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true'
|
|
|
|
# Back-Channel Logout Configuration
|
|
# When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout
|
|
# per OpenID Connect Back-Channel Logout 1.0 spec.
|
|
# Requires Redis for JWT revocation.
|
|
ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.environ.get('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true'
|
|
|
|
####################################
|
|
# SCIM Configuration
|
|
####################################
|
|
|
|
ENABLE_SCIM = os.environ.get('ENABLE_SCIM', os.environ.get('SCIM_ENABLED', 'False')).lower() == 'true'
|
|
SCIM_TOKEN = os.environ.get('SCIM_TOKEN', '')
|
|
SCIM_AUTH_PROVIDER = os.environ.get('SCIM_AUTH_PROVIDER', '')
|
|
|
|
if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
|
|
log.warning(
|
|
'SCIM is enabled but SCIM_AUTH_PROVIDER is not set. '
|
|
"Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') "
|
|
'to enable externalId storage.'
|
|
)
|
|
|
|
####################################
|
|
# LICENSE_KEY
|
|
####################################
|
|
|
|
LICENSE_KEY = os.environ.get('LICENSE_KEY', '')
|
|
|
|
LICENSE_BLOB = None
|
|
LICENSE_BLOB_PATH = os.environ.get('LICENSE_BLOB_PATH', DATA_DIR / 'l.data')
|
|
if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH):
|
|
with open(LICENSE_BLOB_PATH, 'rb') as f:
|
|
LICENSE_BLOB = f.read()
|
|
|
|
LICENSE_PUBLIC_KEY = os.environ.get('LICENSE_PUBLIC_KEY', '')
|
|
|
|
pk = None
|
|
if LICENSE_PUBLIC_KEY:
|
|
pk = serialization.load_pem_public_key(
|
|
f"""
|
|
-----BEGIN PUBLIC KEY-----
|
|
{LICENSE_PUBLIC_KEY}
|
|
-----END PUBLIC KEY-----
|
|
""".encode('utf-8')
|
|
)
|
|
|
|
|
|
####################################
|
|
# MODELS
|
|
####################################
|
|
|
|
ENABLE_CUSTOM_MODEL_FALLBACK = os.environ.get('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
|
|
|
|
MODELS_CACHE_TTL = os.environ.get('MODELS_CACHE_TTL', '1')
|
|
if MODELS_CACHE_TTL == '':
|
|
MODELS_CACHE_TTL = None
|
|
else:
|
|
try:
|
|
MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
|
|
except Exception:
|
|
MODELS_CACHE_TTL = 1
|
|
|
|
|
|
####################################
|
|
# CHAT
|
|
####################################
|
|
|
|
ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
|
|
os.environ.get('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
|
|
)
|
|
|
|
# When enabled, uses a hardcoded extension-to-MIME dictionary as a last-resort
|
|
# fallback when both mimetypes.guess_type() and file.meta.content_type fail to
|
|
# determine the content type. This can help on minimal container images (e.g.
|
|
# wolfi-base) that lack /etc/mime.types AND have legacy files without stored
|
|
# content_type metadata.
|
|
ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK = (
|
|
os.environ.get('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true'
|
|
)
|
|
|
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1')
|
|
|
|
if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == '':
|
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
|
else:
|
|
try:
|
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE)
|
|
except Exception:
|
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
|
|
|
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '30')
|
|
|
|
if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == '':
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
|
|
else:
|
|
try:
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES)
|
|
except Exception:
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30
|
|
|
|
|
|
# WARNING: Experimental. Only enable if your upstream Responses API endpoint
|
|
# supports stateful sessions (i.e. server-side response storage with
|
|
# previous_response_id anchoring). Most proxies and third-party endpoints
|
|
# are stateless and will break if this is enabled.
|
|
ENABLE_RESPONSES_API_STATEFUL = os.environ.get('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true'
|
|
|
|
|
|
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '')
|
|
|
|
if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == '':
|
|
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
|
|
else:
|
|
try:
|
|
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE)
|
|
except Exception:
|
|
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
|
|
|
|
|
|
####################################
|
|
# WEBSOCKET SUPPORT
|
|
####################################
|
|
|
|
ENABLE_WEBSOCKET_SUPPORT = os.environ.get('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
|
|
|
|
|
|
WEBSOCKET_MANAGER = os.environ.get('WEBSOCKET_MANAGER', '')
|
|
|
|
WEBSOCKET_REDIS_OPTIONS = os.environ.get('WEBSOCKET_REDIS_OPTIONS', '')
|
|
|
|
|
|
if WEBSOCKET_REDIS_OPTIONS == '':
|
|
if REDIS_SOCKET_CONNECT_TIMEOUT:
|
|
WEBSOCKET_REDIS_OPTIONS = {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT}
|
|
else:
|
|
log.debug('No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None')
|
|
WEBSOCKET_REDIS_OPTIONS = None
|
|
else:
|
|
try:
|
|
WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
|
|
except Exception:
|
|
log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None')
|
|
WEBSOCKET_REDIS_OPTIONS = None
|
|
|
|
WEBSOCKET_REDIS_URL = os.environ.get('WEBSOCKET_REDIS_URL', REDIS_URL)
|
|
WEBSOCKET_REDIS_CLUSTER = os.environ.get('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'
|
|
|
|
websocket_redis_lock_timeout = os.environ.get('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60')
|
|
|
|
try:
|
|
WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
|
|
except ValueError:
|
|
WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
|
|
|
|
WEBSOCKET_SENTINEL_HOSTS = os.environ.get('WEBSOCKET_SENTINEL_HOSTS', '')
|
|
WEBSOCKET_SENTINEL_PORT = os.environ.get('WEBSOCKET_SENTINEL_PORT', '26379')
|
|
WEBSOCKET_SERVER_LOGGING = os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true'
|
|
WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
|
|
os.environ.get(
|
|
'WEBSOCKET_SERVER_ENGINEIO_LOGGING',
|
|
os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False'),
|
|
).lower()
|
|
== 'true'
|
|
)
|
|
WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get('WEBSOCKET_SERVER_PING_TIMEOUT', '20')
|
|
try:
|
|
WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
|
|
except ValueError:
|
|
WEBSOCKET_SERVER_PING_TIMEOUT = 20
|
|
|
|
WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get('WEBSOCKET_SERVER_PING_INTERVAL', '25')
|
|
try:
|
|
WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
|
|
except ValueError:
|
|
WEBSOCKET_SERVER_PING_INTERVAL = 25
|
|
|
|
WEBSOCKET_EVENT_CALLER_TIMEOUT = os.environ.get('WEBSOCKET_EVENT_CALLER_TIMEOUT', '')
|
|
|
|
if WEBSOCKET_EVENT_CALLER_TIMEOUT == '':
|
|
WEBSOCKET_EVENT_CALLER_TIMEOUT = None
|
|
else:
|
|
try:
|
|
WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT)
|
|
except ValueError:
|
|
WEBSOCKET_EVENT_CALLER_TIMEOUT = 300
|
|
|
|
|
|
REQUESTS_VERIFY = os.environ.get('REQUESTS_VERIFY', 'True').lower() == 'true'
|
|
|
|
AIOHTTP_CLIENT_TIMEOUT = os.environ.get('AIOHTTP_CLIENT_TIMEOUT', '')
|
|
|
|
if AIOHTTP_CLIENT_TIMEOUT == '':
|
|
AIOHTTP_CLIENT_TIMEOUT = None
|
|
else:
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
|
except Exception:
|
|
AIOHTTP_CLIENT_TIMEOUT = 300
|
|
|
|
|
|
AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true'
|
|
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
|
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
|
os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
|
)
|
|
|
|
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == '':
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
|
|
else:
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
|
except Exception:
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
|
|
|
|
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10')
|
|
|
|
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == '':
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
|
|
else:
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA)
|
|
except Exception:
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
|
|
|
|
|
|
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
|
|
os.environ.get('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True').lower() == 'true'
|
|
)
|
|
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '')
|
|
|
|
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '':
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
|
else:
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER)
|
|
except Exception:
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
|
|
|
|
|
|
####################################
|
|
# AIOHTTP Connection Pool
|
|
####################################
|
|
|
|
AIOHTTP_POOL_CONNECTIONS = os.environ.get('AIOHTTP_POOL_CONNECTIONS', '')
|
|
if AIOHTTP_POOL_CONNECTIONS == '':
|
|
AIOHTTP_POOL_CONNECTIONS = None
|
|
else:
|
|
try:
|
|
AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS)
|
|
except ValueError:
|
|
AIOHTTP_POOL_CONNECTIONS = None
|
|
|
|
AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.environ.get('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '')
|
|
if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '':
|
|
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
|
else:
|
|
try:
|
|
AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST)
|
|
except ValueError:
|
|
AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
|
|
|
|
AIOHTTP_POOL_DNS_TTL = os.environ.get('AIOHTTP_POOL_DNS_TTL', '300')
|
|
try:
|
|
AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL)
|
|
if AIOHTTP_POOL_DNS_TTL < 0:
|
|
AIOHTTP_POOL_DNS_TTL = 300
|
|
except ValueError:
|
|
AIOHTTP_POOL_DNS_TTL = 300
|
|
|
|
RAG_EMBEDDING_TIMEOUT = os.environ.get('RAG_EMBEDDING_TIMEOUT', '')
|
|
|
|
if RAG_EMBEDDING_TIMEOUT == '':
|
|
RAG_EMBEDDING_TIMEOUT = None
|
|
else:
|
|
try:
|
|
RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT)
|
|
except Exception:
|
|
RAG_EMBEDDING_TIMEOUT = None
|
|
|
|
|
|
####################################
|
|
# SENTENCE TRANSFORMERS
|
|
####################################
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_BACKEND', '')
|
|
if SENTENCE_TRANSFORMERS_BACKEND == '':
|
|
SENTENCE_TRANSFORMERS_BACKEND = 'torch'
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '')
|
|
if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '':
|
|
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
|
|
else:
|
|
try:
|
|
SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_MODEL_KWARGS)
|
|
except Exception:
|
|
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '')
|
|
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '':
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch'
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
|
|
'SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', ''
|
|
)
|
|
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '':
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
|
|
else:
|
|
try:
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS)
|
|
except Exception:
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
|
|
|
|
# Whether to apply sigmoid normalization to CrossEncoder reranking scores.
|
|
# When enabled (default), scores are normalized to 0-1 range for proper
|
|
# relevance threshold behavior with MS MARCO models.
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
|
|
os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
|
|
)
|
|
|
|
####################################
|
|
# OFFLINE_MODE
|
|
####################################
|
|
|
|
ENABLE_VERSION_UPDATE_CHECK = os.environ.get('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
|
|
OFFLINE_MODE = os.environ.get('OFFLINE_MODE', 'false').lower() == 'true'
|
|
|
|
if OFFLINE_MODE:
|
|
os.environ['HF_HUB_OFFLINE'] = '1'
|
|
ENABLE_VERSION_UPDATE_CHECK = False
|
|
|
|
####################################
|
|
# AUDIT LOGGING
|
|
####################################
|
|
|
|
|
|
ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true'
|
|
ENABLE_AUDIT_LOGS_FILE = os.getenv('ENABLE_AUDIT_LOGS_FILE', 'True').lower() == 'true'
|
|
|
|
# Where to store log file
|
|
# Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to
|
|
# provide the whole path, like: /app/audit.log
|
|
AUDIT_LOGS_FILE_PATH = os.getenv('AUDIT_LOGS_FILE_PATH', f'{DATA_DIR}/audit.log')
|
|
# Maximum size of a file before rotating into a new log file
|
|
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv('AUDIT_LOG_FILE_ROTATION_SIZE', '10MB')
|
|
|
|
# Comma separated list of logger names to use for audit logging
|
|
# Default is "uvicorn.access" which is the access log for Uvicorn
|
|
# You can add more logger names to this list if you want to capture more logs
|
|
AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.access').split(',')
|
|
|
|
# METADATA | REQUEST | REQUEST_RESPONSE
|
|
AUDIT_LOG_LEVEL = os.getenv('AUDIT_LOG_LEVEL', 'NONE').upper()
|
|
try:
|
|
MAX_BODY_LOG_SIZE = int(os.environ.get('MAX_BODY_LOG_SIZE') or 2048)
|
|
except ValueError:
|
|
MAX_BODY_LOG_SIZE = 2048
|
|
|
|
# Comma separated list for urls to exclude from audit
|
|
AUDIT_EXCLUDED_PATHS = os.getenv('AUDIT_EXCLUDED_PATHS', '/chats,/chat,/folders').split(',')
|
|
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
|
|
AUDIT_EXCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_EXCLUDED_PATHS]
|
|
|
|
# Comma separated list of urls to include in audit (whitelist mode)
|
|
# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored
|
|
AUDIT_INCLUDED_PATHS = os.getenv('AUDIT_INCLUDED_PATHS', '').split(',')
|
|
AUDIT_INCLUDED_PATHS = [path.strip() for path in AUDIT_INCLUDED_PATHS]
|
|
AUDIT_INCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_INCLUDED_PATHS if path]
|
|
|
|
# When enabled, GET requests are also audited (disabled by default to avoid log noise)
|
|
ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true'
|
|
|
|
|
|
####################################
|
|
# OPENTELEMETRY
|
|
####################################
|
|
|
|
ENABLE_OTEL = os.environ.get('ENABLE_OTEL', 'False').lower() == 'true'
|
|
ENABLE_OTEL_TRACES = os.environ.get('ENABLE_OTEL_TRACES', 'False').lower() == 'true'
|
|
ENABLE_OTEL_METRICS = os.environ.get('ENABLE_OTEL_METRICS', 'False').lower() == 'true'
|
|
ENABLE_OTEL_LOGS = os.environ.get('ENABLE_OTEL_LOGS', 'False').lower() == 'true'
|
|
|
|
OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317')
|
|
OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
OTEL_EXPORTER_OTLP_INSECURE = os.environ.get('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true'
|
|
OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
|
|
os.environ.get('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
|
)
|
|
OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
|
|
os.environ.get('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
|
)
|
|
OTEL_SERVICE_NAME = os.environ.get('OTEL_SERVICE_NAME', 'open-webui')
|
|
OTEL_RESOURCE_ATTRIBUTES = os.environ.get('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2
|
|
OTEL_TRACES_SAMPLER = os.environ.get('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower()
|
|
OTEL_BASIC_AUTH_USERNAME = os.environ.get('OTEL_BASIC_AUTH_USERNAME', '')
|
|
OTEL_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_BASIC_AUTH_PASSWORD', '')
|
|
OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.environ.get('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000'))
|
|
|
|
OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
|
OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
|
OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
|
OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
|
|
|
OTEL_OTLP_SPAN_EXPORTER = os.environ.get('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http
|
|
|
|
OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get(
|
|
'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
|
).lower() # grpc or http
|
|
|
|
OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get(
|
|
'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
|
).lower() # grpc or http
|
|
|
|
####################################
|
|
# TOOLS/FUNCTIONS PIP OPTIONS
|
|
####################################
|
|
|
|
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
|
|
os.environ.get('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true'
|
|
)
|
|
|
|
PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split()
|
|
PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split()
|
|
|
|
|
|
####################################
|
|
# PROGRESSIVE WEB APP OPTIONS
|
|
####################################
|
|
|
|
EXTERNAL_PWA_MANIFEST_URL = os.environ.get('EXTERNAL_PWA_MANIFEST_URL')
|
|
|
|
####################################
|
|
# 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.environ.get('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower()
|
|
DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true'
|