Date: Fri, 13 Feb 2026 14:48:10 -0600
Subject: [PATCH 19/32] refac
---
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte | 2 +-
src/lib/components/workspace/Skills/SkillEditor.svelte | 2 +-
src/lib/components/workspace/Tools/ToolkitEditor.svelte | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
index 6b796b998c..c85308c9e1 100644
--- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
+++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte
@@ -854,7 +854,7 @@
Date: Fri, 13 Feb 2026 14:55:13 -0600
Subject: [PATCH 20/32] refac
Co-Authored-By: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
---
backend/open_webui/routers/openai.py | 135 ++++++++++++++++++++++++++-
1 file changed, 133 insertions(+), 2 deletions(-)
diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py
index f8688a9c93..a5c08b1d63 100644
--- a/backend/open_webui/routers/openai.py
+++ b/backend/open_webui/routers/openai.py
@@ -455,8 +455,13 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
async def get_filtered_models(models, user, db=None):
# Filter models based on user access control
model_ids = [model["id"] for model in models.get("data", [])]
- model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)}
- user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
+ model_infos = {
+ model_info.id: model_info
+ for model_info in Models.get_models_by_ids(model_ids, db=db)
+ }
+ user_group_ids = {
+ group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
+ }
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
@@ -1215,6 +1220,115 @@ async def embeddings(request: Request, form_data: dict, user):
await cleanup_response(r, session)
+@router.post("/responses")
+async def responses(request: Request, user=Depends(get_verified_user)):
+ """
+ Forward requests to the OpenAI Responses API endpoint.
+ Routes to the correct upstream backend based on the model field.
+ """
+ body = await request.body()
+
+ try:
+ payload = json.loads(body)
+ except (json.JSONDecodeError, ValueError):
+ raise HTTPException(status_code=400, detail="Invalid JSON payload")
+
+ if not isinstance(payload, dict):
+ raise HTTPException(
+ status_code=400,
+ detail="Invalid payload: expected JSON object",
+ )
+
+ idx = 0
+ model_id = payload.get("model")
+ if model_id:
+ models = request.app.state.OPENAI_MODELS
+ if not models or model_id not in models:
+ await get_all_models(request, user=user)
+ models = request.app.state.OPENAI_MODELS
+ if model_id in models:
+ idx = models[model_id]["urlIdx"]
+
+ url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
+ key = request.app.state.config.OPENAI_API_KEYS[idx]
+ api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
+ str(idx),
+ request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
+ )
+
+ r = None
+ session = None
+ streaming = False
+
+ try:
+ headers, cookies = await get_headers_and_cookies(
+ request, url, key, api_config, user=user
+ )
+
+ if api_config.get("azure", False):
+ api_version = api_config.get("api_version", "2023-03-15-preview")
+
+ auth_type = api_config.get("auth_type", "bearer")
+ if auth_type not in ("azure_ad", "microsoft_entra_id"):
+ headers["api-key"] = key
+
+ headers["api-version"] = api_version
+
+ model = payload.get("model", "")
+ request_url = (
+ f"{url}/openai/deployments/{model}/responses?api-version={api_version}"
+ )
+ else:
+ request_url = f"{url}/responses"
+
+ session = aiohttp.ClientSession(
+ trust_env=True,
+ timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
+ )
+ r = await session.request(
+ method="POST",
+ url=request_url,
+ data=body,
+ headers=headers,
+ cookies=cookies,
+ ssl=AIOHTTP_CLIENT_SESSION_SSL,
+ )
+
+ # Check if response is SSE
+ if "text/event-stream" in r.headers.get("Content-Type", ""):
+ streaming = True
+ return StreamingResponse(
+ stream_wrapper(r, session),
+ status_code=r.status,
+ headers=dict(r.headers),
+ )
+ else:
+ try:
+ response_data = await r.json()
+ except Exception:
+ response_data = await r.text()
+
+ if r.status >= 400:
+ if isinstance(response_data, (dict, list)):
+ return JSONResponse(status_code=r.status, content=response_data)
+ else:
+ return PlainTextResponse(
+ status_code=r.status, content=response_data
+ )
+
+ return response_data
+
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=r.status if r else 500,
+ detail="Open WebUI: Server Connection Error",
+ )
+ finally:
+ if not streaming:
+ await cleanup_response(r, session)
+
+
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
"""
@@ -1223,7 +1337,24 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
body = await request.body()
+ # Parse JSON body to resolve model-based routing
+ payload = None
+ if body:
+ try:
+ payload = json.loads(body)
+ except (json.JSONDecodeError, ValueError):
+ payload = None
+
idx = 0
+ model_id = payload.get("model") if isinstance(payload, dict) else None
+ if model_id:
+ models = request.app.state.OPENAI_MODELS
+ if not models or model_id not in models:
+ await get_all_models(request, user=user)
+ models = request.app.state.OPENAI_MODELS
+ if model_id in models:
+ idx = models[model_id]["urlIdx"]
+
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
From 0f3f68b0c43367ccd10f29db34bcbe30696023ee Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Fri, 13 Feb 2026 21:56:53 +0100
Subject: [PATCH 21/32] enh (#21362)
---
backend/open_webui/models/chats.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py
index 6040050fc3..ccd01a1945 100644
--- a/backend/open_webui/models/chats.py
+++ b/backend/open_webui/models/chats.py
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.tags import TagModel, Tag, Tags
from open_webui.models.folders import Folders
-from open_webui.models.chat_messages import ChatMessages
+from open_webui.models.chat_messages import ChatMessage, ChatMessages
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
from pydantic import BaseModel, ConfigDict
@@ -621,6 +621,9 @@ class ChatTable:
) -> bool:
try:
with get_db_context(db) as db:
+ # Use subquery to delete chat_messages for shared chats
+ shared_chat_subq = db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_chat_subq)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
@@ -1410,6 +1413,7 @@ class ChatTable:
def delete_chat_by_id(self, id: str, db: Optional[Session] = None) -> bool:
try:
with get_db_context(db) as db:
+ db.query(ChatMessage).filter_by(chat_id=id).delete()
db.query(Chat).filter_by(id=id).delete()
db.commit()
@@ -1422,6 +1426,7 @@ class ChatTable:
) -> bool:
try:
with get_db_context(db) as db:
+ db.query(ChatMessage).filter_by(chat_id=id).delete()
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
db.commit()
@@ -1436,6 +1441,8 @@ class ChatTable:
with get_db_context(db) as db:
self.delete_shared_chats_by_user_id(user_id, db=db)
+ chat_id_subq = db.query(Chat.id).filter_by(user_id=user_id).subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subq)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
@@ -1448,6 +1455,8 @@ class ChatTable:
) -> bool:
try:
with get_db_context(db) as db:
+ chat_id_subq = db.query(Chat.id).filter_by(user_id=user_id, folder_id=folder_id).subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subq)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
db.commit()
@@ -1481,6 +1490,9 @@ class ChatTable:
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
+ # Use subquery to delete chat_messages for shared chats
+ shared_id_subq = db.query(Chat.id).filter(Chat.user_id.in_(shared_chat_ids)).subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_id_subq)).delete(synchronize_session=False)
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
db.commit()
From a9b8677cc01f63990992f556dc4290e1f63e0d63 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 14:59:05 -0600
Subject: [PATCH 22/32] refac
---
backend/open_webui/models/chats.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py
index ccd01a1945..125d6cc3cd 100644
--- a/backend/open_webui/models/chats.py
+++ b/backend/open_webui/models/chats.py
@@ -622,8 +622,8 @@ class ChatTable:
try:
with get_db_context(db) as db:
# Use subquery to delete chat_messages for shared chats
- shared_chat_subq = db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_chat_subq)).delete(synchronize_session=False)
+ shared_chat_id_subquery = db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_chat_id_subquery)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
@@ -1441,8 +1441,8 @@ class ChatTable:
with get_db_context(db) as db:
self.delete_shared_chats_by_user_id(user_id, db=db)
- chat_id_subq = db.query(Chat.id).filter_by(user_id=user_id).subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subq)).delete(synchronize_session=False)
+ chat_id_subquery = db.query(Chat.id).filter_by(user_id=user_id).subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subquery)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
@@ -1455,8 +1455,8 @@ class ChatTable:
) -> bool:
try:
with get_db_context(db) as db:
- chat_id_subq = db.query(Chat.id).filter_by(user_id=user_id, folder_id=folder_id).subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subq)).delete(synchronize_session=False)
+ chat_id_subquery = db.query(Chat.id).filter_by(user_id=user_id, folder_id=folder_id).subquery()
+ db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subquery)).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
db.commit()
From 79ecbfc757f0642740d0e44fab98263d84295490 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 14:59:20 -0600
Subject: [PATCH 23/32] refac
---
backend/open_webui/routers/openai.py | 44 ++++++++++++++++++----------
1 file changed, 29 insertions(+), 15 deletions(-)
diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py
index a5c08b1d63..e2bb296945 100644
--- a/backend/open_webui/routers/openai.py
+++ b/backend/open_webui/routers/openai.py
@@ -18,7 +18,7 @@ from fastapi.responses import (
JSONResponse,
PlainTextResponse,
)
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
from sqlalchemy.orm import Session
@@ -1220,27 +1220,41 @@ async def embeddings(request: Request, form_data: dict, user):
await cleanup_response(r, session)
+class ResponsesForm(BaseModel):
+ model_config = ConfigDict(extra="allow")
+
+ model: str
+ input: Optional[list | str] = None
+ instructions: Optional[str] = None
+ stream: Optional[bool] = None
+ temperature: Optional[float] = None
+ max_output_tokens: Optional[int] = None
+ top_p: Optional[float] = None
+ tools: Optional[list] = None
+ tool_choice: Optional[str | dict] = None
+ text: Optional[dict] = None
+ truncation: Optional[str] = None
+ metadata: Optional[dict] = None
+ store: Optional[bool] = None
+ reasoning: Optional[dict] = None
+ previous_response_id: Optional[str] = None
+
+
@router.post("/responses")
-async def responses(request: Request, user=Depends(get_verified_user)):
+async def responses(
+ request: Request,
+ form_data: ResponsesForm,
+ user=Depends(get_verified_user),
+):
"""
Forward requests to the OpenAI Responses API endpoint.
Routes to the correct upstream backend based on the model field.
"""
- body = await request.body()
-
- try:
- payload = json.loads(body)
- except (json.JSONDecodeError, ValueError):
- raise HTTPException(status_code=400, detail="Invalid JSON payload")
-
- if not isinstance(payload, dict):
- raise HTTPException(
- status_code=400,
- detail="Invalid payload: expected JSON object",
- )
+ payload = form_data.model_dump(exclude_none=True)
+ body = json.dumps(payload)
idx = 0
- model_id = payload.get("model")
+ model_id = form_data.model
if model_id:
models = request.app.state.OPENAI_MODELS
if not models or model_id not in models:
From 626d236d137915aee090d8a82dc0a57fbc22424b Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 15:00:39 -0600
Subject: [PATCH 24/32] chore: format
---
backend/open_webui/models/chats.py | 36 +++++++++++++++++++------
backend/open_webui/models/users.py | 11 +++++---
backend/open_webui/routers/knowledge.py | 5 +++-
backend/open_webui/routers/models.py | 5 +++-
backend/open_webui/routers/notes.py | 5 +++-
backend/open_webui/routers/ollama.py | 23 ++++++++++++----
backend/open_webui/routers/prompts.py | 5 +++-
backend/open_webui/routers/scim.py | 16 +++--------
backend/open_webui/routers/skills.py | 5 +++-
backend/open_webui/routers/tools.py | 5 +++-
10 files changed, 81 insertions(+), 35 deletions(-)
diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py
index 125d6cc3cd..7ae9f7a38b 100644
--- a/backend/open_webui/models/chats.py
+++ b/backend/open_webui/models/chats.py
@@ -622,8 +622,12 @@ class ChatTable:
try:
with get_db_context(db) as db:
# Use subquery to delete chat_messages for shared chats
- shared_chat_id_subquery = db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_chat_id_subquery)).delete(synchronize_session=False)
+ shared_chat_id_subquery = (
+ db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
+ )
+ db.query(ChatMessage).filter(
+ ChatMessage.chat_id.in_(shared_chat_id_subquery)
+ ).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
@@ -1441,8 +1445,12 @@ class ChatTable:
with get_db_context(db) as db:
self.delete_shared_chats_by_user_id(user_id, db=db)
- chat_id_subquery = db.query(Chat.id).filter_by(user_id=user_id).subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subquery)).delete(synchronize_session=False)
+ chat_id_subquery = (
+ db.query(Chat.id).filter_by(user_id=user_id).subquery()
+ )
+ db.query(ChatMessage).filter(
+ ChatMessage.chat_id.in_(chat_id_subquery)
+ ).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
@@ -1455,8 +1463,14 @@ class ChatTable:
) -> bool:
try:
with get_db_context(db) as db:
- chat_id_subquery = db.query(Chat.id).filter_by(user_id=user_id, folder_id=folder_id).subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(chat_id_subquery)).delete(synchronize_session=False)
+ chat_id_subquery = (
+ db.query(Chat.id)
+ .filter_by(user_id=user_id, folder_id=folder_id)
+ .subquery()
+ )
+ db.query(ChatMessage).filter(
+ ChatMessage.chat_id.in_(chat_id_subquery)
+ ).delete(synchronize_session=False)
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
db.commit()
@@ -1491,8 +1505,14 @@ class ChatTable:
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
# Use subquery to delete chat_messages for shared chats
- shared_id_subq = db.query(Chat.id).filter(Chat.user_id.in_(shared_chat_ids)).subquery()
- db.query(ChatMessage).filter(ChatMessage.chat_id.in_(shared_id_subq)).delete(synchronize_session=False)
+ shared_id_subq = (
+ db.query(Chat.id)
+ .filter(Chat.user_id.in_(shared_chat_ids))
+ .subquery()
+ )
+ db.query(ChatMessage).filter(
+ ChatMessage.chat_id.in_(shared_id_subq)
+ ).delete(synchronize_session=False)
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
db.commit()
diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py
index 2eb76131ab..e5da9231df 100644
--- a/backend/open_webui/models/users.py
+++ b/backend/open_webui/models/users.py
@@ -363,9 +363,7 @@ class UsersTable:
query = db.query(User)
if dialect_name == "sqlite":
query = query.filter(
- User.scim.contains(
- {provider: {"external_id": external_id}}
- )
+ User.scim.contains({provider: {"external_id": external_id}})
)
elif dialect_name == "postgresql":
query = query.filter(
@@ -533,7 +531,12 @@ class UsersTable:
self, user_ids: list[str], db: Optional[Session] = None
) -> list[UserStatusModel]:
with get_db_context(db) as db:
- users = db.query(User).options(defer(User.profile_image_url)).filter(User.id.in_(user_ids)).all()
+ users = (
+ db.query(User)
+ .options(defer(User.profile_image_url))
+ .filter(User.id.in_(user_ids))
+ .all()
+ )
return [UserModel.model_validate(user) for user in users]
def get_num_users(self, db: Optional[Session] = None) -> Optional[int]:
diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py
index d620c1745f..1fedab4466 100644
--- a/backend/open_webui/routers/knowledge.py
+++ b/backend/open_webui/routers/knowledge.py
@@ -567,7 +567,10 @@ async def update_knowledge_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py
index aa573dd720..e93d8a729d 100644
--- a/backend/open_webui/routers/models.py
+++ b/backend/open_webui/routers/models.py
@@ -577,7 +577,10 @@ async def update_model_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants(
diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py
index cba4c3f4ca..8d1a66c4af 100644
--- a/backend/open_webui/routers/notes.py
+++ b/backend/open_webui/routers/notes.py
@@ -358,7 +358,10 @@ async def update_note_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py
index 394735e898..580717987c 100644
--- a/backend/open_webui/routers/ollama.py
+++ b/backend/open_webui/routers/ollama.py
@@ -418,8 +418,13 @@ async def get_all_models(request: Request, user: UserModel = None):
async def get_filtered_models(models, user, db=None):
# Filter models based on user access control
model_ids = [model["model"] for model in models.get("models", [])]
- model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)}
- user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
+ model_infos = {
+ model_info.id: model_info
+ for model_info in Models.get_models_by_ids(model_ids, db=db)
+ }
+ user_group_ids = {
+ group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
+ }
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
@@ -1633,8 +1638,13 @@ async def get_openai_models(
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
# Filter models based on user access control
model_ids = [model["id"] for model in models]
- model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)}
- user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
+ model_infos = {
+ model_info.id: model_info
+ for model_info in Models.get_models_by_ids(model_ids, db=db)
+ }
+ user_group_ids = {
+ group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
+ }
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
@@ -1650,7 +1660,10 @@ async def get_openai_models(
for model in models:
model_info = model_infos.get(model["id"])
if model_info:
- if user.id == model_info.user_id or model_info.id in accessible_model_ids:
+ if (
+ user.id == model_info.user_id
+ or model_info.id in accessible_model_ids
+ ):
filtered_models.append(model)
models = filtered_models
diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py
index 0060ab2b18..2491578959 100644
--- a/backend/open_webui/routers/prompts.py
+++ b/backend/open_webui/routers/prompts.py
@@ -486,7 +486,10 @@ async def update_prompt_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py
index 13d3b5bdf2..0c16eb99bd 100644
--- a/backend/open_webui/routers/scim.py
+++ b/backend/open_webui/routers/scim.py
@@ -329,9 +329,7 @@ def get_scim_provider() -> str:
return SCIM_AUTH_PROVIDER
-def find_user_by_external_id(
- external_id: str, db=None
-) -> Optional[UserModel]:
+def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]:
"""Find a user by SCIM externalId, falling back to OAuth sub match."""
provider = get_scim_provider()
user = Users.get_user_by_scim_external_id(provider, external_id, db=db)
@@ -652,9 +650,7 @@ async def create_user(
# Store externalId in the scim field
if user_data.externalId:
provider = get_scim_provider()
- Users.update_user_scim_by_id(
- user_id, provider, user_data.externalId, db=db
- )
+ Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db)
new_user = Users.get_user_by_id(user_id, db=db)
return user_to_scim(new_user, request, db=db)
@@ -711,9 +707,7 @@ async def update_user(
# Update externalId in the scim field
if user_data.externalId:
provider = get_scim_provider()
- Users.update_user_scim_by_id(
- user_id, provider, user_data.externalId, db=db
- )
+ Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db)
updated_user = Users.get_user_by_id(user_id, db=db)
return user_to_scim(updated_user, request, db=db)
@@ -755,9 +749,7 @@ async def patch_user(
update_data["name"] = value
elif path == "externalId":
provider = get_scim_provider()
- Users.update_user_scim_by_id(
- user_id, provider, value, db=db
- )
+ Users.update_user_scim_by_id(user_id, provider, value, db=db)
# Update user
if update_data:
diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py
index 2a51b993c8..fb7b01b87f 100644
--- a/backend/open_webui/routers/skills.py
+++ b/backend/open_webui/routers/skills.py
@@ -354,7 +354,10 @@ async def update_skill_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py
index 60fecbb6fc..6657b34462 100644
--- a/backend/open_webui/routers/tools.py
+++ b/backend/open_webui/routers/tools.py
@@ -568,7 +568,10 @@ async def update_tool_access_by_id(
form_data.access_grants = [
grant
for grant in form_data.access_grants
- if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*")
+ if not (
+ grant.get("principal_type") == "user"
+ and grant.get("principal_id") == "*"
+ )
]
AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
From b36f8d9314e2a127b88d521a53c804a7cdee4b48 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 15:00:47 -0600
Subject: [PATCH 25/32] chore: format
---
backend/open_webui/routers/channels.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py
index 3add5023e7..e3cdc46a16 100644
--- a/backend/open_webui/routers/channels.py
+++ b/backend/open_webui/routers/channels.py
@@ -540,9 +540,7 @@ async def get_channel_members_by_id(
return {
"users": [
- UserModelResponse(
- **user.model_dump(), is_active=Users.is_active(user)
- )
+ UserModelResponse(**user.model_dump(), is_active=Users.is_active(user))
for user in users
],
"total": total,
@@ -575,9 +573,7 @@ async def get_channel_members_by_id(
return {
"users": [
- UserModelResponse(
- **user.model_dump(), is_active=Users.is_active(user)
- )
+ UserModelResponse(**user.model_dump(), is_active=Users.is_active(user))
for user in users
],
"total": total,
From e5d88be4f3896f3407d762d4b2a24e0bb7447f06 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 15:08:20 -0600
Subject: [PATCH 26/32] doc: changelog
---
CHANGELOG.md | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff30a13565..ae2bed4b60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,9 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- π **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce `sharing.public_*` permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358)
-- π **PostgreSQL analytics query fix.** The `get_chat_ids_by_model_id` function now works correctly with PostgreSQL by using `GROUP BY` with aggregate ordering instead of `DISTINCT` with non-aggregate `ORDER BY`, which PostgreSQL does not support. [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
-- π **Skills PostgreSQL compatibility fix.** Skills now work correctly with PostgreSQL by using SQLAlchemy's native `JSON` column type instead of the custom `JSONField`, which caused compatibility issues. [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa)
+- π **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce sharing.public_* permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358)
+- π **PostgreSQL analytics query fix.** The get_chat_ids_by_model_id function now works correctly with PostgreSQL by using GROUP BY with aggregate ordering instead of DISTINCT with non-aggregate ORDER BY, which PostgreSQL does not support. [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
+- π **Skills PostgreSQL compatibility fix.** Skills now work correctly with PostgreSQL by using SQLAlchemy's native JSON column type instead of the custom JSONField, which caused compatibility issues. [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa)
+- ποΈ **Chat message deletion cascade fix.** Deleting chats now properly removes associated chat messages from the database, preventing orphaned message records from accumulating when chats or shared chats are deleted. [#21362](https://github.com/open-webui/open-webui/pull/21362)
+- π― **Model access control fix for direct models.** Updating access control for non-preset models (direct Ollama or OpenAI models without a database entry) now works correctly by automatically creating the model record with proper defaults before applying permissions. [Commit](https://github.com/open-webui/open-webui/commit/f027a01ab)
+- π₯ **ARM device startup fix.** Open WebUI no longer fails to start on ARM devices (such as Raspberry Pi) due to incompatible torch versions by pinning torch to a compatible version. [#21385](https://github.com/open-webui/open-webui/pull/21385)
+- π§© **Vector metadata mutation fix.** The process_metadata function in the vector retrieval pipeline no longer mutates the original metadata dictionary while iterating over it, preventing potential runtime errors during document processing. [#21105](https://github.com/open-webui/open-webui/pull/21105)
- π **Knowledge tooltip z-index fix.** Knowledge base tooltips in the model editor no longer render behind other UI elements. [#21375](https://github.com/open-webui/open-webui/pull/21375)
- π **Knowledge collection layout fix.** Knowledge collection names in the chat input menu no longer appear indented or truncated due to incorrect flex layout. [#21374](https://github.com/open-webui/open-webui/pull/21374)
- π― **Model selector scroll position fix.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91)
From 3b61562c82448cf83710d8b6ed29b797991aa83a Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 17:26:54 -0600
Subject: [PATCH 27/32] refac
---
backend/open_webui/utils/middleware.py | 277 +++++++++++++++----------
backend/open_webui/utils/misc.py | 32 ++-
2 files changed, 185 insertions(+), 124 deletions(-)
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index e39787f715..1fa562e0d1 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -411,6 +411,34 @@ def serialize_output(output: list) -> str:
if content and not content.endswith("\n"):
content += "\n"
+ # Render the code_interpreter item as a block
+ # so the frontend Collapsible renders "Analyzing..."/"Analyzed".
+ code = item.get("code", "").strip()
+ lang = item.get("lang", "python")
+ status = item.get("status", "in_progress")
+ duration = item.get("duration")
+ is_last_item = idx == len(output) - 1
+
+ # Build inner content: code block
+ display = ""
+ if code:
+ display = f"```{lang}\n{code}\n```"
+
+ # Build output attribute as HTML-escaped JSON for CodeBlock.svelte
+ ci_output = item.get("output")
+ output_attr = ""
+ if ci_output:
+ if isinstance(ci_output, dict):
+ output_json = json.dumps(ci_output, ensure_ascii=False)
+ else:
+ output_json = json.dumps({"result": str(ci_output)}, ensure_ascii=False)
+ output_attr = f' output="{html.escape(output_json)}"'
+
+ if status == "completed" or duration is not None or not is_last_item:
+ content += f'\nAnalyzed
\n{display}\n \n'
+ else:
+ content += f'\nAnalyzingβ¦
\n{display}\n \n'
+
return content.strip()
@@ -2930,11 +2958,14 @@ async def streaming_chat_response_handler(response, ctx):
# Handle as a background task
async def response_handler(response, events):
- def tag_output_handler(content_type, tags, content, output):
+ def tag_output_handler(content_type, tags, output):
"""
Detect special tags (reasoning, solution, code_interpreter) in streaming
content and create corresponding OR-aligned output items directly.
Operates on output items instead of content_blocks.
+
+ Uses the text from the output items themselves for tag detection,
+ eliminating state divergence between accumulated content and items.
"""
end_flag = False
@@ -2974,6 +3005,8 @@ async def streaming_chat_response_handler(response, ctx):
last_type = output[-1].get("type", "") if output else ""
if last_type == "message":
+ # Use the output item's own text for tag detection
+ item_text = get_last_text(output)
for start_tag, end_tag in tags:
start_tag_pattern = rf"{re.escape(start_tag)}"
@@ -2982,7 +3015,7 @@ async def streaming_chat_response_handler(response, ctx):
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
)
- match = re.search(start_tag_pattern, content)
+ match = re.search(start_tag_pattern, item_text)
if match:
try:
attr_content = match.group(1) if match.group(1) else ""
@@ -2991,20 +3024,13 @@ async def streaming_chat_response_handler(response, ctx):
attributes = extract_attributes(attr_content)
- before_tag = content[: match.start()]
- after_tag = content[match.end() :]
+ before_tag = item_text[: match.start()]
+ after_tag = item_text[match.end() :]
- # Remove the start tag and everything after from last message
- current_text = get_last_text(output)
- set_last_text(
- output,
- current_text.replace(match.group(0) + after_tag, ""),
- )
+ # Keep only text before the tag in the message
+ set_last_text(output, before_tag)
- if before_tag:
- set_last_text(output, before_tag)
-
- if not get_last_text(output).strip():
+ if not before_tag.strip():
# Remove empty message item
if output and output[-1].get("type") == "message":
output.pop()
@@ -3069,9 +3095,11 @@ async def streaming_chat_response_handler(response, ctx):
else:
set_last_text(output, after_tag)
- tag_output_handler(
- content_type, tags, after_tag, output
+ _, recursive_end = tag_output_handler(
+ content_type, tags, output
)
+ if recursive_end:
+ end_flag = True
break
@@ -3090,24 +3118,23 @@ async def streaming_chat_response_handler(response, ctx):
start_tag = item.get("start_tag", "")
end_tag = item.get("end_tag", "")
- if end_tag.startswith("<") and end_tag.endswith(">"):
- end_tag_pattern = rf"{re.escape(end_tag)}"
- else:
- end_tag_pattern = rf"{re.escape(end_tag)}"
+ end_tag_pattern = rf"{re.escape(end_tag)}"
- if re.search(end_tag_pattern, content):
+ # Get the block content from the item itself
+ if last_type == "reasoning":
+ parts = item.get("content", [])
+ block_content = ""
+ if parts and parts[-1].get("type") == "output_text":
+ block_content = parts[-1].get("text", "")
+ elif last_type == "open_webui:code_interpreter":
+ block_content = item.get("code", "")
+ else:
+ block_content = get_last_text(output)
+
+ if re.search(end_tag_pattern, block_content):
end_flag = True
- # Get the block content
- if last_type == "reasoning":
- parts = item.get("content", [])
- block_content = ""
- if parts and parts[-1].get("type") == "output_text":
- block_content = parts[-1].get("text", "")
- elif last_type == "open_webui:code_interpreter":
- block_content = item.get("code", "")
- else:
- block_content = get_last_text(output)
+
# Strip start and end tags from content
start_tag_pattern = rf"{re.escape(start_tag)}"
@@ -3151,36 +3178,20 @@ async def streaming_chat_response_handler(response, ctx):
item["ended_at"] = time.time()
# Reset by appending a new message item for leftover
- if content_type != "code_interpreter":
- output.append(
- {
- "type": "message",
- "id": output_id("msg"),
- "status": "in_progress",
- "role": "assistant",
- "content": [
- {
- "type": "output_text",
- "text": leftover_content,
- }
- ],
- }
- )
- else:
- output.append(
- {
- "type": "message",
- "id": output_id("msg"),
- "status": "in_progress",
- "role": "assistant",
- "content": [
- {
- "type": "output_text",
- "text": leftover_content,
- }
- ],
- }
- )
+ output.append(
+ {
+ "type": "message",
+ "id": output_id("msg"),
+ "status": "in_progress",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": leftover_content,
+ }
+ ],
+ }
+ )
else:
# Remove the block if content is empty
output.pop()
@@ -3199,19 +3210,7 @@ async def streaming_chat_response_handler(response, ctx):
}
)
- # Clean processed content
- start_tag_clean = rf"{re.escape(start_tag)}"
- if start_tag.startswith("<") and start_tag.endswith(">"):
- start_tag_clean = rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
-
- content = re.sub(
- rf"{start_tag_clean}(.|\n)*?{re.escape(end_tag)}",
- "",
- content,
- flags=re.DOTALL,
- )
-
- return content, output, end_flag
+ return output, end_flag
message = Chats.get_message_by_id_and_message_id(
metadata["chat_id"], metadata["message_id"]
@@ -3674,58 +3673,122 @@ async def streaming_chat_response_handler(response, ctx):
)
content = f"{content}{value}"
- if (
- not output
- or output[-1].get("type") != "message"
- ):
- output.append(
- {
- "type": "message",
- "id": output_id("msg"),
- "status": "in_progress",
- "role": "assistant",
- "content": [
+
+ # Check if we're inside a tag-based block
+ # (reasoning, code_interpreter, or solution).
+ # If so, append to the existing in-progress
+ # item instead of creating a new message β
+ # otherwise tag_output_handler re-detects the
+ # start tag on every chunk and fragments the
+ # output.
+ last_item = output[-1] if output else None
+ last_item_type = (
+ last_item.get("type", "") if last_item else ""
+ )
+ inside_tag_block = (
+ last_item is not None
+ and last_item.get("status") == "in_progress"
+ and last_item.get("attributes", {}).get("type")
+ != "reasoning_content"
+ and (
+ last_item_type == "reasoning"
+ or last_item_type
+ == "open_webui:code_interpreter"
+ or (
+ last_item_type == "message"
+ and last_item.get("_tag_type")
+ is not None
+ )
+ )
+ )
+
+ if inside_tag_block:
+ # Append to the existing tag-based item
+ if last_item_type == "open_webui:code_interpreter":
+ last_item["code"] = (
+ last_item.get("code", "") + value
+ )
+ elif last_item_type == "reasoning":
+ parts = last_item.get("content", [])
+ if (
+ parts
+ and parts[-1].get("type")
+ == "output_text"
+ ):
+ parts[-1]["text"] += value
+ else:
+ last_item["content"] = [
{
"type": "output_text",
- "text": "",
+ "text": value,
}
- ],
- }
- )
-
- # Append value to last message item's text
- msg_parts = output[-1].get("content", [])
- if (
- msg_parts
- and msg_parts[-1].get("type")
- == "output_text"
- ):
- msg_parts[-1]["text"] += value
+ ]
+ else:
+ # solution or other _tag_type message
+ msg_parts = last_item.get("content", [])
+ if (
+ msg_parts
+ and msg_parts[-1].get("type")
+ == "output_text"
+ ):
+ msg_parts[-1]["text"] += value
+ else:
+ last_item["content"] = [
+ {
+ "type": "output_text",
+ "text": value,
+ }
+ ]
else:
- output[-1]["content"] = [
- {"type": "output_text", "text": value}
- ]
+ if (
+ not output
+ or output[-1].get("type") != "message"
+ ):
+ output.append(
+ {
+ "type": "message",
+ "id": output_id("msg"),
+ "status": "in_progress",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "",
+ }
+ ],
+ }
+ )
+
+ # Append value to last message item's text
+ msg_parts = output[-1].get("content", [])
+ if (
+ msg_parts
+ and msg_parts[-1].get("type")
+ == "output_text"
+ ):
+ msg_parts[-1]["text"] += value
+ else:
+ output[-1]["content"] = [
+ {"type": "output_text", "text": value}
+ ]
if DETECT_REASONING_TAGS:
- content, output, _ = tag_output_handler(
+ output, _ = tag_output_handler(
"reasoning",
reasoning_tags,
- content,
output,
)
- content, output, _ = tag_output_handler(
+ output, _ = tag_output_handler(
"solution",
DEFAULT_SOLUTION_TAGS,
- content,
output,
)
if DETECT_CODE_INTERPRETER:
- content, output, end = tag_output_handler(
+ output, end = tag_output_handler(
"code_interpreter",
DEFAULT_CODE_INTERPRETER_TAGS,
- content,
output,
)
diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py
index a192f7b66d..9d9cfa1d04 100644
--- a/backend/open_webui/utils/misc.py
+++ b/backend/open_webui/utils/misc.py
@@ -230,25 +230,23 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
# else: skip reasoning blocks for normal LLM messages
elif item_type == "open_webui:code_interpreter":
- if raw:
- # Include code interpreter content for LLM re-processing
- code = item.get("code", "")
- code_output = item.get("output", "")
+ # Always include code interpreter content so the LLM knows
+ # the code was already executed and doesn't retry.
+ code = item.get("code", "")
+ code_output = item.get("output", "")
- if code:
- lang = item.get("lang", "python")
- pending_content.append(f"```{lang}\n{code}\n```")
+ if code:
+ pending_content.append(f"\n{code}\n")
- if code_output:
- if isinstance(code_output, dict):
- stdout = code_output.get("stdout", "")
- result = code_output.get("result", "")
- output_text = stdout or result
- else:
- output_text = str(code_output)
- if output_text:
- pending_content.append(f"Output:\n{output_text}")
- # else: skip extension types
+ if code_output:
+ if isinstance(code_output, dict):
+ stdout = code_output.get("stdout", "")
+ result = code_output.get("result", "")
+ output_text = stdout or result
+ else:
+ output_text = str(code_output)
+ if output_text:
+ pending_content.append(f"\n{output_text}\n")
elif item_type.startswith("open_webui:"):
# Skip other extension types
From d33ad462aa6f2105f0d118ab6f2fd74f693c8680 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 17:38:57 -0600
Subject: [PATCH 28/32] refac
---
CHANGELOG.md | 3 +++
backend/open_webui/routers/openai.py | 9 ++++++---
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae2bed4b60..4157629463 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- π **Knowledge tooltip z-index fix.** Knowledge base tooltips in the model editor no longer render behind other UI elements. [#21375](https://github.com/open-webui/open-webui/pull/21375)
- π **Knowledge collection layout fix.** Knowledge collection names in the chat input menu no longer appear indented or truncated due to incorrect flex layout. [#21374](https://github.com/open-webui/open-webui/pull/21374)
- π― **Model selector scroll position fix.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91)
+- π **Builtin web search result count fix.** The built-in web search tool now respects the admin-configured result count instead of always using the model-provided value, ensuring consistent search behavior across all models. [#21373](https://github.com/open-webui/open-webui/pull/21373)
+- π§ **Reasoning tag detection fix.** Tag detection for reasoning, solution, and code interpreter blocks now uses the output item's own text instead of a separate accumulated content string, eliminating edge cases where tags could be missed or incorrectly split across output boundaries. [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
+- π» **Code interpreter context retention fix.** Code interpreter blocks are now always included in chat context regardless of processing mode, ensuring models are aware of previously executed code and don't unnecessarily retry code execution in follow-up turns. [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
- π **Translation updates.** Portuguese (Brazil) translations were updated. [#21345](https://github.com/open-webui/open-webui/pull/21345)
## [0.8.0] - 2026-02-12
diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py
index e2bb296945..9fe421b4e9 100644
--- a/backend/open_webui/routers/openai.py
+++ b/backend/open_webui/routers/openai.py
@@ -604,9 +604,12 @@ async def get_models(
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
- res = await r.json()
- if "error" in res:
- error_detail = f"External Error: {res['error']}"
+ try:
+ res = await r.json()
+ if "error" in res:
+ error_detail = f"External Error: {res['error']}"
+ except Exception:
+ pass
raise Exception(error_detail)
response_data = await r.json()
From a30b106ea3ac64a62ef3a3457689c402c833d6e5 Mon Sep 17 00:00:00 2001
From: Taylor Wilsdon
Date: Fri, 13 Feb 2026 18:42:34 -0500
Subject: [PATCH 29/32] fix issues/21399 (#21400)
---
backend/open_webui/env.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py
index 9bbf0aecdd..86fe787899 100644
--- a/backend/open_webui/env.py
+++ b/backend/open_webui/env.py
@@ -478,7 +478,7 @@ ENABLE_PASSWORD_VALIDATION = (
)
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
"PASSWORD_VALIDATION_REGEX_PATTERN",
- "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
+ r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
)
From 5de60dc922a74c9ce6cf5b2de3129e4ca3d9ffdd Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 17:44:52 -0600
Subject: [PATCH 30/32] refac
---
CHANGELOG.md | 2 +-
backend/open_webui/utils/middleware.py | 24 +++++++++++++++++-------
backend/open_webui/utils/misc.py | 8 ++++++--
3 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4157629463..017176ba39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- π **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce sharing.public_* permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358)
+- π **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce sharing.public\_\* permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358)
- π **PostgreSQL analytics query fix.** The get_chat_ids_by_model_id function now works correctly with PostgreSQL by using GROUP BY with aggregate ordering instead of DISTINCT with non-aggregate ORDER BY, which PostgreSQL does not support. [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
- π **Skills PostgreSQL compatibility fix.** Skills now work correctly with PostgreSQL by using SQLAlchemy's native JSON column type instead of the custom JSONField, which caused compatibility issues. [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa)
- ποΈ **Chat message deletion cascade fix.** Deleting chats now properly removes associated chat messages from the database, preventing orphaned message records from accumulating when chats or shared chats are deleted. [#21362](https://github.com/open-webui/open-webui/pull/21362)
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index 1fa562e0d1..3354889e7e 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -431,7 +431,9 @@ def serialize_output(output: list) -> str:
if isinstance(ci_output, dict):
output_json = json.dumps(ci_output, ensure_ascii=False)
else:
- output_json = json.dumps({"result": str(ci_output)}, ensure_ascii=False)
+ output_json = json.dumps(
+ {"result": str(ci_output)}, ensure_ascii=False
+ )
output_attr = f' output="{html.escape(output_json)}"'
if status == "completed" or duration is not None or not is_last_item:
@@ -3134,8 +3136,6 @@ async def streaming_chat_response_handler(response, ctx):
if re.search(end_tag_pattern, block_content):
end_flag = True
-
-
# Strip start and end tags from content
start_tag_pattern = rf"{re.escape(start_tag)}"
if start_tag.startswith("<") and start_tag.endswith(">"):
@@ -3683,12 +3683,16 @@ async def streaming_chat_response_handler(response, ctx):
# output.
last_item = output[-1] if output else None
last_item_type = (
- last_item.get("type", "") if last_item else ""
+ last_item.get("type", "")
+ if last_item
+ else ""
)
inside_tag_block = (
last_item is not None
and last_item.get("status") == "in_progress"
- and last_item.get("attributes", {}).get("type")
+ and last_item.get("attributes", {}).get(
+ "type"
+ )
!= "reasoning_content"
and (
last_item_type == "reasoning"
@@ -3704,7 +3708,10 @@ async def streaming_chat_response_handler(response, ctx):
if inside_tag_block:
# Append to the existing tag-based item
- if last_item_type == "open_webui:code_interpreter":
+ if (
+ last_item_type
+ == "open_webui:code_interpreter"
+ ):
last_item["code"] = (
last_item.get("code", "") + value
)
@@ -3769,7 +3776,10 @@ async def streaming_chat_response_handler(response, ctx):
msg_parts[-1]["text"] += value
else:
output[-1]["content"] = [
- {"type": "output_text", "text": value}
+ {
+ "type": "output_text",
+ "text": value,
+ }
]
if DETECT_REASONING_TAGS:
diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py
index 9d9cfa1d04..13539ca9d0 100644
--- a/backend/open_webui/utils/misc.py
+++ b/backend/open_webui/utils/misc.py
@@ -236,7 +236,9 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
code_output = item.get("output", "")
if code:
- pending_content.append(f"\n{code}\n")
+ pending_content.append(
+ f"\n{code}\n"
+ )
if code_output:
if isinstance(code_output, dict):
@@ -246,7 +248,9 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
else:
output_text = str(code_output)
if output_text:
- pending_content.append(f"\n{output_text}\n")
+ pending_content.append(
+ f"\n{output_text}\n"
+ )
elif item_type.startswith("open_webui:"):
# Skip other extension types
From 12bad452fa409121effae088316fa2a29ae48541 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Sat, 14 Feb 2026 00:46:52 +0100
Subject: [PATCH 31/32] chore: Changelog updates (#21382)
* Add v0.8.1 release section
* changelog: knowledge menu layout fix
* changelog: knowledge tooltip z-index fix
* changelog: sync modal community sharing fix
* changelog: postgresql distinct ordering fix
* changelog: security fix public sharing bypass
* changelog: add issue ref to postgresql fix
* changelog: fix postgresql skills json compatibility
* changelog: apply new format style to 0.8.1 entries
* changelog: web search result count fix
* changelog: metadata, document, crash fix
* changelog: add channel user active status performance entry
* changelog: add model and prompt list optimization entry
* changelog: batch access control queries, channel status, model list optimization
* changelog: user list, performance, deferred loading
* Update CHANGELOG.md for 0.8.1
* Add emoji variation to Added section
* Remove empty Changed section and finalize 0.8.1 changelog
* changelog: arm, torch compatibility fix
* changelog: update database migration warning format
* changelog: ollama cloud, model naming fix
* Add SCIM externalId entry and database migration warning to 0.8.1
* Fix: move web search to Added, restore 0.8.0 headers
* Fix: SCIM above translations, 0.8.0 restored
* Remove 0.8.1 migration warning, keep 0.8.0 original
* changelog: direct model access control fix
* changelog: add commit link to direct model access control fix
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* changelog: sqlite, cascade delete, database fix
* changelog: responses, api, model-routing
* Add PR and issue links to SCIM externalId changelog entry
* Add commit link to Responses API entry, remove PR link from translation entry
* changelog: reasoning traces, performance, browser
* changelog: password, validation, regex
* Update CHANGELOG.md
---
CHANGELOG.md | 45 ++++++++++++++++++++++++++++++---------------
1 file changed, 30 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 017176ba39..01bdde5f3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,24 +5,39 @@ 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.8.1] - 2026-02-13
+## [0.8.1] - 2026-02-14
+
+### Added
+
+- π **Channel user active status.** Checking user active status in channels is now faster thanks to optimized database queries. [Commit](https://github.com/open-webui/open-webui/commit/ca6b18ab5cb94153a9dae233f975d36bf6b19b76)
+- π **Responses API endpoint with model routing.** The OpenAI API proxy now supports a /responses endpoint that routes requests to the correct backend based on the model field in the request, instead of always using the first configured endpoint. This enables support for backends like vLLM that provide /skills and /v1/responses endpoints. [Commit](https://github.com/open-webui/open-webui/commit/abc9b63093d65f4d74342db85b7d5df1809aa0f0), [Commit](https://github.com/open-webui/open-webui/commit/79ecbfc757f0642740d0e44fab98263d84295490)
+- β‘ **Model and prompt list optimization.** Improved performance when loading models and prompts by pre-fetching user group IDs once instead of making multiple database queries. [Commit](https://github.com/open-webui/open-webui/commit/20de5a87da0c12e4052b50887a42ddd7228c5ef5)
+- ποΈ **Batch access control queries.** Improved performance when loading models, prompts, and knowledge bases by replacing multiple individual access checks with single batch queries, significantly reducing database load for large deployments. [Commit](https://github.com/open-webui/open-webui/commit/589c4e64c1b7bb7a7a5abc20382b92fb860e28c2)
+- π¨ **Faster user list loading.** User lists now load significantly faster by deferring profile image loading; images are fetched separately in parallel by the browser, improving caching and reducing database load. [Commit](https://github.com/open-webui/open-webui/commit/b7549d2f6ca2843661ec79a5a1e55da9e7553368)
+- π **Web search result count.** The built-in search_web tool now respects the admin-configured "Search Result Count" setting instead of always returning 5 results when using Native Function Calling mode. [#21373](https://github.com/open-webui/open-webui/pull/21373), [#21371](https://github.com/open-webui/open-webui/issues/21371)
+- π **SCIM externalId support.** SCIM-enabled deployments can now store and manage externalId for user provisioning, enabling better integration with identity providers like Microsoft Entra ID and Okta. [#21099](https://github.com/open-webui/open-webui/pull/21099), [#21280](https://github.com/open-webui/open-webui/issues/21280), [Commit](https://github.com/open-webui/open-webui/commit/d1d1efe212b16e0052359991d67fd813125077e8)
+- π **Translation updates.** Portuguese (Brazil) translations were updated.
### Fixed
-- π **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce sharing.public\_\* permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358)
-- π **PostgreSQL analytics query fix.** The get_chat_ids_by_model_id function now works correctly with PostgreSQL by using GROUP BY with aggregate ordering instead of DISTINCT with non-aggregate ORDER BY, which PostgreSQL does not support. [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
-- π **Skills PostgreSQL compatibility fix.** Skills now work correctly with PostgreSQL by using SQLAlchemy's native JSON column type instead of the custom JSONField, which caused compatibility issues. [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa)
-- ποΈ **Chat message deletion cascade fix.** Deleting chats now properly removes associated chat messages from the database, preventing orphaned message records from accumulating when chats or shared chats are deleted. [#21362](https://github.com/open-webui/open-webui/pull/21362)
-- π― **Model access control fix for direct models.** Updating access control for non-preset models (direct Ollama or OpenAI models without a database entry) now works correctly by automatically creating the model record with proper defaults before applying permissions. [Commit](https://github.com/open-webui/open-webui/commit/f027a01ab)
-- π₯ **ARM device startup fix.** Open WebUI no longer fails to start on ARM devices (such as Raspberry Pi) due to incompatible torch versions by pinning torch to a compatible version. [#21385](https://github.com/open-webui/open-webui/pull/21385)
-- π§© **Vector metadata mutation fix.** The process_metadata function in the vector retrieval pipeline no longer mutates the original metadata dictionary while iterating over it, preventing potential runtime errors during document processing. [#21105](https://github.com/open-webui/open-webui/pull/21105)
-- π **Knowledge tooltip z-index fix.** Knowledge base tooltips in the model editor no longer render behind other UI elements. [#21375](https://github.com/open-webui/open-webui/pull/21375)
-- π **Knowledge collection layout fix.** Knowledge collection names in the chat input menu no longer appear indented or truncated due to incorrect flex layout. [#21374](https://github.com/open-webui/open-webui/pull/21374)
-- π― **Model selector scroll position fix.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91)
-- π **Builtin web search result count fix.** The built-in web search tool now respects the admin-configured result count instead of always using the model-provided value, ensuring consistent search behavior across all models. [#21373](https://github.com/open-webui/open-webui/pull/21373)
-- π§ **Reasoning tag detection fix.** Tag detection for reasoning, solution, and code interpreter blocks now uses the output item's own text instead of a separate accumulated content string, eliminating edge cases where tags could be missed or incorrectly split across output boundaries. [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
-- π» **Code interpreter context retention fix.** Code interpreter blocks are now always included in chat context regardless of processing mode, ensuring models are aware of previously executed code and don't unnecessarily retry code execution in follow-up turns. [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
-- π **Translation updates.** Portuguese (Brazil) translations were updated. [#21345](https://github.com/open-webui/open-webui/pull/21345)
+- π‘οΈ **Public sharing security fix.** Fixed a security issue where users with write access could see the Public sharing option regardless of their actual public sharing permission, and direct API calls could bypass frontend sharing restrictions. [#21358](https://github.com/open-webui/open-webui/pull/21358), [#21356](https://github.com/open-webui/open-webui/issues/21356)
+- π **Direct model access control fix.** Model access control changes now persist correctly for direct Ollama and OpenAI models that don't have database entries, and error messages display properly instead of showing "[object Object]". [Commit](https://github.com/open-webui/open-webui/commit/f027a01ab2ff3b6175af3dd13a4478c265c0544a), [#21377](https://github.com/open-webui/open-webui/issues/21377)
+- π **Reasoning trace rendering performance.** Reasoning traces from models now render properly without being split into many fragments, preventing browser slowdowns during streaming responses. [#21348](https://github.com/open-webui/open-webui/issues/21348), [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
+- π₯οΈ **ARM device compatibility fix.** Fixed an issue where upgrading to 0.8.0 would fail to start on ARM devices (like Raspberry Pi 4) due to torch 2.10.0 causing SIGILL errors; now pinned to torch<=2.9.1. [#21385](https://github.com/open-webui/open-webui/pull/21385), [#21349](https://github.com/open-webui/open-webui/issues/21349)
+- ποΈ **Skills PostgreSQL compatibility fix.** Fixed a PostgreSQL compatibility issue where creating or listing skills would fail with a TypeError, while SQLite worked correctly. [#21372](https://github.com/open-webui/open-webui/pull/21372), [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa), [#21365](https://github.com/open-webui/open-webui/issues/21365)
+- ποΈ **PostgreSQL analytics query fix.** Fixed an issue where retrieving chat IDs by model ID would fail on PostgreSQL due to incompatible DISTINCT ordering, while SQLite worked correctly. [#21347](https://github.com/open-webui/open-webui/issues/21347), [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
+- ποΈ **SQLite cascade delete fix.** Deleting chats now properly removes all associated messages in SQLite, matching PostgreSQL behavior and preventing orphaned data. [#21362](https://github.com/open-webui/open-webui/pull/21362)
+- βοΈ **Ollama Cloud model naming fix.** Fixed an issue where using Ollama Cloud models would fail with "Model not found" errors because ":latest" was incorrectly appended to model names. [#21386](https://github.com/open-webui/open-webui/issues/21386)
+- π οΈ **Knowledge selector tooltip z-index.** Fixed an issue where tooltips in the "Select Knowledge" dropdown were hidden behind the menu, making it difficult to read knowledge item names and descriptions. [#21375](https://github.com/open-webui/open-webui/pull/21375)
+- π― **Model selector scroll position.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91)
+- π **Sync modal unexpected appearance.** Fixed an issue where the Sync Modal would appear unexpectedly after enabling the "Community Sharing" feature if the user had previously visited the app with the sync parameter. [#21376](https://github.com/open-webui/open-webui/pull/21376)
+- π¨ **Knowledge collection layout fix.** Fixed a layout issue in the Knowledge integration menu where long collection names caused indentation artifacts and now properly truncate with ellipsis. [#21374](https://github.com/open-webui/open-webui/pull/21374)
+- π **Metadata processing crash fix.** Fixed a latent bug where processing document metadata containing certain keys (content, pages, tables, paragraphs, sections, figures) would cause a RuntimeError due to dictionary mutation during iteration. [#21105](https://github.com/open-webui/open-webui/pull/21105)
+- π **Password validation regex fix.** Fixed the password validation regex by adding the raw string prefix, ensuring escape sequences like \d and \w are interpreted correctly. [#21400](https://github.com/open-webui/open-webui/pull/21400), [#21399](https://github.com/open-webui/open-webui/issues/21399)
+
+### Changed
+
+- β οΈ **Database Migrations:** This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility.
## [0.8.0] - 2026-02-12
From 7e224e4a536b07ec008613f06592e34050e7067c Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Fri, 13 Feb 2026 18:26:03 -0600
Subject: [PATCH 32/32] refac
---
backend/open_webui/models/oauth_sessions.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py
index f7ee5cceb8..538937483f 100644
--- a/backend/open_webui/models/oauth_sessions.py
+++ b/backend/open_webui/models/oauth_sessions.py
@@ -102,7 +102,7 @@ class OAuthSessionTable:
decrypted = self.fernet.decrypt(token.encode()).decode()
return json.loads(decrypted)
except Exception as e:
- log.error(f"Error decrypting tokens: {e}")
+ log.error(f"Error decrypting tokens: {type(e).__name__}: {e}")
raise
def create_session(
@@ -209,8 +209,15 @@ class OAuthSessionTable:
results = []
for session in sessions:
- session.token = self._decrypt_token(session.token)
- results.append(OAuthSessionModel.model_validate(session))
+ try:
+ session.token = self._decrypt_token(session.token)
+ results.append(OAuthSessionModel.model_validate(session))
+ except Exception as e:
+ log.warning(
+ f"Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}"
+ )
+ db.query(OAuthSession).filter_by(id=session.id).delete()
+ db.commit()
return results