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