diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 3cec6edd70..278f506634 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1094,21 +1094,27 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""), ) -DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT. - -Examples of titles: -๐Ÿ“‰ Stock Market Trends -๐Ÿช Perfect Chocolate Chip Recipe -Evolution of Music Streaming -Remote Work Productivity Tips -Artificial Intelligence in Healthcare -๐ŸŽฎ Video Game Development Insights - +DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate a concise, 3-5 word title with an emoji summarizing the chat history. +### Guidelines: +- The title should clearly represent the main theme or subject of the conversation. +- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting. +- Write the title in the chat's primary language; default to English if multilingual. +- Prioritize accuracy over excessive creativity; keep it clear and simple. +### Output: +JSON format: { "title": "your concise title here" } +### Examples: +- { "title": "๐Ÿ“‰ Stock Market Trends" }, +- { "title": "๐Ÿช Perfect Chocolate Chip Recipe" }, +- { "title": "Evolution of Music Streaming" }, +- { "title": "Remote Work Productivity Tips" }, +- { "title": "Artificial Intelligence in Healthcare" }, +- { "title": "๐ŸŽฎ Video Game Development Insights" } +### Chat History: {{MESSAGES:END:2}} """ - TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig( "TAGS_GENERATION_PROMPT_TEMPLATE", "task.tags.prompt_template", diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 77e632ccc4..b589e9490b 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -356,15 +356,16 @@ WEBUI_SECRET_KEY = os.environ.get( ), # DEPRECATED: remove at next major version ) -WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get( - "WEBUI_SESSION_COOKIE_SAME_SITE", - os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"), -) +WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax") -WEBUI_SESSION_COOKIE_SECURE = os.environ.get( - "WEBUI_SESSION_COOKIE_SECURE", - os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true", -) +WEBUI_SESSION_COOKIE_SECURE = os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true" + +WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get("WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE) + +WEBUI_AUTH_COOKIE_SECURE = os.environ.get( + "WEBUI_AUTH_COOKIE_SECURE", + os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false") +).lower() == "true" if WEBUI_AUTH and WEBUI_SECRET_KEY == "": raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 985624d816..c5dfad0477 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -877,6 +877,7 @@ async def chat_completion( "tool_ids": form_data.get("tool_ids", None), "files": form_data.get("files", None), "features": form_data.get("features", None), + "variables": form_data.get("variables", None), } form_data["metadata"] = metadata diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 47baeb0acb..b6a2c75628 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -25,8 +25,8 @@ from open_webui.env import ( WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER, WEBUI_AUTH_TRUSTED_NAME_HEADER, - WEBUI_SESSION_COOKIE_SAME_SITE, - WEBUI_SESSION_COOKIE_SECURE, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, SRC_LOG_LEVELS, ) from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -95,8 +95,8 @@ async def get_session_user( value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) user_permissions = get_permissions( @@ -164,7 +164,7 @@ async def update_password( ############################ # LDAP Authentication ############################ -@router.post("/ldap", response_model=SigninResponse) +@router.post("/ldap", response_model=SessionUserResponse) async def ldap_auth(request: Request, response: Response, form_data: LdapForm): ENABLE_LDAP = request.app.state.config.ENABLE_LDAP LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL @@ -288,6 +288,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): httponly=True, # Ensures the cookie is not accessible via JavaScript ) + user_permissions = get_permissions( + user.id, request.app.state.config.USER_PERMISSIONS + ) + return { "token": token, "token_type": "Bearer", @@ -296,6 +300,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): "name": user.name, "role": user.role, "profile_image_url": user.profile_image_url, + "permissions": user_permissions, } else: raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) @@ -378,8 +383,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm): value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) user_permissions = get_permissions( @@ -473,8 +478,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm): value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) if request.app.state.config.WEBHOOK_URL: diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index a001dd01f2..2efd043efe 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -444,15 +444,21 @@ async def pin_chat_by_id(id: str, user=Depends(get_verified_user)): ############################ +class CloneForm(BaseModel): + title: Optional[str] = None + + @router.post("/{id}/clone", response_model=Optional[ChatResponse]) -async def clone_chat_by_id(id: str, user=Depends(get_verified_user)): +async def clone_chat_by_id( + form_data: CloneForm, id: str, user=Depends(get_verified_user) +): chat = Chats.get_chat_by_id_and_user_id(id, user.id) if chat: updated_chat = { **chat.chat, "originalChatId": chat.id, "branchPointMessageId": chat.chat["history"]["currentId"], - "title": f"Clone of {chat.title}", + "title": form_data.title if form_data.title else f"Clone of {chat.title}", } chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat})) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index cce3d63116..aac16e851f 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -264,7 +264,11 @@ def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -342,7 +346,12 @@ def update_file_from_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): + raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -406,7 +415,11 @@ def remove_file_from_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -429,10 +442,6 @@ def remove_file_from_knowledge_by_id( if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection): VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection) - # Delete physical file - if file.path: - Storage.delete_file(file.path) - # Delete file from database Files.delete_file_by_id(form_data.file_id) @@ -484,7 +493,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -543,7 +556,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -582,7 +599,11 @@ def add_files_to_knowledge_batch( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 6c8519b2c8..0cf3308f19 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -183,7 +183,11 @@ async def delete_model_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if model.user_id != user.id and user.role != "admin": + if ( + user.role != "admin" + and model.user_id != user.id + and not has_access(user.id, "write", model.access_control) + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 261cd5ba3a..780fc6f50d 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -395,7 +395,7 @@ async def get_ollama_tags( ) if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: - models["models"] = get_filtered_models(models, user) + models["models"] = await get_filtered_models(models, user) return models @@ -977,6 +977,7 @@ async def generate_chat_completion( if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True + metadata = form_data.pop("metadata", None) try: form_data = GenerateChatCompletionForm(**form_data) except Exception as e: @@ -987,8 +988,6 @@ async def generate_chat_completion( ) payload = {**form_data.model_dump(exclude_none=True)} - if "metadata" in payload: - del payload["metadata"] model_id = payload["model"] model_info = Models.get_model_by_id(model_id) @@ -1006,7 +1005,7 @@ async def generate_chat_completion( payload["options"] = apply_model_params_to_body_ollama( params, payload["options"] ) - payload = apply_model_system_prompt_to_body(params, payload, user) + payload = apply_model_system_prompt_to_body(params, payload, metadata) # Check if user has access to the model if not bypass_filter and user.role == "user": diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index f7d7fd294f..c27f35e7e7 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -489,7 +489,7 @@ async def get_models( raise HTTPException(status_code=500, detail=error_detail) if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: - models["data"] = get_filtered_models(models, user) + models["data"] = await get_filtered_models(models, user) return models @@ -551,9 +551,9 @@ async def generate_chat_completion( bypass_filter = True idx = 0 + payload = {**form_data} - if "metadata" in payload: - del payload["metadata"] + metadata = payload.pop("metadata", None) model_id = form_data.get("model") model_info = Models.get_model_by_id(model_id) @@ -566,7 +566,7 @@ async def generate_chat_completion( params = model_info.params.model_dump() payload = apply_model_params_to_body_openai(params, payload) - payload = apply_model_system_prompt_to_body(params, payload, user) + payload = apply_model_system_prompt_to_body(params, payload, metadata) # Check if user has access to the model if not bypass_filter and user.role == "user": diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 014e5652ec..9fb946c6e7 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -147,7 +147,11 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user) detail=ERROR_MESSAGES.NOT_FOUND, ) - if prompt.user_id != user.id and user.role != "admin": + if ( + prompt.user_id != user.id + and not has_access(user.id, "write", prompt.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 6d7343c8a4..299ec53267 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse, RedirectResponse from pydantic import BaseModel from typing import Optional import logging +import re from open_webui.utils.chat import generate_chat_completion from open_webui.utils.task import ( @@ -161,9 +162,20 @@ async def generate_title( else: template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE + messages = form_data["messages"] + + # Remove reasoning details from the messages + for message in messages: + message["content"] = re.sub( + r"]*>.*?<\/details>", + "", + message["content"], + flags=re.S, + ).strip() + content = title_generation_template( template, - form_data["messages"], + messages, { "name": user.name, "location": user.info.get("location") if user.info else None, @@ -175,10 +187,10 @@ async def generate_title( "messages": [{"role": "user", "content": content}], "stream": False, **( - {"max_tokens": 50} + {"max_tokens": 1000} if models[task_model_id]["owned_by"] == "ollama" else { - "max_completion_tokens": 50, + "max_completion_tokens": 1000, } ), "metadata": { diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 7b9144b4c8..d6a5c5532f 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -227,7 +227,11 @@ async def delete_tools_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if tools.user_id != user.id and user.role != "admin": + if ( + tools.user_id != user.id + and not has_access(user.id, "write", tools.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 27e499e0c1..dde34bec84 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -661,6 +661,9 @@ def apply_params_to_form_data(form_data, model): if "temperature" in params: form_data["temperature"] = params["temperature"] + if "max_tokens" in params: + form_data["max_tokens"] = params["max_tokens"] + if "top_p" in params: form_data["top_p"] = params["top_p"] @@ -741,6 +744,8 @@ async def process_chat_payload(request, form_data, metadata, user, model): files.extend(knowledge_files) form_data["files"] = files + variables = form_data.pop("variables", None) + features = form_data.pop("features", None) if features: if "web_search" in features and features["web_search"]: @@ -884,16 +889,24 @@ async def process_chat_response( if res and isinstance(res, dict): if len(res.get("choices", [])) == 1: - title = ( + title_string = ( res.get("choices", [])[0] .get("message", {}) - .get( - "content", - message.get("content", "New Chat"), - ) - ).strip() + .get("content", message.get("content", "New Chat")) + ) else: - title = None + title_string = "" + + title_string = title_string[ + title_string.find("{") : title_string.rfind("}") + 1 + ] + + try: + title = json.loads(title_string).get( + "title", "New Chat" + ) + except Exception as e: + title = "" if not title: title = messages[0].get("content", "New Chat") diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index a83733d637..8792b1cfcf 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -149,6 +149,7 @@ def openai_chat_chunk_message_template( template["choices"][0]["delta"] = {"content": message} else: template["choices"][0]["finish_reason"] = "stop" + template["choices"][0]["delta"] = {} if usage: template["usage"] = usage diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 1ae6d4aa73..f60e2ff60e 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -35,7 +35,7 @@ from open_webui.config import ( AppConfig, ) from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES -from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE +from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook @@ -276,8 +276,13 @@ class OAuthManager: picture_url = "" if not picture_url: picture_url = "/user.png" + username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM + name = user_data.get(username_claim) + if not isinstance(user, str): + name = email + role = self.get_user_role(None, user_data) user = Auths.insert_new_auth( @@ -285,7 +290,7 @@ class OAuthManager: password=get_password_hash( str(uuid.uuid4()) ), # Random password, not used - name=user_data.get(username_claim, "User"), + name=name, profile_image_url=picture_url, role=role, oauth_sub=provider_sub, @@ -323,8 +328,8 @@ class OAuthManager: key="token", value=jwt_token, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) if ENABLE_OAUTH_SIGNUP.value: @@ -333,8 +338,8 @@ class OAuthManager: key="oauth_id_token", value=oauth_id_token, httponly=True, - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) # Redirect back to the frontend with the JWT token redirect_url = f"{request.base_url}auth#token={jwt_token}" diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py index 13f98ee019..2eb4622c2b 100644 --- a/backend/open_webui/utils/payload.py +++ b/backend/open_webui/utils/payload.py @@ -1,4 +1,4 @@ -from open_webui.utils.task import prompt_template +from open_webui.utils.task import prompt_variables_template from open_webui.utils.misc import ( add_or_update_system_message, ) @@ -7,19 +7,18 @@ from typing import Callable, Optional # inplace function: form_data is modified -def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict: +def apply_model_system_prompt_to_body( + params: dict, form_data: dict, metadata: Optional[dict] = None +) -> dict: system = params.get("system", None) if not system: return form_data - if user: - template_params = { - "user_name": user.name, - "user_location": user.info.get("location") if user.info else None, - } - else: - template_params = {} - system = prompt_template(system, **template_params) + if metadata: + print("apply_model_system_prompt_to_body: metadata", metadata) + variables = metadata.get("variables", {}) + system = prompt_variables_template(system, variables) + form_data["messages"] = add_or_update_system_message( system, form_data.get("messages", []) ) @@ -188,4 +187,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: if ollama_options: ollama_payload["options"] = ollama_options + if "metadata" in openai_payload: + ollama_payload["metadata"] = openai_payload["metadata"] + return ollama_payload diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 17b86cea17..d6e24d6b93 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -167,9 +167,14 @@ def load_function_module_by_id(function_id, content=None): def install_frontmatter_requirements(requirements): if requirements: - req_list = [req.strip() for req in requirements.split(",")] - for req in req_list: - log.info(f"Installing requirement: {req}") - subprocess.check_call([sys.executable, "-m", "pip", "install", req]) + try: + req_list = [req.strip() for req in requirements.split(",")] + for req in req_list: + log.info(f"Installing requirement: {req}") + subprocess.check_call([sys.executable, "-m", "pip", "install", req]) + except Exception as e: + log.error(f"Error installing package: {req}") + raise e + else: log.info("No requirements found in frontmatter.") diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py index d6f7b0ac64..f461f7cc23 100644 --- a/backend/open_webui/utils/response.py +++ b/backend/open_webui/utils/response.py @@ -9,7 +9,48 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict: model = ollama_response.get("model", "ollama") message_content = ollama_response.get("message", {}).get("content", "") - response = openai_chat_completion_message_template(model, message_content) + data = ollama_response + usage = { + "response_token/s": ( + round( + ( + ( + data.get("eval_count", 0) + / ((data.get("eval_duration", 0) / 10_000_000)) + ) + * 100 + ), + 2, + ) + if data.get("eval_duration", 0) > 0 + else "N/A" + ), + "prompt_token/s": ( + round( + ( + ( + data.get("prompt_eval_count", 0) + / ((data.get("prompt_eval_duration", 0) / 10_000_000)) + ) + * 100 + ), + 2, + ) + if data.get("prompt_eval_duration", 0) > 0 + else "N/A" + ), + "total_duration": data.get("total_duration", 0), + "load_duration": data.get("load_duration", 0), + "prompt_eval_count": data.get("prompt_eval_count", 0), + "prompt_eval_duration": data.get("prompt_eval_duration", 0), + "eval_count": data.get("eval_count", 0), + "eval_duration": data.get("eval_duration", 0), + "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")( + (data.get("total_duration", 0) or 0) // 1_000_000_000 + ), + } + + response = openai_chat_completion_message_template(model, message_content, usage) return response diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index f5ba75ebec..3d8c05d455 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -32,6 +32,12 @@ def get_task_model_id( return task_model_id +def prompt_variables_template(template: str, variables: dict[str, str]) -> str: + for variable, value in variables.items(): + template = template.replace(variable, value) + return template + + def prompt_template( template: str, user_name: Optional[str] = None, user_location: Optional[str] = None ) -> str: diff --git a/backend/requirements.txt b/backend/requirements.txt index 0dd7b1a8ad..bb124bf118 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.111.0 +fastapi==0.115.7 uvicorn[standard]==0.30.6 pydantic==2.9.2 python-multipart==0.0.18 diff --git a/pyproject.toml b/pyproject.toml index c8ec0f497c..41c79ddb83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] license = { file = "LICENSE" } dependencies = [ - "fastapi==0.111.0", + "fastapi==0.115.7", "uvicorn[standard]==0.30.6", "pydantic==2.9.2", "python-multipart==0.0.18", diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 1772529d39..7af504cc78 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -580,7 +580,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => { return res; }; -export const cloneChatById = async (token: string, id: string) => { +export const cloneChatById = async (token: string, id: string, title?: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, { @@ -589,7 +589,10 @@ export const cloneChatById = async (token: string, id: string) => { Accept: 'application/json', 'Content-Type': 'application/json', ...(token && { authorization: `Bearer ${token}` }) - } + }, + body: JSON.stringify({ + ...(title && { title: title }) + }) }) .then(async (res) => { if (!res.ok) throw await res.json(); diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts index 5880874bbf..9cf625d035 100644 --- a/src/lib/apis/models/index.ts +++ b/src/lib/apis/models/index.ts @@ -219,7 +219,7 @@ export const deleteModelById = async (token: string, id: string) => { return json; }) .catch((err) => { - error = err; + error = err.detail; console.log(err); return null; diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index a7a0300276..f3b7e982c0 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -51,7 +51,7 @@ models = []; } else { const res = await _getModels(localStorage.token).catch((e) => { - toast.error(e); + toast.error(`${e}`); }); if (res) { @@ -74,7 +74,7 @@ }, 100); } else { const res = await _getVoices(localStorage.token).catch((e) => { - toast.error(e); + toast.error(`${e}`); }); if (res) { diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index 2ee4297a46..ce730571f0 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -6,7 +6,9 @@ import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); + dayjs.extend(localizedFormat); import { toast } from 'svelte-sonner'; @@ -364,7 +366,7 @@ - {dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + {dayjs(user.created_at * 1000).format('LL')} {user.oauth_sub ?? ''} diff --git a/src/lib/components/admin/Users/UserList/EditUserModal.svelte b/src/lib/components/admin/Users/UserList/EditUserModal.svelte index 868951d57a..9b2edb4073 100644 --- a/src/lib/components/admin/Users/UserList/EditUserModal.svelte +++ b/src/lib/components/admin/Users/UserList/EditUserModal.svelte @@ -7,9 +7,11 @@ import { updateUserById } from '$lib/apis/users'; import Modal from '$lib/components/common/Modal.svelte'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; const i18n = getContext('i18n'); const dispatch = createEventDispatcher(); + dayjs.extend(localizedFormat); export let show = false; export let selectedUser; @@ -87,7 +89,7 @@
{$i18n.t('Created at')} - {dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + {dayjs(selectedUser.created_at * 1000).format('LL')}
diff --git a/src/lib/components/admin/Users/UserList/UserChatsModal.svelte b/src/lib/components/admin/Users/UserList/UserChatsModal.svelte index 92970b658a..a95df82917 100644 --- a/src/lib/components/admin/Users/UserList/UserChatsModal.svelte +++ b/src/lib/components/admin/Users/UserList/UserChatsModal.svelte @@ -2,8 +2,10 @@ import { toast } from 'svelte-sonner'; import dayjs from 'dayjs'; import { getContext, createEventDispatcher } from 'svelte'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; const dispatch = createEventDispatcher(); + dayjs.extend(localizedFormat); import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats'; @@ -130,7 +132,7 @@
- {dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} + {dayjs(chat.updated_at * 1000).format('LLL')}
diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index 623649cbf5..c599de5e2b 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -200,7 +200,7 @@ files = files.filter((item) => item?.itemId !== tempItemId); } } catch (e) { - toast.error(e); + toast.error(`${e}`); files = files.filter((item) => item?.itemId !== tempItemId); } }; diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index ef17feb7f6..a84077823b 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -3,10 +3,12 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import isToday from 'dayjs/plugin/isToday'; import isYesterday from 'dayjs/plugin/isYesterday'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); dayjs.extend(isToday); dayjs.extend(isYesterday); + dayjs.extend(localizedFormat); import { getContext, onMount } from 'svelte'; const i18n = getContext>('i18n'); @@ -154,9 +156,9 @@ class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize" > - {dayjs(message.created_at / 1000000).format('HH:mm')} + {dayjs(message.created_at / 1000000).format('LT')} {/if} @@ -175,7 +177,7 @@ class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]" > {formatDate(message.created_at / 1000000)} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 331806a96b..3cfb618806 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -45,7 +45,8 @@ promptTemplate, splitStream, sleep, - removeDetailsWithReasoning + removeDetailsWithReasoning, + getPromptVariables } from '$lib/utils'; import { generateChatCompletion } from '$lib/apis/ollama'; @@ -82,10 +83,12 @@ import EventConfirmDialog from '../common/ConfirmDialog.svelte'; import Placeholder from './Placeholder.svelte'; import NotificationToast from '../NotificationToast.svelte'; + import Spinner from '../common/Spinner.svelte'; export let chatIdProp = ''; - let loaded = false; + let loading = false; + const eventTarget = new EventTarget(); let controlPane; let controlPaneComponent; @@ -133,6 +136,7 @@ $: if (chatIdProp) { (async () => { + loading = true; console.log(chatIdProp); prompt = ''; @@ -141,11 +145,9 @@ webSearchEnabled = false; imageGenerationEnabled = false; - loaded = false; - if (chatIdProp && (await loadChat())) { await tick(); - loaded = true; + loading = false; if (localStorage.getItem(`chat-input-${chatIdProp}`)) { try { @@ -627,7 +629,7 @@ } catch (e) { // Remove the failed doc from the files array files = files.filter((f) => f.name !== url); - toast.error(e); + toast.error(`${e}`); } }; @@ -1557,10 +1559,17 @@ files: (files?.length ?? 0) > 0 ? files : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, + features: { image_generation: imageGenerationEnabled, web_search: webSearchEnabled }, + variables: { + ...getPromptVariables( + $user.name, + $settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined + ) + }, session_id: $socket?.id, chat_id: $chatId, @@ -1861,7 +1870,7 @@ : ' '} w-full max-w-full flex flex-col" id="chat-container" > - {#if !chatIdProp || (loaded && chatIdProp)} + {#if chatIdProp === '' || (!loading && chatIdProp)} {#if $settings?.backgroundImageUrl ?? null}
+
+ +
+
{/if} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 734177df02..69895f513a 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -211,7 +211,7 @@ files = files.filter((item) => item?.itemId !== tempItemId); } } catch (e) { - toast.error(e); + toast.error(`${e}`); files = files.filter((item) => item?.itemId !== tempItemId); } }; diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 20b297eb3d..3c8eaf008f 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -48,6 +48,9 @@ init(); } + let fileUploadEnabled = true; + $: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload; + const init = async () => { if ($_tools === null) { await _tools.set(await getTools(localStorage.token)); @@ -166,26 +169,44 @@ {/if} {#if !$mobile} - { - screenCaptureHandler(); - }} + - -
{$i18n.t('Capture')}
-
+ { + if (fileUploadEnabled) { + screenCaptureHandler(); + } + }} + > + +
{$i18n.t('Capture')}
+
+ {/if} - { - uploadFilesHandler(); - }} + - -
{$i18n.t('Upload Files')}
-
+ { + if (fileUploadEnabled) { + uploadFilesHandler(); + } + }} + > + +
{$i18n.t('Upload Files')}
+
+ {#if $config?.features?.enable_google_drive_integration}