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

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 

16 

17import markdown 

18from bs4 import BeautifulSoup 

19from cryptography.hazmat.primitives import serialization 

20 

21#################################### 

22# Load .env file 

23#################################### 

24 

25# Use .resolve() to get the canonical path, removing any '..' or '.' components 

26ENV_FILE_PATH = Path(__file__).resolve() 

27 

28# OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/) 

29OPEN_WEBUI_DIR = ENV_FILE_PATH.parent 

30 

31# BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/) 

32BACKEND_DIR = OPEN_WEBUI_DIR.parent 

33 

34# BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/) 

35BASE_DIR = BACKEND_DIR.parent 

36 

37try: 

38 from dotenv import find_dotenv, load_dotenv 

39 

40 load_dotenv(find_dotenv(str(BASE_DIR / '.env'))) 

41except ImportError: 

42 print('dotenv not installed, skipping...') 

43 

44DOCKER = os.getenv('DOCKER', 'False').lower() == 'true' 

45USE_SLIM = os.getenv('USE_SLIM_DOCKER', 'False').lower() == 'true' 

46 

47USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false') 

48DEVICE_TYPE = 'cpu' 

49_cuda_error: Optional[str] = None 

50 

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 

54 

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' 

62 

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 

66 

67 if torch.backends.mps.is_available() and torch.backends.mps.is_built(): 

68 DEVICE_TYPE = 'mps' 

69 except Exception: 

70 pass 

71 

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() 

74 

75#################################### 

76# LOGGING 

77#################################### 

78 

79_LEVEL_MAP = { 

80 'DEBUG': 'debug', 

81 'INFO': 'info', 

82 'WARNING': 'warn', 

83 'ERROR': 'error', 

84 'CRITICAL': 'fatal', 

85} 

86 

87 

88class JSONFormatter(logging.Formatter): 

89 """Format log records as single-line JSON objects for structured logging.""" 

90 

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 } 

98 

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 

103 

104 if record.stack_info: 

105 log_entry['stacktrace'] = record.stack_info 

106 

107 return json.dumps(log_entry, ensure_ascii=False, default=str) 

108 

109 

110LOG_FORMAT = os.getenv('LOG_FORMAT', '').lower() 

111LOGURU_DIAGNOSE = os.getenv('LOGURU_DIAGNOSE', 'False').lower() == 'true' 

112 

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' 

125 

126log = logging.getLogger(__name__) 

127log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL) 

128 

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 

132 

133SRC_LOG_LEVELS = {} # Legacy variable, do not remove 

134 

135#################################### 

136# ENV (dev,test,prod) 

137#################################### 

138 

139ENV = os.getenv('ENV', 'dev') 

140 

141FROM_INIT_PY = os.getenv('FROM_INIT_PY', 'False').lower() == 'true' 

142 

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'} 

150 

151VERSION = PACKAGE_DATA['version'] 

152 

153 

154DEPLOYMENT_ID = os.getenv('DEPLOYMENT_ID', '') 

155INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4())) 

156 

157ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' 

158 

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' 

163 

164 

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) 

171 

172 # Extract text without HTML tags 

173 text = li.get_text(separator=' ', strip=True) 

174 

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 

179 

180 items.append({'title': title, 'content': content, 'raw': raw_html}) 

181 return items 

182 

183 

184try: 

185 changelog_path = BASE_DIR / 'CHANGELOG.md' 

186 with open(str(changelog_path.absolute()), encoding='utf8') as file: 

187 changelog_content = file.read() 

188 

189except Exception: 

190 changelog_content = (pkgutil.get_data('open_webui', 'CHANGELOG.md') or b'').decode() 

191 

192# Convert markdown content to HTML 

193html_content = markdown.markdown(changelog_content) 

194 

195# Parse the HTML content 

196soup = BeautifulSoup(html_content, 'html.parser') 

197 

198# Initialize JSON structure 

199changelog_json = {} 

200 

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] 

205 

206 version_data = {'date': date} 

207 

208 # Find the next sibling that is a h3 tag (section title) 

209 current = version.find_next_sibling() 

210 

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 

216 

217 # Move to the next element 

218 current = current.find_next_sibling() 

219 

220 changelog_json[version_number] = version_data 

221 

222CHANGELOG = changelog_json 

223 

224#################################### 

225# DATA/FRONTEND BUILD DIR 

226#################################### 

227 

228DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve() 

229 

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) 

233 

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) 

243 

244 # Zip the data directory 

245 shutil.make_archive(DATA_DIR.parent / 'open_webui_data', 'zip', DATA_DIR) 

246 

247 # Remove the old data directory 

248 shutil.rmtree(DATA_DIR) 

249 

250 DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')) 

251 

252STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')) 

253 

254FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).resolve() 

255 

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() 

258 

