open-webui/backend/open_webui/routers/configs.py

516 lines
19 KiB
Python
Raw Normal View History

2025-09-23 07:36:11 +00:00
import logging
import copy
2025-04-05 10:05:52 +00:00
from fastapi import APIRouter, Depends, Request, HTTPException
from pydantic import BaseModel, ConfigDict
import aiohttp
2024-01-03 00:48:10 +00:00
2024-11-26 16:55:06 +00:00
from typing import Optional
2024-09-03 19:16:07 +00:00
2024-12-09 00:01:56 +00:00
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.config import get_config, save_config
2024-11-26 16:55:06 +00:00
from open_webui.config import BannerModel
from open_webui.utils.tools import (
get_tool_server_data,
2025-07-31 12:47:02 +00:00
get_tool_server_url,
2025-08-18 16:53:46 +00:00
set_tool_servers,
)
2025-09-23 06:03:26 +00:00
from open_webui.utils.mcp.client import MCPClient
from open_webui.models.oauth_sessions import OAuthSessions
2024-09-03 19:16:07 +00:00
2025-09-23 07:36:11 +00:00
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.oauth import (
get_discovery_urls,
get_oauth_client_info_with_dynamic_client_registration,
2025-09-25 06:49:16 +00:00
encrypt_data,
decrypt_data,
OAuthClientInformationFull,
)
from mcp.shared.auth import OAuthMetadata
2025-09-23 07:36:11 +00:00
2024-01-03 00:48:10 +00:00
router = APIRouter()
2025-09-23 07:36:11 +00:00
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
2024-01-03 00:48:10 +00:00
2024-09-03 19:16:07 +00:00
############################
# ImportConfig
############################
class ImportConfigForm(BaseModel):
config: dict
@router.post("/import", response_model=dict)
async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)):
save_config(form_data.config)
return get_config()
############################
# ExportConfig
############################
@router.get("/export", response_model=dict)
async def export_config(user=Depends(get_admin_user)):
return get_config()
2025-02-12 06:29:45 +00:00
############################
2025-06-28 11:12:31 +00:00
# Connections Config
2025-02-12 06:29:45 +00:00
############################
2025-06-28 11:12:31 +00:00
class ConnectionsConfigForm(BaseModel):
2025-02-12 07:12:00 +00:00
ENABLE_DIRECT_CONNECTIONS: bool
ENABLE_BASE_MODELS_CACHE: bool
2025-02-12 06:29:45 +00:00
2025-06-28 11:12:31 +00:00
@router.get("/connections", response_model=ConnectionsConfigForm)
async def get_connections_config(request: Request, user=Depends(get_admin_user)):
2025-02-12 06:29:45 +00:00
return {
2025-02-12 07:12:00 +00:00
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
2025-02-12 06:29:45 +00:00
}
2025-06-28 11:12:31 +00:00
@router.post("/connections", response_model=ConnectionsConfigForm)
async def set_connections_config(
2025-02-12 07:13:48 +00:00
request: Request,
2025-06-28 11:12:31 +00:00
form_data: ConnectionsConfigForm,
2025-02-12 07:13:48 +00:00
user=Depends(get_admin_user),
2025-02-12 06:29:45 +00:00
):
2025-02-12 07:12:00 +00:00
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
form_data.ENABLE_DIRECT_CONNECTIONS
)
request.app.state.config.ENABLE_BASE_MODELS_CACHE = (
form_data.ENABLE_BASE_MODELS_CACHE
)
2025-06-28 11:12:31 +00:00
2025-02-12 06:29:45 +00:00
return {
2025-02-12 07:12:00 +00:00
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
2025-02-12 06:29:45 +00:00
}
class OAuthClientRegistrationForm(BaseModel):
url: str
client_id: str
client_name: Optional[str] = None
@router.post("/oauth/clients/register")
async def register_oauth_client(
request: Request,
form_data: OAuthClientRegistrationForm,
2025-09-25 06:49:16 +00:00
type: Optional[str] = None,
user=Depends(get_admin_user),
):
try:
2025-09-25 06:49:16 +00:00
oauth_client_id = form_data.client_id
if type:
oauth_client_id = f"{type}:{form_data.client_id}"
oauth_client_info = (
await get_oauth_client_info_with_dynamic_client_registration(
2025-09-25 06:49:16 +00:00
request, oauth_client_id, form_data.url
)
)
return {
"status": True,
2025-09-25 06:49:16 +00:00
"oauth_client_info": encrypt_data(
oauth_client_info.model_dump(mode="json")
),
}
except Exception as e:
log.debug(f"Failed to register OAuth client: {e}")
raise HTTPException(
status_code=400,
detail=f"Failed to register OAuth client",
)
2025-04-05 10:05:52 +00:00
############################
# ToolServers Config
############################
class ToolServerConnection(BaseModel):
url: str
path: str
2025-09-23 06:03:26 +00:00
type: Optional[str] = "openapi" # openapi, mcp
2025-04-05 10:05:52 +00:00
auth_type: Optional[str]
key: Optional[str]
config: Optional[dict]
model_config = ConfigDict(extra="allow")
class ToolServersConfigForm(BaseModel):
TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
@router.get("/tool_servers", response_model=ToolServersConfigForm)
async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)):
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers", response_model=ToolServersConfigForm)
async def set_tool_servers_config(
request: Request,
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
2025-10-27 22:38:59 +00:00
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
server_type = connection.get("type", "openapi")
auth_type = connection.get("auth_type", "none")
2025-10-27 22:38:59 +00:00
if auth_type == "oauth_2.1":
# Remove existing OAuth clients for tool servers
server_id = connection.get("info", {}).get("id")
client_key = f"{server_type}:{server_id}"
try:
request.app.state.oauth_client_manager.remove_client(client_key)
except:
pass
2025-10-27 22:31:25 +00:00
# Set new tool server connections
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
]
2025-08-18 16:53:46 +00:00
await set_tool_servers(request)
2025-04-05 10:05:52 +00:00
2025-09-25 06:49:16 +00:00
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
server_type = connection.get("type", "openapi")
if server_type == "mcp":
server_id = connection.get("info", {}).get("id")
auth_type = connection.get("auth_type", "none")
2025-10-27 22:38:59 +00:00
2025-09-25 06:49:16 +00:00
if auth_type == "oauth_2.1" and server_id:
try:
2025-09-25 06:56:09 +00:00
oauth_client_info = connection.get("info", {}).get(
"oauth_client_info", ""
)
2025-09-25 06:49:16 +00:00
oauth_client_info = decrypt_data(oauth_client_info)
2025-09-25 06:56:09 +00:00
2025-10-11 18:40:49 +00:00
request.app.state.oauth_client_manager.add_client(
2025-09-25 06:49:16 +00:00
f"{server_type}:{server_id}",
OAuthClientInformationFull(**oauth_client_info),
)
except Exception as e:
log.debug(f"Failed to add OAuth client for MCP tool server: {e}")
continue
2025-04-05 10:05:52 +00:00
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers/verify")
async def verify_tool_servers_config(
request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)
):
"""
Verify the connection to the tool server.
"""
try:
2025-09-23 06:03:26 +00:00
if form_data.type == "mcp":
if form_data.auth_type == "oauth_2.1":
discovery_urls = get_discovery_urls(form_data.url)
2025-09-26 19:40:30 +00:00
for discovery_url in discovery_urls:
log.debug(
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
)
2025-11-04 17:21:18 +00:00
async with aiohttp.ClientSession(trust_env=True) as session:
2025-09-26 19:40:30 +00:00
async with session.get(
2025-09-30 02:30:19 +00:00
discovery_url
2025-09-26 19:40:30 +00:00
) as oauth_server_metadata_response:
if oauth_server_metadata_response.status == 200:
try:
oauth_server_metadata = (
OAuthMetadata.model_validate(
await oauth_server_metadata_response.json()
)
)
return {
"status": True,
"oauth_server_metadata": oauth_server_metadata.model_dump(
mode="json"
),
}
except Exception as e:
log.info(
f"Failed to parse OAuth 2.1 discovery document: {e}"
)
raise HTTPException(
status_code=400,
2025-10-03 17:50:07 +00:00
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_url}",
2025-09-26 19:40:30 +00:00
)
2025-09-23 06:03:26 +00:00
raise HTTPException(
status_code=400,
2025-09-26 19:40:30 +00:00
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls}",
2025-09-23 06:03:26 +00:00
)
else:
try:
client = MCPClient()
headers = None
token = None
if form_data.auth_type == "bearer":
token = form_data.key
elif form_data.auth_type == "session":
token = request.state.token.credentials
elif form_data.auth_type == "system_oauth":
oauth_token = None
try:
if request.cookies.get("oauth_session_id", None):
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get("oauth_session_id", None),
)
except Exception as e:
pass
if oauth_token:
headers = {"Authorization": f"Bearer {oauth_token.get('access_token', '')}"}
await client.connect(form_data.url, headers=headers)
specs = await client.list_tool_specs()
return {
"status": True,
"specs": specs,
}
except Exception as e:
log.debug(f"Failed to create MCP client: {e}")
raise HTTPException(
status_code=400,
detail=f"Failed to create MCP client",
)
finally:
if client:
await client.disconnect()
2025-09-23 06:03:26 +00:00
else: # openapi
token = None
if form_data.auth_type == "bearer":
token = form_data.key
elif form_data.auth_type == "session":
token = request.state.token.credentials
elif form_data.auth_type == "system_oauth":
try:
if request.cookies.get("oauth_session_id", None):
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
2025-09-23 06:03:26 +00:00
user.id,
request.cookies.get("oauth_session_id", None),
)
if oauth_token:
token = f"{oauth_token.get('access_token', '')}"
2025-09-23 06:03:26 +00:00
except Exception as e:
pass
url = get_tool_server_url(form_data.url, form_data.path)
return await get_tool_server_data(token, url)
2025-09-23 07:35:00 +00:00
except HTTPException as e:
raise e
2025-04-05 10:05:52 +00:00
except Exception as e:
2025-09-23 07:42:25 +00:00
log.debug(f"Failed to connect to the tool server: {e}")
2025-04-05 10:05:52 +00:00
raise HTTPException(
status_code=400,
2025-09-23 07:42:25 +00:00
detail=f"Failed to connect to the tool server",
2025-04-05 10:05:52 +00:00
)
2025-02-10 10:25:02 +00:00
############################
# CodeInterpreterConfig
############################
class CodeInterpreterConfigForm(BaseModel):
ENABLE_CODE_EXECUTION: bool
2025-02-18 00:25:50 +00:00
CODE_EXECUTION_ENGINE: str
CODE_EXECUTION_JUPYTER_URL: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str]
CODE_EXECUTION_JUPYTER_TIMEOUT: Optional[int]
2025-02-10 10:25:02 +00:00
ENABLE_CODE_INTERPRETER: bool
CODE_INTERPRETER_ENGINE: str
2025-02-12 05:36:16 +00:00
CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str]
2025-02-10 10:25:02 +00:00
CODE_INTERPRETER_JUPYTER_URL: Optional[str]
CODE_INTERPRETER_JUPYTER_AUTH: Optional[str]
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str]
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str]
CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int]
2025-02-10 10:25:02 +00:00
2025-02-18 00:25:50 +00:00
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
2025-02-10 10:25:02 +00:00
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
2025-02-18 00:25:50 +00:00
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
2025-02-10 10:25:02 +00:00
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
2025-02-12 05:36:16 +00:00
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
2025-02-10 10:25:02 +00:00
"CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
2025-02-10 10:25:02 +00:00
}
2025-02-18 00:25:50 +00:00
@router.post("/code_execution", response_model=CodeInterpreterConfigForm)
async def set_code_execution_config(
2025-02-10 10:25:02 +00:00
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
):
2025-02-18 00:25:50 +00:00
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
2025-02-18 00:25:50 +00:00
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
form_data.CODE_EXECUTION_JUPYTER_URL
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = (
form_data.CODE_EXECUTION_JUPYTER_AUTH
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
)
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = (
form_data.CODE_EXECUTION_JUPYTER_TIMEOUT
)
2025-02-18 00:25:50 +00:00
2025-02-10 10:25:02 +00:00
request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
2025-02-12 05:36:16 +00:00
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
form_data.CODE_INTERPRETER_PROMPT_TEMPLATE
)
2025-02-10 10:25:02 +00:00
request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = (
form_data.CODE_INTERPRETER_JUPYTER_URL
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = (
form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT
)
2025-02-10 10:25:02 +00:00
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
2025-02-18 00:25:50 +00:00
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
2025-02-10 10:25:02 +00:00
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
2025-02-12 05:36:16 +00:00
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
2025-02-10 10:25:02 +00:00
"CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
2025-02-10 10:25:02 +00:00
}
############################
# SetDefaultModels
############################
class ModelsConfigForm(BaseModel):
2024-11-26 16:55:06 +00:00
DEFAULT_MODELS: Optional[str]
MODEL_ORDER_LIST: Optional[list[str]]
@router.get("/models", response_model=ModelsConfigForm)
async def get_models_config(request: Request, user=Depends(get_admin_user)):
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}
@router.post("/models", response_model=ModelsConfigForm)
async def set_models_config(
request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
}
2024-01-03 00:48:10 +00:00
2024-01-23 05:07:40 +00:00
class PromptSuggestion(BaseModel):
2024-08-14 12:46:31 +00:00
title: list[str]
2024-01-23 05:07:40 +00:00
content: str
class SetDefaultSuggestionsForm(BaseModel):
2024-08-14 12:46:31 +00:00
suggestions: list[PromptSuggestion]
2024-01-23 05:07:40 +00:00
@router.post("/suggestions", response_model=list[PromptSuggestion])
async def set_default_suggestions(
2024-01-23 05:07:40 +00:00
request: Request,
form_data: SetDefaultSuggestionsForm,
user=Depends(get_admin_user),
2024-01-23 05:07:40 +00:00
):
data = form_data.model_dump()
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
############################
# SetBanners
############################
class SetBannersForm(BaseModel):
2024-08-14 12:46:31 +00:00
banners: list[BannerModel]
2024-08-14 12:46:31 +00:00
@router.post("/banners", response_model=list[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data["banners"]
return request.app.state.config.BANNERS
2024-08-14 12:46:31 +00:00
@router.get("/banners", response_model=list[BannerModel])
async def get_banners(
request: Request,
2024-06-27 18:29:59 +00:00
user=Depends(get_verified_user),
):
2025-08-06 10:27:58 +00:00
return request.app.state.config.BANNERS