diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6cfb87e..3956e566c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.114] - 2024-03-20 + +### Added + +- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL. +- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy. +- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others. + +### Fixed + +- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields. +- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding. + +### Changed + +- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization. + ## [0.1.113] - 2024-03-18 ### Added diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py index 9dc5d6f2c..a9922aad7 100644 --- a/backend/apps/litellm/main.py +++ b/backend/apps/litellm/main.py @@ -3,14 +3,26 @@ import logging from litellm.proxy.proxy_server import ProxyConfig, initialize from litellm.proxy.proxy_server import app -from fastapi import FastAPI, Request, Depends, status +from fastapi import FastAPI, Request, Depends, status, Response from fastapi.responses import JSONResponse + +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import StreamingResponse +import json + from utils.utils import get_http_authorization_cred, get_current_user from config import SRC_LOG_LEVELS, ENV log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["LITELLM"]) + +from config import ( + MODEL_FILTER_ENABLED, + MODEL_FILTER_LIST, +) + + proxy_config = ProxyConfig() @@ -31,16 +43,58 @@ async def on_startup(): await startup() +app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST + + @app.middleware("http") async def auth_middleware(request: Request, call_next): auth_header = request.headers.get("Authorization", "") + request.state.user = None - if ENV != "dev": - try: - user = get_current_user(get_http_authorization_cred(auth_header)) - log.debug(f"user: {user}") - except Exception as e: - return JSONResponse(status_code=400, content={"detail": str(e)}) + try: + user = get_current_user(get_http_authorization_cred(auth_header)) + log.debug(f"user: {user}") + request.state.user = user + except Exception as e: + return JSONResponse(status_code=400, content={"detail": str(e)}) response = await call_next(request) return response + + +class ModifyModelsResponseMiddleware(BaseHTTPMiddleware): + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + + response = await call_next(request) + user = request.state.user + + if "/models" in request.url.path: + if isinstance(response, StreamingResponse): + # Read the content of the streaming response + body = b"" + async for chunk in response.body_iterator: + body += chunk + + data = json.loads(body.decode("utf-8")) + + if app.state.MODEL_FILTER_ENABLED: + if user and user.role == "user": + data["data"] = list( + filter( + lambda model: model["id"] + in app.state.MODEL_FILTER_LIST, + data["data"], + ) + ) + + # Modified Flag + data["modified"] = True + return JSONResponse(content=data) + + return response + + +app.add_middleware(ModifyModelsResponseMiddleware) diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 777203e55..8595e52c9 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -712,7 +712,7 @@ class GenerateChatCompletionForm(BaseModel): format: Optional[str] = None options: Optional[dict] = None template: Optional[str] = None - stream: Optional[bool] = True + stream: Optional[bool] = None keep_alive: Optional[Union[int, str]] = None diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index bd14f4bda..dd5c0c704 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -19,6 +19,7 @@ from config import ( DEFAULT_USER_ROLE, ENABLE_SIGNUP, USER_PERMISSIONS, + WEBHOOK_URL, ) app = FastAPI() @@ -32,6 +33,7 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.USER_PERMISSIONS = USER_PERMISSIONS +app.state.WEBHOOK_URL = WEBHOOK_URL app.add_middleware( diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 3db2d0ad2..d881ec746 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -27,7 +27,8 @@ from utils.utils import ( create_token, ) from utils.misc import parse_duration, validate_email_format -from constants import ERROR_MESSAGES +from utils.webhook import post_webhook +from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES router = APIRouter() @@ -155,6 +156,17 @@ async def signup(request: Request, form_data: SignupForm): ) # response.set_cookie(key='token', value=token, httponly=True) + if request.app.state.WEBHOOK_URL: + post_webhook( + request.app.state.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + "action": "signup", + "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + "user": user.model_dump_json(exclude_none=True), + }, + ) + return { "token": token, "token_type": "Bearer", diff --git a/backend/config.py b/backend/config.py index 6d94d2233..c2ee78935 100644 --- a/backend/config.py +++ b/backend/config.py @@ -320,13 +320,19 @@ DEFAULT_PROMPT_SUGGESTIONS = ( DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending") -USER_PERMISSIONS = {"chat": {"deletion": True}} + +USER_PERMISSIONS_CHAT_DELETION = ( + os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true" +) + +USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}} -MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False) +MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true" MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "") MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")] +WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "") #################################### # WEBUI_VERSION diff --git a/backend/constants.py b/backend/constants.py index 05bdebc54..42c5c85eb 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -5,6 +5,13 @@ class MESSAGES(str, Enum): DEFAULT = lambda msg="": f"{msg if msg else ''}" +class WEBHOOK_MESSAGES(str, Enum): + DEFAULT = lambda msg="": f"{msg if msg else ''}" + USER_SIGNUP = lambda username="": ( + f"New user signed up: {username}" if username else "New user signed up" + ) + + class ERROR_MESSAGES(str, Enum): def __str__(self) -> str: return super().__str__() @@ -46,7 +53,7 @@ class ERROR_MESSAGES(str, Enum): PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." INCORRECT_FORMAT = ( - lambda err="": f"Invalid format. Please use the correct format{err if err else ''}" + lambda err="": f"Invalid format. Please use the correct format{err}" ) RATE_LIMIT_EXCEEDED = "API rate limit exceeded" diff --git a/backend/data/config.json b/backend/data/config.json index d3ada59c9..cd6687d79 100644 --- a/backend/data/config.json +++ b/backend/data/config.json @@ -1,5 +1,5 @@ { - "version": "0.0.1", + "version": 0, "ui": { "prompt_suggestions": [ { diff --git a/backend/main.py b/backend/main.py index 24989c814..d4b67079c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -41,6 +41,7 @@ from config import ( MODEL_FILTER_LIST, GLOBAL_LOG_LEVEL, SRC_LOG_LEVELS, + WEBHOOK_URL, ) from constants import ERROR_MESSAGES @@ -64,6 +65,9 @@ app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.WEBHOOK_URL = WEBHOOK_URL + + origins = ["*"] @@ -184,7 +188,7 @@ class ModelFilterConfigForm(BaseModel): @app.post("/api/config/model/filter") -async def get_model_filter_config( +async def update_model_filter_config( form_data: ModelFilterConfigForm, user=Depends(get_admin_user) ): @@ -203,6 +207,28 @@ async def get_model_filter_config( } +@app.get("/api/webhook") +async def get_webhook_url(user=Depends(get_admin_user)): + return { + "url": app.state.WEBHOOK_URL, + } + + +class UrlForm(BaseModel): + url: str + + +@app.post("/api/webhook") +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.WEBHOOK_URL = form_data.url + + webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL + + return { + "url": app.state.WEBHOOK_URL, + } + + @app.get("/api/version") async def get_app_config(): diff --git a/backend/utils/webhook.py b/backend/utils/webhook.py new file mode 100644 index 000000000..1bc5a6048 --- /dev/null +++ b/backend/utils/webhook.py @@ -0,0 +1,20 @@ +import requests + + +def post_webhook(url: str, message: str, event_data: dict) -> bool: + try: + payload = {} + + if "https://hooks.slack.com" in url: + payload["text"] = message + elif "https://discord.com/api/webhooks" in url: + payload["content"] = message + else: + payload = {**event_data} + + r = requests.post(url, json=payload) + r.raise_for_status() + return True + except Exception as e: + print(e) + return False diff --git a/package.json b/package.json index bd1586ce5..e9e1fecf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.1.113", + "version": "0.1.114", "private": true, "scripts": { "dev": "vite dev --host", diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index b33fb571b..a610f7210 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -139,3 +139,60 @@ export const updateModelFilterConfig = async ( return res; }; + +export const getWebhookUrl = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const updateWebhookUrl = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 351f6744b..be319e26f 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -1,4 +1,5 @@
{ - // console.log('submit'); updateJWTExpiresDurationHandler(JWTExpiresIn); + updateWebhookUrlHandler(); saveHandler(); }} > @@ -108,6 +116,23 @@
+
+
+
{$i18n.t('Webhook URL')}
+
+ +
+ +
+
+ +
+
{$i18n.t('JWT Expiration')}
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index f07f7f98d..a23649d8b 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -673,7 +673,7 @@ ? chatInputPlaceholder : isRecording ? $i18n.t('Listening...') - : $i18n.t('Send a Messsage')} + : $i18n.t('Send a Message')} bind:value={prompt} on:keypress={(e) => { if (e.keyCode == 13 && !e.shiftKey) { diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 7c765b75e..91bb35c47 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -422,7 +422,7 @@ class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" > {#if siblings.length > 1} -
+