259#################################### 

260# Database 

261#################################### 

262 

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 

270 

271DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db') 

272 

273DATABASE_TYPE = os.getenv('DATABASE_TYPE') 

274DATABASE_USER = os.getenv('DATABASE_USER') 

275DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD') 

276 

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}' 

282 

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} 

290 

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' 

298 

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://') 

302 

303DATABASE_SCHEMA = os.getenv('DATABASE_SCHEMA', None) 

304DATABASE_ENABLE_IAM_TOKEN_AUTH = os.getenv('DATABASE_ENABLE_IAM_TOKEN_AUTH', 'False').lower() == 'true' 

305 

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 

311 

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 

317 

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 

323 

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 

329 

330DATABASE_ENABLE_SQLITE_WAL = os.getenv('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true' 

331 

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. 

335 

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') 

339 

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') 

343 

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') 

346 

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') 

350 

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') 

354 

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') 

359 

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 

365 

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' 

373 

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()] 

376 

377#################################### 

378# REDIS 

379#################################### 

380 

381REDIS_URL = os.getenv('REDIS_URL', '') 

382REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true' 

383 

384REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui') 

385 

386try: 

387 REDIS_RESPONSE_STREAM_TTL = int(os.getenv('REDIS_RESPONSE_STREAM_TTL', '3600')) 

388except ValueError: 

389 REDIS_RESPONSE_STREAM_TTL = 3600 

390 

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 

398 

399REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '') 

400REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379') 

401 

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 

410 

411 

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 

417 

418REDIS_SOCKET_TIMEOUT = os.getenv('REDIS_SOCKET_TIMEOUT', '') 

419try: 

420 REDIS_SOCKET_TIMEOUT = float(REDIS_SOCKET_TIMEOUT) 

421except ValueError: 

422 REDIS_SOCKET_TIMEOUT = None 

423 

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' 

430 

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 

444 

445REDIS_RECONNECT_DELAY = os.getenv('REDIS_RECONNECT_DELAY', '') 

446 

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 

456 

457#################################### 

458# Uvicorn 

459#################################### 

460 

461try: 

462 UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1) 

463except (ValueError, TypeError): 

464 UVICORN_WORKERS = 1 

465 

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' 

468 

469#################################### 

470# WEBSOCKET SUPPORT 

471#################################### 

472 

473ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true' 

474 

475 

476WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '') 

477 

478WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '') 

479 

480 

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} 

492 

493WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL) 

494WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true' 

495 

496websocket_redis_lock_timeout = os.getenv('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60') 

497 

498try: 

499 WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout) 

500except ValueError: 

501 WEBSOCKET_REDIS_LOCK_TIMEOUT = 60 

502 

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 

518 

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 

524 

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 

533 

534WEBSOCKET_EVENT_CALLER_TIMEOUT = os.getenv('WEBSOCKET_EVENT_CALLER_TIMEOUT', '') 

535 

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 

543 

544 

545import ssl as _ssl 

546 

547 

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() 

557 

558 

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 

572 

573 

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) 

576 

577 

578def _parse_ssl_env(value: str) -> 'bool | _ssl.SSLContext': 

