| from __future__ import annotations |
|
|
| import asyncio |
| import inspect |
| import json |
| import logging |
| import mimetypes |
| import os |
| import random |
| import re |
| import shutil |
| import sys |
| import time |
| from contextlib import asynccontextmanager |
| from typing import Optional |
| from urllib.parse import parse_qs, urlencode, urlparse |
| from uuid import uuid4 |
|
|
| import aiohttp |
| import anyio.to_thread |
| from aiocache import cached |
| from fastapi import ( |
| BackgroundTasks, |
| Depends, |
| FastAPI, |
| File, |
| Form, |
| HTTPException, |
| Request, |
| UploadFile, |
| applications, |
| status, |
| ) |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.openapi.docs import get_swagger_ui_html |
| from fastapi.responses import FileResponse, JSONResponse, RedirectResponse |
| from fastapi.staticfiles import StaticFiles |
| from pydantic import BaseModel |
| from redis import Redis |
| from sqlalchemy import text |
| from sqlalchemy.ext.asyncio import AsyncSession |
| from starlette.datastructures import Headers |
| from starlette.exceptions import HTTPException as StarletteHTTPException |
| from starlette.middleware.sessions import SessionMiddleware |
| from starlette.responses import Response, StreamingResponse |
| from starlette_compress import CompressMiddleware |
| from starsessions import ( |
| SessionAutoloadMiddleware, |
| ) |
| from starsessions import ( |
| SessionMiddleware as StarSessionsMiddleware, |
| ) |
| from starsessions.stores.redis import RedisStore |
|
|
| from open_webui.config import ( |
| ADMIN_EMAIL, |
| API_KEYS_ALLOWED_ENDPOINTS, |
| AUDIO_STT_ALLOWED_EXTENSIONS, |
| AUDIO_STT_AZURE_API_KEY, |
| AUDIO_STT_AZURE_BASE_URL, |
| AUDIO_STT_AZURE_LOCALES, |
| AUDIO_STT_AZURE_MAX_SPEAKERS, |
| AUDIO_STT_AZURE_REGION, |
| |
| AUDIO_STT_ENGINE, |
| AUDIO_STT_MISTRAL_API_BASE_URL, |
| AUDIO_STT_MISTRAL_API_KEY, |
| AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, |
| AUDIO_STT_MODEL, |
| AUDIO_STT_OPENAI_API_BASE_URL, |
| AUDIO_STT_OPENAI_API_KEY, |
| AUDIO_STT_SUPPORTED_CONTENT_TYPES, |
| AUDIO_TTS_API_KEY, |
| AUDIO_TTS_AZURE_SPEECH_BASE_URL, |
| AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, |
| AUDIO_TTS_AZURE_SPEECH_REGION, |
| AUDIO_TTS_ENGINE, |
| AUDIO_TTS_MISTRAL_API_BASE_URL, |
| AUDIO_TTS_MISTRAL_API_KEY, |
| AUDIO_TTS_MODEL, |
| AUDIO_TTS_OPENAI_API_BASE_URL, |
| AUDIO_TTS_OPENAI_API_KEY, |
| AUDIO_TTS_OPENAI_PARAMS, |
| AUDIO_TTS_SPLIT_ON, |
| AUDIO_TTS_VOICE, |
| AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, |
| AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, |
| |
| AUTOMATIC1111_API_AUTH, |
| AUTOMATIC1111_BASE_URL, |
| AUTOMATIC1111_PARAMS, |
| AUTOMATION_MAX_COUNT, |
| AUTOMATION_MIN_INTERVAL, |
| BING_SEARCH_V7_ENDPOINT, |
| BING_SEARCH_V7_SUBSCRIPTION_KEY, |
| BOCHA_SEARCH_API_KEY, |
| BRAVE_SEARCH_API_KEY, |
| BRAVE_SEARCH_CONTEXT_TOKENS, |
| BYPASS_ADMIN_ACCESS_CONTROL, |
| BYPASS_EMBEDDING_AND_RETRIEVAL, |
| BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, |
| BYPASS_WEB_SEARCH_WEB_LOADER, |
| CACHE_DIR, |
| CHUNK_MIN_SIZE_TARGET, |
| CHUNK_OVERLAP, |
| CHUNK_SIZE, |
| CODE_EXECUTION_ENGINE, |
| CODE_EXECUTION_JUPYTER_AUTH, |
| CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, |
| CODE_EXECUTION_JUPYTER_AUTH_TOKEN, |
| CODE_EXECUTION_JUPYTER_TIMEOUT, |
| CODE_EXECUTION_JUPYTER_URL, |
| CODE_INTERPRETER_ENGINE, |
| CODE_INTERPRETER_JUPYTER_AUTH, |
| CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, |
| CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, |
| CODE_INTERPRETER_JUPYTER_TIMEOUT, |
| CODE_INTERPRETER_JUPYTER_URL, |
| CODE_INTERPRETER_PROMPT_TEMPLATE, |
| COMFYUI_API_KEY, |
| COMFYUI_BASE_URL, |
| COMFYUI_WORKFLOW, |
| COMFYUI_WORKFLOW_NODES, |
| CONTENT_EXTRACTION_ENGINE, |
| CORS_ALLOW_ORIGIN, |
| DATALAB_MARKER_ADDITIONAL_CONFIG, |
| DATALAB_MARKER_API_BASE_URL, |
| DATALAB_MARKER_API_KEY, |
| DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, |
| DATALAB_MARKER_FORCE_OCR, |
| DATALAB_MARKER_FORMAT_LINES, |
| DATALAB_MARKER_OUTPUT_FORMAT, |
| DATALAB_MARKER_PAGINATE, |
| DATALAB_MARKER_SKIP_CACHE, |
| DATALAB_MARKER_STRIP_EXISTING_OCR, |
| DATALAB_MARKER_USE_LLM, |
| DDGS_BACKEND, |
| DEEPGRAM_API_KEY, |
| DEFAULT_ARENA_MODEL, |
| DEFAULT_GROUP_ID, |
| DEFAULT_LOCALE, |
| DEFAULT_MODEL_METADATA, |
| DEFAULT_MODEL_PARAMS, |
| DEFAULT_MODELS, |
| DEFAULT_PINNED_MODELS, |
| DEFAULT_PROMPT_SUGGESTIONS, |
| DEFAULT_RAG_TEMPLATE, |
| DEFAULT_USER_ROLE, |
| DOCLING_API_KEY, |
| DOCLING_PARAMS, |
| DOCLING_SERVER_URL, |
| DOCUMENT_INTELLIGENCE_ENDPOINT, |
| DOCUMENT_INTELLIGENCE_KEY, |
| DOCUMENT_INTELLIGENCE_MODEL, |
| ENABLE_ADMIN_ANALYTICS, |
| |
| ENABLE_ADMIN_CHAT_ACCESS, |
| ENABLE_ADMIN_EXPORT, |
| ENABLE_API_KEYS, |
| ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, |
| ENABLE_ASYNC_EMBEDDING, |
| ENABLE_AUTOCOMPLETE_GENERATION, |
| ENABLE_AUTOMATIONS, |
| |
| ENABLE_BASE_MODELS_CACHE, |
| ENABLE_CALENDAR, |
| ENABLE_CHANNELS, |
| |
| ENABLE_CODE_EXECUTION, |
| ENABLE_CODE_INTERPRETER, |
| ENABLE_COMMUNITY_SHARING, |
| |
| ENABLE_DIRECT_CONNECTIONS, |
| ENABLE_EVALUATION_ARENA_MODELS, |
| ENABLE_FOLDERS, |
| ENABLE_FOLLOW_UP_GENERATION, |
| ENABLE_GOOGLE_DRIVE_INTEGRATION, |
| ENABLE_IMAGE_EDIT, |
| ENABLE_IMAGE_GENERATION, |
| ENABLE_IMAGE_PROMPT_GENERATION, |
| |
| ENABLE_LDAP, |
| ENABLE_LDAP_GROUP_CREATION, |
| |
| ENABLE_LDAP_GROUP_MANAGEMENT, |
| ENABLE_LOGIN_FORM, |
| ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, |
| ENABLE_MEMORIES, |
| ENABLE_MESSAGE_RATING, |
| ENABLE_NOTES, |
| |
| ENABLE_OAUTH_ROLE_MANAGEMENT, |
| |
| ENABLE_OLLAMA_API, |
| ENABLE_ONEDRIVE_BUSINESS, |
| ENABLE_ONEDRIVE_INTEGRATION, |
| ENABLE_ONEDRIVE_PERSONAL, |
| |
| ENABLE_OPENAI_API, |
| ENABLE_PASSWORD_CHANGE_FORM, |
| ENABLE_RAG_HYBRID_SEARCH, |
| ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, |
| ENABLE_RAG_LOCAL_WEB_FETCH, |
| ENABLE_RETRIEVAL_QUERY_GENERATION, |
| ENABLE_SEARCH_QUERY_GENERATION, |
| ENABLE_SIGNUP, |
| ENABLE_TAGS_GENERATION, |
| ENABLE_TITLE_GENERATION, |
| ENABLE_USER_STATUS, |
| ENABLE_USER_WEBHOOKS, |
| ENABLE_VOICE_MODE_PROMPT, |
| ENABLE_WEB_LOADER_SSL_VERIFICATION, |
| |
| ENABLE_WEB_SEARCH, |
| |
| ENV, |
| EVALUATION_ARENA_MODELS, |
| EXA_API_KEY, |
| EXTERNAL_DOCUMENT_LOADER_API_KEY, |
| EXTERNAL_DOCUMENT_LOADER_URL, |
| EXTERNAL_WEB_LOADER_API_KEY, |
| EXTERNAL_WEB_LOADER_URL, |
| EXTERNAL_WEB_SEARCH_API_KEY, |
| EXTERNAL_WEB_SEARCH_URL, |
| FILE_IMAGE_COMPRESSION_HEIGHT, |
| FILE_IMAGE_COMPRESSION_WIDTH, |
| FIRECRAWL_API_BASE_URL, |
| FIRECRAWL_API_KEY, |
| FIRECRAWL_TIMEOUT, |
| FOLDER_MAX_FILE_COUNT, |
| FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, |
| FRONTEND_BUILD_DIR, |
| GOOGLE_DRIVE_API_KEY, |
| GOOGLE_DRIVE_CLIENT_ID, |
| GOOGLE_PSE_API_KEY, |
| GOOGLE_PSE_ENGINE_ID, |
| IFRAME_CSP, |
| IMAGE_EDIT_ENGINE, |
| IMAGE_EDIT_MODEL, |
| IMAGE_EDIT_SIZE, |
| IMAGE_GENERATION_ENGINE, |
| IMAGE_GENERATION_MODEL, |
| IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, |
| IMAGE_SIZE, |
| IMAGE_STEPS, |
| IMAGES_EDIT_COMFYUI_API_KEY, |
| IMAGES_EDIT_COMFYUI_BASE_URL, |
| IMAGES_EDIT_COMFYUI_WORKFLOW, |
| IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, |
| IMAGES_EDIT_GEMINI_API_BASE_URL, |
| IMAGES_EDIT_GEMINI_API_KEY, |
| IMAGES_EDIT_OPENAI_API_BASE_URL, |
| IMAGES_EDIT_OPENAI_API_KEY, |
| IMAGES_EDIT_OPENAI_API_VERSION, |
| IMAGES_GEMINI_API_BASE_URL, |
| IMAGES_GEMINI_API_KEY, |
| IMAGES_GEMINI_ENDPOINT_METHOD, |
| IMAGES_OPENAI_API_BASE_URL, |
| IMAGES_OPENAI_API_KEY, |
| IMAGES_OPENAI_API_PARAMS, |
| IMAGES_OPENAI_API_VERSION, |
| JINA_API_BASE_URL, |
| JINA_API_KEY, |
| JWT_EXPIRES_IN, |
| KAGI_SEARCH_API_KEY, |
| LDAP_APP_DN, |
| LDAP_APP_PASSWORD, |
| LDAP_ATTRIBUTE_FOR_GROUPS, |
| LDAP_ATTRIBUTE_FOR_MAIL, |
| LDAP_ATTRIBUTE_FOR_USERNAME, |
| LDAP_CA_CERT_FILE, |
| LDAP_CIPHERS, |
| LDAP_SEARCH_BASE, |
| LDAP_SEARCH_FILTERS, |
| LDAP_SERVER_HOST, |
| LDAP_SERVER_LABEL, |
| LDAP_SERVER_PORT, |
| LDAP_USE_TLS, |
| LDAP_VALIDATE_CERT, |
| MINERU_API_KEY, |
| MINERU_API_MODE, |
| MINERU_API_TIMEOUT, |
| MINERU_API_URL, |
| MINERU_FILE_EXTENSIONS, |
| MINERU_PARAMS, |
| MISTRAL_OCR_API_BASE_URL, |
| MISTRAL_OCR_API_KEY, |
| MODEL_ORDER_LIST, |
| MOJEEK_SEARCH_API_KEY, |
| OAUTH_ADMIN_ROLES, |
| OAUTH_ALLOWED_ROLES, |
| OAUTH_AUTO_REDIRECT, |
| OAUTH_EMAIL_CLAIM, |
| OAUTH_PICTURE_CLAIM, |
| OAUTH_PROVIDERS, |
| OAUTH_ROLES_CLAIM, |
| OAUTH_SUB_CLAIM, |
| OAUTH_USERNAME_CLAIM, |
| OLLAMA_API_CONFIGS, |
| OLLAMA_BASE_URLS, |
| OLLAMA_CLOUD_WEB_SEARCH_API_KEY, |
| ONEDRIVE_CLIENT_ID_BUSINESS, |
| ONEDRIVE_CLIENT_ID_PERSONAL, |
| ONEDRIVE_SHAREPOINT_TENANT_ID, |
| ONEDRIVE_SHAREPOINT_URL, |
| OPENAI_API_BASE_URLS, |
| OPENAI_API_CONFIGS, |
| OPENAI_API_KEYS, |
| PADDLEOCR_VL_BASE_URL, |
| PADDLEOCR_VL_TOKEN, |
| PDF_EXTRACT_IMAGES, |
| PDF_LOADER_MODE, |
| PENDING_USER_OVERLAY_CONTENT, |
| PENDING_USER_OVERLAY_TITLE, |
| PERPLEXITY_API_KEY, |
| PERPLEXITY_MODEL, |
| PERPLEXITY_SEARCH_API_URL, |
| PERPLEXITY_SEARCH_CONTEXT_USAGE, |
| PLAYWRIGHT_TIMEOUT, |
| PLAYWRIGHT_WS_URL, |
| QUERY_GENERATION_PROMPT_TEMPLATE, |
| RAG_ALLOWED_FILE_EXTENSIONS, |
| RAG_AZURE_OPENAI_API_KEY, |
| RAG_AZURE_OPENAI_API_VERSION, |
| RAG_AZURE_OPENAI_BASE_URL, |
| RAG_EMBEDDING_BATCH_SIZE, |
| RAG_EMBEDDING_CONCURRENT_REQUESTS, |
| RAG_EMBEDDING_ENGINE, |
| RAG_EMBEDDING_MODEL, |
| RAG_EMBEDDING_MODEL_AUTO_UPDATE, |
| RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, |
| RAG_EXTERNAL_RERANKER_API_KEY, |
| RAG_EXTERNAL_RERANKER_TIMEOUT, |
| RAG_EXTERNAL_RERANKER_URL, |
| RAG_FILE_MAX_COUNT, |
| RAG_FILE_MAX_SIZE, |
| RAG_FULL_CONTEXT, |
| RAG_HYBRID_BM25_WEIGHT, |
| RAG_OLLAMA_API_KEY, |
| RAG_OLLAMA_BASE_URL, |
| RAG_OPENAI_API_BASE_URL, |
| RAG_OPENAI_API_KEY, |
| RAG_RELEVANCE_THRESHOLD, |
| RAG_RERANKING_BATCH_SIZE, |
| RAG_RERANKING_ENGINE, |
| RAG_RERANKING_MODEL, |
| RAG_RERANKING_MODEL_AUTO_UPDATE, |
| RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, |
| |
| RAG_TEMPLATE, |
| RAG_TEXT_SPLITTER, |
| RAG_TOP_K, |
| RAG_TOP_K_RERANKER, |
| RESPONSE_WATERMARK, |
| SEARCHAPI_API_KEY, |
| SEARCHAPI_ENGINE, |
| SEARXNG_LANGUAGE, |
| SEARXNG_QUERY_URL, |
| SERPAPI_API_KEY, |
| SERPAPI_ENGINE, |
| SERPER_API_KEY, |
| SERPLY_API_KEY, |
| SERPSTACK_API_KEY, |
| SERPSTACK_HTTPS, |
| SHOW_ADMIN_DETAILS, |
| SOUGOU_API_SID, |
| SOUGOU_API_SK, |
| STATIC_DIR, |
| TAGS_GENERATION_PROMPT_TEMPLATE, |
| |
| TASK_MODEL, |
| TASK_MODEL_EXTERNAL, |
| TAVILY_API_KEY, |
| TAVILY_EXTRACT_DEPTH, |
| |
| TERMINAL_SERVER_CONNECTIONS, |
| |
| THREAD_POOL_SIZE, |
| TIKA_SERVER_URL, |
| TIKTOKEN_ENCODING_NAME, |
| TITLE_GENERATION_PROMPT_TEMPLATE, |
| |
| TOOL_SERVER_CONNECTIONS, |
| TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, |
| UPLOAD_DIR, |
| USER_PERMISSIONS, |
| VOICE_MODE_PROMPT_TEMPLATE, |
| WEB_FETCH_MAX_CONTENT_LENGTH, |
| WEB_LOADER_CONCURRENT_REQUESTS, |
| WEB_LOADER_ENGINE, |
| WEB_LOADER_TIMEOUT, |
| WEB_SEARCH_CONCURRENT_REQUESTS, |
| WEB_SEARCH_DOMAIN_FILTER_LIST, |
| WEB_SEARCH_ENGINE, |
| WEB_SEARCH_RESULT_COUNT, |
| WEB_SEARCH_TRUST_ENV, |
| WEBHOOK_URL, |
| |
| WEBUI_AUTH, |
| WEBUI_BANNERS, |
| WEBUI_NAME, |
| WEBUI_URL, |
| WHISPER_LANGUAGE, |
| WHISPER_MODEL, |
| WHISPER_MODEL_AUTO_UPDATE, |
| WHISPER_MODEL_DIR, |
| WHISPER_VAD_FILTER, |
| YACY_PASSWORD, |
| YACY_QUERY_URL, |
| YACY_USERNAME, |
| YANDEX_WEB_SEARCH_API_KEY, |
| YANDEX_WEB_SEARCH_CONFIG, |
| YANDEX_WEB_SEARCH_URL, |
| YOUCOM_API_KEY, |
| LINKUP_API_KEY, |
| LINKUP_SEARCH_PARAMS, |
| YOUTUBE_LOADER_LANGUAGE, |
| YOUTUBE_LOADER_PROXY_URL, |
| AppConfig, |
| async_reset_config, |
| reset_config, |
| ) |
| from open_webui.constants import ERROR_MESSAGES, TASKS |
| from open_webui.env import ( |
| AIOHTTP_CLIENT_SESSION_SSL, |
| AUDIT_EXCLUDED_PATHS, |
| AUDIT_INCLUDED_PATHS, |
| AUDIT_LOG_LEVEL, |
| BYPASS_MODEL_ACCESS_CONTROL, |
| CHANGELOG, |
| DEPLOYMENT_ID, |
| ENABLE_AUDIT_GET_REQUESTS, |
| ENABLE_COMPRESSION_MIDDLEWARE, |
| ENABLE_CUSTOM_MODEL_FALLBACK, |
| ENABLE_EASTER_EGGS, |
| |
| ENABLE_OAUTH_BACKCHANNEL_LOGOUT, |
| ENABLE_OTEL, |
| ENABLE_PUBLIC_ACTIVE_USERS_COUNT, |
| |
| ENABLE_SCIM, |
| ENABLE_SIGNUP_PASSWORD_CONFIRMATION, |
| ENABLE_STAR_SESSIONS_MIDDLEWARE, |
| ENABLE_VERSION_UPDATE_CHECK, |
| ENABLE_WEBSOCKET_SUPPORT, |
| EXTERNAL_PWA_MANIFEST_URL, |
| GLOBAL_LOG_LEVEL, |
| INSTANCE_ID, |
| LICENSE_KEY, |
| LOG_FORMAT, |
| MAX_BODY_LOG_SIZE, |
| |
| REDIS_CLUSTER, |
| REDIS_KEY_PREFIX, |
| REDIS_URL, |
| RESET_CONFIG_ON_START, |
| SAFE_MODE, |
| SCIM_TOKEN, |
| VERSION, |
| |
| WEBUI_ADMIN_EMAIL, |
| WEBUI_ADMIN_NAME, |
| WEBUI_ADMIN_PASSWORD, |
| WEBUI_AUTH_SIGNOUT_REDIRECT_URL, |
| WEBUI_AUTH_TRUSTED_EMAIL_HEADER, |
| WEBUI_AUTH_TRUSTED_NAME_HEADER, |
| WEBUI_BUILD_HASH, |
| WEBUI_SECRET_KEY, |
| WEBUI_SESSION_COOKIE_SAME_SITE, |
| WEBUI_SESSION_COOKIE_SECURE, |
| ) |
| from open_webui.internal.db import ScopedSession, engine, get_async_session |
| from open_webui.models.access_grants import AccessGrants |
| from open_webui.models.channels import Channels |
| from open_webui.models.chats import ChatForm, Chats |
| from open_webui.models.functions import Functions |
| from open_webui.models.messages import Messages |
| from open_webui.models.models import Models |
| from open_webui.models.users import UserModel, Users |
| from open_webui.routers import ( |
| analytics, |
| audio, |
| auths, |
| automations, |
| calendar, |
| channels, |
| chats, |
| configs, |
| evaluations, |
| files, |
| folders, |
| functions, |
| groups, |
| images, |
| knowledge, |
| memories, |
| models, |
| notes, |
| ollama, |
| openai, |
| pipelines, |
| prompts, |
| retrieval, |
| scim, |
| skills, |
| tasks, |
| terminals, |
| tools, |
| users, |
| utils, |
| ) |
| from open_webui.routers.retrieval import ( |
| get_ef, |
| get_embedding_function, |
| get_reranking_function, |
| get_rf, |
| ) |
| from open_webui.socket.main import ( |
| MODELS, |
| get_event_emitter, |
| get_models_in_use, |
| get_user_id_from_session_pool, |
| periodic_session_pool_cleanup, |
| periodic_usage_pool_cleanup, |
| ) |
| from open_webui.socket.main import ( |
| app as socket_app, |
| ) |
| from open_webui.tasks import ( |
| cleanup_task, |
| create_task, |
| has_active_tasks, |
| list_task_ids_by_item_id, |
| list_tasks, |
| redis_task_command_listener, |
| stop_item_tasks, |
| stop_task, |
| ) |
| from open_webui.utils import logger |
| from open_webui.utils.actions import chat_action as chat_action_handler |
| from open_webui.utils.asgi_middleware import ( |
| AuthTokenMiddleware, |
| CommitSessionMiddleware, |
| RedirectMiddleware, |
| WebsocketUpgradeGuardMiddleware, |
| ) |
| from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware |
| from open_webui.utils.auth import ( |
| create_admin_user, |
| decode_token, |
| get_admin_user, |
| get_http_authorization_cred, |
| get_license_data, |
| get_verified_user, |
| ) |
| from open_webui.utils.chat import ( |
| chat_completed as chat_completed_handler, |
| ) |
| from open_webui.utils.chat import ( |
| generate_chat_completion as chat_completion_handler, |
| ) |
| from open_webui.utils.embeddings import generate_embeddings |
| from open_webui.utils.logger import start_logger |
| from open_webui.utils.middleware import ( |
| build_chat_response_context, |
| process_chat_payload, |
| process_chat_response, |
| ) |
| from open_webui.utils.models import ( |
| check_model_access, |
| get_all_base_models, |
| get_all_models, |
| get_filtered_models, |
| ) |
| from open_webui.utils.oauth import ( |
| OAuthClientInformationFull, |
| OAuthClientManager, |
| OAuthManager, |
| decrypt_data, |
| encrypt_data, |
| get_oauth_client_info_with_dynamic_client_registration, |
| get_oauth_client_info_with_static_credentials, |
| resolve_oauth_client_info, |
| ) |
| from open_webui.utils.plugin import install_tool_and_function_dependencies |
| from open_webui.utils.redis import get_redis_client, get_redis_connection |
| from open_webui.utils.security_headers import SecurityHeadersMiddleware |
| from open_webui.utils.session_pool import get_session |
| from open_webui.utils.tools import set_terminal_servers, set_tool_servers |
|
|
| if SAFE_MODE: |
| print('SAFE MODE ENABLED') |
| |
|
|
| 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'): |
| |
| raise ex |
| else: |
| return await super().get_response('index.html', scope) |
| else: |
| raise ex |
|
|
|
|
| if LOG_FORMAT != 'json': |
| banner = 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 |
| """ |
| try: |
| print(banner) |
| except UnicodeEncodeError: |
| |
| print(f'Omnichat v{VERSION} - building the best AI user interface.\nhttps://github.com/open-webui/open-webui') |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| |
| |
| 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) |
|
|
| |
| if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD: |
| if await create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME): |
| |
| app.state.config.ENABLE_SIGNUP = False |
|
|
| if SAFE_MODE: |
| await Functions.deactivate_all_functions() |
|
|
| |
| |
| log.info('Installing external dependencies of functions and tools...') |
| await install_tool_and_function_dependencies() |
|
|
| app.state.redis = get_redis_client(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( |
| |
| { |
| '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}') |
|
|
| |
| if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: |
| 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, |
| } |
| ) |
|
|
| log.info('Initializing tool servers...') |
| try: |
| await set_tool_servers(mock_request) |
| log.info(f'Initialized {len(app.state.TOOL_SERVERS)} tool server(s)') |
| except Exception as e: |
| log.warning(f'Failed to initialize tool servers at startup: {e}') |
|
|
| try: |
| 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 terminal servers at startup: {e}') |
|
|
| |
| app.state.startup_complete = True |
|
|
| yield |
|
|
| |
| 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='Omnichat', |
| docs_url='/docs' if ENV == 'dev' else None, |
| openapi_url='/openapi.json' if ENV == 'dev' else None, |
| redoc_url=None, |
| lifespan=lifespan, |
| ) |
|
|
| |
| app.state.startup_complete = False |
|
|
| |
| oauth_manager = OAuthManager(app) |
| app.state.oauth_manager = oauth_manager |
|
|
| |
| 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_cluster=REDIS_CLUSTER, |
| redis_key_prefix=REDIS_KEY_PREFIX, |
| ) |
| app.state.redis = None |
|
|
| app.state.WEBUI_NAME = WEBUI_NAME |
| app.state.LICENSE_METADATA = None |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| if ENABLE_OTEL: |
| from open_webui.utils.telemetry.setup import setup as setup_opentelemetry |
|
|
| setup_opentelemetry(app=app, db_engine=engine) |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| 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 = {} |
|
|
| |
| |
| |
| |
| |
|
|
| 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 = {} |
|
|
| |
| |
| |
| |
| |
|
|
| app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS |
| app.state.TOOL_SERVERS = [] |
|
|
| |
| |
| |
| |
| |
|
|
| app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS |
| app.state.TERMINAL_SERVERS = [] |
|
|
| |
| |
| |
| |
| |
|
|
| app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS |
|
|
| |
| |
| |
| |
| |
|
|
| app.state.ENABLE_SCIM = ENABLE_SCIM |
| app.state.SCIM_TOKEN = SCIM_TOKEN |
|
|
| |
| |
| |
| |
| |
|
|
| app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE |
| app.state.BASE_MODELS = [] |
|
|
| |
| |
| |
| |
| |
|
|
| 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.OAUTH_AUTO_REDIRECT = OAUTH_AUTO_REDIRECT |
| 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 |
|
|
| |
| from open_webui.utils.access_control import has_permission, 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 |
|
|
| |
| 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 = {} |
|
|
| |
| |
| |
| |
| |
|
|
|
|
| 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.MINERU_FILE_EXTENSIONS = MINERU_FILE_EXTENSIONS |
|
|
| 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.BRAVE_SEARCH_CONTEXT_TOKENS = BRAVE_SEARCH_CONTEXT_TOKENS |
| 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.LINKUP_API_KEY = LINKUP_API_KEY |
| app.state.config.LINKUP_SEARCH_PARAMS = LINKUP_SEARCH_PARAMS |
|
|
|
|
| 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, |
| ) |
|
|
| |
| |
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| 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_ALLOWED_EXTENSIONS = AUDIO_STT_ALLOWED_EXTENSIONS |
|
|
| 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 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| 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 |
| app.state.config.ENABLE_VOICE_MODE_PROMPT = ENABLE_VOICE_MODE_PROMPT |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| app.state.MODELS = MODELS |
|
|
| |
| if ENABLE_COMPRESSION_MIDDLEWARE: |
| app.add_middleware(CompressMiddleware) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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']) |
|
|
| |
| 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, |
| ) |
| |
| |
| |
| |
| |
|
|
|
|
| @app.get('/api/models') |
| @app.get('/api/v1/models') |
| 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: |
| |
| if 'pipeline' in model and model['pipeline'].get('type', None) == 'filter': |
| continue |
|
|
| |
| 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)} |
| |
| 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} |
|
|
|
|
| class ModelUnloadForm(BaseModel): |
| model: str |
|
|
|
|
| @app.post('/api/models/unload') |
| async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depends(get_admin_user)): |
| """ |
| Unified model unload endpoint. |
| Resolves the provider that owns the model and calls its native unload mechanism. |
| Supports: Ollama (keep_alive=0) and llama.cpp (/models/unload). |
| """ |
| model_id = form_data.model |
|
|
| |
| ollama_models = getattr(request.app.state, 'OLLAMA_MODELS', None) or {} |
| if model_id in ollama_models: |
| url_indices = ollama_models[model_id].get('urls', []) |
| errors = [] |
| for idx in url_indices: |
| url = request.app.state.config.OLLAMA_BASE_URLS[idx] |
| api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( |
| str(idx), |
| request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), |
| ) |
| key = api_config.get('key', None) |
|
|
| prefix_id = api_config.get('prefix_id', None) |
| actual_model = model_id |
| if prefix_id and actual_model.startswith(f'{prefix_id}.'): |
| actual_model = actual_model[len(f'{prefix_id}.') :] |
|
|
| payload = json.dumps({'model': actual_model, 'keep_alive': 0, 'prompt': ''}) |
|
|
| try: |
| timeout = aiohttp.ClientTimeout(total=30) |
| async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: |
| headers = { |
| 'Content-Type': 'application/json', |
| **({'Authorization': f'Bearer {key}'} if key else {}), |
| } |
| async with session.post( |
| f'{url}/api/generate', |
| data=payload, |
| headers=headers, |
| ) as r: |
| if not r.ok: |
| errors.append({'url_idx': idx, 'error': await r.text()}) |
| except Exception as e: |
| log.exception(f'Failed to unload model on Ollama node {idx}: {e}') |
| errors.append({'url_idx': idx, 'error': str(e)}) |
|
|
| if errors: |
| raise HTTPException( |
| status_code=500, |
| detail=f'Failed to unload model on {len(errors)} node(s): {errors}', |
| ) |
| return {'status': True} |
|
|
| |
| openai_models = getattr(request.app.state, 'OPENAI_MODELS', None) or {} |
| if model_id in openai_models: |
| model_info = openai_models[model_id] |
| idx = model_info.get('urlIdx') |
| api_config = request.app.state.config.OPENAI_API_CONFIGS.get(str(idx), {}) |
| provider = api_config.get('provider', '') |
| base_url = request.app.state.config.OPENAI_API_BASE_URLS[idx] |
| key = ( |
| request.app.state.config.OPENAI_API_KEYS[idx] if idx < len(request.app.state.config.OPENAI_API_KEYS) else '' |
| ) |
|
|
| if provider == 'llama.cpp': |
| root_url = base_url.rstrip('/').removesuffix('/v1') |
| try: |
| timeout = aiohttp.ClientTimeout(total=30) |
| async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: |
| headers = { |
| 'Content-Type': 'application/json', |
| **({'Authorization': f'Bearer {key}'} if key else {}), |
| } |
| async with session.post( |
| f'{root_url}/models/unload', |
| json={'model': model_id}, |
| headers=headers, |
| ) as r: |
| if not r.ok: |
| detail = await r.text() |
| raise HTTPException(status_code=r.status, detail=detail) |
| return await r.json() |
| except HTTPException: |
| raise |
| except Exception as e: |
| log.exception(f'Failed to unload model via llama.cpp: {e}') |
| raise HTTPException(status_code=500, detail=str(e)) |
| else: |
| raise HTTPException( |
| status_code=400, |
| detail=f'Provider "{provider or "default"}" does not support model unloading', |
| ) |
|
|
| raise HTTPException(status_code=404, detail=f'Model "{model_id}" not found') |
|
|
|
|
| |
| |
| |
|
|
|
|
| @app.post('/api/embeddings') |
| @app.post('/api/v1/embeddings') |
| 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. |
| """ |
| |
| if not request.app.state.MODELS: |
| await get_all_models(request, user=user) |
| |
| return await generate_embeddings(request, form_data, user) |
|
|
|
|
| @app.post('/api/chat/completions') |
| @app.post('/api/v1/chat/completions') |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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 {}), |
| } |
|
|
| |
| 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: |
| |
| 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') |
|
|
| |
| stream_delta_chunk_size = form_data.get('params', {}).get('stream_delta_chunk_size') |
| reasoning_tags = form_data.get('params', {}).get('reasoning_tags') |
|
|
| |
| 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') |
|
|
| |
| |
| |
| |
| 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) |
|
|
| |
| |
| 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) |
|
|
| |
| |
| tool_servers = form_data.pop('tool_servers', None) |
| if ( |
| tool_servers |
| and user.role != 'admin' |
| and not await has_permission( |
| user.id, |
| 'features.direct_tool_servers', |
| request.app.state.config.USER_PERMISSIONS, |
| ) |
| ): |
| tool_servers = None |
|
|
| metadata = { |
| 'user_id': user.id, |
| 'chat_id': form_data.pop('chat_id', None) or '', |
| 'user_message': user_message, |
| 'user_message_id': user_message.get('id') if user_message else None, |
| 'assistant_message_id': form_data.pop('assistant_message_id', 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': tool_servers, |
| '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 chat_id.startswith('channel:'): |
| channel_id = chat_id.removeprefix('channel:') |
| channel = await Channels.get_channel_by_id(channel_id) |
| if not channel: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=ERROR_MESSAGES.NOT_FOUND, |
| ) |
| if user.role != 'admin': |
| if channel.type in ['group', 'dm']: |
| if not await Channels.is_user_channel_member(channel.id, user.id): |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail=ERROR_MESSAGES.DEFAULT(), |
| ) |
| else: |
| if not await AccessGrants.has_access( |
| user_id=user.id, |
| resource_type='channel', |
| resource_id=channel.id, |
| permission='write', |
| ): |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail=ERROR_MESSAGES.DEFAULT(), |
| ) |
| target_message_id = list(message_ids.values())[0] if message_ids else None |
| if target_message_id: |
| target_message = await Messages.get_message_by_id(target_message_id) |
| if target_message and target_message.channel_id != channel.id: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail=ERROR_MESSAGES.DEFAULT(), |
| ) |
|
|
| if not chat_id.startswith('local:') and not chat_id.startswith( |
| 'channel:' |
| ): |
| if is_new_chat: |
| |
| 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 [], |
| 'files': metadata.get('files') or [], |
| 'tags': [], |
| 'timestamp': int(time.time() * 1000), |
| }, |
| folder_id=metadata.get('folder_id'), |
| ), |
| ) |
|
|
| |
| 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: |
| |
| 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(), |
| ) |
|
|
| |
| |
| |
| chat_files = metadata.get('files') |
| if chat_files is not None: |
| existing_chat = await Chats.get_chat_by_id(chat_id) |
| if existing_chat: |
| updated = {**existing_chat.chat, 'files': chat_files} |
| await Chats.update_chat_by_id(chat_id, updated) |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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} |
| ) |
|
|
| |
| 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 |
|
|
| |
| user_message_id = metadata.get('user_message_id') |
| all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] |
|
|
| |
| 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}, |
| ) |
|
|
| |
| 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) |
|
|
| |
| |
| |
| |
| |
| 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 |
| 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'): |
| |
| try: |
| if not metadata.get('chat_id', '').startswith('local:') and not metadata.get( |
| 'chat_id', '' |
| ).startswith('channel:'): |
| 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: |
| |
| |
| |
| |
| raise HTTPException( |
| status_code=status.HTTP_400_BAD_REQUEST, |
| detail=error_detail, |
| ) |
| finally: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| try: |
| if mcp_clients := metadata.get('mcp_clients'): |
| for client in reversed(list(mcp_clients.values())): |
| try: |
| await client.disconnect() |
| except BaseException as e: |
| log.debug(f'Error disconnecting MCP client: {e}') |
| except BaseException as e: |
| log.debug(f'Error cleaning up MCP clients: {e}') |
|
|
| |
| try: |
| chat_id = metadata.get('chat_id') |
| task_id = metadata.get('task_id') |
| if chat_id and task_id: |
| await cleanup_task(request.app.state.redis, task_id, chat_id) |
| if not await has_active_tasks(request.app.state.redis, chat_id): |
| event_emitter = await get_event_emitter(metadata, update_db=False) |
| if event_emitter: |
| try: |
| await asyncio.shield(event_emitter({'type': 'chat:active', 'data': {'active': False}})) |
| except asyncio.CancelledError: |
| pass |
| except Exception: |
| pass |
|
|
| |
| 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 = { |
| **metadata, |
| 'message_id': assistant_message_id, |
| } |
|
|
| |
| model_form_data = { |
| **form_data, |
| 'model': target_model_id, |
| 'metadata': per_model_metadata, |
| } |
|
|
| |
| resolved_model = request.app.state.MODELS.get(target_model_id, model) |
|
|
| |
| |
| 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, |
| ) |
| per_model_metadata['task_id'] = task_id |
| task_ids.append(task_id) |
|
|
| |
| 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: |
| |
| metadata['message_id'] = list(message_ids.values())[0] |
| return await process_chat(request, form_data, user, metadata, model, tasks) |
|
|
|
|
| |
| generate_chat_completions = chat_completion |
| generate_chat_completion = chat_completion |
|
|
| |
| |
| app.state.CHAT_COMPLETION_HANDLER = chat_completion |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| 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') |
| 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 Omnichat are accessible via this endpoint. |
| |
| Authentication: Supports both standard Authorization header and |
| Anthropic's x-api-key header (via middleware translation). |
| """ |
| |
| requested_model = form_data.get('model', '') |
|
|
| openai_payload = convert_anthropic_to_openai_payload(form_data) |
|
|
| |
| response = await chat_completion(request, openai_payload, user) |
|
|
| |
| if isinstance(response, StreamingResponse): |
| |
| 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: |
| |
| 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:') or chat_id.startswith('channel:'): |
| 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:') or chat_id.startswith('channel:'): |
| 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 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| @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']) |
|
|
| onboarding = False |
| if user is None: |
| onboarding = not await Users.has_users() |
|
|
| user_count = await Users.get_num_users() if app.state.LICENSE_METADATA else None |
|
|
| 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()}, |
| 'auto_redirect': app.state.config.OAUTH_AUTO_REDIRECT, |
| }, |
| '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_signup': app.state.config.ENABLE_SIGNUP, |
| 'enable_login_form': app.state.config.ENABLE_LOGIN_FORM, |
| 'enable_websocket': ENABLE_WEBSOCKET_SUPPORT, |
| |
| **( |
| { |
| 'enable_api_keys': app.state.config.ENABLE_API_KEYS, |
| 'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM, |
| '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} if user_count is not None else {}), |
| '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, |
| 'iframe_csp': IFRAME_CSP, |
| }, |
| '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 Omnichat. |
| This is an experimental endpoint and subject to change. |
| """ |
| try: |
| |
| 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') |
|
|
|
|
| |
|
|
|
|
| |
| 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': |
| |
| 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: |
| |
| 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')), |
| }, |
| } |
| |
| |
| 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), |
| ): |
| |
| 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) |
|
|
|
|
| @app.get('/oauth/{provider}/login/callback') |
| @app.get('/oauth/{provider}/callback') |
| async def oauth_login_callback( |
| provider: str, |
| request: Request, |
| response: Response, |
| db: AsyncSession = Depends(get_async_session), |
| ): |
| """Handle the OAuth provider callback. |
| |
| Resolution order: |
| 1. Match by subject ID bound to the provider. |
| 2. If ``OAUTH_MERGE_ACCOUNTS_BY_EMAIL`` is enabled, match by email |
| (note: some providers do not verify email addresses). |
| 3. If no match and ``ENABLE_OAUTH_SIGNUP`` is enabled, create a new user |
| (fails if the email is already registered). |
| """ |
| return await oauth_manager.handle_callback(request, provider, response, db=db) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @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""" |
| <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> |
| <ShortName>{app.state.WEBUI_NAME}</ShortName> |
| <Description>Search {app.state.WEBUI_NAME}</Description> |
| <InputEncoding>UTF-8</InputEncoding> |
| <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image> |
| <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={'{searchTerms}'}"/> |
| <moz:SearchForm>{app.state.config.WEBUI_URL}</moz:SearchForm> |
| </OpenSearchDescription> |
| """ |
| return Response(content=xml_content, media_type='application/xml') |
|
|
|
|
| def _sync_db_ping() -> None: |
| """Verify the database is reachable with a simple SELECT 1. |
| |
| Uses a raw connection from the engine pool instead of the thread-local |
| ScopedSession. This is necessary because CommitSessionMiddleware |
| deliberately skips healthcheck paths (/health, /ready, /health/db), |
| so any ScopedSession opened on a healthcheck worker thread is never |
| rolled back or removed. If the session ever enters an invalid state |
| (e.g. after a transient connection error), it stays broken on that |
| thread permanently, causing PendingRollbackError on every subsequent |
| probe — exactly the failure reported in #24605. |
| |
| A raw ``engine.connect()`` context manager obtains a fresh connection |
| from the pool, executes the ping, and deterministically returns the |
| connection regardless of success or failure. |
| """ |
| with engine.connect() as conn: |
| conn.execute(text('SELECT 1')) |
|
|
|
|
| async def async_db_ping() -> None: |
| await asyncio.to_thread(_sync_db_ping) |
|
|
|
|
| @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. |
| """ |
|
|
| |
| 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', |
| ) |
|
|
| |
| try: |
| await async_db_ping() |
| 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', |
| ) |
|
|
| |
| 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 check_db_health(): |
| """Verify database connectivity by issuing a lightweight ping.""" |
| await async_db_ping() |
| 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), |
| ): |
| """Serve cached files (e.g. tool outputs) with path-traversal protection. |
| |
| Only ``image/*``, ``audio/*``, and ``video/*`` MIME types are served inline; |
| everything else gets a ``Content-Disposition: attachment`` header to prevent |
| XSS from user-generated HTML stored in the cache directory. |
| """ |
| file_path = os.path.abspath(os.path.join(CACHE_DIR, path)) |
| |
| |
| cache_root = os.path.abspath(CACHE_DIR) + os.sep |
| if not file_path.startswith(cache_root): |
| 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') |
|
|
| mime, _ = mimetypes.guess_type(file_path) |
| inline_safe = mime and mime.split('/', 1)[0] in {'image', 'audio', 'video'} |
| headers = {'X-Content-Type-Options': 'nosniff'} |
| if not inline_safe: |
| headers['Content-Disposition'] = f'attachment; filename="{os.path.basename(file_path)}"' |
| return FileResponse(file_path, headers=headers) |
|
|
|
|
| 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.") |
|
|