mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
c5ec01b1f9
Since v0.11.0 shipped aiodns, aiohttp silently switched every outbound request from the OS resolver to c-ares. On some Windows hosts the bundled c-ares 1.34.6 (pycares 5) discovers only 127.0.0.1:53 as nameserver, so every external provider lookup fails (#28013). In Docker the long-lived c-ares channel intermittently stops resolving container names while Docker's embedded DNS keeps answering, which wipes the Ollama model list and fails all in-flight chats with a misleading "Model not found" (#28215). This restores the pre-0.11 ThreadedResolver (OS resolver) by default and gates the c-ares path behind a new env var, AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER, off by default. The event-loop DNS perf improvement is now opt-in for deployments whose resolver setup is known to work with c-ares, instead of a process-wide side effect of the package being installed. aiodns is also downgraded and pinned to 3.6.1 (pycares<5), the last release before the broken c-ares 1.34.6 build, so opting in does not hit the Windows regression. The hardcoded AsyncResolver in the Mistral OCR loader now follows the same switch. Simply removing aiodns instead was not an option because opting in would then be impossible, and #28215 showed the Docker failure is c-ares itself, not aiodns 4.x.
1249 lines
49 KiB
Python
1249 lines
49 KiB
Python
import datetime as dt
|
|
import importlib.metadata
|
|
import json
|
|
import logging
|
|
import os
|
|
import pkgutil
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import traceback
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
from uuid import uuid4
|
|
|
|
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()
|
|
|
|
# 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.getenv('DOCKER', 'False').lower() == 'true'
|
|
|
|
USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false')
|
|
DEVICE_TYPE = 'cpu'
|
|
_cuda_error: Optional[str] = None
|
|
|
|
if USE_CUDA.lower() == 'true':
|
|
try:
|
|
import torch # noqa: E402
|
|
|
|
if not torch.cuda.is_available():
|
|
raise RuntimeError('CUDA not available')
|
|
DEVICE_TYPE = 'cuda'
|
|
except Exception as exc:
|
|
_cuda_error = f'CUDA unavailable (USE_CUDA_DOCKER=true), falling back to CPU: {exc}'
|
|
os.environ['USE_CUDA_DOCKER'] = 'false'
|
|
USE_CUDA = 'false'
|
|
|
|
if sys.platform == 'darwin' and DEVICE_TYPE == 'cpu':
|
|
try:
|
|
import torch # noqa: E402
|
|
|
|
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': dt.datetime.fromtimestamp(record.created, tz=dt.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.getenv('LOG_FORMAT', '').lower()
|
|
LOGURU_DIAGNOSE = os.getenv('LOGURU_DIAGNOSE', 'False').lower() == 'true'
|
|
|
|
GLOBAL_LOG_LEVEL = os.getenv('GLOBAL_LOG_LEVEL', '').upper()
|
|
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
|
|
_log_cfg: dict[str, Any] = {'level': GLOBAL_LOG_LEVEL, 'force': True}
|
|
if LOG_FORMAT == 'json':
|
|
_json_handler = logging.StreamHandler(sys.stdout)
|
|
_json_handler.setFormatter(JSONFormatter())
|
|
_log_cfg['handlers'] = [_json_handler]
|
|
else:
|
|
_log_cfg['stream'] = sys.stdout
|
|
logging.basicConfig(**_log_cfg)
|
|
else:
|
|
GLOBAL_LOG_LEVEL = 'INFO'
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL)
|
|
|
|
if _cuda_error:
|
|
log.error(_cuda_error)
|
|
_cuda_error = None
|
|
|
|
SRC_LOG_LEVELS = {} # Legacy variable, do not remove
|
|
|
|
####################################
|
|
# ENV (dev,test,prod)
|
|
####################################
|
|
|
|
ENV = os.getenv('ENV', 'dev')
|
|
|
|
FROM_INIT_PY = os.getenv('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.getenv('DEPLOYMENT_ID', '')
|
|
INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4()))
|
|
|
|
ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true'
|
|
|
|
# Swap the JSON encoder/decoder used across the app (HTTP request bodies, JSONResponse
|
|
# bodies, upstream provider responses, socket.io payloads) from the stdlib `json` module
|
|
# to orjson. Faster, but stricter: see open_webui/utils/json_codec.py for the differences.
|
|
ENABLE_ORJSON = os.getenv('ENABLE_ORJSON', 'False').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()), 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
|
|
|
|
####################################
|
|
# 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('Moving %s to %s', DATA_DIR, 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.getenv('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db')
|
|
|
|
DATABASE_TYPE = os.getenv('DATABASE_TYPE')
|
|
DATABASE_USER = os.getenv('DATABASE_USER')
|
|
DATABASE_PASSWORD = os.getenv('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.getenv('DATABASE_HOST'),
|
|
'db_port': os.getenv('DATABASE_PORT'),
|
|
'db_name': os.getenv('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.getenv('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.getenv('DATABASE_SCHEMA', None)
|
|
DATABASE_ENABLE_IAM_TOKEN_AUTH = os.getenv('DATABASE_ENABLE_IAM_TOKEN_AUTH', 'False').lower() == 'true'
|
|
|
|
_pool_size_raw = os.getenv('DATABASE_POOL_SIZE')
|
|
try:
|
|
DATABASE_POOL_SIZE = int(_pool_size_raw) if _pool_size_raw else None
|
|
except (ValueError, TypeError):
|
|
DATABASE_POOL_SIZE = None
|
|
|
|
_pool_overflow_raw = os.getenv('DATABASE_POOL_MAX_OVERFLOW', '0')
|
|
try:
|
|
DATABASE_POOL_MAX_OVERFLOW = int(_pool_overflow_raw) if _pool_overflow_raw else 0
|
|
except (ValueError, TypeError):
|
|
DATABASE_POOL_MAX_OVERFLOW = 0
|
|
|
|
_pool_timeout_raw = os.getenv('DATABASE_POOL_TIMEOUT', '30')
|
|
try:
|
|
DATABASE_POOL_TIMEOUT = int(_pool_timeout_raw) if _pool_timeout_raw else 30
|
|
except (ValueError, TypeError):
|
|
DATABASE_POOL_TIMEOUT = 30
|
|
|
|
_pool_recycle_raw = os.getenv('DATABASE_POOL_RECYCLE', '3600')
|
|
try:
|
|
DATABASE_POOL_RECYCLE = int(_pool_recycle_raw) if _pool_recycle_raw else 3600
|
|
except (ValueError, TypeError):
|
|
DATABASE_POOL_RECYCLE = 3600
|
|
|
|
DATABASE_ENABLE_SQLITE_WAL = os.getenv('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.getenv('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.getenv('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000')
|
|
|
|
# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache.
|
|
DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.getenv('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.getenv('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.getenv('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.getenv('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864')
|
|
|
|
DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.getenv('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
|
|
|
|
DATABASE_ENABLE_SESSION_SHARING = os.getenv('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true'
|
|
ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.getenv('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true'
|
|
RESET_CONFIG_ON_START = os.getenv('RESET_CONFIG_ON_START', 'False').lower() == 'true'
|
|
ENABLE_REALTIME_CHAT_SAVE = os.getenv('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true'
|
|
ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'true'
|
|
RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
|
|
|
|
####################################
|
|
# REDIS
|
|
####################################
|
|
|
|
REDIS_URL = os.getenv('REDIS_URL', '')
|
|
REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true'
|
|
|
|
REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui')
|
|
|
|
REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '')
|
|
REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379')
|
|
|
|
# Maximum number of retries for Redis operations when using Sentinel fail-over
|
|
REDIS_SENTINEL_MAX_RETRY_COUNT = os.getenv('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.getenv('REDIS_SOCKET_CONNECT_TIMEOUT', '')
|
|
try:
|
|
REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
|
|
except ValueError:
|
|
REDIS_SOCKET_CONNECT_TIMEOUT = None
|
|
|
|
REDIS_SOCKET_TIMEOUT = os.getenv('REDIS_SOCKET_TIMEOUT', '')
|
|
try:
|
|
REDIS_SOCKET_TIMEOUT = float(REDIS_SOCKET_TIMEOUT)
|
|
except ValueError:
|
|
REDIS_SOCKET_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.getenv('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.getenv('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.getenv('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
|
|
####################################
|
|
|
|
try:
|
|
UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1)
|
|
except (ValueError, TypeError):
|
|
UVICORN_WORKERS = 1
|
|
|
|
####################################
|
|
# WEBSOCKET SUPPORT
|
|
####################################
|
|
|
|
ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
|
|
|
|
|
|
WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '')
|
|
|
|
WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '')
|
|
|
|
|
|
if WEBSOCKET_REDIS_OPTIONS == '':
|
|
WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
|
|
if REDIS_SOCKET_CONNECT_TIMEOUT:
|
|
WEBSOCKET_REDIS_OPTIONS['socket_connect_timeout'] = REDIS_SOCKET_CONNECT_TIMEOUT
|
|
else:
|
|
try:
|
|
WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
|
|
WEBSOCKET_REDIS_OPTIONS.setdefault('socket_timeout', None)
|
|
except Exception:
|
|
log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to socket_timeout=None')
|
|
WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
|
|
|
|
WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL)
|
|
WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'
|
|
|
|
websocket_redis_lock_timeout = os.getenv('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.getenv('WEBSOCKET_SENTINEL_HOSTS', '')
|
|
WEBSOCKET_SENTINEL_PORT = os.getenv('WEBSOCKET_SENTINEL_PORT', '26379')
|
|
WEBSOCKET_SERVER_LOGGING = os.getenv('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true'
|
|
WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
|
|
os.getenv(
|
|
'WEBSOCKET_SERVER_ENGINEIO_LOGGING',
|
|
os.getenv('WEBSOCKET_SERVER_LOGGING', 'False'),
|
|
).lower()
|
|
== 'true'
|
|
)
|
|
WEBSOCKET_SERVER_PING_TIMEOUT = os.getenv('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.getenv('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.getenv('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
|
|
|
|
|
|
import ssl as _ssl
|
|
|
|
|
|
# Dedicated env var for a custom CA bundle file path. When set, this is
|
|
# used as the default CA bundle for all outbound HTTPS connections that
|
|
# have SSL verification enabled (i.e. when their per-connection SSL env
|
|
# var is ``"True"``). Per-connection overrides (setting the SSL env var
|
|
# to a path directly) take precedence over this global fallback.
|
|
#
|
|
# This follows the industry convention of ``SSL_CERT_FILE`` / ``REQUESTS_CA_BUNDLE``
|
|
# but is scoped to Open WebUI to avoid interfering with system-level settings.
|
|
AIOHTTP_CLIENT_SSL_CERT_FILE = os.getenv('AIOHTTP_CLIENT_SSL_CERT_FILE', '').strip()
|
|
|
|
|
|
def _build_ssl_context_from_file(path: str) -> '_ssl.SSLContext | None':
|
|
"""Create an SSLContext from a CA bundle file, or None if invalid."""
|
|
if not path:
|
|
return None
|
|
if not os.path.isfile(path):
|
|
log.warning(
|
|
'SSL CA bundle path does not exist: %r, ignoring',
|
|
path,
|
|
)
|
|
return None
|
|
ctx = _ssl.create_default_context(cafile=path)
|
|
log.info('Using custom SSL CA bundle: %s', path)
|
|
return ctx
|
|
|
|
|
|
# Pre-built SSLContext from the dedicated env var (cached once at startup).
|
|
_GLOBAL_SSL_CONTEXT = _build_ssl_context_from_file(AIOHTTP_CLIENT_SSL_CERT_FILE)
|
|
|
|
|
|
def _parse_ssl_env(value: str) -> 'bool | _ssl.SSLContext':
|
|
"""Parse an SSL env var into a bool or SSLContext.
|
|
|
|
- ``"true"`` → uses ``AIOHTTP_CLIENT_SSL_CERT_FILE`` context if set,
|
|
otherwise ``True`` (default SSL verification via certifi)
|
|
- ``"false"`` → ``False`` (no verification)
|
|
- ``"/path/to/ca-bundle.crt"`` → ``SSLContext`` loading that CA file
|
|
(takes precedence over ``AIOHTTP_CLIENT_SSL_CERT_FILE``)
|
|
|
|
This allows users with corporate or internal CAs to point Open WebUI
|
|
at a custom CA bundle without disabling verification entirely.
|
|
"""
|
|
lower = value.strip().lower()
|
|
if lower == 'true':
|
|
# Use the global dedicated CA bundle if configured, otherwise default
|
|
return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
|
|
if lower == 'false':
|
|
return False
|
|
# Treat as a file path to a CA bundle (per-connection override)
|
|
ctx = _build_ssl_context_from_file(value.strip())
|
|
if ctx is not None:
|
|
return ctx
|
|
# Path was invalid — fall back to default
|
|
return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
|
|
|
|
|
|
REQUESTS_VERIFY = os.getenv('REQUESTS_VERIFY', 'True').lower() == 'true'
|
|
|
|
_aiohttp_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT', '')
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT = int(_aiohttp_timeout_raw) if _aiohttp_timeout_raw else None
|
|
except (ValueError, TypeError):
|
|
AIOHTTP_CLIENT_TIMEOUT = 300
|
|
|
|
# Optional between-chunks idle cap for streaming aiohttp requests.
|
|
AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = os.getenv('AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT', '')
|
|
if AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT == '':
|
|
AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
|
|
else:
|
|
try:
|
|
AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = int(AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT)
|
|
except (ValueError, TypeError):
|
|
AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
|
|
|
|
if AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT is not None and AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT <= 0:
|
|
AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
|
|
|
|
|
|
# SSL verification for general outbound requests (OpenAI, OAuth, etc.).
|
|
# Accepts "True", "False", or a path to a CA bundle file.
|
|
# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
|
|
AIOHTTP_CLIENT_SESSION_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_SSL', 'True'))
|
|
|
|
SEARXNG_CLIENT_CERT_FILE = os.getenv('SEARXNG_CLIENT_CERT_FILE', '').strip()
|
|
SEARXNG_CLIENT_KEY_FILE = os.getenv('SEARXNG_CLIENT_KEY_FILE', '').strip()
|
|
|
|
# When False (default), outbound HTTP requests do not follow 3xx redirects.
|
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.getenv('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true'
|
|
|
|
# Opt-in c-ares DNS resolution (aiodns). Off by default: c-ares breaks name
|
|
# resolution in some environments (#28013, #28215). Must run before any
|
|
# TCPConnector is constructed.
|
|
AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER = os.getenv('AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER', 'False').lower() == 'true'
|
|
|
|
if not AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER:
|
|
import aiohttp
|
|
|
|
aiohttp.DefaultResolver = aiohttp.resolver.ThreadedResolver # for plugin code
|
|
aiohttp.resolver.DefaultResolver = aiohttp.resolver.ThreadedResolver
|
|
aiohttp.connector.DefaultResolver = aiohttp.resolver.ThreadedResolver
|
|
|
|
# Optional User-Agent override for outbound web-loader fetches. When set,
|
|
# SafeWebBaseLoader sends this value instead of the default python-requests UA
|
|
# which is aggressively blocked by Cloudflare, Wikipedia, and similar services.
|
|
USER_AGENT = os.getenv('USER_AGENT', '')
|
|
|
|
_model_list_timeout_raw = os.getenv(
|
|
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
|
os.getenv('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
|
)
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(_model_list_timeout_raw) if _model_list_timeout_raw else None
|
|
except (ValueError, TypeError):
|
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
|
|
|
|
_tool_data_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10')
|
|
try:
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(_tool_data_timeout_raw) if _tool_data_timeout_raw else None
|
|
except (ValueError, TypeError):
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
|
|
|
|
AIOHTTP_FILE_STREAM_CHUNK_SIZE = os.getenv('AIOHTTP_FILE_STREAM_CHUNK_SIZE', str(1024 * 1024))
|
|
try:
|
|
AIOHTTP_FILE_STREAM_CHUNK_SIZE = int(AIOHTTP_FILE_STREAM_CHUNK_SIZE)
|
|
except Exception:
|
|
AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
|
|
|
|
if AIOHTTP_FILE_STREAM_CHUNK_SIZE <= 0:
|
|
AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
|
|
|
|
|
|
# SSL verification for tool server connections specifically.
|
|
# Accepts "True", "False", or a path to a CA bundle file.
|
|
# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
|
|
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True'))
|
|
|
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('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
|
|
|
|
# Timeout (in seconds) for the MCP session.initialize() handshake.
|
|
# The handshake performs a list-tools round-trip and can take tens of
|
|
# seconds on cold-start servers or servers exposing many tools.
|
|
MCP_INITIALIZE_TIMEOUT = os.getenv('MCP_INITIALIZE_TIMEOUT', '10')
|
|
try:
|
|
MCP_INITIALIZE_TIMEOUT = int(MCP_INITIALIZE_TIMEOUT)
|
|
except (ValueError, TypeError):
|
|
MCP_INITIALIZE_TIMEOUT = 10
|
|
|
|
|
|
####################################
|
|
# AIOHTTP Connection Pool
|
|
####################################
|
|
|
|
AIOHTTP_POOL_CONNECTIONS = os.getenv('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.getenv('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.getenv('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.getenv('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
|
|
|
|
|
|
####################################
|
|
# 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.
|
|
WEBUI_SECRET_KEY = os.getenv(
|
|
'WEBUI_SECRET_KEY',
|
|
os.getenv('WEBUI_JWT_SECRET_KEY', ''),
|
|
)
|
|
|
|
ENABLE_VALVE_ENCRYPTION = os.getenv('ENABLE_VALVE_ENCRYPTION', 'False').lower() == 'true'
|
|
|
|
WEBUI_SESSION_COOKIE_SAME_SITE = os.getenv('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax')
|
|
WEBUI_SESSION_COOKIE_SECURE = os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
|
WEBUI_AUTH_COOKIE_SAME_SITE = os.getenv('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE)
|
|
WEBUI_AUTH_COOKIE_SECURE = (
|
|
os.getenv(
|
|
'WEBUI_AUTH_COOKIE_SECURE',
|
|
os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false'),
|
|
).lower()
|
|
== 'true'
|
|
)
|
|
|
|
if WEBUI_AUTH and WEBUI_SECRET_KEY == '':
|
|
raise SystemExit(
|
|
'WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.\n'
|
|
'The supported start methods set or auto-generate it for you: use start.sh (Linux/macOS), '
|
|
'start_windows.bat (Windows), or `open-webui serve`.\n'
|
|
'If you start the backend another way (e.g. invoking uvicorn directly, which is unsupported), '
|
|
'you must set WEBUI_SECRET_KEY yourself to a long random value.\n'
|
|
'See https://docs.openwebui.com/reference/env-configuration#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
|
|
WEBUI_ADMIN_EMAIL = os.getenv('WEBUI_ADMIN_EMAIL', '')
|
|
WEBUI_ADMIN_PASSWORD = os.getenv('WEBUI_ADMIN_PASSWORD', '')
|
|
WEBUI_ADMIN_NAME = os.getenv('WEBUI_ADMIN_NAME', 'Admin')
|
|
|
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_NAME_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None)
|
|
WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None)
|
|
|
|
# Custom header name for API key authentication. Defaults to 'x-api-key'.
|
|
# Useful when Open WebUI sits behind a reverse proxy / API gateway that
|
|
# already uses the Authorization header for its own authentication — set
|
|
# this to a unique header (e.g. 'X-OpenWebUI-Key') so the middleware
|
|
# checks the custom header instead and avoids the 401 short-circuit.
|
|
CUSTOM_API_KEY_HEADER = os.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key')
|
|
|
|
ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
|
|
PASSWORD_HASH_ALGORITHM = os.getenv('PASSWORD_HASH_ALGORITHM', 'bcrypt').lower()
|
|
PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv(
|
|
'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.getenv('PASSWORD_VALIDATION_HINT', '')
|
|
|
|
|
|
BYPASS_MODEL_ACCESS_CONTROL = os.getenv('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true'
|
|
BYPASS_RETRIEVAL_ACCESS_CONTROL = os.getenv('BYPASS_RETRIEVAL_ACCESS_CONTROL', 'False').lower() == 'true'
|
|
|
|
# When True, collection names that do not match any known file-*, user-memory-*,
|
|
# web-search-*, or knowledge-base collection are allowed through access control
|
|
# for non-admin users. When False (default), unknown collection names are
|
|
# denied — closing the legacy unscoped namespace.
|
|
ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS = os.getenv('ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS', 'False').lower() == 'true'
|
|
MINERU_MAX_MARKDOWN_BYTES = (
|
|
int(os.getenv('MINERU_MAX_MARKDOWN_BYTES')) if os.getenv('MINERU_MAX_MARKDOWN_BYTES') else None
|
|
)
|
|
|
|
# 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.getenv('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.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true'
|
|
|
|
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'
|
|
|
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
|
|
|
|
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('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.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10'))
|
|
|
|
# Token Exchange Configuration
|
|
# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
|
|
ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true'
|
|
_oauth_token_exchange_rate_limit = (os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT') or '').strip()
|
|
OAUTH_TOKEN_EXCHANGE_RATE_LIMIT = (
|
|
int(_oauth_token_exchange_rate_limit)
|
|
if _oauth_token_exchange_rate_limit and _oauth_token_exchange_rate_limit.lower() != 'none'
|
|
else None
|
|
)
|
|
OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW = int(os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW', str(60 * 3)))
|
|
OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS = [
|
|
client_id.strip()
|
|
for client_id in os.getenv('OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS', '').split(',')
|
|
if client_id.strip()
|
|
]
|
|
|
|
# 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.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', '')
|
|
SCIM_AUTH_PROVIDER = os.getenv('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.getenv('LICENSE_KEY', '')
|
|
|
|
LICENSE_BLOB = None
|
|
LICENSE_BLOB_PATH = os.getenv('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.getenv('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()
|
|
)
|
|
|
|
|
|
####################################
|
|
# WEBUI Identity
|
|
####################################
|
|
|
|
# LICENSE covers this Open WebUI branding surface, including name, logo,
|
|
# visual, textual, symbolic identifiers, metadata, and surrounding UI.
|
|
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
|
|
# https://docs.openwebui.com/license.
|
|
WEBUI_NAME = os.getenv('WEBUI_NAME', 'Open WebUI')
|
|
if WEBUI_NAME != 'Open WebUI':
|
|
WEBUI_NAME += ' (Open WebUI)'
|
|
|
|
# LICENSE covers this Open WebUI branding surface, including this favicon
|
|
# and any visual, textual, or symbolic identifiers it preserves.
|
|
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
|
|
# https://docs.openwebui.com/license.
|
|
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'
|
|
ENABLE_STAR_SESSIONS_MIDDLEWARE = os.getenv('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true'
|
|
ENABLE_KB_EXEC = os.getenv('ENABLE_KB_EXEC', 'False').lower() == 'true'
|
|
|
|
ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.getenv('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
|
|
PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset(
|
|
t.strip()
|
|
for t in os.getenv(
|
|
'PROFILE_IMAGE_ALLOWED_MIME_TYPES',
|
|
'image/png,image/jpeg,image/gif,image/webp',
|
|
).split(',')
|
|
if t.strip()
|
|
)
|
|
|
|
# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis
|
|
# bloat from inline avatars and model icons. Unset (default) disables the cap.
|
|
_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'
|
|
|
|
FORWARD_USER_INFO_HEADER_USER_NAME = os.getenv('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name')
|
|
FORWARD_USER_INFO_HEADER_USER_ID = os.getenv('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id')
|
|
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.getenv('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email')
|
|
FORWARD_USER_INFO_HEADER_USER_ROLE = os.getenv('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role')
|
|
FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id')
|
|
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id')
|
|
|
|
# If set while ENABLE_FORWARD_USER_INFO_HEADERS is True, send one signed HS256 JWT
|
|
# (FORWARD_USER_INFO_HEADER_JWT) instead of separate X-OpenWebUI-User-* headers.
|
|
FORWARD_USER_INFO_HEADER_JWT_SECRET = (os.environ.get('FORWARD_USER_INFO_HEADER_JWT_SECRET') or '').strip() or None
|
|
FORWARD_USER_INFO_HEADER_JWT = os.environ.get('FORWARD_USER_INFO_HEADER_JWT', 'X-OpenWebUI-User-Jwt')
|
|
try:
|
|
FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = int(
|
|
os.environ.get('FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS', '300')
|
|
)
|
|
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.
|
|
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
|
|
# https://docs.openwebui.com/license.
|
|
EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None)
|
|
|
|
####################################
|
|
# 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
|
|
####################################
|
|
|
|
ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
|
|
|
|
MODELS_CACHE_TTL = os.getenv('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.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
|
|
)
|
|
ENABLE_API_OUTLET_FILTERS = os.getenv('ENABLE_API_OUTLET_FILTERS', 'True').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.getenv('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true'
|
|
)
|
|
|
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('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
|
|
|
|
|
|
# Maximum tool-call iterations per chat response. Set to -1 for unlimited.
|
|
# The old CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES name is accepted as a fallback.
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = os.getenv(
|
|
'CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS',
|
|
os.getenv('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '256'),
|
|
)
|
|
|
|
if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == '':
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
|
|
else:
|
|
try:
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = int(CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS)
|
|
except Exception:
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
|
|
|
|
# -1 means unlimited (no cap).
|
|
if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == -1:
|
|
CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = None
|
|
|
|
|
|
# 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.getenv('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true'
|
|
|
|
|
|
CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('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
|
|
|
|
|
|
####################################
|
|
# SENTENCE TRANSFORMERS
|
|
####################################
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '')
|
|
if SENTENCE_TRANSFORMERS_BACKEND == '':
|
|
SENTENCE_TRANSFORMERS_BACKEND = 'torch'
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.getenv('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.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '')
|
|
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '':
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch'
|
|
|
|
|
|
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.getenv('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.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
|
|
)
|
|
|
|
####################################
|
|
# KNOWLEDGE TOOLS
|
|
####################################
|
|
|
|
|
|
def _int_env(name: str, default: int) -> int:
|
|
try:
|
|
return max(int(os.getenv(name) or default), 1)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
|
|
# Total output of a single kb_exec call, whatever the command.
|
|
KB_EXEC_MAX_OUTPUT_CHARS = _int_env('KB_EXEC_MAX_OUTPUT_CHARS', 30_000)
|
|
# Files a single kb_exec grep may scan before it asks for a narrower scope.
|
|
KB_EXEC_MAX_GREP_FILES = _int_env('KB_EXEC_MAX_GREP_FILES', 200)
|
|
# Matching lines returned by kb_exec grep and grep_knowledge_files.
|
|
KNOWLEDGE_GREP_MAX_MATCHES = _int_env('KNOWLEDGE_GREP_MAX_MATCHES', 50)
|
|
# Characters returned by view_file / view_knowledge_file.
|
|
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
|
|
####################################
|
|
|
|
ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true'
|
|
|
|
ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
|
|
os.getenv('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()
|
|
|
|
|
|
####################################
|
|
# OFFLINE_MODE
|
|
####################################
|
|
|
|
ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
|
|
OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true'
|
|
|
|
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'
|
|
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.getenv('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 = [
|
|
path
|
|
for path in (
|
|
path.strip().lstrip('/') for path in os.getenv('AUDIT_EXCLUDED_PATHS', '/chats,/chat,/folders').split(',')
|
|
)
|
|
if path
|
|
]
|
|
|
|
# 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 = [
|
|
path for path in (path.strip().lstrip('/') for path in os.getenv('AUDIT_INCLUDED_PATHS', '').split(',')) 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.getenv('ENABLE_OTEL', 'False').lower() == 'true'
|
|
ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true'
|
|
ENABLE_OTEL_METRICS = os.getenv('ENABLE_OTEL_METRICS', 'False').lower() == 'true'
|
|
ENABLE_OTEL_LOGS = os.getenv('ENABLE_OTEL_LOGS', 'False').lower() == 'true'
|
|
|
|
OTEL_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317')
|
|
OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
OTEL_EXPORTER_OTLP_INSECURE = os.getenv('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true'
|
|
OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
|
|
os.getenv('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
|
)
|
|
OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
|
|
os.getenv('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
|
|
)
|
|
OTEL_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', 'open-webui')
|
|
OTEL_RESOURCE_ATTRIBUTES = os.getenv('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2
|
|
OTEL_TRACES_SAMPLER = os.getenv('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower()
|
|
OTEL_BASIC_AUTH_USERNAME = os.getenv('OTEL_BASIC_AUTH_USERNAME', '')
|
|
OTEL_BASIC_AUTH_PASSWORD = os.getenv('OTEL_BASIC_AUTH_PASSWORD', '')
|
|
OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.getenv('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000'))
|
|
|
|
OTEL_METRICS_BASIC_AUTH_USERNAME = os.getenv('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
|
OTEL_METRICS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
|
OTEL_LOGS_BASIC_AUTH_USERNAME = os.getenv('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
|
|
OTEL_LOGS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
|
|
|
|
OTEL_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http
|
|
|
|
OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv(
|
|
'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
|
).lower() # grpc or http
|
|
|
|
OTEL_LOGS_OTLP_SPAN_EXPORTER = os.getenv(
|
|
'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
|
|
).lower() # grpc or http
|