Skip to content
Merged

Dev #256

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG_EXTRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions backend/open_webui/routers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
):
Expand Down
6 changes: 4 additions & 2 deletions backend/open_webui/routers/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 25 additions & 22 deletions backend/open_webui/routers/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 10 additions & 8 deletions backend/open_webui/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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


Expand Down
2 changes: 2 additions & 0 deletions backend/open_webui/utils/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
)

from typing import Callable, Optional
import copy
import json


Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion backend/open_webui/utils/telemetry/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
]

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 1 addition & 5 deletions src/lib/components/admin/Evaluations/Feedbacks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,7 @@
}
};

$: if (page) {
getFeedbacks();
}

$: if (orderBy && direction) {
$: if (orderBy && direction && page) {
getFeedbacks();
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/admin/Evaluations/Leaderboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
13 changes: 13 additions & 0 deletions src/lib/components/admin/Settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,19 @@
/>
</div>

<!-- {$i18n.t('General')} -->
<!-- {$i18n.t('Connections')} -->
<!-- {$i18n.t('Models')} -->
<!-- {$i18n.t('Evaluations')} -->
<!-- {$i18n.t('External Tools')} -->
<!-- {$i18n.t('Documents')} -->
<!-- {$i18n.t('Web Search')} -->
<!-- {$i18n.t('Code Execution')} -->
<!-- {$i18n.t('Interface')} -->
<!-- {$i18n.t('Audio')} -->
<!-- {$i18n.t('Images')} -->
<!-- {$i18n.t('Pipelines')} -->
<!-- {$i18n.t('Database')} -->
{#each filteredSettings as tab (tab.id)}
<button
id={tab.id}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/layout/Sidebar/ChannelItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
<div class="flex items-center">
{#if channel?.unread_count > 0}
<div
class="text-xs py-[1px] px-2 rounded-xl bg-gray-100 text-black dark:bg-gray-800 dark:text-white font-medium"
class="text-xs py-[1px] px-2 rounded-xl bg-gray-100 text-black dark:bg-gray-800 dark:text-white font-medium whitespace-nowrap"
>
{new Intl.NumberFormat($i18n.locale, {
notation: 'compact',
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/workspace/Prompts/PromptEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
export let edit = false;
export let prompt = null;
export let clone = false;
export let disabled = true;
export let disabled = false;

const i18n = getContext('i18n');

Expand Down
3 changes: 3 additions & 0 deletions src/lib/i18n/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@
"Document Intelligence endpoint required.": "",
"Document Intelligence Model": "",
"Documentation": "",
"Documents": "",
"does not make any external connections, and your data stays securely on your locally hosted server.": "",
"Domain Filter List": "",
"don't fetch random pipelines from sources you don't trust.": "",
Expand Down Expand Up @@ -985,6 +986,7 @@
"Image Prompt Generation": "",
"Image Prompt Generation Prompt": "",
"Image Size": "",
"Images": "",
"Import": "",
"Import Chats": "",
"Import Config from JSON File": "",
Expand Down Expand Up @@ -1388,6 +1390,7 @@
"Pipe": "",
"Pipeline deleted successfully": "",
"Pipeline downloaded successfully": "",
"Pipelines": "",
"Pipelines are a plugin system with arbitrary code execution —": "",
"Pipelines Not Detected": "",
"Pipelines Valves": "",
Expand Down
3 changes: 3 additions & 0 deletions src/lib/i18n/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@
"Document Intelligence endpoint required.": "Document Intelligence 接口地址是必填项。",
"Document Intelligence Model": "Document Intelligence 模型",
"Documentation": "帮助文档",
"Documents": "",
"does not make any external connections, and your data stays securely on your locally hosted server.": "不会与外部建立任何连接,您的数据会安全地存储在本地托管的服务器上。",
"Domain Filter List": "域名过滤列表",
"don't fetch random pipelines from sources you don't trust.": "请勿从未经验证或不可信的来源获取 Pipelines。",
Expand Down Expand Up @@ -984,6 +985,7 @@
"Image Prompt Generation": "图像提示词生成",
"Image Prompt Generation Prompt": "用于生成图像提示词的提示词",
"Image Size": "图片尺寸",
"Images": "",
"Import": "导入",
"Import Chats": "导入对话记录",
"Import Config from JSON File": "从 JSON 文件中导入配置信息",
Expand Down Expand Up @@ -1387,6 +1389,7 @@
"Pipe": "Pipe",
"Pipeline deleted successfully": "Pipeline 删除成功",
"Pipeline downloaded successfully": "Pipeline 下载成功",
"Pipelines": "",
"Pipelines are a plugin system with arbitrary code execution —": "Pipelines 是具有任意代码执行风险的插件系统 —",
"Pipelines Not Detected": "未检测到 Pipeline",
"Pipelines Valves": "Pipeline 配置项",
Expand Down
3 changes: 3 additions & 0 deletions src/lib/i18n/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@
"Document Intelligence endpoint required.": "需要提供 Document Intelligence 端點。",
"Document Intelligence Model": "Document Intelligence 模型",
"Documentation": "說明檔案",
"Documents": "",
"does not make any external connections, and your data stays securely on your locally hosted server.": "不會建立任何外部連線,而且您的資料會安全地儲存在您本機伺服器上。",
"Domain Filter List": "網域篩選列表",
"don't fetch random pipelines from sources you don't trust.": "請勿從您無法信任的來源擷取任意管線。",
Expand Down Expand Up @@ -984,6 +985,7 @@
"Image Prompt Generation": "圖片提示詞生成",
"Image Prompt Generation Prompt": "生成圖片提示詞的提示詞",
"Image Size": "圖片尺寸",
"Images": "",
"Import": "匯入",
"Import Chats": "匯入對話紀錄",
"Import Config from JSON File": "從 JSON 檔案匯入設定",
Expand Down Expand Up @@ -1387,6 +1389,7 @@
"Pipe": "Pipe",
"Pipeline deleted successfully": "成功刪除管線",
"Pipeline downloaded successfully": "成功下載管線",
"Pipelines": "",
"Pipelines are a plugin system with arbitrary code execution —": "管線是具任意程式碼執行風險的外掛系統 —",
"Pipelines Not Detected": "未偵測到管線",
"Pipelines Valves": "管線設定項目",
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/workspace/prompts/edit/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';

let prompt = null;
let disabled = true;
let disabled = false;

const onSubmit = async (_prompt) => {
console.log(_prompt);
Expand Down
Loading