This commit is contained in:
Timothy Jaeryang Baek committed 2026-06-17 02:52:35 +02:00
1 parent e8d55c0a8b
commit 5cdcdbaeec
53 files changed
+7829 -7954

No files matched your search

+112 -154
View File
@@ -52,6 +52,7 @@ from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS,
ENV,
)
from open_webui.models.config import Config
from open_webui.utils.access_control import has_permission
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.headers import include_user_info_headers
@@ -71,6 +72,50 @@ AZURE_MAX_FILE_SIZE: int = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024
SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech'
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
TTS_CONFIG_KEYS = {
'OPENAI_API_BASE_URL': 'audio.tts.openai.api_base_url',
'OPENAI_API_KEY': 'audio.tts.openai.api_key',
'OPENAI_PARAMS': 'audio.tts.openai.params',
'API_KEY': 'audio.tts.api_key',
'ENGINE': 'audio.tts.engine',
'MODEL': 'audio.tts.model',
'VOICE': 'audio.tts.voice',
'SPLIT_ON': 'audio.tts.split_on',
'AZURE_SPEECH_REGION': 'audio.tts.azure.speech_region',
'AZURE_SPEECH_BASE_URL': 'audio.tts.azure.speech_base_url',
'AZURE_SPEECH_OUTPUT_FORMAT': 'audio.tts.azure.speech_output_format',
'MISTRAL_API_KEY': 'audio.tts.mistral.api_key',
'MISTRAL_API_BASE_URL': 'audio.tts.mistral.api_base_url',
}
STT_CONFIG_KEYS = {
'OPENAI_API_BASE_URL': 'audio.stt.openai.api_base_url',
'OPENAI_API_KEY': 'audio.stt.openai.api_key',
'ENGINE': 'audio.stt.engine',
'MODEL': 'audio.stt.model',
'SUPPORTED_CONTENT_TYPES': 'audio.stt.supported_content_types',
'ALLOWED_EXTENSIONS': 'audio.stt.allowed_extensions',
'WHISPER_MODEL': 'audio.stt.whisper_model',
'DEEPGRAM_API_KEY': 'audio.stt.deepgram.api_key',
'AZURE_API_KEY': 'audio.stt.azure.api_key',
'AZURE_REGION': 'audio.stt.azure.region',
'AZURE_LOCALES': 'audio.stt.azure.locales',
'AZURE_BASE_URL': 'audio.stt.azure.base_url',
'AZURE_MAX_SPEAKERS': 'audio.stt.azure.max_speakers',
'MISTRAL_API_KEY': 'audio.stt.mistral.api_key',
'MISTRAL_API_BASE_URL': 'audio.stt.mistral.api_base_url',
'MISTRAL_USE_CHAT_COMPLETIONS': 'audio.stt.mistral.use_chat_completions',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
def is_audio_conversion_required(file_path):
"""
@@ -228,119 +273,28 @@ class AudioConfigUpdateForm(BaseModel):
@router.get('/config')
async def get_audio_config(request: Request, user=Depends(get_admin_user)):
return {
'tts': {
'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL,
'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY,
'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS,
'API_KEY': request.app.state.config.TTS_API_KEY,
'ENGINE': request.app.state.config.TTS_ENGINE,
'MODEL': request.app.state.config.TTS_MODEL,
'VOICE': request.app.state.config.TTS_VOICE,
'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON,
'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION,
'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY,
'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL,
},
'stt': {
'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL,
'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY,
'ENGINE': request.app.state.config.STT_ENGINE,
'MODEL': request.app.state.config.STT_MODEL,
'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES,
'ALLOWED_EXTENSIONS': request.app.state.config.STT_ALLOWED_EXTENSIONS,
'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL,
'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY,
'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY,
'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION,
'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES,
'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
},
'tts': await get_config_values(TTS_CONFIG_KEYS),
'stt': await get_config_values(STT_CONFIG_KEYS),
}
@router.post('/config/update')
async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)):
# TTS settings
request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
request.app.state.config.TTS_OPENAI_PARAMS = form_data.tts.OPENAI_PARAMS
request.app.state.config.TTS_API_KEY = form_data.tts.API_KEY
request.app.state.config.TTS_ENGINE = form_data.tts.ENGINE
request.app.state.config.TTS_MODEL = form_data.tts.MODEL
request.app.state.config.TTS_VOICE = form_data.tts.VOICE
request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = form_data.tts.AZURE_SPEECH_BASE_URL
request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
request.app.state.config.TTS_MISTRAL_API_KEY = form_data.tts.MISTRAL_API_KEY
request.app.state.config.TTS_MISTRAL_API_BASE_URL = form_data.tts.MISTRAL_API_BASE_URL
await Config.upsert(
{
**config_updates(form_data.tts.model_dump(), TTS_CONFIG_KEYS),
**config_updates(form_data.stt.model_dump(), STT_CONFIG_KEYS),
}
)
# STT settings
request.app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
request.app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
request.app.state.config.STT_ENGINE = form_data.stt.ENGINE
request.app.state.config.STT_MODEL = form_data.stt.MODEL
request.app.state.config.STT_SUPPORTED_CONTENT_TYPES = form_data.stt.SUPPORTED_CONTENT_TYPES
request.app.state.config.STT_ALLOWED_EXTENSIONS = form_data.stt.ALLOWED_EXTENSIONS
request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY
request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = form_data.stt.AZURE_MAX_SPEAKERS
request.app.state.config.AUDIO_STT_MISTRAL_API_KEY = form_data.stt.MISTRAL_API_KEY
request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = form_data.stt.MISTRAL_API_BASE_URL
request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS
if request.app.state.config.STT_ENGINE == '':
if form_data.stt.ENGINE == '':
request.app.state.faster_whisper_model = set_faster_whisper_model(
form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE
)
else:
request.app.state.faster_whisper_model = None
return {
'tts': {
'ENGINE': request.app.state.config.TTS_ENGINE,
'MODEL': request.app.state.config.TTS_MODEL,
'VOICE': request.app.state.config.TTS_VOICE,
'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL,
'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY,
'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS,
'API_KEY': request.app.state.config.TTS_API_KEY,
'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON,
'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION,
'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL,
'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY,
'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL,
},
'stt': {
'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL,
'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY,
'ENGINE': request.app.state.config.STT_ENGINE,
'MODEL': request.app.state.config.STT_MODEL,
'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES,
'ALLOWED_EXTENSIONS': request.app.state.config.STT_ALLOWED_EXTENSIONS,
'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL,
'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY,
'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY,
'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION,
'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES,
'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY,
'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL,
'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS,
},
}
return await get_audio_config(request, user)
def load_speech_pipeline(request):
@@ -388,14 +342,16 @@ async def _write_tts_cache(
async def _tts_openai(request, payload, file_path, file_body_path, user):
"""Generate speech via an OpenAI-compatible TTS endpoint."""
payload['model'] = request.app.state.config.TTS_MODEL
payload['model'] = await Config.get('audio.tts.model')
if not payload.get('voice'):
payload['voice'] = request.app.state.config.TTS_VOICE
payload = {**payload, **(request.app.state.config.TTS_OPENAI_PARAMS or {})}
payload['voice'] = await Config.get('audio.tts.voice')
payload = {**payload, **(await Config.get('audio.tts.openai.params') or {})}
api_key = await Config.get('audio.tts.openai.api_key')
api_base_url = await Config.get('audio.tts.openai.api_base_url')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {request.app.state.config.TTS_OPENAI_API_KEY}',
'Authorization': f'Bearer {api_key}',
}
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers = include_user_info_headers(headers, user)
@@ -404,7 +360,7 @@ async def _tts_openai(request, payload, file_path, file_body_path, user):
try:
session = await get_session()
r = await session.post(
url=f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech',
url=f'{api_base_url}/audio/speech',
json=payload,
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -444,13 +400,13 @@ async def _tts_elevenlabs(request, payload, file_path, file_body_path, user):
f'{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}',
json={
'text': payload['input'],
'model_id': request.app.state.config.TTS_MODEL,
'model_id': await Config.get('audio.tts.model'),
'voice_settings': {'stability': 0.5, 'similarity_boost': 0.5},
},
headers={
'Accept': 'audio/mpeg',
'Content-Type': 'application/json',
'xi-api-key': request.app.state.config.TTS_API_KEY,
'xi-api-key': await Config.get('audio.tts.api_key'),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
@@ -464,11 +420,11 @@ async def _tts_elevenlabs(request, payload, file_path, file_body_path, user):
async def _tts_azure(request, payload, file_path, file_body_path, user):
"""Generate speech via Azure Cognitive Services TTS."""
az_region = request.app.state.config.TTS_AZURE_SPEECH_REGION or 'eastus'
az_base = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
language = payload.get('voice') or request.app.state.config.TTS_VOICE
az_region = await Config.get('audio.tts.azure.speech_region') or 'eastus'
az_base = await Config.get('audio.tts.azure.speech_base_url')
language = payload.get('voice') or await Config.get('audio.tts.voice')
locale = '-'.join(language.split('-')[:2])
output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
output_format = await Config.get('audio.tts.azure.speech_output_format')
ssml = (
f'<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">'
@@ -482,7 +438,7 @@ async def _tts_azure(request, payload, file_path, file_body_path, user):
async with session.post(
(az_base or f'https://{az_region}.tts.speech.microsoft.com') + '/cognitiveservices/v1',
headers={
'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY,
'Ocp-Apim-Subscription-Key': await Config.get('audio.tts.api_key'),
'Content-Type': 'application/ssml+xml',
'X-Microsoft-OutputFormat': output_format,
},
@@ -505,7 +461,7 @@ async def _tts_transformers(request, payload, file_path, file_body_path, user):
load_speech_pipeline(request)
embeddings = request.app.state.speech_speaker_embeddings_dataset
model_name = request.app.state.config.TTS_MODEL
model_name = await Config.get('audio.tts.model')
idx = 6799
try:
@@ -533,8 +489,8 @@ async def _tts_transformers(request, payload, file_path, file_body_path, user):
async def _tts_mistral(request, payload, file_path, file_body_path, user):
"""Generate speech via the Mistral TTS API."""
api_key = request.app.state.config.TTS_MISTRAL_API_KEY
api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
api_key = await Config.get('audio.tts.mistral.api_key')
api_base_url = await Config.get('audio.tts.mistral.api_base_url') or 'https://api.mistral.ai/v1'
if not api_key:
raise HTTPException(status_code=400, detail='Mistral API key is required for Mistral TTS')
@@ -546,7 +502,7 @@ async def _tts_mistral(request, payload, file_path, file_body_path, user):
url=f'{api_base_url}/audio/speech',
json={
'input': payload.get('input', ''), # text to synthesize
'model': request.app.state.config.TTS_MODEL or 'voxtral-mini-tts-2603',
'model': await Config.get('audio.tts.model') or 'voxtral-mini-tts-2603',
'voice_id': payload.get('voice', ''),
'response_format': 'mp3',
},
@@ -582,7 +538,7 @@ _TTS_ENGINES = {
@router.post('/speech')
async def speech(request: Request, user=Depends(get_verified_user)):
engine = request.app.state.config.TTS_ENGINE
engine = await Config.get('audio.tts.engine')
if engine == '':
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -590,7 +546,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
)
if user.role != 'admin' and not await has_permission(
user.id, 'chat.tts', request.app.state.config.USER_PERMISSIONS
user.id, 'chat.tts', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -599,7 +555,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
name = hashlib.sha256(
body + str(engine).encode('utf-8') + str(request.app.state.config.TTS_MODEL).encode('utf-8')
body + str(engine).encode('utf-8') + str(await Config.get('audio.tts.model')).encode('utf-8')
).hexdigest()
file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3')
@@ -624,7 +580,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async def _transcribe_whisper(request, file_path, languages, file_dir, id):
if request.app.state.faster_whisper_model is None:
request.app.state.faster_whisper_model = set_faster_whisper_model(request.app.state.config.WHISPER_MODEL)
request.app.state.faster_whisper_model = set_faster_whisper_model(await Config.get('audio.stt.whisper_model'))
model = request.app.state.faster_whisper_model
@@ -655,11 +611,13 @@ async def _transcribe_openai(request, file_path, filename, languages, file_dir,
try:
session = await get_session()
for language in languages:
payload = {'model': request.app.state.config.STT_MODEL}
payload = {'model': await Config.get('audio.stt.model')}
if language:
payload['language'] = language
api_key = await Config.get('audio.stt.openai.api_key')
api_base_url = await Config.get('audio.stt.openai.api_base_url')
headers = {'Authorization': f'Bearer {request.app.state.config.STT_OPENAI_API_KEY}'}
headers = {'Authorization': f'Bearer {api_key}'}
if user and ENABLE_FORWARD_USER_INFO_HEADERS:
headers = include_user_info_headers(headers, user)
@@ -669,7 +627,7 @@ async def _transcribe_openai(request, file_path, filename, languages, file_dir,
form_data.add_field('file', open(file_path, 'rb'), filename=filename)
r = await session.post(
url=f'{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions',
url=f'{api_base_url}/audio/transcriptions',
headers=headers,
data=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -703,8 +661,8 @@ async def _transcribe_deepgram(request, file_path, languages, file_dir, id):
async with aiofiles.open(file_path, 'rb') as f:
audio_bytes = await f.read()
api_key = request.app.state.config.DEEPGRAM_API_KEY
stt_model = request.app.state.config.STT_MODEL
api_key = await Config.get('audio.stt.deepgram.api_key')
stt_model = await Config.get('audio.stt.model')
r = None
try:
@@ -771,11 +729,11 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
detail=f'File size ({audio_size // (1024 * 1024)}MB) exceeds Azure limit of {AZURE_MAX_FILE_SIZE_MB}MB',
)
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
region = request.app.state.config.AUDIO_STT_AZURE_REGION or 'eastus'
locale_str = request.app.state.config.AUDIO_STT_AZURE_LOCALES
base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL
max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3
api_key = await Config.get('audio.stt.azure.api_key')
region = await Config.get('audio.stt.azure.region') or 'eastus'
locale_str = await Config.get('audio.stt.azure.locales')
base_url = await Config.get('audio.stt.azure.base_url')
max_speakers = await Config.get('audio.stt.azure.max_speakers') or 3
# Default to a broad set of locales when none are configured
if len(locale_str) < 2:
@@ -885,16 +843,16 @@ async def transcription_handler(request, file_path, metadata, user=None):
None, # Always fallback to None in case transcription fails
]
if request.app.state.config.STT_ENGINE == '':
if await Config.get('audio.stt.engine') == '':
return await _transcribe_whisper(request, file_path, languages, file_dir, id)
elif request.app.state.config.STT_ENGINE == 'openai':
elif await Config.get('audio.stt.engine') == 'openai':
return await _transcribe_openai(request, file_path, filename, languages, file_dir, id, user)
elif request.app.state.config.STT_ENGINE == 'deepgram':
elif await Config.get('audio.stt.engine') == 'deepgram':
return await _transcribe_deepgram(request, file_path, languages, file_dir, id)
elif request.app.state.config.STT_ENGINE == 'azure':
elif await Config.get('audio.stt.engine') == 'azure':
return await _transcribe_azure(request, file_path, filename, file_dir, id)
elif request.app.state.config.STT_ENGINE == 'mistral':
elif await Config.get('audio.stt.engine') == 'mistral':
return await _transcribe_mistral(request, file_path, filename, metadata, file_dir, id)
@@ -907,16 +865,16 @@ async def _transcribe_mistral(request, file_path, filename, metadata, file_dir,
if file_size > MAX_FILE_SIZE:
raise HTTPException(status_code=400, detail=f'File size exceeds limit of {MAX_FILE_SIZE_MB}MB')
api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY
api_base_url = request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
use_chat_completions = request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS
api_key = await Config.get('audio.stt.mistral.api_key')
api_base_url = await Config.get('audio.stt.mistral.api_base_url') or 'https://api.mistral.ai/v1'
use_chat_completions = await Config.get('audio.stt.mistral.use_chat_completions')
if not api_key:
raise HTTPException(status_code=400, detail='Mistral API key is required for Mistral STT')
r = None
try:
model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest'
model = await Config.get('audio.stt.model') or 'voxtral-mini-latest'
log.info(
f'Mistral STT - model: {model}, method: {"chat_completions" if use_chat_completions else "transcriptions"}'
)
@@ -1158,14 +1116,14 @@ async def transcription(
user=Depends(get_verified_user),
):
if user.role != 'admin' and not await has_permission(
user.id, 'chat.stt', request.app.state.config.USER_PERMISSIONS
user.id, 'chat.stt', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
log.info(f'file.content_type: {file.content_type}')
stt_supported_content_types = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', [])
stt_supported_content_types = await Config.get('audio.stt.supported_content_types', [])
if not strict_match_mime_type(stt_supported_content_types, file.content_type):
raise HTTPException(
@@ -1177,7 +1135,7 @@ async def transcription(
safe_name = os.path.basename(file.filename) if file.filename else ''
ext = safe_name.rsplit('.', 1)[-1].lower() if '.' in safe_name else ''
allowed_extensions = getattr(request.app.state.config, 'STT_ALLOWED_EXTENSIONS', [])
allowed_extensions = await Config.get('audio.stt.allowed_extensions', [])
if allowed_extensions and ext not in allowed_extensions:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -1237,11 +1195,11 @@ async def transcription(
async def get_available_models(request: Request) -> list[dict]:
"""Return the list of available TTS models for the configured engine."""
available_models = []
engine = request.app.state.config.TTS_ENGINE
engine = await Config.get('audio.tts.engine')
_timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
if engine == 'openai':
base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL
base_url = await Config.get('audio.tts.openai.api_base_url')
if not base_url.startswith('https://api.openai.com'):
session = await get_session()
try:
@@ -1276,7 +1234,7 @@ async def get_available_models(request: Request) -> list[dict]:
async with session.get(
f'{ELEVENLABS_API_BASE_URL}/v1/models',
headers={
'xi-api-key': request.app.state.config.TTS_API_KEY,
'xi-api-key': await Config.get('audio.tts.api_key'),
'Content-Type': 'application/json',
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -1311,11 +1269,11 @@ _OPENAI_DEFAULT_VOICES = {
async def get_available_voices(request) -> dict:
"""Return ``{voice_id: voice_name}`` for the configured TTS engine."""
engine = request.app.state.config.TTS_ENGINE
engine = await Config.get('audio.tts.engine')
_timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
if engine == 'openai':
base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL
base_url = await Config.get('audio.tts.openai.api_base_url')
if not base_url.startswith('https://api.openai.com'):
try:
session = await get_session()
@@ -1338,7 +1296,7 @@ async def get_available_voices(request) -> dict:
async with session.get(
f'{ELEVENLABS_API_BASE_URL}/v1/voices',
headers={
'xi-api-key': request.app.state.config.TTS_API_KEY,
'xi-api-key': await Config.get('audio.tts.api_key'),
'Content-Type': 'application/json',
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -1353,14 +1311,14 @@ async def get_available_voices(request) -> dict:
if engine == 'azure':
try:
region = request.app.state.config.TTS_AZURE_SPEECH_REGION
base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL
region = await Config.get('audio.tts.azure.speech_region')
base_url = await Config.get('audio.tts.azure.speech_base_url')
url = (base_url or f'https://{region}.tts.speech.microsoft.com') + '/cognitiveservices/voices/list'
session = await get_session()
async with session.get(
url,
headers={'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY},
headers={'Ocp-Apim-Subscription-Key': await Config.get('audio.tts.api_key')},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
timeout=_timeout,
) as resp:
@@ -1372,8 +1330,8 @@ async def get_available_voices(request) -> dict:
return {}
if engine == 'mistral':
api_key = request.app.state.config.TTS_MISTRAL_API_KEY
api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1'
api_key = await Config.get('audio.tts.mistral.api_key')
api_base_url = await Config.get('audio.tts.mistral.api_base_url') or 'https://api.mistral.ai/v1'
if api_key:
try:
session = await get_session()
+263 -198
View File
@@ -8,21 +8,15 @@ import time
import urllib
import uuid
from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
from typing import List, Optional
from aiohttp import ClientSession
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse, Response
from fastapi.responses import JSONResponse, Response
from ldap3 import NONE, Connection, Server, Tls
from ldap3.utils.conv import escape_filter_chars
from open_webui.config import (
ENABLE_LDAP,
ENABLE_OAUTH_SIGNUP,
ENABLE_PASSWORD_AUTH,
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
OAUTH_PROVIDERS,
OPENID_END_SESSION_ENDPOINT,
OPENID_PROVIDER_URL,
)
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
@@ -50,6 +44,7 @@ from open_webui.models.auths import (
Token,
UpdatePasswordForm,
)
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.models.users import (
@@ -75,7 +70,6 @@ from open_webui.utils.auth import (
)
from open_webui.utils.groups import apply_default_group_assignment
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.oauth import auth_manager_config
from open_webui.utils.rate_limit import RateLimiter
from open_webui.utils.redis import get_redis_client
from open_webui.utils.webhook import post_webhook
@@ -90,6 +84,60 @@ log = logging.getLogger(__name__)
# who exceed their allotted rate against this gate.
signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3)
ADMIN_CONFIG_KEYS = {
'SHOW_ADMIN_DETAILS': 'auth.admin.show',
'ADMIN_EMAIL': 'auth.admin.email',
'WEBUI_URL': 'webui.url',
'ENABLE_SIGNUP': 'ui.enable_signup',
'ENABLE_API_KEYS': 'auth.enable_api_keys',
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': 'auth.api_key.endpoint_restrictions',
'API_KEYS_ALLOWED_ENDPOINTS': 'auth.api_key.allowed_endpoints',
'DEFAULT_USER_ROLE': 'ui.default_user_role',
'DEFAULT_GROUP_ID': 'ui.default_group_id',
'JWT_EXPIRES_IN': 'auth.jwt_expiry',
'ENABLE_COMMUNITY_SHARING': 'ui.enable_community_sharing',
'ENABLE_MESSAGE_RATING': 'ui.enable_message_rating',
'ENABLE_FOLDERS': 'folders.enable',
'FOLDER_MAX_FILE_COUNT': 'folders.max_file_count',
'AUTOMATION_MAX_COUNT': 'automations.max_count',
'AUTOMATION_MIN_INTERVAL': 'automations.min_interval',
'ENABLE_AUTOMATIONS': 'automations.enable',
'ENABLE_CHANNELS': 'channels.enable',
'ENABLE_CALENDAR': 'calendar.enable',
'ENABLE_MEMORIES': 'memories.enable',
'ENABLE_NOTES': 'notes.enable',
'ENABLE_USER_WEBHOOKS': 'ui.enable_user_webhooks',
'ENABLE_USER_STATUS': 'users.enable_status',
'PENDING_USER_OVERLAY_TITLE': 'ui.pending_user_overlay_title',
'PENDING_USER_OVERLAY_CONTENT': 'ui.pending_user_overlay_content',
'RESPONSE_WATERMARK': 'ui.watermark',
}
LDAP_SERVER_CONFIG_KEYS = {
'label': 'ldap.server.label',
'host': 'ldap.server.host',
'port': 'ldap.server.port',
'attribute_for_mail': 'ldap.server.attribute_for_mail',
'attribute_for_username': 'ldap.server.attribute_for_username',
'app_dn': 'ldap.server.app_dn',
'app_dn_password': 'ldap.server.app_password',
'search_base': 'ldap.server.users_dn',
'search_filters': 'ldap.server.search_filter',
'use_tls': 'ldap.server.use_tls',
'certificate_path': 'ldap.server.ca_cert_file',
'validate_cert': 'ldap.server.validate_cert',
'ciphers': 'ldap.server.ciphers',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
async def create_session_response(
request: Request, user, db, response: Response = None, set_cookie: bool = False
@@ -105,7 +153,7 @@ async def create_session_response(
response: FastAPI response object (required if set_cookie is True)
set_cookie: Whether to set the auth cookie on the response
"""
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_delta = parse_duration(await Config.get('auth.jwt_expiry'))
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
@@ -128,7 +176,7 @@ async def create_session_response(
**({'max_age': max_age} if max_age is not None else {}),
)
user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
user_permissions = await get_permissions(user.id, await Config.get('user.permissions'), db=db)
return {
'token': token,
@@ -201,7 +249,7 @@ async def get_session_user(
**({'max_age': max_age} if max_age is not None else {}),
)
user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
user_permissions = await get_permissions(user.id, await Config.get('user.permissions'), db=db)
response_data = {
'token': token,
@@ -320,7 +368,7 @@ async def ldap_auth(
db: AsyncSession = Depends(get_async_session),
):
# Security checks FIRST - before loading any config
if not request.app.state.config.ENABLE_LDAP:
if not await Config.get('ldap.enable'):
raise HTTPException(400, detail='LDAP authentication is not enabled')
if not ENABLE_PASSWORD_AUTH:
@@ -338,19 +386,19 @@ async def ldap_auth(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# NOW load LDAP config variables
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST
LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT
LDAP_ATTRIBUTE_FOR_MAIL = request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL
LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME
LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE
LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS
LDAP_APP_DN = request.app.state.config.LDAP_APP_DN
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_VALIDATE_CERT = CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE
LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL'
LDAP_SERVER_LABEL = await Config.get('ldap.server.label')
LDAP_SERVER_HOST = await Config.get('ldap.server.host')
LDAP_SERVER_PORT = await Config.get('ldap.server.port')
LDAP_ATTRIBUTE_FOR_MAIL = await Config.get('ldap.server.attribute_for_mail')
LDAP_ATTRIBUTE_FOR_USERNAME = await Config.get('ldap.server.attribute_for_username')
LDAP_SEARCH_BASE = await Config.get('ldap.server.users_dn')
LDAP_SEARCH_FILTERS = await Config.get('ldap.server.search_filter')
LDAP_APP_DN = await Config.get('ldap.server.app_dn')
LDAP_APP_PASSWORD = await Config.get('ldap.server.app_password')
LDAP_USE_TLS = await Config.get('ldap.server.use_tls')
LDAP_CA_CERT_FILE = await Config.get('ldap.server.ca_cert_file')
LDAP_VALIDATE_CERT = CERT_REQUIRED if await Config.get('ldap.server.validate_cert') else CERT_NONE
LDAP_CIPHERS = await Config.get('ldap.server.ciphers') if await Config.get('ldap.server.ciphers') else 'ALL'
try:
tls = Tls(
@@ -381,9 +429,9 @@ async def ldap_auth(
if not await asyncio.to_thread(connection_app.bind):
raise HTTPException(400, detail='Application account bind failed')
ENABLE_LDAP_GROUP_MANAGEMENT = request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT
ENABLE_LDAP_GROUP_CREATION = request.app.state.config.ENABLE_LDAP_GROUP_CREATION
LDAP_ATTRIBUTE_FOR_GROUPS = request.app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS
ENABLE_LDAP_GROUP_MANAGEMENT = await Config.get('ldap.group.enable_management')
ENABLE_LDAP_GROUP_CREATION = await Config.get('ldap.group.enable_creation')
LDAP_ATTRIBUTE_FOR_GROUPS = await Config.get('ldap.server.attribute_for_groups')
search_attributes = [
f'{LDAP_ATTRIBUTE_FOR_USERNAME}',
@@ -500,7 +548,7 @@ async def ldap_auth(
email=email,
password=str(uuid.uuid4()),
name=cn,
role=request.app.state.config.DEFAULT_USER_ROLE,
role=await Config.get('ui.default_user_role'),
db=db,
)
@@ -514,15 +562,15 @@ async def ldap_auth(
user = await Users.get_user_by_id(user.id, db=db)
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
if request.app.state.config.WEBHOOK_URL:
if await Config.get('webhook_url'):
await post_webhook(
request.app.state.WEBUI_NAME,
request.app.state.config.WEBHOOK_URL,
await Config.get('webhook_url'),
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
'action': 'signup',
@@ -703,7 +751,7 @@ async def signup_handler(
password=hashed,
name=name,
profile_image_url=profile_image_url,
role=request.app.state.config.DEFAULT_USER_ROLE,
role=await Config.get('ui.default_user_role'),
db=db,
)
if not user:
@@ -714,12 +762,12 @@ async def signup_handler(
if await Users.get_num_users(db=db) == 1:
await Users.update_user_role_by_id(user.id, 'admin', db=db)
user = await Users.get_user_by_id(user.id, db=db)
request.app.state.config.ENABLE_SIGNUP = False
await Config.upsert({'ui.enable_signup': False})
if request.app.state.config.WEBHOOK_URL:
if await Config.get('webhook_url'):
await post_webhook(
request.app.state.WEBUI_NAME,
request.app.state.config.WEBHOOK_URL,
await Config.get('webhook_url'),
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
'action': 'signup',
@@ -729,7 +777,7 @@ async def signup_handler(
)
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
@@ -748,10 +796,10 @@ async def signup(
if WEBUI_AUTH:
if has_users:
if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM:
if not await Config.get('ui.enable_signup') or not await Config.get('ui.enable_login_form'):
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
# Don't gate the first admin on ENABLE_SIGNUP: it auto-disables and can persist stale across a DB reset.
elif not request.app.state.config.ENABLE_LOGIN_FORM and not ENABLE_INITIAL_ADMIN_SIGNUP:
elif not await Config.get('ui.enable_login_form') and not ENABLE_INITIAL_ADMIN_SIGNUP:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
else:
if has_users:
@@ -812,19 +860,21 @@ async def signout(request: Request, response: Response, db: AsyncSession = Depen
# If a custom end_session_endpoint is configured (e.g. AWS Cognito), redirect
# there directly instead of attempting OIDC discovery.
if OPENID_END_SESSION_ENDPOINT.value:
openid_end_session_endpoint = await Config.get('oauth.end_session_endpoint')
if openid_end_session_endpoint:
return JSONResponse(
status_code=200,
content={
'status': True,
'redirect_url': OPENID_END_SESSION_ENDPOINT.value,
'redirect_url': openid_end_session_endpoint,
},
headers=response.headers,
)
openid_provider_url = await Config.get('oauth.provider_url')
oauth_server_metadata_url = (
request.app.state.oauth_manager.get_server_metadata_url(session.provider) if session else None
) or OPENID_PROVIDER_URL.value
) or openid_provider_url
if session and oauth_server_metadata_url:
oauth_id_token = session.token.get('id_token')
@@ -934,12 +984,12 @@ async def add_user(
if user:
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_delta = parse_duration(await Config.get('auth.jwt_expiry'))
token = create_token(data={'id': user.id}, expires_delta=expires_delta)
return {
'token': token,
@@ -968,8 +1018,8 @@ async def add_user(
async def get_admin_details(
request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)
):
if request.app.state.config.SHOW_ADMIN_DETAILS:
admin_email = request.app.state.config.ADMIN_EMAIL
if await Config.get('auth.admin.show'):
admin_email = await Config.get('auth.admin.email')
admin_name = None
log.info(f'Admin details - Email: {admin_email}, Name: {admin_name}')
@@ -999,34 +1049,7 @@ async def get_admin_details(
@router.get('/admin/config')
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
return {
'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS,
'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL,
'WEBUI_URL': request.app.state.config.WEBUI_URL,
'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP,
'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS,
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE,
'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID,
'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN,
'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING,
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS,
'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE,
'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK,
}
return await get_config_values(ADMIN_CONFIG_KEYS)
class AdminConfig(BaseModel):
@@ -1060,81 +1083,24 @@ class AdminConfig(BaseModel):
@router.post('/admin/config')
async def update_admin_config(request: Request, form_data: AdminConfig, user=Depends(get_admin_user)):
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
request.app.state.config.ADMIN_EMAIL = form_data.ADMIN_EMAIL
request.app.state.config.WEBUI_URL = form_data.WEBUI_URL
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
request.app.state.config.ENABLE_API_KEYS = form_data.ENABLE_API_KEYS
request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = form_data.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS
request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS = form_data.API_KEYS_ALLOWED_ENDPOINTS
request.app.state.config.ENABLE_FOLDERS = form_data.ENABLE_FOLDERS
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
)
request.app.state.config.AUTOMATION_MAX_COUNT = (
int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
)
request.app.state.config.AUTOMATION_MIN_INTERVAL = (
updates = config_updates(form_data.model_dump(), ADMIN_CONFIG_KEYS)
updates['folders.max_file_count'] = int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
updates['automations.max_count'] = int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
updates['automations.min_interval'] = (
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
)
request.app.state.config.ENABLE_AUTOMATIONS = form_data.ENABLE_AUTOMATIONS
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ['pending', 'user', 'admin']:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
request.app.state.config.DEFAULT_GROUP_ID = form_data.DEFAULT_GROUP_ID
if form_data.DEFAULT_USER_ROLE not in ['pending', 'user', 'admin']:
updates.pop('ui.default_user_role', None)
pattern = r'^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$'
# Check if the input string matches the pattern
if re.match(pattern, form_data.JWT_EXPIRES_IN):
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
if not re.match(pattern, form_data.JWT_EXPIRES_IN):
updates.pop('auth.jwt_expiry', None)
request.app.state.config.ENABLE_COMMUNITY_SHARING = form_data.ENABLE_COMMUNITY_SHARING
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS
request.app.state.config.PENDING_USER_OVERLAY_TITLE = form_data.PENDING_USER_OVERLAY_TITLE
request.app.state.config.PENDING_USER_OVERLAY_CONTENT = form_data.PENDING_USER_OVERLAY_CONTENT
request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK
return {
'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS,
'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL,
'WEBUI_URL': request.app.state.config.WEBUI_URL,
'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP,
'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS,
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE,
'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID,
'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN,
'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING,
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS,
'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE,
'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK,
}
await Config.upsert(updates)
return await get_config_values(ADMIN_CONFIG_KEYS)
class LdapServerConfig(BaseModel):
@@ -1155,21 +1121,7 @@ class LdapServerConfig(BaseModel):
@router.get('/admin/config/ldap/server', response_model=LdapServerConfig)
async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
return {
'label': request.app.state.config.LDAP_SERVER_LABEL,
'host': request.app.state.config.LDAP_SERVER_HOST,
'port': request.app.state.config.LDAP_SERVER_PORT,
'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
'app_dn': request.app.state.config.LDAP_APP_DN,
'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD,
'search_base': request.app.state.config.LDAP_SEARCH_BASE,
'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS,
'use_tls': request.app.state.config.LDAP_USE_TLS,
'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE,
'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT,
'ciphers': request.app.state.config.LDAP_CIPHERS,
}
return await get_config_values(LDAP_SERVER_CONFIG_KEYS)
@router.post('/admin/config/ldap/server')
@@ -1186,40 +1138,16 @@ async def update_ldap_server(request: Request, form_data: LdapServerConfig, user
if not value:
raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key))
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
request.app.state.config.LDAP_SERVER_HOST = form_data.host
request.app.state.config.LDAP_SERVER_PORT = form_data.port
request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = form_data.attribute_for_mail
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username
request.app.state.config.LDAP_APP_DN = form_data.app_dn or ''
request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password or ''
request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert
request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return {
'label': request.app.state.config.LDAP_SERVER_LABEL,
'host': request.app.state.config.LDAP_SERVER_HOST,
'port': request.app.state.config.LDAP_SERVER_PORT,
'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
'app_dn': request.app.state.config.LDAP_APP_DN,
'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD,
'search_base': request.app.state.config.LDAP_SEARCH_BASE,
'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS,
'use_tls': request.app.state.config.LDAP_USE_TLS,
'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE,
'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT,
'ciphers': request.app.state.config.LDAP_CIPHERS,
}
updates = config_updates(form_data.model_dump(), LDAP_SERVER_CONFIG_KEYS)
updates['ldap.server.app_dn'] = form_data.app_dn or ''
updates['ldap.server.app_password'] = form_data.app_dn_password or ''
await Config.upsert(updates)
return await get_config_values(LDAP_SERVER_CONFIG_KEYS)
@router.get('/admin/config/ldap')
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP}
return {'ENABLE_LDAP': await Config.get('ldap.enable')}
class LdapConfigForm(BaseModel):
@@ -1228,8 +1156,8 @@ class LdapConfigForm(BaseModel):
@router.post('/admin/config/ldap')
async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP}
await Config.upsert({'ldap.enable': form_data.enable_ldap})
return {'ENABLE_LDAP': await Config.get('ldap.enable')}
############################
@@ -1237,11 +1165,148 @@ async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=D
############################
class OAuthConfigForm(BaseModel):
"""All OAuth/OIDC settings exposed to the admin panel."""
# General OAuth
ENABLE_OAUTH_SIGNUP: bool | None = None
OAUTH_MERGE_ACCOUNTS_BY_EMAIL: bool | None = None
OAUTH_AUTO_REDIRECT: bool | None = None
OAUTH_ALLOWED_DOMAINS: str | None = None
OAUTH_BLOCKED_GROUPS: str | None = None
# Role management
ENABLE_OAUTH_ROLE_MANAGEMENT: bool | None = None
OAUTH_ROLES_CLAIM: str | None = None
OAUTH_ADMIN_ROLES: str | None = None
OAUTH_ALLOWED_ROLES: str | None = None
# Group management
ENABLE_OAUTH_GROUP_MANAGEMENT: bool | None = None
ENABLE_OAUTH_GROUP_CREATION: bool | None = None
OAUTH_GROUP_CLAIM: str | None = None
OAUTH_GROUP_DEFAULT_SHARE: bool | str | None = None
# OIDC provider settings
OAUTH_PROVIDER_NAME: str | None = None
OPENID_PROVIDER_URL: str | None = None
OAUTH_CLIENT_ID: str | None = None
OAUTH_CLIENT_SECRET: str | None = None
OPENID_REDIRECT_URI: str | None = None
OAUTH_SCOPES: str | None = None
OAUTH_CODE_CHALLENGE_METHOD: str | None = None
OAUTH_TOKEN_ENDPOINT_AUTH_METHOD: str | None = None
OPENID_END_SESSION_ENDPOINT: str | None = None
OAUTH_TIMEOUT: int | str | None = None
OAUTH_CLIENT_TIMEOUT: int | str | None = None
# Claims
OAUTH_EMAIL_CLAIM: str | None = None
OAUTH_USERNAME_CLAIM: str | None = None
OAUTH_PICTURE_CLAIM: str | None = None
OAUTH_SUB_CLAIM: str | None = None
OAUTH_AUDIENCE: str | None = None
# Profile update toggles
OAUTH_UPDATE_EMAIL_ON_LOGIN: bool | None = None
OAUTH_UPDATE_NAME_ON_LOGIN: bool | None = None
OAUTH_UPDATE_PICTURE_ON_LOGIN: bool | None = None
# Token
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE: bool | None = None
OAUTH_COMMA_LIST_FIELDS = {
'OAUTH_ALLOWED_DOMAINS',
'OAUTH_ADMIN_ROLES',
'OAUTH_ALLOWED_ROLES',
}
OAUTH_CONFIG_KEYS = {
'ENABLE_OAUTH_SIGNUP': 'oauth.enable_signup',
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL': 'oauth.merge_accounts_by_email',
'OAUTH_AUTO_REDIRECT': 'oauth.auto_redirect',
'OAUTH_ALLOWED_DOMAINS': 'oauth.allowed_domains',
'OAUTH_BLOCKED_GROUPS': 'oauth.blocked_groups',
'ENABLE_OAUTH_ROLE_MANAGEMENT': 'oauth.enable_role_mapping',
'OAUTH_ROLES_CLAIM': 'oauth.roles_claim',
'OAUTH_ADMIN_ROLES': 'oauth.admin_roles',
'OAUTH_ALLOWED_ROLES': 'oauth.allowed_roles',
'ENABLE_OAUTH_GROUP_MANAGEMENT': 'oauth.enable_group_mapping',
'ENABLE_OAUTH_GROUP_CREATION': 'oauth.enable_group_creation',
'OAUTH_GROUP_CLAIM': 'oauth.group_claim',
'OAUTH_GROUP_DEFAULT_SHARE': 'oauth.group_default_share',
'OAUTH_PROVIDER_NAME': 'oauth.provider_name',
'OPENID_PROVIDER_URL': 'oauth.provider_url',
'OAUTH_CLIENT_ID': 'oauth.client_id',
'OAUTH_CLIENT_SECRET': 'oauth.client_secret',
'OPENID_REDIRECT_URI': 'oauth.redirect_uri',
'OAUTH_SCOPES': 'oauth.scopes',
'OAUTH_CODE_CHALLENGE_METHOD': 'oauth.code_challenge_method',
'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD': 'oauth.token_endpoint_auth_method',
'OPENID_END_SESSION_ENDPOINT': 'oauth.end_session_endpoint',
'OAUTH_TIMEOUT': 'oauth.timeout',
'OAUTH_CLIENT_TIMEOUT': 'oauth.client.timeout',
'OAUTH_EMAIL_CLAIM': 'oauth.email_claim',
'OAUTH_USERNAME_CLAIM': 'oauth.username_claim',
'OAUTH_PICTURE_CLAIM': 'oauth.picture_claim',
'OAUTH_SUB_CLAIM': 'oauth.sub_claim',
'OAUTH_AUDIENCE': 'oauth.audience',
'OAUTH_UPDATE_EMAIL_ON_LOGIN': 'oauth.update_email_on_login',
'OAUTH_UPDATE_NAME_ON_LOGIN': 'oauth.update_name_on_login',
'OAUTH_UPDATE_PICTURE_ON_LOGIN': 'oauth.update_picture_on_login',
'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE': 'oauth.refresh_token.include_scope',
}
def _format_oauth_form_value(field: str, value):
if field in OAUTH_COMMA_LIST_FIELDS and isinstance(value, list):
return ','.join(str(item) for item in value)
return value
def _parse_oauth_update_value(field: str, value):
if field in OAUTH_COMMA_LIST_FIELDS and isinstance(value, str):
return [item.strip() for item in value.split(',') if item.strip()]
if field in {'OAUTH_TIMEOUT', 'OAUTH_CLIENT_TIMEOUT'} and value == '':
return ''
return value
async def get_oauth_config_values() -> dict:
values = await Config.get_many(*OAUTH_CONFIG_KEYS.values())
return {
field: _format_oauth_form_value(field, values[storage_key])
for field, storage_key in OAUTH_CONFIG_KEYS.items()
if storage_key in values
}
def oauth_config_updates(data: dict) -> dict:
return {
OAUTH_CONFIG_KEYS[field]: _parse_oauth_update_value(field, value)
for field, value in data.items()
if field in OAUTH_CONFIG_KEYS
}
@router.get('/admin/config/oauth', response_model=OAuthConfigForm)
async def get_oauth_config(request: Request, user=Depends(get_admin_user)):
return await get_oauth_config_values()
@router.post('/admin/config/oauth', response_model=OAuthConfigForm)
async def update_oauth_config(request: Request, form_data: OAuthConfigForm, user=Depends(get_admin_user)):
await Config.upsert(oauth_config_updates(form_data.model_dump(exclude_none=True)))
return await get_oauth_config_values()
async def _check_api_key_permission(request: Request, user, db: AsyncSession):
if not request.app.state.config.ENABLE_API_KEYS or (
if not await Config.get('auth.enable_api_keys') or (
user.role != 'admin'
and not await has_permission(
user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.api_keys', await Config.get('user.permissions'), db=db
)
):
raise HTTPException(
@@ -1354,11 +1419,11 @@ async def token_exchange(
)
# Extract user information from the token claims
email_claim = request.app.state.config.OAUTH_EMAIL_CLAIM
username_claim = request.app.state.config.OAUTH_USERNAME_CLAIM
email_claim = await Config.get('oauth.email_claim', 'email')
# Get sub claim
sub = user_data.get(request.app.state.config.OAUTH_SUB_CLAIM or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
sub_claim = await Config.get('oauth.sub_claim')
sub = user_data.get(sub_claim or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
if not sub:
log.warning(f'Token exchange failed: sub claim missing from user data')
raise HTTPException(
@@ -1376,10 +1441,10 @@ async def token_exchange(
email = email.lower()
# Enforce domain allowlist — same check as the normal OAuth callback
if (
'*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
):
oauth_allowed_domains = await Config.get('oauth.allowed_domains', [])
if isinstance(oauth_allowed_domains, str):
oauth_allowed_domains = [domain.strip() for domain in oauth_allowed_domains.split(',') if domain.strip()]
if '*' not in oauth_allowed_domains and email.split('@')[-1] not in oauth_allowed_domains:
log.warning(f'Token exchange denied: email domain not in allowed domains list')
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -1389,7 +1454,7 @@ async def token_exchange(
# Try to find the user by OAuth sub
user = await Users.get_user_by_oauth_sub(provider, sub, db=db)
if not user and OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value:
if not user and await Config.get('oauth.merge_accounts_by_email'):
# Try to find by email if merge is enabled
user = await Users.get_user_by_email(email, db=db)
if user:
+6 -4
View File
@@ -14,6 +14,7 @@ from open_webui.models.automations import (
AutomationRuns,
Automations,
)
from open_webui.models.config import Config
from open_webui.utils.access_control import has_permission
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.automations import (
@@ -38,13 +39,14 @@ PAGE_ITEM_COUNT = 30
async def check_automations_permission(request, user):
if not request.app.state.config.ENABLE_AUTOMATIONS:
config = await Config.get_many('automations.enable', 'user.permissions')
if not config.get('automations.enable'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if user.role != 'admin' and not await has_permission(
user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS
user.id, 'features.automations', config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -72,7 +74,7 @@ async def check_automation_limits(request, user, rrule_str: str, db, is_create:
# Max count (create only)
if is_create:
max_count = request.app.state.config.AUTOMATION_MAX_COUNT
max_count = await Config.get('automations.max_count')
if max_count:
max_count = int(max_count)
if max_count > 0 and await Automations.count_by_user(user.id, db=db) >= max_count:
@@ -82,7 +84,7 @@ async def check_automation_limits(request, user, rrule_str: str, db, is_create:
)
# Min interval (create + update)
min_interval = request.app.state.config.AUTOMATION_MIN_INTERVAL
min_interval = await Config.get('automations.min_interval')
if min_interval:
min_interval = int(min_interval)
if min_interval > 0:
+9 -6
View File
@@ -19,6 +19,7 @@ from open_webui.models.calendar import (
CalendarUpdateForm,
RSVPForm,
)
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.users import UserModel
from open_webui.utils.access_control import filter_allowed_access_grants, has_permission
@@ -34,13 +35,14 @@ SCHEDULED_TASKS_CALENDAR_ID = '__scheduled_tasks__'
async def check_calendar_permission(request: Request, user):
"""Check global feature flag AND per-user permission for calendar access."""
if not request.app.state.config.ENABLE_CALENDAR:
config = await Config.get_many('calendar.enable', 'user.permissions')
if not config.get('calendar.enable'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if user.role != 'admin' and not await has_permission(
user.id, 'features.calendar', request.app.state.config.USER_PERMISSIONS
user.id, 'features.calendar', config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -50,11 +52,12 @@ async def check_calendar_permission(request: Request, user):
async def _user_has_automations(request: Request, user) -> bool:
"""Check if automations feature is available to this user."""
if not getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False):
config = await Config.get_many('automations.enable', 'user.permissions')
if not config.get('automations.enable', False):
return False
if user.role == 'admin':
return True
return await has_permission(user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS)
return await has_permission(user.id, 'features.automations', config.get('user.permissions'))
async def _check_calendar_access(calendar_id: str, user: UserModel, permission: str = 'write') -> CalendarModel:
@@ -116,7 +119,7 @@ async def create_calendar(request: Request, form_data: CalendarForm, user: UserM
# could create a calendar with `principal_id='*' permission='read'|'write'`,
# making their events readable or writable by any other verified user.
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -373,7 +376,7 @@ async def update_calendar(
# publicly readable/writable without the corresponding sharing permission.
if form_data.access_grants is not None:
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+8 -7
View File
@@ -11,6 +11,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import STATIC_DIR
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_public_write_access_grant
from open_webui.models.config import Config
from open_webui.models.channels import (
ChannelForm,
ChannelModel,
@@ -123,7 +124,7 @@ def get_channel_permitted_group_and_user_ids(
async def check_channels_access(request: Request, user: Optional[UserModel] = None):
"""Dependency to ensure channels are globally enabled."""
if not request.app.state.config.ENABLE_CHANNELS:
if not await Config.get('channels.enable'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Channels'),
@@ -131,7 +132,7 @@ async def check_channels_access(request: Request, user: Optional[UserModel] = No
if user:
if user.role != 'admin' and not await has_permission(
user.id, 'features.channels', request.app.state.config.USER_PERMISSIONS
user.id, 'features.channels', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -292,7 +293,7 @@ async def create_new_channel(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -663,7 +664,7 @@ async def update_channel_by_id(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -857,8 +858,8 @@ async def get_pinned_channel_messages(
async def send_notification(request, channel, message, active_user_ids, db=None):
name = request.app.state.WEBUI_NAME
webui_url = request.app.state.config.WEBUI_URL
enable_user_webhooks = request.app.state.config.ENABLE_USER_WEBHOOKS
webui_url = await Config.get('webui.url')
enable_user_webhooks = await Config.get('ui.enable_user_webhooks')
users = await get_channel_users_with_access(channel, 'read', db=db)
@@ -1009,7 +1010,7 @@ async def model_response_handler(request, channel, message, user, db=None):
)
tool_ids = _resolve_model_tool_ids(request.app, model_id)
features = _resolve_model_features(request.app, model_id)
features = await _resolve_model_features(request.app, model_id)
filter_ids = _resolve_model_filter_ids(request.app, model_id)
# Build full form_data — same shape as frontend POST.
+8 -7
View File
@@ -12,6 +12,7 @@ from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.chats import (
AggregateChatStats,
ChatBody,
@@ -46,7 +47,7 @@ router = APIRouter()
async def require_chat_import_permission(request: Request, user, db: AsyncSession):
if user.role != 'admin' and not await has_permission(
user.id, 'chat.import', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'chat.import', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -412,7 +413,7 @@ async def export_chat_stats(
user=Depends(get_verified_user),
):
# Check if the user has permission to share/export chats
if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING):
if (user.role != 'admin') and (not await Config.get('ui.enable_community_sharing')):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -461,7 +462,7 @@ async def export_single_chat_stats(
Returns ChatStatsExport for the specified chat.
"""
# Check if the user has permission to share/export chats
if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING):
if (user.role != 'admin') and (not await Config.get('ui.enable_community_sharing')):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -508,7 +509,7 @@ async def delete_all_user_chats(
db: AsyncSession = Depends(get_async_session),
):
if user.role == 'user' and not await has_permission(
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS
user.id, 'chat.delete', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -1164,7 +1165,7 @@ async def delete_chat_by_id(
return result
else:
if not await has_permission(user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS):
if not await has_permission(user.id, 'chat.delete', await Config.get('user.permissions')):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -1384,7 +1385,7 @@ async def share_chat_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'chat.share', request.app.state.config.USER_PERMISSIONS
user.id, 'chat.share', await Config.get('user.permissions')
):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
@@ -1460,7 +1461,7 @@ async def update_shared_chat_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+74 -116
View File
@@ -7,7 +7,8 @@ from typing import Optional
import aiohttp
from fastapi import APIRouter, Depends, HTTPException, Request
from mcp.shared.auth import OAuthMetadata
from open_webui.config import BannerModel, async_save_config, get_config, save_config
from open_webui.config import BannerModel
from open_webui.models.config import Config
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.utils.auth import get_admin_user, get_verified_user
@@ -34,6 +35,44 @@ router = APIRouter()
log = logging.getLogger(__name__)
CONNECTIONS_CONFIG_KEYS = {
'ENABLE_DIRECT_CONNECTIONS': 'direct.enable',
'ENABLE_BASE_MODELS_CACHE': 'models.base_models_cache',
}
CODE_EXECUTION_CONFIG_KEYS = {
'ENABLE_CODE_EXECUTION': 'code_execution.enable',
'CODE_EXECUTION_ENGINE': 'code_execution.engine',
'CODE_EXECUTION_JUPYTER_URL': 'code_execution.jupyter.url',
'CODE_EXECUTION_JUPYTER_AUTH': 'code_execution.jupyter.auth',
'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': 'code_execution.jupyter.auth_token',
'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': 'code_execution.jupyter.auth_password',
'CODE_EXECUTION_JUPYTER_TIMEOUT': 'code_execution.jupyter.timeout',
'ENABLE_CODE_INTERPRETER': 'code_interpreter.enable',
'CODE_INTERPRETER_ENGINE': 'code_interpreter.engine',
'CODE_INTERPRETER_PROMPT_TEMPLATE': 'code_interpreter.prompt_template',
'CODE_INTERPRETER_JUPYTER_URL': 'code_interpreter.jupyter.url',
'CODE_INTERPRETER_JUPYTER_AUTH': 'code_interpreter.jupyter.auth',
'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': 'code_interpreter.jupyter.auth_token',
'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': 'code_interpreter.jupyter.auth_password',
'CODE_INTERPRETER_JUPYTER_TIMEOUT': 'code_interpreter.jupyter.timeout',
}
MODELS_CONFIG_KEYS = {
'DEFAULT_MODELS': 'ui.default_models',
'DEFAULT_PINNED_MODELS': 'ui.default_pinned_models',
'MODEL_ORDER_LIST': 'ui.model_order_list',
'DEFAULT_MODEL_METADATA': 'models.default_metadata',
'DEFAULT_MODEL_PARAMS': 'models.default_params',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
############################
# ImportConfig
@@ -48,9 +87,8 @@ class ImportConfigForm(BaseModel):
@router.post('/import', response_model=dict)
async def import_config(request: Request, form_data: ImportConfigForm, user=Depends(get_admin_user)):
await async_save_config(form_data.config)
request.app.state.config._sync_to_redis()
return get_config()
await Config.upsert(form_data.config)
return await Config.get_all()
############################
@@ -60,7 +98,12 @@ async def import_config(request: Request, form_data: ImportConfigForm, user=Depe
@router.get('/export', response_model=dict)
async def export_config(user=Depends(get_admin_user)):
return get_config()
return await Config.get_all()
@router.get('/namespace/{namespace}', response_model=dict)
async def get_config_namespace(namespace: str, user=Depends(get_admin_user)):
return await Config.get_namespace(namespace)
############################
@@ -75,10 +118,7 @@ class ConnectionsConfigForm(BaseModel):
@router.get('/connections', response_model=ConnectionsConfigForm)
async def get_connections_config(request: Request, user=Depends(get_admin_user)):
return {
'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
return await get_config_values(CONNECTIONS_CONFIG_KEYS)
@router.post('/connections', response_model=ConnectionsConfigForm)
@@ -87,13 +127,8 @@ async def set_connections_config(
form_data: ConnectionsConfigForm,
user=Depends(get_admin_user),
):
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
return {
'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
await Config.upsert(config_updates(form_data.model_dump(), CONNECTIONS_CONFIG_KEYS))
return await get_config_values(CONNECTIONS_CONFIG_KEYS)
class OAuthClientRegistrationForm(BaseModel):
@@ -167,9 +202,7 @@ class ToolServersConfigForm(BaseModel):
@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,
}
return {'TOOL_SERVER_CONNECTIONS': await Config.get('tool_server.connections')}
@router.post('/tool_servers', response_model=ToolServersConfigForm)
@@ -178,7 +211,8 @@ async def set_tool_servers_config(
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
existing_connections = await Config.get('tool_server.connections', []) or []
for connection in existing_connections:
server_type = connection.get('type', 'openapi')
auth_type = connection.get('auth_type', 'none')
@@ -193,13 +227,12 @@ async def set_tool_servers_config(
pass
# Set new tool server connections
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
]
connections = [connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS]
await Config.upsert({'tool_server.connections': connections})
await set_tool_servers(request)
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
for connection in connections:
server_type = connection.get('type', 'openapi')
if server_type == 'mcp':
server_id = connection.get('info', {}).get('id')
@@ -216,9 +249,7 @@ async def set_tool_servers_config(
log.debug(f'Failed to add OAuth client for MCP tool server: {e}')
continue
return {
'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
return {'TOOL_SERVER_CONNECTIONS': connections}
class TerminalServerConnection(BaseModel):
@@ -249,9 +280,7 @@ class TerminalServersConfigForm(BaseModel):
@router.get('/terminal_servers')
async def get_terminal_servers_config(request: Request, user=Depends(get_admin_user)):
return {
'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
}
return {'TERMINAL_SERVER_CONNECTIONS': await Config.get('terminal_server.connections')}
@router.post('/terminal_servers')
@@ -260,15 +289,12 @@ async def set_terminal_servers_config(
form_data: TerminalServersConfigForm,
user=Depends(get_admin_user),
):
request.app.state.config.TERMINAL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TERMINAL_SERVER_CONNECTIONS
]
connections = [connection.model_dump() for connection in form_data.TERMINAL_SERVER_CONNECTIONS]
await Config.upsert({'terminal_server.connections': connections})
await set_terminal_servers(request)
return {
'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
}
return {'TERMINAL_SERVER_CONNECTIONS': connections}
@router.post('/terminal_servers/verify')
@@ -518,67 +544,15 @@ class CodeInterpreterConfigForm(BaseModel):
@router.get('/code_execution', response_model=CodeInterpreterConfigForm)
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
return {
'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION,
'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,
'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER,
'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE,
'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
'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,
}
return await get_config_values(CODE_EXECUTION_CONFIG_KEYS)
@router.post('/code_execution', response_model=CodeInterpreterConfigForm)
async def set_code_execution_config(
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
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
request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = form_data.CODE_INTERPRETER_PROMPT_TEMPLATE
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
return {
'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION,
'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,
'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER,
'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE,
'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
'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,
}
await Config.upsert(config_updates(form_data.model_dump(), CODE_EXECUTION_CONFIG_KEYS))
return await get_config_values(CODE_EXECUTION_CONFIG_KEYS)
############################
@@ -595,35 +569,19 @@ class ModelsConfigForm(BaseModel):
@router.get('/models/defaults')
async def get_models_defaults(request: Request, user=Depends(get_verified_user)):
return {
'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA,
'DEFAULT_MODEL_METADATA': await Config.get('models.default_metadata'),
}
@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,
'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS,
'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST,
'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA,
'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS,
}
return await get_config_values(MODELS_CONFIG_KEYS)
@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.DEFAULT_PINNED_MODELS = form_data.DEFAULT_PINNED_MODELS
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
request.app.state.config.DEFAULT_MODEL_METADATA = form_data.DEFAULT_MODEL_METADATA
request.app.state.config.DEFAULT_MODEL_PARAMS = form_data.DEFAULT_MODEL_PARAMS
return {
'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS,
'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS,
'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST,
'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA,
'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS,
}
await Config.upsert(config_updates(form_data.model_dump(), MODELS_CONFIG_KEYS))
return await get_config_values(MODELS_CONFIG_KEYS)
class PromptSuggestion(BaseModel):
@@ -642,8 +600,8 @@ async def set_default_suggestions(
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data['suggestions']
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
await Config.upsert({'ui.prompt_suggestions': data['suggestions']})
return await Config.get('ui.prompt_suggestions')
############################
@@ -662,8 +620,8 @@ async def set_banners(
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data['banners']
return request.app.state.config.BANNERS
await Config.upsert({'ui.banners': data['banners']})
return await Config.get('ui.banners')
@router.get('/banners', response_model=list[BannerModel])
@@ -671,4 +629,4 @@ async def get_banners(
request: Request,
user=Depends(get_verified_user),
):
return request.app.state.config.BANNERS
return await Config.get('ui.banners')
+17 -11
View File
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.concurrency import run_in_threadpool
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.config import Config
from open_webui.models.feedbacks import (
FeedbackForm,
FeedbackIdResponse,
@@ -25,6 +26,16 @@ log = logging.getLogger(__name__)
router = APIRouter()
EVALUATION_CONFIG_KEYS = {
'ENABLE_EVALUATION_ARENA_MODELS': 'evaluation.arena.enable',
'EVALUATION_ARENA_MODELS': 'evaluation.arena.models',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
# Leaderboard Elo Rating Computation
# The judgment has already been rendered with grace;
@@ -255,10 +266,7 @@ async def get_model_history(
@router.get('/config')
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
'ENABLE_EVALUATION_ARENA_MODELS': request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS,
'EVALUATION_ARENA_MODELS': request.app.state.config.EVALUATION_ARENA_MODELS,
}
return await get_config_values(EVALUATION_CONFIG_KEYS)
############################
@@ -277,15 +285,13 @@ async def update_config(
form_data: UpdateConfigForm,
user=Depends(get_admin_user),
):
config = request.app.state.config
updates = {}
if form_data.ENABLE_EVALUATION_ARENA_MODELS is not None:
config.ENABLE_EVALUATION_ARENA_MODELS = form_data.ENABLE_EVALUATION_ARENA_MODELS
updates['evaluation.arena.enable'] = form_data.ENABLE_EVALUATION_ARENA_MODELS
if form_data.EVALUATION_ARENA_MODELS is not None:
config.EVALUATION_ARENA_MODELS = form_data.EVALUATION_ARENA_MODELS
return {
'ENABLE_EVALUATION_ARENA_MODELS': config.ENABLE_EVALUATION_ARENA_MODELS,
'EVALUATION_ARENA_MODELS': config.EVALUATION_ARENA_MODELS,
}
updates['evaluation.arena.models'] = form_data.EVALUATION_ARENA_MODELS
await Config.upsert(updates)
return await get_config_values(EVALUATION_CONFIG_KEYS)
@router.get('/feedbacks/models', response_model=list[str])
+7 -7
View File
@@ -26,6 +26,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_db_context, get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.channels import Channels
from open_webui.models.config import Config
from open_webui.models.chats import Chats
from open_webui.models.files import (
FileForm,
@@ -123,7 +124,7 @@ async def process_uploaded_file(
if _is_text_file(file_path):
content_type = 'text/plain'
stt_supported = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', [])
stt_supported = await Config.get('audio.stt.supported_content_types', [])
if content_type and strict_match_mime_type(stt_supported, content_type):
# Audio / STT-supported files → transcribe then index
@@ -144,7 +145,7 @@ async def process_uploaded_file(
elif (
content_type
and content_type.startswith(('image/', 'video/'))
and request.app.state.config.CONTENT_EXTRACTION_ENGINE != 'external'
and await Config.get('rag.content_extraction_engine') != 'external'
):
# Media files without an external extraction engine
if content_type.startswith('video/'):
@@ -288,12 +289,11 @@ async def upload_file_handler(
# Remove the leading dot from the file extension and lowercase it
file_extension = file_extension[1:].lower() if file_extension else ''
if process and request.app.state.config.ALLOWED_FILE_EXTENSIONS:
request.app.state.config.ALLOWED_FILE_EXTENSIONS = [
ext for ext in request.app.state.config.ALLOWED_FILE_EXTENSIONS if ext
]
allowed_file_extensions = await Config.get('rag.file.allowed_extensions')
if process and allowed_file_extensions:
allowed_file_extensions = [ext for ext in allowed_file_extensions if ext]
if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS:
if file_extension not in allowed_file_extensions:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(f'File type {file_extension} is not allowed'),
+6 -4
View File
@@ -11,6 +11,7 @@ from fastapi.responses import FileResponse, StreamingResponse
from open_webui.config import UPLOAD_DIR
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.config import Config
from open_webui.models.chats import Chats
from open_webui.models.folders import (
FolderForm,
@@ -42,7 +43,8 @@ from open_webui.utils.access_control.folders import has_folder_access as _has_fo
async def check_folders_permission(request: Request, user, db=None):
"""Verify the folders feature is enabled and the user has permission."""
if request.app.state.config.ENABLE_FOLDERS is False:
config = await Config.get_many('folders.enable', 'user.permissions')
if config.get('folders.enable') is False:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -50,7 +52,7 @@ async def check_folders_permission(request: Request, user, db=None):
if user.role != 'admin' and not await has_permission(
user.id,
'features.folders',
request.app.state.config.USER_PERMISSIONS,
config.get('user.permissions'),
db=db,
):
raise HTTPException(
@@ -411,7 +413,7 @@ async def update_folder_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id, user.role,
form_data.access_grants,
None,
@@ -522,7 +524,7 @@ async def delete_folder_by_id(
folder_ids = await Folders.get_folder_ids_by_id_and_user_id_in_subtree(id, folder_owner_id, db=db)
if await Chats.count_chats_by_folder_ids_and_user_id(folder_ids, folder_owner_id, db=db):
chat_delete_permission = await has_permission(
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'chat.delete', await Config.get('user.permissions'), db=db
)
if user.role != 'admin' and not chat_delete_permission:
raise HTTPException(
+167 -218
View File
@@ -9,6 +9,7 @@ import mimetypes
import re
import uuid
from pathlib import Path
from types import SimpleNamespace
from typing import Optional
from urllib.parse import quote, urlparse
@@ -24,6 +25,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS
from open_webui.internal.db import get_async_session
from open_webui.models.chats import Chats
from open_webui.models.config import Config
from open_webui.retrieval.web.utils import get_ssrf_safe_session, validate_url
from open_webui.routers.files import get_file_content_by_id, upload_file_handler
from open_webui.utils.access_control import has_permission
@@ -50,17 +52,68 @@ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
router = APIRouter()
IMAGE_CONFIG_KEYS = {
'ENABLE_IMAGE_GENERATION': 'image_generation.enable',
'ENABLE_IMAGE_PROMPT_GENERATION': 'image_generation.prompt.enable',
'IMAGE_GENERATION_ENGINE': 'image_generation.engine',
'IMAGE_GENERATION_MODEL': 'image_generation.model',
'IMAGE_SIZE': 'image_generation.size',
'IMAGE_STEPS': 'image_generation.steps',
'IMAGES_OPENAI_API_BASE_URL': 'image_generation.openai.api_base_url',
'IMAGES_OPENAI_API_KEY': 'image_generation.openai.api_key',
'IMAGES_OPENAI_API_VERSION': 'image_generation.openai.api_version',
'IMAGES_OPENAI_API_PARAMS': 'image_generation.openai.params',
'AUTOMATIC1111_BASE_URL': 'image_generation.automatic1111.base_url',
'AUTOMATIC1111_API_AUTH': 'image_generation.automatic1111.api_auth',
'AUTOMATIC1111_PARAMS': 'image_generation.automatic1111.api_params',
'COMFYUI_BASE_URL': 'image_generation.comfyui.base_url',
'COMFYUI_API_KEY': 'image_generation.comfyui.api_key',
'COMFYUI_WORKFLOW': 'image_generation.comfyui.workflow',
'COMFYUI_WORKFLOW_NODES': 'image_generation.comfyui.nodes',
'IMAGES_GEMINI_API_BASE_URL': 'image_generation.gemini.api_base_url',
'IMAGES_GEMINI_API_KEY': 'image_generation.gemini.api_key',
'IMAGES_GEMINI_ENDPOINT_METHOD': 'image_generation.gemini.endpoint_method',
'ENABLE_IMAGE_EDIT': 'images.edit.enable',
'IMAGE_EDIT_ENGINE': 'images.edit.engine',
'IMAGE_EDIT_MODEL': 'images.edit.model',
'IMAGE_EDIT_SIZE': 'images.edit.size',
'IMAGES_EDIT_OPENAI_API_BASE_URL': 'images.edit.openai.api_base_url',
'IMAGES_EDIT_OPENAI_API_KEY': 'images.edit.openai.api_key',
'IMAGES_EDIT_OPENAI_API_VERSION': 'images.edit.openai.api_version',
'IMAGES_EDIT_GEMINI_API_BASE_URL': 'images.edit.gemini.api_base_url',
'IMAGES_EDIT_GEMINI_API_KEY': 'images.edit.gemini.api_key',
'IMAGES_EDIT_COMFYUI_BASE_URL': 'images.edit.comfyui.base_url',
'IMAGES_EDIT_COMFYUI_API_KEY': 'images.edit.comfyui.api_key',
'IMAGES_EDIT_COMFYUI_WORKFLOW': 'images.edit.comfyui.workflow',
'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': 'images.edit.comfyui.nodes',
'USER_PERMISSIONS': 'user.permissions',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
async def get_image_config() -> SimpleNamespace:
return SimpleNamespace(**await get_config_values(IMAGE_CONFIG_KEYS))
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
async def set_image_model(request: Request, model: str):
log.info(f'Setting image model to {model}')
request.app.state.config.IMAGE_GENERATION_MODEL = model
if request.app.state.config.IMAGE_GENERATION_ENGINE in ['', 'automatic1111']:
api_auth = get_automatic1111_api_auth(request)
await Config.upsert({'image_generation.model': model})
image_config = await get_image_config()
if image_config.IMAGE_GENERATION_ENGINE in ['', 'automatic1111']:
api_auth = get_automatic1111_api_auth(image_config)
try:
session = await get_session()
async with session.get(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
headers={'authorization': api_auth},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
@@ -68,7 +121,7 @@ async def set_image_model(request: Request, model: str):
if model != options['sd_model_checkpoint']:
options['sd_model_checkpoint'] = model
async with session.post(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
json=options,
headers={'authorization': api_auth},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -77,35 +130,36 @@ async def set_image_model(request: Request, model: str):
except Exception as e:
log.debug(f'{e}')
return request.app.state.config.IMAGE_GENERATION_MODEL
return image_config.IMAGE_GENERATION_MODEL
async def get_image_model(request):
if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai':
image_config = await get_image_config()
if image_config.IMAGE_GENERATION_ENGINE == 'openai':
return (
request.app.state.config.IMAGE_GENERATION_MODEL
if request.app.state.config.IMAGE_GENERATION_MODEL
image_config.IMAGE_GENERATION_MODEL
if image_config.IMAGE_GENERATION_MODEL
else 'dall-e-2'
)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini':
elif image_config.IMAGE_GENERATION_ENGINE == 'gemini':
return (
request.app.state.config.IMAGE_GENERATION_MODEL
if request.app.state.config.IMAGE_GENERATION_MODEL
image_config.IMAGE_GENERATION_MODEL
if image_config.IMAGE_GENERATION_MODEL
else 'imagen-3.0-generate-002'
)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui':
elif image_config.IMAGE_GENERATION_ENGINE == 'comfyui':
return (
request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL else ''
image_config.IMAGE_GENERATION_MODEL if image_config.IMAGE_GENERATION_MODEL else ''
)
elif (
request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or request.app.state.config.IMAGE_GENERATION_ENGINE == ''
image_config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or image_config.IMAGE_GENERATION_ENGINE == ''
):
try:
session = await get_session()
async with session.get(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
headers={'authorization': get_automatic1111_api_auth(request)},
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
headers={'authorization': get_automatic1111_api_auth(image_config)},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
options = await r.json()
@@ -159,52 +213,11 @@ class ImagesConfig(BaseModel):
@router.get('/config', response_model=ImagesConfig)
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION,
'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE,
'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL,
'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE,
'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS,
'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY,
'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION,
'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS,
'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL,
'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH,
'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS,
'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL,
'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY,
'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW,
'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES,
'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY,
'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD,
'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT,
'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE,
'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL,
'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE,
'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL,
'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY,
'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
}
return await get_config_values(IMAGE_CONFIG_KEYS)
@router.post('/config/update')
async def update_config(request: Request, form_data: ImagesConfig, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.ENABLE_IMAGE_GENERATION
# Create Image
request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = form_data.ENABLE_IMAGE_PROMPT_GENERATION
request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.IMAGE_GENERATION_ENGINE
await set_image_model(request, form_data.IMAGE_GENERATION_MODEL)
if form_data.IMAGE_SIZE == 'auto' and not re.match(
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL
):
@@ -216,100 +229,31 @@ async def update_config(request: Request, form_data: ImagesConfig, user=Depends(
)
pattern = r'^\d+x\d+$'
if form_data.IMAGE_SIZE == 'auto' or form_data.IMAGE_SIZE == '' or re.match(pattern, form_data.IMAGE_SIZE):
request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
else:
if not (form_data.IMAGE_SIZE == 'auto' or form_data.IMAGE_SIZE == '' or re.match(pattern, form_data.IMAGE_SIZE)):
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 512x512).'),
)
if form_data.IMAGE_STEPS >= 0:
request.app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
else:
if form_data.IMAGE_STEPS < 0:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 50).'),
)
request.app.state.config.IMAGES_OPENAI_API_BASE_URL = form_data.IMAGES_OPENAI_API_BASE_URL
request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.IMAGES_OPENAI_API_KEY
request.app.state.config.IMAGES_OPENAI_API_VERSION = form_data.IMAGES_OPENAI_API_VERSION
request.app.state.config.IMAGES_OPENAI_API_PARAMS = form_data.IMAGES_OPENAI_API_PARAMS
request.app.state.config.AUTOMATIC1111_BASE_URL = form_data.AUTOMATIC1111_BASE_URL
request.app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
request.app.state.config.AUTOMATIC1111_PARAMS = form_data.AUTOMATIC1111_PARAMS
request.app.state.config.COMFYUI_BASE_URL = form_data.COMFYUI_BASE_URL.strip('/')
request.app.state.config.COMFYUI_API_KEY = form_data.COMFYUI_API_KEY
request.app.state.config.COMFYUI_WORKFLOW = form_data.COMFYUI_WORKFLOW
request.app.state.config.COMFYUI_WORKFLOW_NODES = form_data.COMFYUI_WORKFLOW_NODES
request.app.state.config.IMAGES_GEMINI_API_BASE_URL = form_data.IMAGES_GEMINI_API_BASE_URL
request.app.state.config.IMAGES_GEMINI_API_KEY = form_data.IMAGES_GEMINI_API_KEY
request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = form_data.IMAGES_GEMINI_ENDPOINT_METHOD
# Edit Image
request.app.state.config.ENABLE_IMAGE_EDIT = form_data.ENABLE_IMAGE_EDIT
request.app.state.config.IMAGE_EDIT_ENGINE = form_data.IMAGE_EDIT_ENGINE
request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL
request.app.state.config.IMAGE_EDIT_SIZE = form_data.IMAGE_EDIT_SIZE
request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = form_data.IMAGES_EDIT_OPENAI_API_BASE_URL
request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY = form_data.IMAGES_EDIT_OPENAI_API_KEY
request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = form_data.IMAGES_EDIT_OPENAI_API_VERSION
request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = form_data.IMAGES_EDIT_GEMINI_API_BASE_URL
request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY = form_data.IMAGES_EDIT_GEMINI_API_KEY
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip('/')
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = form_data.IMAGES_EDIT_COMFYUI_API_KEY
request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW
request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
return {
'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION,
'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE,
'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL,
'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE,
'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS,
'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY,
'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION,
'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS,
'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL,
'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH,
'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS,
'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL,
'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY,
'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW,
'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES,
'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY,
'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD,
'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT,
'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE,
'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL,
'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE,
'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL,
'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY,
'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
}
updates = config_updates(form_data.model_dump(), IMAGE_CONFIG_KEYS)
updates['image_generation.comfyui.base_url'] = form_data.COMFYUI_BASE_URL.strip('/')
updates['images.edit.comfyui.base_url'] = form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip('/')
await Config.upsert(updates)
await set_image_model(request, form_data.IMAGE_GENERATION_MODEL)
return await get_config_values(IMAGE_CONFIG_KEYS)
def get_automatic1111_api_auth(request: Request):
if request.app.state.config.AUTOMATIC1111_API_AUTH is None:
def get_automatic1111_api_auth(image_config):
if image_config.AUTOMATIC1111_API_AUTH is None:
return ''
else:
auth1111_byte_string = request.app.state.config.AUTOMATIC1111_API_AUTH.encode('utf-8')
auth1111_byte_string = image_config.AUTOMATIC1111_API_AUTH.encode('utf-8')
auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode('utf-8')
return f'Basic {auth1111_base64_encoded_string}'
@@ -317,26 +261,27 @@ def get_automatic1111_api_auth(request: Request):
@router.get('/config/url/verify')
async def verify_url(request: Request, user=Depends(get_admin_user)):
if request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111':
image_config = await get_image_config()
if image_config.IMAGE_GENERATION_ENGINE == 'automatic1111':
try:
session = await get_session()
async with session.get(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
headers={'authorization': get_automatic1111_api_auth(request)},
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options',
headers={'authorization': get_automatic1111_api_auth(image_config)},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
return True
except Exception:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui':
elif image_config.IMAGE_GENERATION_ENGINE == 'comfyui':
headers = None
if request.app.state.config.COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'}
if image_config.COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {image_config.COMFYUI_API_KEY}'}
try:
session = await get_session()
async with session.get(
url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info',
url=f'{image_config.COMFYUI_BASE_URL}/object_info',
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
@@ -350,33 +295,34 @@ async def verify_url(request: Request, user=Depends(get_admin_user)):
@router.get('/models')
async def get_models(request: Request, user=Depends(get_verified_user)):
image_config = await get_image_config()
try:
if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai':
if image_config.IMAGE_GENERATION_ENGINE == 'openai':
return [
{'id': 'dall-e-2', 'name': 'DALL·E 2'},
{'id': 'dall-e-3', 'name': 'DALL·E 3'},
{'id': 'gpt-image-1', 'name': 'GPT-IMAGE 1'},
{'id': 'gpt-image-1.5', 'name': 'GPT-IMAGE 1.5'},
]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini':
elif image_config.IMAGE_GENERATION_ENGINE == 'gemini':
return [
{'id': 'imagen-3.0-generate-002', 'name': 'imagen-3.0 generate-002'},
]
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui':
elif image_config.IMAGE_GENERATION_ENGINE == 'comfyui':
# TODO - get models from comfyui
headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'}
headers = {'Authorization': f'Bearer {image_config.COMFYUI_API_KEY}'}
session = await get_session()
async with session.get(
url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info',
url=f'{image_config.COMFYUI_BASE_URL}/object_info',
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
info = await r.json()
workflow = json.loads(request.app.state.config.COMFYUI_WORKFLOW)
workflow = json.loads(image_config.COMFYUI_WORKFLOW)
model_node_id = None
for node in request.app.state.config.COMFYUI_WORKFLOW_NODES:
for node in image_config.COMFYUI_WORKFLOW_NODES:
if node['type'] == 'model':
if node['node_ids']:
model_node_id = node['node_ids'][0]
@@ -406,13 +352,13 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
)
)
elif (
request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or request.app.state.config.IMAGE_GENERATION_ENGINE == ''
image_config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or image_config.IMAGE_GENERATION_ENGINE == ''
):
session = await get_session()
async with session.get(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models',
headers={'authorization': get_automatic1111_api_auth(request)},
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models',
headers={'authorization': get_automatic1111_api_auth(image_config)},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
models = await r.json()
@@ -540,14 +486,15 @@ async def upload_image(request, image_data, content_type, metadata, user, db=Non
@router.post('/generations')
async def generate_images(request: Request, form_data: CreateImageForm, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_IMAGE_GENERATION:
image_config = await get_image_config()
if not image_config.ENABLE_IMAGE_GENERATION:
raise HTTPException(
status_code=403,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if user.role != 'admin' and not await has_permission(
user.id, 'features.image_generation', request.app.state.config.USER_PERMISSIONS
user.id, 'features.image_generation', image_config.USER_PERMISSIONS
):
raise HTTPException(
status_code=403,
@@ -563,13 +510,14 @@ async def image_generations(
metadata: dict | None = None,
user=None,
):
image_config = await get_image_config()
# if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default
# This is only relevant when the user has set IMAGE_SIZE to 'auto' with an
# image model other than gpt-image-1, which is warned about on settings save
size = '512x512'
if request.app.state.config.IMAGE_SIZE and 'x' in request.app.state.config.IMAGE_SIZE:
size = request.app.state.config.IMAGE_SIZE
if image_config.IMAGE_SIZE and 'x' in image_config.IMAGE_SIZE:
size = image_config.IMAGE_SIZE
if form_data.size and 'x' in form_data.size:
size = form_data.size
@@ -581,40 +529,40 @@ async def image_generations(
model = await get_image_model(request)
try:
if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai':
if image_config.IMAGE_GENERATION_ENGINE == 'openai':
headers = {
'Authorization': f'Bearer {request.app.state.config.IMAGES_OPENAI_API_KEY}',
'Authorization': f'Bearer {image_config.IMAGES_OPENAI_API_KEY}',
'Content-Type': 'application/json',
}
if ENABLE_FORWARD_USER_INFO_HEADERS:
headers = include_user_info_headers(headers, user)
url = f'{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations'
if request.app.state.config.IMAGES_OPENAI_API_VERSION:
url = f'{url}?api-version={request.app.state.config.IMAGES_OPENAI_API_VERSION}'
url = f'{image_config.IMAGES_OPENAI_API_BASE_URL}/images/generations'
if image_config.IMAGES_OPENAI_API_VERSION:
url = f'{url}?api-version={image_config.IMAGES_OPENAI_API_VERSION}'
data = {
'model': model,
'prompt': form_data.prompt,
'n': form_data.n,
**(
{'size': form_data.size or request.app.state.config.IMAGE_SIZE}
if (form_data.size or request.app.state.config.IMAGE_SIZE)
{'size': form_data.size or image_config.IMAGE_SIZE}
if (form_data.size or image_config.IMAGE_SIZE)
else {}
),
**(
{}
if re.match(
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN,
request.app.state.config.IMAGE_GENERATION_MODEL,
image_config.IMAGE_GENERATION_MODEL,
)
else {'response_format': 'b64_json'}
),
**(
{}
if not request.app.state.config.IMAGES_OPENAI_API_PARAMS
else request.app.state.config.IMAGES_OPENAI_API_PARAMS
if not image_config.IMAGES_OPENAI_API_PARAMS
else image_config.IMAGES_OPENAI_API_PARAMS
),
}
@@ -643,17 +591,17 @@ async def image_generations(
images.append({'url': url})
return images
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini':
elif image_config.IMAGE_GENERATION_ENGINE == 'gemini':
headers = {
'Content-Type': 'application/json',
'x-goog-api-key': request.app.state.config.IMAGES_GEMINI_API_KEY,
'x-goog-api-key': image_config.IMAGES_GEMINI_API_KEY,
}
data = {}
if (
request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == ''
or request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'predict'
image_config.IMAGES_GEMINI_ENDPOINT_METHOD == ''
or image_config.IMAGES_GEMINI_ENDPOINT_METHOD == 'predict'
):
model = f'{model}:predict'
data = {
@@ -664,13 +612,13 @@ async def image_generations(
},
}
elif request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'generateContent':
elif image_config.IMAGES_GEMINI_ENDPOINT_METHOD == 'generateContent':
model = f'{model}:generateContent'
data = {'contents': [{'parts': [{'text': form_data.prompt}]}]}
session = await get_session()
async with session.post(
url=f'{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}',
url=f'{image_config.IMAGES_GEMINI_API_BASE_URL}/models/{model}',
json=data,
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -701,7 +649,7 @@ async def image_generations(
return images
elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui':
elif image_config.IMAGE_GENERATION_ENGINE == 'comfyui':
data = {
'prompt': form_data.prompt,
'width': width,
@@ -709,8 +657,8 @@ async def image_generations(
'n': form_data.n,
}
if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None:
data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS
if image_config.IMAGE_STEPS is not None or form_data.steps is not None:
data['steps'] = form_data.steps if form_data.steps is not None else image_config.IMAGE_STEPS
if form_data.negative_prompt is not None:
data['negative_prompt'] = form_data.negative_prompt
@@ -719,8 +667,8 @@ async def image_generations(
**{
'workflow': ComfyUIWorkflow(
**{
'workflow': request.app.state.config.COMFYUI_WORKFLOW,
'nodes': request.app.state.config.COMFYUI_WORKFLOW_NODES,
'workflow': image_config.COMFYUI_WORKFLOW,
'nodes': image_config.COMFYUI_WORKFLOW_NODES,
}
),
**data,
@@ -730,8 +678,8 @@ async def image_generations(
model,
form_data,
str(uuid.uuid4()),
request.app.state.config.COMFYUI_BASE_URL,
request.app.state.config.COMFYUI_API_KEY,
image_config.COMFYUI_BASE_URL,
image_config.COMFYUI_API_KEY,
)
log.debug(f'res: {res}')
@@ -739,13 +687,13 @@ async def image_generations(
for image in res['data']:
headers = None
if request.app.state.config.COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'}
if image_config.COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {image_config.COMFYUI_API_KEY}'}
image_data, content_type = await get_image_data(
image['url'],
headers,
trusted_base_url=request.app.state.config.COMFYUI_BASE_URL,
trusted_base_url=image_config.COMFYUI_BASE_URL,
)
_, url = await upload_image(
request,
@@ -757,8 +705,8 @@ async def image_generations(
images.append({'url': url})
return images
elif (
request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or request.app.state.config.IMAGE_GENERATION_ENGINE == ''
image_config.IMAGE_GENERATION_ENGINE == 'automatic1111'
or image_config.IMAGE_GENERATION_ENGINE == ''
):
if form_data.model:
await set_image_model(request, form_data.model)
@@ -770,20 +718,20 @@ async def image_generations(
'height': height,
}
if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None:
data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS
if image_config.IMAGE_STEPS is not None or form_data.steps is not None:
data['steps'] = form_data.steps if form_data.steps is not None else image_config.IMAGE_STEPS
if form_data.negative_prompt is not None:
data['negative_prompt'] = form_data.negative_prompt
if request.app.state.config.AUTOMATIC1111_PARAMS:
data = {**data, **request.app.state.config.AUTOMATIC1111_PARAMS}
if image_config.AUTOMATIC1111_PARAMS:
data = {**data, **image_config.AUTOMATIC1111_PARAMS}
session = await get_session()
async with session.post(
url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img',
url=f'{image_config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img',
json=data,
headers={'authorization': get_automatic1111_api_auth(request)},
headers={'authorization': get_automatic1111_api_auth(image_config)},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
res = await r.json(content_type=None)
@@ -826,17 +774,18 @@ async def image_edits(
metadata: dict | None = None,
user=Depends(get_verified_user),
):
image_config = await get_image_config()
size = None
width, height = None, None
metadata = metadata or {}
if (request.app.state.config.IMAGE_EDIT_SIZE and 'x' in request.app.state.config.IMAGE_EDIT_SIZE) or (
if (image_config.IMAGE_EDIT_SIZE and 'x' in image_config.IMAGE_EDIT_SIZE) or (
form_data.size and 'x' in form_data.size
):
size = form_data.size if form_data.size else request.app.state.config.IMAGE_EDIT_SIZE
size = form_data.size if form_data.size else image_config.IMAGE_EDIT_SIZE
width, height = tuple(map(int, size.split('x')))
model = request.app.state.config.IMAGE_EDIT_MODEL if form_data.model is None else form_data.model
model = image_config.IMAGE_EDIT_MODEL if form_data.model is None else form_data.model
try:
@@ -905,9 +854,9 @@ async def image_edits(
)
try:
if request.app.state.config.IMAGE_EDIT_ENGINE == 'openai':
if image_config.IMAGE_EDIT_ENGINE == 'openai':
headers = {
'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY}',
'Authorization': f'Bearer {image_config.IMAGES_EDIT_OPENAI_API_KEY}',
}
if ENABLE_FORWARD_USER_INFO_HEADERS:
@@ -923,7 +872,7 @@ async def image_edits(
{}
if re.match(
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN,
request.app.state.config.IMAGE_EDIT_MODEL,
image_config.IMAGE_EDIT_MODEL,
)
else {'response_format': 'b64_json'}
),
@@ -937,8 +886,8 @@ async def image_edits(
files.append(get_image_file_item(img, 'image[]'))
url_search_params = ''
if request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION:
url_search_params += f'?api-version={request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION}'
if image_config.IMAGES_EDIT_OPENAI_API_VERSION:
url_search_params += f'?api-version={image_config.IMAGES_EDIT_OPENAI_API_VERSION}'
# Build multipart form data for aiohttp
form = aiohttp.FormData()
@@ -957,7 +906,7 @@ async def image_edits(
session = await get_session()
async with session.post(
url=f'{request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}',
url=f'{image_config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}',
headers=headers,
data=form,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -979,10 +928,10 @@ async def image_edits(
images.append({'url': url})
return images
elif request.app.state.config.IMAGE_EDIT_ENGINE == 'gemini':
elif image_config.IMAGE_EDIT_ENGINE == 'gemini':
headers = {
'Content-Type': 'application/json',
'x-goog-api-key': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
'x-goog-api-key': image_config.IMAGES_EDIT_GEMINI_API_KEY,
}
model = f'{model}:generateContent'
@@ -1012,7 +961,7 @@ async def image_edits(
session = await get_session()
async with session.post(
url=f'{request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}',
url=f'{image_config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}',
json=data,
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -1036,7 +985,7 @@ async def image_edits(
return images
elif request.app.state.config.IMAGE_EDIT_ENGINE == 'comfyui':
elif image_config.IMAGE_EDIT_ENGINE == 'comfyui':
try:
files = []
if isinstance(form_data.image, str):
@@ -1050,8 +999,8 @@ async def image_edits(
for file_item in files:
res = await comfyui_upload_image(
file_item,
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
image_config.IMAGES_EDIT_COMFYUI_BASE_URL,
image_config.IMAGES_EDIT_COMFYUI_API_KEY,
)
comfyui_images.append(res.get('name', file_item[1][0]))
except Exception as e:
@@ -1070,8 +1019,8 @@ async def image_edits(
**{
'workflow': ComfyUIWorkflow(
**{
'workflow': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
'nodes': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
'workflow': image_config.IMAGES_EDIT_COMFYUI_WORKFLOW,
'nodes': image_config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
}
),
**data,
@@ -1081,8 +1030,8 @@ async def image_edits(
model,
form_data,
str(uuid.uuid4()),
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
image_config.IMAGES_EDIT_COMFYUI_BASE_URL,
image_config.IMAGES_EDIT_COMFYUI_API_KEY,
)
log.debug(f'res: {res}')
@@ -1101,13 +1050,13 @@ async def image_edits(
for image_url in image_urls:
headers = None
if request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}'}
if image_config.IMAGES_EDIT_COMFYUI_API_KEY:
headers = {'Authorization': f'Bearer {image_config.IMAGES_EDIT_COMFYUI_API_KEY}'}
image_data, content_type = await get_image_data(
image_url,
headers,
trusted_base_url=request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
trusted_base_url=image_config.IMAGES_EDIT_COMFYUI_BASE_URL,
)
_, url = await upload_image(
request,
+5 -4
View File
@@ -13,6 +13,7 @@ from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.files import FileMetadataResponse, FileModel, FileModelResponse, Files
from open_webui.models.groups import Groups
from open_webui.models.knowledge import (
@@ -257,7 +258,7 @@ async def create_new_knowledge(
# This prevents holding a connection during embed_knowledge_base_metadata()
# which makes external embedding API calls (1-5+ seconds).
if user.role != 'admin' and not await has_permission(
user.id, 'workspace.knowledge', request.app.state.config.USER_PERMISSIONS
user.id, 'workspace.knowledge', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -265,7 +266,7 @@ async def create_new_knowledge(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -469,7 +470,7 @@ async def update_knowledge_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -537,7 +538,7 @@ async def update_knowledge_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+26 -78
View File
@@ -7,6 +7,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.config import Config
from open_webui.models.memories import Memories, MemoryModel
from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT
from open_webui.config import RAG_EMBEDDING_QUERY_PREFIX
@@ -20,6 +21,23 @@ log = logging.getLogger(__name__)
router = APIRouter()
async def check_memories_permission(user):
config = await Config.get_many('memories.enable', 'user.permissions')
if not config.get('memories.enable'):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(
user.id, 'features.memories', config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# GetMemories
# Let what is remembered here spare someone the cost
@@ -33,17 +51,7 @@ async def get_memories(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
return await Memories.get_memories_by_user_id(user.id, db=db)
@@ -73,17 +81,7 @@ async def add_memory(
own short-lived sessions so a connection is not held during the external
embedding API call (``EMBEDDING_FUNCTION``), which can take 1-5+ seconds.
"""
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
memory = await Memories.insert_new_memory(user.id, form_data.content)
@@ -124,17 +122,7 @@ async def query_memory(
# Database operations (get_memories_by_user_id) manage their own short-lived sessions.
# This prevents holding a connection during EMBEDDING_FUNCTION()
# which makes external embedding API calls (1-5+ seconds).
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
memories = await Memories.get_memories_by_user_id(user.id)
if not memories:
@@ -154,7 +142,7 @@ async def query_memory(
# same RELEVANCE_THRESHOLD used by RAG ensures only genuinely matching
# memories are surfaced (distances are normalised to 0→1, higher is
# better).
relevance_threshold = getattr(request.app.state.config, 'RELEVANCE_THRESHOLD', 0.0)
relevance_threshold = await Config.get('rag.relevance_threshold', 0.0)
if results and relevance_threshold > 0.0 and results.distances and results.distances[0]:
from open_webui.retrieval.vector.main import SearchResult
@@ -199,17 +187,7 @@ async def reset_memory_from_vector_db(
calls simultaneously. With a session held, this could block a connection
for MINUTES, completely exhausting the connection pool.
"""
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
await ASYNC_VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}')
@@ -250,17 +228,7 @@ async def delete_memory_by_user_id(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
result = await Memories.delete_memories_by_user_id(user.id, db=db)
@@ -290,17 +258,7 @@ async def update_memory_by_id(
# Database operations (update_memory_by_id_and_user_id) manage their own
# short-lived sessions. This prevents holding a connection during
# EMBEDDING_FUNCTION() which makes external API calls (1-5+ seconds).
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
memory = await Memories.update_memory_by_id_and_user_id(memory_id, user.id, form_data.content)
if memory is None:
@@ -339,17 +297,7 @@ async def delete_memory_by_id(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if not request.app.state.config.ENABLE_MEMORIES:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin' and not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await check_memories_permission(user)
result = await Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db)
+9 -8
View File
@@ -23,6 +23,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.models import (
ModelAccessListResponse,
@@ -230,7 +231,7 @@ async def create_new_model(
):
"""Create a new workspace model entry."""
if user.role != 'admin' and not await has_permission(
user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'workspace.models', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -258,7 +259,7 @@ async def create_new_model(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -289,7 +290,7 @@ async def export_models(
if user.role != 'admin' and not await has_permission(
user.id,
'workspace.models_export',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
):
raise HTTPException(
@@ -322,7 +323,7 @@ async def import_models(
if user.role != 'admin' and not await has_permission(
user.id,
'workspace.models_import',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
):
raise HTTPException(
@@ -403,7 +404,7 @@ async def import_models(
# metadata-only imports.
if 'access_grants' in model_data:
updated_model.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
updated_model.access_grants,
@@ -416,7 +417,7 @@ async def import_models(
model_data['params'] = model_data.get('params', {})
new_model = ModelForm(**model_data)
new_model.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
new_model.access_grants,
@@ -677,7 +678,7 @@ async def update_model_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -749,7 +750,7 @@ async def update_model_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+13 -12
View File
@@ -11,6 +11,7 @@ from open_webui.config import (
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.notes import (
NoteForm,
@@ -66,7 +67,7 @@ async def get_notes(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -114,7 +115,7 @@ async def get_pinned_notes(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -155,7 +156,7 @@ async def search_notes(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -208,7 +209,7 @@ async def create_new_note(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -216,7 +217,7 @@ async def create_new_note(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -249,7 +250,7 @@ async def get_note_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -308,7 +309,7 @@ async def update_note_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -332,7 +333,7 @@ async def update_note_by_id(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -375,7 +376,7 @@ async def update_note_access_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -399,7 +400,7 @@ async def update_note_access_by_id(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -427,7 +428,7 @@ async def pin_note_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -469,7 +470,7 @@ async def delete_note_by_id(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.notes', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
+114 -92
View File
@@ -31,6 +31,7 @@ from open_webui.env import (
)
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.users import UserModel
@@ -181,6 +182,32 @@ def get_api_key(idx, url, configs):
router = APIRouter()
OLLAMA_CONFIG_KEYS = {
'ENABLE_OLLAMA_API': 'ollama.enable',
'OLLAMA_BASE_URLS': 'ollama.base_urls',
'OLLAMA_API_CONFIGS': 'ollama.api_configs',
}
async def get_ollama_config_values() -> dict:
values = await Config.get_many(*OLLAMA_CONFIG_KEYS.values())
return {field: values[storage_key] for field, storage_key in OLLAMA_CONFIG_KEYS.items() if storage_key in values}
async def get_ollama_runtime_config() -> tuple[bool, list[str], dict]:
values = await Config.get_many('ollama.enable', 'ollama.base_urls', 'ollama.api_configs')
return (
values.get('ollama.enable'),
values.get('ollama.base_urls') or [],
values.get('ollama.api_configs') or {},
)
async def get_ollama_connection(idx: int) -> tuple[str, dict, str | None]:
_, base_urls, api_configs = await get_ollama_runtime_config()
url = base_urls[idx]
return url, resolve_api_config(api_configs, idx, url), get_api_key(idx, url, api_configs)
@router.head('/')
@router.get('/')
@@ -236,11 +263,7 @@ async def get_config(
user=Depends(get_admin_user),
) -> dict:
"""Return the current Ollama connection configuration."""
return {
'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API,
'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS,
'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS,
}
return await get_ollama_config_values()
class OllamaConfigForm(BaseModel):
@@ -258,20 +281,20 @@ async def update_config(
user=Depends(get_admin_user),
) -> dict:
"""Persist updated Ollama connection settings."""
request.app.state.config.ENABLE_OLLAMA_API = form_data.ENABLE_OLLAMA_API
request.app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS
request.app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS
# Prune stale config entries that no longer map to a URL index
valid_keys = {str(i) for i in range(len(request.app.state.config.OLLAMA_BASE_URLS))}
request.app.state.config.OLLAMA_API_CONFIGS = {
k: v for k, v in request.app.state.config.OLLAMA_API_CONFIGS.items() if k in valid_keys
}
valid_keys = {str(i) for i in range(len(form_data.OLLAMA_BASE_URLS))}
api_configs = {k: v for k, v in form_data.OLLAMA_API_CONFIGS.items() if k in valid_keys}
await Config.upsert(
{
'ollama.enable': form_data.ENABLE_OLLAMA_API,
'ollama.base_urls': form_data.OLLAMA_BASE_URLS,
'ollama.api_configs': api_configs,
}
)
return {
'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API,
'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS,
'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS,
'ENABLE_OLLAMA_API': form_data.ENABLE_OLLAMA_API,
'OLLAMA_BASE_URLS': form_data.OLLAMA_BASE_URLS,
'OLLAMA_API_CONFIGS': api_configs,
}
@@ -293,9 +316,8 @@ def merge_models_lists(model_lists) -> list[dict]:
return list(merged.values())
def _resolve_api_config(request: Request, idx: int, url: str) -> dict:
def resolve_api_config(api_configs: dict, idx: int, url: str) -> dict:
"""Look up the API config for a backend by numeric index, falling back to URL key (legacy)."""
api_configs = request.app.state.config.OLLAMA_API_CONFIGS
return api_configs.get(str(idx), api_configs.get(url, {}))
@@ -307,15 +329,15 @@ async def get_all_models(request: Request, user: UserModel | None = None):
"""Aggregate model tags from every enabled Ollama backend."""
log.info('get_all_models()')
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
models_dict: dict = {'models': []}
request.app.state.OLLAMA_MODELS = {}
return models_dict
# Fan-out tag requests to every backend
tasks = []
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
api_config = _resolve_api_config(request, idx, url)
for idx, url in enumerate(await Config.get('ollama.base_urls', [])):
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), idx, url)
if not api_config:
tasks.append(send_get_request(f'{url}/api/tags', user=user))
elif api_config.get('enable', True):
@@ -329,8 +351,8 @@ async def get_all_models(request: Request, user: UserModel | None = None):
for idx, response in enumerate(responses):
if not response:
continue
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
api_config = _resolve_api_config(request, idx, url)
url = (await Config.get('ollama.base_urls', []))[idx]
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), idx, url)
connection_type = api_config.get('connection_type', 'local')
prefix_id = api_config.get('prefix_id')
@@ -394,14 +416,14 @@ async def get_ollama_tags(
user=Depends(get_verified_user),
):
"""List Ollama model tags, optionally from a specific backend."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
if url_idx is None:
result = await get_all_models(request, user=user)
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
url = (await Config.get('ollama.base_urls', []))[url_idx]
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
result = await send_request(f'{url}/api/tags', 'GET', key=key, user=user)
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
@@ -416,12 +438,12 @@ async def get_ollama_loaded_models(
user=Depends(get_admin_user),
) -> dict:
"""List models currently loaded in Ollama memory across all backends."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
return {'models': []}
tasks = []
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
api_config = _resolve_api_config(request, idx, url)
for idx, url in enumerate(await Config.get('ollama.base_urls', [])):
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), idx, url)
if not api_config:
tasks.append(send_get_request(f'{url}/api/ps', user=user))
elif api_config.get('enable', True):
@@ -434,7 +456,7 @@ async def get_ollama_loaded_models(
for idx, response in enumerate(responses):
if not response:
continue
api_config = _resolve_api_config(request.app.state.config, idx, request.app.state.config.OLLAMA_BASE_URLS[idx])
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), idx, (await Config.get('ollama.base_urls', []))[idx])
prefix_id = api_config.get('prefix_id')
if prefix_id:
for m in response.get('models', []):
@@ -450,19 +472,19 @@ async def get_ollama_versions(
url_idx: int | None = None,
):
"""Return the lowest Ollama version across all configured backends."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
return {'version': False}
if url_idx is not None:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
return await send_request(f'{url}/api/version', 'GET')
# Fan-out to every enabled backend
tasks = []
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
for idx, url in enumerate(await Config.get('ollama.base_urls', [])):
api_config = (await Config.get('ollama.api_configs', {})).get(
str(idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
(await Config.get('ollama.api_configs', {})).get(url, {}),
)
if api_config.get('enable', True):
tasks.append(send_get_request(f'{url}/api/version', api_config.get('key')))
@@ -511,11 +533,11 @@ async def unload_model(
results = []
errors = []
for idx in url_indices:
url = request.app.state.config.OLLAMA_BASE_URLS[idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
str(idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {})
url = (await Config.get('ollama.base_urls', []))[idx]
api_config = (await Config.get('ollama.api_configs', {})).get(
str(idx), (await Config.get('ollama.api_configs', {})).get(url, {})
)
key = get_api_key(idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
key = get_api_key(idx, url, (await Config.get('ollama.api_configs', {})))
prefix_id = api_config.get('prefix_id', None)
if prefix_id and model.startswith(f'{prefix_id}.'):
@@ -552,20 +574,20 @@ async def pull_model(
url_idx: int = 0,
user=Depends(get_admin_user),
):
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
form_data = form_data.model_dump(exclude_none=True)
form_data['model'] = form_data.get('model', form_data.get('name'))
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
log.info(f'url: {url}')
# Admins may pull from any registry
return await send_request(
f'{url}/api/pull',
payload=json.dumps({**form_data, 'insecure': True}),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=True,
)
@@ -588,7 +610,7 @@ async def push_model(
user=Depends(get_admin_user),
):
"""Push a local model to a remote registry."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
if url_idx is None:
@@ -598,13 +620,13 @@ async def push_model(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model))
url_idx = models[form_data.model]['urls'][0]
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
log.debug(f'url: {url}')
return await send_request(
f'{url}/api/push',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=True,
)
@@ -627,16 +649,16 @@ async def create_model(
url_idx: int = 0,
user=Depends(get_admin_user),
):
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
log.debug(f'form_data: {form_data}')
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
return await send_request(
f'{url}/api/create',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=True,
)
@@ -658,7 +680,7 @@ async def copy_model(
user=Depends(get_admin_user),
):
"""Duplicate an existing model under a new name."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
if url_idx is None:
@@ -668,8 +690,8 @@ async def copy_model(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source))
url_idx = models[form_data.source]['urls'][0]
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
url = (await Config.get('ollama.base_urls', []))[url_idx]
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
await send_request(
f'{url}/api/copy',
@@ -689,7 +711,7 @@ async def delete_model(
user=Depends(get_admin_user),
):
"""Remove a model from an Ollama backend."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
payload = form_data.model_dump(exclude_none=True)
@@ -703,8 +725,8 @@ async def delete_model(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model))
url_idx = models[model]['urls'][0]
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
url = (await Config.get('ollama.base_urls', []))[url_idx]
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
await send_request(
f'{url}/api/delete',
@@ -723,7 +745,7 @@ async def show_model_info(
user=Depends(get_verified_user),
):
"""Retrieve model metadata from the Ollama backend."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
payload = form_data.model_dump(exclude_none=True)
@@ -739,8 +761,8 @@ async def show_model_info(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model))
url_idx = random.choice(models[model]['urls'])
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
url = (await Config.get('ollama.base_urls', []))[url_idx]
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
return await send_request(
f'{url}/api/show',
@@ -770,7 +792,7 @@ async def embed(
user=Depends(get_verified_user),
):
"""Generate embeddings via the Ollama /api/embed endpoint."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
log.info(f'generate_ollama_batch_embeddings {form_data}')
@@ -787,12 +809,12 @@ async def embed(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model))
url_idx = random.choice(models[model]['urls'])
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
url = (await Config.get('ollama.base_urls', []))[url_idx]
api_config = (await Config.get('ollama.api_configs', {})).get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
(await Config.get('ollama.api_configs', {})).get(url, {}),
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
prefix_id = api_config.get('prefix_id')
if prefix_id:
@@ -824,7 +846,7 @@ async def embeddings(
user=Depends(get_verified_user),
):
"""Generate embeddings via the legacy Ollama /api/embeddings endpoint."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
log.info(f'generate_ollama_embeddings {form_data}')
@@ -841,12 +863,12 @@ async def embeddings(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model))
url_idx = random.choice(models[model]['urls'])
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
url = (await Config.get('ollama.base_urls', []))[url_idx]
api_config = (await Config.get('ollama.api_configs', {})).get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
(await Config.get('ollama.api_configs', {})).get(url, {}),
)
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
key = get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {})))
prefix_id = api_config.get('prefix_id')
if prefix_id:
@@ -886,7 +908,7 @@ async def generate_completion(
user=Depends(get_verified_user),
):
"""Run text completion via Ollama /api/generate."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL)
@@ -900,10 +922,10 @@ async def generate_completion(
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model))
url_idx = random.choice(models[model]['urls'])
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
url = (await Config.get('ollama.base_urls', []))[url_idx]
api_config = (await Config.get('ollama.api_configs', {})).get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
(await Config.get('ollama.api_configs', {})).get(url, {}),
)
prefix_id = api_config.get('prefix_id')
@@ -913,7 +935,7 @@ async def generate_completion(
return await send_request(
f'{url}/api/generate',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=True,
)
@@ -973,7 +995,7 @@ async def get_ollama_url(request: Request, model: str, url_idx: int | None = Non
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
)
url_idx = random.choice(models[model].get('urls', []))
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
return url, url_idx
@@ -986,7 +1008,7 @@ async def generate_chat_completion(
user=Depends(get_verified_user), # noqa: B008
):
"""Forward a chat completion request to an Ollama backend."""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
# NOTE: We intentionally do NOT use Depends(get_async_session) here.
@@ -1035,7 +1057,7 @@ async def generate_chat_completion(
await check_model_access(user, None, bypass_filter)
url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user)
api_config = _resolve_api_config(request, url_idx, url)
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), url_idx, url)
prefix_id = api_config.get('prefix_id')
if prefix_id:
@@ -1044,7 +1066,7 @@ async def generate_chat_completion(
return await send_request(
f'{url}/api/chat',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=form_data.stream,
content_type='application/x-ndjson',
@@ -1121,7 +1143,7 @@ async def generate_openai_completion(
await check_model_access(user, None)
url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user)
api_config = _resolve_api_config(request, url_idx, url)
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), url_idx, url)
prefix_id = api_config.get('prefix_id')
if prefix_id:
@@ -1130,7 +1152,7 @@ async def generate_openai_completion(
return await send_request(
f'{url}/v1/completions',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=payload.get('stream', False),
metadata=metadata,
@@ -1178,7 +1200,7 @@ async def generate_openai_chat_completion(
await check_model_access(user, None)
url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user)
api_config = _resolve_api_config(request, url_idx, url)
api_config = resolve_api_config((await Config.get('ollama.api_configs', {})), url_idx, url)
prefix_id = api_config.get('prefix_id')
if prefix_id:
@@ -1187,7 +1209,7 @@ async def generate_openai_chat_completion(
return await send_request(
f'{url}/v1/chat/completions',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=payload.get('stream', False),
metadata=metadata,
@@ -1211,7 +1233,7 @@ async def generate_anthropic_messages(
See https://docs.ollama.com/api/anthropic-compatibility
"""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
payload = {**form_data}
@@ -1227,9 +1249,9 @@ async def generate_anthropic_messages(
await check_model_access(user, None)
url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user)
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
api_config = (await Config.get('ollama.api_configs', {})).get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
(await Config.get('ollama.api_configs', {})).get(url, {}), # Legacy support
)
prefix_id = api_config.get('prefix_id', None)
@@ -1239,7 +1261,7 @@ async def generate_anthropic_messages(
return await send_request(
f'{url}/v1/messages',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=payload.get('stream', False),
content_type='text/event-stream' if payload.get('stream', False) else None,
@@ -1269,7 +1291,7 @@ async def generate_responses(
See https://ollama.com/blog/responses-api
"""
if not request.app.state.config.ENABLE_OLLAMA_API:
if not await Config.get('ollama.enable'):
raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED)
payload = form_data.model_dump()
@@ -1285,9 +1307,9 @@ async def generate_responses(
await check_model_access(user, None)
url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user)
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
api_config = (await Config.get('ollama.api_configs', {})).get(
str(url_idx),
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
(await Config.get('ollama.api_configs', {})).get(url, {}), # Legacy support
)
prefix_id = api_config.get('prefix_id', None)
@@ -1297,7 +1319,7 @@ async def generate_responses(
return await send_request(
f'{url}/v1/responses',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
key=get_api_key(url_idx, url, (await Config.get('ollama.api_configs', {}))),
user=user,
stream=payload.get('stream', False),
content_type='text/event-stream' if payload.get('stream', False) else None,
@@ -1317,7 +1339,7 @@ async def get_openai_models(
model_list = await get_all_models(request, user=user)
raw_models = model_list['models']
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
url = (await Config.get('ollama.base_urls', []))[url_idx]
model_list = await send_request(f'{url}/api/tags', 'GET')
raw_models = model_list.get('models', [])
@@ -1429,7 +1451,7 @@ async def download_model(
detail='Invalid file_url. Only URLs from allowed hosts are permitted.',
)
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0]
url = (await Config.get('ollama.base_urls', []))[url_idx if url_idx is not None else 0]
file_name = parse_huggingface_url(form_data.url)
if not file_name:
@@ -1450,7 +1472,7 @@ async def upload_model(
user=Depends(get_admin_user),
):
"""Upload a local model file, push it as a blob, and create the model in Ollama."""
ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0]
ollama_url = (await Config.get('ollama.base_urls', []))[url_idx if url_idx is not None else 0]
filename = os.path.basename(file.filename)
file_path = os.path.join(UPLOAD_DIR, filename)
+76 -97
View File
@@ -34,6 +34,7 @@ from open_webui.env import (
)
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.users import UserModel
@@ -236,15 +237,50 @@ def get_microsoft_entra_id_access_token():
router = APIRouter()
OPENAI_CONFIG_KEYS = {
'ENABLE_OPENAI_API': 'openai.enable',
'OPENAI_API_BASE_URLS': 'openai.api_base_urls',
'OPENAI_API_KEYS': 'openai.api_keys',
'OPENAI_API_CONFIGS': 'openai.api_configs',
}
async def get_openai_config() -> dict:
values = await Config.get_many(*OPENAI_CONFIG_KEYS.values())
return {field: values[storage_key] for field, storage_key in OPENAI_CONFIG_KEYS.items() if storage_key in values}
async def get_openai_runtime_config() -> tuple[bool, list[str], list[str], dict]:
values = await Config.get_many('openai.enable', 'openai.api_base_urls', 'openai.api_keys', 'openai.api_configs')
return (
values.get('openai.enable'),
values.get('openai.api_base_urls') or [],
values.get('openai.api_keys') or [],
values.get('openai.api_configs') or {},
)
async def normalize_openai_api_keys(api_base_urls: list[str], api_keys: list[str]) -> list[str]:
if len(api_keys) > len(api_base_urls):
api_keys = api_keys[: len(api_base_urls)]
elif len(api_keys) < len(api_base_urls):
api_keys = [*api_keys, *([''] * (len(api_base_urls) - len(api_keys)))]
await Config.upsert({'openai.api_keys': api_keys})
return api_keys
async def get_openai_connection(idx: int) -> tuple[str, str, dict]:
_, api_base_urls, api_keys, api_configs = await get_openai_runtime_config()
url = api_base_urls[idx]
key = api_keys[idx]
api_config = api_configs.get(str(idx), api_configs.get(url, {}))
return url, key, api_config
@router.get('/config')
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API,
'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS,
'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS,
'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS,
}
return await get_openai_config()
class OpenAIConfigForm(BaseModel):
@@ -256,41 +292,37 @@ class OpenAIConfigForm(BaseModel):
@router.post('/config/update')
async def update_config(request: Request, form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_OPENAI_API = form_data.ENABLE_OPENAI_API
request.app.state.config.OPENAI_API_BASE_URLS = form_data.OPENAI_API_BASE_URLS
request.app.state.config.OPENAI_API_KEYS = form_data.OPENAI_API_KEYS
api_keys = form_data.OPENAI_API_KEYS
# Check if API KEYS length is same than API URLS length
if len(request.app.state.config.OPENAI_API_KEYS) != len(request.app.state.config.OPENAI_API_BASE_URLS):
if len(request.app.state.config.OPENAI_API_KEYS) > len(request.app.state.config.OPENAI_API_BASE_URLS):
request.app.state.config.OPENAI_API_KEYS = request.app.state.config.OPENAI_API_KEYS[
: len(request.app.state.config.OPENAI_API_BASE_URLS)
]
else:
request.app.state.config.OPENAI_API_KEYS += [''] * (
len(request.app.state.config.OPENAI_API_BASE_URLS) - len(request.app.state.config.OPENAI_API_KEYS)
)
if len(api_keys) > len(form_data.OPENAI_API_BASE_URLS):
api_keys = api_keys[: len(form_data.OPENAI_API_BASE_URLS)]
elif len(api_keys) < len(form_data.OPENAI_API_BASE_URLS):
api_keys = [*api_keys, *([''] * (len(form_data.OPENAI_API_BASE_URLS) - len(api_keys)))]
request.app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS
valid_keys = set(map(str, range(len(form_data.OPENAI_API_BASE_URLS))))
api_configs = {key: value for key, value in form_data.OPENAI_API_CONFIGS.items() if key in valid_keys}
# Remove the API configs that are not in the API URLS
keys = list(map(str, range(len(request.app.state.config.OPENAI_API_BASE_URLS))))
request.app.state.config.OPENAI_API_CONFIGS = {
key: value for key, value in request.app.state.config.OPENAI_API_CONFIGS.items() if key in keys
}
await Config.upsert(
{
'openai.enable': form_data.ENABLE_OPENAI_API,
'openai.api_base_urls': form_data.OPENAI_API_BASE_URLS,
'openai.api_keys': api_keys,
'openai.api_configs': api_configs,
}
)
return {
'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API,
'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS,
'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS,
'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS,
'ENABLE_OPENAI_API': form_data.ENABLE_OPENAI_API,
'OPENAI_API_BASE_URLS': form_data.OPENAI_API_BASE_URLS,
'OPENAI_API_KEYS': api_keys,
'OPENAI_API_CONFIGS': api_configs,
}
@router.post('/audio/speech')
async def speech(request: Request, user=Depends(get_verified_user)):
if user.role != 'admin' and not await has_permission(
user.id, 'chat.tts', request.app.state.config.USER_PERMISSIONS
user.id, 'chat.tts', await Config.get('user.permissions')
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -299,7 +331,8 @@ async def speech(request: Request, user=Depends(get_verified_user)):
idx = None
try:
idx = request.app.state.config.OPENAI_API_BASE_URLS.index('https://api.openai.com/v1')
_, api_base_urls, _, _ = await get_openai_runtime_config()
idx = api_base_urls.index('https://api.openai.com/v1')
body = await request.body()
name = hashlib.sha256(body).hexdigest()
@@ -313,12 +346,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
if file_path.is_file():
return FileResponse(file_path)
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
url, key, api_config = await get_openai_connection(idx)
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
@@ -368,29 +396,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async def get_all_models_responses(request: Request, user: UserModel) -> list:
if not request.app.state.config.ENABLE_OPENAI_API:
enable_openai_api, api_base_urls, api_keys, api_configs = await get_openai_runtime_config()
if not enable_openai_api:
return []
# Cache config values locally to avoid repeated Redis lookups.
# Each access to request.app.state.config.<KEY> triggers a Redis GET;
# caching here avoids hundreds of redundant round-trips.
api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS
api_keys = list(request.app.state.config.OPENAI_API_KEYS)
api_configs = request.app.state.config.OPENAI_API_CONFIGS
# Check if API KEYS length is same than API URLS length
num_urls = len(api_base_urls)
num_keys = len(api_keys)
if num_keys != num_urls:
# if there are more keys than urls, remove the extra keys
if num_keys > num_urls:
api_keys = api_keys[:num_urls]
request.app.state.config.OPENAI_API_KEYS = api_keys
# if there are more urls than keys, add empty keys
else:
api_keys += [''] * (num_urls - num_keys)
request.app.state.config.OPENAI_API_KEYS = api_keys
api_keys = await normalize_openai_api_keys(api_base_urls, api_keys)
request_tasks = []
for idx, url in enumerate(api_base_urls):
@@ -500,13 +514,10 @@ async def get_filtered_models(models, user, db=None):
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info('get_all_models()')
if not request.app.state.config.ENABLE_OPENAI_API:
enable_openai_api, api_base_urls, _, _ = await get_openai_runtime_config()
if not enable_openai_api:
return {'data': []}
# Cache config value locally to avoid repeated Redis lookups inside
# the nested loop in get_merged_models (one GET per model otherwise).
api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS
responses = await get_all_models_responses(request, user=user)
def extract_data(response):
@@ -577,7 +588,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
@router.get('/models')
@router.get('/models/{url_idx}')
async def get_models(request: Request, url_idx: int | None = None, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_OPENAI_API:
if not await Config.get('openai.enable'):
raise HTTPException(status_code=503, detail='OpenAI API is disabled')
models = {
@@ -587,13 +598,7 @@ async def get_models(request: Request, url_idx: int | None = None, user=Depends(
if url_idx is None:
models = await get_all_models(request, user=user)
else:
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = request.app.state.config.OPENAI_API_KEYS[url_idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
url, key, api_config = await get_openai_connection(url_idx)
r = None
async with aiohttp.ClientSession(
@@ -1122,13 +1127,7 @@ async def generate_chat_completion(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(),
)
# Get the API config for the model
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
), # Legacy support
)
url, key, api_config = await get_openai_connection(idx)
prefix_id = api_config.get('prefix_id', None)
if prefix_id:
@@ -1143,9 +1142,6 @@ async def generate_chat_completion(
'role': user.role,
}
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
# Check if model is a reasoning model that needs special handling
if is_openai_new_model(payload['model']):
payload = openai_reasoning_model_handler(payload)
@@ -1311,12 +1307,7 @@ async def embeddings(request: Request, form_data: dict, user):
if model_id in models:
idx = models[model_id]['urlIdx']
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
url, key, api_config = await get_openai_connection(idx)
r = None
streaming = False
@@ -1434,12 +1425,7 @@ async def responses(
if model_id in models:
idx = models[model_id]['urlIdx']
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
url, key, api_config = await get_openai_connection(idx)
r = None
streaming = False
@@ -1543,14 +1529,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
if model_id in models:
idx = models[model_id]['urlIdx']
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
), # Legacy support
)
url, key, api_config = await get_openai_connection(idx)
r = None
streaming = False
+18 -19
View File
@@ -18,6 +18,7 @@ from fastapi import (
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
from open_webui.models.config import Config
from open_webui.routers.openai import get_all_models_responses
from open_webui.utils.auth import get_admin_user
from pydantic import BaseModel
@@ -51,6 +52,12 @@ def get_sorted_filters(model_id, models):
return sorted_filters
async def get_openai_connection(url_idx: int) -> tuple[str, str]:
base_urls = await Config.get('openai.api_base_urls', [])
api_keys = await Config.get('openai.api_keys', [])
return base_urls[url_idx], api_keys[url_idx]
async def process_pipeline_inlet_filter(request, payload, user, models):
user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role}
model_id = payload['model']
@@ -69,8 +76,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
except Exception:
continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
if not key:
continue
@@ -133,8 +139,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
except Exception:
continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
if not key:
continue
@@ -194,11 +199,12 @@ async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
log.debug(f'get_pipelines_list: get_openai_models_responses returned {responses}')
urlIdxs = [idx for idx, response in enumerate(responses) if response is not None and 'pipelines' in response]
base_urls = await Config.get('openai.api_base_urls', [])
return {
'data': [
{
'url': request.app.state.config.OPENAI_API_BASE_URLS[urlIdx],
'url': base_urls[urlIdx],
'idx': urlIdx,
}
for urlIdx in urlIdxs
@@ -233,8 +239,7 @@ async def upload_pipeline(
with open(file_path, 'wb') as buffer:
shutil.copyfileobj(file.file, buffer)
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
headers = {'Authorization': f'Bearer {key}'}
@@ -294,8 +299,7 @@ async def add_pipeline(request: Request, form_data: AddPipelineForm, user=Depend
try:
urlIdx = form_data.urlIdx
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
@@ -338,8 +342,7 @@ async def delete_pipeline(request: Request, form_data: DeletePipelineForm, user=
try:
urlIdx = form_data.urlIdx
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.delete(
@@ -375,8 +378,7 @@ async def delete_pipeline(request: Request, form_data: DeletePipelineForm, user=
async def get_pipelines(request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
response = None
try:
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
@@ -416,8 +418,7 @@ async def get_pipeline_valves(
):
response = None
try:
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
@@ -457,8 +458,7 @@ async def get_pipeline_valves_spec(
):
response = None
try:
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
@@ -499,8 +499,7 @@ async def update_pipeline_valves(
):
response = None
try:
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
url, key = await get_openai_connection(urlIdx)
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
+6 -5
View File
@@ -7,6 +7,7 @@ from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.prompt_history import (
PromptHistories,
@@ -149,13 +150,13 @@ async def create_new_prompt(
await has_permission(
user.id,
'workspace.prompts',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
)
or await has_permission(
user.id,
'workspace.prompts_import',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
)
):
@@ -165,7 +166,7 @@ async def create_new_prompt(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -281,7 +282,7 @@ async def update_prompt_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -438,7 +439,7 @@ async def update_prompt_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
File diff suppressed because it is too large. Load diff
-7
View File
@@ -259,10 +259,6 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
enable_scim = getattr(request.app.state, 'ENABLE_SCIM', False)
log.info(f'SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}')
# Handle both ConfigVar and direct value
if hasattr(enable_scim, 'value'):
enable_scim = enable_scim.value
if not enable_scim:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -271,9 +267,6 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
# Verify the SCIM token
scim_token = getattr(request.app.state, 'SCIM_TOKEN', None)
# Handle both ConfigVar and direct value
if hasattr(scim_token, 'value'):
scim_token = scim_token.value
log.debug(f'SCIM token configured: {bool(scim_token)}')
if not scim_token or not hmac.compare_digest(token, scim_token):
raise HTTPException(
+6 -5
View File
@@ -6,6 +6,7 @@ from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.skills import (
SkillAccessListResponse,
@@ -130,7 +131,7 @@ async def export_skills(
if user.role != 'admin' and not await has_permission(
user.id,
'workspace.skills',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
):
raise HTTPException(
@@ -157,7 +158,7 @@ async def create_new_skill(
db: AsyncSession = Depends(get_async_session),
):
if user.role != 'admin' and not await has_permission(
user.id, 'workspace.skills', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'workspace.skills', await Config.get('user.permissions'), db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -179,7 +180,7 @@ async def create_new_skill(
# grants in the create payload, bypassing the sharing.public_skills gate
# that the dedicated /access/update endpoint already enforces.
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -292,7 +293,7 @@ async def update_skill_by_id(
# they may set, so a non-admin owner cannot make their own skill publicly
# readable/writable without sharing.public_skills permission.
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -361,7 +362,7 @@ async def update_skill_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+75 -99
View File
@@ -16,6 +16,7 @@ from open_webui.config import (
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE,
)
from open_webui.constants import ERROR_MESSAGES, TASKS
from open_webui.models.config import Config
from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.chat import generate_chat_completion
@@ -36,6 +37,35 @@ log = logging.getLogger(__name__)
router = APIRouter()
TASK_CONFIG_KEYS = {
'TASK_MODEL': 'task.model.default',
'TASK_MODEL_EXTERNAL': 'task.model.external',
'TITLE_GENERATION_PROMPT_TEMPLATE': 'task.title.prompt_template',
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': 'task.image.prompt_template',
'ENABLE_AUTOCOMPLETE_GENERATION': 'task.autocomplete.enable',
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': 'task.autocomplete.input_max_length',
'TAGS_GENERATION_PROMPT_TEMPLATE': 'task.tags.prompt_template',
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': 'task.follow_up.prompt_template',
'ENABLE_FOLLOW_UP_GENERATION': 'task.follow_up.enable',
'ENABLE_TAGS_GENERATION': 'task.tags.enable',
'ENABLE_TITLE_GENERATION': 'task.title.enable',
'ENABLE_SEARCH_QUERY_GENERATION': 'task.query.search.enable',
'ENABLE_RETRIEVAL_QUERY_GENERATION': 'task.query.retrieval.enable',
'QUERY_GENERATION_PROMPT_TEMPLATE': 'task.query.prompt_template',
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': 'task.tools.prompt_template',
'ENABLE_VOICE_MODE_PROMPT': 'task.voice.prompt.enable',
'VOICE_MODE_PROMPT_TEMPLATE': 'task.voice.prompt_template',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
##################################
#
@@ -59,25 +89,7 @@ async def check_active_chats(request: Request, form_data: ActiveChatsForm, user=
@router.get('/config')
async def get_task_config(request: Request, user=Depends(get_verified_user)):
return {
'TASK_MODEL': request.app.state.config.TASK_MODEL,
'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL,
'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION,
'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION,
'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
'ENABLE_VOICE_MODE_PROMPT': request.app.state.config.ENABLE_VOICE_MODE_PROMPT,
'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
}
return await get_config_values(TASK_CONFIG_KEYS)
class TaskConfigForm(BaseModel):
@@ -102,56 +114,13 @@ class TaskConfigForm(BaseModel):
@router.post('/config/update')
async def update_task_config(request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)):
request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = form_data.TITLE_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = form_data.ENABLE_FOLLOW_UP_GENERATION
request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = form_data.ENABLE_AUTOCOMPLETE_GENERATION
request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
form_data.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
)
request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = form_data.TAGS_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_TAGS_GENERATION = form_data.ENABLE_TAGS_GENERATION
request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = form_data.ENABLE_SEARCH_QUERY_GENERATION
request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = form_data.ENABLE_RETRIEVAL_QUERY_GENERATION
request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = form_data.QUERY_GENERATION_PROMPT_TEMPLATE
request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
request.app.state.config.ENABLE_VOICE_MODE_PROMPT = form_data.ENABLE_VOICE_MODE_PROMPT
request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = form_data.VOICE_MODE_PROMPT_TEMPLATE
return {
'TASK_MODEL': request.app.state.config.TASK_MODEL,
'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL,
'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION,
'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION,
'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
'ENABLE_VOICE_MODE_PROMPT': request.app.state.config.ENABLE_VOICE_MODE_PROMPT,
'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
}
await Config.upsert(config_updates(form_data.model_dump(), TASK_CONFIG_KEYS))
return await get_config_values(TASK_CONFIG_KEYS)
@router.post('/title/completions')
async def generate_title(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_TITLE_GENERATION:
if not await Config.get('task.title.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Title generation is disabled'},
@@ -181,15 +150,16 @@ async def generate_title(request: Request, form_data: dict, user=Depends(get_ver
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating chat title using model {task_model_id} for user {user.email} ')
if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
title_template = await Config.get('task.title.prompt_template')
if title_template != '':
template = title_template
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
@@ -234,7 +204,7 @@ async def generate_title(request: Request, form_data: dict, user=Depends(get_ver
@router.post('/follow_up/completions')
async def generate_follow_ups(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_FOLLOW_UP_GENERATION:
if not await Config.get('task.follow_up.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Follow-up generation is disabled'},
@@ -259,15 +229,16 @@ async def generate_follow_ups(request: Request, form_data: dict, user=Depends(ge
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating chat title using model {task_model_id} for user {user.email} ')
if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
follow_up_template = await Config.get('task.follow_up.prompt_template')
if follow_up_template != '':
template = follow_up_template
else:
template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
@@ -303,7 +274,7 @@ async def generate_follow_ups(request: Request, form_data: dict, user=Depends(ge
@router.post('/tags/completions')
async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_TAGS_GENERATION:
if not await Config.get('task.tags.enable'):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'detail': 'Tags generation is disabled'},
@@ -328,15 +299,16 @@ async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating chat tags using model {task_model_id} for user {user.email} ')
if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE
tags_template = await Config.get('task.tags.prompt_template')
if tags_template != '':
template = tags_template
else:
template = DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE
@@ -391,15 +363,16 @@ async def generate_image_prompt(request: Request, form_data: dict, user=Depends(
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating image prompt using model {task_model_id} for user {user.email} ')
if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
image_prompt_template = await Config.get('task.image.prompt_template')
if image_prompt_template != '':
template = image_prompt_template
else:
template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
@@ -437,13 +410,13 @@ async def generate_image_prompt(request: Request, form_data: dict, user=Depends(
async def generate_queries(request: Request, form_data: dict, user=Depends(get_verified_user)):
type = form_data.get('type')
if type == 'web_search':
if not request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION:
if not await Config.get('task.query.search.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Search query generation'),
)
elif type == 'retrieval':
if not request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION:
if not await Config.get('task.query.retrieval.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Query generation'),
@@ -472,15 +445,16 @@ async def generate_queries(request: Request, form_data: dict, user=Depends(get_v
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating {type} queries using model {task_model_id} for user {user.email}')
if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != '':
template = request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE
query_template = await Config.get('task.query.prompt_template')
if query_template.strip() != '':
template = query_template
else:
template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE
@@ -515,7 +489,7 @@ async def generate_queries(request: Request, form_data: dict, user=Depends(get_v
@router.post('/auto/completions')
async def generate_autocompletion(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION:
if not await Config.get('task.autocomplete.enable'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Autocompletion generation'),
@@ -525,11 +499,12 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend
prompt = form_data.get('prompt')
messages = form_data.get('messages')
if request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH > 0:
if len(prompt) > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH:
autocomplete_input_max_length = await Config.get('task.autocomplete.input_max_length')
if autocomplete_input_max_length > 0:
if len(prompt) > autocomplete_input_max_length:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.INPUT_TOO_LONG(request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH),
detail=ERROR_MESSAGES.INPUT_TOO_LONG(autocomplete_input_max_length),
)
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
@@ -551,15 +526,16 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
log.debug(f'generating autocompletion using model {task_model_id} for user {user.email}')
if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != '':
template = request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
autocomplete_template = await Config.get('task.autocomplete.prompt_template')
if autocomplete_template.strip() != '':
template = autocomplete_template
else:
template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
@@ -614,8 +590,8 @@ async def generate_emoji(request: Request, form_data: dict, user=Depends(get_ver
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
await Config.get('task.model.default'),
await Config.get('task.model.external'),
models,
)
+4 -3
View File
@@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, Request, Response, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse
from open_webui.config import TERMINAL_PROXY_HEADERS
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.users import Users
from open_webui.utils.access_control import has_connection_access
@@ -62,7 +63,7 @@ def _sanitize_proxy_path(path: str) -> str | None:
@router.get('/')
async def list_terminal_servers(request: Request, user=Depends(get_verified_user)):
"""Return terminal servers the authenticated user has access to."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connections = await Config.get('terminal_server.connections', []) or []
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)}
return [
@@ -87,7 +88,7 @@ async def proxy_terminal(
user=Depends(get_verified_user),
):
"""Proxy a request to the admin terminal server identified by *server_id*."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connections = await Config.get('terminal_server.connections', []) or []
connection = next((c for c in connections if c.get('id') == server_id), None)
if connection is None:
@@ -235,7 +236,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
return None
# Resolve terminal server
connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connections = await Config.get('terminal_server.connections', []) or []
connection = next((c for c in connections if c.get('id') == server_id), None)
if connection is None:
+11 -10
View File
@@ -13,6 +13,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT
from open_webui.internal.db import get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.models.tools import (
@@ -84,7 +85,7 @@ async def get_tools(
server_access_grants = {}
for server in await get_tool_servers(request):
server_idx = server.get('idx', 0)
connections = request.app.state.config.TOOL_SERVER_CONNECTIONS
connections = await Config.get('tool_server.connections', [])
if server_idx >= len(connections):
log.warning(
f'Tool server index {server_idx} out of range '
@@ -113,7 +114,7 @@ async def get_tools(
)
# MCP Tool Servers
for server in request.app.state.config.TOOL_SERVER_CONNECTIONS:
for server in await Config.get('tool_server.connections', []):
if server.get('type', 'openapi') == 'mcp' and server.get('config', {}).get('enable'):
server_id = server.get('info', {}).get('id')
auth_type = server.get('auth_type', 'none')
@@ -303,7 +304,7 @@ async def export_tools(
if user.role != 'admin' and not await has_permission(
user.id,
'workspace.tools_export',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
):
raise HTTPException(
@@ -331,11 +332,11 @@ async def create_new_tools(
):
"""Create a new tool from user-supplied Python source code."""
if user.role != 'admin' and not (
await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db)
await has_permission(user.id, 'workspace.tools', await Config.get('user.permissions'), db=db)
or await has_permission(
user.id,
'workspace.tools_import',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
db=db,
)
):
@@ -356,7 +357,7 @@ async def create_new_tools(
if tools is None:
try:
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -484,8 +485,8 @@ async def update_tools_by_id(
# Content edits trigger exec on load — gate them behind workspace.tools (matches /create).
if form_data.content != tools.content:
if user.role != 'admin' and not (
await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db)
or await has_permission(user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db)
await has_permission(user.id, 'workspace.tools', await Config.get('user.permissions'), db=db)
or await has_permission(user.id, 'workspace.tools_import', await Config.get('user.permissions'), db=db)
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -503,7 +504,7 @@ async def update_tools_by_id(
specs = get_tool_specs(TOOLS[id])
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
@@ -574,7 +575,7 @@ async def update_tool_access_by_id(
)
form_data.access_grants = await filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
user.id,
user.role,
form_data.access_grants,
+16 -13
View File
@@ -12,6 +12,7 @@ from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR
from open_webui.internal.db import get_async_session
from open_webui.models.auths import Auths
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.models.users import (
@@ -157,7 +158,7 @@ async def get_user_permissisions(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
user_permissions = await get_permissions(user.id, await Config.get('user.permissions'), db=db)
return user_permissions
@@ -239,7 +240,7 @@ class FeaturesPermissions(BaseModel):
memories: bool = True
automations: bool = False
calendar: bool = True
user_webhooks: bool = False
webhooks: bool = False
class SettingsPermissions(BaseModel):
@@ -257,20 +258,22 @@ class UserPermissions(BaseModel):
@router.get('/default/permissions', response_model=UserPermissions)
async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)):
user_permissions = await Config.get('user.permissions')
return {
'workspace': WorkspacePermissions(**request.app.state.config.USER_PERMISSIONS.get('workspace', {})),
'sharing': SharingPermissions(**request.app.state.config.USER_PERMISSIONS.get('sharing', {})),
'access_grants': AccessGrantsPermissions(**request.app.state.config.USER_PERMISSIONS.get('access_grants', {})),
'chat': ChatPermissions(**request.app.state.config.USER_PERMISSIONS.get('chat', {})),
'features': FeaturesPermissions(**request.app.state.config.USER_PERMISSIONS.get('features', {})),
'settings': SettingsPermissions(**request.app.state.config.USER_PERMISSIONS.get('settings', {})),
'workspace': WorkspacePermissions(**user_permissions.get('workspace', {})),
'sharing': SharingPermissions(**user_permissions.get('sharing', {})),
'access_grants': AccessGrantsPermissions(**user_permissions.get('access_grants', {})),
'chat': ChatPermissions(**user_permissions.get('chat', {})),
'features': FeaturesPermissions(**user_permissions.get('features', {})),
'settings': SettingsPermissions(**user_permissions.get('settings', {})),
}
@router.post('/default/permissions')
async def update_default_user_permissions(request: Request, form_data: UserPermissions, user=Depends(get_admin_user)):
request.app.state.config.USER_PERMISSIONS = form_data.model_dump(by_alias=True)
return request.app.state.config.USER_PERMISSIONS
user_permissions = form_data.model_dump(by_alias=True)
await Config.upsert({'user.permissions': user_permissions})
return user_permissions
@router.get('/default/permissions/defaults', response_model=UserPermissions)
@@ -321,7 +324,7 @@ async def update_user_settings_by_session_user(
and not await has_permission(
user.id,
'features.direct_tool_servers',
request.app.state.config.USER_PERMISSIONS,
await Config.get('user.permissions'),
)
):
# If the user is not an admin and does not have permission to use tool servers, remove the key
@@ -348,7 +351,7 @@ async def get_user_status_by_session_user(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if not request.app.state.config.ENABLE_USER_STATUS:
if not await Config.get('users.enable_status'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
@@ -369,7 +372,7 @@ async def update_user_status_by_session_user(
user=Depends(get_verified_user),
db: AsyncSession = Depends(get_async_session),
):
if not request.app.state.config.ENABLE_USER_STATUS:
if not await Config.get('users.enable_status'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
+9 -8
View File
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES
from open_webui.models.chats import ChatTitleMessagesForm
from open_webui.models.config import Config
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.code_interpreter import execute_code_jupyter
from open_webui.utils.misc import get_gravatar_url
@@ -41,27 +42,27 @@ async def format_code(form_data: CodeForm, user=Depends(get_admin_user)):
@router.post('/code/execute')
async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_CODE_EXECUTION:
if not await Config.get('code_execution.enable'):
raise HTTPException(
status_code=403,
detail=ERROR_MESSAGES.FEATURE_DISABLED('Code execution'),
)
if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter':
if await Config.get('code_execution.engine') == 'jupyter':
output = await execute_code_jupyter(
request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
await Config.get('code_execution.jupyter.url'),
form_data.code,
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'token'
await Config.get('code_execution.jupyter.auth_token')
if await Config.get('code_execution.jupyter.auth') == 'token'
else None
),
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'password'
await Config.get('code_execution.jupyter.auth_password')
if await Config.get('code_execution.jupyter.auth') == 'password'
else None
),
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
await Config.get('code_execution.jupyter.timeout'),
)
return output