refac
This commit is contained in:
1 parent
e8d55c0a8b
commit
5cdcdbaeec
53 files changed
+7829
-7954
No files matched your search
+112
-154
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
@@ -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])
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user