Skip to content
Merged

Dev #226

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3b23b96
Update translation.json (pt-BR)
joaoback Nov 24, 2025
e567f42
Merge pull request #19428 from joaoback/patch-16
tjbck Nov 24, 2025
a7b611c
fix: "No connection adapters were found" routers/images.py (#19435)
proAlexandr Nov 24, 2025
0a68798
Update knowledge.py (#19434)
Classic298 Nov 24, 2025
f0d75e3
refac/fix: db operations
tjbck Nov 24, 2025
b875a43
Update translation.json (#19445)
Classic298 Nov 24, 2025
9c19d0a
refac/breaking: docling params
tjbck Nov 24, 2025
38c6b0b
fix: inline citations
tjbck Nov 25, 2025
b1c1e68
refac/fix: group member user list
tjbck Nov 25, 2025
2328dc2
feat/enh: async embedding processing setting
tjbck Nov 25, 2025
488631d
refac
tjbck Nov 25, 2025
743199f
feat/enh: tool server function name filter list
tjbck Nov 25, 2025
f0c7bd3
refac
tjbck Nov 25, 2025
a7ee362
refac: styling
tjbck Nov 25, 2025
3b5710d
feat/enh: show user count in channels
tjbck Nov 25, 2025
f2ee70c
fix: ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION env var
tjbck Nov 25, 2025
baa1e07
refac
tjbck Nov 25, 2025
c0e1203
feat: user list in channels
tjbck Nov 25, 2025
e6d8f89
chore: version bump
tjbck Nov 25, 2025
84ca225
refac
tjbck Nov 25, 2025
f22d92e
refac: styling
tjbck Nov 25, 2025
6a09509
chore: add chardet (#19458)
Classic298 Nov 25, 2025
63ca0a3
refac
tjbck Nov 25, 2025
6235243
refac
tjbck Nov 25, 2025
d5c3e9e
refac
tjbck Nov 25, 2025
0fa97bd
fix: i18n
tjbck Nov 25, 2025
4847bdc
chore: format
tjbck Nov 25, 2025
82a5f11
CHANGELOG: 0.6.39 (#19446)
Classic298 Nov 25, 2025
03dc4d7
refac/enh: copy formatted table
tjbck Nov 25, 2025
3fa484f
doc: changelog
tjbck Nov 25, 2025
9899293
Merge pull request #19448 from open-webui/dev
tjbck Nov 25, 2025
97ba5b8
fix: changelog
tjbck Nov 25, 2025
35ab6b7
fix: postgres user list issue
tjbck Nov 25, 2025
33a5262
chore: bump
tjbck Nov 25, 2025
363ef19
chore: bump python-socketio==5.14.0
tjbck Nov 25, 2025
15c6860
Update CHANGELOG.md (#19463)
Classic298 Nov 25, 2025
f354756
refac: channel user list order by
tjbck Nov 25, 2025
140605e
Merge pull request #19462 from open-webui/dev
tjbck Nov 25, 2025
53168f8
Merge remote-tracking branch 'oui/main' into dev
OrenZhang Nov 26, 2025
6643fcd
chore(repo): merge from remote
OrenZhang Nov 26, 2025
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ 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.6.40] - 2025-11-25

### Fixed

- 🗄️ A critical PostgreSQL user listing performance issue was resolved by removing a redundant count operation that caused severe database slowdowns and potential timeouts when viewing user lists in admin panels.

## [0.6.39] - 2025-11-25

### Added

- 💬 A user list modal was added to channels, displaying all users with access and featuring search, sorting, and pagination capabilities. [Commit](https://github.com/open-webui/open-webui/commit/c0e120353824be00a2ef63cbde8be5d625bd6fd0)
- 💬 Channel navigation now displays the total number of users with access to the channel. [Commit](https://github.com/open-webui/open-webui/commit/3b5710d0cd445cf86423187f5ee7c40472a0df0b)
- 🔌 Tool servers and MCP connections now support function name filtering, allowing administrators to selectively enable or block specific functions using allow/block lists. [Commit](https://github.com/open-webui/open-webui/commit/743199f2d097ae1458381bce450d9025a0ab3f3d)
- ⚡ A toggle to disable parallel embedding processing was added via "ENABLE_ASYNC_EMBEDDING", allowing sequential processing for rate-limited or resource-constrained local embedding setups. [#19444](https://github.com/open-webui/open-webui/pull/19444)
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
- 🌐 Localization improvements were made for German (de-DE) and Portuguese (Brazil) translations.

### Fixed

- 📝 Inline citations now render correctly within markdown lists and nested elements instead of displaying as "undefined" values. [#19452](https://github.com/open-webui/open-webui/issues/19452)
- 👥 Group member selection now works correctly without randomly selecting other users or causing the user list to jump around. [#19426](https://github.com/open-webui/open-webui/issues/19426)
- 👥 Admin panel user list now displays the correct total user count and properly paginates 30 items per page after fixing database query issues with group member joins. [#19429](https://github.com/open-webui/open-webui/issues/19429)
- 🔍 Knowledge base reindexing now works correctly after resolving async execution chain issues by implementing threadpool workers for embedding operations. [#19434](https://github.com/open-webui/open-webui/pull/19434)
- 🖼️ OpenAI image generation now works correctly after fixing a connection adapter error caused by incorrect URL formatting. [#19435](https://github.com/open-webui/open-webui/pull/19435)

### Changed

- 🔧 BREAKING: Docling configuration has been consolidated from individual environment variables into a single "DOCLING_PARAMS" JSON configuration and now supports API key authentication via "DOCLING_API_KEY", requiring users to migrate existing Docling settings to the new format. [#16841](https://github.com/open-webui/open-webui/issues/16841), [#19427](https://github.com/open-webui/open-webui/pull/19427)
- 🔧 The environment variable "REPLACE_IMAGE_URLS_IN_CHAT_RESPONSE" has been renamed to "ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION" for naming consistency.

## [0.6.38] - 2025-11-24

### 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.6.40.1] - 2025.11.26

### Changed

- 合并官方 0.6.40 改动

## [0.6.38.1] - 2025.11.24

### Changed
Expand Down
90 changes: 12 additions & 78 deletions backend/open_webui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,12 @@ class BannerModel(BaseModel):
os.getenv("DOCLING_SERVER_URL", "http://docling:5001"),
)

DOCLING_API_KEY = PersistentConfig(
"DOCLING_API_KEY",
"rag.docling_api_key",
os.getenv("DOCLING_API_KEY", ""),
)

docling_params = os.getenv("DOCLING_PARAMS", "")
try:
docling_params = json.loads(docling_params)
Expand All @@ -2571,84 +2577,6 @@ class BannerModel(BaseModel):
docling_params,
)

DOCLING_DO_OCR = PersistentConfig(
"DOCLING_DO_OCR",
"rag.docling_do_ocr",
os.getenv("DOCLING_DO_OCR", "True").lower() == "true",
)

DOCLING_FORCE_OCR = PersistentConfig(
"DOCLING_FORCE_OCR",
"rag.docling_force_ocr",
os.getenv("DOCLING_FORCE_OCR", "False").lower() == "true",
)

DOCLING_OCR_ENGINE = PersistentConfig(
"DOCLING_OCR_ENGINE",
"rag.docling_ocr_engine",
os.getenv("DOCLING_OCR_ENGINE", "tesseract"),
)

DOCLING_OCR_LANG = PersistentConfig(
"DOCLING_OCR_LANG",
"rag.docling_ocr_lang",
os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"),
)

DOCLING_PDF_BACKEND = PersistentConfig(
"DOCLING_PDF_BACKEND",
"rag.docling_pdf_backend",
os.getenv("DOCLING_PDF_BACKEND", "dlparse_v4"),
)

DOCLING_TABLE_MODE = PersistentConfig(
"DOCLING_TABLE_MODE",
"rag.docling_table_mode",
os.getenv("DOCLING_TABLE_MODE", "accurate"),
)

DOCLING_PIPELINE = PersistentConfig(
"DOCLING_PIPELINE",
"rag.docling_pipeline",
os.getenv("DOCLING_PIPELINE", "standard"),
)

DOCLING_DO_PICTURE_DESCRIPTION = PersistentConfig(
"DOCLING_DO_PICTURE_DESCRIPTION",
"rag.docling_do_picture_description",
os.getenv("DOCLING_DO_PICTURE_DESCRIPTION", "False").lower() == "true",
)

DOCLING_PICTURE_DESCRIPTION_MODE = PersistentConfig(
"DOCLING_PICTURE_DESCRIPTION_MODE",
"rag.docling_picture_description_mode",
os.getenv("DOCLING_PICTURE_DESCRIPTION_MODE", ""),
)

docling_picture_description_local = os.getenv("DOCLING_PICTURE_DESCRIPTION_LOCAL", "")
try:
docling_picture_description_local = json.loads(docling_picture_description_local)
except json.JSONDecodeError:
docling_picture_description_local = {}

DOCLING_PICTURE_DESCRIPTION_LOCAL = PersistentConfig(
"DOCLING_PICTURE_DESCRIPTION_LOCAL",
"rag.docling_picture_description_local",
docling_picture_description_local,
)

docling_picture_description_api = os.getenv("DOCLING_PICTURE_DESCRIPTION_API", "")
try:
docling_picture_description_api = json.loads(docling_picture_description_api)
except json.JSONDecodeError:
docling_picture_description_api = {}

DOCLING_PICTURE_DESCRIPTION_API = PersistentConfig(
"DOCLING_PICTURE_DESCRIPTION_API",
"rag.docling_picture_description_api",
docling_picture_description_api,
)

DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
"DOCUMENT_INTELLIGENCE_ENDPOINT",
"rag.document_intelligence_endpoint",
Expand Down Expand Up @@ -2804,6 +2732,12 @@ class BannerModel(BaseModel):
),
)

ENABLE_ASYNC_EMBEDDING = PersistentConfig(
"ENABLE_ASYNC_EMBEDDING",
"rag.enable_async_embedding",
os.environ.get("ENABLE_ASYNC_EMBEDDING", "True").lower() == "true",
)

RAG_EMBEDDING_QUERY_PREFIX = os.environ.get("RAG_EMBEDDING_QUERY_PREFIX", None)

RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get("RAG_EMBEDDING_CONTENT_PREFIX", None)
Expand Down
3 changes: 2 additions & 1 deletion backend/open_webui/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,8 @@ def parse_section(section):
####################################

ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = (
os.environ.get("REPLACE_IMAGE_URLS_IN_CHAT_RESPONSE", "False").lower() == "true"
os.environ.get("ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION", "False").lower()
== "true"
)

CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get(
Expand Down
26 changes: 4 additions & 22 deletions backend/open_webui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_BATCH_SIZE,
ENABLE_ASYNC_EMBEDDING,
RAG_TOP_K,
RAG_TOP_K_RERANKER,
RAG_RELEVANCE_THRESHOLD,
Expand Down Expand Up @@ -258,18 +259,8 @@
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL,
DOCLING_SERVER_URL,
DOCLING_API_KEY,
DOCLING_PARAMS,
DOCLING_DO_OCR,
DOCLING_FORCE_OCR,
DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG,
DOCLING_PDF_BACKEND,
DOCLING_TABLE_MODE,
DOCLING_PIPELINE,
DOCLING_DO_PICTURE_DESCRIPTION,
DOCLING_PICTURE_DESCRIPTION_MODE,
DOCLING_PICTURE_DESCRIPTION_LOCAL,
DOCLING_PICTURE_DESCRIPTION_API,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_BASE_URL,
Expand Down Expand Up @@ -897,18 +888,8 @@ async def lifespan(app: FastAPI):
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_API_KEY = DOCLING_API_KEY
app.state.config.DOCLING_PARAMS = DOCLING_PARAMS
app.state.config.DOCLING_DO_OCR = DOCLING_DO_OCR
app.state.config.DOCLING_FORCE_OCR = DOCLING_FORCE_OCR
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
app.state.config.DOCLING_PDF_BACKEND = DOCLING_PDF_BACKEND
app.state.config.DOCLING_TABLE_MODE = DOCLING_TABLE_MODE
app.state.config.DOCLING_PIPELINE = DOCLING_PIPELINE
app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE = DOCLING_PICTURE_DESCRIPTION_MODE
app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION_LOCAL
app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_API
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL
Expand All @@ -927,6 +908,7 @@ async def lifespan(app: FastAPI):
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING

app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
Expand Down
1 change: 1 addition & 0 deletions backend/open_webui/models/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ChannelModel(BaseModel):

class ChannelResponse(ChannelModel):
write_access: bool = False
user_count: Optional[int] = None


class ChannelForm(BaseModel):
Expand Down
17 changes: 17 additions & 0 deletions backend/open_webui/models/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,23 @@ def get_group_user_ids_by_id(self, id: str) -> Optional[list[str]]:

return [m[0] for m in members]

def get_group_user_ids_by_ids(self, group_ids: list[str]) -> dict[str, list[str]]:
with get_db() as db:
members = (
db.query(GroupMember.group_id, GroupMember.user_id)
.filter(GroupMember.group_id.in_(group_ids))
.all()
)

group_user_ids: dict[str, list[str]] = {
group_id: [] for group_id in group_ids
}

for group_id, user_id in members:
group_user_ids[group_id].append(user_id)

return group_user_ids

def set_group_user_ids_by_id(self, group_id: str, user_ids: list[str]) -> None:
with get_db() as db:
# Delete existing members
Expand Down
11 changes: 8 additions & 3 deletions backend/open_webui/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,15 @@ def search_models(

models = []
for model, user in items:
model_model = ModelModel.model_validate(model)
user_model = UserResponse(**UserModel.model_validate(user).model_dump())
models.append(
ModelUserResponse(**model_model.model_dump(), user=user_model)
ModelUserResponse(
**ModelModel.model_validate(model).model_dump(),
user=(
UserResponse(**UserModel.model_validate(user).model_dump())
if user
else None
),
)
)

return ModelListResponse(items=models, total=total)
Expand Down
72 changes: 61 additions & 11 deletions backend/open_webui/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@


from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text, Date
from sqlalchemy import or_
from sqlalchemy import BigInteger, Column, String, Text, Date, exists, select
from sqlalchemy import or_, case

import datetime

Expand Down Expand Up @@ -99,7 +99,16 @@ class UserGroupIdsModel(UserModel):
group_ids: list[str] = []


class UserModelResponse(UserModel):
model_config = ConfigDict(extra="allow")


class UserListResponse(BaseModel):
users: list[UserModelResponse]
total: int


class UserGroupIdsListResponse(BaseModel):
users: list[UserGroupIdsModel]
total: int

Expand Down Expand Up @@ -233,9 +242,7 @@ def get_users(
) -> dict:
with get_db() as db:
# Join GroupMember so we can order by group_id when requested
query = db.query(User).outerjoin(
GroupMember, GroupMember.user_id == User.id
)
query = db.query(User)

if filter:
query_key = filter.get("query")
Expand All @@ -247,23 +254,65 @@ def get_users(
)
)

user_ids = filter.get("user_ids")
group_ids = filter.get("group_ids")

if isinstance(user_ids, list) and isinstance(group_ids, list):
# If both are empty lists, return no users
if not user_ids and not group_ids:
return {"users": [], "total": 0}

if user_ids:
query = query.filter(User.id.in_(user_ids))

if group_ids:
query = query.filter(
exists(
select(GroupMember.id).where(
GroupMember.user_id == User.id,
GroupMember.group_id.in_(group_ids),
)
)
)

roles = filter.get("roles")
if roles:
include_roles = [role for role in roles if not role.startswith("!")]
exclude_roles = [role[1:] for role in roles if role.startswith("!")]

if include_roles:
query = query.filter(User.role.in_(include_roles))
if exclude_roles:
query = query.filter(~User.role.in_(exclude_roles))

order_by = filter.get("order_by")
direction = filter.get("direction")

if order_by and order_by.startswith("group_id:"):
group_id = order_by.split(":", 1)[1]

# Subquery that checks if the user belongs to the group
membership_exists = exists(
select(GroupMember.id).where(
GroupMember.user_id == User.id,
GroupMember.group_id == group_id,
)
)

# CASE: user in group → 1, user not in group → 0
group_sort = case((membership_exists, 1), else_=0)

if direction == "asc":
query = query.order_by((GroupMember.group_id == group_id).asc())
query = query.order_by(group_sort.asc(), User.name.asc())
else:
query = query.order_by(
(GroupMember.group_id == group_id).desc()
)
query = query.order_by(group_sort.desc(), User.name.asc())

elif order_by == "name":
if direction == "asc":
query = query.order_by(User.name.asc())
else:
query = query.order_by(User.name.desc())

elif order_by == "email":
if direction == "asc":
query = query.order_by(User.email.asc())
Expand Down Expand Up @@ -299,9 +348,10 @@ def get_users(
# Count BEFORE pagination
total = query.count()

if skip:
# correct pagination logic
if skip is not None:
query = query.offset(skip)
if limit:
if limit is not None:
query = query.limit(limit)

users = query.all()
Expand Down
Loading
Loading