diff --git "a/backend/open_webui/main.py" "b/backend/open_webui/main.py" new file mode 100644--- /dev/null +++ "b/backend/open_webui/main.py" @@ -0,0 +1,2802 @@ +import asyncio +import inspect +import json +import logging +import mimetypes +import os +import shutil +import sys +import time +import random +import re +from uuid import uuid4 + + +from contextlib import asynccontextmanager +from urllib.parse import urlencode, parse_qs, urlparse +from pydantic import BaseModel +from sqlalchemy import text + +from typing import Optional +from aiocache import cached +import aiohttp +import anyio.to_thread + +from redis import Redis + + +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, + applications, + BackgroundTasks, +) +from fastapi.openapi.docs import get_swagger_ui_html + +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles + +from starlette_compress import CompressMiddleware + +from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import Response, StreamingResponse +from starlette.datastructures import Headers + +from starsessions import ( + SessionMiddleware as StarSessionsMiddleware, + SessionAutoloadMiddleware, +) +from starsessions.stores.redis import RedisStore + +from open_webui.utils import logger +from open_webui.utils.asgi_middleware import ( + AuthTokenMiddleware, + CommitSessionMiddleware, + RedirectMiddleware, + WebsocketUpgradeGuardMiddleware, +) +from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware +from open_webui.utils.logger import start_logger +from open_webui.utils.session_pool import get_session +from open_webui.socket.main import ( + MODELS, + app as socket_app, + periodic_usage_pool_cleanup, + periodic_session_pool_cleanup, + get_event_emitter, + get_models_in_use, + get_user_id_from_session_pool, +) +from open_webui.routers import ( + analytics, + audio, + images, + ollama, + openai, + retrieval, + pipelines, + tasks, + auths, + channels, + chats, + notes, + folders, + configs, + groups, + files, + functions, + memories, + models, + knowledge, + prompts, + evaluations, + skills, + tools, + users, + utils, + scim, + terminals, + automations, + calendar, +) + +from open_webui.routers.retrieval import ( + get_embedding_function, + get_reranking_function, + get_ef, + get_rf, +) + + +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import ScopedSession, engine, get_async_session + +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.users import UserModel, Users +from open_webui.models.chats import Chats, ChatForm + +from open_webui.config import ( + # Ollama + ENABLE_OLLAMA_API, + OLLAMA_BASE_URLS, + OLLAMA_API_CONFIGS, + # OpenAI + ENABLE_OPENAI_API, + OPENAI_API_BASE_URLS, + OPENAI_API_KEYS, + OPENAI_API_CONFIGS, + # Direct Connections + ENABLE_DIRECT_CONNECTIONS, + # Model list + ENABLE_BASE_MODELS_CACHE, + # Thread pool size for FastAPI/AnyIO + THREAD_POOL_SIZE, + # Tool Server Configs + TOOL_SERVER_CONNECTIONS, + # Terminal Server + TERMINAL_SERVER_CONNECTIONS, + # Code Execution + ENABLE_CODE_EXECUTION, + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_URL, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + CODE_EXECUTION_JUPYTER_TIMEOUT, + ENABLE_CODE_INTERPRETER, + CODE_INTERPRETER_ENGINE, + CODE_INTERPRETER_PROMPT_TEMPLATE, + CODE_INTERPRETER_JUPYTER_URL, + CODE_INTERPRETER_JUPYTER_AUTH, + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + CODE_INTERPRETER_JUPYTER_TIMEOUT, + ENABLE_MEMORIES, + # Image + AUTOMATIC1111_API_AUTH, + AUTOMATIC1111_BASE_URL, + AUTOMATIC1111_PARAMS, + COMFYUI_BASE_URL, + COMFYUI_API_KEY, + COMFYUI_WORKFLOW, + COMFYUI_WORKFLOW_NODES, + ENABLE_IMAGE_GENERATION, + ENABLE_IMAGE_PROMPT_GENERATION, + IMAGE_GENERATION_ENGINE, + IMAGE_GENERATION_MODEL, + IMAGE_SIZE, + IMAGE_STEPS, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_VERSION, + IMAGES_OPENAI_API_KEY, + IMAGES_OPENAI_API_PARAMS, + IMAGES_GEMINI_API_BASE_URL, + IMAGES_GEMINI_API_KEY, + IMAGES_GEMINI_ENDPOINT_METHOD, + ENABLE_IMAGE_EDIT, + IMAGE_EDIT_ENGINE, + IMAGE_EDIT_MODEL, + IMAGE_EDIT_SIZE, + IMAGES_EDIT_OPENAI_API_BASE_URL, + IMAGES_EDIT_OPENAI_API_KEY, + IMAGES_EDIT_OPENAI_API_VERSION, + IMAGES_EDIT_GEMINI_API_BASE_URL, + IMAGES_EDIT_GEMINI_API_KEY, + IMAGES_EDIT_COMFYUI_BASE_URL, + IMAGES_EDIT_COMFYUI_API_KEY, + IMAGES_EDIT_COMFYUI_WORKFLOW, + IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + # Audio + AUDIO_STT_ENGINE, + AUDIO_STT_MODEL, + AUDIO_STT_SUPPORTED_CONTENT_TYPES, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_STT_AZURE_API_KEY, + AUDIO_STT_AZURE_REGION, + AUDIO_STT_AZURE_LOCALES, + AUDIO_STT_AZURE_BASE_URL, + AUDIO_STT_AZURE_MAX_SPEAKERS, + AUDIO_STT_MISTRAL_API_KEY, + AUDIO_STT_MISTRAL_API_BASE_URL, + AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MODEL, + AUDIO_TTS_VOICE, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_OPENAI_PARAMS, + AUDIO_TTS_API_KEY, + AUDIO_TTS_SPLIT_ON, + AUDIO_TTS_AZURE_SPEECH_REGION, + AUDIO_TTS_AZURE_SPEECH_BASE_URL, + AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, + AUDIO_TTS_MISTRAL_API_KEY, + AUDIO_TTS_MISTRAL_API_BASE_URL, + PLAYWRIGHT_WS_URL, + PLAYWRIGHT_TIMEOUT, + FIRECRAWL_API_BASE_URL, + FIRECRAWL_API_KEY, + FIRECRAWL_TIMEOUT, + WEB_LOADER_ENGINE, + WEB_LOADER_CONCURRENT_REQUESTS, + WEB_LOADER_TIMEOUT, + WHISPER_MODEL, + WHISPER_VAD_FILTER, + WHISPER_LANGUAGE, + DEEPGRAM_API_KEY, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + # Retrieval + RAG_TEMPLATE, + DEFAULT_RAG_TEMPLATE, + RAG_FULL_CONTEXT, + BYPASS_EMBEDDING_AND_RETRIEVAL, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_RERANKING_ENGINE, + RAG_RERANKING_MODEL, + RAG_EXTERNAL_RERANKER_URL, + RAG_EXTERNAL_RERANKER_API_KEY, + RAG_EXTERNAL_RERANKER_TIMEOUT, + RAG_RERANKING_BATCH_SIZE, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + RAG_EMBEDDING_ENGINE, + RAG_EMBEDDING_BATCH_SIZE, + ENABLE_ASYNC_EMBEDDING, + RAG_EMBEDDING_CONCURRENT_REQUESTS, + RAG_TOP_K, + RAG_TOP_K_RERANKER, + RAG_RELEVANCE_THRESHOLD, + RAG_HYBRID_BM25_WEIGHT, + RAG_ALLOWED_FILE_EXTENSIONS, + RAG_FILE_MAX_COUNT, + RAG_FILE_MAX_SIZE, + FILE_IMAGE_COMPRESSION_WIDTH, + FILE_IMAGE_COMPRESSION_HEIGHT, + RAG_OPENAI_API_BASE_URL, + RAG_OPENAI_API_KEY, + RAG_AZURE_OPENAI_BASE_URL, + RAG_AZURE_OPENAI_API_KEY, + RAG_AZURE_OPENAI_API_VERSION, + RAG_OLLAMA_BASE_URL, + RAG_OLLAMA_API_KEY, + CHUNK_OVERLAP, + CHUNK_MIN_SIZE_TARGET, + CHUNK_SIZE, + CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY, + DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_ADDITIONAL_CONFIG, + DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORMAT_LINES, + DATALAB_MARKER_OUTPUT_FORMAT, + MINERU_API_MODE, + MINERU_API_URL, + MINERU_API_KEY, + MINERU_API_TIMEOUT, + MINERU_PARAMS, + DATALAB_MARKER_USE_LLM, + EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_DOCUMENT_LOADER_API_KEY, + TIKA_SERVER_URL, + DOCLING_SERVER_URL, + DOCLING_API_KEY, + DOCLING_PARAMS, + DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL, + MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY, + PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN, + RAG_TEXT_SPLITTER, + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + TIKTOKEN_ENCODING_NAME, + PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE, + YOUTUBE_LOADER_LANGUAGE, + YOUTUBE_LOADER_PROXY_URL, + # Retrieval (Web Search) + ENABLE_WEB_SEARCH, + WEB_SEARCH_ENGINE, + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_WEB_LOADER, + WEB_SEARCH_RESULT_COUNT, + WEB_SEARCH_CONCURRENT_REQUESTS, + WEB_FETCH_MAX_CONTENT_LENGTH, + WEB_SEARCH_TRUST_ENV, + WEB_SEARCH_DOMAIN_FILTER_LIST, + OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + JINA_API_KEY, + JINA_API_BASE_URL, + SEARCHAPI_API_KEY, + SEARCHAPI_ENGINE, + SERPAPI_API_KEY, + SERPAPI_ENGINE, + SEARXNG_QUERY_URL, + SEARXNG_LANGUAGE, + YACY_QUERY_URL, + YACY_USERNAME, + YACY_PASSWORD, + SERPER_API_KEY, + SERPLY_API_KEY, + DDGS_BACKEND, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, + TAVILY_API_KEY, + TAVILY_EXTRACT_DEPTH, + BING_SEARCH_V7_ENDPOINT, + BING_SEARCH_V7_SUBSCRIPTION_KEY, + BRAVE_SEARCH_API_KEY, + EXA_API_KEY, + PERPLEXITY_API_KEY, + PERPLEXITY_MODEL, + PERPLEXITY_SEARCH_CONTEXT_USAGE, + PERPLEXITY_SEARCH_API_URL, + SOUGOU_API_SID, + SOUGOU_API_SK, + KAGI_SEARCH_API_KEY, + MOJEEK_SEARCH_API_KEY, + BOCHA_SEARCH_API_KEY, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + GOOGLE_DRIVE_CLIENT_ID, + GOOGLE_DRIVE_API_KEY, + ENABLE_ONEDRIVE_INTEGRATION, + ONEDRIVE_CLIENT_ID_PERSONAL, + ONEDRIVE_CLIENT_ID_BUSINESS, + ONEDRIVE_SHAREPOINT_URL, + ONEDRIVE_SHAREPOINT_TENANT_ID, + ENABLE_ONEDRIVE_PERSONAL, + ENABLE_ONEDRIVE_BUSINESS, + ENABLE_RAG_HYBRID_SEARCH, + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + ENABLE_RAG_LOCAL_WEB_FETCH, + ENABLE_WEB_LOADER_SSL_VERIFICATION, + ENABLE_GOOGLE_DRIVE_INTEGRATION, + UPLOAD_DIR, + EXTERNAL_WEB_SEARCH_URL, + EXTERNAL_WEB_SEARCH_API_KEY, + EXTERNAL_WEB_LOADER_URL, + EXTERNAL_WEB_LOADER_API_KEY, + YANDEX_WEB_SEARCH_URL, + YANDEX_WEB_SEARCH_API_KEY, + YANDEX_WEB_SEARCH_CONFIG, + YOUCOM_API_KEY, + # WebUI + WEBUI_AUTH, + WEBUI_NAME, + WEBUI_BANNERS, + WEBHOOK_URL, + ADMIN_EMAIL, + SHOW_ADMIN_DETAILS, + JWT_EXPIRES_IN, + ENABLE_SIGNUP, + ENABLE_LOGIN_FORM, + ENABLE_PASSWORD_CHANGE_FORM, + ENABLE_API_KEYS, + ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + API_KEYS_ALLOWED_ENDPOINTS, + ENABLE_FOLDERS, + FOLDER_MAX_FILE_COUNT, + ENABLE_AUTOMATIONS, + AUTOMATION_MAX_COUNT, + AUTOMATION_MIN_INTERVAL, + ENABLE_CHANNELS, + ENABLE_CALENDAR, + ENABLE_NOTES, + ENABLE_USER_STATUS, + ENABLE_COMMUNITY_SHARING, + ENABLE_MESSAGE_RATING, + ENABLE_USER_WEBHOOKS, + ENABLE_EVALUATION_ARENA_MODELS, + BYPASS_ADMIN_ACCESS_CONTROL, + USER_PERMISSIONS, + DEFAULT_USER_ROLE, + DEFAULT_GROUP_ID, + PENDING_USER_OVERLAY_CONTENT, + PENDING_USER_OVERLAY_TITLE, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_MODELS, + DEFAULT_PINNED_MODELS, + DEFAULT_ARENA_MODEL, + MODEL_ORDER_LIST, + DEFAULT_MODEL_METADATA, + DEFAULT_MODEL_PARAMS, + EVALUATION_ARENA_MODELS, + # WebUI (OAuth) + ENABLE_OAUTH_ROLE_MANAGEMENT, + OAUTH_SUB_CLAIM, + OAUTH_ROLES_CLAIM, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, + OAUTH_USERNAME_CLAIM, + OAUTH_ALLOWED_ROLES, + OAUTH_ADMIN_ROLES, + # WebUI (LDAP) + ENABLE_LDAP, + LDAP_SERVER_LABEL, + LDAP_SERVER_HOST, + LDAP_SERVER_PORT, + LDAP_ATTRIBUTE_FOR_MAIL, + LDAP_ATTRIBUTE_FOR_USERNAME, + LDAP_SEARCH_FILTERS, + LDAP_SEARCH_BASE, + LDAP_APP_DN, + LDAP_APP_PASSWORD, + LDAP_USE_TLS, + LDAP_CA_CERT_FILE, + LDAP_VALIDATE_CERT, + LDAP_CIPHERS, + # LDAP Group Management + ENABLE_LDAP_GROUP_MANAGEMENT, + ENABLE_LDAP_GROUP_CREATION, + LDAP_ATTRIBUTE_FOR_GROUPS, + # Misc + ENV, + CACHE_DIR, + STATIC_DIR, + FRONTEND_BUILD_DIR, + CORS_ALLOW_ORIGIN, + DEFAULT_LOCALE, + OAUTH_PROVIDERS, + WEBUI_URL, + RESPONSE_WATERMARK, + # Admin + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_ANALYTICS, + BYPASS_ADMIN_ACCESS_CONTROL, + ENABLE_ADMIN_EXPORT, + # Tasks + TASK_MODEL, + TASK_MODEL_EXTERNAL, + ENABLE_TAGS_GENERATION, + ENABLE_TITLE_GENERATION, + ENABLE_FOLLOW_UP_GENERATION, + ENABLE_SEARCH_QUERY_GENERATION, + ENABLE_RETRIEVAL_QUERY_GENERATION, + ENABLE_AUTOCOMPLETE_GENERATION, + TITLE_GENERATION_PROMPT_TEMPLATE, + FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + TAGS_GENERATION_PROMPT_TEMPLATE, + IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + VOICE_MODE_PROMPT_TEMPLATE, + QUERY_GENERATION_PROMPT_TEMPLATE, + AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + AppConfig, + reset_config, + async_reset_config, +) +from open_webui.env import ( + ENABLE_CUSTOM_MODEL_FALLBACK, + LICENSE_KEY, + AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, + ENABLE_AUDIT_GET_REQUESTS, + AUDIT_LOG_LEVEL, + CHANGELOG, + REDIS_URL, + REDIS_CLUSTER, + REDIS_KEY_PREFIX, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_PORT, + GLOBAL_LOG_LEVEL, + MAX_BODY_LOG_SIZE, + SAFE_MODE, + VERSION, + DEPLOYMENT_ID, + INSTANCE_ID, + WEBUI_BUILD_HASH, + WEBUI_SECRET_KEY, + WEBUI_SESSION_COOKIE_SAME_SITE, + WEBUI_SESSION_COOKIE_SECURE, + ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + # SCIM + ENABLE_SCIM, + SCIM_TOKEN, + ENABLE_COMPRESSION_MIDDLEWARE, + ENABLE_WEBSOCKET_SUPPORT, + BYPASS_MODEL_ACCESS_CONTROL, + RESET_CONFIG_ON_START, + ENABLE_VERSION_UPDATE_CHECK, + ENABLE_OTEL, + EXTERNAL_PWA_MANIFEST_URL, + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_STAR_SESSIONS_MIDDLEWARE, + ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + # Admin Account Runtime Creation + WEBUI_ADMIN_EMAIL, + WEBUI_ADMIN_PASSWORD, + WEBUI_ADMIN_NAME, + ENABLE_EASTER_EGGS, + LOG_FORMAT, + # OAuth Back-Channel Logout + ENABLE_OAUTH_BACKCHANNEL_LOGOUT, +) + + +from open_webui.utils.models import ( + get_all_models, + get_all_base_models, + check_model_access, + get_filtered_models, +) +from open_webui.utils.chat import ( + generate_chat_completion as chat_completion_handler, + chat_completed as chat_completed_handler, +) +from open_webui.utils.actions import chat_action as chat_action_handler +from open_webui.utils.embeddings import generate_embeddings +from open_webui.utils.middleware import ( + build_chat_response_context, + process_chat_payload, + process_chat_response, +) +from open_webui.utils.tools import set_tool_servers, set_terminal_servers + +from open_webui.utils.auth import ( + get_license_data, + get_http_authorization_cred, + decode_token, + get_admin_user, + get_verified_user, + create_admin_user, +) +from open_webui.utils.plugin import install_tool_and_function_dependencies +from open_webui.utils.oauth import ( + get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, + encrypt_data, + decrypt_data, + resolve_oauth_client_info, + OAuthManager, + OAuthClientManager, + OAuthClientInformationFull, +) +from open_webui.utils.security_headers import SecurityHeadersMiddleware +from open_webui.utils.redis import get_redis_connection + +from open_webui.tasks import ( + redis_task_command_listener, + list_task_ids_by_item_id, + create_task, + stop_task, + stop_item_tasks, + list_tasks, +) # Import from tasks.py + +from open_webui.utils.redis import get_sentinels_from_env + + +from open_webui.constants import ERROR_MESSAGES, TASKS + +if SAFE_MODE: + print('SAFE MODE ENABLED') + # Functions.deactivate_all_functions() is awaited in lifespan below + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +class SPAStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): + try: + return await super().get_response(path, scope) + except (HTTPException, StarletteHTTPException) as ex: + if ex.status_code == 404: + if path.endswith('.js'): + # Return 404 for javascript files + raise ex + else: + return await super().get_response('index.html', scope) + else: + raise ex + + +if LOG_FORMAT != 'json': + print(rf""" + ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ +██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ +██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ +██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║ +╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║ + ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝ + + +v{VERSION} - building the best AI user interface. +{f'Commit: {WEBUI_BUILD_HASH}' if WEBUI_BUILD_HASH != 'dev-build' else ''} +https://github.com/open-webui/open-webui +""") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Store reference to main event loop for sync->async calls (e.g., embedding generation) + # This allows sync functions to schedule work on the main loop without blocking health checks + app.state.main_loop = asyncio.get_running_loop() + + app.state.instance_id = INSTANCE_ID + start_logger() + + if RESET_CONFIG_ON_START: + await async_reset_config() + + if LICENSE_KEY: + get_license_data(app, LICENSE_KEY) + + # Create admin account from env vars if specified and no users exist + if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD: + if await create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME): + # Disable signup since we now have an admin + app.state.config.ENABLE_SIGNUP = False + + if SAFE_MODE: + await Functions.deactivate_all_functions() + + # This should be blocking (sync) so functions are not deactivated on first /get_models calls + # when the first user lands on the / route. + log.info('Installing external dependencies of functions and tools...') + await install_tool_and_function_dependencies() + + app.state.redis = get_redis_connection( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), + redis_cluster=REDIS_CLUSTER, + async_mode=True, + ) + + if app.state.redis is not None: + app.state.redis_task_command_listener = asyncio.create_task(redis_task_command_listener(app)) + + if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0: + limiter = anyio.to_thread.current_default_thread_limiter() + limiter.total_tokens = THREAD_POOL_SIZE + + asyncio.create_task(periodic_usage_pool_cleanup()) + asyncio.create_task(periodic_session_pool_cleanup()) + + from open_webui.utils.automations import scheduler_worker_loop + + asyncio.create_task(scheduler_worker_loop(app)) + + if app.state.config.ENABLE_BASE_MODELS_CACHE: + try: + await get_all_models( + Request( + # Creating a mock request object to pass to get_all_models + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + ), + None, + ) + except Exception as e: + log.warning(f'Failed to pre-fetch models at startup: {e}') + + # Pre-fetch tool server specs so the first request doesn't pay the latency cost + if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: + log.info('Initializing tool servers...') + try: + mock_request = Request( + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + ) + await set_tool_servers(mock_request) + log.info(f'Initialized {len(app.state.TOOL_SERVERS)} tool server(s)') + + await set_terminal_servers(mock_request) + log.info(f'Initialized {len(app.state.TERMINAL_SERVERS)} terminal server(s)') + except Exception as e: + log.warning(f'Failed to initialize tool/terminal servers at startup: {e}') + + # Mark application as ready to accept traffic from a startup perspective. + app.state.startup_complete = True + + yield + + # Shutdown: clean up shared resources + from open_webui.utils.session_pool import close_session + + await close_session() + + if hasattr(app.state, 'redis_task_command_listener'): + app.state.redis_task_command_listener.cancel() + + +app = FastAPI( + title='Open WebUI', + docs_url='/docs' if ENV == 'dev' else None, + openapi_url='/openapi.json' if ENV == 'dev' else None, + redoc_url=None, + lifespan=lifespan, +) + +# Used by readiness checks to gate traffic until startup work is done. +app.state.startup_complete = False + +# For Open WebUI OIDC/OAuth2 +oauth_manager = OAuthManager(app) +app.state.oauth_manager = oauth_manager + +# For Integrations +oauth_client_manager = OAuthClientManager(app) +app.state.oauth_client_manager = oauth_client_manager + +app.state.instance_id = None +app.state.config = AppConfig( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), + redis_cluster=REDIS_CLUSTER, + redis_key_prefix=REDIS_KEY_PREFIX, +) +app.state.redis = None + +app.state.WEBUI_NAME = WEBUI_NAME +app.state.LICENSE_METADATA = None + + +######################################## +# +# OPENTELEMETRY +# +######################################## + +if ENABLE_OTEL: + from open_webui.utils.telemetry.setup import setup as setup_opentelemetry + + setup_opentelemetry(app=app, db_engine=engine) + + +######################################## +# +# OLLAMA +# +######################################## + + +app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API +app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS +app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS + +app.state.OLLAMA_MODELS = {} + +######################################## +# +# OPENAI +# +######################################## + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API +app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS +app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS + +app.state.OPENAI_MODELS = {} + +######################################## +# +# TOOL SERVERS +# +######################################## + +app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS +app.state.TOOL_SERVERS = [] + +######################################## +# +# TERMINAL SERVER +# +######################################## + +app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS +app.state.TERMINAL_SERVERS = [] + +######################################## +# +# DIRECT CONNECTIONS +# +######################################## + +app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS + +######################################## +# +# SCIM +# +######################################## + +app.state.ENABLE_SCIM = ENABLE_SCIM +app.state.SCIM_TOKEN = SCIM_TOKEN + +######################################## +# +# MODELS +# +######################################## + +app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE +app.state.BASE_MODELS = [] + +######################################## +# +# WEBUI +# +######################################## + +app.state.config.WEBUI_URL = WEBUI_URL +app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP +app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM +app.state.config.ENABLE_PASSWORD_CHANGE_FORM = ENABLE_PASSWORD_CHANGE_FORM + +app.state.config.ENABLE_API_KEYS = ENABLE_API_KEYS +app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS +app.state.config.API_KEYS_ALLOWED_ENDPOINTS = API_KEYS_ALLOWED_ENDPOINTS + +app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN + +app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS +app.state.config.ADMIN_EMAIL = ADMIN_EMAIL + + +app.state.config.DEFAULT_MODELS = DEFAULT_MODELS +app.state.config.DEFAULT_PINNED_MODELS = DEFAULT_PINNED_MODELS +app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST +app.state.config.DEFAULT_MODEL_METADATA = DEFAULT_MODEL_METADATA +app.state.config.DEFAULT_MODEL_PARAMS = DEFAULT_MODEL_PARAMS + + +app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS +app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE +app.state.config.DEFAULT_GROUP_ID = DEFAULT_GROUP_ID + +app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT +app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE + +app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK + +app.state.config.USER_PERMISSIONS = USER_PERMISSIONS +app.state.config.WEBHOOK_URL = WEBHOOK_URL +app.state.config.BANNERS = WEBUI_BANNERS + + +app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS +app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT +app.state.config.ENABLE_AUTOMATIONS = ENABLE_AUTOMATIONS +app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT +app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL +app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS +app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR +app.state.config.ENABLE_NOTES = ENABLE_NOTES +app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING +app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING +app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS +app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS + +app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS +app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS + +# Migrate legacy access_control → access_grants on boot +from open_webui.utils.access_control import migrate_access_control + +connections = app.state.config.TOOL_SERVER_CONNECTIONS +if any('access_control' in c.get('config', {}) for c in connections): + for connection in connections: + migrate_access_control(connection.get('config', {})) + app.state.config.TOOL_SERVER_CONNECTIONS = connections + +arena_models = app.state.config.EVALUATION_ARENA_MODELS +if any('access_control' in m.get('meta', {}) for m in arena_models): + for model in arena_models: + migrate_access_control(model.get('meta', {})) + app.state.config.EVALUATION_ARENA_MODELS = arena_models + +app.state.config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM +app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM +app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM +app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM + +app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT +app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM +app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES +app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES + +app.state.config.ENABLE_LDAP = ENABLE_LDAP +app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL +app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST +app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT +app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL +app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME +app.state.config.LDAP_APP_DN = LDAP_APP_DN +app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD +app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE +app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS +app.state.config.LDAP_USE_TLS = LDAP_USE_TLS +app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE +app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT +app.state.config.LDAP_CIPHERS = LDAP_CIPHERS + +# For LDAP Group Management +app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT = ENABLE_LDAP_GROUP_MANAGEMENT +app.state.config.ENABLE_LDAP_GROUP_CREATION = ENABLE_LDAP_GROUP_CREATION +app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS = LDAP_ATTRIBUTE_FOR_GROUPS + + +app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER +app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER +app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL +app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL + +app.state.USER_COUNT = None + +app.state.TOOLS = {} +app.state.TOOL_CONTENTS = {} + +app.state.FUNCTIONS = {} +app.state.FUNCTION_CONTENTS = {} + +######################################## +# +# RETRIEVAL +# +######################################## + + +app.state.config.TOP_K = RAG_TOP_K +app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER +app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD +app.state.config.HYBRID_BM25_WEIGHT = RAG_HYBRID_BM25_WEIGHT + + +app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS +app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE +app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT +app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = FILE_IMAGE_COMPRESSION_WIDTH +app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = FILE_IMAGE_COMPRESSION_HEIGHT + + +app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT +app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL +app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH +app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS +app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION + +app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE +app.state.config.DATALAB_MARKER_API_KEY = DATALAB_MARKER_API_KEY +app.state.config.DATALAB_MARKER_API_BASE_URL = DATALAB_MARKER_API_BASE_URL +app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG = DATALAB_MARKER_ADDITIONAL_CONFIG +app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE +app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR +app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE +app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR +app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION +app.state.config.DATALAB_MARKER_FORMAT_LINES = DATALAB_MARKER_FORMAT_LINES +app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM +app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT +app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL +app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY +app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL +app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL +app.state.config.DOCLING_API_KEY = DOCLING_API_KEY +app.state.config.DOCLING_PARAMS = DOCLING_PARAMS +app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT +app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY +app.state.config.DOCUMENT_INTELLIGENCE_MODEL = DOCUMENT_INTELLIGENCE_MODEL +app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL +app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY +app.state.config.PADDLEOCR_VL_BASE_URL = PADDLEOCR_VL_BASE_URL +app.state.config.PADDLEOCR_VL_TOKEN = PADDLEOCR_VL_TOKEN +app.state.config.MINERU_API_MODE = MINERU_API_MODE +app.state.config.MINERU_API_URL = MINERU_API_URL +app.state.config.MINERU_API_KEY = MINERU_API_KEY +app.state.config.MINERU_API_TIMEOUT = MINERU_API_TIMEOUT +app.state.config.MINERU_PARAMS = MINERU_PARAMS + +app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER +app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER + +app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME + +app.state.config.CHUNK_SIZE = CHUNK_SIZE +app.state.config.CHUNK_MIN_SIZE_TARGET = CHUNK_MIN_SIZE_TARGET +app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP + + +app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE +app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE +app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING +app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = RAG_EMBEDDING_CONCURRENT_REQUESTS + +app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE +app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL +app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL +app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY +app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT = RAG_EXTERNAL_RERANKER_TIMEOUT +app.state.config.RAG_RERANKING_BATCH_SIZE = RAG_RERANKING_BATCH_SIZE + +app.state.config.RAG_TEMPLATE = RAG_TEMPLATE + +app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL +app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY + +app.state.config.RAG_AZURE_OPENAI_BASE_URL = RAG_AZURE_OPENAI_BASE_URL +app.state.config.RAG_AZURE_OPENAI_API_KEY = RAG_AZURE_OPENAI_API_KEY +app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION + +app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL +app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY + +app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES +app.state.config.PDF_LOADER_MODE = PDF_LOADER_MODE + +app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE +app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL + + +app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH +app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE +app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST +app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT +app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS +app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = WEB_FETCH_MAX_CONTENT_LENGTH + +app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE +app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS +app.state.config.WEB_LOADER_TIMEOUT = WEB_LOADER_TIMEOUT + +app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV +app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL +app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER + +app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION +app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION + +app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = OLLAMA_CLOUD_WEB_SEARCH_API_KEY +app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL +app.state.config.SEARXNG_LANGUAGE = SEARXNG_LANGUAGE +app.state.config.YACY_QUERY_URL = YACY_QUERY_URL +app.state.config.YACY_USERNAME = YACY_USERNAME +app.state.config.YACY_PASSWORD = YACY_PASSWORD +app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY +app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID +app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY +app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY +app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY +app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY +app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY +app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS +app.state.config.SERPER_API_KEY = SERPER_API_KEY +app.state.config.SERPLY_API_KEY = SERPLY_API_KEY +app.state.config.DDGS_BACKEND = DDGS_BACKEND +app.state.config.TAVILY_API_KEY = TAVILY_API_KEY +app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY +app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE +app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY +app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE +app.state.config.JINA_API_KEY = JINA_API_KEY +app.state.config.JINA_API_BASE_URL = JINA_API_BASE_URL +app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT +app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY +app.state.config.EXA_API_KEY = EXA_API_KEY +app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY +app.state.config.PERPLEXITY_MODEL = PERPLEXITY_MODEL +app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = PERPLEXITY_SEARCH_CONTEXT_USAGE +app.state.config.PERPLEXITY_SEARCH_API_URL = PERPLEXITY_SEARCH_API_URL +app.state.config.SOUGOU_API_SID = SOUGOU_API_SID +app.state.config.SOUGOU_API_SK = SOUGOU_API_SK +app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL +app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY +app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL +app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY +app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL +app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY +app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG +app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY + + +app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL +app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT +app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL +app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY +app.state.config.FIRECRAWL_TIMEOUT = FIRECRAWL_TIMEOUT +app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH + +app.state.EMBEDDING_FUNCTION = None +app.state.RERANKING_FUNCTION = None +app.state.ef = None +app.state.rf = None + +app.state.YOUTUBE_LOADER_TRANSLATION = None + + +try: + app.state.ef = get_ef(app.state.config.RAG_EMBEDDING_ENGINE, app.state.config.RAG_EMBEDDING_MODEL) + if app.state.config.ENABLE_RAG_HYBRID_SEARCH and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + app.state.rf = get_rf( + app.state.config.RAG_RERANKING_ENGINE, + app.state.config.RAG_RERANKING_MODEL, + app.state.config.RAG_EXTERNAL_RERANKER_URL, + app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + ) + else: + app.state.rf = None +except Exception as e: + log.error(f'Error updating models: {e}') + pass + + +app.state.EMBEDDING_FUNCTION = get_embedding_function( + app.state.config.RAG_EMBEDDING_ENGINE, + app.state.config.RAG_EMBEDDING_MODEL, + embedding_function=app.state.ef, + url=( + app.state.config.RAG_OPENAI_API_BASE_URL + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + app.state.config.RAG_OLLAMA_BASE_URL + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) + ), + key=( + app.state.config.RAG_OPENAI_API_KEY + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + app.state.config.RAG_OLLAMA_API_KEY + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else app.state.config.RAG_AZURE_OPENAI_API_KEY + ) + ), + embedding_batch_size=app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + app.state.config.RAG_AZURE_OPENAI_API_VERSION + if app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' + else None + ), + enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, +) + +app.state.RERANKING_FUNCTION = get_reranking_function( + app.state.config.RAG_RERANKING_ENGINE, + app.state.config.RAG_RERANKING_MODEL, + reranking_function=app.state.rf, + reranking_batch_size=app.state.config.RAG_RERANKING_BATCH_SIZE, +) + +######################################## +# +# CODE EXECUTION +# +######################################## + +app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION +app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE +app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL +app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = CODE_EXECUTION_JUPYTER_AUTH_PASSWORD +app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT + +app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER +app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE +app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE + +app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = CODE_INTERPRETER_JUPYTER_AUTH_TOKEN +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD +app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT + +######################################## +# +# IMAGES +# +######################################## + +app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE +app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION +app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION +app.state.config.ENABLE_MEMORIES = ENABLE_MEMORIES + +app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL +app.state.config.IMAGE_SIZE = IMAGE_SIZE +app.state.config.IMAGE_STEPS = IMAGE_STEPS + +app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL +app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION +app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY +app.state.config.IMAGES_OPENAI_API_PARAMS = IMAGES_OPENAI_API_PARAMS + +app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL +app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY +app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = IMAGES_GEMINI_ENDPOINT_METHOD + +app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL +app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH +app.state.config.AUTOMATIC1111_PARAMS = AUTOMATIC1111_PARAMS + +app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL +app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY +app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW +app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES + + +app.state.config.ENABLE_IMAGE_EDIT = ENABLE_IMAGE_EDIT +app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE +app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL +app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE +app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = IMAGES_EDIT_OPENAI_API_BASE_URL +app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY +app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION +app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL +app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY +app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL +app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY +app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW +app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES + + +######################################## +# +# AUDIO +# +######################################## + +app.state.config.STT_ENGINE = AUDIO_STT_ENGINE +app.state.config.STT_MODEL = AUDIO_STT_MODEL +app.state.config.STT_SUPPORTED_CONTENT_TYPES = AUDIO_STT_SUPPORTED_CONTENT_TYPES + +app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL +app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY + +app.state.config.WHISPER_MODEL = WHISPER_MODEL +app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY + +app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY +app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION +app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES +app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL +app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS + +app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY +app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL +app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS + +app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE + +app.state.config.TTS_MODEL = AUDIO_TTS_MODEL +app.state.config.TTS_VOICE = AUDIO_TTS_VOICE + +app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL +app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY +app.state.config.TTS_OPENAI_PARAMS = AUDIO_TTS_OPENAI_PARAMS + +app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY +app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON + + +app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION +app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL +app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT + +app.state.config.TTS_MISTRAL_API_KEY = AUDIO_TTS_MISTRAL_API_KEY +app.state.config.TTS_MISTRAL_API_BASE_URL = AUDIO_TTS_MISTRAL_API_BASE_URL + + +app.state.faster_whisper_model = None +app.state.speech_synthesiser = None +app.state.speech_speaker_embeddings_dataset = None + + +######################################## +# +# TASKS +# +######################################## + + +app.state.config.TASK_MODEL = TASK_MODEL +app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL + + +app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION +app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION +app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION +app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION +app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION +app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION + + +app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE +app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE +app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE +app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = FOLLOW_UP_GENERATION_PROMPT_TEMPLATE + +app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE +app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE +app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE +app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH +app.state.config.VOICE_MODE_PROMPT_TEMPLATE = VOICE_MODE_PROMPT_TEMPLATE + + +######################################## +# +# WEBUI +# +######################################## + +app.state.MODELS = MODELS + +# Add the middleware to the app +if ENABLE_COMPRESSION_MIDDLEWARE: + app.add_middleware(CompressMiddleware) + + +# All HTTP middlewares below are pure-ASGI implementations. The previous +# `BaseHTTPMiddleware` / `@app.middleware('http')` versions wrapped the +# downstream app in an anyio task group whose cancel scope cancelled +# in-flight DB calls (and any other awaits) on client disconnect / +# response completion — which surfaced as noisy SQLAlchemy +# `terminate_force_close` tracebacks under aiosqlite and as random +# CancelledError storms across the request path. See +# `open_webui.utils.asgi_middleware` for the rationale. +app.add_middleware(RedirectMiddleware) +app.add_middleware(SecurityHeadersMiddleware) +app.add_middleware(CommitSessionMiddleware) +app.add_middleware(AuthTokenMiddleware, fastapi_app=app) +app.add_middleware(WebsocketUpgradeGuardMiddleware) + + +app.add_middleware( + CORSMiddleware, + allow_origins=CORS_ALLOW_ORIGIN, + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + + +app.mount('/ws', socket_app) + + +app.include_router(ollama.router, prefix='/ollama', tags=['ollama']) +app.include_router(openai.router, prefix='/openai', tags=['openai']) + + +app.include_router(pipelines.router, prefix='/api/v1/pipelines', tags=['pipelines']) +app.include_router(tasks.router, prefix='/api/v1/tasks', tags=['tasks']) +app.include_router(images.router, prefix='/api/v1/images', tags=['images']) + +app.include_router(audio.router, prefix='/api/v1/audio', tags=['audio']) +app.include_router(retrieval.router, prefix='/api/v1/retrieval', tags=['retrieval']) + +app.include_router(configs.router, prefix='/api/v1/configs', tags=['configs']) + +app.include_router(auths.router, prefix='/api/v1/auths', tags=['auths']) +app.include_router(users.router, prefix='/api/v1/users', tags=['users']) + + +app.include_router(channels.router, prefix='/api/v1/channels', tags=['channels']) +app.include_router(chats.router, prefix='/api/v1/chats', tags=['chats']) +app.include_router(notes.router, prefix='/api/v1/notes', tags=['notes']) + + +app.include_router(models.router, prefix='/api/v1/models', tags=['models']) +app.include_router(knowledge.router, prefix='/api/v1/knowledge', tags=['knowledge']) +app.include_router(prompts.router, prefix='/api/v1/prompts', tags=['prompts']) +app.include_router(tools.router, prefix='/api/v1/tools', tags=['tools']) +app.include_router(skills.router, prefix='/api/v1/skills', tags=['skills']) + +app.include_router(memories.router, prefix='/api/v1/memories', tags=['memories']) +app.include_router(folders.router, prefix='/api/v1/folders', tags=['folders']) +app.include_router(groups.router, prefix='/api/v1/groups', tags=['groups']) +app.include_router(files.router, prefix='/api/v1/files', tags=['files']) +app.include_router(functions.router, prefix='/api/v1/functions', tags=['functions']) +app.include_router(evaluations.router, prefix='/api/v1/evaluations', tags=['evaluations']) +if ENABLE_ADMIN_ANALYTICS: + app.include_router(analytics.router, prefix='/api/v1/analytics', tags=['analytics']) +app.include_router(utils.router, prefix='/api/v1/utils', tags=['utils']) +app.include_router(terminals.router, prefix='/api/v1/terminals', tags=['terminals']) +app.include_router(automations.router, prefix='/api/v1/automations', tags=['automations']) +app.include_router(calendar.router, prefix='/api/v1/calendars', tags=['calendars']) + +# SCIM 2.0 API for identity management +if ENABLE_SCIM: + app.include_router(scim.router, prefix='/api/v1/scim/v2', tags=['scim']) + + +try: + audit_level = AuditLevel(AUDIT_LOG_LEVEL) +except ValueError as e: + logger.error(f'Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}') + audit_level = AuditLevel.NONE + +if audit_level != AuditLevel.NONE: + app.add_middleware( + AuditLoggingMiddleware, + audit_level=audit_level, + excluded_paths=AUDIT_EXCLUDED_PATHS, + included_paths=AUDIT_INCLUDED_PATHS, + audit_get_requests=ENABLE_AUDIT_GET_REQUESTS, + max_body_size=MAX_BODY_LOG_SIZE, + ) +################################## +# +# Chat Endpoints +# +################################## + + +@app.get('/api/models') +@app.get('/api/v1/models') # Experimental: Compatibility with OpenAI API +async def get_models(request: Request, refresh: bool = False, user=Depends(get_verified_user)): + all_models = await get_all_models(request, refresh=refresh, user=user) + + models = [] + for model in all_models: + # Filter out filter pipelines + if 'pipeline' in model and model['pipeline'].get('type', None) == 'filter': + continue + + # Remove profile image URL to reduce payload size + if model.get('info', {}).get('meta', {}).get('profile_image_url'): + model['info']['meta'].pop('profile_image_url', None) + + try: + model_tags = [tag.get('name') for tag in model.get('info', {}).get('meta', {}).get('tags', [])] + tags = [tag.get('name') for tag in model.get('tags', [])] + + tags = list(set(model_tags + tags)) + model['tags'] = [{'name': tag} for tag in tags] + except Exception as e: + log.debug(f'Error processing model tags: {e}') + model['tags'] = [] + pass + + models.append(model) + + model_order_list = request.app.state.config.MODEL_ORDER_LIST + if model_order_list: + model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)} + # Sort models by order list priority, with fallback for those not in the list + models.sort( + key=lambda model: ( + model_order_dict.get(model.get('id', ''), float('inf')), + (model.get('name', '') or ''), + ) + ) + + models = await get_filtered_models(models, user) + + log.debug( + f'/api/models returned filtered models accessible to the user: {json.dumps([model.get("id") for model in models])}' + ) + return {'data': models} + + +@app.get('/api/models/base') +async def get_base_models(request: Request, user=Depends(get_admin_user)): + models = await get_all_base_models(request, user=user) + return {'data': models} + + +################################## +# Embeddings +################################## + + +@app.post('/api/embeddings') +@app.post('/api/v1/embeddings') # Experimental: Compatibility with OpenAI API +async def embeddings(request: Request, form_data: dict, user=Depends(get_verified_user)): + """ + OpenAI-compatible embeddings endpoint. + + This handler: + - Performs user/model checks and dispatches to the correct backend. + - Supports OpenAI, Ollama, arena models, pipelines, and any compatible provider. + + Args: + request (Request): Request context. + form_data (dict): OpenAI-like payload (e.g., {"model": "...", "input": [...]}) + user (UserModel): Authenticated user. + + Returns: + dict: OpenAI-compatible embeddings response. + """ + # Make sure models are loaded in app state + if not request.app.state.MODELS: + await get_all_models(request, user=user) + # Use generic dispatcher in utils.embeddings + return await generate_embeddings(request, form_data, user) + + +@app.post('/api/chat/completions') +@app.post('/api/v1/chat/completions') # Experimental: Compatibility with OpenAI API +async def chat_completion( + request: Request, + form_data: dict, + user=Depends(get_verified_user), +): + if not request.app.state.MODELS: + await get_all_models(request, user=user) + + model_id = form_data.get('model', None) + model_item = form_data.pop('model_item', {}) + tasks = form_data.pop('background_tasks', None) + + metadata = {} + try: + model_info = None + if not model_item.get('direct', False): + if model_id not in request.app.state.MODELS: + raise Exception('Model not found') + + model = request.app.state.MODELS[model_id] + model_info = await Models.get_model_by_id(model_id) + + # Check if user has access to the model + if not BYPASS_MODEL_ACCESS_CONTROL and (user.role != 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL): + try: + await check_model_access(user, model) + except Exception as e: + raise e + else: + model = model_item + + request.state.direct = True + request.state.model = model + + # Model params: global defaults as base, per-model overrides win + default_model_params = getattr(request.app.state.config, 'DEFAULT_MODEL_PARAMS', None) or {} + model_info_params = { + **default_model_params, + **(model_info.params.model_dump() if model_info and model_info.params else {}), + } + + # Check base model existence for custom models + if model_info and model_info.base_model_id: + base_model_id = model_info.base_model_id + if base_model_id not in request.app.state.MODELS: + if ENABLE_CUSTOM_MODEL_FALLBACK: + default_models = (request.app.state.config.DEFAULT_MODELS or '').split(',') + + fallback_model_id = default_models[0].strip() if default_models[0] else None + + if fallback_model_id and fallback_model_id in request.app.state.MODELS: + # Update model and form_data so routing uses the fallback model's type + model = request.app.state.MODELS[fallback_model_id] + form_data['model'] = fallback_model_id + else: + raise Exception('Model not found') + else: + raise Exception('Model not found') + + # Chat Params + stream_delta_chunk_size = form_data.get('params', {}).get('stream_delta_chunk_size') + reasoning_tags = form_data.get('params', {}).get('reasoning_tags') + + # Model Params + if model_info_params.get('stream_response') is not None: + form_data['stream'] = model_info_params.get('stream_response') + + if model_info_params.get('stream_delta_chunk_size'): + stream_delta_chunk_size = model_info_params.get('stream_delta_chunk_size') + + if model_info_params.get('reasoning_tags') is not None: + reasoning_tags = model_info_params.get('reasoning_tags') + + # parent_id signals intent: + # null → new chat (root message, no parent) + # value → follow-up (user message's parentId = prev assistant) + # absent → legacy caller, no chat management + is_new_chat = 'parent_id' in form_data and form_data['parent_id'] is None and not form_data.get('chat_id') + parent_id = form_data.pop('parent_id', None) + form_data.pop('new_chat', None) # Legacy field + + # Multi-model: {model_id: assistant_message_id} + # Single-model fallback: built from 'model' + 'id' + message_ids = form_data.pop('message_ids', None) + if not message_ids: + message_ids = {model_id: form_data.pop('id', None)} + else: + form_data.pop('id', None) + + user_message = form_data.pop('user_message', None) or form_data.pop('parent_message', None) + metadata = { + 'user_id': user.id, + 'chat_id': form_data.pop('chat_id', None), + 'user_message': user_message, + 'user_message_id': user_message.get('id') if user_message else None, + 'session_id': form_data.pop('session_id', None), + 'folder_id': form_data.pop('folder_id', None), + 'filter_ids': form_data.pop('filter_ids', []), + 'tool_ids': form_data.get('tool_ids', None), + 'tool_servers': form_data.pop('tool_servers', None), + 'files': form_data.get('files', None), + 'features': form_data.get('features', {}), + 'variables': form_data.get('variables', {}), + 'model': model, + 'direct': model_item.get('direct', False), + 'params': { + 'stream_delta_chunk_size': stream_delta_chunk_size, + 'reasoning_tags': reasoning_tags, + 'function_calling': ( + 'native' + if ( + form_data.get('params', {}).get('function_calling') == 'native' + or model_info_params.get('function_calling') == 'native' + ) + else 'default' + ), + }, + } + + if is_new_chat: + metadata['chat_id'] = str(uuid4()) + + if metadata.get('chat_id') and user: + chat_id = metadata['chat_id'] + if not chat_id.startswith('local:'): # temporary chats are not stored + if is_new_chat: + # Build the full history upfront with ALL assistant placeholders + user_message = metadata.get('user_message') or {} + user_message_id = user_message.get('id') if user_message else None + + history_messages = {} + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + if user_message_id and user_message: + user_message['childrenIds'] = all_assistant_ids + history_messages[user_message_id] = user_message + + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + history_messages[assistant_message_id] = { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + } + + await Chats.insert_new_chat( + chat_id, + user.id, + ChatForm( + chat={ + 'id': chat_id, + 'title': 'New Chat', + 'models': list(message_ids.keys()), + 'history': { + 'currentId': all_assistant_ids[0] if all_assistant_ids else user_message_id, + 'messages': history_messages, + }, + 'messages': [ + {'role': 'user', 'content': user_message.get('content', '')}, + ] + if user_message_id + else [], + 'tags': [], + 'timestamp': int(time.time() * 1000), + }, + folder_id=metadata.get('folder_id'), + ), + ) + + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message_id, + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + else: + # Existing chat — verify ownership + if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + # Save user message to DB + user_message = metadata.get('user_message') or {} + if user_message and user_message.get('id'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message['id'], + user_message, + ) + + # Link grandparent → user message (childrenIds) + grandparent_id = user_message.get('parentId') + if grandparent_id: + grandparent = await Chats.get_message_by_id_and_message_id(chat_id, grandparent_id) + if grandparent: + child_ids = grandparent.get('childrenIds', []) + if user_message['id'] not in child_ids: + child_ids.append(user_message['id']) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, grandparent_id, {'childrenIds': child_ids} + ) + + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message.get('id'), + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + + # Save ALL assistant placeholders + user_message_id = metadata.get('user_message_id') + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + # Link user message → all assistant messages (childrenIds) + if user_message_id and all_assistant_ids: + existing_user_message = await Chats.get_message_by_id_and_message_id(chat_id, user_message_id) + if existing_user_message: + child_ids = existing_user_message.get('childrenIds', []) + for assistant_id in all_assistant_ids: + if assistant_id not in child_ids: + child_ids.append(assistant_id) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message_id, + {'childrenIds': child_ids}, + ) + + # Save each assistant placeholder + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + assistant_message_id, + { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + }, + ) + + request.state.metadata = metadata + form_data['metadata'] = metadata + + except HTTPException: + raise + except Exception as e: + log.warning(f'Error processing chat metadata: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + async def process_chat(request, form_data, user, metadata, model, tasks=None): + try: + form_data, metadata, events = await process_chat_payload(request, form_data, user, metadata, model) + + response = await chat_completion_handler(request, form_data, user) + + # When the upstream provider returns an error (e.g. HTTP 400 + # content-filter, quota exceeded), generate_chat_completion + # returns a JSONResponse instead of raising. Detect this and + # raise so the except-block below emits chat:message:error + + # chat:tasks:cancel, unblocking the frontend. + if isinstance(response, JSONResponse) and response.status_code >= 400: + try: + error_body = json.loads(response.body.decode('utf-8', 'replace')) + detail = error_body.get('error', error_body) if isinstance(error_body, dict) else error_body + if isinstance(detail, dict): + detail = detail.get('message', detail.get('detail', str(detail))) + except Exception: + detail = f'Provider returned HTTP {response.status_code}' + raise Exception(detail) + + ctx = await build_chat_response_context(request, form_data, user, model, metadata, tasks, events) + + return await process_chat_response(response, ctx) + except asyncio.CancelledError: + log.info('Chat processing was cancelled') + try: + + async def emit_cancel_event(): + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter({'type': 'chat:tasks:cancel'}) + + await asyncio.shield(emit_cancel_event()) + except Exception: + pass + raise # re-raise to ensure proper task cancellation handling + except Exception as e: + error_detail = e.detail if isinstance(e, HTTPException) else str(e) + log.error('Error processing chat payload: %s', error_detail) + if metadata.get('chat_id') and metadata.get('message_id'): + # Update the chat message with the error + try: + if not metadata['chat_id'].startswith('local:'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'parentId': metadata.get('user_message_id', None), + 'error': {'content': error_detail}, + }, + ) + + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter( + { + 'type': 'chat:message:error', + 'data': {'error': {'content': error_detail}}, + } + ) + await event_emitter( + {'type': 'chat:tasks:cancel'}, + ) + + except Exception: + pass + else: + # No chat_id/message_id → legacy/direct API path with no + # WebSocket error channel. We must surface the error as + # a proper HTTP response; without this the function would + # return None which FastAPI serializes as null. #23924 + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=error_detail, + ) + finally: + # MCP cleanup — MUST run in the SAME asyncio task as + # connect() because the MCP SDK's streamablehttp_client + # uses anyio task groups whose cancel scopes enforce + # same-task exit. Do NOT wrap in asyncio.shield() or + # asyncio.wait_for() — both create a new task. + try: + if mcp_clients := metadata.get('mcp_clients'): + for client in reversed(list(mcp_clients.values())): + try: + await client.disconnect() + except Exception as e: + log.debug(f'Error disconnecting MCP client: {e}') + except asyncio.CancelledError: + # Let the client close asynchronously by GC + pass + except Exception as e: + log.debug(f'Error cleaning up MCP clients: {e}') + except asyncio.CancelledError: + pass + + try: + if metadata.get('chat_id'): + + async def emit_inactive_event(): + try: + event_emitter = await get_event_emitter(metadata, update_db=False) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': False}}) + except Exception: + pass + + try: + # Shield the event emission so it finishes even if the main task is cancelled + await asyncio.shield(emit_inactive_event()) + except asyncio.CancelledError: + pass + except Exception: + pass + + # Fan out: one task per model + if metadata.get('session_id') and metadata.get('chat_id'): + task_ids = [] + chat_id = metadata['chat_id'] + + for idx, (target_model_id, assistant_message_id) in enumerate(message_ids.items()): + if not assistant_message_id: + continue + + # Per-model metadata: own message_id + model + per_model_metadata = { + **metadata, + 'message_id': assistant_message_id, + } + + # Per-model form_data: own model + model_form_data = { + **form_data, + 'model': target_model_id, + 'metadata': per_model_metadata, + } + + # Resolve the model object for this specific model + resolved_model = request.app.state.MODELS.get(target_model_id, model) + + # Only the first model runs title/tags generation; + # subsequent models only run follow-ups. + task_id, _ = await create_task( + request.app.state.redis, + process_chat( + request, + model_form_data, + user, + per_model_metadata, + resolved_model, + tasks + if idx == 0 + else { + k: v + for k, v in (tasks or {}).items() + if k not in (TASKS.TITLE_GENERATION, TASKS.TAGS_GENERATION) + } + or None, + ), + id=chat_id, + ) + task_ids.append(task_id) + + # Emit chat:active=true + if task_ids: + event_emitter = await get_event_emitter( + {**metadata, 'message_id': list(message_ids.values())[0]}, + update_db=False, + ) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': True}}) + + return { + 'status': True, + 'task_ids': task_ids, + 'chat_id': chat_id, + } + else: + # Legacy/direct: single model, synchronous + metadata['message_id'] = list(message_ids.values())[0] + return await process_chat(request, form_data, user, metadata, model, tasks) + + +# Alias for chat_completion (Legacy) +generate_chat_completions = chat_completion +generate_chat_completion = chat_completion + +# Expose as app.state so internal callers (e.g. automations) can +# use the full pipeline without importing from main.py (avoids circular deps). +app.state.CHAT_COMPLETION_HANDLER = chat_completion + + +################################## +# +# Anthropic Messages API Compatible Endpoint +# +################################## + + +from open_webui.utils.anthropic import ( + convert_anthropic_to_openai_payload, + convert_openai_to_anthropic_response, + openai_stream_to_anthropic_stream, +) + + +@app.post('/api/message') +@app.post('/api/v1/messages') # Anthropic Messages API compatible endpoint +async def generate_messages( + request: Request, + form_data: dict, + user=Depends(get_verified_user), +): + """ + Anthropic Messages API compatible endpoint. + + Accepts the Anthropic Messages API format, converts internally to OpenAI + Chat Completions format, routes through the existing chat completion + pipeline, then converts the response back to Anthropic Messages format. + + Supports both streaming and non-streaming requests. + All models configured in Open WebUI are accessible via this endpoint. + + Authentication: Supports both standard Authorization header and + Anthropic's x-api-key header (via middleware translation). + """ + # Convert Anthropic payload to OpenAI format + requested_model = form_data.get('model', '') + + openai_payload = convert_anthropic_to_openai_payload(form_data) + + # Route through the existing chat_completion handler + response = await chat_completion(request, openai_payload, user) + + # Convert response back to Anthropic format + if isinstance(response, StreamingResponse): + # Streaming response: wrap the generator to convert SSE format + return StreamingResponse( + openai_stream_to_anthropic_stream(response.body_iterator, model=requested_model), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + ) + elif isinstance(response, dict): + return convert_openai_to_anthropic_response(response, model=requested_model) + else: + # Passthrough for error responses (JSONResponse, PlainTextResponse, etc.) + return response + + +@app.post('/api/chat/completed') +async def chat_completed(request: Request, form_data: dict, user=Depends(get_verified_user)): + """Deprecated: outlet filters now run inline during chat completion. + Kept for backward compatibility with external integrations.""" + try: + model_item = form_data.pop('model_item', {}) + + if model_item.get('direct', False): + request.state.direct = True + request.state.model = model_item + + return await chat_completed_handler(request, form_data, user) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@app.post('/api/chat/actions/{action_id}') +async def chat_action(request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)): + try: + model_item = form_data.pop('model_item', {}) + + if model_item.get('direct', False): + request.state.direct = True + request.state.model = model_item + + return await chat_action_handler(request, action_id, form_data, user) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@app.post('/api/tasks/stop/{task_id}') +async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)): + try: + result = await stop_task(request.app.state.redis, task_id) + return result + except ValueError as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + + +@app.get('/api/tasks') +async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)): + return {'tasks': await list_tasks(request.app.state.redis)} + + +@app.get('/api/tasks/chat/{chat_id:path}') +async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + return {'task_ids': []} + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + return {'task_ids': []} + + task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id) + + log.debug(f'Task IDs for chat {chat_id}: {task_ids}') + return {'task_ids': task_ids} + + +@app.post('/api/tasks/chat/{chat_id:path}/stop') +async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + result = await stop_item_tasks(request.app.state.redis, chat_id) + return result + + +################################## +# +# Config Endpoints +# +################################## + + +@app.get('/api/config') +async def get_app_config(request: Request): + user = None + token = None + + auth_header = request.headers.get('Authorization') + if auth_header: + cred = get_http_authorization_cred(auth_header) + if cred: + token = cred.credentials + + if not token and 'token' in request.cookies: + token = request.cookies.get('token') + + if token: + try: + data = decode_token(token) + except Exception as e: + log.debug(e) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid token', + ) + if data is not None and 'id' in data: + user = await Users.get_user_by_id(data['id']) + + user_count = await Users.get_num_users() + onboarding = False + + if user is None: + onboarding = user_count == 0 + + return { + **({'onboarding': True} if onboarding else {}), + 'status': True, + 'name': app.state.WEBUI_NAME, + 'version': VERSION, + 'default_locale': str(DEFAULT_LOCALE), + 'oauth': {'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()}}, + 'features': { + 'auth': WEBUI_AUTH, + 'auth_trusted_header': bool(app.state.AUTH_TRUSTED_EMAIL_HEADER), + 'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + 'enable_ldap': app.state.config.ENABLE_LDAP, + 'enable_api_keys': app.state.config.ENABLE_API_KEYS, + 'enable_signup': app.state.config.ENABLE_SIGNUP, + 'enable_login_form': app.state.config.ENABLE_LOGIN_FORM, + 'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM, + 'enable_websocket': ENABLE_WEBSOCKET_SUPPORT, + 'enable_version_update_check': ENABLE_VERSION_UPDATE_CHECK, + 'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + 'enable_easter_eggs': ENABLE_EASTER_EGGS, + **( + { + 'enable_direct_connections': app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'enable_folders': app.state.config.ENABLE_FOLDERS, + 'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT, + 'enable_channels': app.state.config.ENABLE_CHANNELS, + 'enable_calendar': app.state.config.ENABLE_CALENDAR, + 'enable_automations': app.state.config.ENABLE_AUTOMATIONS, + 'enable_notes': app.state.config.ENABLE_NOTES, + 'enable_web_search': app.state.config.ENABLE_WEB_SEARCH, + 'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION, + 'enable_code_interpreter': app.state.config.ENABLE_CODE_INTERPRETER, + 'enable_image_generation': app.state.config.ENABLE_IMAGE_GENERATION, + 'enable_autocomplete_generation': app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'enable_community_sharing': app.state.config.ENABLE_COMMUNITY_SHARING, + 'enable_message_rating': app.state.config.ENABLE_MESSAGE_RATING, + 'enable_user_webhooks': app.state.config.ENABLE_USER_WEBHOOKS, + 'enable_user_status': app.state.config.ENABLE_USER_STATUS, + 'enable_admin_export': ENABLE_ADMIN_EXPORT, + 'enable_admin_chat_access': ENABLE_ADMIN_CHAT_ACCESS, + 'enable_admin_analytics': ENABLE_ADMIN_ANALYTICS, + 'enable_google_drive_integration': app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'enable_onedrive_integration': app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + 'enable_memories': app.state.config.ENABLE_MEMORIES, + **( + { + 'enable_onedrive_personal': ENABLE_ONEDRIVE_PERSONAL, + 'enable_onedrive_business': ENABLE_ONEDRIVE_BUSINESS, + } + if app.state.config.ENABLE_ONEDRIVE_INTEGRATION + else {} + ), + } + if user is not None + else {} + ), + }, + **( + { + 'default_models': app.state.config.DEFAULT_MODELS, + 'default_pinned_models': app.state.config.DEFAULT_PINNED_MODELS, + 'default_prompt_suggestions': app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + 'user_count': user_count, + 'code': { + 'engine': app.state.config.CODE_EXECUTION_ENGINE, + 'interpreter_engine': app.state.config.CODE_INTERPRETER_ENGINE, + }, + 'audio': { + 'tts': { + 'engine': app.state.config.TTS_ENGINE, + 'voice': app.state.config.TTS_VOICE, + 'split_on': app.state.config.TTS_SPLIT_ON, + }, + 'stt': { + 'engine': app.state.config.STT_ENGINE, + }, + }, + 'file': { + 'max_size': app.state.config.FILE_MAX_SIZE, + 'max_count': app.state.config.FILE_MAX_COUNT, + 'image_compression': { + 'width': app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'height': app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + }, + }, + 'permissions': {**app.state.config.USER_PERMISSIONS}, + 'google_drive': { + 'client_id': GOOGLE_DRIVE_CLIENT_ID.value, + 'api_key': GOOGLE_DRIVE_API_KEY.value, + }, + 'onedrive': { + 'client_id_personal': ONEDRIVE_CLIENT_ID_PERSONAL, + 'client_id_business': ONEDRIVE_CLIENT_ID_BUSINESS, + 'sharepoint_url': ONEDRIVE_SHAREPOINT_URL.value, + 'sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID.value, + }, + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'response_watermark': app.state.config.RESPONSE_WATERMARK, + }, + 'license_metadata': app.state.LICENSE_METADATA, + **( + { + 'active_entries': app.state.USER_COUNT, + } + if user.role == 'admin' + else {} + ), + } + if user is not None and (user.role in ['admin', 'user']) + else { + **( + { + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, + } + } + if user and user.role == 'pending' + else {} + ), + **( + { + 'metadata': { + 'login_footer': app.state.LICENSE_METADATA.get('login_footer', ''), + 'auth_logo_position': app.state.LICENSE_METADATA.get('auth_logo_position', ''), + } + } + if app.state.LICENSE_METADATA + else {} + ), + } + ), + } + + +class UrlForm(BaseModel): + url: str + + +@app.get('/api/webhook') +async def get_webhook_url(user=Depends(get_admin_user)): + return { + 'url': app.state.config.WEBHOOK_URL, + } + + +@app.post('/api/webhook') +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.config.WEBHOOK_URL = form_data.url + app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL + return {'url': app.state.config.WEBHOOK_URL} + + +@app.get('/api/version') +async def get_app_version(): + return { + 'version': VERSION, + 'deployment_id': DEPLOYMENT_ID, + } + + +@app.get('/api/version/updates') +async def get_app_latest_release_version(user=Depends(get_verified_user)): + if not ENABLE_VERSION_UPDATE_CHECK: + log.debug(f'Version update check is disabled, returning current version as latest version') + return {'current': VERSION, 'latest': VERSION} + try: + timeout = aiohttp.ClientTimeout(total=1) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + 'https://api.github.com/repos/open-webui/open-webui/releases/latest', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + latest_version = data['tag_name'] + + return {'current': VERSION, 'latest': latest_version[1:]} + except Exception as e: + log.debug(e) + return {'current': VERSION, 'latest': VERSION} + + +@app.get('/api/changelog') +async def get_app_changelog(): + return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5} + + +@app.get('/api/usage') +async def get_current_usage(user=Depends(get_verified_user)): + """ + Get current usage statistics for Open WebUI. + This is an experimental endpoint and subject to change. + """ + try: + # If public visibility is disabled, only allow admins to access this endpoint + if not ENABLE_PUBLIC_ACTIVE_USERS_COUNT and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Access denied. Only administrators can view usage statistics.', + ) + + return { + 'model_ids': get_models_in_use(), + 'user_count': await Users.get_active_user_count(), + } + except HTTPException: + raise + except Exception as e: + log.error(f'Error getting usage statistics: {e}') + raise HTTPException(status_code=500, detail='Internal Server Error') + + +############################ +# OAuth Login & Callback +############################ + + +# Initialize OAuth client manager with any MCP tool servers using OAuth 2.1 +if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: + for tool_server_connection in app.state.config.TOOL_SERVER_CONNECTIONS: + if tool_server_connection.get('type', 'openapi') == 'mcp': + server_id = tool_server_connection.get('info', {}).get('id') + auth_type = tool_server_connection.get('auth_type', 'none') + + if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'): + try: + oauth_client_info = resolve_oauth_client_info(tool_server_connection) + app.state.oauth_client_manager.add_client( + f'mcp:{server_id}', + OAuthClientInformationFull(**oauth_client_info), + ) + except Exception as e: + log.error(f'Error adding OAuth client for MCP tool server {server_id}: {e}') + pass + +try: + if ENABLE_STAR_SESSIONS_MIDDLEWARE: + redis_session_store = RedisStore( + url=REDIS_URL, + prefix=(f'{REDIS_KEY_PREFIX}:session:' if REDIS_KEY_PREFIX else 'session:'), + ) + + app.add_middleware(SessionAutoloadMiddleware) + app.add_middleware( + StarSessionsMiddleware, + store=redis_session_store, + cookie_name='owui-session', + cookie_same_site=WEBUI_SESSION_COOKIE_SAME_SITE, + cookie_https_only=WEBUI_SESSION_COOKIE_SECURE, + ) + log.info('Using Redis for session') + else: + raise ValueError('No Redis URL provided') +except Exception as e: + app.add_middleware( + SessionMiddleware, + secret_key=WEBUI_SECRET_KEY, + session_cookie='owui-session', + same_site=WEBUI_SESSION_COOKIE_SAME_SITE, + https_only=WEBUI_SESSION_COOKIE_SECURE, + ) + + +async def register_client(request, client_id: str) -> bool: + server_type, server_id = client_id.split(':', 1) + + connection = None + connection_idx = None + + for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []): + if conn.get('type', 'openapi') == server_type: + info = conn.get('info', {}) + if info.get('id') == server_id: + connection = conn + connection_idx = idx + break + + if connection is None or connection_idx is None: + log.warning(f'Unable to locate MCP tool server configuration for client {client_id} during re-registration') + return False + + server_url = connection.get('url') + auth_type = connection.get('auth_type', 'none') + oauth_server_key = (connection.get('config') or {}).get('oauth_server_key') + + try: + if auth_type == 'oauth_2.1_static': + # Static credentials: rebuild from admin-provided credentials + fresh metadata + info = connection.get('info', {}) + oauth_client_id = info.get('oauth_client_id') or '' + oauth_client_secret = info.get('oauth_client_secret') or '' + if not oauth_client_id or not oauth_client_secret: + # Fall back to blob for backward compatibility + existing_client_info = info.get('oauth_client_info', '') + if not existing_client_info: + log.error(f'No stored OAuth client info for static client {client_id}') + return False + existing_data = decrypt_data(existing_client_info) + oauth_client_id = oauth_client_id or existing_data.get('client_id', '') + oauth_client_secret = oauth_client_secret or existing_data.get('client_secret', '') + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + client_id, + server_url, + oauth_client_id=oauth_client_id, + oauth_client_secret=oauth_client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, + client_id, + server_url, + oauth_server_key, + ) + except Exception as e: + log.error(f'OAuth client re-registration failed for {client_id}: {e}') + return False + + try: + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + connections[connection_idx] = { + **connection, + 'info': { + **connection.get('info', {}), + 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), + }, + } + # Re-assign the full list to trigger AppConfig.__setattr__ → PersistentConfig.save() + # (in-place list mutation via list[idx] = ... does not trigger __setattr__) + request.app.state.config.TOOL_SERVER_CONNECTIONS = connections + except Exception as e: + log.error(f'Failed to persist updated OAuth client info for tool server {client_id}: {e}') + return False + + oauth_client_manager.remove_client(client_id) + oauth_client_manager.add_client(client_id, oauth_client_info) + log.info(f'Re-registered OAuth client {client_id} for tool server') + return True + + +@app.get('/oauth/clients/{client_id}/authorize') +async def oauth_client_authorize( + client_id: str, + request: Request, + response: Response, + user=Depends(get_verified_user), +): + # ensure_valid_client_registration + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + if not await oauth_client_manager._preflight_authorization_url(client, client_info): + log.info( + 'Detected invalid OAuth client %s; attempting re-registration', + client_id, + ) + + registered = await register_client(request, client_id) + if not registered: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to re-register OAuth client', + ) + + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='OAuth client unavailable after re-registration', + ) + + if not await oauth_client_manager._preflight_authorization_url(client, client_info): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='OAuth client registration is still invalid after re-registration', + ) + + return await oauth_client_manager.handle_authorize(request, client_id=client_id) + + +@app.get('/oauth/clients/{client_id}/callback') +async def oauth_client_callback( + client_id: str, + request: Request, + response: Response, + user=Depends(get_verified_user), +): + return await oauth_client_manager.handle_callback( + request, + client_id=client_id, + user_id=user.id if user else None, + response=response, + ) + + +@app.get('/oauth/{provider}/login') +async def oauth_login(provider: str, request: Request): + return await oauth_manager.handle_login(request, provider) + + +# OAuth login logic is as follows: +# 1. Attempt to find a user with matching subject ID, tied to the provider +# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth +# - This is considered insecure in general, as OAuth providers do not always verify email addresses +# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user +# - Email addresses are considered unique, so we fail registration if the email address is already taken +@app.get('/oauth/{provider}/login/callback') +@app.get('/oauth/{provider}/callback') # Legacy endpoint +async def oauth_login_callback( + provider: str, + request: Request, + response: Response, + db: AsyncSession = Depends(get_async_session), +): + return await oauth_manager.handle_callback(request, provider, response, db=db) + + +############################ +# OIDC Back-Channel Logout +############################ + + +@app.post('/oauth/backchannel-logout') +async def oauth_backchannel_logout( + request: Request, + db: AsyncSession = Depends(get_async_session), +): + if not ENABLE_OAUTH_BACKCHANNEL_LOGOUT: + raise HTTPException(status_code=404) + return await oauth_manager.handle_backchannel_logout(request, db=db) + + +@app.get('/manifest.json') +async def get_manifest_json(): + if app.state.EXTERNAL_PWA_MANIFEST_URL: + session = await get_session() + async with session.get( + app.state.EXTERNAL_PWA_MANIFEST_URL, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.json() + else: + return { + 'name': app.state.WEBUI_NAME, + 'short_name': app.state.WEBUI_NAME, + 'description': f'{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.', + 'start_url': '/', + 'display': 'standalone', + 'background_color': '#343541', + 'icons': [ + { + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'any', + }, + { + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'maskable', + }, + ], + 'share_target': { + 'action': '/', + 'method': 'GET', + 'params': {'text': 'shared'}, + }, + } + + +@app.get('/opensearch.xml') +async def get_opensearch_xml(): + xml_content = rf""" + + {app.state.WEBUI_NAME} + Search {app.state.WEBUI_NAME} + UTF-8 + {app.state.config.WEBUI_URL}/static/favicon.png + + {app.state.config.WEBUI_URL} + + """ + return Response(content=xml_content, media_type='application/xml') + + +@app.get('/health') +async def healthcheck(): + return {'status': True} + + +@app.get('/ready') +async def readiness_check(): + """ + Returns 200 only when the application is ready to accept traffic. + """ + + # Ensure application startup work has completed + if not getattr(app.state, 'startup_complete', False): + log.info('Readiness check failed: startup not complete') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Startup not complete', + ) + + # Check database connectivity + try: + ScopedSession.execute(text('SELECT 1;')).all() + except Exception as e: + log.warning(f'Readiness check DB ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Database not ready', + ) + + # Check Redis connectivity if configured + redis = app.state.redis + if redis is not None: + try: + pong = await redis.ping() + if pong is False: + raise Exception('Redis PING returned False') + except Exception as e: + log.warning(f'Readiness check Redis ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Redis not ready', + ) + + return {'status': True} + + +@app.get('/health/db') +async def healthcheck_with_db(): + ScopedSession.execute(text('SELECT 1;')).all() + return {'status': True} + + +app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') + + +@app.get('/cache/{path:path}') +async def serve_cache_file( + path: str, + user=Depends(get_verified_user), +): + file_path = os.path.abspath(os.path.join(CACHE_DIR, path)) + # prevent path traversal + if not file_path.startswith(os.path.abspath(CACHE_DIR)): + raise HTTPException(status_code=404, detail='File not found') + if not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail='File not found') + return FileResponse(file_path) + + +def swagger_ui_html(*args, **kwargs): + return get_swagger_ui_html( + *args, + **kwargs, + swagger_js_url='/static/swagger-ui/swagger-ui-bundle.js', + swagger_css_url='/static/swagger-ui/swagger-ui.css', + swagger_favicon_url='/static/swagger-ui/favicon.png', + ) + + +applications.get_swagger_ui_html = swagger_ui_html + +if os.path.exists(FRONTEND_BUILD_DIR): + mimetypes.add_type('text/javascript', '.js') + app.mount( + '/', + SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), + name='spa-static-files', + ) +else: + log.warning(f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only.")