From f5e8d4d5a004115489c35725408b057e24dfe318 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 1 Dec 2025 13:34:57 -0500 Subject: [PATCH 01/45] refac --- backend/open_webui/models/users.py | 2 +- .../layout/Sidebar/ChannelItem.svelte | 37 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index ba56b74ece..692633e64b 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -188,7 +188,7 @@ class UserIdNameResponse(BaseModel): name: str -class UserIdNameStatusResponse(BaseModel): +class UserIdNameStatusResponse(UserStatus): id: str name: str is_active: Optional[bool] = None diff --git a/src/lib/components/layout/Sidebar/ChannelItem.svelte b/src/lib/components/layout/Sidebar/ChannelItem.svelte index 524724c9a1..866dfb13cf 100644 --- a/src/lib/components/layout/Sidebar/ChannelItem.svelte +++ b/src/lib/components/layout/Sidebar/ChannelItem.svelte @@ -15,6 +15,7 @@ import Hashtag from '$lib/components/icons/Hashtag.svelte'; import Users from '$lib/components/icons/Users.svelte'; import XMark from '$lib/components/icons/XMark.svelte'; + import Emoji from '$lib/components/common/Emoji.svelte'; export let onUpdate: Function = () => {}; @@ -131,14 +132,38 @@ {/if} -
+
{#if channel?.name} - {channel.name} + + {channel.name} + {:else} - {channel?.users - ?.filter((u) => u.id !== $user?.id) - .map((u) => u.name) - .join(', ')} + + {channel?.users + ?.filter((u) => u.id !== $user?.id) + .map((u) => u.name) + .join(', ')} + + + {#if channel?.users?.length === 2} + {@const dmUser = channel.users.find((u) => u.id !== $user?.id)} + + {#if dmUser?.status_emoji || dmUser?.status_message} + + {#if dmUser?.status_emoji} +
+ +
+ {/if} + +
+ {dmUser?.status_message} +
+
+ {/if} + {/if} {/if}
From 52ccab8fc0d18be5562c44b7414d6ceb1d1b1b01 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 1 Dec 2025 13:52:09 -0500 Subject: [PATCH 02/45] refac --- backend/open_webui/utils/middleware.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 2b789f3f17..140d2bc85d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -458,12 +458,6 @@ async def chat_completion_tools_handler( } ) - print( - f"Tool {tool_function_name} result: {tool_result}", - tool_result_files, - tool_result_embeds, - ) - if tool_result: tool = tools[tool_function_name] tool_id = tool.get("tool_id", "") @@ -491,12 +485,6 @@ async def chat_completion_tools_handler( } ) - # Citation is not enabled for this tool - body["messages"] = add_or_update_user_message( - f"\nTool `{tool_name}` Output: {tool_result}", - body["messages"], - ) - if ( tools[tool_function_name] .get("metadata", {}) From 4f50571b5323be5fdb28ef645573434afd97e225 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:34:57 +0100 Subject: [PATCH 03/45] Chore: dep bump (#19667) * Update pyproject.toml * Update requirements-min.txt * Update requirements.txt --------- Co-authored-by: Tim Baek --- backend/requirements-min.txt | 18 ++++++++-------- backend/requirements.txt | 40 ++++++++++++++++++------------------ pyproject.toml | 40 ++++++++++++++++++------------------ 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt index 8d63bd4b82..f22ad7f0cf 100644 --- a/backend/requirements-min.txt +++ b/backend/requirements-min.txt @@ -1,9 +1,9 @@ # Minimal requirements for backend to run # WIP: use this as a reference to build a minimal docker image -fastapi==0.118.0 +fastapi==0.123.0 uvicorn[standard]==0.37.0 -pydantic==2.11.9 +pydantic==2.12.5 python-multipart==0.0.20 itsdangerous==2.2.0 @@ -20,14 +20,14 @@ aiohttp==3.12.15 async-timeout aiocache aiofiles -starlette-compress==1.6.0 +starlette-compress==1.6.1 httpx[socks,http2,zstd,cli,brotli]==0.28.1 starsessions[redis]==2.2.1 sqlalchemy==2.0.38 -alembic==1.14.0 -peewee==3.18.1 -peewee-migrate==1.12.2 +alembic==1.17.2 +peewee==3.18.3 +peewee-migrate==1.14.3 pycrdt==0.12.25 redis @@ -36,9 +36,9 @@ APScheduler==3.10.4 RestrictedPython==8.0 loguru==0.7.3 -asgiref==3.8.1 +asgiref==3.11.0 -mcp==1.21.2 +mcp==1.22.0 openai langchain==0.3.27 @@ -46,6 +46,6 @@ langchain-community==0.3.29 fake-useragent==2.2.0 chromadb==1.1.0 -black==25.9.0 +black==25.11.0 pydub chardet==5.2.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 1ddd886a8c..a1a8034959 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,6 @@ -fastapi==0.118.0 +fastapi==0.123.0 uvicorn[standard]==0.37.0 -pydantic==2.11.9 +pydantic==2.12.5 python-multipart==0.0.20 itsdangerous==2.2.0 @@ -17,14 +17,14 @@ aiohttp==3.12.15 async-timeout aiocache aiofiles -starlette-compress==1.6.0 +starlette-compress==1.6.1 httpx[socks,http2,zstd,cli,brotli]==0.28.1 starsessions[redis]==2.2.1 sqlalchemy==2.0.38 -alembic==1.14.0 -peewee==3.18.1 -peewee-migrate==1.12.2 +alembic==1.17.2 +peewee==3.18.3 +peewee-migrate==1.14.3 pycrdt==0.12.25 redis @@ -33,11 +33,11 @@ APScheduler==3.10.4 RestrictedPython==8.0 loguru==0.7.3 -asgiref==3.8.1 +asgiref==3.11.0 # AI libraries tiktoken -mcp==1.21.2 +mcp==1.22.0 openai anthropic @@ -58,18 +58,18 @@ accelerate pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897 einops==0.8.1 -ftfy==6.2.3 +ftfy==6.3.1 chardet==5.2.0 pypdf==6.4.0 fpdf2==2.8.2 -pymdown-extensions==10.14.2 +pymdown-extensions==10.17.2 docx2txt==0.8 python-pptx==1.0.2 -unstructured==0.18.18 +unstructured==0.18.21 msoffcrypto-tool==5.4.2 nltk==3.9.1 -Markdown==3.9 -pypandoc==1.15 +Markdown==3.10 +pypandoc==1.16.2 pandas==2.2.3 openpyxl==3.1.5 pyxlsb==1.0.10 @@ -87,12 +87,12 @@ rank-bm25==0.2.2 onnxruntime==1.20.1 faster-whisper==1.1.1 -black==25.9.0 +black==25.11.0 youtube-transcript-api==1.2.2 pytube==15.0.0 pydub -ddgs==9.0.0 +ddgs==9.9.2 azure-ai-documentintelligence==1.0.2 azure-identity==1.25.0 @@ -104,7 +104,7 @@ google-api-python-client google-auth-httplib2 google-auth-oauthlib -googleapis-common-protos==1.70.0 +googleapis-common-protos==1.72.0 google-cloud-storage==2.19.0 ## Databases @@ -113,11 +113,11 @@ psycopg2-binary==2.9.10 pgvector==0.4.1 PyMySQL==1.1.1 -boto3==1.40.5 +boto3==1.41.5 pymilvus==2.6.4 qdrant-client==1.14.3 -playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml +playwright==1.56.0 # Caution: version must match docker-compose.playwright.yaml elasticsearch==9.1.0 pinecone==6.0.2 oracledb==3.2.0 @@ -130,13 +130,13 @@ colbert-ai==0.2.21 ## Tests docker~=7.1.0 pytest~=8.4.1 -pytest-docker~=3.1.1 +pytest-docker~=3.2.5 ## LDAP ldap3==2.9.1 ## Firecrawl -firecrawl-py==4.5.0 +firecrawl-py==4.10.0 ## Trace opentelemetry-api==1.38.0 diff --git a/pyproject.toml b/pyproject.toml index 709f4ec672..10dd3259e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,9 @@ authors = [ ] license = { file = "LICENSE" } dependencies = [ - "fastapi==0.118.0", + "fastapi==0.123.0", "uvicorn[standard]==0.37.0", - "pydantic==2.11.9", + "pydantic==2.12.5", "python-multipart==0.0.20", "itsdangerous==2.2.0", @@ -25,14 +25,14 @@ dependencies = [ "async-timeout", "aiocache", "aiofiles", - "starlette-compress==1.6.0", + "starlette-compress==1.6.1", "httpx[socks,http2,zstd,cli,brotli]==0.28.1", "starsessions[redis]==2.2.1", "sqlalchemy==2.0.38", - "alembic==1.14.0", - "peewee==3.18.1", - "peewee-migrate==1.12.2", + "alembic==1.17.2", + "peewee==3.18.3", + "peewee-migrate==1.14.3", "pycrdt==0.12.25", "redis", @@ -41,10 +41,10 @@ dependencies = [ "RestrictedPython==8.0", "loguru==0.7.3", - "asgiref==3.8.1", + "asgiref==3.11.0", "tiktoken", - "mcp==1.21.2", + "mcp==1.22.0", "openai", "anthropic", @@ -58,7 +58,7 @@ dependencies = [ "chromadb==1.0.20", "opensearch-py==2.8.0", "PyMySQL==1.1.1", - "boto3==1.40.5", + "boto3==1.41.5", "transformers==4.57.3", "sentence-transformers==5.1.2", @@ -66,18 +66,18 @@ dependencies = [ "pyarrow==20.0.0", "einops==0.8.1", - "ftfy==6.2.3", + "ftfy==6.3.1", "chardet==5.2.0", "pypdf==6.4.0", "fpdf2==2.8.2", - "pymdown-extensions==10.14.2", + "pymdown-extensions==10.17.2", "docx2txt==0.8", "python-pptx==1.0.2", - "unstructured==0.18.18", + "unstructured==0.18.21", "msoffcrypto-tool==5.4.2", "nltk==3.9.1", - "Markdown==3.9", - "pypandoc==1.15", + "Markdown==3.10", + "pypandoc==1.16.2", "pandas==2.2.3", "openpyxl==3.1.5", "pyxlsb==1.0.10", @@ -96,18 +96,18 @@ dependencies = [ "onnxruntime==1.20.1", "faster-whisper==1.1.1", - "black==25.9.0", + "black==25.11.0", "youtube-transcript-api==1.2.2", "pytube==15.0.0", "pydub", - "ddgs==9.0.0", + "ddgs==9.9.2", "google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib", - "googleapis-common-protos==1.70.0", + "googleapis-common-protos==1.72.0", "google-cloud-storage==2.19.0", "azure-identity==1.25.0", @@ -142,8 +142,8 @@ all = [ "gcp-storage-emulator>=2024.8.3", "docker~=7.1.0", "pytest~=8.3.2", - "pytest-docker~=3.1.1", - "playwright==1.49.1", + "pytest-docker~=3.2.5", + "playwright==1.56.0", "elasticsearch==9.1.0", "qdrant-client==1.14.3", @@ -153,7 +153,7 @@ all = [ "oracledb==3.2.0", "colbert-ai==0.2.21", - "firecrawl-py==4.5.0", + "firecrawl-py==4.10.0", "azure-search-documents==11.6.0", ] From 734c04ebf0062e3ebfbb487156b4685447d736d6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 02:53:49 -0500 Subject: [PATCH 04/45] refac --- .../Messages/Message/UserStatus.svelte | 2 +- src/lib/components/layout/Sidebar.svelte | 13 +++------ src/routes/+layout.svelte | 27 +++++-------------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/lib/components/channel/Messages/Message/UserStatus.svelte b/src/lib/components/channel/Messages/Message/UserStatus.svelte index d04c9eb291..7c4749dc7f 100644 --- a/src/lib/components/channel/Messages/Message/UserStatus.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatus.svelte @@ -77,7 +77,7 @@
{#if user?.status_emoji}
diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 5ef0e96b88..272e6f1c44 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -183,16 +183,9 @@ const initChannels = async () => { // default (none), group, dm type await channels.set( - (await getChannels(localStorage.token)).sort((a, b) => - a.type === b.type - ? 0 - : a.type === 'dm' - ? 1 - : a.type === 'group' - ? b.type === 'dm' - ? -1 - : 0 - : -1 + (await getChannels(localStorage.token)).sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) ) ); }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 6a3756f4ca..c4a8b6c5b2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -488,16 +488,9 @@ // handle channel created event if (event.data?.type === 'channel:created') { await channels.set( - (await getChannels(localStorage.token)).sort((a, b) => - a.type === b.type - ? 0 - : a.type === 'dm' - ? 1 - : a.type === 'group' - ? b.type === 'dm' - ? -1 - : 0 - : -1 + (await getChannels(localStorage.token)).sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - ['', null, 'group', 'dm'].indexOf(b.type) ) ); return; @@ -539,16 +532,10 @@ ); } else { await channels.set( - (await getChannels(localStorage.token)).sort((a, b) => - a.type === b.type - ? 0 - : a.type === 'dm' - ? 1 - : a.type === 'group' - ? b.type === 'dm' - ? -1 - : 0 - : -1 + (await getChannels(localStorage.token)).sort( + (a, b) => + ['', null, 'group', 'dm'].indexOf(a.type) - + ['', null, 'group', 'dm'].indexOf(b.type) ) ); } From 7b166370432414ce8f186747fb098e0c70fb2d6b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 03:52:38 -0500 Subject: [PATCH 05/45] feat: signin rate limit --- backend/open_webui/routers/auths.py | 15 +++ backend/open_webui/utils/rate_limit.py | 139 +++++++++++++++++++++++++ backend/open_webui/utils/redis.py | 23 +++- 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 backend/open_webui/utils/rate_limit.py diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 42302043ed..0bf1d65d0c 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientSession import urllib + from open_webui.models.auths import ( AddUserForm, ApiKey, @@ -65,6 +66,10 @@ from open_webui.utils.auth import ( from open_webui.utils.webhook import post_webhook from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.utils.redis import get_redis_client +from open_webui.utils.rate_limit import RateLimiter + + from typing import Optional, List from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS @@ -77,6 +82,10 @@ router = APIRouter() log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) +signin_rate_limiter = RateLimiter( + redis_client=get_redis_client(), limit=5 * 3, window=60 * 3 +) + ############################ # GetSessionUser ############################ @@ -551,6 +560,12 @@ async def signin(request: Request, response: Response, form_data: SigninForm): admin_email.lower(), lambda pw: verify_password(admin_password, pw) ) else: + if signin_rate_limiter.is_limited(form_data.email.lower()): + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + ) + password_bytes = form_data.password.encode("utf-8") if len(password_bytes) > 72: # TODO: Implement other hashing algorithms that support longer passwords diff --git a/backend/open_webui/utils/rate_limit.py b/backend/open_webui/utils/rate_limit.py new file mode 100644 index 0000000000..b657a937ab --- /dev/null +++ b/backend/open_webui/utils/rate_limit.py @@ -0,0 +1,139 @@ +import time +from typing import Optional, Dict +from open_webui.env import REDIS_KEY_PREFIX + + +class RateLimiter: + """ + General-purpose rate limiter using Redis with a rolling window strategy. + Falls back to in-memory storage if Redis is not available. + """ + + # In-memory fallback storage + _memory_store: Dict[str, Dict[int, int]] = {} + + def __init__( + self, + redis_client, + limit: int, + window: int, + bucket_size: int = 60, + enabled: bool = True, + ): + """ + :param redis_client: Redis client instance or None + :param limit: Max allowed events in the window + :param window: Time window in seconds + :param bucket_size: Bucket resolution + :param enabled: Turn on/off rate limiting globally + """ + self.r = redis_client + self.limit = limit + self.window = window + self.bucket_size = bucket_size + self.num_buckets = window // bucket_size + self.enabled = enabled + + def _bucket_key(self, key: str, bucket_index: int) -> str: + return f"{REDIS_KEY_PREFIX}:ratelimit:{key.lower()}:{bucket_index}" + + def _current_bucket(self) -> int: + return int(time.time()) // self.bucket_size + + def _redis_available(self) -> bool: + return self.r is not None + + def is_limited(self, key: str) -> bool: + """ + Main rate-limit check. + Gracefully handles missing or failing Redis. + """ + if not self.enabled: + return False + + if self._redis_available(): + try: + return self._is_limited_redis(key) + except Exception: + return self._is_limited_memory(key) + else: + return self._is_limited_memory(key) + + def get_count(self, key: str) -> int: + if not self.enabled: + return 0 + + if self._redis_available(): + try: + return self._get_count_redis(key) + except Exception: + return self._get_count_memory(key) + else: + return self._get_count_memory(key) + + def remaining(self, key: str) -> int: + used = self.get_count(key) + return max(0, self.limit - used) + + def _is_limited_redis(self, key: str) -> bool: + now_bucket = self._current_bucket() + bucket_key = self._bucket_key(key, now_bucket) + + attempts = self.r.incr(bucket_key) + if attempts == 1: + self.r.expire(bucket_key, self.window + self.bucket_size) + + # Collect buckets + buckets = [ + self._bucket_key(key, now_bucket - i) for i in range(self.num_buckets + 1) + ] + + counts = self.r.mget(buckets) + total = sum(int(c) for c in counts if c) + + return total > self.limit + + def _get_count_redis(self, key: str) -> int: + now_bucket = self._current_bucket() + buckets = [ + self._bucket_key(key, now_bucket - i) for i in range(self.num_buckets + 1) + ] + counts = self.r.mget(buckets) + return sum(int(c) for c in counts if c) + + def _is_limited_memory(self, key: str) -> bool: + now_bucket = self._current_bucket() + + # Init storage + if key not in self._memory_store: + self._memory_store[key] = {} + + store = self._memory_store[key] + + # Increment bucket + store[now_bucket] = store.get(now_bucket, 0) + 1 + + # Drop expired buckets + min_bucket = now_bucket - self.num_buckets + expired = [b for b in store if b < min_bucket] + for b in expired: + del store[b] + + # Count totals + total = sum(store.values()) + return total > self.limit + + def _get_count_memory(self, key: str) -> int: + now_bucket = self._current_bucket() + if key not in self._memory_store: + return 0 + + store = self._memory_store[key] + min_bucket = now_bucket - self.num_buckets + + # Remove expired + expired = [b for b in store if b < min_bucket] + for b in expired: + del store[b] + + return sum(store.values()) diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index c60a6fa517..cc29ce6683 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -5,7 +5,13 @@ import logging import redis -from open_webui.env import REDIS_SENTINEL_MAX_RETRY_COUNT +from open_webui.env import ( + REDIS_CLUSTER, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_MAX_RETRY_COUNT, + REDIS_SENTINEL_PORT, + REDIS_URL, +) log = logging.getLogger(__name__) @@ -108,6 +114,21 @@ def parse_redis_service_url(redis_url): } +def get_redis_client(async_mode=False): + try: + return get_redis_connection( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env( + REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT + ), + redis_cluster=REDIS_CLUSTER, + async_mode=async_mode, + ) + except Exception as e: + log.debug(f"Failed to get Redis client: {e}") + return None + + def get_redis_connection( redis_url, redis_sentinels, From 0a14196afbfdfab391d353629d00dcc53e1af5eb Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:57:14 +0100 Subject: [PATCH 06/45] Update milvus_multitenancy.py (#19680) --- backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py index 5c80d155d3..82dd322359 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -157,7 +157,6 @@ class MilvusClient(VectorDBBase): for item in items ] collection.insert(entities) - collection.flush() def search( self, collection_name: str, vectors: List[List[float]], limit: int From 5388cc1bc65f55fe0f91806b20a5b711650d2d01 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 04:03:44 -0500 Subject: [PATCH 07/45] refac --- backend/open_webui/utils/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 3f05256c70..23fe517150 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -235,7 +235,7 @@ async def invalidate_token(request, token): jti = decoded.get("jti") exp = decoded.get("exp") - if jti: + if jti and exp: ttl = exp - int( datetime.now(UTC).timestamp() ) # Calculate time-to-live for the token From 562f22960c5ebfb3f5009e1892d370c5e28c5f30 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 04:07:02 -0500 Subject: [PATCH 08/45] refac --- .../channel/Messages/Message/UserStatus.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/components/channel/Messages/Message/UserStatus.svelte b/src/lib/components/channel/Messages/Message/UserStatus.svelte index 7c4749dc7f..a9e04e2ea8 100644 --- a/src/lib/components/channel/Messages/Message/UserStatus.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatus.svelte @@ -92,6 +92,16 @@
{/if} + {#if user?.bio} +
+ +
+ {user?.bio} +
+
+
+ {/if} + {#if $_user?.id !== user.id}
From 6e531679f4dccf9ac4f5fa2b13378b80e00127d0 Mon Sep 17 00:00:00 2001 From: Poccia <114789517+kjpoccia@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:17:32 -0500 Subject: [PATCH 09/45] fix/adjust web search to properly block domains (#19670) Co-authored-by: Tim Baek --- backend/open_webui/retrieval/web/main.py | 2 +- backend/open_webui/utils/misc.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/retrieval/web/main.py b/backend/open_webui/retrieval/web/main.py index 6d2fd1bc5a..1b8df9f8ee 100644 --- a/backend/open_webui/retrieval/web/main.py +++ b/backend/open_webui/retrieval/web/main.py @@ -33,7 +33,7 @@ def get_filtered_results(results, filter_list): except Exception: pass - if any(is_string_allowed(hostname, filter_list) for hostname in hostnames): + if is_string_allowed(hostnames, filter_list): filtered_results.append(result) continue diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index c7ff2a3edd..2f58ab21d4 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -6,7 +6,7 @@ import uuid import logging from datetime import timedelta from pathlib import Path -from typing import Callable, Optional +from typing import Callable, Optional, Sequence, Union import json import aiohttp @@ -43,25 +43,29 @@ def get_allow_block_lists(filter_list): return allow_list, block_list -def is_string_allowed(string: str, filter_list: Optional[list[str]] = None) -> bool: +def is_string_allowed( + string: Union[str, Sequence[str]], filter_list: Optional[list[str]] = None +) -> bool: """ Checks if a string is allowed based on the provided filter list. - :param string: The string to check (e.g., domain or hostname). + :param string: The string or sequence of strings to check (e.g., domain or hostname). :param filter_list: List of allowed/blocked strings. Strings starting with "!" are blocked. - :return: True if the string is allowed, False otherwise. + :return: True if the string or sequence of strings is allowed, False otherwise. """ if not filter_list: return True allow_list, block_list = get_allow_block_lists(filter_list) + print(string, allow_list, block_list) + strings = [string] if isinstance(string, str) else list(string) # If allow list is non-empty, require domain to match one of them if allow_list: - if not any(string.endswith(allowed) for allowed in allow_list): + if not any(s.endswith(allowed) for s in strings for allowed in allow_list): return False # Block list always removes matches - if any(string.endswith(blocked) for blocked in block_list): + if any(s.endswith(blocked) for s in strings for blocked in block_list): return False return True From 143d3fbce25419c716853410fc2d41ba40cb7307 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 04:18:19 -0500 Subject: [PATCH 10/45] refac --- backend/open_webui/utils/misc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 2f58ab21d4..5e3f3c4834 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -56,7 +56,6 @@ def is_string_allowed( return True allow_list, block_list = get_allow_block_lists(filter_list) - print(string, allow_list, block_list) strings = [string] if isinstance(string, str) else list(string) # If allow list is non-empty, require domain to match one of them From 9f42b9369fa5328442dfb9eca488c1590230097b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 05:29:34 -0500 Subject: [PATCH 11/45] refac --- backend/open_webui/models/users.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 692633e64b..424d9676a5 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -24,8 +24,10 @@ from sqlalchemy import ( Date, exists, select, + cast, ) from sqlalchemy import or_, case +from sqlalchemy.dialects.postgresql import JSONB import datetime @@ -296,14 +298,21 @@ class UsersTable: def get_user_by_oauth_sub(self, provider: str, sub: str) -> Optional[UserModel]: try: - with get_db() as db: - user = ( - db.query(User) - .filter(User.oauth.contains({provider: {"sub": sub}})) - .first() - ) + with get_db() as db: # type: Session + dialect_name = db.bind.dialect.name + + query = db.query(User) + if dialect_name == "sqlite": + query = query.filter(User.oauth.contains({provider: {"sub": sub}})) + elif dialect_name == "postgresql": + query = query.filter( + User.oauth[provider].cast(JSONB)["sub"].astext == sub + ) + + user = query.first() return UserModel.model_validate(user) if user else None - except Exception: + except Exception as e: + # You may want to log the exception here return None def get_users( From aa589fcbd98cb96899062f28789715f971096850 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 05:36:45 -0500 Subject: [PATCH 12/45] refac --- src/lib/components/layout/Sidebar/UserMenu.svelte | 2 ++ .../components/layout/Sidebar/UserStatusModal.svelte | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte index 1bfff6751e..746834933d 100644 --- a/src/lib/components/layout/Sidebar/UserMenu.svelte +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -137,6 +137,7 @@ class="mb-1 w-full gap-2 px-2.5 py-1.5 rounded-xl bg-gray-50 dark:text-white dark:bg-gray-900/50 text-black transition text-xs flex items-center" type="button" on:click={() => { + show = false; showUserStatusModal = true; }} > @@ -187,6 +188,7 @@ class="mb-1 w-full px-3 py-1.5 gap-1 rounded-xl bg-gray-50 dark:text-white dark:bg-gray-900/50 text-black transition text-xs flex items-center justify-center" type="button" on:click={() => { + show = false; showUserStatusModal = true; }} > diff --git a/src/lib/components/layout/Sidebar/UserStatusModal.svelte b/src/lib/components/layout/Sidebar/UserStatusModal.svelte index 25e0f5f1c1..72994ffb74 100644 --- a/src/lib/components/layout/Sidebar/UserStatusModal.svelte +++ b/src/lib/components/layout/Sidebar/UserStatusModal.svelte @@ -1,5 +1,5 @@ {#if user} -
-
+
+
profile
From 39f7575b648731813a88e7aa0ff4a75e846d0cc7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 06:19:48 -0500 Subject: [PATCH 14/45] refac: show connection type for custom models --- backend/open_webui/utils/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 525ba22e76..fbd1089382 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -191,6 +191,8 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) ): # Custom model based on a base model owned_by = "openai" + connection_type = None + pipe = None for m in models: @@ -201,6 +203,8 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) owned_by = m.get("owned_by", "unknown") if "pipe" in m: pipe = m["pipe"] + + connection_type = m.get("connection_type", None) break model = { @@ -209,6 +213,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) "object": "model", "created": custom_model.created_at, "owned_by": owned_by, + "connection_type": connection_type, "preset": True, **({"pipe": pipe} if pipe is not None else {}), } From 6ce9afd95dee1bda88d8f491bf1ecbc5cf37a3db Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 09:21:03 -0500 Subject: [PATCH 15/45] refac --- backend/open_webui/main.py | 5 ++--- backend/open_webui/routers/retrieval.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 127f22e103..087dc5fb03 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -983,8 +983,7 @@ app.state.YOUTUBE_LOADER_TRANSLATION = None try: app.state.ef = get_ef( app.state.config.RAG_EMBEDDING_ENGINE, - app.state.config.RAG_EMBEDDING_MODEL, - RAG_EMBEDDING_MODEL_AUTO_UPDATE, + app.state.config.RAG_EMBEDDING_MODEL ) if ( app.state.config.ENABLE_RAG_HYBRID_SEARCH @@ -995,7 +994,7 @@ try: app.state.config.RAG_RERANKING_MODEL, app.state.config.RAG_EXTERNAL_RERANKER_URL, app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, - RAG_RERANKING_MODEL_AUTO_UPDATE, + ) else: app.state.rf = None diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 72090e3ba0..190f001edd 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -123,7 +123,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"]) def get_ef( engine: str, embedding_model: str, - auto_update: bool = False, + auto_update: bool = RAG_EMBEDDING_MODEL_AUTO_UPDATE, ): ef = None if embedding_model and engine == "": @@ -148,7 +148,7 @@ def get_rf( reranking_model: Optional[str] = None, external_reranker_url: str = "", external_reranker_api_key: str = "", - auto_update: bool = False, + auto_update: bool = RAG_RERANKING_MODEL_AUTO_UPDATE, ): rf = None if reranking_model: @@ -927,7 +927,6 @@ async def update_rag_config( request.app.state.config.RAG_RERANKING_MODEL, request.app.state.config.RAG_EXTERNAL_RERANKER_URL, request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, - True, ) request.app.state.RERANKING_FUNCTION = get_reranking_function( From 29236aefe891c8e2d7fc5d0612a051c3cc63c3c3 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 10:25:38 -0500 Subject: [PATCH 16/45] refac --- src/routes/(app)/+layout.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 1729bc583c..9d84d0bee8 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -9,11 +9,7 @@ import { page } from '$app/stores'; import { fade } from 'svelte/transition'; - import { getKnowledgeBases } from '$lib/apis/knowledge'; - import { getFunctions } from '$lib/apis/functions'; import { getModels, getToolServersData, getVersionUpdates } from '$lib/apis'; - import { getAllTags } from '$lib/apis/chats'; - import { getPrompts } from '$lib/apis/prompts'; import { getTools } from '$lib/apis/tools'; import { getBanners } from '$lib/apis/configs'; import { getUserSettings } from '$lib/apis/users'; From d19023288e2ca40f86e2dc3fd9f230540f3e70d7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 10:53:32 -0500 Subject: [PATCH 17/45] feat/enh: kb files db migration --- .../3e0e00844bb0_add_knowledge_file_table.py | 160 +++++++++++++ backend/open_webui/models/knowledge.py | 130 ++++++++++- backend/open_webui/retrieval/utils.py | 24 +- backend/open_webui/routers/files.py | 1 + backend/open_webui/routers/knowledge.py | 216 ++++-------------- 5 files changed, 335 insertions(+), 196 deletions(-) create mode 100644 backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py diff --git a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py new file mode 100644 index 0000000000..02909ace9d --- /dev/null +++ b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py @@ -0,0 +1,160 @@ +"""Add knowledge_file table + +Revision ID: 3e0e00844bb0 +Revises: 90ef40d4714e +Create Date: 2025-12-02 06:54:19.401334 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect +import open_webui.internal.db + +import time +import json +import uuid + +# revision identifiers, used by Alembic. +revision: str = "3e0e00844bb0" +down_revision: Union[str, None] = "90ef40d4714e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "knowledge_file", + sa.Column("id", sa.Text(), primary_key=True), + sa.Column("user_id", sa.Text(), nullable=False), + sa.Column( + "knowledge_id", + sa.Text(), + sa.ForeignKey("knowledge.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "file_id", + sa.Text(), + sa.ForeignKey("file.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("created_at", sa.BigInteger(), nullable=False), + sa.Column("updated_at", sa.BigInteger(), nullable=False), + # indexes + sa.Index("ix_knowledge_file_knowledge_id", "knowledge_id"), + sa.Index("ix_knowledge_file_file_id", "file_id"), + sa.Index("ix_knowledge_file_user_id", "user_id"), + # unique constraints + sa.UniqueConstraint( + "knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file" + ), # prevent duplicate entries + ) + + connection = op.get_bind() + + # 2. Read existing group with user_ids JSON column + knowledge_table = sa.Table( + "knowledge", + sa.MetaData(), + sa.Column("id", sa.Text()), + sa.Column("user_id", sa.Text()), + sa.Column("data", sa.JSON()), # JSON stored as text in SQLite + PG + ) + + results = connection.execute( + sa.select( + knowledge_table.c.id, knowledge_table.c.user_id, knowledge_table.c.data + ) + ).fetchall() + + # 3. Insert members into group_member table + kf_table = sa.Table( + "knowledge_file", + sa.MetaData(), + sa.Column("id", sa.Text()), + sa.Column("user_id", sa.Text()), + sa.Column("knowledge_id", sa.Text()), + sa.Column("file_id", sa.Text()), + sa.Column("created_at", sa.BigInteger()), + sa.Column("updated_at", sa.BigInteger()), + ) + + now = int(time.time()) + for knowledge_id, user_id, data in results: + if not data: + continue + + if isinstance(data, str): + try: + data = json.loads(data) + except Exception: + continue # skip invalid JSON + + if not isinstance(data, dict): + continue + + file_ids = data.get("file_ids", []) + + rows = [ + { + "id": str(uuid.uuid4()), + "user_id": user_id, + "knowledge_id": knowledge_id, + "file_id": file_id, + "created_at": now, + "updated_at": now, + } + for file_id in file_ids + ] + + if rows: + connection.execute(kf_table.insert(), rows) + + with op.batch_alter_table("knowledge") as batch: + batch.drop_column("data") + + +def downgrade() -> None: + # 1. Add back the old data column + op.add_column("knowledge", sa.Column("data", sa.JSON(), nullable=True)) + + connection = op.get_bind() + + # 2. Read knowledge_file entries and reconstruct data JSON + knowledge_table = sa.Table( + "knowledge", + sa.MetaData(), + sa.Column("id", sa.Text()), + sa.Column("data", sa.JSON()), + ) + + kf_table = sa.Table( + "knowledge_file", + sa.MetaData(), + sa.Column("id", sa.Text()), + sa.Column("knowledge_id", sa.Text()), + sa.Column("file_id", sa.Text()), + ) + + results = connection.execute(sa.select(knowledge_table.c.id)).fetchall() + + for (knowledge_id,) in results: + file_ids = connection.execute( + sa.select(kf_table.c.file_id).where(kf_table.c.knowledge_id == knowledge_id) + ).fetchall() + + file_ids_list = [fid for (fid,) in file_ids] + + data_json = {"file_ids": file_ids_list} + + connection.execute( + knowledge_table.update() + .where(knowledge_table.c.id == knowledge_id) + .values(data=data_json) + ) + + # 3. Drop the knowledge_file table + op.drop_table("knowledge_file") diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index cfef77e237..76548c8da4 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -7,13 +7,21 @@ import uuid from open_webui.internal.db import Base, get_db from open_webui.env import SRC_LOG_LEVELS -from open_webui.models.files import FileMetadataResponse +from open_webui.models.files import File, FileModel, FileMetadataResponse from open_webui.models.groups import Groups from open_webui.models.users import Users, UserResponse from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, JSON +from sqlalchemy import ( + BigInteger, + Column, + ForeignKey, + String, + Text, + JSON, + UniqueConstraint, +) from open_webui.utils.access_control import has_access @@ -34,9 +42,7 @@ class Knowledge(Base): name = Column(Text) description = Column(Text) - data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) - access_control = Column(JSON, nullable=True) # Controls data access levels. # Defines access control rules for this entry. # - `None`: Public access, available to all users with the "user" role. @@ -67,7 +73,6 @@ class KnowledgeModel(BaseModel): name: str description: str - data: Optional[dict] = None meta: Optional[dict] = None access_control: Optional[dict] = None @@ -76,11 +81,42 @@ class KnowledgeModel(BaseModel): updated_at: int # timestamp in epoch +class KnowledgeFile(Base): + __tablename__ = "knowledge_file" + + id = Column(Text, unique=True, primary_key=True) + + knowledge_id = Column( + Text, ForeignKey("knowledge.id", ondelete="CASCADE"), nullable=False + ) + file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False) + user_id = Column(Text, nullable=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint( + "knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file" + ), + ) + + +class KnowledgeFileModel(BaseModel): + id: str + knowledge_id: str + file_id: str + user_id: str + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + #################### # Forms #################### - - class KnowledgeUserModel(KnowledgeModel): user: Optional[UserResponse] = None @@ -96,7 +132,6 @@ class KnowledgeUserResponse(KnowledgeUserModel): class KnowledgeForm(BaseModel): name: str description: str - data: Optional[dict] = None access_control: Optional[dict] = None @@ -182,6 +217,85 @@ class KnowledgeTable: except Exception: return None + def get_files_by_id(self, knowledge_id: str) -> list[FileModel]: + try: + with get_db() as db: + files = ( + db.query(File) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + .all() + ) + return [FileModel.model_validate(file) for file in files] + except Exception: + return [] + + def get_file_metadatas_by_id(self, knowledge_id: str) -> list[FileMetadataResponse]: + try: + with get_db() as db: + files = self.get_files_by_id(knowledge_id) + return [FileMetadataResponse(**file.model_dump()) for file in files] + except Exception: + return [] + + def add_file_to_knowledge_by_id( + self, knowledge_id: str, file_id: str, user_id: str + ) -> Optional[KnowledgeFileModel]: + with get_db() as db: + knowledge_file = KnowledgeFileModel( + **{ + "id": str(uuid.uuid4()), + "knowledge_id": knowledge_id, + "file_id": file_id, + "user_id": user_id, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + + try: + result = KnowledgeFile(**knowledge_file.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return KnowledgeFileModel.model_validate(result) + else: + return None + except Exception: + return None + + def remove_file_from_knowledge_by_id(self, knowledge_id: str, file_id: str) -> bool: + try: + with get_db() as db: + db.query(KnowledgeFile).filter_by( + knowledge_id=knowledge_id, file_id=file_id + ).delete() + db.commit() + return True + except Exception: + return False + + def reset_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]: + try: + with get_db() as db: + # Delete all knowledge_file entries for this knowledge_id + db.query(KnowledgeFile).filter_by(knowledge_id=id).delete() + db.commit() + + # Update the knowledge entry's updated_at timestamp + db.query(Knowledge).filter_by(id=id).update( + { + "updated_at": int(time.time()), + } + ) + db.commit() + + return self.get_knowledge_by_id(id=id) + except Exception as e: + log.exception(e) + return None + def update_knowledge_by_id( self, id: str, form_data: KnowledgeForm, overwrite: bool = False ) -> Optional[KnowledgeModel]: diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index b041a00471..711b1a8b79 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -1088,23 +1088,19 @@ async def get_sources_from_items( or knowledge_base.user_id == user.id or has_access(user.id, "read", knowledge_base.access_control) ): - - file_ids = knowledge_base.data.get("file_ids", []) + files = Knowledges.get_files_by_id(knowledge_base.id) documents = [] metadatas = [] - for file_id in file_ids: - file_object = Files.get_file_by_id(file_id) - - if file_object: - documents.append(file_object.data.get("content", "")) - metadatas.append( - { - "file_id": file_id, - "name": file_object.filename, - "source": file_object.filename, - } - ) + for file in files: + documents.append(file.data.get("content", "")) + metadatas.append( + { + "file_id": file.id, + "name": file.filename, + "source": file.filename, + } + ) query_result = { "documents": [documents], diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 54084941fe..1d27c6ab38 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -53,6 +53,7 @@ router = APIRouter() ############################ +# TODO: Optimize this function to use the knowledge_file table for faster lookups. def has_access_to_file( file_id: Optional[str], access_type: str, user=Depends(get_verified_user) ) -> bool: diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 46baa0eaea..3bfc961ac3 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -42,97 +42,38 @@ router = APIRouter() @router.get("/", response_model=list[KnowledgeUserResponse]) async def get_knowledge(user=Depends(get_verified_user)): + # Return knowledge bases with read access knowledge_bases = [] - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: knowledge_bases = Knowledges.get_knowledge_bases() else: knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read") - # Get files for each knowledge base - knowledge_with_files = [] - for knowledge_base in knowledge_bases: - files = [] - if knowledge_base.data: - files = Files.get_file_metadatas_by_ids( - knowledge_base.data.get("file_ids", []) - ) - - # Check if all files exist - if len(files) != len(knowledge_base.data.get("file_ids", [])): - missing_files = list( - set(knowledge_base.data.get("file_ids", [])) - - set([file.id for file in files]) - ) - if missing_files: - data = knowledge_base.data or {} - file_ids = data.get("file_ids", []) - - for missing_file in missing_files: - file_ids.remove(missing_file) - - data["file_ids"] = file_ids - Knowledges.update_knowledge_data_by_id( - id=knowledge_base.id, data=data - ) - - files = Files.get_file_metadatas_by_ids(file_ids) - - knowledge_with_files.append( - KnowledgeUserResponse( - **knowledge_base.model_dump(), - files=files, - ) + return [ + KnowledgeUserResponse( + **knowledge_base.model_dump(), + files=Knowledges.get_file_metadatas_by_id(knowledge_base.id), ) - - return knowledge_with_files + for knowledge_base in knowledge_bases + ] @router.get("/list", response_model=list[KnowledgeUserResponse]) async def get_knowledge_list(user=Depends(get_verified_user)): + # Return knowledge bases with write access knowledge_bases = [] - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: knowledge_bases = Knowledges.get_knowledge_bases() else: knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "write") - # Get files for each knowledge base - knowledge_with_files = [] - for knowledge_base in knowledge_bases: - files = [] - if knowledge_base.data: - files = Files.get_file_metadatas_by_ids( - knowledge_base.data.get("file_ids", []) - ) - - # Check if all files exist - if len(files) != len(knowledge_base.data.get("file_ids", [])): - missing_files = list( - set(knowledge_base.data.get("file_ids", [])) - - set([file.id for file in files]) - ) - if missing_files: - data = knowledge_base.data or {} - file_ids = data.get("file_ids", []) - - for missing_file in missing_files: - file_ids.remove(missing_file) - - data["file_ids"] = file_ids - Knowledges.update_knowledge_data_by_id( - id=knowledge_base.id, data=data - ) - - files = Files.get_file_metadatas_by_ids(file_ids) - - knowledge_with_files.append( - KnowledgeUserResponse( - **knowledge_base.model_dump(), - files=files, - ) + return [ + KnowledgeUserResponse( + **knowledge_base.model_dump(), + files=Knowledges.get_file_metadatas_by_id(knowledge_base.id), ) - return knowledge_with_files + for knowledge_base in knowledge_bases + ] ############################ @@ -192,26 +133,9 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases") - deleted_knowledge_bases = [] - for knowledge_base in knowledge_bases: - # -- Robust error handling for missing or invalid data - if not knowledge_base.data or not isinstance(knowledge_base.data, dict): - log.warning( - f"Knowledge base {knowledge_base.id} has no data or invalid data ({knowledge_base.data!r}). Deleting." - ) - try: - Knowledges.delete_knowledge_by_id(id=knowledge_base.id) - deleted_knowledge_bases.append(knowledge_base.id) - except Exception as e: - log.error( - f"Failed to delete invalid knowledge base {knowledge_base.id}: {e}" - ) - continue - try: - file_ids = knowledge_base.data.get("file_ids", []) - files = Files.get_files_by_ids(file_ids) + files = Knowledges.get_files_by_id(knowledge_base.id) try: if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id): VECTOR_DB_CLIENT.delete_collection( @@ -251,9 +175,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us for failed in failed_files: log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}") - log.info( - f"Reindexing completed. Deleted {len(deleted_knowledge_bases)} invalid knowledge bases: {deleted_knowledge_bases}" - ) + log.info(f"Reindexing completed.") return True @@ -271,19 +193,15 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)): knowledge = Knowledges.get_knowledge_by_id(id=id) if knowledge: - if ( user.role == "admin" or knowledge.user_id == user.id or has_access(user.id, "read", knowledge.access_control) ): - file_ids = knowledge.data.get("file_ids", []) if knowledge.data else [] - files = Files.get_file_metadatas_by_ids(file_ids) - return KnowledgeFilesResponse( **knowledge.model_dump(), - files=files, + files=Knowledges.get_file_metadatas_by_id(knowledge.id), ) else: raise HTTPException( @@ -335,12 +253,9 @@ async def update_knowledge_by_id( knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data) if knowledge: - file_ids = knowledge.data.get("file_ids", []) if knowledge.data else [] - files = Files.get_file_metadatas_by_ids(file_ids) - return KnowledgeFilesResponse( **knowledge.model_dump(), - files=files, + files=Knowledges.get_file_metadatas_by_id(knowledge.id), ) else: raise HTTPException( @@ -366,7 +281,6 @@ def add_file_to_knowledge_by_id( user=Depends(get_verified_user), ): knowledge = Knowledges.get_knowledge_by_id(id=id) - if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -395,6 +309,11 @@ def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.FILE_NOT_PROCESSED, ) + # Add file to knowledge base + Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, file_id=form_data.file_id, user_id=user.id + ) + # Add content to the vector database try: process_file( @@ -410,32 +329,10 @@ def add_file_to_knowledge_by_id( ) if knowledge: - data = knowledge.data or {} - file_ids = data.get("file_ids", []) - - if form_data.file_id not in file_ids: - file_ids.append(form_data.file_id) - data["file_ids"] = file_ids - - knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data) - - if knowledge: - files = Files.get_file_metadatas_by_ids(file_ids) - - return KnowledgeFilesResponse( - **knowledge.model_dump(), - files=files, - ) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("knowledge"), - ) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("file_id"), - ) + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=Knowledges.get_file_metadatas_by_id(knowledge.id), + ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -494,14 +391,9 @@ def update_file_from_knowledge_by_id( ) if knowledge: - data = knowledge.data or {} - file_ids = data.get("file_ids", []) - - files = Files.get_file_metadatas_by_ids(file_ids) - return KnowledgeFilesResponse( **knowledge.model_dump(), - files=files, + files=Knowledges.get_file_metadatas_by_id(knowledge.id), ) else: raise HTTPException( @@ -546,6 +438,10 @@ def remove_file_from_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) + Knowledges.remove_file_from_knowledge_by_id( + knowledge_id=id, file_id=form_data.file_id + ) + # Remove content from the vector database try: VECTOR_DB_CLIENT.delete( @@ -575,31 +471,10 @@ def remove_file_from_knowledge_by_id( Files.delete_file_by_id(form_data.file_id) if knowledge: - data = knowledge.data or {} - file_ids = data.get("file_ids", []) - - if form_data.file_id in file_ids: - file_ids.remove(form_data.file_id) - data["file_ids"] = file_ids - - knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data) - if knowledge: - files = Files.get_file_metadatas_by_ids(file_ids) - - return KnowledgeFilesResponse( - **knowledge.model_dump(), - files=files, - ) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("knowledge"), - ) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("file_id"), - ) + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=Knowledges.get_file_metadatas_by_id(knowledge.id), + ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -700,8 +575,7 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)): log.debug(e) pass - knowledge = Knowledges.update_knowledge_data_by_id(id=id, data={"file_ids": []}) - + knowledge = Knowledges.reset_knowledge_by_id(id=id) return knowledge @@ -762,25 +636,19 @@ async def add_files_to_knowledge_batch( ) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) - # Add successful files to knowledge base - data = knowledge.data or {} - existing_file_ids = data.get("file_ids", []) - # Only add files that were successfully processed successful_file_ids = [r.file_id for r in result.results if r.status == "completed"] for file_id in successful_file_ids: - if file_id not in existing_file_ids: - existing_file_ids.append(file_id) - - data["file_ids"] = existing_file_ids - knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data) + Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, file_id=file_id, user_id=user.id + ) # If there were any errors, include them in the response if result.errors: error_details = [f"{err.file_id}: {err.error}" for err in result.errors] return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Files.get_file_metadatas_by_ids(existing_file_ids), + files=Knowledges.get_file_metadatas_by_id(knowledge.id), warnings={ "message": "Some files failed to process", "errors": error_details, @@ -789,5 +657,5 @@ async def add_files_to_knowledge_batch( return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Files.get_file_metadatas_by_ids(existing_file_ids), + files=Knowledges.get_file_metadatas_by_id(knowledge.id), ) From 9f6c91987fcd03033186b3ab4a2f9be505856efe Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 11:00:34 -0500 Subject: [PATCH 18/45] refac --- .../3e0e00844bb0_add_knowledge_file_table.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py index 02909ace9d..82249bb278 100644 --- a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py +++ b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py @@ -82,6 +82,12 @@ def upgrade() -> None: sa.Column("updated_at", sa.BigInteger()), ) + file_table = sa.Table( + "file", + sa.MetaData(), + sa.Column("id", sa.Text()), + ) + now = int(time.time()) for knowledge_id, user_id, data in results: if not data: @@ -98,8 +104,15 @@ def upgrade() -> None: file_ids = data.get("file_ids", []) - rows = [ - { + for file_id in file_ids: + file_exists = connection.execute( + sa.select(file_table.c.id).where(file_table.c.id == file_id) + ).fetchone() + + if not file_exists: + continue # skip non-existing files + + row = { "id": str(uuid.uuid4()), "user_id": user_id, "knowledge_id": knowledge_id, @@ -107,11 +120,7 @@ def upgrade() -> None: "created_at": now, "updated_at": now, } - for file_id in file_ids - ] - - if rows: - connection.execute(kf_table.insert(), rows) + connection.execute(kf_table.insert().values(**row)) with op.batch_alter_table("knowledge") as batch: batch.drop_column("data") From e301d1962e45900ababd3eabb7e9a2ad275a5761 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 11:11:17 -0500 Subject: [PATCH 19/45] refac/perf: has_access_to_file optimization --- backend/open_webui/models/knowledge.py | 15 +++++++++++++++ backend/open_webui/routers/files.py | 25 +++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 76548c8da4..2c72401181 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -217,6 +217,21 @@ class KnowledgeTable: except Exception: return None + def get_knowledges_by_file_id(self, file_id: str) -> list[KnowledgeModel]: + try: + with get_db() as db: + knowledges = ( + db.query(Knowledge) + .join(KnowledgeFile, Knowledge.id == KnowledgeFile.knowledge_id) + .filter(KnowledgeFile.file_id == file_id) + .all() + ) + return [ + KnowledgeModel.model_validate(knowledge) for knowledge in knowledges + ] + except Exception: + return [] + def get_files_by_id(self, knowledge_id: str) -> list[FileModel]: try: with get_db() as db: diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 1d27c6ab38..8af921bc7a 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -22,6 +22,7 @@ from fastapi import ( ) from fastapi.responses import FileResponse, StreamingResponse + from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT @@ -34,12 +35,19 @@ from open_webui.models.files import ( Files, ) from open_webui.models.knowledge import Knowledges +from open_webui.models.groups import Groups + from open_webui.routers.knowledge import get_knowledge, get_knowledge_list from open_webui.routers.retrieval import ProcessFileForm, process_file from open_webui.routers.audio import transcribe + from open_webui.storage.provider import Storage + + from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_access + from pydantic import BaseModel log = logging.getLogger(__name__) @@ -59,26 +67,31 @@ def has_access_to_file( ) -> bool: file = Files.get_file_by_id(file_id) log.debug(f"Checking if user has {access_type} access to file") - if not file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - has_access = False - knowledge_base_id = file.meta.get("collection_name") if file.meta else None + knowledge_bases = Knowledges.get_knowledges_by_file_id(file_id) + user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)} + for knowledge_base in knowledge_bases: + if knowledge_base.user_id == user.id or has_access( + user.id, access_type, knowledge_base.access_control, user_group_ids + ): + return True + + knowledge_base_id = file.meta.get("collection_name") if file.meta else None if knowledge_base_id: knowledge_bases = Knowledges.get_knowledge_bases_by_user_id( user.id, access_type ) for knowledge_base in knowledge_bases: if knowledge_base.id == knowledge_base_id: - has_access = True - break + return True - return has_access + return False ############################ From 01868e856a10f474f74fbd1b4425dafdf949222f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 11:24:23 -0500 Subject: [PATCH 20/45] enh: group members endpoint --- backend/open_webui/models/users.py | 10 ++++++++++ backend/open_webui/routers/groups.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 424d9676a5..86f9d011e8 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -452,6 +452,16 @@ class UsersTable: "total": total, } + def get_users_by_group_id(self, group_id: str) -> list[UserModel]: + with get_db() as db: + users = ( + db.query(User) + .join(GroupMember, User.id == GroupMember.user_id) + .filter(GroupMember.group_id == group_id) + .all() + ) + return [UserModel.model_validate(user) for user in users] + def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserStatusModel]: with get_db() as db: users = db.query(User).filter(User.id.in_(user_ids)).all() diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index 05d52c5c7b..7d2efcf899 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Optional import logging -from open_webui.models.users import Users +from open_webui.models.users import Users, UserInfoResponse from open_webui.models.groups import ( Groups, GroupForm, @@ -118,6 +118,24 @@ async def export_group_by_id(id: str, user=Depends(get_admin_user)): ) +############################ +# GetUsersInGroupById +############################ + + +@router.post("/id/{id}/users", response_model=list[UserInfoResponse]) +async def get_users_in_group(id: str, user=Depends(get_admin_user)): + try: + users = Users.get_users_by_group_id(id) + return users + except Exception as e: + log.exception(f"Error adding users to group {id}: {e}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + ############################ # UpdateGroupById ############################ From 34169b35819da70a21fe158d2ff9c8730d57bef6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 11:31:23 -0500 Subject: [PATCH 21/45] refac --- .../channel/ChannelInfoModal/AddMembersModal.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte b/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte index 241f863591..6b70ad820f 100644 --- a/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte +++ b/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte @@ -37,6 +37,16 @@ toast.error($i18n.t('Failed to add members')); } }; + + const reset = () => { + userIds = []; + groupIds = []; + loading = false; + }; + + $: if (!show) { + reset(); + } {#if channel} From e5c6b739c25804b5e926065bae492b69760cb5f4 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 11:43:00 -0500 Subject: [PATCH 22/45] refac --- src/lib/components/admin/Users/UserList.svelte | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index 238edb4f54..e51241d77d 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -33,6 +33,7 @@ import Banner from '$lib/components/common/Banner.svelte'; import Markdown from '$lib/components/chat/Messages/Markdown.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; + import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte'; const i18n = getContext('i18n'); @@ -356,11 +357,13 @@
- user + + user +
{user.name}
From a7e614ca4c915db6a2bbfe4bb7998dfb0a41d841 Mon Sep 17 00:00:00 2001 From: Henne <65833107+HennieLP@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:41:09 +0100 Subject: [PATCH 23/45] feat: Adds document intelligence model configuration (#19692) * Adds document intelligence model configuration Enables the configuration of the Document Intelligence model to be used by the RAG pipeline. This allows users to specify the model they want to use for document processing, providing flexibility and control over the extraction process. * Added Titel to Document Intelligence Model Config Added Titel to Document Intelligence Model Config --- backend/open_webui/config.py | 6 ++++++ backend/open_webui/main.py | 2 ++ backend/open_webui/retrieval/loaders/main.py | 2 ++ backend/open_webui/routers/retrieval.py | 9 +++++++++ src/lib/apis/retrieval/index.ts | 1 + src/lib/components/admin/Settings/Documents.svelte | 12 ++++++++++++ src/lib/i18n/locales/en-GB/translation.json | 2 ++ src/lib/i18n/locales/en-US/translation.json | 2 ++ 8 files changed, 36 insertions(+) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index a3a9050f78..41e88df5d2 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2590,6 +2590,12 @@ DOCUMENT_INTELLIGENCE_KEY = PersistentConfig( os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""), ) +DOCUMENT_INTELLIGENCE_MODEL = PersistentConfig( + "DOCUMENT_INTELLIGENCE_MODEL", + "rag.document_intelligence_model", + os.getenv("DOCUMENT_INTELLIGENCE_MODEL", "prebuilt-layout"), +) + MISTRAL_OCR_API_BASE_URL = PersistentConfig( "MISTRAL_OCR_API_BASE_URL", "rag.MISTRAL_OCR_API_BASE_URL", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 087dc5fb03..e1f3b39a3e 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -273,6 +273,7 @@ from open_webui.config import ( DOCLING_PARAMS, DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL, MISTRAL_OCR_API_BASE_URL, MISTRAL_OCR_API_KEY, RAG_TEXT_SPLITTER, @@ -871,6 +872,7 @@ app.state.config.DOCLING_API_KEY = DOCLING_API_KEY app.state.config.DOCLING_PARAMS = DOCLING_PARAMS app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY +app.state.config.DOCUMENT_INTELLIGENCE_MODEL = DOCUMENT_INTELLIGENCE_MODEL app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY app.state.config.MINERU_API_MODE = MINERU_API_MODE diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index fcc507e088..1346cd065c 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -322,12 +322,14 @@ class Loader: file_path=file_path, api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"), + api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"), ) else: loader = AzureAIDocumentIntelligenceLoader( file_path=file_path, api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), azure_credential=DefaultAzureCredential(), + api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"), ) elif self.engine == "mineru" and file_ext in [ "pdf" diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 190f001edd..b7ed993895 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -468,6 +468,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + "DOCUMENT_INTELLIGENCE_MODEL": request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, "MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, # MinerU settings @@ -647,6 +648,7 @@ class ConfigForm(BaseModel): DOCLING_PARAMS: Optional[dict] = None DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None + DOCUMENT_INTELLIGENCE_MODEL: Optional[str] = None MISTRAL_OCR_API_BASE_URL: Optional[str] = None MISTRAL_OCR_API_KEY: Optional[str] = None @@ -842,6 +844,11 @@ async def update_rag_config( if form_data.DOCUMENT_INTELLIGENCE_KEY is not None else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY ) + request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL = ( + form_data.DOCUMENT_INTELLIGENCE_MODEL + if form_data.DOCUMENT_INTELLIGENCE_MODEL is not None + else request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL + ) request.app.state.config.MISTRAL_OCR_API_BASE_URL = ( form_data.MISTRAL_OCR_API_BASE_URL @@ -1131,6 +1138,7 @@ async def update_rag_config( "DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + "DOCUMENT_INTELLIGENCE_MODEL": request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, "MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, # MinerU settings @@ -1543,6 +1551,7 @@ def process_file( PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL=request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, MISTRAL_OCR_API_BASE_URL=request.app.state.config.MISTRAL_OCR_API_BASE_URL, MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY, MINERU_API_MODE=request.app.state.config.MINERU_API_MODE, diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts index 5cb0f60a72..75065910d6 100644 --- a/src/lib/apis/retrieval/index.ts +++ b/src/lib/apis/retrieval/index.ts @@ -35,6 +35,7 @@ type ChunkConfigForm = { type DocumentIntelligenceConfigForm = { key: string; endpoint: string; + model: string; }; type ContentExtractConfigForm = { diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 26c23028ed..0b9accd4bf 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -597,6 +597,18 @@ required={false} />
+
+
{$i18n.t('Document Intelligence Model')}
+
+
+ +
+
+
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
Date: Tue, 2 Dec 2025 15:16:36 -0500 Subject: [PATCH 24/45] Fix dropdown backgrounds (#19693) --- src/lib/components/AddConnectionModal.svelte | 2 +- src/lib/components/AddToolServerModal.svelte | 4 ++-- .../admin/Settings/Evaluations/ArenaModelModal.svelte | 2 +- src/lib/components/admin/Settings/Models/ModelSelector.svelte | 2 +- src/lib/components/admin/Users/UserList/AddUserModal.svelte | 2 +- src/lib/components/chat/Settings/Account.svelte | 2 +- src/lib/components/chat/Settings/Audio.svelte | 2 +- src/lib/components/workspace/Models/ModelEditor.svelte | 2 +- src/lib/components/workspace/common/AccessControl.svelte | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index 5a75774fa0..557549098c 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -358,7 +358,7 @@
@@ -644,7 +644,7 @@
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index d3ce223a6a..906bed422f 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -546,7 +546,7 @@
{ if (e.target.value === 'public') { @@ -224,7 +224,7 @@
Date: Tue, 2 Dec 2025 22:48:00 +0100 Subject: [PATCH 30/45] fix: Default Group ID assignment on SSO/OAUTH and LDAP (#19685) * fix (#99) Co-authored-by: Tim Baek Co-authored-by: Claude * Update auths.py * unified logic * PUSH * remove getattr * rem getattr * whitespace * Update oauth.py * trusted header group sync Added default group re-application after trusted header group sync * not apply after syncs * . * rem --------- Co-authored-by: Tim Baek Co-authored-by: Claude --- backend/open_webui/routers/auths.py | 23 ++++++++++++++++++----- backend/open_webui/utils/groups.py | 24 ++++++++++++++++++++++++ backend/open_webui/utils/oauth.py | 8 +++++++- 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 backend/open_webui/utils/groups.py diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 0bf1d65d0c..3d83dcaea6 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -65,6 +65,7 @@ from open_webui.utils.auth import ( ) from open_webui.utils.webhook import post_webhook from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.utils.groups import apply_default_group_assignment from open_webui.utils.redis import get_redis_client from open_webui.utils.rate_limit import RateLimiter @@ -417,6 +418,11 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR ) + apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + ) + except HTTPException: raise except Exception as err: @@ -465,7 +471,6 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): ): if ENABLE_LDAP_GROUP_CREATION: Groups.create_groups_by_group_names(user.id, user_groups) - try: Groups.sync_groups_by_group_names(user.id, user_groups) log.info( @@ -722,9 +727,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm): # Disable signup after the first user is created request.app.state.config.ENABLE_SIGNUP = False - default_group_id = getattr(request.app.state.config, "DEFAULT_GROUP_ID", "") - if default_group_id and default_group_id: - Groups.add_users_to_group(default_group_id, [user.id]) + apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + ) return { "token": token, @@ -829,7 +835,9 @@ async def signout(request: Request, response: Response): @router.post("/add", response_model=SigninResponse) -async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)): +async def add_user( + request: Request, form_data: AddUserForm, user=Depends(get_admin_user) +): if not validate_email_format(form_data.email.lower()): raise HTTPException( status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT @@ -854,6 +862,11 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)): ) if user: + apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + ) + token = create_token(data={"id": user.id}) return { "token": token, diff --git a/backend/open_webui/utils/groups.py b/backend/open_webui/utils/groups.py new file mode 100644 index 0000000000..6198aec2e7 --- /dev/null +++ b/backend/open_webui/utils/groups.py @@ -0,0 +1,24 @@ +import logging +from open_webui.models.groups import Groups + +log = logging.getLogger(__name__) + + +def apply_default_group_assignment( + default_group_id: str, + user_id: str, +) -> None: + """ + Apply default group assignment to a user if default_group_id is provided. + + Args: + default_group_id: ID of the default group to add the user to + user_id: ID of the user to add to the default group + """ + if default_group_id: + try: + Groups.add_users_to_group(default_group_id, [user_id]) + except Exception as e: + log.error( + f"Failed to add user {user_id} to default group {default_group_id}: {e}" + ) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 9cd329a861..61c98ca744 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -72,6 +72,7 @@ from open_webui.env import ( 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 +from open_webui.utils.groups import apply_default_group_assignment from mcp.shared.auth import ( OAuthClientMetadata as MCPOAuthClientMetadata, @@ -1167,7 +1168,6 @@ class OAuthManager: log.debug( f"Removing user from group {group_model.name} as it is no longer in their oauth groups" ) - Groups.remove_users_from_group(group_model.id, [user.id]) # In case a group is created, but perms are never assigned to the group by hitting "save" @@ -1478,6 +1478,12 @@ class OAuthManager: "user": user.model_dump_json(exclude_none=True), }, ) + + apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + ) + else: raise HTTPException( status.HTTP_403_FORBIDDEN, From 4f9677ffcf837d0a3df1422c0849ae5df4f7ea35 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:48:11 +0100 Subject: [PATCH 31/45] Update translation.json (#19697) * Update translation.json * Update translation.json --- src/lib/i18n/locales/de-DE/translation.json | 70 ++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lib/i18n/locales/de-DE/translation.json b/src/lib/i18n/locales/de-DE/translation.json index aba589f140..6f13f83c5f 100644 --- a/src/lib/i18n/locales/de-DE/translation.json +++ b/src/lib/i18n/locales/de-DE/translation.json @@ -18,15 +18,15 @@ "{{COUNT}} words": "{{COUNT}} Wörter", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} um {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "Der Download von {{model}} wurde abgebrochen", - "{{NAMES}} reacted with {{REACTION}}": "", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} haben mit {{REACTION}} reagiert", "{{user}}'s Chats": "{{user}}s Chats", "{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich", "*Prompt node ID(s) are required for image generation": "*Prompt-Node-ID(s) sind für die Bildgenerierung erforderlich", "1 Source": "1 Quelle", - "A collaboration channel where people join as members": "", - "A discussion channel where access is controlled by groups and permissions": "", + "A collaboration channel where people join as members": "Ein Kollaborationskanal, dem Personen als Mitglieder beitreten", + "A discussion channel where access is controlled by groups and permissions": "Ein Diskussionskanal, dessen Zugriff durch Gruppen und Berechtigungen gesteuert wird", "A new version (v{{LATEST_VERSION}}) is now available.": "Eine neue Version (v{{LATEST_VERSION}}) ist jetzt verfügbar.", - "A private conversation between you and selected users": "", + "A private conversation between you and selected users": "Eine private Unterhaltung zwischen Ihnen und ausgewählten Benutzern", "A task model is used when performing tasks such as generating titles for chats and web search queries": "Aufgabenmodelle werden beispielsweise zur Generierung von Chat-Titeln oder Websuchanfragen verwendet", "a user": "ein Benutzer", "About": "Über", @@ -57,8 +57,8 @@ "Add Custom Prompt": "Benutzerdefinierten Prompt hinzufügen", "Add Details": "Details hinzufügen", "Add Files": "Dateien hinzufügen", - "Add Member": "", - "Add Members": "", + "Add Member": "Mitglied hinzufügen", + "Add Members": "Mitglieder hinzufügen", "Add Memory": "Erinnerung hinzufügen", "Add Model": "Modell hinzufügen", "Add Reaction": "Reaktion hinzufügen", @@ -257,7 +257,7 @@ "Citations": "Zitate", "Clear memory": "Alle Erinnerungen entfernen", "Clear Memory": "Alle Erinnerungen entfernen", - "Clear status": "", + "Clear status": "Status löschen", "click here": "hier klicken", "Click here for filter guides.": "Klicken Sie hier für Filteranleitungen.", "Click here for help.": "Klicken Sie hier für Hilfe.", @@ -294,7 +294,7 @@ "Code Interpreter": "Code-Interpreter", "Code Interpreter Engine": "Code Interpreter-Engine", "Code Interpreter Prompt Template": "Code Interpreter Prompt Vorlage", - "Collaboration channel where people join as members": "", + "Collaboration channel where people join as members": "Kollaborationskanal, dem Personen als Mitglieder beitreten", "Collapse": "Zuklappen", "Collection": "Kollektion", "Color": "Farbe", @@ -454,7 +454,7 @@ "Discover, download, and explore custom prompts": "Entdecken und beziehen Sie benutzerdefinierte Prompts", "Discover, download, and explore custom tools": "Entdecken und beziehen Sie benutzerdefinierte Werkzeuge", "Discover, download, and explore model presets": "Entdecken und beziehen Sie benutzerdefinierte Modellvorlagen", - "Discussion channel where access is based on groups and permissions": "", + "Discussion channel where access is based on groups and permissions": "Diskussionskanal, dessen Zugriff auf Gruppen und Berechtigungen basiert", "Display": "Anzeigen", "Display chat title in tab": "Chat-Titel im Tab anzeigen", "Display Emoji in Call": "Emojis im Anruf anzeigen", @@ -471,7 +471,7 @@ "Document": "Dokument", "Document Intelligence": "Dokumentenintelligenz", "Document Intelligence endpoint required.": "Dokumentenintelligenz-Endpunkt erforderlich.", - "Document Intelligence Model": "", + "Document Intelligence Model": "Dokomentenintelligenz Modell", "Documentation": "Dokumentation", "Documents": "Dokumente", "does not make any external connections, and your data stays securely on your locally hosted server.": "stellt keine externen Verbindungen her, und Ihre Daten bleiben sicher auf Ihrem lokal gehosteten Server.", @@ -494,15 +494,15 @@ "e.g. \"json\" or a JSON schema": "z. B. \"json\" oder ein JSON-Schema", "e.g. 60": "z. B. 60", "e.g. A filter to remove profanity from text": "z. B. Ein Filter, um Schimpfwörter aus Text zu entfernen", - "e.g. about the Roman Empire": "", + "e.g. about the Roman Empire": "z.B. über das Römische Reich", "e.g. en": "z. B. en", "e.g. My Filter": "z. B. Mein Filter", "e.g. My Tools": "z. B. Meine Werkzeuge", "e.g. my_filter": "z. B. mein_filter", "e.g. my_tools": "z. B. meine_werkzeuge", "e.g. pdf, docx, txt": "z. B. pdf, docx, txt", - "e.g. Tell me a fun fact": "", - "e.g. Tell me a fun fact about the Roman Empire": "", + "e.g. Tell me a fun fact": "z.B. Erzähl mir eine lustige Tatsache", + "e.g. Tell me a fun fact about the Roman Empire": "z.B. Erzähl mir eine lustige Tatsache über das Römische Reich", "e.g. Tools for performing various operations": "z. B. Werkzeuge für verschiedene Operationen", "e.g., 3, 4, 5 (leave blank for default)": "z. B. 3, 4, 5 (leer lassen für Standard)", "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "z. B. audio/wav,audio/mpeg,video/* (leer lassen für Standardwerte)", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "Docling Server-URL eingeben", "Enter Document Intelligence Endpoint": "Endpunkt für Document Intelligence eingeben", "Enter Document Intelligence Key": "Schlüssel für Document Intelligence eingeben", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "Dokumentenintelligenz-Modell eingeben", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "Kommaseparierte Domainnamen eingeben (z.B., erlaubteseite.com,!gesperrteseite.org)", "Enter Exa API Key": "Geben Sie den Exa-API-Schlüssel ein", "Enter External Document Loader API Key": "API-Schlüssel für externen Dokumenten-Loader eingeben", @@ -716,8 +716,8 @@ "External Web Search URL": "Externe Websuche URL", "Fade Effect for Streaming Text": "Überblendeffekt für Text-Streaming", "Failed to add file.": "Fehler beim Hinzufügen der Datei.", - "Failed to add members": "", - "Failed to clear status": "", + "Failed to add members": "Mitglieder konnten nicht hinzugefügt werden", + "Failed to clear status": "Status konnte nicht gelöscht werden", "Failed to connect to {{URL}} OpenAPI tool server": "Verbindung zum OpenAPI-Toolserver {{URL}} fehlgeschlagen", "Failed to copy link": "Fehler beim kopieren des Links", "Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.", @@ -731,14 +731,14 @@ "Failed to load file content.": "Fehler beim Laden des Dateiinhalts.", "Failed to move chat": "Chat konnte nicht verschoben werden", "Failed to read clipboard contents": "Fehler beim Lesen des Inhalts der Zwischenablage.", - "Failed to remove member": "", + "Failed to remove member": "Mitglied konnte nicht entfernt werden", "Failed to render diagram": "Diagramm konnte nicht gerendert werden", "Failed to render visualization": "Visualisierung konnte nicht gerendert werden", "Failed to save connections": "Verbindungen konnten nicht gespeichert werden", "Failed to save conversation": "Unterhaltung konnte nicht gespeichert werden", "Failed to save models configuration": "Fehler beim Speichern der Modellkonfiguration", "Failed to update settings": "Fehler beim Aktualisieren der Einstellungen", - "Failed to update status": "", + "Failed to update status": "Status konnte nicht aktualisiert werden", "Failed to upload file.": "Fehler beim Hochladen der Datei.", "Features": "Funktionalitäten", "Features Permissions": "Funktionen-Berechtigungen", @@ -832,13 +832,13 @@ "Google PSE Engine Id": "Google PSE-Engine-ID", "Gravatar": "Gravatar", "Group": "Gruppe", - "Group Channel": "", + "Group Channel": "Gruppenkanal", "Group created successfully": "Gruppe erfolgreich erstellt", "Group deleted successfully": "Gruppe erfolgreich gelöscht", "Group Description": "Gruppenbeschreibung", "Group Name": "Gruppenname", "Group updated successfully": "Gruppe erfolgreich aktualisiert", - "groups": "", + "groups": "Gruppen", "Groups": "Gruppen", "H1": "Überschrift 1", "H2": "Überschrift 2", @@ -1028,9 +1028,9 @@ "MCP": "MCP", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "Die MCP-Unterstützung ist experimentell und ihre Spezifikation ändert sich häufig, was zu Inkompatibilitäten führen kann. Die Unterstützung der OpenAPI-Spezifikation wird direkt vom Open‑WebUI‑Team gepflegt und ist daher die verlässlichere Option in Bezug auf Kompatibilität.", "Medium": "Mittel", - "Member removed successfully": "", - "Members": "", - "Members added successfully": "", + "Member removed successfully": "Mitglied erfolgreich entfernt", + "Members": "Mitglieder", + "Members added successfully": "Mitglieder erfolgreich hinzugefügt", "Memories accessible by LLMs will be shown here.": "Erinnerungen, die für Modelle zugänglich sind, werden hier angezeigt.", "Memory": "Erinnerungen", "Memory added successfully": "Erinnerung erfolgreich hinzugefügt", @@ -1130,7 +1130,7 @@ "No models selected": "Keine Modelle ausgewählt", "No Notes": "Keine Notizen", "No notes found": "Keine Notizen gefunden", - "No pinned messages": "", + "No pinned messages": "Keine angehefteten Nachrichten", "No prompts found": "Keine Prompts gefunden", "No results": "Keine Ergebnisse gefunden", "No results found": "Keine Ergebnisse gefunden", @@ -1178,7 +1178,7 @@ "Only alphanumeric characters and hyphens are allowed in the command string.": "In der Befehlszeichenfolge sind nur alphanumerische Zeichen und Bindestriche erlaubt.", "Only can be triggered when the chat input is in focus.": "Kann nur ausgelöst werden, wenn das Chat-Eingabefeld fokussiert ist.", "Only collections can be edited, create a new knowledge base to edit/add documents.": "Nur Sammlungen können bearbeitet werden. Erstellen Sie eine neue Wissensbasis, um Dokumente zu bearbeiten/hinzuzufügen.", - "Only invited users can access": "", + "Only invited users can access": "Nur eingeladene Benutzer haben Zugriff", "Only markdown files are allowed": "Nur Markdown-Dateien sind erlaubt", "Only select users and groups with permission can access": "Nur ausgewählte Benutzer und Gruppen mit Berechtigung können darauf zugreifen", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppla! Es scheint, dass die URL ungültig ist. Bitte überprüfen Sie diese und versuchen Sie es erneut.", @@ -1241,7 +1241,7 @@ "Personalization": "Personalisierung", "Pin": "Anheften", "Pinned": "Angeheftet", - "Pinned Messages": "", + "Pinned Messages": "Angeheftete Nachrichten", "Pioneer insights": "Bahnbrechende Erkenntnisse", "Pipe": "Pipe", "Pipeline deleted successfully": "Pipeline erfolgreich gelöscht", @@ -1284,7 +1284,7 @@ "Previous 7 days": "Vorherige 7 Tage", "Previous message": "Vorherige Nachricht", "Private": "Privat", - "Private conversation between selected users": "", + "Private conversation between selected users": "Private Unterhaltung zwischen ausgewählten Benutzern", "Profile": "Profil", "Prompt": "Prompt", "Prompt Autocompletion": "Prompt Autovervollständigung", @@ -1472,7 +1472,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Legt die Anzahl der für die Berechnung verwendeten Worker-Threads fest. Diese Option steuert, wie viele Threads zur gleichzeitigen Verarbeitung eingehender Anfragen verwendet werden. Eine Erhöhung dieses Wertes kann die Leistung bei hoher Parallelität verbessern, kann aber mehr CPU-Ressourcen verbrauchen.", "Set Voice": "Stimme festlegen", "Set whisper model": "Whisper-Modell festlegen", - "Set your status": "", + "Set your status": "Status festlegen", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Legt einen festen Bias gegen Token fest, die mindestens einmal erschienen sind. Ein höherer Wert (z.\u202fB. 1.5) bestraft Wiederholungen stärker, während ein niedrigerer Wert (z.\u202fB. 0.9) nachsichtiger ist. Bei 0 ist die Funktion deaktiviert.", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Legt einen skalierenden Bias gegen Token fest, um Wiederholungen basierend auf ihrer Häufigkeit zu bestrafen. Ein höherer Wert (z.\u202fB. 1.5) bestraft Wiederholungen stärker, während ein niedrigerer Wert (z.\u202fB. 0.9) nachsichtiger ist. Bei 0 ist die Funktion deaktiviert.", "Sets how far back for the model to look back to prevent repetition.": "Legt fest, wie weit das Modell zurückblickt, um Wiederholungen zu verhindern.", @@ -1525,9 +1525,9 @@ "Start a new conversation": "Neue Unterhaltung starten", "Start of the channel": "Beginn des Kanals", "Start Tag": "Start-Tag", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "Status", + "Status cleared successfully": "Status erfolgreich gelöscht", + "Status updated successfully": "Status erfolgreich aktualisiert", "Status Updates": "Statusaktualisierungen", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "Schritte", @@ -1543,7 +1543,7 @@ "STT Model": "STT-Modell", "STT Settings": "STT-Einstellungen", "Stylized PDF Export": "Stilisierter PDF-Export", - "Subtitle": "", + "Subtitle": "Untertitel", "Success": "Erfolg", "Successfully imported {{userCount}} users.": "Erfolgreich {{userCount}} Benutzer importiert.", "Successfully updated.": "Erfolgreich aktualisiert.", @@ -1694,7 +1694,7 @@ "Update and Copy Link": "Aktualisieren und Link kopieren", "Update for the latest features and improvements.": "Aktualisieren Sie für die neuesten Funktionen und Verbesserungen.", "Update password": "Passwort aktualisieren", - "Update your status": "", + "Update your status": "Aktualisiere deinen Status", "Updated": "Aktualisiert", "Updated at": "Aktualisiert am", "Updated At": "Aktualisiert am", @@ -1748,7 +1748,7 @@ "View Replies": "Antworten anzeigen", "View Result from **{{NAME}}**": "Ergebnis von **{{NAME}}** anzeigen", "Visibility": "Sichtbarkeit", - "Visible to all users": "", + "Visible to all users": "Sichtbar für alle Nutzer", "Vision": "Bilderkennung", "Voice": "Stimme", "Voice Input": "Spracheingabe", @@ -1776,7 +1776,7 @@ "What are you trying to achieve?": "Was versuchen Sie zu erreichen?", "What are you working on?": "Woran arbeiten Sie?", "What's New in": "Neuigkeiten von", - "What's on your mind?": "", + "What's on your mind?": "Was beschäftigt Sie gerade?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Wenn aktiviert, antwortet das Modell in Echtzeit auf jede Chat-Nachricht und generiert eine Antwort, sobald der Benutzer eine Nachricht sendet. Dieser Modus ist nützlich für Live-Chat-Anwendungen, kann jedoch die Leistung auf langsamerer Hardware beeinträchtigen.", "wherever you are": "wo immer Sie sind", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Ob die Ausgabe paginiert werden soll. Jede Seite wird durch eine horizontale Linie und eine Seitenzahl getrennt. Standardmäßig deaktiviert.", From 9d87688ecc99a25bdc5608ee1574703c8818bf4f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 16:53:05 -0500 Subject: [PATCH 32/45] chore: bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 899f3f5356..1572d43240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.40", + "version": "0.6.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.40", + "version": "0.6.41", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 97bdda0871..ae4bc3f8ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.40", + "version": "0.6.41", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", From 11efb982c171f4d1c2460c86a4c03c6c5ffdbc83 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 17:14:45 -0500 Subject: [PATCH 33/45] refac --- .../components/layout/Sidebar/ChatItem.svelte | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 41a14a1d37..46aedd2045 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -213,10 +213,15 @@ }; const onClickOutside = (event) => { - if (confirmEdit && !event.target.closest(`#chat-title-input-${id}`)) { - confirmEdit = false; - ignoreBlur = false; - chatTitle = ''; + if (!itemElement.contains(event.target)) { + if (confirmEdit) { + if (chatTitle !== title) { + editChatTitle(id, chatTitle); + } + + confirmEdit = false; + chatTitle = ''; + } } }; @@ -365,16 +370,6 @@ disabled={generating} on:keydown={chatTitleInputKeydownHandler} on:blur={async (e) => { - // check if target is generate button - if (ignoreBlur) { - ignoreBlur = false; - - if (e.relatedTarget?.id === 'generate-title-button') { - generateTitleHandler(); - } - return; - } - if (doubleClicked) { e.preventDefault(); e.stopPropagation(); @@ -388,13 +383,6 @@ doubleClicked = false; return; } - - if (chatTitle !== title) { - editChatTitle(id, chatTitle); - } - - confirmEdit = false; - chatTitle = ''; }} />
@@ -473,16 +461,8 @@ class=" self-center dark:hover:text-white transition disabled:cursor-not-allowed" id="generate-title-button" disabled={generating} - on:mouseenter={() => { - ignoreBlur = true; - }} - on:click={(e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - e.stopPropagation(); - + on:click={() => { generateTitleHandler(); - ignoreBlur = false; }} > From 8361f73ca6d5ad042f6017fc8f1d4b6fbea6e0f5 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:15:19 +0100 Subject: [PATCH 34/45] chore: 0.6.41 Changelog (#19473) * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cacf29521..d0730963ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,79 @@ 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.41] - 2025-12-02 + +### Added + +- 🚦 Sign-in rate limiting was implemented to protect against brute force attacks, limiting login attempts to 15 per 3-minute window per email address using Redis with automatic fallback to in-memory storage when Redis is unavailable. [Commit](https://github.com/open-webui/open-webui/commit/7b166370432414ce8f186747fb098e0c70fb2d6b) +- 📂 Administrators can now globally disable the folders feature and control user-level folder permissions through the admin panel, enabling minimalist interface configurations for deployments that don't require workspace organization features. [#19529](https://github.com/open-webui/open-webui/pull/19529), [#19210](https://github.com/open-webui/open-webui/discussions/19210), [#18459](https://github.com/open-webui/open-webui/discussions/18459), [#18299](https://github.com/open-webui/open-webui/discussions/18299) +- 👥 Group channels were introduced as a new channel type enabling membership-based collaboration spaces where users explicitly join as members rather than accessing through permissions, with support for public or private visibility, automatic member inclusion from specified user groups, member role tracking with invitation metadata, and post-creation member management allowing channel managers to add or remove members through the channel info modal. [Commit](https://github.com/open-webui/open-webui/commit/f589b7c1895a6a77166c047891acfa21bc0936c4), [Commit](https://github.com/open-webui/open-webui/commit/3f1d9ccbf8443a2fa5278f36202bad930a216680) +- 💬 Direct Message channels were introduced with a dedicated channel type selector and multi-user member selection interface, enabling private conversations between specific users without requiring full channel visibility. [Commit](https://github.com/open-webui/open-webui/commit/64b4d5d9c280b926746584aaf92b447d09deb386) +- 📨 Direct Message channels now support a complete user-to-user messaging system with member-based access control, automatic deduplication for one-on-one conversations, optional channel naming, and distinct visual presentation using participant avatars instead of channel icons. [Commit](https://github.com/open-webui/open-webui/commit/acccb9afdd557274d6296c70258bb897bbb6652f) +- 🙈 Users can now hide Direct Message channels from their sidebar while preserving message history, with automatic reactivation when new messages arrive from other participants, providing a cleaner interface for managing active conversations. [Commit](https://github.com/open-webui/open-webui/commit/acccb9afdd557274d6296c70258bb897bbb6652f) +- ☑️ A comprehensive user selection component was added to the channel creation modal, featuring search functionality, sortable user lists, pagination support, and multi-select checkboxes for building Direct Message participant lists. [Commit](https://github.com/open-webui/open-webui/commit/acccb9afdd557274d6296c70258bb897bbb6652f) +- 🔴 Channel unread message count tracking was implemented with visual badge indicators in the sidebar, automatically updating counts in real-time and marking messages as read when users view channels, with join/leave functionality to manage membership status. [Commit](https://github.com/open-webui/open-webui/commit/64b4d5d9c280b926746584aaf92b447d09deb386) +- 📌 Message pinning functionality was added to channels, allowing users to pin important messages for easy reference with visual highlighting, a dedicated pinned messages modal accessible from the navbar, and complete backend support for tracking pinned status, pin timestamp, and the user who pinned each message. [Commit](https://github.com/open-webui/open-webui/commit/64b4d5d9c280b926746584aaf92b447d09deb386), [Commit](https://github.com/open-webui/open-webui/commit/aae2fce17355419d9c29f8100409108037895201) +- 🟢 Direct Message channels now display an active status indicator for one-on-one conversations, showing a green dot when the other participant is currently online or a gray dot when offline. [Commit](https://github.com/open-webui/open-webui/commit/4b6773885cd7527c5a56b963781dac5e95105eec), [Commit](https://github.com/open-webui/open-webui/commit/39645102d14f34e71b34e5ddce0625790be33f6f) +- 🆔 Users can now start Direct Message conversations directly from user profile previews by clicking the "Message" button, enabling quick access to private messaging without navigating away from the current channel. [Commit](https://github.com/open-webui/open-webui/commit/a0826ec9fedb56320532616d568fa59dda831d4e) +- ⚡ Channel messages now appear instantly when sent using optimistic UI rendering, displaying with a pending state while the server confirms delivery, providing a more responsive messaging experience. [Commit](https://github.com/open-webui/open-webui/commit/25994dd3da90600401f53596d4e4fb067c1b8eaa) +- 👍 Channel message reactions now display the names of users who reacted when hovering over the emoji, showing up to three names with a count for additional reactors. [Commit](https://github.com/open-webui/open-webui/commit/05e79bdd0c7af70b631e958924e3656db1013b80) +- 🛠️ Channel creators can now edit and delete their own group and DM channels without requiring administrator privileges, enabling users to manage the channels they create independently. [Commit](https://github.com/open-webui/open-webui/commit/f589b7c1895a6a77166c047891acfa21bc0936c4) +- 🔌 A new API endpoint was added to directly get or create a Direct Message channel with a specific user by their ID, streamlining programmatic DM channel creation for integrations and frontend workflows. [Commit](https://github.com/open-webui/open-webui/commit/f589b7c1895a6a77166c047891acfa21bc0936c4) +- 💭 Users can now set a custom status with an emoji and message that displays in profile previews, the sidebar user menu, and Direct Message channel items in the sidebar, with the ability to clear status at any time, providing visibility into availability or current focus similar to team communication platforms. [Commit](https://github.com/open-webui/open-webui/commit/51621ba91a982e52da168ce823abffd11ad3e4fa), [Commit](https://github.com/open-webui/open-webui/commit/f5e8d4d5a004115489c35725408b057e24dfe318) +- 📤 A group export API endpoint was added, enabling administrators to export complete group data including member lists for backup and migration purposes. [Commit](https://github.com/open-webui/open-webui/commit/09b6ea38c579659f8ca43ae5ea3746df3ac561ad) +- 📡 A new API endpoint was added to retrieve all users belonging to a specific group, enabling programmatic access to group membership information for administrative workflows. [Commit](https://github.com/open-webui/open-webui/commit/01868e856a10f474f74fbd1b4425dafdf949222f) +- 👁️ The admin user list now displays an active status indicator next to each user, showing a visual green dot for users who have been active within the last three minutes. [Commit](https://github.com/open-webui/open-webui/commit/1b095d12ff2465b83afa94af89ded9593f8a8655) +- 🔑 The admin user edit modal now displays OAuth identity information with a per-provider breakdown, showing each linked identity provider and its associated subject identifier separately. [#19573](https://github.com/open-webui/open-webui/pull/19573) +- 🧩 OAuth role claim parsing now respects the "OAUTH_ROLES_SEPARATOR" configuration, enabling proper parsing of roles returned as comma-separated strings and providing consistent behavior with group claim handling. [#19514](https://github.com/open-webui/open-webui/pull/19514) +- 🎛️ Channel feature access can now be controlled through both the "USER_PERMISSIONS_FEATURES_CHANNELS" environment variable and group permission toggles in the admin panel, allowing administrators to restrict channel functionality for specific users or groups while defaulting to enabled for all users. [Commit](https://github.com/open-webui/open-webui/commit/f589b7c1895a6a77166c047891acfa21bc0936c4) +- 🎨 The model editor interface was refined with access control settings moved to a dedicated modal, group member counts now displayed when configuring permissions, reorganized layout with improved visual hierarchy, and redesigned prompt suggestions cards with tooltips for field guidance. [Commit](https://github.com/open-webui/open-webui/commit/e65d92fc6f49da5ca059e1c65a729e7973354b99), [Commit](https://github.com/open-webui/open-webui/commit/9d39b9b42c653ee2acf2674b2df343ecbceb4954) +- 🏗️ Knowledge base file management was rebuilt with a dedicated database table replacing the previous JSON array storage, enabling pagination support for large knowledge bases, significantly faster file listing performance, and more reliable file-knowledge base relationship tracking. [Commit](https://github.com/open-webui/open-webui/commit/d19023288e2ca40f86e2dc3fd9f230540f3e70d7) +- ☁️ Azure Document Intelligence model selection was added, allowing administrators to specify which model to use for document processing via the "DOCUMENT_INTELLIGENCE_MODEL" environment variable or admin UI setting, with "prebuilt-layout" as the default. [#19692](https://github.com/open-webui/open-webui/pull/19692), [Docs:#872](https://github.com/open-webui/docs/pull/872) +- 🚀 Milvus multitenancy vector database performance was improved by removing manual flush calls after upsert operations, eliminating rate limit errors and reducing load on etcd and MinIO/S3 storage by allowing Milvus to manage segment persistence automatically via its WAL and auto-flush policies. [#19680](https://github.com/open-webui/open-webui/pull/19680) +- ✨ Various improvements were implemented across the frontend and backend to enhance performance, stability, and security. +- 🌍 Translations for German, French, Portuguese (Brazil), Catalan, Simplified Chinese, and Traditional Chinese were enhanced and expanded. + +### Fixed + +- 🔄 Tool call response token duplication was fixed by removing redundant message history additions in non-native function calling mode, resolving an issue where tool results were included twice in the context and causing 2x token consumption. [#19656](https://github.com/open-webui/open-webui/issues/19656), [Commit](https://github.com/open-webui/open-webui/commit/52ccab8) +- 🛡️ Web search domain filtering was corrected to properly block results when any resolved hostname or IP address matches a blocked domain, preventing blocked sites from appearing in search results due to permissive hostname resolution logic that previously allowed results through if any single resolved address passed the filter. [#19670](https://github.com/open-webui/open-webui/pull/19670), [#19669](https://github.com/open-webui/open-webui/issues/19669) +- 🧠 Custom models based on Ollama or OpenAI now properly inherit the connection type from their base model, ensuring they appear correctly in the "Local" or "External" model selection tabs instead of only appearing under "All". [#19183](https://github.com/open-webui/open-webui/issues/19183), [Commit](https://github.com/open-webui/open-webui/commit/39f7575) +- 🐍 SentenceTransformers embedding initialization was fixed by updating the transformers dependency to version 4.57.3, resolving a regression in v0.6.40 where document ingestion failed with "'NoneType' object has no attribute 'encode'" errors due to a bug in transformers 4.57.2. [#19512](https://github.com/open-webui/open-webui/issues/19512), [#19513](https://github.com/open-webui/open-webui/pull/19513) +- 📈 Active user count accuracy was significantly improved by replacing the socket-based USER_POOL tracking with a database-backed heartbeat mechanism, resolving long-standing issues where Redis deployments displayed inflated user counts due to stale sessions never being cleaned up on disconnect. [#16074](https://github.com/open-webui/open-webui/discussions/16074), [Commit](https://github.com/open-webui/open-webui/commit/70948f8803e417459d5203839f8077fdbfbbb213) +- 👥 Default group assignment now applies consistently across all user registration methods including OAuth/SSO, LDAP, and admin-created users, fixing an issue where the "DEFAULT_GROUP_ID" setting was only being applied to users who signed up via the email/password signup form. [#19685](https://github.com/open-webui/open-webui/pull/19685) +- 🔦 Model list filtering in workspaces was corrected to properly include models shared with user groups, ensuring members can view models they have write access to through group permissions. [#19461](https://github.com/open-webui/open-webui/issues/19461), [Commit](https://github.com/open-webui/open-webui/commit/69722ba973768a5f689f2e2351bf583a8db9bba8) +- 🖼️ User profile image display in preview contexts was fixed by resolving a Pydantic validation error that prevented proper rendering. [Commit](https://github.com/open-webui/open-webui/commit/c7eb7136893b0ddfdc5d55ffc7a05bd84a00f5d6) +- 🔒 Redis TLS connection failures were resolved by updating the python-socketio dependency to version 5.15.0, restoring support for the "rediss://" URL schema. [#19480](https://github.com/open-webui/open-webui/issues/19480), [#19488](https://github.com/open-webui/open-webui/pull/19488) +- 📝 MCP tool server configuration was corrected to properly handle the "Function Name Filter List" as both string and list types, preventing AttributeError when the field is empty and ensuring backward compatibility. [#19486](https://github.com/open-webui/open-webui/issues/19486), [Commit](https://github.com/open-webui/open-webui/commit/c5b73d71843edc024325d4a6e625ec939a747279), [Commit](https://github.com/open-webui/open-webui/commit/477097c2e42985c14892301d0127314629d07df1) +- 📎 Web page attachment failures causing TypeError on metadata checks were resolved by correcting async threadpool parameter passing in vector database operations. [#19493](https://github.com/open-webui/open-webui/issues/19493), [Commit](https://github.com/open-webui/open-webui/commit/4370dee79e19d77062c03fba81780cb3b779fca3) +- 💾 Model allowlist persistence in multi-worker deployments was fixed by implementing Redis-based shared state for the internal models dictionary, ensuring configuration changes are consistently visible across all worker processes. [#19395](https://github.com/open-webui/open-webui/issues/19395), [Commit](https://github.com/open-webui/open-webui/commit/b5e5617d7f7ad3e4eec9f15f4cc7f07cb5afc2fa) +- ⏳ Chat history infinite loading was prevented by enhancing message data structure to properly track parent message relationships, resolving issues where missing parentId fields caused perpetual loading states. [#19225](https://github.com/open-webui/open-webui/issues/19225), [Commit](https://github.com/open-webui/open-webui/commit/ff4b1b9862d15adfa15eac17d2ce066c3d8ae38f) +- 🩹 Database migration robustness was improved by automatically detecting and correcting missing primary key constraints on the user table, ensuring successful schema upgrades for databases with non-standard configurations. [#19487](https://github.com/open-webui/open-webui/discussions/19487), [Commit](https://github.com/open-webui/open-webui/commit/453ea9b9a167c0b03d86c46e6efd086bf10056ce) +- 🏷️ OAuth group assignment now updates correctly on first login when users transition from admin to user role, ensuring group memberships reflect immediately when group management is enabled. [#19475](https://github.com/open-webui/open-webui/issues/19475), [#19476](https://github.com/open-webui/open-webui/pull/19476) +- 💡 Knowledge base file tooltips now properly display the parent collection name when referencing files with the hash symbol, preventing confusion between identically-named files in different collections. [#19491](https://github.com/open-webui/open-webui/issues/19491), [Commit](https://github.com/open-webui/open-webui/commit/3fe5a47b0ff84ac97f8e4ff56a19fa2ec065bf66) +- 🔐 Knowledge base file access inconsistencies were resolved where authorized non-admin users received "Not found" or permission errors for certain files due to race conditions during upload causing mismatched collection_name values, with file access validation now properly checking against knowledge base file associations. [#18689](https://github.com/open-webui/open-webui/issues/18689), [#19523](https://github.com/open-webui/open-webui/pull/19523), [Commit](https://github.com/open-webui/open-webui/commit/e301d1962e45900ababd3eabb7e9a2ad275a5761) +- 📦 Knowledge API batch file addition endpoint was corrected to properly handle async operations, resolving 500 Internal Server Error responses when adding multiple files simultaneously. [#19538](https://github.com/open-webui/open-webui/issues/19538), [Commit](https://github.com/open-webui/open-webui/commit/28659f60d94feb4f6a99bb1a5b54d7f45e5ea10f) +- 🤖 Embedding model auto-update functionality was fixed to properly respect the "RAG_EMBEDDING_MODEL_AUTO_UPDATE" setting by correctly passing the flag to the model path resolver, ensuring models update as expected when the auto-update option is enabled. [#19687](https://github.com/open-webui/open-webui/pull/19687) +- 📉 API response payload sizes were dramatically reduced by removing base64-encoded profile images from most endpoints, eliminating multi-megabyte responses caused by high-resolution avatars and enabling better browser caching. [#19519](https://github.com/open-webui/open-webui/issues/19519), [Commit](https://github.com/open-webui/open-webui/commit/384753c4c17f62a68d38af4bbcf55a21ee08e0f2) +- 📞 Redundant API calls on the admin user overview page were eliminated by consolidating reactive statements, reducing four duplicate requests to a single efficient call and significantly improving page load performance. [#19509](https://github.com/open-webui/open-webui/issues/19509), [Commit](https://github.com/open-webui/open-webui/commit/9f89cc5e9f7e1c6c9e2bc91177e08df7c79f66f9) +- 🧹 Duplicate API calls on the workspace models page were eliminated by removing redundant model list fetching, reducing two identical requests to a single call and improving page responsiveness. [#19517](https://github.com/open-webui/open-webui/issues/19517), [Commit](https://github.com/open-webui/open-webui/commit/d1bbf6be7a4d1d53fa8ad46ca4f62fc4b2e6a8cb) +- 🔘 The model valves button was corrected to prevent unintended form submission by adding explicit button type attribute, ensuring it no longer triggers message sending when the input area contains text. [#19534](https://github.com/open-webui/open-webui/pull/19534) +- 🗑️ Ollama model deletion was fixed by correcting the request payload format and ensuring the model selector properly displays the placeholder option. [Commit](https://github.com/open-webui/open-webui/commit/0f3156651c64bc5af188a65fc2908bdcecf30c74) +- 🎨 Image generation in temporary chats was fixed by correctly handling local chat sessions that are not persisted to the database. [Commit](https://github.com/open-webui/open-webui/commit/a7c7993bbf3a21cb7ba416525b89233cf2ad877f) +- 🕵️‍♂️ Audit logging was fixed by correctly awaiting the async user authentication call, resolving failures where coroutine objects were passed instead of user data. [#19658](https://github.com/open-webui/open-webui/pull/19658), [Commit](https://github.com/open-webui/open-webui/commit/dba86bc) +- 🌙 Dark mode select dropdown styling was corrected to use proper background colors, fixing an issue where dropdown borders and hover states appeared white instead of matching the dark theme. [#19693](https://github.com/open-webui/open-webui/pull/19693), [#19442](https://github.com/open-webui/open-webui/issues/19442) +- 🔍 Milvus vector database query filtering was fixed by correcting string quote handling in filter expressions and using the proper parameter name for queries, resolving false "duplicate content detected" errors that prevented uploading multiple files to knowledge bases. [#19602](https://github.com/open-webui/open-webui/pull/19602), [#18119](https://github.com/open-webui/open-webui/issues/18119), [#16345](https://github.com/open-webui/open-webui/issues/16345), [#17088](https://github.com/open-webui/open-webui/issues/17088), [#18485](https://github.com/open-webui/open-webui/issues/18485) +- 🆙 Milvus multitenancy vector database was updated to use query_iterator() for improved robustness and consistency with the standard Milvus implementation, fixing the same false duplicate detection errors and improving handling of large result sets in multi-tenant deployments. [#19695](https://github.com/open-webui/open-webui/pull/19695) + +### Changed + +- ⚠️ **IMPORTANT for Multi-Instance Deployments** — This release includes database schema changes; multi-worker, multi-server, or load-balanced deployments must update all instances simultaneously rather than performing rolling updates, as running mixed versions will cause application failures due to schema incompatibility between old and new instances. +- 👮 Channel creation is now restricted to administrators only, with the channel add button hidden for regular users to maintain organizational control over communication channels. [Commit](https://github.com/open-webui/open-webui/commit/421aba7cd7cd708168b1f2565026c74525a67905) +- ➖ The active user count indicator was removed from the bottom-left user menu in the sidebar to streamline the interface. [Commit](https://github.com/open-webui/open-webui/commit/848f3fd4d86ca66656e0ff0335773945af8d7d8d) +- 🗂️ The user table was restructured with API keys migrated to a dedicated table supporting future multi-key functionality, OAuth data storage converted to a JSON structure enabling multiple identity providers per user account, and internal column types optimized from TEXT to JSON for the "info" and "settings" fields, with automatic migration preserving all existing data and associations. [#19573](https://github.com/open-webui/open-webui/pull/19573) +- 🔄 The knowledge base API was restructured to support the new file relationship model. + ## [0.6.40] - 2025-11-25 ### Fixed From 73f7e91dec6b257826508df663ca43036ac40222 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 17:16:12 -0500 Subject: [PATCH 35/45] chore: format --- backend/open_webui/utils/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/groups.py b/backend/open_webui/utils/groups.py index 6198aec2e7..0f15f27e2c 100644 --- a/backend/open_webui/utils/groups.py +++ b/backend/open_webui/utils/groups.py @@ -10,7 +10,7 @@ def apply_default_group_assignment( ) -> None: """ Apply default group assignment to a user if default_group_id is provided. - + Args: default_group_id: ID of the default group to add the user to user_id: ID of the user to add to the default group From c86fcb75b7fc9959ebe80fa44bb489b6b71fd726 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 2 Dec 2025 19:59:14 -0500 Subject: [PATCH 36/45] chore --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3991050972..faa0129c65 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI) +Copyright (c) 2023- Open WebUI Inc. [Created by Timothy Jaeryang Baek] All rights reserved. Redistribution and use in source and binary forms, with or without From 85b1e21765307adf93fd3977bf1ccc2b346981b0 Mon Sep 17 00:00:00 2001 From: joaoback <156559121+joaoback@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:15:47 -0300 Subject: [PATCH 37/45] Update translation.json (pt-BR) (#19700) Translations of the new items added in the latest version. --- src/lib/i18n/locales/pt-BR/translation.json | 70 ++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index cde99b4dd3..42b24844d9 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -18,15 +18,15 @@ "{{COUNT}} words": "{{COUNT}} palavras", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} às {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "O download do {{model}} foi cancelado", - "{{NAMES}} reacted with {{REACTION}}": "", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} reagiu com {{REACTION}}", "{{user}}'s Chats": "Chats de {{user}}", "{{webUIName}} Backend Required": "Backend {{webUIName}} necessário", "*Prompt node ID(s) are required for image generation": "*Prompt node ID(s) são obrigatórios para gerar imagens", "1 Source": "1 Origem", - "A collaboration channel where people join as members": "", - "A discussion channel where access is controlled by groups and permissions": "", + "A collaboration channel where people join as members": "Um canal de colaboração onde as pessoas se juntam como membros.", + "A discussion channel where access is controlled by groups and permissions": "Um canal de discussão onde o acesso é controlado por grupos e permissões.", "A new version (v{{LATEST_VERSION}}) is now available.": "Um nova versão (v{{LATEST_VERSION}}) está disponível.", - "A private conversation between you and selected users": "", + "A private conversation between you and selected users": "Uma conversa privada entre você e usuários selecionados.", "A task model is used when performing tasks such as generating titles for chats and web search queries": "Um modelo de tarefa é usado ao realizar tarefas como gerar títulos para chats e consultas de pesquisa na web", "a user": "um usuário", "About": "Sobre", @@ -57,8 +57,8 @@ "Add Custom Prompt": "Adicionar prompt personalizado", "Add Details": "Adicionar detalhes", "Add Files": "Adicionar Arquivos", - "Add Member": "", - "Add Members": "", + "Add Member": "Adicionar membro", + "Add Members": "Adicionar membros", "Add Memory": "Adicionar Memória", "Add Model": "Adicionar Modelo", "Add Reaction": "Adicionar reação", @@ -257,7 +257,7 @@ "Citations": "Citações", "Clear memory": "Limpar memória", "Clear Memory": "Limpar Memória", - "Clear status": "", + "Clear status": "Limpar status", "click here": "Clique aqui", "Click here for filter guides.": "Clique aqui para obter instruções de filtros.", "Click here for help.": "Clique aqui para obter ajuda.", @@ -294,7 +294,7 @@ "Code Interpreter": "Intérprete de código", "Code Interpreter Engine": "Motor de interpretação de código", "Code Interpreter Prompt Template": "Modelo de Prompt do Interpretador de Código", - "Collaboration channel where people join as members": "", + "Collaboration channel where people join as members": "Canal de colaboração onde as pessoas se juntam como membros.", "Collapse": "Recolher", "Collection": "Coleção", "Color": "Cor", @@ -454,7 +454,7 @@ "Discover, download, and explore custom prompts": "Descubra, baixe e explore prompts personalizados", "Discover, download, and explore custom tools": "Descubra, baixe e explore ferramentas personalizadas", "Discover, download, and explore model presets": "Descubra, baixe e explore predefinições de modelos", - "Discussion channel where access is based on groups and permissions": "", + "Discussion channel where access is based on groups and permissions": "Canal de discussão onde o acesso é baseado em grupos e permissões.", "Display": "Exibir", "Display chat title in tab": "Exibir título do chat na aba", "Display Emoji in Call": "Exibir Emoji na Chamada", @@ -471,7 +471,7 @@ "Document": "Documento", "Document Intelligence": "Inteligência de documentos", "Document Intelligence endpoint required.": "É necessário o endpoint do Document Intelligence.", - "Document Intelligence Model": "", + "Document Intelligence Model": "Modelo de Inteligência de Documentos", "Documentation": "Documentação", "Documents": "Documentos", "does not make any external connections, and your data stays securely on your locally hosted server.": "não faz nenhuma conexão externa, e seus dados permanecem seguros no seu servidor local.", @@ -494,15 +494,15 @@ "e.g. \"json\" or a JSON schema": "por exemplo, \"json\" ou um esquema JSON", "e.g. 60": "por exemplo, 60", "e.g. A filter to remove profanity from text": "Exemplo: Um filtro para remover palavrões do texto", - "e.g. about the Roman Empire": "", + "e.g. about the Roman Empire": "Por exemplo, sobre o Império Romano.", "e.g. en": "por exemplo, en", "e.g. My Filter": "Exemplo: Meu Filtro", "e.g. My Tools": "Exemplo: Minhas Ferramentas", "e.g. my_filter": "Exemplo: my_filter", "e.g. my_tools": "Exemplo: my_tools", "e.g. pdf, docx, txt": "por exemplo, pdf, docx, txt", - "e.g. Tell me a fun fact": "", - "e.g. Tell me a fun fact about the Roman Empire": "", + "e.g. Tell me a fun fact": "Por exemplo: Conte-me uma curiosidade.", + "e.g. Tell me a fun fact about the Roman Empire": "Por exemplo: Conte-me uma curiosidade sobre o Império Romano.", "e.g. Tools for performing various operations": "Exemplo: Ferramentas para executar operações diversas", "e.g., 3, 4, 5 (leave blank for default)": "por exemplo, 3, 4, 5 (deixe em branco para o padrão)", "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "por exemplo, áudio/wav, áudio/mpeg, vídeo/* (deixe em branco para os padrões)", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "Digite a URL do servidor Docling", "Enter Document Intelligence Endpoint": "Insira o endpoint do Document Intelligence", "Enter Document Intelligence Key": "Insira a chave de inteligência do documento", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "Insira o modelo de inteligência de documentos", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "Insira os domínios separados por vírgulas (ex.: example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "Insira a chave da API Exa", "Enter External Document Loader API Key": "Insira a chave da API do carregador de documentos externo", @@ -716,8 +716,8 @@ "External Web Search URL": "URL de pesquisa na Web externa", "Fade Effect for Streaming Text": "Efeito de desbotamento para texto em streaming", "Failed to add file.": "Falha ao adicionar arquivo.", - "Failed to add members": "", - "Failed to clear status": "", + "Failed to add members": "Falha ao adicionar membros", + "Failed to clear status": "Falha ao limpar o status", "Failed to connect to {{URL}} OpenAPI tool server": "Falha ao conectar ao servidor da ferramenta OpenAPI {{URL}}", "Failed to copy link": "Falha ao copiar o link", "Failed to create API Key.": "Falha ao criar a Chave API.", @@ -731,14 +731,14 @@ "Failed to load file content.": "Falha ao carregar o conteúdo do arquivo.", "Failed to move chat": "Falha ao mover o chat", "Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência", - "Failed to remove member": "", + "Failed to remove member": "Falha ao remover membro", "Failed to render diagram": "Falha ao renderizar o diagrama", "Failed to render visualization": "Falha ao renderizar a visualização", "Failed to save connections": "Falha ao salvar conexões", "Failed to save conversation": "Falha ao salvar a conversa", "Failed to save models configuration": "Falha ao salvar a configuração dos modelos", "Failed to update settings": "Falha ao atualizar as configurações", - "Failed to update status": "", + "Failed to update status": "Falha ao atualizar o status", "Failed to upload file.": "Falha ao carregar o arquivo.", "Features": "Funcionalidades", "Features Permissions": "Permissões das Funcionalidades", @@ -832,13 +832,13 @@ "Google PSE Engine Id": "ID do Motor do Google PSE", "Gravatar": "", "Group": "Grupo", - "Group Channel": "", + "Group Channel": "Canal do grupo", "Group created successfully": "Grupo criado com sucesso", "Group deleted successfully": "Grupo excluído com sucesso", "Group Description": "Descrição do Grupo", "Group Name": "Nome do Grupo", "Group updated successfully": "Grupo atualizado com sucesso", - "groups": "", + "groups": "grupos", "Groups": "Grupos", "H1": "Título", "H2": "Subtítulo", @@ -1028,9 +1028,9 @@ "MCP": "", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "O suporte ao MCP é experimental e suas especificações mudam com frequência, o que pode levar a incompatibilidades. O suporte à especificação OpenAPI é mantido diretamente pela equipe do Open WebUI, tornando-o a opção mais confiável para compatibilidade.", "Medium": "Médio", - "Member removed successfully": "", - "Members": "", - "Members added successfully": "", + "Member removed successfully": "Membro removido com sucesso", + "Members": "Membros", + "Members added successfully": "Membros adicionados com sucesso", "Memories accessible by LLMs will be shown here.": "Memórias acessíveis por LLMs serão mostradas aqui.", "Memory": "Memória", "Memory added successfully": "Memória adicionada com sucesso", @@ -1130,7 +1130,7 @@ "No models selected": "Nenhum modelo selecionado", "No Notes": "Sem Notas", "No notes found": "Notas não encontradas", - "No pinned messages": "", + "No pinned messages": "Nenhuma mensagem fixada", "No prompts found": "Nenhum prompt encontrado", "No results": "Nenhum resultado encontrado", "No results found": "Nenhum resultado encontrado", @@ -1178,7 +1178,7 @@ "Only alphanumeric characters and hyphens are allowed in the command string.": "Apenas caracteres alfanuméricos e hífens são permitidos na string de comando.", "Only can be triggered when the chat input is in focus.": "Só pode ser acionado quando o campo de entrada do chat estiver em foco.", "Only collections can be edited, create a new knowledge base to edit/add documents.": "Somente coleções podem ser editadas. Crie uma nova base de conhecimento para editar/adicionar documentos.", - "Only invited users can access": "", + "Only invited users can access": "Somente usuários convidados podem acessar.", "Only markdown files are allowed": "Somente arquivos markdown são permitidos", "Only select users and groups with permission can access": "Somente usuários e grupos selecionados com permissão podem acessar.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ops! Parece que a URL é inválida. Por favor, verifique novamente e tente de novo.", @@ -1241,7 +1241,7 @@ "Personalization": "Personalização", "Pin": "Fixar", "Pinned": "Fixado", - "Pinned Messages": "", + "Pinned Messages": "Mensagens fixadas", "Pioneer insights": "Insights pioneiros", "Pipe": "", "Pipeline deleted successfully": "Pipeline excluído com sucesso", @@ -1284,7 +1284,7 @@ "Previous 7 days": "Últimos 7 dias", "Previous message": "Mensagem anterior", "Private": "Privado", - "Private conversation between selected users": "", + "Private conversation between selected users": "Conversa privada entre usuários selecionados", "Profile": "Perfil", "Prompt": "", "Prompt Autocompletion": "Preenchimento automático de prompts", @@ -1473,7 +1473,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Defina o número de threads de trabalho usadas para computação. Esta opção controla quantos threads são usados para processar as solicitações recebidas de forma simultânea. Aumentar esse valor pode melhorar o desempenho em cargas de trabalho de alta concorrência, mas também pode consumir mais recursos da CPU.", "Set Voice": "Definir Voz", "Set whisper model": "Definir modelo Whisper", - "Set your status": "", + "Set your status": "Defina seu status", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Define um viés fixo contra tokens que apareceram pelo menos uma vez. Um valor mais alto (por exemplo, 1,5) penalizará as repetições com mais força, enquanto um valor mais baixo (por exemplo, 0,9) será mais tolerante. Em 0, está desabilitado.", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "Define um viés de escala contra tokens para penalizar repetições, com base em quantas vezes elas apareceram. Um valor mais alto (por exemplo, 1,5) penalizará as repetições com mais rigor, enquanto um valor mais baixo (por exemplo, 0,9) será mais brando. Em 0, está desabilitado.", "Sets how far back for the model to look back to prevent repetition.": "Define até que ponto o modelo deve olhar para trás para evitar repetições.", @@ -1526,9 +1526,9 @@ "Start a new conversation": "Iniciar uma nova conversa", "Start of the channel": "Início do canal", "Start Tag": "Tag inicial", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "Status", + "Status cleared successfully": "Status liberado com sucesso", + "Status updated successfully": "Status atualizado com sucesso", "Status Updates": "Atualizações de status", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "Passos", @@ -1544,7 +1544,7 @@ "STT Model": "Modelo STT", "STT Settings": "Configurações STT", "Stylized PDF Export": "Exportação de PDF estilizado", - "Subtitle": "", + "Subtitle": "Legenda", "Success": "Sucesso", "Successfully imported {{userCount}} users.": "{{userCount}} usuários importados com sucesso.", "Successfully updated.": "Atualizado com sucesso.", @@ -1695,7 +1695,7 @@ "Update and Copy Link": "Atualizar e Copiar Link", "Update for the latest features and improvements.": "Atualizar para as novas funcionalidades e melhorias.", "Update password": "Atualizar senha", - "Update your status": "", + "Update your status": "Atualize seu status", "Updated": "Atualizado", "Updated at": "Atualizado em", "Updated At": "Atualizado Em", @@ -1749,7 +1749,7 @@ "View Replies": "Ver respostas", "View Result from **{{NAME}}**": "Ver resultado de **{{NAME}}**", "Visibility": "Visibilidade", - "Visible to all users": "", + "Visible to all users": "Visível para todos os usuários", "Vision": "Visão", "Voice": "Voz", "Voice Input": "Entrada de voz", @@ -1777,7 +1777,7 @@ "What are you trying to achieve?": "O que está tentando alcançar?", "What are you working on?": "No que está trabalhando?", "What's New in": "O que há de novo em", - "What's on your mind?": "", + "What's on your mind?": "O que você têm em mente?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Quando habilitado, o modelo responderá a cada mensagem de chat em tempo real, gerando uma resposta assim que o usuário enviar uma mensagem. Este modo é útil para aplicativos de chat ao vivo, mas pode impactar o desempenho em hardware mais lento.", "wherever you are": "onde quer que você esteja.", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Se a saída deve ser paginada. Cada página será separada por uma régua horizontal e um número de página. O padrão é Falso.", From 964333ed65bae29a0b403ee5568fc2ecc4d62b00 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Thu, 4 Dec 2025 06:16:03 +0800 Subject: [PATCH 38/45] i18n: improve Chinese translation (#19703) --- src/lib/i18n/locales/zh-CN/translation.json | 22 ++++++++++----------- src/lib/i18n/locales/zh-TW/translation.json | 22 ++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index b4c2e4b300..c8d7b350e8 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -257,7 +257,7 @@ "Citations": "引用", "Clear memory": "清除记忆", "Clear Memory": "清除记忆", - "Clear status": "", + "Clear status": "清除状态", "click here": "点击此处", "Click here for filter guides.": "点击此处查看筛选指南", "Click here for help.": "点击此处获取帮助", @@ -471,7 +471,7 @@ "Document": "文档", "Document Intelligence": "Document Intelligence", "Document Intelligence endpoint required.": "Document Intelligence 接口地址是必填项。", - "Document Intelligence Model": "", + "Document Intelligence Model": "Document Intelligence 模型", "Documentation": "帮助文档", "Documents": "文档", "does not make any external connections, and your data stays securely on your locally hosted server.": "不会与外部建立任何连接,您的数据会安全地存储在本地托管的服务器上。", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "输入 Docling 服务器接口地址", "Enter Document Intelligence Endpoint": "输入 Document Intelligence 端点", "Enter Document Intelligence Key": "输入 Document Intelligence 密钥", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "输入 Document Intelligence 模型", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "输入域名,多个域名用逗号分隔(例如:example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "输入 Exa API 密钥", "Enter External Document Loader API Key": "输入外部文档加载器接口密钥", @@ -717,7 +717,7 @@ "Fade Effect for Streaming Text": "流式输出内容时启用动态渐显效果", "Failed to add file.": "添加文件失败", "Failed to add members": "添加成员失败", - "Failed to clear status": "", + "Failed to clear status": "清除状态失败", "Failed to connect to {{URL}} OpenAPI tool server": "连接到 {{URL}} OpenAPI 工具服务器失败", "Failed to copy link": "复制链接失败", "Failed to create API Key.": "创建接口密钥失败", @@ -738,7 +738,7 @@ "Failed to save conversation": "保存对话失败", "Failed to save models configuration": "保存模型配置失败", "Failed to update settings": "更新设置失败", - "Failed to update status": "", + "Failed to update status": "更新状态失败", "Failed to upload file.": "上传文件失败", "Features": "功能", "Features Permissions": "功能权限", @@ -1471,7 +1471,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "设置用于计算的工作线程数量。该选项可控制并发处理传入请求的线程数量。增加该值可以提高高并发工作负载下的性能,但也可能消耗更多的 CPU 资源。", "Set Voice": "设置音色", "Set whisper model": "设置 whisper 模型", - "Set your status": "", + "Set your status": "设置您的状态", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "对至少出现过一次的标记设置固定偏置值。较高的值(例如 1.5)表示严厉惩罚重复,而较低的值(例如 0.9)则表示相对宽松。当值为 0 时,此功能将被禁用。", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "根据标记出现的次数,设置缩放偏置值惩罚重复。较高的值(例如 1.5)表示严厉惩罚重复,而较低的值(例如 0.9)则表示相对宽松。当值为 0 时,此功能将被禁用。", "Sets how far back for the model to look back to prevent repetition.": "设置模型回溯的范围,以防止重复。", @@ -1524,9 +1524,9 @@ "Start a new conversation": "开始新对话", "Start of the channel": "频道起点", "Start Tag": "起始标签", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "状态", + "Status cleared successfully": "状态已清除", + "Status updated successfully": "状态已更新", "Status Updates": "显示实时回答状态", "STDOUT/STDERR": "标准输出/标准错误", "Steps": "迭代步数", @@ -1693,7 +1693,7 @@ "Update and Copy Link": "更新和复制链接", "Update for the latest features and improvements.": "更新以获取最新功能与优化", "Update password": "更新密码", - "Update your status": "", + "Update your status": "更新您的状态", "Updated": "已更新", "Updated at": "更新于", "Updated At": "更新于", @@ -1775,7 +1775,7 @@ "What are you trying to achieve?": "您想要达到什么目标?", "What are you working on?": "您在忙于什么?", "What's New in": "最近更新内容于", - "What's on your mind?": "", + "What's on your mind?": "聊聊您的想法吧!", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "启用后,模型将实时回答每条对话信息,在用户发送信息后立即生成回答。这种模式适用于即时对话应用,但在性能较低的设备上可能会影响响应速度", "wherever you are": "纵使身在远方,亦与世界同频", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "是否对输出内容进行分页。每页之间将用水平分隔线和页码隔开。默认为关闭", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index e0f3c62aa7..6e610e3fa1 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -257,7 +257,7 @@ "Citations": "引用", "Clear memory": "清除記憶", "Clear Memory": "清除記憶", - "Clear status": "", + "Clear status": "清除狀態", "click here": "點選此處", "Click here for filter guides.": "點選此處檢視篩選器指南。", "Click here for help.": "點選此處取得協助。", @@ -471,7 +471,7 @@ "Document": "檔案", "Document Intelligence": "Document Intelligence", "Document Intelligence endpoint required.": "需要提供 Document Intelligence 端點。", - "Document Intelligence Model": "", + "Document Intelligence Model": "Document Intelligence 模型", "Documentation": "說明檔案", "Documents": "檔案", "does not make any external connections, and your data stays securely on your locally hosted server.": "不會建立任何外部連線,而且您的資料會安全地儲存在您本機伺服器上。", @@ -576,7 +576,7 @@ "Enter Docling Server URL": "請輸入 Docling 伺服器 URL", "Enter Document Intelligence Endpoint": "輸入 Document Intelligence 端點", "Enter Document Intelligence Key": "輸入 Document Intelligence 金鑰", - "Enter Document Intelligence Model": "", + "Enter Document Intelligence Model": "輸入 Document Intelligence 模型", "Enter domains separated by commas (e.g., example.com,site.org,!excludedsite.com)": "輸入網域,以逗號分隔(例如:example.com,site.org,!excludedsite.com)", "Enter Exa API Key": "輸入 Exa API 金鑰", "Enter External Document Loader API Key": "請輸入外部文件載入器 API 金鑰", @@ -717,7 +717,7 @@ "Fade Effect for Streaming Text": "串流文字淡入效果", "Failed to add file.": "新增檔案失敗。", "Failed to add members": "新增成員失敗", - "Failed to clear status": "", + "Failed to clear status": "清除狀態失敗", "Failed to connect to {{URL}} OpenAPI tool server": "無法連線至 {{URL}} OpenAPI 工具伺服器", "Failed to copy link": "複製連結失敗", "Failed to create API Key.": "建立 API 金鑰失敗。", @@ -738,7 +738,7 @@ "Failed to save conversation": "儲存對話失敗", "Failed to save models configuration": "儲存模型設定失敗", "Failed to update settings": "更新設定失敗", - "Failed to update status": "", + "Failed to update status": "更新狀態失敗", "Failed to upload file.": "上傳檔案失敗。", "Features": "功能", "Features Permissions": "功能權限", @@ -1471,7 +1471,7 @@ "Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "設定用於計算的工作執行緒數量。此選項控制使用多少執行緒來同時處理傳入的請求。增加此值可以在高併發工作負載下提升效能,但也可能消耗更多 CPU 資源。", "Set Voice": "設定語音", "Set whisper model": "設定 whisper 模型", - "Set your status": "", + "Set your status": "設定您的狀態", "Sets a flat bias against tokens that have appeared at least once. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "對至少出現過一次的 token 設定統一的偏差值。較高的值(例如:1.5)會更強烈地懲罰重複,而較低的值(例如:0.9)會更寬容。設為 0 時,此功能將停用。", "Sets a scaling bias against tokens to penalize repetitions, based on how many times they have appeared. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. At 0, it is disabled.": "根據 token 出現的次數,設定一個縮放偏差值來懲罰重複。較高的值(例如:1.5)會更強烈地懲罰重複,而較低的值(例如:0.9)會更寬容。設為 0 時,此功能將停用。", "Sets how far back for the model to look back to prevent repetition.": "設定模型要回溯多遠來防止重複。", @@ -1524,9 +1524,9 @@ "Start a new conversation": "開始新對話", "Start of the channel": "頻道起點", "Start Tag": "起始標籤", - "Status": "", - "Status cleared successfully": "", - "Status updated successfully": "", + "Status": "狀態", + "Status cleared successfully": "狀態已清除", + "Status updated successfully": "狀態已更新", "Status Updates": "顯示實時回答狀態", "STDOUT/STDERR": "STDOUT/STDERR", "Steps": "迭代步數", @@ -1693,7 +1693,7 @@ "Update and Copy Link": "更新並複製連結", "Update for the latest features and improvements.": "更新以獲得最新功能和改進。", "Update password": "更新密碼", - "Update your status": "", + "Update your status": "更新您的狀態", "Updated": "已更新", "Updated at": "更新於", "Updated At": "更新於", @@ -1775,7 +1775,7 @@ "What are you trying to achieve?": "您正在試圖完成什麼?", "What are you working on?": "您現在的工作是什麼?", "What's New in": "新功能", - "What's on your mind?": "", + "What's on your mind?": "聊聊您的想法?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "啟用時,模型將即時回應每個對話訊息,在使用者傳送訊息後立即生成回應。此模式適用於即時對話應用程式,但在較慢的硬體上可能會影響效能。", "wherever you are": "無論您在何處", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "是否啟用輸出分頁功能,每頁會以水平線與頁碼分隔。預設為 False。", From b5607757131031c1a85dccebacf3c8c400ce6a9c Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Thu, 4 Dec 2025 06:16:38 +0800 Subject: [PATCH 39/45] fix: do not display the move button when no folders (#19705) --- src/lib/components/layout/Navbar/Menu.svelte | 54 +++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index a64e438ef0..6d5d478719 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -437,33 +437,37 @@ {#if !$temporaryChatEnabled && chat?.id}
- - - + {#if $folders.length > 0} + + + -
{$i18n.t('Move')}
-
- - {#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder} - { - moveChatHandler(chat?.id, folder?.id); - }} - > - +
{$i18n.t('Move')}
+
+ + {#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder} + {#if folder?.id} + { + moveChatHandler(chat.id, folder.id); + }} + > + -
{folder?.name ?? 'Folder'}
-
- {/each} -
-
+
{folder.name ?? 'Folder'}
+ + {/if} + {/each} + + + {/if} Date: Wed, 3 Dec 2025 17:17:05 -0500 Subject: [PATCH 40/45] fix: UI scale buttons (- and +) (#19699) --- src/lib/components/chat/Settings/Interface.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index e1bf8aa64b..b905e09b11 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -364,7 +364,7 @@ type="button" class="rounded-lg p-1 transition outline-gray-200 hover:bg-gray-100 dark:outline-gray-700 dark:hover:bg-gray-800" on:click={() => { - textScale = Math.max(1, textScale); + textScale = Math.max(1, parseFloat((textScale - 0.1).toFixed(2))); setTextScaleHandler(textScale); }} aria-labelledby="ui-scale-label" @@ -397,7 +397,7 @@ type="button" class="rounded-lg p-1 transition outline-gray-200 hover:bg-gray-100 dark:outline-gray-700 dark:hover:bg-gray-800" on:click={() => { - textScale = Math.min(1.5, textScale); + textScale = Math.min(1.5, parseFloat((textScale + 0.1).toFixed(2))); setTextScaleHandler(textScale); }} aria-labelledby="ui-scale-label" From 54b7ec56d6bcd2d79addc1694b757dab18cf18c5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 17:52:44 -0500 Subject: [PATCH 41/45] enh/refac: channels message lazy load data --- backend/open_webui/routers/channels.py | 59 +++++++++++++++++-- src/lib/apis/channels/index.ts | 38 ++++++++++++ src/lib/components/channel/Messages.svelte | 1 + .../channel/Messages/Message.svelte | 25 +++++++- 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 0dff67da3e..a6d9323d32 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -5,7 +5,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks from pydantic import BaseModel - +from pydantic import field_validator from open_webui.socket.main import ( emit_to_users, @@ -666,7 +666,16 @@ async def delete_channel_by_id( class MessageUserResponse(MessageResponse): - pass + data: bool | None = None + + @field_validator("data", mode="before") + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) @router.get("/{id}/messages", response_model=list[MessageUserResponse]) @@ -1108,7 +1117,7 @@ async def post_new_message( ############################ -@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageUserResponse]) +@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageResponse]) async def get_channel_message( id: str, message_id: str, user=Depends(get_verified_user) ): @@ -1142,7 +1151,7 @@ async def get_channel_message( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() ) - return MessageUserResponse( + return MessageResponse( **{ **message.model_dump(), "user": UserNameResponse( @@ -1152,6 +1161,48 @@ async def get_channel_message( ) +############################ +# GetChannelMessageData +############################ + + +@router.get("/{id}/messages/{message_id}/data", response_model=Optional[dict]) +async def get_channel_message_data( + id: str, message_id: str, user=Depends(get_verified_user) +): + channel = Channels.get_channel_by_id(id) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if channel.type in ["group", "dm"]: + if not Channels.is_user_channel_member(channel.id, user.id): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + else: + if user.role != "admin" and not has_access( + user.id, type="read", access_control=channel.access_control + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() + ) + + message = Messages.get_message_by_id(message_id) + if not message: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + if message.channel_id != id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + return message.data + + ############################ # PinChannelMessage ############################ diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index 0731b2ea9f..44817e97ef 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -491,6 +491,44 @@ export const getChannelThreadMessages = async ( return res; }; +export const getMessageData = async ( + token: string = '', + channel_id: string, + message_id: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/data`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + type MessageForm = { temp_id?: string; reply_to_id?: string; diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte index b0d1fb54db..7ad8798297 100644 --- a/src/lib/components/channel/Messages.svelte +++ b/src/lib/components/channel/Messages.svelte @@ -126,6 +126,7 @@ {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)} { + if (message && message?.data) { + const res = await getMessageData(localStorage.token, channel?.id, message.id); + if (res) { + message.data = res; + } + } + }; + + onMount(async () => { + if (message && message?.data) { + await loadMessageData(); + } + }); {/if} - {#if (message?.data?.files ?? []).length > 0} + {#if message?.data === true} + +
+ +
+ {:else if (message?.data?.files ?? []).length > 0}
{#each message?.data?.files as file}
From 39f778c275e13f6ebef9b64c6893393f3af71f3a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 18:22:00 -0500 Subject: [PATCH 42/45] refac --- backend/open_webui/models/messages.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 98be21463d..5b068b6449 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -9,7 +9,7 @@ from open_webui.models.users import Users, User, UserNameResponse from open_webui.models.channels import Channels, ChannelMember -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON from sqlalchemy import or_, func, select, and_, text from sqlalchemy.sql import exists @@ -108,11 +108,24 @@ class MessageUserResponse(MessageModel): user: Optional[UserNameResponse] = None +class MessageUserSlimResponse(MessageUserResponse): + data: bool | None = None + + @field_validator("data", mode="before") + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) + + class MessageReplyToResponse(MessageUserResponse): - reply_to_message: Optional[MessageUserResponse] = None + reply_to_message: Optional[MessageUserSlimResponse] = None -class MessageWithReactionsResponse(MessageUserResponse): +class MessageWithReactionsResponse(MessageUserSlimResponse): reactions: list[Reactions] From 22f1b764a7ea1add0a896906a9ef00b4b6743adc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 3 Dec 2025 19:06:02 -0500 Subject: [PATCH 43/45] refac/perf: channel image upload behaviour --- backend/open_webui/routers/files.py | 2 +- src/lib/apis/files/index.ts | 14 +++++++++++-- .../components/channel/MessageInput.svelte | 21 ++++++++----------- .../channel/Messages/Message.svelte | 8 +++++-- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 8af921bc7a..e10722c0c8 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -179,7 +179,7 @@ def upload_file_handler( user=Depends(get_verified_user), background_tasks: Optional[BackgroundTasks] = None, ): - log.info(f"file.content_type: {file.content_type}") + log.info(f"file.content_type: {file.content_type} {process}") if isinstance(metadata, str): try: diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts index 8351393e3c..07042c4ade 100644 --- a/src/lib/apis/files/index.ts +++ b/src/lib/apis/files/index.ts @@ -1,16 +1,26 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; import { splitStream } from '$lib/utils'; -export const uploadFile = async (token: string, file: File, metadata?: object | null) => { +export const uploadFile = async ( + token: string, + file: File, + metadata?: object | null, + process?: boolean | null +) => { const data = new FormData(); data.append('file', file); if (metadata) { data.append('metadata', JSON.stringify(metadata)); } + const searchParams = new URLSearchParams(); + if (process !== undefined && process !== null) { + searchParams.append('process', String(process)); + } + let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/files/?${searchParams.toString()}`, { method: 'POST', headers: { Accept: 'application/json', diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index fef7c3a078..337a3affb8 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -421,13 +421,10 @@ imageUrl = await compressImageHandler(imageUrl, $settings, $config); } - files = [ - ...files, - { - type: 'image', - url: `${imageUrl}` - } - ]; + const blob = await (await fetch(imageUrl)).blob(); + const compressedFile = new File([blob], file.name, { type: file.type }); + + uploadFileHandler(compressedFile, false); }; reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file); @@ -437,7 +434,7 @@ }); }; - const uploadFileHandler = async (file) => { + const uploadFileHandler = async (file, process = true) => { const tempItemId = uuidv4(); const fileItem = { type: 'file', @@ -461,7 +458,6 @@ try { // During the file upload, file content is automatically extracted. - // If the file is an audio file, provide the language for STT. let metadata = null; if ( @@ -473,7 +469,7 @@ }; } - const uploadedFile = await uploadFile(localStorage.token, file, metadata); + const uploadedFile = await uploadFile(localStorage.token, file, metadata, process); if (uploadedFile) { console.info('File upload completed:', { @@ -492,6 +488,7 @@ fileItem.id = uploadedFile.id; fileItem.collection_name = uploadedFile?.meta?.collection_name || uploadedFile?.collection_name; + fileItem.content_type = uploadedFile.meta?.content_type || uploadedFile.content_type; fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`; files = files; @@ -807,11 +804,11 @@ {#if files.length > 0}
{#each files as file, fileIdx} - {#if file.type === 'image'} + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')}
diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index c54a96d528..a31f7b0412 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -341,8 +341,12 @@
{#each message?.data?.files as file}
- {#if file.type === 'image'} - {file.name} + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')} + {file.name} {:else} Date: Wed, 3 Dec 2025 19:15:30 -0500 Subject: [PATCH 44/45] enh: channels video file upload behaviour --- src/lib/components/channel/Messages/Message.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index a31f7b0412..ab52a84eb6 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -347,6 +347,12 @@ alt={file.name} imageClassName=" max-h-96 rounded-lg" /> + {:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')} + {:else} Date: Wed, 3 Dec 2025 19:31:23 -0500 Subject: [PATCH 45/45] refac --- backend/open_webui/routers/channels.py | 6 ++++++ backend/open_webui/utils/files.py | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index a6d9323d32..58cdcdc661 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -39,6 +39,8 @@ from open_webui.models.messages import ( ) +from open_webui.utils.files import get_image_base64_from_file_id + from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS @@ -915,6 +917,10 @@ async def model_response_handler(request, channel, message, user): for file in thread_message_files: if file.get("type", "") == "image": images.append(file.get("url", "")) + elif file.get("content_type", "").startswith("image/"): + image = get_image_base64_from_file_id(file.get("id", "")) + if image: + images.append(image) thread_history_string = "\n\n".join(thread_history) system_message = { diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py index 4f9564b7d4..cd94a41144 100644 --- a/backend/open_webui/utils/files.py +++ b/backend/open_webui/utils/files.py @@ -10,7 +10,11 @@ from fastapi import ( Request, UploadFile, ) +from typing import Optional +from pathlib import Path +from open_webui.storage.provider import Storage +from open_webui.models.files import Files from open_webui.routers.files import upload_file_handler import mimetypes @@ -113,3 +117,26 @@ def get_file_url_from_base64(request, base64_file_string, metadata, user): elif "data:audio/wav;base64" in base64_file_string: return get_audio_url_from_base64(request, base64_file_string, metadata, user) return None + + +def get_image_base64_from_file_id(id: str) -> Optional[str]: + file = Files.get_file_by_id(id) + if not file: + return None + + try: + file_path = Storage.get_file(file.path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + import base64 + + with open(file_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + content_type, _ = mimetypes.guess_type(file_path.name) + return f"data:{content_type};base64,{encoded_string}" + else: + return None + except Exception as e: + return None