From d13a35ab96bd24b4c760f76ab66c5e12ad13abac Mon Sep 17 00:00:00 2001 From: Aslan Vatsaev Date: Fri, 10 Oct 2025 13:26:12 +0200 Subject: [PATCH 001/125] fix: auto show artifacts when opening a conversation --- src/lib/components/chat/Messages/ContentRenderer.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Messages/ContentRenderer.svelte b/src/lib/components/chat/Messages/ContentRenderer.svelte index 219c0db436..b1591bf11b 100644 --- a/src/lib/components/chat/Messages/ContentRenderer.svelte +++ b/src/lib/components/chat/Messages/ContentRenderer.svelte @@ -185,8 +185,10 @@ !$mobile && $chatId ) { - showArtifacts.set(true); - showControls.set(true); + setTimeout(() => { + showArtifacts.set(true); + showControls.set(true); + }); } }} onPreview={async (value) => { From a9c4e4b422a4908cbaca21c3b3436124df6face8 Mon Sep 17 00:00:00 2001 From: Aslan Vatsaev Date: Fri, 10 Oct 2025 13:47:33 +0200 Subject: [PATCH 002/125] fix: auto show artifacts when opening a conversation --- src/lib/components/chat/Messages/ContentRenderer.svelte | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/components/chat/Messages/ContentRenderer.svelte b/src/lib/components/chat/Messages/ContentRenderer.svelte index b1591bf11b..bc9cd3265d 100644 --- a/src/lib/components/chat/Messages/ContentRenderer.svelte +++ b/src/lib/components/chat/Messages/ContentRenderer.svelte @@ -176,7 +176,7 @@ {onSourceClick} {onTaskClick} {onSave} - onUpdate={(token) => { + onUpdate={async (token) => { const { lang, text: code } = token; if ( @@ -185,10 +185,9 @@ !$mobile && $chatId ) { - setTimeout(() => { - showArtifacts.set(true); - showControls.set(true); - }); + await tick(); + showArtifacts.set(true); + showControls.set(true); } }} onPreview={async (value) => { From 40c450e6e54303ead5473476f0c2519cd928ae9e Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Sat, 18 Oct 2025 13:43:51 -0400 Subject: [PATCH 003/125] Add more granular information to oauth failure messages --- backend/open_webui/utils/oauth.py | 55 ++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index e0bf7582c6..4fa493b1d5 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -74,6 +74,8 @@ from mcp.shared.auth import ( OAuthMetadata, ) +from authlib.oauth2.rfc6749.errors import OAuth2Error + class OAuthClientInformationFull(OAuthClientMetadata): issuer: Optional[str] = None # URL of the OAuth server that issued this client @@ -150,6 +152,37 @@ def decrypt_data(data: str): raise +def _build_oauth_callback_error_message(exc: Exception) -> str: + """ + Produce a user-facing callback error string with actionable context. + Keeps the message short and strips newlines for safe redirect usage. + """ + if isinstance(exc, OAuth2Error): + parts = [p for p in [exc.error, exc.description] if p] + detail = " - ".join(parts) + elif isinstance(exc, HTTPException): + detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail) + elif isinstance(exc, aiohttp.ClientResponseError): + detail = f"Upstream provider returned {exc.status}: {exc.message}" + elif isinstance(exc, aiohttp.ClientError): + detail = str(exc) + elif isinstance(exc, KeyError): + missing = str(exc).strip("'") + if missing.lower() == "state": + detail = "Missing state parameter in callback (session may have expired)" + else: + detail = f"Missing expected key '{missing}' in OAuth response" + else: + detail = str(exc) + + detail = detail.replace("\n", " ").strip() + if not detail: + detail = exc.__class__.__name__ + + message = f"OAuth callback failed: {detail}" + return message[:197] + "..." if len(message) > 200 else message + + def is_in_blocked_groups(group_name: str, groups: list) -> bool: """ Check if a group name matches any blocked pattern. @@ -621,8 +654,14 @@ class OAuthClientManager: error_message = "Failed to obtain OAuth token" log.warning(error_message) except Exception as e: - error_message = "OAuth callback error" - log.warning(f"OAuth callback error: {e}") + error_message = _build_oauth_callback_error_message(e) + log.warning( + "OAuth callback error for user_id=%s client_id=%s: %s", + user_id, + client_id, + error_message, + exc_info=True, + ) redirect_url = ( str(request.app.state.config.WEBUI_URL or request.base_url) @@ -630,7 +669,9 @@ class OAuthClientManager: if error_message: log.debug(error_message) - redirect_url = f"{redirect_url}/?error={error_message}" + redirect_url = ( + f"{redirect_url}/?error={urllib.parse.quote_plus(error_message)}" + ) return RedirectResponse(url=redirect_url, headers=response.headers) response = RedirectResponse(url=redirect_url, headers=response.headers) @@ -1104,7 +1145,13 @@ class OAuthManager: try: token = await client.authorize_access_token(request) except Exception as e: - log.warning(f"OAuth callback error: {e}") + detailed_error = _build_oauth_callback_error_message(e) + log.warning( + "OAuth callback error during authorize_access_token for provider %s: %s", + provider, + detailed_error, + exc_info=True, + ) raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) # Try to get userinfo from the token first, some providers include it there From c107a3799f3695e57c303544dc93cadbdeb91c0a Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Sat, 18 Oct 2025 14:00:46 -0400 Subject: [PATCH 004/125] Added a targeted utility to wipe all OAuth sessions for a provider so the cleanup can remove stale access tokens across every user when a connection is updated --- backend/open_webui/models/oauth_sessions.py | 11 +++++ backend/open_webui/routers/configs.py | 51 ++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index 81ce220384..b0e465dbe7 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -262,5 +262,16 @@ class OAuthSessionTable: log.error(f"Error deleting OAuth sessions by user ID: {e}") return False + def delete_sessions_by_provider(self, provider: str) -> bool: + """Delete all OAuth sessions for a provider""" + try: + with get_db() as db: + db.query(OAuthSession).filter_by(provider=provider).delete() + db.commit() + return True + except Exception as e: + log.error(f"Error deleting OAuth sessions by provider {provider}: {e}") + return False + OAuthSessions = OAuthSessionTable() diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index e7fa13d1ff..e8e876eac7 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -1,4 +1,5 @@ import logging +import copy from fastapi import APIRouter, Depends, Request, HTTPException from pydantic import BaseModel, ConfigDict import aiohttp @@ -15,6 +16,7 @@ from open_webui.utils.tools import ( set_tool_servers, ) from open_webui.utils.mcp.client import MCPClient +from open_webui.models.oauth_sessions import OAuthSessions from open_webui.env import SRC_LOG_LEVELS @@ -165,12 +167,59 @@ async def set_tool_servers_config( form_data: ToolServersConfigForm, user=Depends(get_admin_user), ): - request.app.state.config.TOOL_SERVER_CONNECTIONS = [ + old_connections = copy.deepcopy( + request.app.state.config.TOOL_SERVER_CONNECTIONS or [] + ) + + new_connections = [ connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS ] + old_mcp_connections = { + conn.get("info", {}).get("id"): conn + for conn in old_connections + if conn.get("type") == "mcp" + } + new_mcp_connections = { + conn.get("info", {}).get("id"): conn + for conn in new_connections + if conn.get("type") == "mcp" + } + + purge_oauth_clients = set() + + for server_id, old_conn in old_mcp_connections.items(): + if not server_id: + continue + + old_auth_type = old_conn.get("auth_type", "none") + new_conn = new_mcp_connections.get(server_id) + + if new_conn is None: + if old_auth_type == "oauth_2.1": + purge_oauth_clients.add(server_id) + continue + + new_auth_type = new_conn.get("auth_type", "none") + + if old_auth_type == "oauth_2.1": + if ( + new_auth_type != "oauth_2.1" + or old_conn.get("url") != new_conn.get("url") + or old_conn.get("info", {}).get("oauth_client_info") + != new_conn.get("info", {}).get("oauth_client_info") + ): + purge_oauth_clients.add(server_id) + + request.app.state.config.TOOL_SERVER_CONNECTIONS = new_connections + await set_tool_servers(request) + for server_id in purge_oauth_clients: + client_key = f"mcp:{server_id}" + request.app.state.oauth_client_manager.remove_client(client_key) + OAuthSessions.delete_sessions_by_provider(client_key) + for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: server_type = connection.get("type", "openapi") if server_type == "mcp": From d49fb9c01038dc580b2229ac2554727506357372 Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Sat, 18 Oct 2025 14:16:10 -0400 Subject: [PATCH 005/125] complete cleanup of oauth clients --- backend/open_webui/utils/oauth.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 4fa493b1d5..85f694cb13 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -401,8 +401,19 @@ class OAuthClientManager: return self.clients[client_id] def remove_client(self, client_id): + removed = False if client_id in self.clients: del self.clients[client_id] + removed = True + if hasattr(self.oauth, "_clients"): + if client_id in self.oauth._clients: + self.oauth._clients.pop(client_id, None) + removed = True + if hasattr(self.oauth, "_registry"): + if client_id in self.oauth._registry: + self.oauth._registry.pop(client_id, None) + removed = True + if removed: log.info(f"Removed OAuth client {client_id}") return True From ecbf74dbea59534cebdd3ce1a40749fcfd68133e Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Sat, 18 Oct 2025 16:53:44 -0400 Subject: [PATCH 006/125] Added a preflight authorize check that automatically re-registers MCP OAuth clients when the stored client ID no longer exists on the server, so the browser flow never hits the stale-ID failure --- backend/open_webui/utils/oauth.py | 206 +++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 85f694cb13..d24a379ede 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1,4 +1,5 @@ import base64 +import copy import hashlib import logging import mimetypes @@ -417,6 +418,205 @@ class OAuthClientManager: log.info(f"Removed OAuth client {client_id}") return True + def _find_mcp_connection(self, request, client_id: str): + try: + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS or [] + except Exception: + connections = [] + + normalized_client_id = client_id.split(":")[-1] + + for idx, connection in enumerate(connections): + if not isinstance(connection, dict): + continue + if connection.get("type") != "mcp": + continue + + info = connection.get("info") or {} + server_id = info.get("id") + if not server_id: + continue + + normalized_server_id = server_id.split(":")[-1] + if normalized_server_id == normalized_client_id: + return idx, connection + + return None, None + + async def _preflight_authorization_url( + self, client, client_info: OAuthClientInformationFull + ) -> bool: + # Only perform preflight checks for Starlette OAuth clients + if not hasattr(client, "create_authorization_url"): + return True + + redirect_uri = None + if client_info.redirect_uris: + redirect_uri = str(client_info.redirect_uris[0]) + + try: + auth_data = await client.create_authorization_url(redirect_uri=redirect_uri) + authorize_url = auth_data.get("url") + if not authorize_url: + return True + except Exception as e: + log.debug( + "Skipping OAuth preflight for client %s: %s", + client_info.client_id, + e, + ) + return True + + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + authorize_url, + allow_redirects=False, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.status < 400: + return True + + body_text = await resp.text() + error = None + error_description = "" + content_type = resp.headers.get("content-type", "") + + if "application/json" in content_type: + try: + payload = json.loads(body_text) + error = payload.get("error") + error_description = payload.get( + "error_description", "" + ) + except json.JSONDecodeError: + error = None + error_description = "" + else: + error_description = body_text + + combined = f"{error or ''} {error_description}".lower() + if "invalid_client" in combined or "invalid client" in combined or "client id" in combined: + log.warning( + "OAuth client preflight detected invalid registration for %s: %s %s", + client_info.client_id, + error, + error_description, + ) + return False + except Exception as e: + log.debug( + "Skipping OAuth preflight network check for client %s: %s", + client_info.client_id, + e, + ) + + return True + + async def _re_register_client(self, request, client_id: str) -> bool: + idx, connection = self._find_mcp_connection(request, client_id) + if idx is None or connection is None: + log.warning( + "Unable to locate MCP tool server configuration for client %s during re-registration", + client_id, + ) + return False + + server_url = connection.get("url") + oauth_server_key = (connection.get("config") or {}).get("oauth_server_key") + + try: + oauth_client_info = ( + await get_oauth_client_info_with_dynamic_client_registration( + request, + client_id, + server_url, + oauth_server_key, + ) + ) + except Exception as e: + log.error( + "Dynamic client re-registration failed for %s: %s", + client_id, + e, + ) + return False + + encrypted_info = encrypt_data(oauth_client_info.model_dump(mode="json")) + + updated_connections = copy.deepcopy( + request.app.state.config.TOOL_SERVER_CONNECTIONS or [] + ) + if idx >= len(updated_connections): + log.error( + "MCP tool server index %s out of range during OAuth client re-registration for %s", + idx, + client_id, + ) + return False + + updated_connection = copy.deepcopy(connection) + updated_connection.setdefault("info", {}) + updated_connection["info"]["oauth_client_info"] = encrypted_info + updated_connections[idx] = updated_connection + + try: + request.app.state.config.TOOL_SERVER_CONNECTIONS = updated_connections + except Exception as e: + log.error( + "Failed to persist updated OAuth client info for %s: %s", + client_id, + e, + ) + return False + + self.remove_client(client_id) + self.add_client(client_id, oauth_client_info) + OAuthSessions.delete_sessions_by_provider(client_id) + + log.info("Re-registered OAuth client %s for MCP tool server", client_id) + return True + + async def _ensure_valid_client_registration( + self, request, client_id: str + ) -> None: + if not client_id.startswith("mcp:"): + return + + client = self.get_client(client_id) + client_info = self.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + is_valid = await self._preflight_authorization_url(client, client_info) + if is_valid: + return + + log.info( + "Detected invalid OAuth client %s; attempting re-registration", + client_id, + ) + re_registered = await self._re_register_client(request, client_id) + if not re_registered: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to re-register OAuth client", + ) + + client = self.get_client(client_id) + client_info = self.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="OAuth client unavailable after re-registration", + ) + + if not await self._preflight_authorization_url(client, client_info): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="OAuth client registration is still invalid after re-registration", + ) + def get_client(self, client_id): client = self.clients.get(client_id) return client["client"] if client else None @@ -602,10 +802,11 @@ class OAuthClientManager: return None async def handle_authorize(self, request, client_id: str) -> RedirectResponse: + await self._ensure_valid_client_registration(request, client_id) + client = self.get_client(client_id) if client is None: raise HTTPException(404) - client_info = self.get_client_info(client_id) if client_info is None: raise HTTPException(404) @@ -613,7 +814,8 @@ class OAuthClientManager: redirect_uri = ( client_info.redirect_uris[0] if client_info.redirect_uris else None ) - return await client.authorize_redirect(request, str(redirect_uri)) + redirect_uri_str = str(redirect_uri) if redirect_uri else None + return await client.authorize_redirect(request, redirect_uri_str) async def handle_callback(self, request, client_id: str, user_id: str, response): client = self.get_client(client_id) From 4b7403496795fd7476243e7b6a48292c0cfe1b4b Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Sun, 19 Oct 2025 16:58:09 -0400 Subject: [PATCH 007/125] black fmt --- backend/open_webui/utils/oauth.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index d24a379ede..34fb441679 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -486,9 +486,7 @@ class OAuthClientManager: try: payload = json.loads(body_text) error = payload.get("error") - error_description = payload.get( - "error_description", "" - ) + error_description = payload.get("error_description", "") except json.JSONDecodeError: error = None error_description = "" @@ -496,7 +494,11 @@ class OAuthClientManager: error_description = body_text combined = f"{error or ''} {error_description}".lower() - if "invalid_client" in combined or "invalid client" in combined or "client id" in combined: + if ( + "invalid_client" in combined + or "invalid client" in combined + or "client id" in combined + ): log.warning( "OAuth client preflight detected invalid registration for %s: %s %s", client_info.client_id, @@ -577,9 +579,7 @@ class OAuthClientManager: log.info("Re-registered OAuth client %s for MCP tool server", client_id) return True - async def _ensure_valid_client_registration( - self, request, client_id: str - ) -> None: + async def _ensure_valid_client_registration(self, request, client_id: str) -> None: if not client_id.startswith("mcp:"): return From 0031fb82748872c24d68efa03fa7f83b44cd8a23 Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Sun, 19 Oct 2025 20:15:39 -0400 Subject: [PATCH 008/125] fix: correctly handle clipboard images in prompts The textVariableHandler was using URL.createObjectURL() for clipboard images, which created a blob URL instead of the required base64-encoded data URL. This caused an "illegal base64 data" error when sending messages with images pasted via a {{CLIPBOARD}} prompt. This commit updates the handler to use FileReader.readAsDataURL() to properly encode the image, aligning it with the existing on:paste logic. Additionally, it adds error handling for navigator.clipboard.read() to address potential permission issues in Firefox. --- src/lib/components/chat/MessageInput.svelte | 29 +++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c525dcf34c..b8539c19c8 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -168,29 +168,30 @@ return '{{CLIPBOARD}}'; }); - const clipboardItems = await navigator.clipboard.read(); + const clipboardItems = await navigator.clipboard.read().catch((err) => { + console.error('Failed to read clipboard items:', err); + return []; + }); - let imageUrl = null; for (const item of clipboardItems) { - // Check for known image types for (const type of item.types) { if (type.startsWith('image/')) { const blob = await item.getType(type); - imageUrl = URL.createObjectURL(blob); + const reader = new FileReader(); + reader.onload = (event) => { + files = [ + ...files, + { + type: 'image', + url: event.target.result as string + } + ]; + }; + reader.readAsDataURL(blob); } } } - if (imageUrl) { - files = [ - ...files, - { - type: 'image', - url: imageUrl - } - ]; - } - text = text.replaceAll('{{CLIPBOARD}}', clipboardText); } From b2ed5be457b1abfa86ad3de8d43c5305ed97c653 Mon Sep 17 00:00:00 2001 From: sinejespersen Date: Mon, 27 Oct 2025 09:11:31 +0100 Subject: [PATCH 009/125] add danish translation --- src/lib/i18n/locales/da-DK/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/i18n/locales/da-DK/translation.json b/src/lib/i18n/locales/da-DK/translation.json index 40d040e8e0..02a4dc4dcb 100644 --- a/src/lib/i18n/locales/da-DK/translation.json +++ b/src/lib/i18n/locales/da-DK/translation.json @@ -1049,7 +1049,7 @@ "New Folder": "Ny mappe", "New Function": "Ny funktion", "New Knowledge": "", - "New Model": "", + "New Model": "Ny model", "New Note": "Ny note", "New Password": "Ny adgangskode", "New Prompt": "", From 5e178824886366b632b31a89251beb4eea441cdf Mon Sep 17 00:00:00 2001 From: Wang Weixuan Date: Tue, 28 Oct 2025 04:58:00 +0800 Subject: [PATCH 010/125] fix: use trusted env in web search loader Signed-off-by: Wang Weixuan --- backend/open_webui/retrieval/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 08dcde34da..da570330b3 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -71,6 +71,7 @@ def get_loader(request, url: str): url, verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV, ) From b72443004da66e266b7b22bff5394bf47a0a244f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 15:06:53 -0700 Subject: [PATCH 011/125] refac/chore: svelte 5 migration --- src/lib/components/common/RichTextInput/suggestions.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/components/common/RichTextInput/suggestions.ts b/src/lib/components/common/RichTextInput/suggestions.ts index a87b95abf9..0667e83060 100644 --- a/src/lib/components/common/RichTextInput/suggestions.ts +++ b/src/lib/components/common/RichTextInput/suggestions.ts @@ -1,3 +1,4 @@ +import { mount, unmount } from 'svelte'; import tippy from 'tippy.js'; export function getSuggestionRenderer(Component: any, ComponentProps = {}) { @@ -15,7 +16,7 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { document.body.appendChild(container); // mount Svelte component - component = new Component({ + component = mount(Component, { target: container, props: { char: props?.text, @@ -104,7 +105,12 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { popup?.destroy(); popup = null; - component?.$destroy(); + try { + unmount(component); + } catch (e) { + console.error('Error unmounting component:', e); + } + component = null; if (container?.parentNode) container.parentNode.removeChild(container); From 182e4138bf94c87c51d484a4e30ba6491285171b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 15:07:00 -0700 Subject: [PATCH 012/125] refac: styling --- src/lib/components/common/FileItemModal.svelte | 4 ++-- src/lib/components/notes/NoteEditor/Chat.svelte | 6 +++--- src/lib/components/notes/NoteEditor/Controls.svelte | 6 +++--- src/lib/components/notes/NotePanel.svelte | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index 52bf75127d..51afbde1af 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -161,7 +161,7 @@ {#if edit} -
+
{:else if isPDF}
-
+
@@ -377,7 +377,7 @@ Based on the user's instruction, update and enhance the existing notes or select
{#if selectedContent} -
+
{selectedContent?.text} diff --git a/src/lib/components/notes/NoteEditor/Controls.svelte b/src/lib/components/notes/NoteEditor/Controls.svelte index df988c28d9..675056ee41 100644 --- a/src/lib/components/notes/NoteEditor/Controls.svelte +++ b/src/lib/components/notes/NoteEditor/Controls.svelte @@ -17,7 +17,7 @@ }; -
+
-
+
{#if files.length > 0} -
{$i18n.t('Files')}
+
{$i18n.t('Files')}
{#each files.filter((file) => file.type !== 'image') as file, fileIdx} diff --git a/src/lib/components/notes/NotePanel.svelte b/src/lib/components/notes/NotePanel.svelte index 676d86b83d..96e687854e 100644 --- a/src/lib/components/notes/NotePanel.svelte +++ b/src/lib/components/notes/NotePanel.svelte @@ -99,7 +99,7 @@ {#if show}
From 92aafd6c0644c8b095ef8693633be053176a7576 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 15:31:25 -0700 Subject: [PATCH 013/125] refac --- backend/open_webui/routers/configs.py | 62 +++++++-------------------- backend/open_webui/utils/oauth.py | 37 ++++++++-------- 2 files changed, 32 insertions(+), 67 deletions(-) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index e8e876eac7..43ef73f29b 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -167,59 +167,27 @@ async def set_tool_servers_config( form_data: ToolServersConfigForm, user=Depends(get_admin_user), ): - old_connections = copy.deepcopy( - request.app.state.config.TOOL_SERVER_CONNECTIONS or [] - ) + mcp_server_ids = [ + conn.get("info", {}).get("id") + for conn in form_data.TOOL_SERVER_CONNECTIONS + if conn.get("type") == "mcp" + ] - new_connections = [ + for server_id in mcp_server_ids: + # Remove existing OAuth clients for MCP tool servers that are no longer present + client_key = f"mcp:{server_id}" + try: + request.app.state.oauth_client_manager.remove_client(client_key) + except: + pass + + # Set new tool server connections + request.app.state.config.TOOL_SERVER_CONNECTIONS = [ connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS ] - old_mcp_connections = { - conn.get("info", {}).get("id"): conn - for conn in old_connections - if conn.get("type") == "mcp" - } - new_mcp_connections = { - conn.get("info", {}).get("id"): conn - for conn in new_connections - if conn.get("type") == "mcp" - } - - purge_oauth_clients = set() - - for server_id, old_conn in old_mcp_connections.items(): - if not server_id: - continue - - old_auth_type = old_conn.get("auth_type", "none") - new_conn = new_mcp_connections.get(server_id) - - if new_conn is None: - if old_auth_type == "oauth_2.1": - purge_oauth_clients.add(server_id) - continue - - new_auth_type = new_conn.get("auth_type", "none") - - if old_auth_type == "oauth_2.1": - if ( - new_auth_type != "oauth_2.1" - or old_conn.get("url") != new_conn.get("url") - or old_conn.get("info", {}).get("oauth_client_info") - != new_conn.get("info", {}).get("oauth_client_info") - ): - purge_oauth_clients.add(server_id) - - request.app.state.config.TOOL_SERVER_CONNECTIONS = new_connections - await set_tool_servers(request) - for server_id in purge_oauth_clients: - client_key = f"mcp:{server_id}" - request.app.state.oauth_client_manager.remove_client(client_key) - OAuthSessions.delete_sessions_by_provider(client_key) - for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: server_type = connection.get("type", "openapi") if server_type == "mcp": diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 34fb441679..30939eb20a 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -153,32 +153,32 @@ def decrypt_data(data: str): raise -def _build_oauth_callback_error_message(exc: Exception) -> str: +def _build_oauth_callback_error_message(e: Exception) -> str: """ Produce a user-facing callback error string with actionable context. Keeps the message short and strips newlines for safe redirect usage. """ - if isinstance(exc, OAuth2Error): - parts = [p for p in [exc.error, exc.description] if p] + if isinstance(e, OAuth2Error): + parts = [p for p in [e.error, e.description] if p] detail = " - ".join(parts) - elif isinstance(exc, HTTPException): - detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail) - elif isinstance(exc, aiohttp.ClientResponseError): - detail = f"Upstream provider returned {exc.status}: {exc.message}" - elif isinstance(exc, aiohttp.ClientError): - detail = str(exc) - elif isinstance(exc, KeyError): - missing = str(exc).strip("'") + elif isinstance(e, HTTPException): + detail = e.detail if isinstance(e.detail, str) else str(e.detail) + elif isinstance(e, aiohttp.ClientResponseError): + detail = f"Upstream provider returned {e.status}: {e.message}" + elif isinstance(e, aiohttp.ClientError): + detail = str(e) + elif isinstance(e, KeyError): + missing = str(e).strip("'") if missing.lower() == "state": detail = "Missing state parameter in callback (session may have expired)" else: detail = f"Missing expected key '{missing}' in OAuth response" else: - detail = str(exc) + detail = str(e) detail = detail.replace("\n", " ").strip() if not detail: - detail = exc.__class__.__name__ + detail = e.__class__.__name__ message = f"OAuth callback failed: {detail}" return message[:197] + "..." if len(message) > 200 else message @@ -402,20 +402,18 @@ class OAuthClientManager: return self.clients[client_id] def remove_client(self, client_id): - removed = False if client_id in self.clients: del self.clients[client_id] - removed = True + log.info(f"Removed OAuth client {client_id}") + if hasattr(self.oauth, "_clients"): if client_id in self.oauth._clients: self.oauth._clients.pop(client_id, None) - removed = True + if hasattr(self.oauth, "_registry"): if client_id in self.oauth._registry: self.oauth._registry.pop(client_id, None) - removed = True - if removed: - log.info(f"Removed OAuth client {client_id}") + return True def _find_mcp_connection(self, request, client_id: str): @@ -574,7 +572,6 @@ class OAuthClientManager: self.remove_client(client_id) self.add_client(client_id, oauth_client_info) - OAuthSessions.delete_sessions_by_provider(client_id) log.info("Re-registered OAuth client %s for MCP tool server", client_id) return True From c8b2313362ce42476fc5775b6cfa7399b53f58f1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 15:38:59 -0700 Subject: [PATCH 014/125] refac --- backend/open_webui/main.py | 1 + backend/open_webui/routers/configs.py | 25 +++++++++++++------------ backend/open_webui/utils/oauth.py | 4 +++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 1405a43061..da89fd7de4 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1941,6 +1941,7 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: if tool_server_connection.get("type", "openapi") == "mcp": server_id = tool_server_connection.get("info", {}).get("id") auth_type = tool_server_connection.get("auth_type", "none") + if server_id and auth_type == "oauth_2.1": oauth_client_info = tool_server_connection.get("info", {}).get( "oauth_client_info", "" diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 43ef73f29b..5c08fded23 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -167,19 +167,19 @@ async def set_tool_servers_config( form_data: ToolServersConfigForm, user=Depends(get_admin_user), ): - mcp_server_ids = [ - conn.get("info", {}).get("id") - for conn in form_data.TOOL_SERVER_CONNECTIONS - if conn.get("type") == "mcp" - ] + for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: + server_type = connection.get("type", "openapi") + auth_type = connection.get("auth_type", "none") - for server_id in mcp_server_ids: - # Remove existing OAuth clients for MCP tool servers that are no longer present - client_key = f"mcp:{server_id}" - try: - request.app.state.oauth_client_manager.remove_client(client_key) - except: - pass + if auth_type == "oauth_2.1": + # Remove existing OAuth clients for tool servers + server_id = connection.get("info", {}).get("id") + client_key = f"{server_type}:{server_id}" + + try: + request.app.state.oauth_client_manager.remove_client(client_key) + except: + pass # Set new tool server connections request.app.state.config.TOOL_SERVER_CONNECTIONS = [ @@ -193,6 +193,7 @@ async def set_tool_servers_config( if server_type == "mcp": server_id = connection.get("info", {}).get("id") auth_type = connection.get("auth_type", "none") + if auth_type == "oauth_2.1" and server_id: try: oauth_client_info = connection.get("info", {}).get( diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 30939eb20a..03f7337774 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -582,6 +582,7 @@ class OAuthClientManager: client = self.get_client(client_id) client_info = self.get_client_info(client_id) + if client is None or client_info is None: raise HTTPException(status.HTTP_404_NOT_FOUND) @@ -593,6 +594,7 @@ class OAuthClientManager: "Detected invalid OAuth client %s; attempting re-registration", client_id, ) + re_registered = await self._re_register_client(request, client_id) if not re_registered: raise HTTPException( @@ -799,7 +801,7 @@ class OAuthClientManager: return None async def handle_authorize(self, request, client_id: str) -> RedirectResponse: - await self._ensure_valid_client_registration(request, client_id) + # await self._ensure_valid_client_registration(request, client_id) client = self.get_client(client_id) if client is None: From cbcab062eba85678d8bd6966cd85da1b6d50b16b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 16:46:04 -0700 Subject: [PATCH 015/125] refac --- backend/open_webui/main.py | 97 ++++++++++++++++- backend/open_webui/utils/oauth.py | 175 ++++-------------------------- 2 files changed, 116 insertions(+), 156 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index da89fd7de4..14ee4dc870 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -482,9 +482,11 @@ from open_webui.utils.auth import ( ) from open_webui.utils.plugin import install_tool_and_function_dependencies from open_webui.utils.oauth import ( + get_oauth_client_info_with_dynamic_client_registration, + encrypt_data, + decrypt_data, OAuthManager, OAuthClientManager, - decrypt_data, OAuthClientInformationFull, ) from open_webui.utils.security_headers import SecurityHeadersMiddleware @@ -1987,6 +1989,64 @@ except Exception as e: ) +async def register_client(self, request, client_id: str) -> bool: + server_type, server_id = client_id.split(":", 1) + + connection = None + connection_idx = None + + for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []): + if conn.get("type", "openapi") == server_type: + info = conn.get("info", {}) + if info.get("id") == server_id: + connection = conn + connection_idx = idx + break + + if connection is None or connection_idx is None: + log.warning( + f"Unable to locate MCP tool server configuration for client {client_id} during re-registration" + ) + return False + + server_url = connection.get("url") + oauth_server_key = (connection.get("config") or {}).get("oauth_server_key") + + try: + oauth_client_info = ( + await get_oauth_client_info_with_dynamic_client_registration( + request, + client_id, + server_url, + oauth_server_key, + ) + ) + except Exception as e: + log.error(f"Dynamic client re-registration failed for {client_id}: {e}") + return False + + try: + request.app.state.config.TOOL_SERVER_CONNECTIONS[connection_idx] = { + **connection, + "info": { + **connection.get("info", {}), + "oauth_client_info": encrypt_data( + oauth_client_info.model_dump(mode="json") + ), + }, + } + except Exception as e: + log.error( + f"Failed to persist updated OAuth client info for tool server {client_id}: {e}" + ) + return False + + oauth_client_manager.remove_client(client_id) + oauth_client_manager.add_client(client_id, oauth_client_info) + log.info(f"Re-registered OAuth client {client_id} for tool server") + return True + + @app.get("/oauth/clients/{client_id}/authorize") async def oauth_client_authorize( client_id: str, @@ -1994,6 +2054,41 @@ async def oauth_client_authorize( response: Response, user=Depends(get_verified_user), ): + # ensure_valid_client_registration + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + if not await oauth_client_manager._preflight_authorization_url(client, client_info): + log.info( + "Detected invalid OAuth client %s; attempting re-registration", + client_id, + ) + + re_registered = await register_client(request, client_id) + if not re_registered: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to re-register OAuth client", + ) + + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="OAuth client unavailable after re-registration", + ) + + if not await oauth_client_manager._preflight_authorization_url( + client, client_info + ): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="OAuth client registration is still invalid after re-registration", + ) + return await oauth_client_manager.handle_authorize(request, client_id=client_id) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 03f7337774..6889f377bc 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -416,34 +416,10 @@ class OAuthClientManager: return True - def _find_mcp_connection(self, request, client_id: str): - try: - connections = request.app.state.config.TOOL_SERVER_CONNECTIONS or [] - except Exception: - connections = [] - - normalized_client_id = client_id.split(":")[-1] - - for idx, connection in enumerate(connections): - if not isinstance(connection, dict): - continue - if connection.get("type") != "mcp": - continue - - info = connection.get("info") or {} - server_id = info.get("id") - if not server_id: - continue - - normalized_server_id = server_id.split(":")[-1] - if normalized_server_id == normalized_client_id: - return idx, connection - - return None, None - async def _preflight_authorization_url( self, client, client_info: OAuthClientInformationFull ) -> bool: + # TODO: Replace this logic with a more robust OAuth client registration validation # Only perform preflight checks for Starlette OAuth clients if not hasattr(client, "create_authorization_url"): return True @@ -454,168 +430,59 @@ class OAuthClientManager: try: auth_data = await client.create_authorization_url(redirect_uri=redirect_uri) - authorize_url = auth_data.get("url") - if not authorize_url: + authorization_url = auth_data.get("url") + + if not authorization_url: return True except Exception as e: log.debug( - "Skipping OAuth preflight for client %s: %s", - client_info.client_id, - e, + f"Skipping OAuth preflight for client {client_info.client_id}: {e}", ) return True try: async with aiohttp.ClientSession(trust_env=True) as session: async with session.get( - authorize_url, + authorization_url, allow_redirects=False, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as resp: if resp.status < 400: return True + response_text = await resp.text() - body_text = await resp.text() error = None error_description = "" - content_type = resp.headers.get("content-type", "") + content_type = resp.headers.get("content-type", "") if "application/json" in content_type: try: - payload = json.loads(body_text) + payload = json.loads(response_text) error = payload.get("error") error_description = payload.get("error_description", "") - except json.JSONDecodeError: - error = None - error_description = "" + except: + pass else: - error_description = body_text + error_description = response_text - combined = f"{error or ''} {error_description}".lower() - if ( - "invalid_client" in combined - or "invalid client" in combined - or "client id" in combined + error_message = f"{error or ''} {error_description or ''}".lower() + + if any( + keyword in error_message + for keyword in ("invalid_client", "invalid client", "client id") ): log.warning( - "OAuth client preflight detected invalid registration for %s: %s %s", - client_info.client_id, - error, - error_description, + f"OAuth client preflight detected invalid registration for {client_info.client_id}: {error} {error_description}" ) + return False except Exception as e: log.debug( - "Skipping OAuth preflight network check for client %s: %s", - client_info.client_id, - e, + f"Skipping OAuth preflight network check for client {client_info.client_id}: {e}" ) return True - async def _re_register_client(self, request, client_id: str) -> bool: - idx, connection = self._find_mcp_connection(request, client_id) - if idx is None or connection is None: - log.warning( - "Unable to locate MCP tool server configuration for client %s during re-registration", - client_id, - ) - return False - - server_url = connection.get("url") - oauth_server_key = (connection.get("config") or {}).get("oauth_server_key") - - try: - oauth_client_info = ( - await get_oauth_client_info_with_dynamic_client_registration( - request, - client_id, - server_url, - oauth_server_key, - ) - ) - except Exception as e: - log.error( - "Dynamic client re-registration failed for %s: %s", - client_id, - e, - ) - return False - - encrypted_info = encrypt_data(oauth_client_info.model_dump(mode="json")) - - updated_connections = copy.deepcopy( - request.app.state.config.TOOL_SERVER_CONNECTIONS or [] - ) - if idx >= len(updated_connections): - log.error( - "MCP tool server index %s out of range during OAuth client re-registration for %s", - idx, - client_id, - ) - return False - - updated_connection = copy.deepcopy(connection) - updated_connection.setdefault("info", {}) - updated_connection["info"]["oauth_client_info"] = encrypted_info - updated_connections[idx] = updated_connection - - try: - request.app.state.config.TOOL_SERVER_CONNECTIONS = updated_connections - except Exception as e: - log.error( - "Failed to persist updated OAuth client info for %s: %s", - client_id, - e, - ) - return False - - self.remove_client(client_id) - self.add_client(client_id, oauth_client_info) - - log.info("Re-registered OAuth client %s for MCP tool server", client_id) - return True - - async def _ensure_valid_client_registration(self, request, client_id: str) -> None: - if not client_id.startswith("mcp:"): - return - - client = self.get_client(client_id) - client_info = self.get_client_info(client_id) - - if client is None or client_info is None: - raise HTTPException(status.HTTP_404_NOT_FOUND) - - is_valid = await self._preflight_authorization_url(client, client_info) - if is_valid: - return - - log.info( - "Detected invalid OAuth client %s; attempting re-registration", - client_id, - ) - - re_registered = await self._re_register_client(request, client_id) - if not re_registered: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to re-register OAuth client", - ) - - client = self.get_client(client_id) - client_info = self.get_client_info(client_id) - if client is None or client_info is None: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="OAuth client unavailable after re-registration", - ) - - if not await self._preflight_authorization_url(client, client_info): - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="OAuth client registration is still invalid after re-registration", - ) - def get_client(self, client_id): client = self.clients.get(client_id) return client["client"] if client else None @@ -801,8 +668,6 @@ class OAuthClientManager: return None async def handle_authorize(self, request, client_id: str) -> RedirectResponse: - # await self._ensure_valid_client_registration(request, client_id) - client = self.get_client(client_id) if client is None: raise HTTPException(404) From b9bbf2258173301ba3b9be11a5b170ef6189a991 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 16:46:49 -0700 Subject: [PATCH 016/125] refac --- backend/open_webui/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 14ee4dc870..105fc5c337 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2066,8 +2066,8 @@ async def oauth_client_authorize( client_id, ) - re_registered = await register_client(request, client_id) - if not re_registered: + registered = await register_client(request, client_id) + if not registered: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to re-register OAuth client", From a776dbd01d851ddc5f8f45771f9d18ef1d558946 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 23:42:00 -0700 Subject: [PATCH 017/125] refac --- .../chat/Messages/Markdown/MarkdownInlineTokens.svelte | 2 +- .../Messages/Markdown/MarkdownInlineTokens/TextToken.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte index 4abcbd3246..85ba8740c1 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte @@ -24,7 +24,7 @@ export let onSourceClick: Function = () => {}; -{#each tokens as token} +{#each tokens as token, tokenIdx (tokenIdx)} {#if token.type === 'escape'} {unescapeHtml(token.text)} {:else if token.type === 'html'} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte index d5ae387afe..e7efd10064 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte @@ -13,7 +13,7 @@ {:else} {#each texts as text} - {text} + {text}{' '} {/each} {/if} From 76bde402feab78ffb6e5c57ba724afdf854a53f1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 27 Oct 2025 23:49:26 -0700 Subject: [PATCH 018/125] refac --- src/lib/components/workspace/Prompts.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 079ce85bdb..8cac0d5298 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -33,7 +33,7 @@ let promptsImportInputElement: HTMLInputElement; let loaded = false; - let importFiles = ''; + let importFiles = null; let query = ''; let prompts = []; From e986488ab5951bd3a5a0cc8d13286a2e420bb0d7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 00:06:52 -0700 Subject: [PATCH 019/125] enh: ELEVENLABS_API_BASE_URL env var --- backend/open_webui/config.py | 4 ++++ backend/open_webui/routers/audio.py | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index c794065974..d0a76e0238 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -3343,6 +3343,10 @@ DEEPGRAM_API_KEY = PersistentConfig( os.getenv("DEEPGRAM_API_KEY", ""), ) +# ElevenLabs configuration +ELEVENLABS_API_BASE_URL = os.getenv( + "ELEVENLABS_API_BASE_URL", "https://api.elevenlabs.io" +) AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( "AUDIO_STT_OPENAI_API_BASE_URL", diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index cb7a57b5b7..1213ffbd05 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -39,13 +39,14 @@ from open_webui.config import ( WHISPER_MODEL_DIR, CACHE_DIR, WHISPER_LANGUAGE, + ELEVENLABS_API_BASE_URL, ) from open_webui.constants import ERROR_MESSAGES from open_webui.env import ( + ENV, AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, - ENV, SRC_LOG_LEVELS, DEVICE_TYPE, ENABLE_FORWARD_USER_INFO_HEADERS, @@ -413,7 +414,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): timeout=timeout, trust_env=True ) as session: async with session.post( - f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}", + f"{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}", json={ "text": payload["input"], "model_id": request.app.state.config.TTS_MODEL, @@ -1037,7 +1038,7 @@ def get_available_models(request: Request) -> list[dict]: elif request.app.state.config.TTS_ENGINE == "elevenlabs": try: response = requests.get( - "https://api.elevenlabs.io/v1/models", + f"{ELEVENLABS_API_BASE_URL}/v1/models", headers={ "xi-api-key": request.app.state.config.TTS_API_KEY, "Content-Type": "application/json", @@ -1141,7 +1142,7 @@ def get_elevenlabs_voices(api_key: str) -> dict: try: # TODO: Add retries response = requests.get( - "https://api.elevenlabs.io/v1/voices", + f"{ELEVENLABS_API_BASE_URL}/v1/voices", headers={ "xi-api-key": api_key, "Content-Type": "application/json", From f524a6a8e7d10754d21b81a59fbf75f4cfa63213 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 00:34:53 -0700 Subject: [PATCH 020/125] refac/fix: kb image upload handling --- backend/open_webui/routers/files.py | 4 ++++ src/lib/apis/files/index.ts | 4 ++++ .../workspace/Knowledge/KnowledgeBase.svelte | 15 ++++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 84d8f841cf..2a5c3e5bb1 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -115,6 +115,10 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external" ): process_file(request, ProcessFileForm(file_id=file_item.id), user=user) + else: + raise Exception( + f"File type {file.content_type} is not supported for processing" + ) else: log.info( f"File type {file.content_type} is not provided, but trying to process anyway" diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts index 6a1763edb8..8351393e3c 100644 --- a/src/lib/apis/files/index.ts +++ b/src/lib/apis/files/index.ts @@ -63,6 +63,10 @@ export const uploadFile = async (token: string, file: File, metadata?: object | console.error(data.error); res.error = data.error; } + + if (res?.data) { + res.data = data; + } } } } diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 3c494e7609..0054eb2964 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -184,12 +184,6 @@ if (uploadedFile) { console.log(uploadedFile); - - if (uploadedFile.error) { - console.warn('File upload warning:', uploadedFile.error); - toast.warning(uploadedFile.error); - } - knowledge.files = knowledge.files.map((item) => { if (item.itemId === tempItemId) { item.id = uploadedFile.id; @@ -199,7 +193,14 @@ delete item.itemId; return item; }); - await addFileHandler(uploadedFile.id); + + if (uploadedFile.error) { + console.warn('File upload warning:', uploadedFile.error); + toast.warning(uploadedFile.error); + knowledge.files = knowledge.files.filter((file) => file.id !== uploadedFile.id); + } else { + await addFileHandler(uploadedFile.id); + } } else { toast.error($i18n.t('Failed to upload file.')); } From d1c9555a0b2c77c6b52cc487d74090565b5083df Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 01:46:36 -0700 Subject: [PATCH 021/125] refac --- src/lib/components/common/RichTextInput/suggestions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/components/common/RichTextInput/suggestions.ts b/src/lib/components/common/RichTextInput/suggestions.ts index 0667e83060..92111f1f19 100644 --- a/src/lib/components/common/RichTextInput/suggestions.ts +++ b/src/lib/components/common/RichTextInput/suggestions.ts @@ -1,4 +1,6 @@ import { mount, unmount } from 'svelte'; +import { createClassComponent } from 'svelte/legacy'; + import tippy from 'tippy.js'; export function getSuggestionRenderer(Component: any, ComponentProps = {}) { @@ -16,7 +18,8 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { document.body.appendChild(container); // mount Svelte component - component = mount(Component, { + component = createClassComponent({ + component: Component, target: container, props: { char: props?.text, @@ -106,7 +109,7 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { popup = null; try { - unmount(component); + component.$destroy(); } catch (e) { console.error('Error unmounting component:', e); } From 61a2909a88e96afa191cdbbe17a7905e41cf6e71 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 14:03:05 -0700 Subject: [PATCH 022/125] refac --- .../components/chat/Messages/Markdown/HTMLToken.svelte | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte index b66a0b01fd..fac4fad1de 100644 --- a/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte +++ b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte @@ -131,12 +131,9 @@ {/if} {:else if token.text.includes(` + {:else if token.text.trim().match(/^$/i)} +
{:else} - {@const br = token.text.match(//)} - {#if br} -
- {:else} - {token.text} - {/if} + {token.text} {/if} {/if} From a0068c4a17f2686a2c1eefe4f50d51bc9c96475a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 14:05:49 -0700 Subject: [PATCH 023/125] refac --- src/lib/components/layout/Sidebar.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 14e73f2046..4b4960930d 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -183,6 +183,7 @@ console.log('initChatList'); currentChatPage.set(1); allChatsLoaded = false; + scrollPaginationEnabled.set(false); initFolders(); await Promise.all([ From 292be82754136cc10fcd2707d78a2edfec6764d0 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 28 Oct 2025 17:31:38 -0700 Subject: [PATCH 024/125] refac/fix: sidebar open status --- src/lib/components/layout/Sidebar.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 4b4960930d..a5a6c59aca 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -369,10 +369,6 @@ navElement.style['-webkit-app-region'] = 'drag'; } } - - if (!$showSidebar && !value) { - showSidebar.set(true); - } }), showSidebar.subscribe(async (value) => { localStorage.sidebar = value; From 4a0359789f0f4e21a078f7c4d634625f346feb22 Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Tue, 28 Oct 2025 21:54:29 -0400 Subject: [PATCH 025/125] fix: add tooltips to tools on model edit page This change adds tooltips to the tools on the model edit page, displaying the tool's description on hover. --- src/lib/components/workspace/Models/ToolsSelector.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/components/workspace/Models/ToolsSelector.svelte b/src/lib/components/workspace/Models/ToolsSelector.svelte index 5f1b3b4821..af5f9f09f2 100644 --- a/src/lib/components/workspace/Models/ToolsSelector.svelte +++ b/src/lib/components/workspace/Models/ToolsSelector.svelte @@ -1,5 +1,6 @@ @@ -235,9 +177,11 @@ }} >
- {#if config && imageGenerationConfig} + {#if config}
-
{$i18n.t('Image Settings')}
+
{$i18n.t('Create Image')}
+ +
@@ -247,29 +191,35 @@
{ const enabled = e.detail; if (enabled) { if ( - config.engine === 'automatic1111' && - config.automatic1111.AUTOMATIC1111_BASE_URL === '' + config.IMAGE_GENERATION_ENGINE === 'automatic1111' && + config.AUTOMATIC1111_BASE_URL === '' ) { toast.error($i18n.t('AUTOMATIC1111 Base URL is required.')); - config.enabled = false; + config.ENABLE_IMAGE_GENERATION = false; } else if ( - config.engine === 'comfyui' && - config.comfyui.COMFYUI_BASE_URL === '' + config.IMAGE_GENERATION_ENGINE === 'comfyui' && + config.COMFYUI_BASE_URL === '' ) { toast.error($i18n.t('ComfyUI Base URL is required.')); - config.enabled = false; - } else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') { + config.ENABLE_IMAGE_GENERATION = false; + } else if ( + config.IMAGE_GENERATION_ENGINE === 'openai' && + config.OPENAI_API_KEY === '' + ) { toast.error($i18n.t('OpenAI API Key is required.')); - config.enabled = false; - } else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') { + config.ENABLE_IMAGE_GENERATION = false; + } else if ( + config.IMAGE_GENERATION_ENGINE === 'gemini' && + config.GEMINI_API_KEY === '' + ) { toast.error($i18n.t('Gemini API Key is required.')); - config.enabled = false; + config.ENABLE_IMAGE_GENERATION = false; } } @@ -284,7 +234,7 @@
{$i18n.t('Image Prompt Generation')}
- +
{/if} @@ -294,7 +244,7 @@
@@ -388,66 +339,22 @@
- -
-
{$i18n.t('Set Sampler')}
-
-
- - +
+
{$i18n.t('Additional Parameters')}
+
+
+