diff --git a/CHANGELOG.md b/CHANGELOG.md index b63e1e4e21..572ac40257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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.7.2] - 2026-01-10 + +### Fixed + +- ⚡ Users no longer experience database connection timeouts under high concurrency due to connections being held during LLM calls, telemetry collection, and file status streaming. [#20545](https://github.com/open-webui/open-webui/pull/20545), [#20542](https://github.com/open-webui/open-webui/pull/20542), [#20547](https://github.com/open-webui/open-webui/pull/20547) +- 📝 Users can now create and save prompts in the workspace prompts editor without encountering errors. [Commit](https://github.com/open-webui/open-webui/commit/ab99d3b1129cffbc13cf7de5aa897692e3f8662e) +- 🎙️ Users can now use local Whisper for speech-to-text when STT_ENGINE is left empty (the default for local mode). [#20534](https://github.com/open-webui/open-webui/pull/20534) +- 📊 The Evaluations page now loads faster by eliminating duplicate API calls to the leaderboard and feedbacks endpoints. [Commit](https://github.com/open-webui/open-webui/commit/2dd09223f2aac301a4d5c17fb667d974c34f3ff1) +- 🌐 Fixed missing Settings tab i18n label keys. [#20526](https://github.com/open-webui/open-webui/pull/20526) + ## [0.7.1] - 2026-01-09 ### Fixed diff --git a/CHANGELOG_EXTRA.md b/CHANGELOG_EXTRA.md index a0c1f25a93..9d0d8289f0 100644 --- a/CHANGELOG_EXTRA.md +++ b/CHANGELOG_EXTRA.md @@ -5,6 +5,12 @@ 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.7.2.1] - 2026.01.11 + +### Changed + +- 合并官方 0.7.2 改动 + ## [0.7.1.1] - 2026.01.10 ### Changed diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 2a64a3598d..52e0182cad 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -334,8 +334,8 @@ def load_speech_pipeline(request): async def speech(request: Request, user=Depends(get_verified_user)): if request.app.state.config.TTS_ENGINE == "": raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, ) if user.role != "admin" and not has_permission( @@ -1169,12 +1169,6 @@ def transcription( language: Optional[str] = Form(None), user=Depends(get_verified_user), ): - if request.app.state.config.STT_ENGINE == "": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - if user.role != "admin" and not has_permission( user.id, "chat.stt", request.app.state.config.USER_PERMISSIONS ): diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 777f5e74ea..4e697142bf 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1256,15 +1256,17 @@ async def post_new_message( active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") + # NOTE: We intentionally do NOT pass db to background_handler. + # Background tasks should manage their own short-lived sessions to avoid + # holding database connections during slow operations (e.g., LLM calls). async def background_handler(): - await model_response_handler(request, channel, message, user, db) + await model_response_handler(request, channel, message, user) await send_notification( request.app.state.WEBUI_NAME, request.app.state.config.WEBUI_URL, channel, message, active_user_ids, - db=db, ) background_tasks.add_task(background_handler) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index d0e56075eb..e3dd63525a 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -495,32 +495,35 @@ async def get_file_process_status( if stream: MAX_FILE_PROCESSING_DURATION = 3600 * 2 - async def event_stream(file_item): - if file_item: - for _ in range(MAX_FILE_PROCESSING_DURATION): - file_item = Files.get_file_by_id(file_item.id, db=db) - if file_item: - data = file_item.model_dump().get("data", {}) - status = data.get("status") - - if status: - event = {"status": status} - if status == "failed": - event["error"] = data.get("error") - - yield f"data: {json.dumps(event)}\n\n" - if status in ("completed", "failed"): - break - else: - # Legacy + async def event_stream(file_id): + # NOTE: We intentionally do NOT capture the request's db session here. + # Each poll creates its own short-lived session to avoid holding a + # connection for hours. A WebSocket push would be more efficient. + for _ in range(MAX_FILE_PROCESSING_DURATION): + file_item = Files.get_file_by_id(file_id) # Creates own session + if file_item: + data = file_item.model_dump().get("data", {}) + status = data.get("status") + + if status: + event = {"status": status} + if status == "failed": + event["error"] = data.get("error") + + yield f"data: {json.dumps(event)}\n\n" + if status in ("completed", "failed"): break + else: + # Legacy + break + else: + yield f"data: {json.dumps({'status': 'not_found'})}\n\n" + break - await asyncio.sleep(0.5) - else: - yield f"data: {json.dumps({'status': 'not_found'})}\n\n" + await asyncio.sleep(1) return StreamingResponse( - event_stream(file), + event_stream(file.id), media_type="text/event-stream", ) else: diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 7c683c3706..87ebb19b63 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -53,8 +53,6 @@ from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from sqlalchemy.orm import Session -from open_webui.internal.db import get_session from open_webui.utils.redis import get_redis_connection, get_sentinels_from_env @@ -297,7 +295,10 @@ async def get_current_user( response: Response, background_tasks: BackgroundTasks, auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), - db: Session = Depends(get_session), + # NOTE: We intentionally do NOT use Depends(get_session) here. + # Sessions are managed internally with short-lived context managers. + # This ensures connections are released immediately after auth queries, + # not held for the entire request duration (e.g., during 30+ second LLM calls). ): token = None @@ -312,7 +313,7 @@ async def get_current_user( # auth by api key if token.startswith("sk-"): - user = get_current_user_by_api_key(request, token, db=db) + user = get_current_user_by_api_key(request, token) # Add user info to current span current_span = trace.get_current_span() @@ -341,7 +342,7 @@ async def get_current_user( detail="Invalid token", ) - user = Users.get_user_by_id(data["id"], db=db) + user = Users.get_user_by_id(data["id"]) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -391,8 +392,9 @@ async def get_current_user( raise e -def get_current_user_by_api_key(request, api_key: str, db: Session = None): - user = Users.get_user_by_api_key(api_key, db=db) +def get_current_user_by_api_key(request, api_key: str): + # Each function call manages its own short-lived session internally + user = Users.get_user_by_api_key(api_key) if user is None: raise HTTPException( @@ -420,7 +422,7 @@ def get_current_user_by_api_key(request, api_key: str, db: Session = None): current_span.set_attribute("client.user.role", user.role) current_span.set_attribute("client.auth.type", "api_key") - Users.update_last_active_by_id(user.id, db=db) + Users.update_last_active_by_id(user.id) return user diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py index bf372e0e76..458687b371 100644 --- a/backend/open_webui/utils/payload.py +++ b/backend/open_webui/utils/payload.py @@ -6,6 +6,7 @@ ) from typing import Callable, Optional +import copy import json @@ -286,6 +287,7 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: Returns: dict: A modified payload compatible with the Ollama API. """ + openai_payload = copy.deepcopy(openai_payload) ollama_payload = {} # Mapping basic model and message details diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py index d935ddaafa..f129f5f002 100644 --- a/backend/open_webui/utils/telemetry/metrics.py +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -141,9 +141,12 @@ def observe_active_users( def observe_total_registered_users( options: metrics.CallbackOptions, ) -> Sequence[metrics.Observation]: + # IMPORTANT: Use get_num_users() for efficient COUNT(*) query. + # Do NOT use len(get_users()["users"]) - it loads ALL user records into memory, + # causing connection pool exhaustion on high-latency databases (e.g., Aurora). return [ metrics.Observation( - value=len(Users.get_users()["users"]), + value=Users.get_num_users() or 0, ) ] diff --git a/package-lock.json b/package-lock.json index 6cfdfc6153..1d4fb24caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.7.1.1", + "version": "0.7.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.7.1.1", + "version": "0.7.2.1", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index ca8b67fe31..de70d1ccbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.7.1.1", + "version": "0.7.2.1", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index 54a17682a6..0ec5678f0e 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -80,11 +80,7 @@ } }; - $: if (page) { - getFeedbacks(); - } - - $: if (orderBy && direction) { + $: if (orderBy && direction && page) { getFeedbacks(); } diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index 837400e007..e16e62c98c 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -79,9 +79,9 @@ debounceTimer = setTimeout(() => loadLeaderboard(query), 500); }; - $: query, debouncedLoad(); - - onMount(() => loadLeaderboard()); + $: if (query !== null) { + debouncedLoad(); + } $: sortedModels = [...rankedModels].sort((a, b) => { const getValue = (m, key) => { diff --git a/src/lib/components/admin/Settings.svelte b/src/lib/components/admin/Settings.svelte index 613a16d76f..648caef298 100644 --- a/src/lib/components/admin/Settings.svelte +++ b/src/lib/components/admin/Settings.svelte @@ -315,6 +315,19 @@ /> + + + + + + + + + + + + + {#each filteredSettings as tab (tab.id)}