579 """Parse an SSL env var into a bool or SSLContext. 

580 

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``) 

586 

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 

602 

603 

604REQUESTS_VERIFY = os.getenv('REQUESTS_VERIFY', 'True').lower() == 'true' 

605 

606TAVILY_API_BASE_URL = os.getenv('TAVILY_API_BASE_URL', 'https://api.tavily.com').rstrip('/') 

607 

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 

613 

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 

623 

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 

626 

627 

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')) 

632 

633SEARXNG_CLIENT_CERT_FILE = os.getenv('SEARXNG_CLIENT_CERT_FILE', '').strip() 

634SEARXNG_CLIENT_KEY_FILE = os.getenv('SEARXNG_CLIENT_KEY_FILE', '').strip() 

635 

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' 

638 

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' 

643 

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 

646 

647 aiohttp.DefaultResolver = aiohttp.resolver.ThreadedResolver # for plugin code 

648 aiohttp.resolver.DefaultResolver = aiohttp.resolver.ThreadedResolver 

649 aiohttp.connector.DefaultResolver = aiohttp.resolver.ThreadedResolver 

650 

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', '') 

655 

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 

664 

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 

670 

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 

676 

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 

679 

680 

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')) 

685 

686AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '') 

687 

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 

695 

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 

704 

705 

706#################################### 

707# AIOHTTP Connection Pool 

708#################################### 

709 

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 

718 

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 

727 

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 

735 

736RAG_EMBEDDING_TIMEOUT = os.getenv('RAG_EMBEDDING_TIMEOUT', '') 

737 

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 

745 

746 

747#################################### 

748# Auth 

749#################################### 

750 

751WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true' 

752 

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' 

755 

756#################################### 

757# Secret key & cookies 

758#################################### 

759 

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) 

766 

767ENABLE_VALVE_ENCRYPTION = os.getenv('ENABLE_VALVE_ENCRYPTION', 'False').lower() == 'true' 

768 

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) 

779 

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 ) 

789 

790ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true' 

791 

792#################################### 

793# Admin Account Runtime Creation 

794#################################### 

795 

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') 

801 

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) 

806 

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') 

813 

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) 

820 

821 

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,}$') 

828 

829PASSWORD_VALIDATION_HINT = os.getenv('PASSWORD_VALIDATION_HINT', '') 

830 

831 

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' 

834 

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' 

840 

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) 

849 

850MINERU_MAX_MARKDOWN_BYTES = ( 

851 int(os.getenv('MINERU_MAX_MARKDOWN_BYTES')) if os.getenv('MINERU_MAX_MARKDOWN_BYTES') else None 

852) 

853 

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' 

858 

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' 

863 

864WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None) 

865 

866#################################### 

867# OAUTH Configuration 

868#################################### 

869ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true' 

870 

871ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true' 

872 

873OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY) 

874 

875OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY) 

876 

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')) 

880 

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] 

896 

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' 

902 

903#################################### 

904# SCIM Configuration 

905#################################### 

906 

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', '') 

910 

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 ) 

917 

918#################################### 

919# LICENSE_KEY 

920#################################### 

921 

922LICENSE_KEY = os.getenv('LICENSE_KEY', '') 

923 

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() 

929 

930LICENSE_PUBLIC_KEY = os.getenv('LICENSE_PUBLIC_KEY', '') 

931 

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 ) 

941 

942 

943#################################### 

944# WEBUI Identity 

945#################################### 

946 

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)' 

954 

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', '') 

962 

963#################################### 

964# Feature flags 

965#################################### 

966 

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' 

971 

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) 

981 

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 

986 

987#################################### 

988# Forward Headers 

989#################################### 

990 

991ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true' 

992 

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') 

1000 

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 

1011 

1012#################################### 

1013# Progressive Web App 

1014#################################### 

1015 

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) 

1021 

1022#################################### 

1023# GROUP DEFAULTS 

1024#################################### 

1025 

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' 

1030 

1031#################################### 

1032# MODELS 

1033#################################### 

1034 

1035ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true' 

1036 

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 

1045 

1046 

1047#################################### 

1048# CHAT 

1049#################################### 

1050 

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' 

1055 

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) 

1062 

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) 

1071 

1072CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1') 

1073 

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 

1081 

1082 

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) 

1089 

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 

1097 

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 

1101 

1102 

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' 

1108 

1109 

1110CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '') 

1111 

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 

1119 

1120 

1121#################################### 

1122# SENTENCE TRANSFORMERS 

1123#################################### 

1124 

1125 

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' 

1129 

1130 

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 

1139 

1140 

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' 

1144 

1145 

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 

1154 

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) 

1161 

1162#################################### 

1163# KNOWLEDGE TOOLS 

1164#################################### 

1165 

1166 

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 

1172 

1173 

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) 

1183 

1184#################################### 

1185# TOOLS/FUNCTIONS PIP OPTIONS 

1186#################################### 

1187 

1188ENABLE_PLUGINS = os.getenv('ENABLE_PLUGINS', 'True').lower() == 'true' 

1189 

1190ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( 

1191 os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true' 

1192) 

1193 

1194PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split() 

1195PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split() 

1196 

1197 

1198#################################### 

1199# OFFLINE_MODE 

1200#################################### 

1201 

1202ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true' 

1203OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true' 

1204 

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 

1208 

1209#################################### 

1210# Pyodide file persistence 

1211#################################### 

1212 

1213ENABLE_PYODIDE_FILE_PERSISTENCE = os.getenv('ENABLE_PYODIDE_FILE_PERSISTENCE', 'false').lower() == 'true' 

1214 

1215#################################### 

1216# Audit logging 

1217#################################### 

1218 

1219 

1220ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true' 

1221ENABLE_AUDIT_LOGS_FILE = os.getenv('ENABLE_AUDIT_LOGS_FILE', 'True').lower() == 'true' 

1222 

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') 

1229 

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(',') 

1234 

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 

1241 

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] 

1250 

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] 

1256 

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' 

1259 

1260 

1261#################################### 

1262# OPENTELEMETRY 

1263#################################### 

1264 

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' 

1269 

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')) 

1286 

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) 

1291 

1292OTEL_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http 

1293 

1294OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv( 

1295 'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER 

1296).lower() # grpc or http 

1297 

1298OTEL_LOGS_OTLP_SPAN_EXPORTER = os.getenv( 

1299 'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER 

1300).lower() # grpc or http