Coverage for open_webui/env.py: 64%
620 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
1import datetime as dt
2import importlib.metadata
3import json
4import logging
5import os
6import pkgutil
7import re
8import shutil
9import sys
10import threading
11import traceback
12from contextlib import nullcontext
13from pathlib import Path
14from typing import Any, Optional
15from uuid import uuid4
17import markdown
18from bs4 import BeautifulSoup
19from cryptography.hazmat.primitives import serialization
21####################################
22# Load .env file
23####################################
25# Use .resolve() to get the canonical path, removing any '..' or '.' components
26ENV_FILE_PATH = Path(__file__).resolve()
28# OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/)
29OPEN_WEBUI_DIR = ENV_FILE_PATH.parent
31# BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/)
32BACKEND_DIR = OPEN_WEBUI_DIR.parent
34# BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/)
35BASE_DIR = BACKEND_DIR.parent
37try:
38 from dotenv import find_dotenv, load_dotenv
40 load_dotenv(find_dotenv(str(BASE_DIR / '.env')))
41except ImportError:
42 print('dotenv not installed, skipping...')
44DOCKER = os.getenv('DOCKER', 'False').lower() == 'true'
45USE_SLIM = os.getenv('USE_SLIM_DOCKER', 'False').lower() == 'true'
47USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false')
48DEVICE_TYPE = 'cpu'
49_cuda_error: Optional[str] = None
51if not USE_SLIM and USE_CUDA.lower() == 'true': 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 try:
53 import torch # noqa: E402
55 if not torch.cuda.is_available():
56 raise RuntimeError('CUDA not available')
57 DEVICE_TYPE = 'cuda'
58 except Exception as exc:
59 _cuda_error = f'CUDA unavailable (USE_CUDA_DOCKER=true), falling back to CPU: {exc}'
60 os.environ['USE_CUDA_DOCKER'] = 'false'
61 USE_CUDA = 'false'
63if not USE_SLIM and sys.platform == 'darwin' and DEVICE_TYPE == 'cpu': 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 try:
65 import torch # noqa: E402
67 if torch.backends.mps.is_available() and torch.backends.mps.is_built():
68 DEVICE_TYPE = 'mps'
69 except Exception:
70 pass
72# Torch MPS inference is not thread-safe and a concurrent call kills the whole process.
73MPS_INFERENCE_LOCK = threading.Lock() if DEVICE_TYPE == 'mps' else nullcontext()
75####################################
76# LOGGING
77####################################
79_LEVEL_MAP = {
80 'DEBUG': 'debug',
81 'INFO': 'info',
82 'WARNING': 'warn',
83 'ERROR': 'error',
84 'CRITICAL': 'fatal',
85}
88class JSONFormatter(logging.Formatter):
89 """Format log records as single-line JSON objects for structured logging."""
91 def format(self, record: logging.LogRecord) -> str:
92 log_entry: dict[str, Any] = {
93 'ts': dt.datetime.fromtimestamp(record.created, tz=dt.UTC).isoformat(timespec='milliseconds'),
94 'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
95 'msg': record.getMessage(),
96 'caller': record.name,
97 }
99 if record.exc_info and record.exc_info[0] is not None:
100 log_entry['error'] = ''.join(traceback.format_exception(*record.exc_info)).rstrip()
101 elif record.exc_text:
102 log_entry['error'] = record.exc_text
104 if record.stack_info:
105 log_entry['stacktrace'] = record.stack_info
107 return json.dumps(log_entry, ensure_ascii=False, default=str)
110LOG_FORMAT = os.getenv('LOG_FORMAT', '').lower()
111LOGURU_DIAGNOSE = os.getenv('LOGURU_DIAGNOSE', 'False').lower() == 'true'
113GLOBAL_LOG_LEVEL = os.getenv('GLOBAL_LOG_LEVEL', '').upper()
114if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping(): 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 _log_cfg: dict[str, Any] = {'level': GLOBAL_LOG_LEVEL, 'force': True}
116 if LOG_FORMAT == 'json':
117 _json_handler = logging.StreamHandler(sys.stdout)
118 _json_handler.setFormatter(JSONFormatter())
119 _log_cfg['handlers'] = [_json_handler]
120 else:
121 _log_cfg['stream'] = sys.stdout
122 logging.basicConfig(**_log_cfg)
123else:
124 GLOBAL_LOG_LEVEL = 'INFO'
126log = logging.getLogger(__name__)
127log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL)
129if _cuda_error: 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 log.error(_cuda_error)
131 _cuda_error = None
133SRC_LOG_LEVELS = {} # Legacy variable, do not remove
135####################################
136# ENV (dev,test,prod)
137####################################
139ENV = os.getenv('ENV', 'dev')
141FROM_INIT_PY = os.getenv('FROM_INIT_PY', 'False').lower() == 'true'
143if FROM_INIT_PY: 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true
144 PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')}
145else:
146 try:
147 PACKAGE_DATA = json.loads((BASE_DIR / 'package.json').read_text())
148 except Exception:
149 PACKAGE_DATA = {'version': '0.0.0'}
151VERSION = PACKAGE_DATA['version']
154DEPLOYMENT_ID = os.getenv('DEPLOYMENT_ID', '')
155INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4()))
157ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true'
159# Swap the JSON encoder/decoder used across the app (HTTP request bodies, JSONResponse
160# bodies, upstream provider responses, socket.io payloads) from the stdlib `json` module
161# to orjson. Faster, but stricter: see open_webui/utils/json_codec.py for the differences.
162ENABLE_ORJSON = os.getenv('ENABLE_ORJSON', 'False').lower() == 'true'
165# Function to parse each section
166def parse_section(section):
167 items = []
168 for li in section.find_all('li'):
169 # Extract raw HTML string
170 raw_html = str(li)
172 # Extract text without HTML tags
173 text = li.get_text(separator=' ', strip=True)
175 # Split into title and content
176 parts = text.split(': ', 1)
177 title = parts[0].strip() if len(parts) > 1 else ''
178 content = parts[1].strip() if len(parts) > 1 else text
180 items.append({'title': title, 'content': content, 'raw': raw_html})
181 return items
184try:
185 changelog_path = BASE_DIR / 'CHANGELOG.md'
186 with open(str(changelog_path.absolute()), encoding='utf8') as file:
187 changelog_content = file.read()
189except Exception:
190 changelog_content = (pkgutil.get_data('open_webui', 'CHANGELOG.md') or b'').decode()
192# Convert markdown content to HTML
193html_content = markdown.markdown(changelog_content)
195# Parse the HTML content
196soup = BeautifulSoup(html_content, 'html.parser')
198# Initialize JSON structure
199changelog_json = {}
201# Iterate over each version
202for version in soup.find_all('h2'):
203 version_number = version.get_text().strip().split(' - ')[0][1:-1] # Remove brackets
204 date = version.get_text().strip().split(' - ')[1]
206 version_data = {'date': date}
208 # Find the next sibling that is a h3 tag (section title)
209 current = version.find_next_sibling()
211 while current and current.name != 'h2':
212 if current.name == 'h3':
213 section_title = current.get_text().lower() # e.g., "added", "fixed"
214 section_items = parse_section(current.find_next_sibling('ul'))
215 version_data[section_title] = section_items
217 # Move to the next element
218 current = current.find_next_sibling()
220 changelog_json[version_number] = version_data
222CHANGELOG = changelog_json
224####################################
225# DATA/FRONTEND BUILD DIR
226####################################
228DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve()
230if FROM_INIT_PY: 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 NEW_DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')).resolve()
232 NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
234 # Check if the data directory exists in the package directory
235 if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
236 log.info('Moving %s to %s', DATA_DIR, NEW_DATA_DIR)
237 for item in DATA_DIR.iterdir():
238 dest = NEW_DATA_DIR / item.name
239 if item.is_dir():
240 shutil.copytree(item, dest, dirs_exist_ok=True)
241 else:
242 shutil.copy2(item, dest)
244 # Zip the data directory
245 shutil.make_archive(DATA_DIR.parent / 'open_webui_data', 'zip', DATA_DIR)
247 # Remove the old data directory
248 shutil.rmtree(DATA_DIR)
250 DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data'))
252STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static'))
254FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).resolve()
256if FROM_INIT_PY: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true
257 FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', OPEN_WEBUI_DIR / 'frontend')).resolve()
259####################################
260# Database
261####################################
263# Check if the file exists
264if os.path.exists(f'{DATA_DIR}/ollama.db'): 264 ↛ 266line 264 didn't jump to line 266 because the condition on line 264 was never true
265 # Rename the file
266 os.rename(f'{DATA_DIR}/ollama.db', f'{DATA_DIR}/webui.db')
267 log.info('Database migrated from Ollama-WebUI successfully.')
268else:
269 pass
271DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db')
273DATABASE_TYPE = os.getenv('DATABASE_TYPE')
274DATABASE_USER = os.getenv('DATABASE_USER')
275DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD')
277DATABASE_CRED = ''
278if DATABASE_USER: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 DATABASE_CRED += f'{DATABASE_USER}'
280if DATABASE_PASSWORD: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true
281 DATABASE_CRED += f':{DATABASE_PASSWORD}'
283DB_VARS = {
284 'db_type': DATABASE_TYPE,
285 'db_cred': DATABASE_CRED,
286 'db_host': os.getenv('DATABASE_HOST'),
287 'db_port': os.getenv('DATABASE_PORT'),
288 'db_name': os.getenv('DATABASE_NAME'),
289}
291if all(DB_VARS.values()): 291 ↛ 292line 291 didn't jump to line 292 because the condition on line 291 was never true
292 DATABASE_URL = (
293 f'{DB_VARS["db_type"]}://{DB_VARS["db_cred"]}@{DB_VARS["db_host"]}:{DB_VARS["db_port"]}/{DB_VARS["db_name"]}'
294 )
295elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.getenv('DATABASE_URL'): 295 ↛ 297line 295 didn't jump to line 297 because the condition on line 295 was never true
296 # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set
297 DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db'
299# Replace the postgres:// with postgresql://
300if 'postgres://' in DATABASE_URL: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true
301 DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://')
303DATABASE_SCHEMA = os.getenv('DATABASE_SCHEMA', None)
304DATABASE_ENABLE_IAM_TOKEN_AUTH = os.getenv('DATABASE_ENABLE_IAM_TOKEN_AUTH', 'False').lower() == 'true'
306_pool_size_raw = os.getenv('DATABASE_POOL_SIZE')
307try:
308 DATABASE_POOL_SIZE = int(_pool_size_raw) if _pool_size_raw else None
309except (ValueError, TypeError):
310 DATABASE_POOL_SIZE = None
312_pool_overflow_raw = os.getenv('DATABASE_POOL_MAX_OVERFLOW', '0')
313try:
314 DATABASE_POOL_MAX_OVERFLOW = int(_pool_overflow_raw) if _pool_overflow_raw else 0
315except (ValueError, TypeError):
316 DATABASE_POOL_MAX_OVERFLOW = 0
318_pool_timeout_raw = os.getenv('DATABASE_POOL_TIMEOUT', '30')
319try:
320 DATABASE_POOL_TIMEOUT = int(_pool_timeout_raw) if _pool_timeout_raw else 30
321except (ValueError, TypeError):
322 DATABASE_POOL_TIMEOUT = 30
324_pool_recycle_raw = os.getenv('DATABASE_POOL_RECYCLE', '3600')
325try:
326 DATABASE_POOL_RECYCLE = int(_pool_recycle_raw) if _pool_recycle_raw else 3600
327except (ValueError, TypeError):
328 DATABASE_POOL_RECYCLE = 3600
330DATABASE_ENABLE_SQLITE_WAL = os.getenv('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true'
332# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server
333# workloads. Each can be overridden via its environment variable.
334# Set any value to an empty string to skip that PRAGMA entirely.
336# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per
337# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3).
338DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.getenv('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL')
340# PRAGMA busy_timeout (ms): how long a connection waits for a write lock
341# before raising SQLITE_BUSY.
342DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.getenv('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000')
344# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache.
345DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536')
347# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM.
348# Valid values: DEFAULT (0), FILE (1), MEMORY (2).
349DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.getenv('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY')
351# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB.
352# Set to 0 to disable mmap.
353DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456')
355# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint.
356# Without this the WAL grows unbounded during write bursts and is never
357# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default).
358DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.getenv('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864')
360# Seconds between presence writes per user per worker; keep under the 180s active-user window. 0 disables.
361try:
362 DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(os.getenv('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', '60'))
363except ValueError:
364 DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 60.0
366DATABASE_ENABLE_SESSION_SHARING = os.getenv('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true'
367ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.getenv('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true'
368RESET_CONFIG_ON_START = os.getenv('RESET_CONFIG_ON_START', 'False').lower() == 'true'
369ENABLE_REALTIME_CHAT_SAVE = os.getenv('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true'
370ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'true'
371ENABLE_ADMIN_CHAT_ACCESS = os.getenv('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true'
372RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true'
374# Empty by default: chunk metadata also holds internal bookkeeping (file hashes, collection names, scores).
375RAG_SOURCE_METADATA_KEYS = [key.strip() for key in os.getenv('RAG_SOURCE_METADATA_KEYS', '').split(',') if key.strip()]
377####################################
378# REDIS
379####################################
381REDIS_URL = os.getenv('REDIS_URL', '')
382REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true'
384REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui')
386try:
387 REDIS_RESPONSE_STREAM_TTL = int(os.getenv('REDIS_RESPONSE_STREAM_TTL', '3600'))
388except ValueError:
389 REDIS_RESPONSE_STREAM_TTL = 3600
391# Seconds a task survives without a heartbeat. 0 disables expiry.
392try:
393 REDIS_TASK_TTL = int(os.getenv('REDIS_TASK_TTL', '300'))
394 if REDIS_TASK_TTL != 0 and REDIS_TASK_TTL < 60: 394 ↛ 395line 394 didn't jump to line 395 because the condition on line 394 was never true
395 REDIS_TASK_TTL = 300
396except ValueError:
397 REDIS_TASK_TTL = 300
399REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '')
400REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379')
402# Maximum number of retries for Redis operations when using Sentinel fail-over
403REDIS_SENTINEL_MAX_RETRY_COUNT = os.getenv('REDIS_SENTINEL_MAX_RETRY_COUNT', '2')
404try:
405 REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
406 if REDIS_SENTINEL_MAX_RETRY_COUNT < 1: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true
407 REDIS_SENTINEL_MAX_RETRY_COUNT = 2
408except ValueError:
409 REDIS_SENTINEL_MAX_RETRY_COUNT = 2
412REDIS_SOCKET_CONNECT_TIMEOUT = os.getenv('REDIS_SOCKET_CONNECT_TIMEOUT', '')
413try:
414 REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
415except ValueError:
416 REDIS_SOCKET_CONNECT_TIMEOUT = None
418REDIS_SOCKET_TIMEOUT = os.getenv('REDIS_SOCKET_TIMEOUT', '')
419try:
420 REDIS_SOCKET_TIMEOUT = float(REDIS_SOCKET_TIMEOUT)
421except ValueError:
422 REDIS_SOCKET_TIMEOUT = None
424# Whether to enable TCP SO_KEEPALIVE on Redis client sockets. Opt-in:
425# defaults to off so behavior is unchanged for existing deployments. When
426# enabled, the kernel sends TCP keepalive probes on idle connections so
427# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC
428# flap) are detected before the next command lands on them.
429REDIS_SOCKET_KEEPALIVE = os.getenv('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true'
431# How often (in seconds) redis-py should PING an idle pooled connection
432# before reusing it. Opt-in: defaults to unset (empty string) so behavior
433# is unchanged for existing deployments. When set, should be shorter than
434# the Redis server `timeout` setting and any firewall/LB idle timeout on
435# the path to Redis, so stale connections are detected before a real
436# command lands on them. Set to 0 or empty to disable.
437REDIS_HEALTH_CHECK_INTERVAL = os.getenv('REDIS_HEALTH_CHECK_INTERVAL', '')
438try:
439 REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL)
440 if REDIS_HEALTH_CHECK_INTERVAL <= 0:
441 REDIS_HEALTH_CHECK_INTERVAL = None
442except ValueError:
443 REDIS_HEALTH_CHECK_INTERVAL = None
445REDIS_RECONNECT_DELAY = os.getenv('REDIS_RECONNECT_DELAY', '')
447if REDIS_RECONNECT_DELAY == '': 447 ↛ 450line 447 didn't jump to line 450 because the condition on line 447 was always true
448 REDIS_RECONNECT_DELAY = None
449else:
450 try:
451 REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
452 if REDIS_RECONNECT_DELAY < 0:
453 REDIS_RECONNECT_DELAY = None
454 except Exception:
455 REDIS_RECONNECT_DELAY = None
457####################################
458# Uvicorn
459####################################
461try:
462 UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1)
463except (ValueError, TypeError):
464 UVICORN_WORKERS = 1
466# tiny delta-stream frames make per-frame websocket compression CPU-bound, allow opting out (true/false)
467UVICORN_WS_PER_MESSAGE_DEFLATE = os.getenv('UVICORN_WS_PER_MESSAGE_DEFLATE', 'True').lower() == 'true'
469####################################
470# WEBSOCKET SUPPORT
471####################################
473ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true'
476WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '')
478WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '')
481if WEBSOCKET_REDIS_OPTIONS == '': 481 ↛ 486line 481 didn't jump to line 486 because the condition on line 481 was always true
482 WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
483 if REDIS_SOCKET_CONNECT_TIMEOUT: 483 ↛ 484line 483 didn't jump to line 484 because the condition on line 483 was never true
484 WEBSOCKET_REDIS_OPTIONS['socket_connect_timeout'] = REDIS_SOCKET_CONNECT_TIMEOUT
485else:
486 try:
487 WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS)
488 WEBSOCKET_REDIS_OPTIONS.setdefault('socket_timeout', None)
489 except Exception:
490 log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to socket_timeout=None')
491 WEBSOCKET_REDIS_OPTIONS = {'socket_timeout': None}
493WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL)
494WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true'
496websocket_redis_lock_timeout = os.getenv('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60')
498try:
499 WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
500except ValueError:
501 WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
503WEBSOCKET_SENTINEL_HOSTS = os.getenv('WEBSOCKET_SENTINEL_HOSTS', '')
504WEBSOCKET_SENTINEL_PORT = os.getenv('WEBSOCKET_SENTINEL_PORT', '26379')
505WEBSOCKET_SERVER_LOGGING = os.getenv('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true'
506WEBSOCKET_SERVER_ENGINEIO_LOGGING = (
507 os.getenv(
508 'WEBSOCKET_SERVER_ENGINEIO_LOGGING',
509 os.getenv('WEBSOCKET_SERVER_LOGGING', 'False'),
510 ).lower()
511 == 'true'
512)
513WEBSOCKET_SERVER_PING_TIMEOUT = os.getenv('WEBSOCKET_SERVER_PING_TIMEOUT', '20')
514try:
515 WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT)
516except ValueError:
517 WEBSOCKET_SERVER_PING_TIMEOUT = 20
519WEBSOCKET_SERVER_PING_INTERVAL = os.getenv('WEBSOCKET_SERVER_PING_INTERVAL', '25')
520try:
521 WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL)
522except ValueError:
523 WEBSOCKET_SERVER_PING_INTERVAL = 25
525WEBSOCKET_HEARTBEAT_INTERVAL = os.getenv('WEBSOCKET_HEARTBEAT_INTERVAL', '')
526if WEBSOCKET_HEARTBEAT_INTERVAL == '': 526 ↛ 529line 526 didn't jump to line 529 because the condition on line 526 was always true
527 WEBSOCKET_HEARTBEAT_INTERVAL = None
528else:
529 try:
530 WEBSOCKET_HEARTBEAT_INTERVAL = min(max(int(WEBSOCKET_HEARTBEAT_INTERVAL), 5), 90)
531 except ValueError:
532 WEBSOCKET_HEARTBEAT_INTERVAL = 30
534WEBSOCKET_EVENT_CALLER_TIMEOUT = os.getenv('WEBSOCKET_EVENT_CALLER_TIMEOUT', '')
536if WEBSOCKET_EVENT_CALLER_TIMEOUT == '': 536 ↛ 539line 536 didn't jump to line 539 because the condition on line 536 was always true
537 WEBSOCKET_EVENT_CALLER_TIMEOUT = None
538else:
539 try:
540 WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT)
541 except ValueError:
542 WEBSOCKET_EVENT_CALLER_TIMEOUT = 300
545import ssl as _ssl
548# Dedicated env var for a custom CA bundle file path. When set, this is
549# used as the default CA bundle for all outbound HTTPS connections that
550# have SSL verification enabled (i.e. when their per-connection SSL env
551# var is ``"True"``). Per-connection overrides (setting the SSL env var
552# to a path directly) take precedence over this global fallback.
553#
554# This follows the industry convention of ``SSL_CERT_FILE`` / ``REQUESTS_CA_BUNDLE``
555# but is scoped to Open WebUI to avoid interfering with system-level settings.
556AIOHTTP_CLIENT_SSL_CERT_FILE = os.getenv('AIOHTTP_CLIENT_SSL_CERT_FILE', '').strip()
559def _build_ssl_context_from_file(path: str) -> '_ssl.SSLContext | None':
560 """Create an SSLContext from a CA bundle file, or None if invalid."""
561 if not path: 561 ↛ 563line 561 didn't jump to line 563 because the condition on line 561 was always true
562 return None
563 if not os.path.isfile(path):
564 log.warning(
565 'SSL CA bundle path does not exist: %r, ignoring',
566 path,
567 )
568 return None
569 ctx = _ssl.create_default_context(cafile=path)
570 log.info('Using custom SSL CA bundle: %s', path)
571 return ctx
574# Pre-built SSLContext from the dedicated env var (cached once at startup).
575_GLOBAL_SSL_CONTEXT = _build_ssl_context_from_file(AIOHTTP_CLIENT_SSL_CERT_FILE)
578def _parse_ssl_env(value: str) -> 'bool | _ssl.SSLContext':
579 """Parse an SSL env var into a bool or SSLContext.
581 - ``"true"`` → uses ``AIOHTTP_CLIENT_SSL_CERT_FILE`` context if set,
582 otherwise ``True`` (default SSL verification via certifi)
583 - ``"false"`` → ``False`` (no verification)
584 - ``"/path/to/ca-bundle.crt"`` → ``SSLContext`` loading that CA file
585 (takes precedence over ``AIOHTTP_CLIENT_SSL_CERT_FILE``)
587 This allows users with corporate or internal CAs to point Open WebUI
588 at a custom CA bundle without disabling verification entirely.
589 """
590 lower = value.strip().lower()
591 if lower == 'true': 591 ↛ 594line 591 didn't jump to line 594 because the condition on line 591 was always true
592 # Use the global dedicated CA bundle if configured, otherwise default
593 return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
594 if lower == 'false':
595 return False
596 # Treat as a file path to a CA bundle (per-connection override)
597 ctx = _build_ssl_context_from_file(value.strip())
598 if ctx is not None:
599 return ctx
600 # Path was invalid — fall back to default
601 return _GLOBAL_SSL_CONTEXT if _GLOBAL_SSL_CONTEXT is not None else True
604REQUESTS_VERIFY = os.getenv('REQUESTS_VERIFY', 'True').lower() == 'true'
606TAVILY_API_BASE_URL = os.getenv('TAVILY_API_BASE_URL', 'https://api.tavily.com').rstrip('/')
608_aiohttp_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT', '')
609try:
610 AIOHTTP_CLIENT_TIMEOUT = int(_aiohttp_timeout_raw) if _aiohttp_timeout_raw else None
611except (ValueError, TypeError):
612 AIOHTTP_CLIENT_TIMEOUT = 300
614# Optional between-chunks idle cap for streaming aiohttp requests.
615AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = os.getenv('AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT', '')
616if AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT == '': 616 ↛ 619line 616 didn't jump to line 619 because the condition on line 616 was always true
617 AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
618else:
619 try:
620 AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = int(AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT)
621 except (ValueError, TypeError):
622 AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
624if AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT is not None and AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT <= 0: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true
625 AIOHTTP_CLIENT_STREAM_IDLE_TIMEOUT = None
628# SSL verification for general outbound requests (OpenAI, OAuth, etc.).
629# Accepts "True", "False", or a path to a CA bundle file.
630# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
631AIOHTTP_CLIENT_SESSION_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_SSL', 'True'))
633SEARXNG_CLIENT_CERT_FILE = os.getenv('SEARXNG_CLIENT_CERT_FILE', '').strip()
634SEARXNG_CLIENT_KEY_FILE = os.getenv('SEARXNG_CLIENT_KEY_FILE', '').strip()
636# When False (default), outbound HTTP requests do not follow 3xx redirects.
637AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.getenv('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true'
639# Opt-in c-ares DNS resolution (aiodns). Off by default: c-ares breaks name
640# resolution in some environments (#28013, #28215). Must run before any
641# TCPConnector is constructed.
642AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER = os.getenv('AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER', 'False').lower() == 'true'
644if not AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER: 644 ↛ 654line 644 didn't jump to line 654 because the condition on line 644 was always true
645 import aiohttp
647 aiohttp.DefaultResolver = aiohttp.resolver.ThreadedResolver # for plugin code
648 aiohttp.resolver.DefaultResolver = aiohttp.resolver.ThreadedResolver
649 aiohttp.connector.DefaultResolver = aiohttp.resolver.ThreadedResolver
651# Optional User-Agent override for outbound web-loader fetches. When set,
652# SafeWebBaseLoader sends this value instead of the default python-requests UA
653# which is aggressively blocked by Cloudflare, Wikipedia, and similar services.
654USER_AGENT = os.getenv('USER_AGENT', '')
656_model_list_timeout_raw = os.getenv(
657 'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
658 os.getenv('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
659)
660try:
661 AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(_model_list_timeout_raw) if _model_list_timeout_raw else None
662except (ValueError, TypeError):
663 AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
665_tool_data_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10')
666try:
667 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(_tool_data_timeout_raw) if _tool_data_timeout_raw else None
668except (ValueError, TypeError):
669 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
671AIOHTTP_FILE_STREAM_CHUNK_SIZE = os.getenv('AIOHTTP_FILE_STREAM_CHUNK_SIZE', str(1024 * 1024))
672try:
673 AIOHTTP_FILE_STREAM_CHUNK_SIZE = int(AIOHTTP_FILE_STREAM_CHUNK_SIZE)
674except Exception:
675 AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
677if AIOHTTP_FILE_STREAM_CHUNK_SIZE <= 0: 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
681# SSL verification for tool server connections specifically.
682# Accepts "True", "False", or a path to a CA bundle file.
683# When "True", falls back to AIOHTTP_CLIENT_SSL_CERT_FILE if set.
684AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = _parse_ssl_env(os.getenv('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True'))
686AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '')
688if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '': 688 ↛ 691line 688 didn't jump to line 691 because the condition on line 688 was always true
689 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
690else:
691 try:
692 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER)
693 except Exception:
694 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT
696# Timeout (in seconds) for the MCP session.initialize() handshake.
697# The handshake performs a list-tools round-trip and can take tens of
698# seconds on cold-start servers or servers exposing many tools.
699MCP_INITIALIZE_TIMEOUT = os.getenv('MCP_INITIALIZE_TIMEOUT', '10')
700try:
701 MCP_INITIALIZE_TIMEOUT = int(MCP_INITIALIZE_TIMEOUT)
702except (ValueError, TypeError):
703 MCP_INITIALIZE_TIMEOUT = 10
706####################################
707# AIOHTTP Connection Pool
708####################################
710AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '')
711if AIOHTTP_POOL_CONNECTIONS == '': 711 ↛ 714line 711 didn't jump to line 714 because the condition on line 711 was always true
712 AIOHTTP_POOL_CONNECTIONS = None
713else:
714 try:
715 AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS)
716 except ValueError:
717 AIOHTTP_POOL_CONNECTIONS = None
719AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.getenv('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '')
720if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '': 720 ↛ 723line 720 didn't jump to line 723 because the condition on line 720 was always true
721 AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
722else:
723 try:
724 AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST)
725 except ValueError:
726 AIOHTTP_POOL_CONNECTIONS_PER_HOST = None
728AIOHTTP_POOL_DNS_TTL = os.getenv('AIOHTTP_POOL_DNS_TTL', '300')
729try:
730 AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL)
731 if AIOHTTP_POOL_DNS_TTL < 0: 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true
732 AIOHTTP_POOL_DNS_TTL = 300
733except ValueError:
734 AIOHTTP_POOL_DNS_TTL = 300
736RAG_EMBEDDING_TIMEOUT = os.getenv('RAG_EMBEDDING_TIMEOUT', '')
738if RAG_EMBEDDING_TIMEOUT == '': 738 ↛ 741line 738 didn't jump to line 741 because the condition on line 738 was always true
739 RAG_EMBEDDING_TIMEOUT = None
740else:
741 try:
742 RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT)
743 except Exception:
744 RAG_EMBEDDING_TIMEOUT = None
747####################################
748# Auth
749####################################
751WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true'
753ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true'
754ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true'
756####################################
757# Secret key & cookies
758####################################
760# WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead.
761# No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below.
762WEBUI_SECRET_KEY = os.getenv(
763 'WEBUI_SECRET_KEY',
764 os.getenv('WEBUI_JWT_SECRET_KEY', ''),
765)
767ENABLE_VALVE_ENCRYPTION = os.getenv('ENABLE_VALVE_ENCRYPTION', 'False').lower() == 'true'
769WEBUI_SESSION_COOKIE_SAME_SITE = os.getenv('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax')
770WEBUI_SESSION_COOKIE_SECURE = os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
771WEBUI_AUTH_COOKIE_SAME_SITE = os.getenv('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE)
772WEBUI_AUTH_COOKIE_SECURE = (
773 os.getenv(
774 'WEBUI_AUTH_COOKIE_SECURE',
775 os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false'),
776 ).lower()
777 == 'true'
778)
780if WEBUI_AUTH and WEBUI_SECRET_KEY == '': 780 ↛ 781line 780 didn't jump to line 781 because the condition on line 780 was never true
781 raise SystemExit(
782 'WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.\n'
783 'The supported start methods set or auto-generate it for you: use start.sh (Linux/macOS), '
784 'start_windows.bat (Windows), or `open-webui serve`.\n'
785 'If you start the backend another way (e.g. invoking uvicorn directly, which is unsupported), '
786 'you must set WEBUI_SECRET_KEY yourself to a long random value.\n'
787 'See https://docs.openwebui.com/reference/env-configuration#webui_secret_key'
788 )
790ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true'
792####################################
793# Admin Account Runtime Creation
794####################################
796# Optional env vars for creating an admin account on startup
797# Useful for headless/automated deployments
798WEBUI_ADMIN_EMAIL = os.getenv('WEBUI_ADMIN_EMAIL', '')
799WEBUI_ADMIN_PASSWORD = os.getenv('WEBUI_ADMIN_PASSWORD', '')
800WEBUI_ADMIN_NAME = os.getenv('WEBUI_ADMIN_NAME', 'Admin')
802WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None)
803WEBUI_AUTH_TRUSTED_NAME_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_NAME_HEADER', None)
804WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None)
805WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None)
807# Custom header name for API key authentication. Defaults to 'x-api-key'.
808# Useful when Open WebUI sits behind a reverse proxy / API gateway that
809# already uses the Authorization header for its own authentication — set
810# this to a unique header (e.g. 'X-OpenWebUI-Key') so the middleware
811# checks the custom header instead and avoids the 401 short-circuit.
812CUSTOM_API_KEY_HEADER = os.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key')
814ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true'
815PASSWORD_HASH_ALGORITHM = os.getenv('PASSWORD_HASH_ALGORITHM', 'bcrypt').lower()
816PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv(
817 'PASSWORD_VALIDATION_REGEX_PATTERN',
818 r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$',
819)
822try:
823 PASSWORD_VALIDATION_REGEX_PATTERN = rf'{PASSWORD_VALIDATION_REGEX_PATTERN}'
824 PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN)
825except Exception as e:
826 log.error(f'Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}')
827 PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$')
829PASSWORD_VALIDATION_HINT = os.getenv('PASSWORD_VALIDATION_HINT', '')
832BYPASS_MODEL_ACCESS_CONTROL = os.getenv('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true'
833BYPASS_RETRIEVAL_ACCESS_CONTROL = os.getenv('BYPASS_RETRIEVAL_ACCESS_CONTROL', 'False').lower() == 'true'
835# When True, collection names that do not match any known file-*, user-memory-*,
836# web-search-*, or knowledge-base collection are allowed through access control
837# for non-admin users. When False (default), unknown collection names are
838# denied — closing the legacy unscoped namespace.
839ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS = os.getenv('ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS', 'False').lower() == 'true'
841# Falls back to the upload size limit, because a document cannot legitimately carry more metadata
842# than the file itself is allowed to be. Left unbounded, a small archive that expands enormously
843# during extraction can exhaust memory. RAG_FILE_MAX_SIZE is in MB.
844RAG_METADATA_MAX_VALUE_CHARS = (
845 int(os.getenv('RAG_METADATA_MAX_VALUE_CHARS'))
846 if os.getenv('RAG_METADATA_MAX_VALUE_CHARS')
847 else ((int(os.getenv('RAG_FILE_MAX_SIZE', '0')) or 0) * 1024 * 1024 or None)
848)
850MINERU_MAX_MARKDOWN_BYTES = (
851 int(os.getenv('MINERU_MAX_MARKDOWN_BYTES')) if os.getenv('MINERU_MAX_MARKDOWN_BYTES') else None
852)
854# When enabled, skips pydub-based preprocessing (format conversion, compression,
855# and chunked splitting) before sending files to processing engines. Useful when
856# the upstream provider handles these steps or when ffmpeg is unavailable.
857BYPASS_PYDUB_PREPROCESSING = USE_SLIM or os.getenv('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true'
859# When disabled (default), the OpenAI catch-all proxy endpoint (/{path:path})
860# is blocked. Enable only if you need direct passthrough to upstream OpenAI-
861# compatible APIs for endpoints not natively handled by Open WebUI.
862ENABLE_OPENAI_API_PASSTHROUGH = os.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true'
864WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None)
866####################################
867# OAUTH Configuration
868####################################
869ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true'
871ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true'
873OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
875OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY)
877# Maximum number of concurrent OAuth sessions per user per provider
878# This prevents unbounded session growth while allowing multi-device usage
879OAUTH_MAX_SESSIONS_PER_USER = int(os.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10'))
881# Token Exchange Configuration
882# Allows external apps to exchange OAuth tokens for OpenWebUI tokens
883ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true'
884_oauth_token_exchange_rate_limit = (os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT') or '').strip()
885OAUTH_TOKEN_EXCHANGE_RATE_LIMIT = (
886 int(_oauth_token_exchange_rate_limit)
887 if _oauth_token_exchange_rate_limit and _oauth_token_exchange_rate_limit.lower() != 'none'
888 else None
889)
890OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW = int(os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW', str(60 * 3)))
891OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS = [
892 client_id.strip()
893 for client_id in os.getenv('OAUTH_TOKEN_EXCHANGE_TRUSTED_CLIENT_IDS', '').split(',')
894 if client_id.strip()
895]
897# Back-Channel Logout Configuration
898# When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout
899# per OpenID Connect Back-Channel Logout 1.0 spec.
900# Requires Redis for JWT revocation.
901ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true'
903####################################
904# SCIM Configuration
905####################################
907ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true'
908SCIM_TOKEN = os.getenv('SCIM_TOKEN', '')
909SCIM_AUTH_PROVIDER = os.getenv('SCIM_AUTH_PROVIDER', '')
911if ENABLE_SCIM and not SCIM_AUTH_PROVIDER: 911 ↛ 912line 911 didn't jump to line 912 because the condition on line 911 was never true
912 log.warning(
913 'SCIM is enabled but SCIM_AUTH_PROVIDER is not set. '
914 "Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') "
915 'to enable externalId storage.'
916 )
918####################################
919# LICENSE_KEY
920####################################
922LICENSE_KEY = os.getenv('LICENSE_KEY', '')
924LICENSE_BLOB = None
925LICENSE_BLOB_PATH = os.getenv('LICENSE_BLOB_PATH', DATA_DIR / 'l.data')
926if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH): 926 ↛ 927line 926 didn't jump to line 927 because the condition on line 926 was never true
927 with open(LICENSE_BLOB_PATH, 'rb') as f:
928 LICENSE_BLOB = f.read()
930LICENSE_PUBLIC_KEY = os.getenv('LICENSE_PUBLIC_KEY', '')
932pk = None
933if LICENSE_PUBLIC_KEY: 933 ↛ 934line 933 didn't jump to line 934 because the condition on line 933 was never true
934 pk = serialization.load_pem_public_key(
935 f"""
936-----BEGIN PUBLIC KEY-----
937{LICENSE_PUBLIC_KEY}
938-----END PUBLIC KEY-----
939""".encode()
940 )
943####################################
944# WEBUI Identity
945####################################
947# LICENSE covers this Open WebUI branding surface, including name, logo,
948# visual, textual, symbolic identifiers, metadata, and surrounding UI.
949# Do not alter, remove, obscure, or replace it except as LICENSE permits:
950# https://docs.openwebui.com/license.
951WEBUI_NAME = os.getenv('WEBUI_NAME', 'Open WebUI')
952if WEBUI_NAME != 'Open WebUI': 952 ↛ 953line 952 didn't jump to line 953 because the condition on line 952 was never true
953 WEBUI_NAME += ' (Open WebUI)'
955# LICENSE covers this Open WebUI branding surface, including this favicon
956# and any visual, textual, or symbolic identifiers it preserves.
957# Do not alter, remove, obscure, or replace it except as LICENSE permits:
958# https://docs.openwebui.com/license.
959WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png'
960WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build')
961TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '')
963####################################
964# Feature flags
965####################################
967SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true'
968ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
969ENABLE_STAR_SESSIONS_MIDDLEWARE = os.getenv('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true'
970ENABLE_KB_EXEC = os.getenv('ENABLE_KB_EXEC', 'False').lower() == 'true'
972ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.getenv('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
973PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset(
974 t.strip()
975 for t in os.getenv(
976 'PROFILE_IMAGE_ALLOWED_MIME_TYPES',
977 'image/png,image/jpeg,image/gif,image/webp',
978 ).split(',')
979 if t.strip()
980)
982# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis
983# bloat from inline avatars and model icons. Unset (default) disables the cap.
984_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip()
985PROFILE_IMAGE_MAX_DATA_URI_SIZE = int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None
987####################################
988# Forward Headers
989####################################
991ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true'
993FORWARD_USER_INFO_HEADER_USER_NAME = os.getenv('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name')
994FORWARD_USER_INFO_HEADER_USER_ID = os.getenv('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id')
995FORWARD_USER_INFO_HEADER_USER_EMAIL = os.getenv('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email')
996FORWARD_USER_INFO_HEADER_USER_ROLE = os.getenv('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role')
997FORWARD_USER_INFO_HEADER_AUTH_TYPE = os.getenv('FORWARD_USER_INFO_HEADER_AUTH_TYPE', 'X-OpenWebUI-Auth-Type')
998FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id')
999FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id')
1001# If set while ENABLE_FORWARD_USER_INFO_HEADERS is True, send one signed HS256 JWT
1002# (FORWARD_USER_INFO_HEADER_JWT) instead of separate X-OpenWebUI-User-* headers.
1003FORWARD_USER_INFO_HEADER_JWT_SECRET = (os.environ.get('FORWARD_USER_INFO_HEADER_JWT_SECRET') or '').strip() or None
1004FORWARD_USER_INFO_HEADER_JWT = os.environ.get('FORWARD_USER_INFO_HEADER_JWT', 'X-OpenWebUI-User-Jwt')
1005try:
1006 FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = int(
1007 os.environ.get('FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS', '300')
1008 )
1009except ValueError:
1010 FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = 300
1012####################################
1013# Progressive Web App
1014####################################
1016# LICENSE covers this install-time Open WebUI branding surface, including
1017# names, logos, manifests, metadata, and surrounding UI.
1018# Do not alter, remove, obscure, or replace it except as LICENSE permits:
1019# https://docs.openwebui.com/license.
1020EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None)
1022####################################
1023# GROUP DEFAULTS
1024####################################
1026# Controls the default "Who can share to this group" setting for new groups.
1027# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
1028_default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower()
1029DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true'
1031####################################
1032# MODELS
1033####################################
1035ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true'
1037MODELS_CACHE_TTL = os.getenv('MODELS_CACHE_TTL', '1')
1038if MODELS_CACHE_TTL == '': 1038 ↛ 1039line 1038 didn't jump to line 1039 because the condition on line 1038 was never true
1039 MODELS_CACHE_TTL = None
1040else:
1041 try:
1042 MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
1043 except Exception:
1044 MODELS_CACHE_TTL = 1
1047####################################
1048# CHAT
1049####################################
1051ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
1052 os.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true'
1053)
1054ENABLE_API_OUTLET_FILTERS = os.getenv('ENABLE_API_OUTLET_FILTERS', 'True').lower() == 'true'
1056# Opt in to CPython's in-place string append optimization for streamed responses.
1057# Off by default for a staged rollout. Only a host already out of memory can lose
1058# text here; the default path (a full copy per chunk) raises there too.
1059ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND = (
1060 os.getenv('ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND', 'False').lower() == 'true'
1061)
1063# When enabled, uses a hardcoded extension-to-MIME dictionary as a last-resort
1064# fallback when both mimetypes.guess_type() and file.meta.content_type fail to
1065# determine the content type. This can help on minimal container images (e.g.
1066# wolfi-base) that lack /etc/mime.types AND have legacy files without stored
1067# content_type metadata.
1068ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK = (
1069 os.getenv('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true'
1070)
1072CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1')
1074if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == '': 1074 ↛ 1075line 1074 didn't jump to line 1075 because the condition on line 1074 was never true
1075 CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
1076else:
1077 try:
1078 CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE)
1079 except Exception:
1080 CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
1083# Maximum tool-call iterations per chat response. Set to -1 for unlimited.
1084# The old CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES name is accepted as a fallback.
1085CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = os.getenv(
1086 'CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS',
1087 os.getenv('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '256'),
1088)
1090if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == '': 1090 ↛ 1091line 1090 didn't jump to line 1091 because the condition on line 1090 was never true
1091 CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
1092else:
1093 try:
1094 CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = int(CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS)
1095 except Exception:
1096 CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256
1098# -1 means unlimited (no cap).
1099if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == -1: 1099 ↛ 1100line 1099 didn't jump to line 1100 because the condition on line 1099 was never true
1100 CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = None
1103# WARNING: Experimental. Only enable if your upstream Responses API endpoint
1104# supports stateful sessions (i.e. server-side response storage with
1105# previous_response_id anchoring). Most proxies and third-party endpoints
1106# are stateless and will break if this is enabled.
1107ENABLE_RESPONSES_API_STATEFUL = os.getenv('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true'
1110CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '')
1112if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == '': 1112 ↛ 1115line 1112 didn't jump to line 1115 because the condition on line 1112 was always true
1113 CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
1114else:
1115 try:
1116 CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE)
1117 except Exception:
1118 CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None
1121####################################
1122# SENTENCE TRANSFORMERS
1123####################################
1126SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '')
1127if SENTENCE_TRANSFORMERS_BACKEND == '': 1127 ↛ 1131line 1127 didn't jump to line 1131 because the condition on line 1127 was always true
1128 SENTENCE_TRANSFORMERS_BACKEND = 'torch'
1131SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '')
1132if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '': 1132 ↛ 1135line 1132 didn't jump to line 1135 because the condition on line 1132 was always true
1133 SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
1134else:
1135 try:
1136 SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_MODEL_KWARGS)
1137 except Exception:
1138 SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
1141SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '')
1142if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '': 1142 ↛ 1146line 1142 didn't jump to line 1146 because the condition on line 1142 was always true
1143 SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch'
1146SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', '')
1147if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '': 1147 ↛ 1150line 1147 didn't jump to line 1150 because the condition on line 1147 was always true
1148 SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
1149else:
1150 try:
1151 SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS)
1152 except Exception:
1153 SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
1155# Whether to apply sigmoid normalization to CrossEncoder reranking scores.
1156# When enabled (default), scores are normalized to 0-1 range for proper
1157# relevance threshold behavior with MS MARCO models.
1158SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = (
1159 os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true'
1160)
1162####################################
1163# KNOWLEDGE TOOLS
1164####################################
1167def _int_env(name: str, default: int) -> int:
1168 try:
1169 return max(int(os.getenv(name) or default), 1)
1170 except (ValueError, TypeError):
1171 return default
1174# Total output of a single kb_exec call, whatever the command.
1175KB_EXEC_MAX_OUTPUT_CHARS = _int_env('KB_EXEC_MAX_OUTPUT_CHARS', 30_000)
1176# Files a single kb_exec grep may scan before it asks for a narrower scope.
1177KB_EXEC_MAX_GREP_FILES = _int_env('KB_EXEC_MAX_GREP_FILES', 200)
1178# Matching lines returned by kb_exec grep and grep_knowledge_files.
1179KNOWLEDGE_GREP_MAX_MATCHES = _int_env('KNOWLEDGE_GREP_MAX_MATCHES', 50)
1180# Characters returned by view_file / view_knowledge_file.
1181VIEW_FILE_MAX_CHARS = _int_env('VIEW_FILE_MAX_CHARS', 100_000)
1182VIEW_FILE_DEFAULT_MAX_CHARS = _int_env('VIEW_FILE_DEFAULT_MAX_CHARS', 10_000)
1184####################################
1185# TOOLS/FUNCTIONS PIP OPTIONS
1186####################################
1188ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true'
1190ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = (
1191 os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true'
1192)
1194PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split()
1195PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split()
1198####################################
1199# OFFLINE_MODE
1200####################################
1202ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true'
1203OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true'
1205if OFFLINE_MODE: 1205 ↛ 1213line 1205 didn't jump to line 1213 because the condition on line 1205 was always true
1206 os.environ['HF_HUB_OFFLINE'] = '1'
1207 ENABLE_VERSION_UPDATE_CHECK = False
1209####################################
1210# Pyodide file persistence
1211####################################
1213ENABLE_PYODIDE_FILE_PERSISTENCE = os.getenv('ENABLE_PYODIDE_FILE_PERSISTENCE', 'false').lower() == 'true'
1215####################################
1216# Audit logging
1217####################################
1220ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true'
1221ENABLE_AUDIT_LOGS_FILE = os.getenv('ENABLE_AUDIT_LOGS_FILE', 'True').lower() == 'true'
1223# Where to store log file
1224# Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to
1225# provide the whole path, like: /app/audit.log
1226AUDIT_LOGS_FILE_PATH = os.getenv('AUDIT_LOGS_FILE_PATH', f'{DATA_DIR}/audit.log')
1227# Maximum size of a file before rotating into a new log file
1228AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv('AUDIT_LOG_FILE_ROTATION_SIZE', '10MB')
1230# Comma separated list of logger names to use for audit logging
1231# Default is "uvicorn.access" which is the access log for Uvicorn
1232# You can add more logger names to this list if you want to capture more logs
1233AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.access').split(',')
1235# METADATA | REQUEST | REQUEST_RESPONSE
1236AUDIT_LOG_LEVEL = os.getenv('AUDIT_LOG_LEVEL', 'NONE').upper()
1237try:
1238 MAX_BODY_LOG_SIZE = int(os.getenv('MAX_BODY_LOG_SIZE') or 2048)
1239except ValueError:
1240 MAX_BODY_LOG_SIZE = 2048
1242# Comma separated list for urls to exclude from audit
1243AUDIT_EXCLUDED_PATHS = [
1244 path
1245 for path in (
1246 path.strip().lstrip('/') for path in os.getenv('AUDIT_EXCLUDED_PATHS', '/chats,/chat,/folders').split(',')
1247 )
1248 if path
1249]
1251# Comma separated list of urls to include in audit (whitelist mode)
1252# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored
1253AUDIT_INCLUDED_PATHS = [
1254 path for path in (path.strip().lstrip('/') for path in os.getenv('AUDIT_INCLUDED_PATHS', '').split(',')) if path
1255]
1257# When enabled, GET requests are also audited (disabled by default to avoid log noise)
1258ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true'
1261####################################
1262# OPENTELEMETRY
1263####################################
1265ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true'
1266ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true'
1267ENABLE_OTEL_METRICS = os.getenv('ENABLE_OTEL_METRICS', 'False').lower() == 'true'
1268ENABLE_OTEL_LOGS = os.getenv('ENABLE_OTEL_LOGS', 'False').lower() == 'true'
1270OTEL_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317')
1271OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
1272OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT)
1273OTEL_EXPORTER_OTLP_INSECURE = os.getenv('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true'
1274OTEL_METRICS_EXPORTER_OTLP_INSECURE = (
1275 os.getenv('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
1276)
1277OTEL_LOGS_EXPORTER_OTLP_INSECURE = (
1278 os.getenv('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true'
1279)
1280OTEL_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', 'open-webui')
1281OTEL_RESOURCE_ATTRIBUTES = os.getenv('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2
1282OTEL_TRACES_SAMPLER = os.getenv('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower()
1283OTEL_BASIC_AUTH_USERNAME = os.getenv('OTEL_BASIC_AUTH_USERNAME', '')
1284OTEL_BASIC_AUTH_PASSWORD = os.getenv('OTEL_BASIC_AUTH_PASSWORD', '')
1285OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.getenv('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000'))
1287OTEL_METRICS_BASIC_AUTH_USERNAME = os.getenv('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
1288OTEL_METRICS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
1289OTEL_LOGS_BASIC_AUTH_USERNAME = os.getenv('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME)
1290OTEL_LOGS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD)
1292OTEL_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http
1294OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv(
1295 'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
1296).lower() # grpc or http
1298OTEL_LOGS_OTLP_SPAN_EXPORTER = os.getenv(
1299 'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER
1300).lower() # grpc or http