diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index ec541f5a33..394a1c840e 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1,7 +1,6 @@ from __future__ import annotations import base64 -import json import logging import os import shutil @@ -35,11 +34,12 @@ from open_webui.env import ( log, ) from open_webui.models.config import Config +from open_webui.utils.json_codec import JSONCodec async def seed_registered_defaults(): await Config.rename_prefix('rag.web', 'web') - await Config.repair_flattened_dict_configs() + await Config.repair_config_rows() await Config.seed_defaults(DEFAULT_CONFIG) @@ -84,7 +84,7 @@ async def import_legacy_config_json(): if not os.path.exists(f'{DATA_DIR}/config.json'): return with open(f'{DATA_DIR}/config.json', 'r') as _f: - await Config.upsert(json.load(_f)) + await Config.upsert(JSONCodec.loads(_f.read())) os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json') @@ -114,6 +114,9 @@ for file_path in (FRONTEND_BUILD_DIR / 'static').glob('**/*'): except Exception as e: logging.error(f'An error occurred: {e}') +# LICENSE covers copied Open WebUI logo/favicon assets. +# Do not alter, remove, obscure, or replace them except as LICENSE permits: +# https://docs.openwebui.com/license. frontend_favicon = FRONTEND_BUILD_DIR / 'static' / 'favicon.png' if frontend_favicon.exists(): @@ -181,6 +184,9 @@ CACHE_DIR.mkdir(parents=True, exist_ok=True) # CUSTOM_NAME (Legacy) #################################### +# LICENSE covers this legacy Open WebUI branding path. +# Do not alter, remove, obscure, or replace it except as LICENSE permits: +# https://docs.openwebui.com/license. CUSTOM_NAME = os.getenv('CUSTOM_NAME', '') if CUSTOM_NAME: @@ -270,9 +276,9 @@ def _resolve_ollama_base_url(url: str) -> str: if not default.result() and fallback.result(): url = url.replace(':11434', ':12434') - log.info(f'Ollama port 11434 unreachable on {host}, falling back to 12434') + log.info('Ollama port 11434 unreachable on %s, falling back to 12434', host) elif not default.result(): - log.info(f'Ollama ports 11434 and 12434 both unreachable on {host}') + log.info('Ollama ports 11434 and 12434 both unreachable on %s', host) return url @@ -293,12 +299,12 @@ OLLAMA_API_CONFIGS = {} _ollama_api_configs = os.getenv('OLLAMA_API_CONFIGS', '') if _ollama_api_configs: try: - parsed = json.loads(_ollama_api_configs) + parsed = JSONCodec.loads(_ollama_api_configs) if isinstance(parsed, dict): OLLAMA_API_CONFIGS = parsed else: log.warning('OLLAMA_API_CONFIGS must be a JSON object, ignoring') - except (json.JSONDecodeError, TypeError): + except (JSONCodec.JSONDecodeError, TypeError): log.warning('OLLAMA_API_CONFIGS is not valid JSON, ignoring') #################################### @@ -340,12 +346,12 @@ OPENAI_API_CONFIGS = {} _openai_api_configs = os.getenv('OPENAI_API_CONFIGS', '') if _openai_api_configs: try: - parsed = json.loads(_openai_api_configs) + parsed = JSONCodec.loads(_openai_api_configs) if isinstance(parsed, dict): OPENAI_API_CONFIGS = parsed else: log.warning('OPENAI_API_CONFIGS must be a JSON object, ignoring') - except (json.JSONDecodeError, TypeError): + except (JSONCodec.JSONDecodeError, TypeError): log.warning('OPENAI_API_CONFIGS is not valid JSON, ignoring') # Get the actual OpenAI API key based on the base URL @@ -369,7 +375,7 @@ ENABLE_BASE_MODELS_CACHE = os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower( #################################### try: - tool_server_connections = json.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]')) + tool_server_connections = JSONCodec.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]')) except Exception as e: log.exception(f'Error loading TOOL_SERVER_CONNECTIONS: {e}') tool_server_connections = [] @@ -383,12 +389,12 @@ OAUTH_CLIENT_TIMEOUT = os.getenv('OAUTH_CLIENT_TIMEOUT', '') # TERMINAL_SERVER #################################### -terminal_server_connections = json.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]')) +terminal_server_connections = JSONCodec.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]')) TERMINAL_SERVER_CONNECTIONS = terminal_server_connections try: - TERMINAL_PROXY_HEADERS = json.loads(os.getenv('TERMINAL_PROXY_HEADERS', '{}')) + TERMINAL_PROXY_HEADERS = JSONCodec.loads(os.getenv('TERMINAL_PROXY_HEADERS', '{}')) except Exception: TERMINAL_PROXY_HEADERS = {} @@ -606,6 +612,7 @@ QDRANT_PREFER_GRPC = os.getenv('QDRANT_PREFER_GRPC', 'false').lower() == 'true' QDRANT_GRPC_PORT = int(os.getenv('QDRANT_GRPC_PORT', '6334')) QDRANT_TIMEOUT = int(os.getenv('QDRANT_TIMEOUT', '5')) QDRANT_HNSW_M = int(os.getenv('QDRANT_HNSW_M', '16')) +QDRANT_EMBEDDING_MODEL = os.getenv('QDRANT_EMBEDDING_MODEL', 'BAAI/bge-m3') ENABLE_QDRANT_MULTITENANCY_MODE = os.getenv('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true' QDRANT_COLLECTION_PREFIX = os.getenv('QDRANT_COLLECTION_PREFIX', 'open-webui') @@ -805,7 +812,7 @@ if VECTOR_DB == 'oracle23ai': 'Oracle23ai requires setting ORACLE_WALLET_DIR and ORACLE_WALLET_PASSWORD when using wallet authentication.' ) -log.info(f'VECTOR_DB: {VECTOR_DB}') +log.info('VECTOR_DB: %s', VECTOR_DB) # S3 Vector S3_VECTOR_BUCKET_NAME = os.getenv('S3_VECTOR_BUCKET_NAME', None) @@ -894,8 +901,8 @@ MINERU_API_KEY = os.getenv('MINERU_API_KEY', '') mineru_params = os.getenv('MINERU_PARAMS', '') try: - mineru_params = json.loads(mineru_params) -except json.JSONDecodeError: + mineru_params = JSONCodec.loads(mineru_params) +except JSONCodec.JSONDecodeError: mineru_params = {} MINERU_PARAMS = mineru_params @@ -908,8 +915,8 @@ EXTERNAL_DOCUMENT_LOADER_API_KEY = os.getenv('EXTERNAL_DOCUMENT_LOADER_API_KEY', external_document_loader_headers = os.getenv('EXTERNAL_DOCUMENT_LOADER_HEADERS', '') try: - external_document_loader_headers = json.loads(external_document_loader_headers) -except json.JSONDecodeError: + external_document_loader_headers = JSONCodec.loads(external_document_loader_headers) +except JSONCodec.JSONDecodeError: external_document_loader_headers = {} if not isinstance(external_document_loader_headers, dict): external_document_loader_headers = {} @@ -924,8 +931,8 @@ DOCLING_API_KEY = os.getenv('DOCLING_API_KEY', '') docling_params = os.getenv('DOCLING_PARAMS', '') try: - docling_params = json.loads(docling_params) -except json.JSONDecodeError: + docling_params = JSONCodec.loads(docling_params) +except JSONCodec.JSONDecodeError: docling_params = {} DOCLING_PARAMS = docling_params @@ -988,7 +995,7 @@ PDF_EXTRACT_IMAGES = os.getenv('PDF_EXTRACT_IMAGES', 'False').lower() == 'true' PDF_LOADER_MODE = os.getenv('PDF_LOADER_MODE', 'page') RAG_EMBEDDING_MODEL = os.getenv('RAG_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2') -log.info(f'Embedding model set: {RAG_EMBEDDING_MODEL}') +log.info('Embedding model set: %s', RAG_EMBEDDING_MODEL) RAG_TOKENIZER_MODEL = os.getenv('RAG_TOKENIZER_MODEL', '') @@ -1016,7 +1023,7 @@ RAG_RERANKING_ENGINE = os.getenv('RAG_RERANKING_ENGINE', '') RAG_RERANKING_MODEL = os.getenv('RAG_RERANKING_MODEL', '') if RAG_RERANKING_MODEL != '': - log.info(f'Reranking model set: {RAG_RERANKING_MODEL}') + log.info('Reranking model set: %s', RAG_RERANKING_MODEL) RAG_RERANKING_MODEL_AUTO_UPDATE = ( @@ -1148,7 +1155,7 @@ WEB_SEARCH_RESULT_COUNT = int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3')) try: - web_search_domain_filter_list = json.loads(os.getenv('WEB_SEARCH_DOMAIN_FILTER_LIST', '[]')) + web_search_domain_filter_list = JSONCodec.loads(os.getenv('WEB_SEARCH_DOMAIN_FILTER_LIST', '[]')) except Exception as e: web_search_domain_filter_list = [ # "wikipedia.com", @@ -1297,8 +1304,8 @@ LINKUP_API_KEY = os.getenv('LINKUP_API_KEY', '') linkup_search_params = os.getenv('LINKUP_SEARCH_PARAMS', '') try: - linkup_search_params = json.loads(linkup_search_params) -except json.JSONDecodeError: + linkup_search_params = JSONCodec.loads(linkup_search_params) +except JSONCodec.JSONDecodeError: linkup_search_params = {} LINKUP_SEARCH_PARAMS = linkup_search_params @@ -1330,8 +1337,8 @@ AUTOMATIC1111_API_AUTH = os.getenv('AUTOMATIC1111_API_AUTH', '') automatic1111_params = os.getenv('AUTOMATIC1111_PARAMS', '') try: - automatic1111_params = json.loads(automatic1111_params) -except json.JSONDecodeError: + automatic1111_params = JSONCodec.loads(automatic1111_params) +except JSONCodec.JSONDecodeError: automatic1111_params = {} AUTOMATIC1111_PARAMS = automatic1111_params @@ -1455,8 +1462,8 @@ COMFYUI_WORKFLOW = os.getenv('COMFYUI_WORKFLOW', COMFYUI_DEFAULT_WORKFLOW) comfyui_workflow_nodes = os.getenv('COMFYUI_WORKFLOW_NODES', '') try: - comfyui_workflow_nodes = json.loads(comfyui_workflow_nodes) -except json.JSONDecodeError: + comfyui_workflow_nodes = JSONCodec.loads(comfyui_workflow_nodes) +except JSONCodec.JSONDecodeError: comfyui_workflow_nodes = [] COMFYUI_WORKFLOW_NODES = comfyui_workflow_nodes @@ -1468,8 +1475,8 @@ IMAGES_OPENAI_API_KEY = os.getenv('IMAGES_OPENAI_API_KEY', OPENAI_API_KEY) images_openai_params = os.getenv('IMAGES_OPENAI_PARAMS', '') try: - images_openai_params = json.loads(images_openai_params) -except json.JSONDecodeError: + images_openai_params = JSONCodec.loads(images_openai_params) +except JSONCodec.JSONDecodeError: images_openai_params = {} @@ -1507,8 +1514,8 @@ IMAGES_EDIT_COMFYUI_WORKFLOW = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW', '') images_edit_comfyui_workflow_nodes = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', '') try: - images_edit_comfyui_workflow_nodes = json.loads(images_edit_comfyui_workflow_nodes) -except json.JSONDecodeError: + images_edit_comfyui_workflow_nodes = JSONCodec.loads(images_edit_comfyui_workflow_nodes) +except JSONCodec.JSONDecodeError: images_edit_comfyui_workflow_nodes = [] IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = images_edit_comfyui_workflow_nodes @@ -1582,8 +1589,8 @@ AUDIO_TTS_OPENAI_API_KEY = os.getenv('AUDIO_TTS_OPENAI_API_KEY', OPENAI_API_KEY) audio_tts_openai_params = os.getenv('AUDIO_TTS_OPENAI_PARAMS', '') try: - audio_tts_openai_params = json.loads(audio_tts_openai_params) -except json.JSONDecodeError: + audio_tts_openai_params = JSONCodec.loads(audio_tts_openai_params) +except JSONCodec.JSONDecodeError: audio_tts_openai_params = {} AUDIO_TTS_OPENAI_PARAMS = audio_tts_openai_params @@ -1635,7 +1642,7 @@ DEFAULT_MODELS = os.getenv('DEFAULT_MODELS', None) DEFAULT_PINNED_MODELS = os.getenv('DEFAULT_PINNED_MODELS', None) try: - default_prompt_suggestions = json.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', '[]')) + default_prompt_suggestions = JSONCodec.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', '[]')) except Exception as e: log.exception(f'Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}') default_prompt_suggestions = [] @@ -1673,7 +1680,7 @@ if default_prompt_suggestions == []: DEFAULT_PROMPT_SUGGESTIONS = default_prompt_suggestions try: - model_order_list = json.loads(os.getenv('MODEL_ORDER_LIST', '[]')) + model_order_list = JSONCodec.loads(os.getenv('MODEL_ORDER_LIST', '[]')) except Exception as e: log.exception(f'Error loading MODEL_ORDER_LIST: {e}') model_order_list = [] @@ -1681,7 +1688,7 @@ except Exception as e: MODEL_ORDER_LIST = model_order_list try: - default_model_metadata = json.loads(os.getenv('DEFAULT_MODEL_METADATA', '{}')) + default_model_metadata = JSONCodec.loads(os.getenv('DEFAULT_MODEL_METADATA', '{}')) except Exception as e: log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}') default_model_metadata = {} @@ -1689,13 +1696,22 @@ except Exception as e: DEFAULT_MODEL_METADATA = default_model_metadata try: - default_model_params = json.loads(os.getenv('DEFAULT_MODEL_PARAMS', '{}')) + default_model_params = JSONCodec.loads(os.getenv('DEFAULT_MODEL_PARAMS', '{}')) except Exception as e: log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}') default_model_params = {} DEFAULT_MODEL_PARAMS = default_model_params + +try: + default_interface_settings = JSONCodec.loads(os.getenv('DEFAULT_INTERFACE_SETTINGS', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_INTERFACE_SETTINGS: {e}') + default_interface_settings = {} + +DEFAULT_INTERFACE_SETTINGS = default_interface_settings if isinstance(default_interface_settings, dict) else {} + DEFAULT_USER_ROLE = os.getenv('DEFAULT_USER_ROLE', 'pending') DEFAULT_GROUP_ID = os.getenv('DEFAULT_GROUP_ID', '') @@ -2032,7 +2048,7 @@ ENABLE_USER_STATUS = os.getenv('ENABLE_USER_STATUS', 'True').lower() == 'true' ENABLE_EVALUATION_ARENA_MODELS = os.getenv('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true' try: - evaluation_arena_models = json.loads(os.getenv('EVALUATION_ARENA_MODELS', '[]')) + evaluation_arena_models = JSONCodec.loads(os.getenv('EVALUATION_ARENA_MODELS', '[]')) if not isinstance(evaluation_arena_models, list) or not all( isinstance(model, dict) for model in evaluation_arena_models ): @@ -2047,6 +2063,9 @@ DEFAULT_ARENA_MODEL = { 'id': 'arena-model', 'name': 'Arena Model', 'meta': { + # LICENSE covers this Open WebUI fallback logo. + # Do not alter, remove, obscure, or replace it except as LICENSE permits: + # https://docs.openwebui.com/license. 'profile_image_url': '/favicon.png', 'description': 'Submit your questions to anonymous AI chatbots and vote on the best response.', 'model_ids': None, @@ -2079,6 +2098,7 @@ ENABLE_USER_WEBHOOKS = os.getenv('ENABLE_USER_WEBHOOKS', 'False').lower() == 'tr # FastAPI / AnyIO settings THREAD_POOL_SIZE = os.getenv('THREAD_POOL_SIZE', None) +THREAD_POOL_THREAD_NAME_PREFIX = os.getenv('THREAD_POOL_THREAD_NAME_PREFIX', '') if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str): try: @@ -2134,7 +2154,7 @@ class BannerModel(BaseModel): try: - banners = json.loads(os.getenv('WEBUI_BANNERS', '[]')) + banners = JSONCodec.loads(os.getenv('WEBUI_BANNERS', '[]')) banners = [BannerModel(**banner) for banner in banners] except Exception as e: log.exception(f'Error loading WEBUI_BANNERS: {e}') @@ -2157,10 +2177,20 @@ TASK_MODEL = os.getenv('TASK_MODEL', '') TASK_MODEL_EXTERNAL = os.getenv('TASK_MODEL_EXTERNAL', '') +try: + task_model_params = JSONCodec.loads(os.getenv('TASK_MODEL_PARAMS', '{}')) +except Exception as e: + log.exception(f'Error loading TASK_MODEL_PARAMS: {e}') + task_model_params = {} + +TASK_MODEL_PARAMS = task_model_params + CONTEXT_COMPACTION_MODEL = os.getenv('CONTEXT_COMPACTION_MODEL', '') ENABLE_CONTEXT_COMPACTION = os.getenv('ENABLE_CONTEXT_COMPACTION', 'False').lower() == 'true' +ENABLE_TOOL_PERMISSIONS = os.getenv('ENABLE_TOOL_PERMISSIONS', 'False').lower() == 'true' + CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000')) _CONTEXT_COMPACTION_TOKEN_CAP = os.getenv('CONTEXT_COMPACTION_TOKEN_CAP') @@ -2470,12 +2500,12 @@ GOOGLE_OAUTH_AUTHORIZE_PARAMS = {} _google_oauth_authorize_params = os.getenv('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '') if _google_oauth_authorize_params: try: - _parsed = json.loads(_google_oauth_authorize_params) + _parsed = JSONCodec.loads(_google_oauth_authorize_params) if isinstance(_parsed, dict): GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed else: log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') - except (json.JSONDecodeError, TypeError): + except (JSONCodec.JSONDecodeError, TypeError): log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') MICROSOFT_CLIENT_ID = os.getenv('MICROSOFT_CLIENT_ID', '') @@ -2590,12 +2620,12 @@ OAUTH_AUTHORIZE_PARAMS = {} _oauth_authorize_params = os.getenv('OAUTH_AUTHORIZE_PARAMS', '') if _oauth_authorize_params: try: - _parsed = json.loads(_oauth_authorize_params) + _parsed = JSONCodec.loads(_oauth_authorize_params) if isinstance(_parsed, dict): OAUTH_AUTHORIZE_PARAMS = _parsed else: log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') - except (json.JSONDecodeError, TypeError): + except (JSONCodec.JSONDecodeError, TypeError): log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') @@ -3046,6 +3076,7 @@ DEFAULT_CONFIG = { 'ui.default_locale': DEFAULT_LOCALE, 'ui.default_models': DEFAULT_MODELS, 'ui.default_pinned_models': DEFAULT_PINNED_MODELS, + 'ui.default_interface_settings': DEFAULT_INTERFACE_SETTINGS, 'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS, 'ui.model_order_list': MODEL_ORDER_LIST, 'models.default_metadata': DEFAULT_MODEL_METADATA, @@ -3085,12 +3116,14 @@ DEFAULT_CONFIG = { 'auth.admin.email': ADMIN_EMAIL, 'task.model.default': TASK_MODEL, 'task.model.external': TASK_MODEL_EXTERNAL, + 'task.model.params': TASK_MODEL_PARAMS, 'chat.context_compaction.model': CONTEXT_COMPACTION_MODEL, 'chat.context_compaction.enable': ENABLE_CONTEXT_COMPACTION, 'chat.context_compaction.token_threshold': CONTEXT_COMPACTION_TOKEN_THRESHOLD, 'chat.context_compaction.token_cap': CONTEXT_COMPACTION_TOKEN_CAP, 'chat.context_compaction.retention_percentage': CONTEXT_COMPACTION_RETENTION_PERCENTAGE, 'chat.context_compaction.prompt_template': CONTEXT_COMPACTION_PROMPT_TEMPLATE, + 'chat.tool_permissions.enable': ENABLE_TOOL_PERMISSIONS, 'task.title.prompt_template': TITLE_GENERATION_PROMPT_TEMPLATE, 'task.tags.prompt_template': TAGS_GENERATION_PROMPT_TEMPLATE, 'task.image.prompt_template': IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,