From 74988189b8974c463618687c4a284de7eabca869 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 13:06:50 -0600 Subject: [PATCH 01/70] refac --- backend/open_webui/routers/retrieval.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 0d513c9ae1..87ea359efa 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1746,6 +1746,7 @@ def process_file( DOCLING_API_KEY=request.app.state.config.DOCLING_API_KEY, DOCLING_PARAMS=request.app.state.config.DOCLING_PARAMS, PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE=request.app.state.config.PDF_LOADER_MODE, DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_MODEL=request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, From 49c36238d01aaff5466344ecd316a6dd3edd74a3 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 13:54:59 -0600 Subject: [PATCH 02/70] refac --- src/lib/components/AddConnectionModal.svelte | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index a455627e11..b8aa7f5e64 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -309,8 +309,21 @@ bind:value={url} placeholder={$i18n.t('API Base URL')} autocomplete="off" + list={ollama ? undefined : 'suggestions'} required /> + + {#if !ollama} + + + {/if} From e9d852545cc17f0eeb8bdcfa77575a80fed8706d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 14:24:42 -0600 Subject: [PATCH 03/70] refac --- backend/open_webui/routers/openai.py | 39 ++++++++----- backend/open_webui/utils/anthropic.py | 80 +++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 backend/open_webui/utils/anthropic.py diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 9fe421b4e9..c0a9268338 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -57,6 +57,7 @@ from open_webui.utils.misc import ( from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.anthropic import is_anthropic_url, get_anthropic_models log = logging.getLogger(__name__) @@ -68,6 +69,8 @@ log = logging.getLogger(__name__) ########################################## + + async def send_get_request(url, key=None, user: UserModel = None): timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) try: @@ -91,6 +94,12 @@ async def send_get_request(url, key=None, user: UserModel = None): return None +async def get_models_request(url, key=None, user: UserModel = None): + if is_anthropic_url(url): + return await get_anthropic_models(url, key, user=user) + return await send_get_request(f"{url}/models", key, user=user) + + def openai_reasoning_model_handler(payload): """ Handle reasoning model specific parameters @@ -366,11 +375,7 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: for idx, url in enumerate(api_base_urls): if (str(idx) not in api_configs) and (url not in api_configs): # Legacy support request_tasks.append( - send_get_request( - f"{url}/models", - api_keys[idx], - user=user, - ) + get_models_request(url, api_keys[idx], user=user) ) else: api_config = api_configs.get( @@ -384,11 +389,7 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: if enable: if len(model_ids) == 0: request_tasks.append( - send_get_request( - f"{url}/models", - api_keys[idx], - user=user, - ) + get_models_request(url, api_keys[idx], user=user) ) else: model_list = { @@ -594,6 +595,10 @@ async def get_models( "data": api_config.get("model_ids", []) or [], "object": "list", } + elif is_anthropic_url(url): + models = await get_anthropic_models(url, key, user=user) + if models is None: + raise Exception("Failed to connect to Anthropic API") else: async with session.get( f"{url}/models", @@ -602,7 +607,6 @@ async def get_models( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: if r.status != 200: - # Extract response error details if available error_detail = f"HTTP Error: {r.status}" try: res = await r.json() @@ -614,9 +618,7 @@ async def get_models( response_data = await r.json() - # Check if we're calling OpenAI API based on the URL if "api.openai.com" in url: - # Filter models according to the specified conditions response_data["data"] = [ model for model in response_data.get("data", []) @@ -707,6 +709,17 @@ async def verify_connection( ) return response_data + elif is_anthropic_url(url): + result = await get_anthropic_models(url, key) + if result is None: + raise HTTPException( + status_code=500, detail="Failed to connect to Anthropic API" + ) + if "error" in result: + raise HTTPException( + status_code=500, detail=result["error"] + ) + return result else: async with session.get( f"{url}/models", diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py new file mode 100644 index 0000000000..59236c32df --- /dev/null +++ b/backend/open_webui/utils/anthropic.py @@ -0,0 +1,80 @@ +import logging + +import aiohttp + +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + ENABLE_FORWARD_USER_INFO_HEADERS, +) +from open_webui.models.users import UserModel +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +def is_anthropic_url(url: str) -> bool: + """Check if the URL is an Anthropic API endpoint.""" + return "api.anthropic.com" in url + + +async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> dict: + """ + Fetch models from Anthropic's /v1/models endpoint with pagination. + Normalizes the response to OpenAI format. + """ + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + all_models = [] + after_id = None + + try: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + headers = { + "x-api-key": key, + "anthropic-version": "2023-06-01", + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + while True: + params = {"limit": 1000} + if after_id: + params["after_id"] = after_id + + async with session.get( + f"{url}/models", + headers=headers, + params=params, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + if response.status != 200: + error_detail = f"HTTP Error: {response.status}" + try: + res = await response.json() + if "error" in res: + error_detail = f"External Error: {res['error']}" + except Exception: + pass + return {"object": "list", "data": [], "error": error_detail} + + data = await response.json() + + for model in data.get("data", []): + all_models.append({ + "id": model.get("id"), + "object": "model", + "created": 0, + "owned_by": "anthropic", + "name": model.get("display_name", model.get("id")), + }) + + if not data.get("has_more", False): + break + after_id = data.get("last_id") + + except Exception as e: + log.error(f"Anthropic connection error: {e}") + return None + + return {"object": "list", "data": all_models} From a0195cd5ae9b9915295839cd0a5fbac5a1b0bfa2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 14:33:18 -0600 Subject: [PATCH 04/70] refac --- .../chat/ModelSelector/Selector.svelte | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index d2ecde992f..529bc44a44 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -569,11 +569,31 @@
{#if filteredItems.length === 0} -
-
- {$i18n.t('No results found')} + {#if items.length === 0 && $user?.role === 'admin'} +
+
+ {$i18n.t('No models available')} +
+
+ {$i18n.t('Connect to an AI provider to start chatting')} +
+ { + show = false; + }} + > + {$i18n.t('Manage Connections')} +
-
+ {:else} +
+
+ {$i18n.t('No results found')} +
+
+ {/if} {:else}
Date: Wed, 18 Feb 2026 14:40:40 -0600 Subject: [PATCH 05/70] refac --- .../Settings/Connections/OllamaConnection.svelte | 13 ++++++++++++- .../Settings/Connections/OpenAIConnection.svelte | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/lib/components/admin/Settings/Connections/OllamaConnection.svelte b/src/lib/components/admin/Settings/Connections/OllamaConnection.svelte index b6d33eb49f..036deadd05 100644 --- a/src/lib/components/admin/Settings/Connections/OllamaConnection.svelte +++ b/src/lib/components/admin/Settings/Connections/OllamaConnection.svelte @@ -3,6 +3,7 @@ const i18n = getContext('i18n'); import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; @@ -75,7 +76,7 @@ /> -
+
+ + + { + config.enable = config.enable ?? false; + onSubmit({ url, key: config?.key ?? '', config }); + }} + /> +
diff --git a/src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte b/src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte index a4c2ada059..cc97761740 100644 --- a/src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte +++ b/src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte @@ -3,6 +3,7 @@ const i18n = getContext('i18n'); import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import Cog6 from '$lib/components/icons/Cog6.svelte'; import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; @@ -98,7 +99,7 @@
-
+
+ + + { + config.enable = config.enable ?? false; + onSubmit({ url, key, config }); + }} + /> +
From 8a7f698e9db115f889473565041c12082a8cc21d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 14:42:00 -0600 Subject: [PATCH 06/70] refac --- .../chat/Settings/Tools/Connection.svelte | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Tools/Connection.svelte b/src/lib/components/chat/Settings/Tools/Connection.svelte index 05888eadd5..df4908aac2 100644 --- a/src/lib/components/chat/Settings/Tools/Connection.svelte +++ b/src/lib/components/chat/Settings/Tools/Connection.svelte @@ -3,6 +3,7 @@ const i18n = getContext('i18n'); import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import Cog6 from '$lib/components/icons/Cog6.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; @@ -67,7 +68,7 @@
-
+
+ + + { + if (!connection.config) connection.config = {}; + connection.config.enable = !(connection?.config?.enable ?? true); + onSubmit(connection); + }} + /> +
From 9b55343509b252fc8af355e23bbc73dddcbf097a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 14:43:07 -0600 Subject: [PATCH 07/70] refac --- .../chat/Settings/Connections/Connection.svelte | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/Settings/Connections/Connection.svelte b/src/lib/components/chat/Settings/Connections/Connection.svelte index c8a5ec152a..4ecd0d4d7b 100644 --- a/src/lib/components/chat/Settings/Connections/Connection.svelte +++ b/src/lib/components/chat/Settings/Connections/Connection.svelte @@ -4,6 +4,7 @@ import { settings } from '$lib/stores'; import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import Cog6 from '$lib/components/icons/Cog6.svelte'; import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; @@ -75,7 +76,7 @@
-
+
+ + + { + config.enable = config.enable ?? false; + onSubmit({ url, key, config }); + }} + /> +
From 094ed0b48cb86b9b6aff3c93f522072d11230761 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 14:58:39 -0600 Subject: [PATCH 08/70] fix: prompts delete --- backend/open_webui/models/prompts.py | 59 ++++++++++----------- backend/open_webui/routers/prompts.py | 42 +++++++++++++++ src/lib/apis/prompts/index.ts | 28 ++++++++++ src/lib/components/workspace/Prompts.svelte | 16 ++++++ 4 files changed, 113 insertions(+), 32 deletions(-) diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 3ab7a496ab..80b7856f0f 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -259,7 +259,6 @@ class PromptsTable: # Join with User table for user filtering and sorting query = db.query(Prompt, User).outerjoin(User, User.id == Prompt.user_id) - query = query.filter(Prompt.is_active == True) if filter: query_key = filter.get("query") @@ -562,43 +561,24 @@ class PromptsTable: except Exception: return None - def delete_prompt_by_command( - self, command: str, db: Optional[Session] = None - ) -> bool: - """Soft delete a prompt by setting is_active to False.""" - try: - with get_db_context(db) as db: - prompt = db.query(Prompt).filter_by(command=command).first() - if prompt: - PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) - AccessGrants.revoke_all_access("prompt", prompt.id, db=db) - - prompt.is_active = False - prompt.updated_at = int(time.time()) - db.commit() - return True - return False - except Exception: - return False - - def delete_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> bool: - """Soft delete a prompt by setting is_active to False.""" + def toggle_prompt_active( + self, prompt_id: str, db: Optional[Session] = None + ) -> Optional[PromptModel]: + """Toggle the is_active flag on a prompt.""" try: with get_db_context(db) as db: prompt = db.query(Prompt).filter_by(id=prompt_id).first() if prompt: - PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) - AccessGrants.revoke_all_access("prompt", prompt.id, db=db) - - prompt.is_active = False + prompt.is_active = not prompt.is_active prompt.updated_at = int(time.time()) db.commit() - return True - return False + db.refresh(prompt) + return self._to_prompt_model(prompt, db=db) + return None except Exception: - return False + return None - def hard_delete_prompt_by_command( + def delete_prompt_by_command( self, command: str, db: Optional[Session] = None ) -> bool: """Permanently delete a prompt and its history.""" @@ -609,8 +589,23 @@ class PromptsTable: PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) AccessGrants.revoke_all_access("prompt", prompt.id, db=db) - # Delete prompt - db.query(Prompt).filter_by(command=command).delete() + db.delete(prompt) + db.commit() + return True + return False + except Exception: + return False + + def delete_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> bool: + """Permanently delete a prompt and its history.""" + try: + with get_db_context(db) as db: + prompt = db.query(Prompt).filter_by(id=prompt_id).first() + if prompt: + PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) + AccessGrants.revoke_all_access("prompt", prompt.id, db=db) + + db.delete(prompt) db.commit() return True return False diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 2491578959..9653571fbb 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -497,6 +497,48 @@ async def update_prompt_access_by_id( return Prompts.get_prompt_by_id(prompt_id, db=db) +############################ +# TogglePromptActiveById +############################ + + +@router.post("/id/{prompt_id}/toggle", response_model=Optional[PromptModel]) +async def toggle_prompt_active( + prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) +): + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not AccessGrants.has_access( + user_id=user.id, + resource_type="prompt", + resource_id=prompt.id, + permission="write", + db=db, + ) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = Prompts.toggle_prompt_active(prompt.id, db=db) + if result: + return result + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + ############################ # DeletePromptById ############################ diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index 1fd311c76f..5db7dc3540 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -395,6 +395,34 @@ export const setProductionPromptVersion = async ( return res; }; +export const togglePromptById = async (token: string, promptId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const deletePromptById = async (token: string, promptId: string) => { let error = null; diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 4e99651f85..726571d8a3 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -10,6 +10,7 @@ import { createNewPrompt, deletePromptById, + togglePromptById, getPrompts, getPromptItems, getPromptTags @@ -31,6 +32,7 @@ import ViewSelector from './common/ViewSelector.svelte'; import TagSelector from './common/TagSelector.svelte'; import Badge from '$lib/components/common/Badge.svelte'; + import Switch from '../common/Switch.svelte'; import Pagination from '../common/Pagination.svelte'; let shiftKey = false; @@ -398,6 +400,9 @@
/{prompt.command}
+ {#if prompt.is_active === false} + + {/if} {#if !prompt.write_access} @@ -484,6 +489,17 @@ + + {/if} From 3dd44c4f1931d13bfd46062291c6f23b33dde003 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Feb 2026 15:23:50 -0600 Subject: [PATCH 09/70] refac --- backend/open_webui/models/access_grants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index dd5a344b46..a15e4f3c5d 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -402,7 +402,7 @@ class AccessGrantsTable: results = [] for grant_dict in normalized_grants: grant = AccessGrant( - id=grant_dict["id"], + id=str(uuid.uuid4()), resource_type=resource_type, resource_id=resource_id, principal_type=grant_dict["principal_type"], From f872a178bc2ede37ba4d5c80cbd9002286f6d0cf Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 14:06:24 -0600 Subject: [PATCH 10/70] refac --- backend/open_webui/tools/builtin.py | 75 +++++++++++++++++++++++++++++ backend/open_webui/utils/tools.py | 4 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index 31c79484ab..330baf1318 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -36,6 +36,8 @@ from open_webui.models.chats import Chats from open_webui.models.channels import Channels, ChannelMember, Channel from open_webui.models.messages import Messages, Message from open_webui.models.groups import Groups +from open_webui.models.memories import Memories +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT from open_webui.utils.sanitize import sanitize_code log = logging.getLogger(__name__) @@ -634,6 +636,79 @@ async def replace_memory_content( return json.dumps({"error": str(e)}) +async def delete_memory( + memory_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Delete a memory by its ID. + + :param memory_id: The ID of the memory to delete + :return: Confirmation that the memory was deleted + """ + if __request__ is None: + return json.dumps({"error": "Request context not available"}) + + try: + user = UserModel(**__user__) if __user__ else None + + result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id) + + if result: + VECTOR_DB_CLIENT.delete( + collection_name=f"user-memory-{user.id}", ids=[memory_id] + ) + return json.dumps( + {"status": "success", "message": f"Memory {memory_id} deleted"}, + ensure_ascii=False, + ) + else: + return json.dumps({"error": "Memory not found or access denied"}) + except Exception as e: + log.exception(f"delete_memory error: {e}") + return json.dumps({"error": str(e)}) + + +async def list_memories( + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + List all stored memories for the user. + + :return: JSON list of all memories with id, content, and dates + """ + if __request__ is None: + return json.dumps({"error": "Request context not available"}) + + try: + user = UserModel(**__user__) if __user__ else None + + memories = Memories.get_memories_by_user_id(user.id) + + if memories: + result = [ + { + "id": m.id, + "content": m.content, + "created_at": time.strftime( + "%Y-%m-%d %H:%M", time.localtime(m.created_at) + ), + "updated_at": time.strftime( + "%Y-%m-%d %H:%M", time.localtime(m.updated_at) + ), + } + for m in memories + ] + return json.dumps(result, ensure_ascii=False) + else: + return json.dumps([]) + except Exception as e: + log.exception(f"list_memories error: {e}") + return json.dumps({"error": str(e)}) + + # ============================================================================= # NOTES TOOLS # ============================================================================= diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index aeba213392..653e002740 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -60,6 +60,8 @@ from open_webui.tools.builtin import ( search_memories, add_memory, replace_memory_content, + delete_memory, + list_memories, get_current_timestamp, calculate_timestamp, search_notes, @@ -471,7 +473,7 @@ def get_builtin_tools( # Add memory tools if builtin category enabled AND enabled for this chat if is_builtin_tool_enabled("memory") and features.get("memory"): - builtin_functions.extend([search_memories, add_memory, replace_memory_content]) + builtin_functions.extend([search_memories, add_memory, replace_memory_content, delete_memory, list_memories]) # Add web search tools if builtin category enabled AND enabled globally AND model has web_search capability if ( From af5661c2c807465f5600899e8c1a421f96cd7a8c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:08:15 +0100 Subject: [PATCH 11/70] Merge pull request #21485 from Classic298/claude/fix-mcp-ssl-check-0janH fix: mcp ssl check --- backend/open_webui/utils/mcp/client.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/utils/mcp/client.py b/backend/open_webui/utils/mcp/client.py index 33803648be..1004536e4d 100644 --- a/backend/open_webui/utils/mcp/client.py +++ b/backend/open_webui/utils/mcp/client.py @@ -9,14 +9,27 @@ from mcp.client.auth import OAuthClientProvider, TokenStorage from mcp.client.streamable_http import streamablehttp_client from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken import httpx -from mcp.shared._httpx_utils import create_mcp_http_client from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL def create_insecure_httpx_client(headers=None, timeout=None, auth=None): - client = create_mcp_http_client(headers=headers, timeout=timeout, auth=auth) - client.verify = False - return client + """Create an httpx AsyncClient with SSL verification disabled. + + Note: verify=False must be passed at construction time because httpx + configures the SSL context during __init__. Setting client.verify = False + after construction does not affect the underlying transport's SSL context. + """ + kwargs = { + "follow_redirects": True, + "verify": False, + } + if timeout is not None: + kwargs["timeout"] = timeout + if headers is not None: + kwargs["headers"] = headers + if auth is not None: + kwargs["auth"] = auth + return httpx.AsyncClient(**kwargs) class MCPClient: From 56246324b222df1c8377ed172a20ff1099bf8e62 Mon Sep 17 00:00:00 2001 From: Minwoo 'Charlie' Choi Date: Fri, 20 Feb 2026 05:13:50 +0900 Subject: [PATCH 12/70] fix: apply AIOHTTP_CLIENT_TIMEOUT to embeddings endpoint (#21558) --- backend/open_webui/routers/openai.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index c0a9268338..f468a8d105 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -1194,7 +1194,10 @@ async def embeddings(request: Request, form_data: dict, user): request, url, key, api_config, user=user ) try: - session = aiohttp.ClientSession(trust_env=True) + session = aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) r = await session.request( method="POST", url=f"{url}/embeddings", @@ -1421,7 +1424,10 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): else: request_url = f"{url}/{path}" - session = aiohttp.ClientSession(trust_env=True) + session = aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) r = await session.request( method=request.method, url=request_url, From 8bfab327ec5f635f9fe93c26efd198712ff7116d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 14:14:36 -0600 Subject: [PATCH 13/70] refac --- backend/open_webui/env.py | 4 ++++ backend/open_webui/utils/plugin.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 86fe787899..02891d6810 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -978,6 +978,10 @@ OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get( # TOOLS/FUNCTIONS PIP OPTIONS #################################### +ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( + os.environ.get("ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS", "True").lower() == "true" +) + PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split() PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split() diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 2dd49fb8ff..df52da6e9f 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -8,7 +8,7 @@ import tempfile import logging from typing import Any -from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, OFFLINE_MODE +from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, OFFLINE_MODE, ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS from open_webui.models.functions import Functions from open_webui.models.tools import Tools @@ -401,6 +401,10 @@ def get_function_module_from_cache(request, function_id, load_from_db=True): def install_frontmatter_requirements(requirements: str): + if not ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS: + log.info("ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS is disabled, skipping installation of requirements.") + return + if OFFLINE_MODE: log.info("Offline mode enabled, skipping installation of requirements.") return From 2f1344d619cc053c68e1c939753a59a9090e8075 Mon Sep 17 00:00:00 2001 From: Kungs-Fr <102975249+Kungs-Fr@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:15:31 +0100 Subject: [PATCH 14/70] Update translation.json (#21602) Typography issue correction Co-authored-by: Tim Baek Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> --- src/lib/i18n/locales/fr-FR/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/i18n/locales/fr-FR/translation.json b/src/lib/i18n/locales/fr-FR/translation.json index e44f3791dd..ea43ee6d3a 100644 --- a/src/lib/i18n/locales/fr-FR/translation.json +++ b/src/lib/i18n/locales/fr-FR/translation.json @@ -128,7 +128,7 @@ "Amazing": "Incroyable", "an assistant": "un assistant", "An error occurred while fetching the explanation": "Une erreur s'est produite lors de la récupération de l'explication", - "Analytics": "Analtique", + "Analytics": "Analytique", "Analyzed": "Analysé", "Analyzing...": "Analyse en cours", "and {{COUNT}} more": "et {{COUNT}} autres", From 27c76c677aab6f0ec4fe27471d700f95f748e7a9 Mon Sep 17 00:00:00 2001 From: Patrick Monteith Date: Thu, 19 Feb 2026 21:08:42 +0000 Subject: [PATCH 15/70] fix: clamp SCIM pagination args instead of rejecting them (#21577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 7644 §3.4.2.4 specifies that out-of-range pagination values MUST be clamped, not rejected. The previous implementation used FastAPI Query constraints (ge=1, le=100) which caused a 422 response for values like startIndex=0 or count=9999 — violating the spec. For both /Users and /Groups: - startIndex < 1 is now treated as 1 (spec: "SHALL be interpreted as 1") - count < 0 is now treated as 0 (spec: "SHALL be interpreted as 0") - count > 100 is clamped to the server maximum of 100 Co-authored-by: Claude Sonnet 4.6 --- backend/open_webui/routers/scim.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 0c16eb99bd..4d56a7e97f 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -523,13 +523,17 @@ async def get_schemas(): @router.get("/Users", response_model=SCIMListResponse) async def get_users( request: Request, - startIndex: int = Query(1, ge=1), - count: int = Query(20, ge=1, le=100), + startIndex: int = Query(1), + count: int = Query(20), filter: Optional[str] = None, _: bool = Depends(get_scim_auth), db: Session = Depends(get_session), ): """List SCIM Users""" + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) skip = startIndex - 1 limit = count @@ -794,13 +798,18 @@ async def delete_user( @router.get("/Groups", response_model=SCIMListResponse) async def get_groups( request: Request, - startIndex: int = Query(1, ge=1), - count: int = Query(20, ge=1, le=100), + startIndex: int = Query(1), + count: int = Query(20), filter: Optional[str] = None, _: bool = Depends(get_scim_auth), db: Session = Depends(get_session), ): """List SCIM Groups""" + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) + # Get all groups groups_list = Groups.get_all_groups(db=db) From 35763a352c43fae605130a4ad13453329bc77d1c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:50:03 +0100 Subject: [PATCH 16/70] Optimize shared chats list to use column projection (#163) (#21614) The GET /chats/shared endpoint was loading full Chat rows including the entire conversation history JSON blob, only to discard it and return SharedChatResponse (id, title, share_id, timestamps). Now uses with_entities() to select only the 5 needed columns, avoiding deserialization of potentially large chat JSON for every shared chat. --- backend/open_webui/models/chats.py | 20 ++++++++++++++++++-- backend/open_webui/routers/chats.py | 19 +++++++------------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 1418abd62d..89148f08be 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -754,7 +754,7 @@ class ChatTable: skip: int = 0, limit: int = 50, db: Optional[Session] = None, - ) -> list[ChatModel]: + ) -> list[SharedChatResponse]: with get_db_context(db) as db: query = ( @@ -784,13 +784,29 @@ class ChatTable: else: query = query.order_by(Chat.updated_at.desc()) + # Select only the columns needed for SharedChatResponse + # to avoid loading the heavy chat JSON blob + query = query.with_entities( + Chat.id, Chat.title, Chat.share_id, + Chat.updated_at, Chat.created_at, + ) + if skip: query = query.offset(skip) if limit: query = query.limit(limit) all_chats = query.all() - return [ChatModel.model_validate(chat) for chat in all_chats] + return [ + SharedChatResponse( + id=chat[0], + title=chat[1], + share_id=chat[2], + updated_at=chat[3], + created_at=chat[4], + ) + for chat in all_chats + ] def get_chat_list_by_user_id( self, diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 69e47123f0..e04c234966 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -887,18 +887,13 @@ async def get_shared_session_user_chat_list( if direction: filter["direction"] = direction - chat_list = [ - SharedChatResponse(**chat.model_dump()) - for chat in Chats.get_shared_chat_list_by_user_id( - user.id, - filter=filter, - skip=skip, - limit=limit, - db=db, - ) - ] - - return chat_list + return Chats.get_shared_chat_list_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) ############################ From 723185c22f4480e703f9237f8e6260f612674fbc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 15:59:58 -0600 Subject: [PATCH 17/70] refac --- src/lib/components/admin/Functions.svelte | 5 ++++- src/lib/components/workspace/Tools.svelte | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index 48c1863e74..8cd283381a 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -681,7 +681,8 @@ } toast.success($i18n.t('Functions imported successfully')); - functions.set(await getFunctions(localStorage.token)); + functions = await getFunctionList(localStorage.token); + _functions.set(await getFunctions(localStorage.token)); models.set( await getModels( localStorage.token, @@ -690,6 +691,8 @@ true ) ); + importFiles = null; + functionsImportInputElement.value = ''; }; reader.readAsText(importFiles[0]); diff --git a/src/lib/components/workspace/Tools.svelte b/src/lib/components/workspace/Tools.svelte index cd0d5c35ba..169b270c37 100644 --- a/src/lib/components/workspace/Tools.svelte +++ b/src/lib/components/workspace/Tools.svelte @@ -615,7 +615,9 @@ } toast.success($i18n.t('Tool imported successfully')); - tools.set(await getTools(localStorage.token)); + await init(); + importFiles = null; + toolsImportInputElement.value = ''; }; reader.readAsText(importFiles[0]); From 4bef69cc6344ff809090441aa6bced573a2aa838 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:03:03 -0600 Subject: [PATCH 18/70] refac --- backend/open_webui/routers/retrieval.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 87ea359efa..b16bc022ee 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1934,6 +1934,7 @@ async def process_web( request: Request, form_data: ProcessUrlForm, process: bool = Query(True, description="Whether to process and save the content"), + overwrite: bool = Query(True, description="Whether to overwrite existing collection"), user=Depends(get_verified_user), ): try: @@ -1953,7 +1954,7 @@ async def process_web( request, docs, collection_name, - overwrite=True, + overwrite=overwrite, user=user, ) else: From 139f02a9d9fa2ffffcc96aa0de8af8ef51b6bcf2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:04:41 -0600 Subject: [PATCH 19/70] refac --- backend/open_webui/models/chats.py | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 89148f08be..f579a11822 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -489,6 +489,7 @@ class ChatTable: if isinstance(message.get("content"), str): message["content"] = sanitize_text_for_db(message["content"]) + user_id = chat.user_id chat = chat.chat history = chat.get("history", {}) @@ -509,7 +510,7 @@ class ChatTable: ChatMessages.upsert_message( message_id=message_id, chat_id=id, - user_id=self.get_chat_by_id(id).user_id, + user_id=user_id, data=history["messages"][message_id], ) except Exception as e: @@ -787,8 +788,11 @@ class ChatTable: # Select only the columns needed for SharedChatResponse # to avoid loading the heavy chat JSON blob query = query.with_entities( - Chat.id, Chat.title, Chat.share_id, - Chat.updated_at, Chat.created_at, + Chat.id, + Chat.title, + Chat.share_id, + Chat.updated_at, + Chat.created_at, ) if skip: @@ -1146,23 +1150,29 @@ class ChatTable: # Check if there are any tags to filter, it should have all the tags if "none" in tag_ids: - query = query.filter(text(""" + query = query.filter( + text( + """ NOT EXISTS ( SELECT 1 FROM json_each(Chat.meta, '$.tags') AS tag ) - """)) + """ + ) + ) elif tag_ids: query = query.filter( and_( *[ - text(f""" + text( + f""" EXISTS ( SELECT 1 FROM json_each(Chat.meta, '$.tags') AS tag WHERE tag.value = :tag_id_{tag_idx} ) - """).params(**{f"tag_id_{tag_idx}": tag_id}) + """ + ).params(**{f"tag_id_{tag_idx}": tag_id}) for tag_idx, tag_id in enumerate(tag_ids) ] ) @@ -1198,23 +1208,29 @@ class ChatTable: # Check if there are any tags to filter, it should have all the tags if "none" in tag_ids: - query = query.filter(text(""" + query = query.filter( + text( + """ NOT EXISTS ( SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') AS tag ) - """)) + """ + ) + ) elif tag_ids: query = query.filter( and_( *[ - text(f""" + text( + f""" EXISTS ( SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') AS tag WHERE tag = :tag_id_{tag_idx} ) - """).params(**{f"tag_id_{tag_idx}": tag_id}) + """ + ).params(**{f"tag_id_{tag_idx}": tag_id}) for tag_idx, tag_id in enumerate(tag_ids) ] ) From ff837031e447faedfce1c5924f245b5b3b73bfc8 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:05:47 +0100 Subject: [PATCH 20/70] Update iframe sandbox attributes based on settings (#21529) Co-authored-by: :o <52920416+gg0h@users.noreply.github.com> --- src/lib/components/common/ToolCallDisplay.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/components/common/ToolCallDisplay.svelte b/src/lib/components/common/ToolCallDisplay.svelte index 9c2ed0932e..b692a3674d 100644 --- a/src/lib/components/common/ToolCallDisplay.svelte +++ b/src/lib/components/common/ToolCallDisplay.svelte @@ -16,6 +16,7 @@ import CheckCircle from '../icons/CheckCircle.svelte'; import Image from './Image.svelte'; import FullHeightIframe from './FullHeightIframe.svelte'; + import { settings } from '$lib/stores'; export let id: string = ''; export let attributes: { @@ -92,8 +93,8 @@ src={embed} {args} allowScripts={true} - allowForms={true} - allowSameOrigin={true} + allowForms={$settings?.iframeSandboxAllowForms ?? false} + allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false} allowPopups={true} /> From 1dfe546b6b1be87aae3fd84064d1fad59e99e380 Mon Sep 17 00:00:00 2001 From: joaoback <156559121+joaoback@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:06:08 -0300 Subject: [PATCH 21/70] i18n: pt-BR - add translations for newly added UI items + consistency pass (#21527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate all remaining untranslated strings to Brazilian Portuguese (pt-BR) Translated ~100 previously untranslated entries (empty "" values) across the Open WebUI i18n JSON file. Changes include: Translated UI labels, form fields, tooltips, and error messages Kept brand/product names unchanged where appropriate (e.g., Bing, Brave, Gemini, OpenAI, YouTube) Translated technical terms with context-appropriate Brazilian Portuguese equivalents (e.g., "Timeout" → "Tempo limite", "Config" → "Configuração", "Endpoint URL" → "URL do Endpoint") Added translations for search engine integrations (Kagi, Perplexity, SerpApi, Tavily, Yacy, Yandex, etc.) Translated plural forms for source retrieval messages (sources_one, sources_many, sources_other) No untranslated entries remain in the file. --- src/lib/i18n/locales/pt-BR/translation.json | 236 ++++++++++---------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index b3cea4aad6..c20d8afd07 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -208,11 +208,11 @@ "Banners": "Banners", "Base Model (From)": "Modelo Base (De)", "Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.": "O cache da lista de modelos base acelera o acesso buscando modelos base somente na inicialização ou ao salvar as configurações — mais rápido, mas pode não mostrar alterações recentes no modelo base.", - "Bearer": "", + "Bearer": "Bearer", "before": "antes", "Being lazy": "Sendo preguiçoso", "Beta": "Beta", - "Bing": "", + "Bing": "Bing", "Bing Search V7 Endpoint": "Endpoint do Bing Search V7", "Bing Search V7 Subscription Key": "Chave de assinatura do Bing Search V7", "Bio": "Sobre você", @@ -221,7 +221,7 @@ "Bocha Search API Key": "Chave da API de pesquisa Bocha", "Bold": "Negrito", "Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Aumentar ou penalizar tokens específicos para respostas restritas. Os valores de viés serão fixados entre -100 e 100 (inclusive). (Padrão: nenhum)", - "Brave": "", + "Brave": "Brave", "Brave Search API Key": "Chave API do Brave Search", "Browse and query knowledge bases": "Navegue e consulte bases de conhecimento.", "Builtin Tools": "Ferramentas integradas", @@ -314,7 +314,7 @@ "Close settings modal": "Fechar configurações modal", "Close Sidebar": "Fechar barra lateral", "cloud": "cloud", - "CMU ARCTIC speaker embedding name": "", + "CMU ARCTIC speaker embedding name": "Nome do embedding de voz CMU ARCTIC", "Code Block": "Bloco de código", "Code Editor": "Editor de código", "Code execution": "Execução de código", @@ -334,8 +334,8 @@ "ComfyUI API Key": "Chave de API do ComfyUI", "ComfyUI Base URL": "URL Base do ComfyUI", "ComfyUI Base URL is required.": "URL Base do ComfyUI é necessária.", - "ComfyUI Workflow": "", - "ComfyUI Workflow Nodes": "", + "ComfyUI Workflow": "Fluxo de trabalho ComfyUI", + "ComfyUI Workflow Nodes": "Nós do fluxo de trabalho ComfyUI", "Comma separated Node Ids (e.g. 1 or 1,2)": "IDs de Nodes separados por vírgula (por exemplo, 1 ou 1,2)", "command": "comando", "Command": "Comando", @@ -345,7 +345,7 @@ "Completions": "Completions", "Compress Images in Channels": "Comprimir imagens em canais", "Concurrent Requests": "Solicitações simultâneas", - "Config": "", + "Config": "Configuração", "Config imported successfully": "Configuração importada com sucesso", "Configure": "Configurar", "Confirm": "Confirmar", @@ -427,10 +427,10 @@ "Database": "Banco de Dados", "Datalab Marker API": "API do Marcador do Datalab", "DD/MM/YYYY": "DD/MM/AAAA", - "DDGS Backend": "", + "DDGS Backend": "Backend DDGS", "December": "Dezembro", "Decrease UI Scale": "Diminuir a escala da interface do usuário", - "Deepgram": "", + "Deepgram": "Deepgram", "Default": "Padrão", "Default (Open AI)": "Padrão (Open AI)", "Default (SentenceTransformers)": "Padrão (SentenceTransformers)", @@ -519,7 +519,7 @@ "Do not install functions from sources you do not fully trust.": "Não instale funções de origens nas quais você não confia totalmente.", "Do not install tools from sources you do not fully trust.": "Não instale ferramentas de origens nas quais você não confia totalmente.", "Do you want to sync your usage stats with Open WebUI Community?": "Deseja sincronizar suas estatísticas de uso com a Comunidade Open WebUI?", - "Docling": "", + "Docling": "Docling", "Docling Parameters": "Parâmetros Docling", "Docling Server URL required.": "URL do servidor Docling necessária.", "Document": "Documento", @@ -545,7 +545,7 @@ "Downloading stats...": "Baixando estatísticas...", "Draw": "Empate", "Drop any files here to upload": "Solte qualquer arquivo aqui para fazer upload", - "DuckDuckGo": "", + "DuckDuckGo": "DuckDuckGo", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "por exemplo, '30s', '10m'. Unidades de tempo válidas são 's', 'm', 'h'.", "e.g. \"json\" or a JSON schema": "por exemplo, \"json\" ou um esquema JSON", "e.g. 60": "por exemplo, 60", @@ -585,7 +585,7 @@ "Edited": "Editado", "Editing": "Editando", "Eject": "Ejetar", - "ElevenLabs": "", + "ElevenLabs": "ElevenLabs", "Email": "Email", "Embark on adventures": "Embarque em aventuras", "Embedding": "Embedding", @@ -607,7 +607,7 @@ "Enable, disable, or customize the reasoning tags used by the model. \"Enabled\" uses default tags, \"Disabled\" turns off reasoning tags, and \"Custom\" lets you specify your own start and end tags.": "Habilite, desabilite ou personalize as tags de raciocínio usadas pelo modelo. \"Ativado\" usa tags padrão, \"Desativado\" desativa as tags de raciocínio e \"Personalizado\" permite que você especifique suas próprias tags de início e fim.", "Enabled": "Ativado", "End Tag": "Tag final", - "Endpoint URL": "", + "Endpoint URL": "URL do Endpoint", "Enforce Temporary Chat": "Aplicar chat temporário", "Enhance": "Melhorar", "Enrich Hybrid Search Text": "Enriquecer o texto da pesquisa híbrida", @@ -680,9 +680,9 @@ "Enter Number of Steps (e.g. 50)": "Digite o Número de Passos (por exemplo, 50)", "Enter Ollama Cloud API Key": "Insira a chave da API do Ollama Cloud", "Enter Perplexity API Key": "Insira a chave da API Perplexity", - "Enter Perplexity Search API URL": "", - "Enter Playwright Timeout": "", - "Enter Playwright WebSocket URL": "", + "Enter Perplexity Search API URL": "Insira a URL da API de pesquisa Perplexity", + "Enter Playwright Timeout": "Insira o tempo limite do Playwright", + "Enter Playwright WebSocket URL": "Insira a URL do WebSocket do Playwright", "Enter proxy URL (e.g. https://user:password@host:port)": "Insira a URL do proxy (por exemplo, https://usuário:senha@host:porta)", "Enter reasoning effort": "Insira o esforço de raciocínio", "Enter Score": "Digite a Pontuação", @@ -706,7 +706,7 @@ "Enter system prompt": "Digite o prompt do sistema", "Enter system prompt here": "Insira o prompt do sistema aqui", "Enter Tavily API Key": "Digite a Chave API do Tavily", - "Enter Tavily Extract Depth": "", + "Enter Tavily Extract Depth": "Insira a profundidade de extração do Tavily", "Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Insira a URL pública da sua WebUI. Esta URL será usada para gerar links nas notificações.", "Enter the URL of the function to import": "Digite a URL da função a ser importada", "Enter the URL to import": "Digite a URL para importar", @@ -749,7 +749,7 @@ "Error: A model with the ID '{{modelId}}' already exists. Please select a different ID to proceed.": "Erro: Já existe um modelo com o ID '{{modelId}}'. Selecione um ID diferente para prosseguir.", "Error: Model ID cannot be empty. Please enter a valid ID to proceed.": "Erro: O ID do modelo não pode estar vazio. Insira um ID válido para prosseguir.", "Evaluations": "Avaliações", - "Exa API Key": "", + "Exa API Key": "Chave da API Exa", "Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Exemplo: (&(objectClass=inetOrgPerson)(uid=%s))", "Example: ALL": "Exemplo: ALL", "Example: mail": "Exemplo: mail", @@ -769,7 +769,7 @@ "Export All Chats (All Users)": "Exportar Todos os Chats (Todos os Usuários)", "Export chat (.json)": "Exportar chat (.json)", "Export Chats": "Exportar Chats", - "Export Config": "", + "Export Config": "Exportar Configuração", "Export Models": "Exportar Modelos", "Export Presets": "Exportar Presets", "Export Prompts": "Exportar Prompts", @@ -817,7 +817,7 @@ "Features": "Funcionalidades", "Features Permissions": "Permissões das Funcionalidades", "February": "Fevereiro", - "Feedback": "", + "Feedback": "Feedback", "Feedback Activity": "Atividade de feedback", "Feedback deleted successfully": "O feedback foi excluído com sucesso.", "Feedback Details": "Detalhes do comentário", @@ -845,7 +845,7 @@ "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Falsificação de impressão digital detectada: Não foi possível usar as iniciais como avatar. Usando a imagem de perfil padrão.", "Firecrawl API Base URL": "URL base da API do Firecrawl", "Firecrawl API Key": "Chave de API do Firecrawl", - "Firecrawl Timeout (s)": "", + "Firecrawl Timeout (s)": "Tempo limite do Firecrawl (s)", "Floating Quick Actions": "Ações rápidas flutuantes", "Focus Chat Input": "Foco na janela do Chat", "Folder": "Pasta", @@ -888,11 +888,11 @@ "Functions": "Funções", "Functions allow arbitrary code execution.": "Funções permitem a execução arbitrária de código.", "Functions imported successfully": "Funções importadas com sucesso", - "Gemini": "", + "Gemini": "Gemini", "Gemini API Key": "Chave da API Gemini", "Gemini API Key is required.": "A chave da API Gemini é necessária.", "Gemini Base URL": "URL base do Gemini", - "Gemini Endpoint Method": "", + "Gemini Endpoint Method": "Método de Endpoint do Gemini", "Gender": "Gênero", "General": "Geral", "Generate": "Gerar", @@ -909,13 +909,13 @@ "Get started with {{WEBUI_NAME}}": "Iniciar com {{WEBUI_NAME}}", "Global": "Global", "Good Response": "Boa Resposta", - "Google": "", - "Google Drive": "", + "Google": "Google", + "Google Drive": "Google Drive", "Google PSE API Key": "Chave API do Google PSE", "Google PSE Engine Id": "ID do Motor do Google PSE", - "Gravatar": "", + "Gravatar": "Gravatar", "Grid": "Grade", - "Grokipedia": "", + "Grokipedia": "Grokipedia", "Group": "Grupo", "Group Channel": "Canal do grupo", "Group created successfully": "Grupo criado com sucesso", @@ -948,16 +948,16 @@ "Host": "Servidor", "How can I help you today?": "Como posso ajudar você hoje?", "How would you rate this response?": "Como você avalia essa resposta?", - "HTML": "", - "http://localhost:8000": "", - "https://mineru.net/api/v4": "", + "HTML": "HTML", + "http://localhost:8000": "http://localhost:8000", + "https://mineru.net/api/v4": "https://mineru.net/api/v4", "Hybrid Search": "Pesquisa Híbrida", "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Eu reconheço que li e entendi as implicações da minha ação. Estou ciente dos riscos associados à execução de código arbitrário e verifiquei a confiabilidade da fonte.", - "ID": "", + "ID": "ID", "ID cannot contain \":\" or \"|\" characters": "O ID não pode conter caracteres \":\" ou \"|\"", "ID copied to clipboard": "ID copiado para a área de transferência", - "iframe Sandbox Allow Forms": "", - "iframe Sandbox Allow Same Origin": "", + "iframe Sandbox Allow Forms": "Permitir formulários no sandbox do iframe", + "iframe Sandbox Allow Same Origin": "Permitir mesma origem no sandbox do iframe", "Ignite curiosity": "Desperte a curiosidade", "Image": "Imagem", "Image Compression": "Compressão de imagem", @@ -976,7 +976,7 @@ "Images": "Imagens", "Import": "Importar", "Import Chats": "Importar Chats", - "Import Config": "", + "Import Config": "Importar Configuração", "Import From Link": "Importar do link", "Import Models": "Importar Modelos", "Import Presets": "Importar Presets", @@ -1020,7 +1020,7 @@ "is typing...": "está digitando...", "Italic": "Itálico", "January": "Janeiro", - "Jina API Base URL": "", + "Jina API Base URL": "URL base da API Jina", "Jina API Key": "Chave de API Jina", "join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.", "JSON": "JSON", @@ -1028,11 +1028,11 @@ "JSON Spec": "Especificação JSON", "July": "Julho", "June": "Junho", - "Jupyter Auth": "", - "Jupyter URL": "", + "Jupyter Auth": "Autenticação Jupyter", + "Jupyter URL": "URL do Jupyter", "JWT Expiration": "Expiração do JWT", "JWT Token": "Token JWT", - "Kagi Search API Key": "", + "Kagi Search API Key": "Chave da API de pesquisa Kagi", "Keep Follow-Up Prompts in Chat": "Mantenha prompts de acompanhamento no chat", "Keep in Sidebar": "Manter na barra lateral", "Key": "Chave", @@ -1051,8 +1051,8 @@ "Knowledge reset successfully.": "Conhecimento resetado com sucesso.", "Knowledge Sharing": "Compartilhamento da Base de Conhecimento", "Knowledge updated successfully": "Conhecimento atualizado com sucesso", - "Kokoro.js (Browser)": "", - "Kokoro.js Dtype": "", + "Kokoro.js (Browser)": "Kokoro.js (Navegador)", + "Kokoro.js Dtype": "Dtype do Kokoro.js", "Label": "Rótulo", "Landing Page Mode": "Modo Landing Page", "Language": "Idioma", @@ -1060,7 +1060,7 @@ "Last Active": "Última Atividade", "Last Modified": "Última Modificação", "Last reply": "Última resposta", - "LDAP": "", + "LDAP": "LDAP", "LDAP server updated": "Servidor LDAP atualizado", "Leaderboard": "Tabela de classificação", "Learn More": "Saiba Mais", @@ -1079,21 +1079,21 @@ "Leave empty to use the default prompt, or enter a custom prompt": "Deixe vazio para usar o prompt padrão, ou insira um prompt personalizado", "Leave model field empty to use the default model.": "Deixe o campo do modelo vazio para usar o modelo padrão.", "Legacy": "Legado", - "lexical": "", + "lexical": "lexical", "License": "Licença", - "Lift List": "", + "Lift List": "Lista de elevação", "Light": "Claro", "Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).": "Limitar consultas de pesquisa simultâneas. 0 = ilimitado (padrão). Defina como 1 para execução sequencial (recomendado para APIs com limites de taxa rígidos, como o nível gratuito do Brave).", "List": "Lista", "Listening...": "Escutando...", "Live": "Ao vivo", - "Llama.cpp": "", + "Llama.cpp": "Llama.cpp", "LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", "Loader": "Carregador", "Loading Kokoro.js...": "Carregando Kokoro.js...", "Loading...": "Carregando...", - "local": "", - "Local": "", + "local": "local", + "Local": "Local", "Local Task Model": "Modelo de Tarefa Local", "Location access not allowed": "Acesso ao local não permitido", "Lost": "Perdeu", @@ -1114,17 +1114,17 @@ "Manage Tool Servers": "Gerenciar servidores de ferramentas", "Manage your account information.": "Gerencie as informações da sua conta.", "March": "Março", - "Markdown": "", + "Markdown": "Markdown", "Markdown Header Text Splitter": "Separador de texto de cabeçalho Markdown", - "Max Speakers": "", + "Max Speakers": "Máximo de locutores", "Max Upload Count": "Quantidade máxima de anexos", "Max Upload Size": "Tamanho máximo do arquivo", "Maximum number of files allowed per folder.": "Número máximo de arquivos permitidos por pasta.", "Maximum number of files per folder is {{max}}.": "O número máximo de arquivos por pasta é {{max}}.", "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Máximo de 3 modelos podem ser baixados simultaneamente. Por favor, tente novamente mais tarde.", "May": "Maio", - "MBR": "", - "MCP": "", + "MBR": "MBR", + "MCP": "MCP", "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "O suporte ao MCP é experimental e suas especificações mudam com frequência, o que pode levar a incompatibilidades. O suporte à especificação OpenAPI é mantido diretamente pela equipe do Open WebUI, tornando-o a opção mais confiável para compatibilidade.", "Medium": "Médio", "Member removed successfully": "Membro removido com sucesso", @@ -1147,14 +1147,14 @@ "messages": "mensagens", "Messages": "Mensagens", "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens enviadas após criar seu link não serão compartilhadas. Usuários com o URL poderão visualizar o chat compartilhado.", - "Microsoft OneDrive": "", - "Microsoft OneDrive (personal)": "", - "Microsoft OneDrive (work/school)": "", - "MinerU": "", + "Microsoft OneDrive": "Microsoft OneDrive", + "Microsoft OneDrive (personal)": "Microsoft OneDrive (pessoal)", + "Microsoft OneDrive (work/school)": "Microsoft OneDrive (trabalho/escola)", + "MinerU": "MinerU", "MinerU API Key required for Cloud API mode.": "Chave de API MinerU necessária para o modo Cloud API.", - "Mistral OCR": "", + "Mistral OCR": "Mistral OCR", "Mistral OCR API Key required.": "Chave de API do Mistral OCR necessária.", - "MistralAI": "", + "MistralAI": "MistralAI", "Model": "Modelo", "Model '{{modelName}}' has been successfully downloaded.": "Modelo '{{modelName}}' foi baixado com sucesso.", "Model '{{modelTag}}' is already in queue for downloading.": "Modelo '{{modelTag}}' já está na fila para download.", @@ -1191,10 +1191,10 @@ "Models": "Modelos", "Models Access": "Acesso aos Modelos", "Models configuration saved successfully": "Configuração de modelos salva com sucesso", - "Models imported successfully": "", + "Models imported successfully": "Modelos importados com sucesso", "Models Public Sharing": "Compartilhamento Público de Modelos", "Models Sharing": "Compartilhamento de Modelos", - "Mojeek": "", + "Mojeek": "Mojeek", "Mojeek Search API Key": "Chave de API Mojeek Search", "More": "Mais", "More Concise": "Mais conciso", @@ -1266,7 +1266,7 @@ "No valves": "Sem configurações", "No valves to update": "Nenhuma configuração para atualizar", "No webhooks yet": "Ainda não há webhooks", - "Node Ids": "", + "Node Ids": "IDs dos nós", "None": "Nenhum", "Not factually correct": "Não está factualmente correto", "Not helpful": "Não é útil", @@ -1281,8 +1281,8 @@ "Notification Webhook": "Webhook de notificação", "Notifications": "Notificações", "November": "Novembro", - "OAuth": "", - "OAuth 2.1": "", + "OAuth": "OAuth", + "OAuth 2.1": "OAuth 2.1", "OAuth ID": "OAuth ID", "October": "Outubro", "Off": "Desligado", @@ -1291,10 +1291,10 @@ "Ollama": "Ollama", "Ollama API": "API Ollama", "Ollama API settings updated": "Configurações da API Ollama atualizadas", - "Ollama Cloud API Key": "", + "Ollama Cloud API Key": "Chave da API Ollama Cloud", "Ollama Version": "Versão Ollama", "On": "Ligado", - "OneDrive": "", + "OneDrive": "OneDrive", "Only active when \"Paste Large Text as File\" setting is toggled on.": "Ativo somente quando a configuração \"Colar texto grande como arquivo\" estiver ativada.", "Only active when the chat input is in focus and an LLM is generating a response.": "Ativo somente quando o campo de entrada do chat está em foco e um LLM está gerando uma resposta.", "Only active when the chat input is in focus.": "Ativo somente quando o campo de entrada do chat estiver em foco.", @@ -1327,15 +1327,15 @@ "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "A versão do Open WebUI (v{{OPEN_WEBUI_VERSION}}) é inferior à versão necessária (v{{REQUIRED_VERSION}})", "OpenAI": "OpenAI", "OpenAI API": "API OpenAI", - "OpenAI API Base URL": "", - "OpenAI API Key": "", + "OpenAI API Base URL": "URL base da API OpenAI", + "OpenAI API Key": "Chave da API OpenAI", "OpenAI API Key is required.": "Chave API OpenAI é necessária.", "OpenAI API settings updated": "Configurações OpenAI atualizadas", - "OpenAI API Version": "", + "OpenAI API Version": "Versão da API OpenAI", "OpenAI URL/Key required.": "URL/Chave OpenAI necessária.", - "OpenAPI": "", - "OpenAPI Spec": "", - "openapi.json URL or Path": "", + "OpenAPI": "OpenAPI", + "OpenAPI Spec": "Especificação OpenAPI", + "openapi.json URL or Path": "URL ou caminho do openapi.json", "optional": "opcional", "Optional": "Opcional", "or": "ou", @@ -1370,14 +1370,14 @@ "Permissions": "Permissões", "Perplexity API Key": "Chave API da Perplexity", "Perplexity Model": "Modelo Perplexity", - "Perplexity Search API URL": "", + "Perplexity Search API URL": "URL da API de pesquisa Perplexity", "Perplexity Search Context Usage": "Uso do contexto de pesquisa do Perplexity", "Personalization": "Personalização", "Pin": "Fixar", "Pinned": "Fixado", "Pinned Messages": "Mensagens fixadas", "Pioneer insights": "Insights pioneiros", - "Pipe": "", + "Pipe": "Pipe", "Pipeline deleted successfully": "Pipeline excluído com sucesso", "Pipeline downloaded successfully": "Pipeline baixado com sucesso", "Pipelines": "Pipelines", @@ -1387,8 +1387,8 @@ "Plain text (.md)": "Texto simples (.md)", "Plain text (.txt)": "Texto simples (.txt)", "Playground": "Playground", - "Playwright Timeout (ms)": "", - "Playwright WebSocket URL": "", + "Playwright Timeout (ms)": "Tempo limite do Playwright (ms)", + "Playwright WebSocket URL": "URL do WebSocket do Playwright", "Please carefully review the following warnings:": "Por favor, revise cuidadosamente os seguintes avisos:", "Please do not close the settings page while loading the model.": "Não feche a página de configurações enquanto estiver carregando o modelo.", "Please enter a message or attach a file.": "Por favor, insira uma mensagem ou anexe um arquivo.", @@ -1421,7 +1421,7 @@ "Private conversation between selected users": "Conversa privada entre usuários selecionados", "Production version updated": "Versão de produção atualizada", "Profile": "Perfil", - "Prompt": "", + "Prompt": "Prompt", "Prompt Autocompletion": "Preenchimento automático de prompts", "Prompt Content": "Conteúdo do Prompt", "Prompt created successfully": "Prompt criado com sucesso", @@ -1504,9 +1504,9 @@ "Retrieval": "Recuperação", "Retrieval Query Generation": "Geração de Consulta de Recuperação", "Retrieved {{count}} sources": "{{count}} fontes recuperadas", - "Retrieved {{count}} sources_one": "", - "Retrieved {{count}} sources_many": "", - "Retrieved {{count}} sources_other": "", + "Retrieved {{count}} sources_one": "{{count}} fonte recuperada", + "Retrieved {{count}} sources_many": "{{count}} fontes recuperadas", + "Retrieved {{count}} sources_other": "{{count}} fontes recuperadas", "Retrieved 1 source": "1 fonte recuperada", "Rich Text Input for Chat": "Entrada de rich text para o chat", "Role": "Função", @@ -1603,8 +1603,8 @@ "Send now": "Enviar agora", "Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia `stream_options: { include_usage: true }` na solicitação. Provedores compatíveis retornarão informações sobre o uso de tokens na resposta quando configurado.", "September": "Setembro", - "SerpApi API Key": "", - "SerpApi Engine": "", + "SerpApi API Key": "Chave da API SerpApi", + "SerpApi Engine": "Motor SerpApi", "Serper API Key": "Chave da API Serper", "Serply API Key": "Chave da API Serply", "Serpstack API Key": "Chave da API Serpstack", @@ -1659,8 +1659,8 @@ "Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.": "Melhora significativamente a precisão usando um LLM para aprimorar tabelas, formulários, cálculos em linha e detecção de layout. Aumenta a latência. O padrão é Falso.", "Signing in to {{WEBUI_NAME}}": "Fazendo login em {{WEBUI_NAME}}", "Single": "Único", - "Sink List": "", - "sk-1234": "", + "Sink List": "Lista de destino", + "sk-1234": "sk-1234", "Skill created successfully": "Skill criada com sucesso", "Skill deleted successfully": "Skill excluída com sucesso", "Skill Description": "Descrição da skill", @@ -1674,14 +1674,14 @@ "Skip Cache": "Pular cache", "Skip the cache and re-run the inference. Defaults to False.": "Ignore o cache e execute a inferência novamente. O padrão é Falso.", "Something went wrong :/": "Algo deu errado :/", - "Sonar": "", - "Sonar Deep Research": "", - "Sonar Pro": "", - "Sonar Reasoning": "", - "Sonar Reasoning Pro": "", + "Sonar": "Sonar", + "Sonar Deep Research": "Sonar Deep Research", + "Sonar Pro": "Sonar Pro", + "Sonar Reasoning": "Sonar Reasoning", + "Sonar Reasoning Pro": "Sonar Reasoning Pro", "Sort": "Organizar", - "Sougou Search API sID": "", - "Sougou Search API SK": "", + "Sougou Search API sID": "sID da API de pesquisa Sougou", + "Sougou Search API SK": "SK da API de pesquisa Sougou", "Source": "Fonte", "Speech Playback Speed": "Velocidade de reprodução de fala", "Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}", @@ -1702,8 +1702,8 @@ "Stop Generating": "Parar de Gerar", "Stop Sequence": "Sequência de Parada", "Stream Chat Response": "Stream Resposta do Chat", - "Stream Delta Chunk Size": "", - "Streamable HTTP": "", + "Stream Delta Chunk Size": "Tamanho do bloco delta do stream", + "Streamable HTTP": "HTTP com streaming", "Strikethrough": "Tachado", "Strip Existing OCR": "Remover OCR existente", "Strip existing OCR text from the PDF and re-run OCR. Ignored if Force OCR is enabled. Defaults to False.": "Remove o texto OCR existente do PDF e executa o OCR novamente. Ignorado se a opção Forçar OCR estiver habilitada. O padrão é Falso.", @@ -1730,8 +1730,8 @@ "System": "Sistema", "System Instructions": "Instruções do sistema", "System Prompt": "Prompt do Sistema", - "Tag": "", - "Tags": "", + "Tag": "Tag", + "Tags": "Tags", "Tags Generation": "Geração de tags", "Tags Generation Prompt": "Prompt para geração de Tags", "Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.": "A amostragem livre de cauda é usada para reduzir o impacto de tokens menos prováveis na saída. Um valor mais alto (por exemplo, 2,0) reduzirá ainda mais o impacto, enquanto um valor de 1,0 desabilita essa configuração.", @@ -1741,7 +1741,7 @@ "Task Model": "Modelo de Tarefa", "Tasks": "Tarefas", "Tavily API Key": "Chave da API Tavily", - "Tavily Extract Depth": "", + "Tavily Extract Depth": "Profundidade de extração do Tavily", "Tell us more:": "Conte-nos mais:", "Temperature": "Temperatura", "Temporary Chat": "Chat temporário", @@ -1801,15 +1801,15 @@ "Thumbs up/down ratings from users on model responses": "Avaliações positivas/negativas dos usuários sobre as respostas do modelo", "Tika": "Tika", "Tika Server URL required.": "URL do servidor Tika necessária.", - "Tiktoken": "", + "Tiktoken": "Tiktoken", "Time & Calculation": "Tempo e Cálculo", - "Timeout": "", + "Timeout": "Tempo limite", "Title": "Título", "Title Auto-Generation": "Geração Automática de Título", "Title cannot be an empty string.": "O Título não pode ser uma string vazia.", "Title Generation": "Geração de Títulos", "Title Generation Prompt": "Prompt de Geração de Título", - "TLS": "", + "TLS": "TLS", "To access the available model names for downloading,": "Para acessar os nomes de modelos disponíveis para download,", "To access the GGUF models available for downloading,": "Para acessar os modelos GGUF disponíveis para download,", "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acessar a WebUI, entre em contato com o administrador. Os administradores podem gerenciar os status dos usuários no Painel de Administração.", @@ -1822,10 +1822,10 @@ "Today at {{LOCALIZED_TIME}}": "Hoje às {{LOCALIZED_TIME}}", "Toggle Sidebar": "Alternar barra lateral", "Toggle whether current connection is active.": "Alterna se a conexão atual está ativa.", - "Token": "", + "Token": "Token", "Token counts are estimates and may not reflect actual API usage": "A contagem de tokens é uma estimativa e pode não refletir o uso real da API.", - "tokens": "", - "Tokens": "", + "tokens": "tokens", + "Tokens": "Tokens", "Too verbose": "Muito detalhado", "Tool created successfully": "Ferramenta criada com sucesso", "Tool deleted successfully": "Ferramenta excluída com sucesso", @@ -1842,10 +1842,10 @@ "Tools have a function calling system that allows arbitrary code execution.": "Ferramentas possuem um sistema de chamada de funções que permite a execução de código arbitrário.", "Tools Public Sharing": "Compartilhamento Público de Ferramentas", "Tools Sharing": "Compartilhamento de Ferramentas", - "Top": "", + "Top": "Top", "Top K": "Top K", - "Top K Reranker": "", - "Transformers": "", + "Top K Reranker": "Top K Reclassificador", + "Transformers": "Transformers", "Trouble accessing Ollama?": "Problemas para acessar o Ollama?", "Trust Proxy Environment": "Ambiente de Proxy Confiável", "Try adjusting your search or filter to find what you are looking for.": "Tente ajustar sua pesquisa ou filtro para encontrar o que você está procurando.", @@ -1894,14 +1894,14 @@ "Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)": "Progresso do upload: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)", "Uploaded files or images": "Arquivos ou imagens carregados", "Uploading file...": "Enviando arquivo...", - "URL": "", + "URL": "URL", "URL is required": "URL é obrigatória", "URL Mode": "Modo URL", "Usage": "Uso", "Use": "Usar", "Use '#' in the prompt input to load and include your knowledge.": "Usar '#' no prompt para carregar e incluir seus conhecimentos.", "Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Use o endpoint /v1/chat/completions em vez de /v1/audio/transcriptions para obter uma precisão potencialmente melhor.", - "Use Chat Completions API": "", + "Use Chat Completions API": "Usar API de Chat Completions", "Use groups to group your users and assign permissions.": "Use grupos para agrupar seus usuários e atribuir permissões.", "Use LLM": "Usar LLM", "Use no proxy to fetch page contents.": "Não utilize proxy para buscar o conteúdo da página.", @@ -1958,10 +1958,10 @@ "Web Search Query Generation": "Geração de consulta de pesquisa na Web", "Webhook Name": "Nome do Webhook", "Webhook URL": "URL do Webhook", - "Webhooks": "", + "Webhooks": "Webhooks", "Webpage URLs": "URLs de páginas da web", "WebUI Settings": "Configurações da WebUI", - "WebUI URL": "", + "WebUI URL": "URL da WebUI", "WebUI will make requests to \"{{url}}\"": "A WebUI fará requisições para \"{{url}}\"", "WebUI will make requests to \"{{url}}/api/chat\"": "A WebUI fará requisições para \"{{url}}/api/chat\".", "WebUI will make requests to \"{{url}}/chat/completions\"": "A WebUI fará requisições para \"{{url}}/chat/completions\".", @@ -1979,7 +1979,7 @@ "Why?": "Por que?", "Widescreen Mode": "Modo Widescreen", "Width": "Largura", - "Wikipedia": "", + "Wikipedia": "Wikipédia", "Won": "Ganhou", "Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text.": "Funciona em conjunto com o top-k. Um valor mais alto (por exemplo, 0,95) resultará em um texto mais diverso, enquanto um valor mais baixo (por exemplo, 0,5) gerará um texto mais focado e conservador.", "Workspace": "Espaço de Trabalho", @@ -1988,14 +1988,14 @@ "Write a summary in 50 words that summarizes {{topic}}.": "Escreva um resumo em 50 palavras que resuma {{topic}}.", "Write something...": "Escreva algo...", "Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.": "Escreva aqui o conteúdo do system prompt do seu modelo\nex.: Você é o Mario do Super Mario Bros e atua como assistente.", - "Yacy Instance URL": "", - "Yacy Password": "", - "Yacy Username": "", - "Yahoo": "", - "Yandex": "", - "Yandex Web Search API Key": "", - "Yandex Web Search config": "", - "Yandex Web Search URL": "", + "Yacy Instance URL": "URL da instância Yacy", + "Yacy Password": "Senha do Yacy", + "Yacy Username": "Nome de usuário do Yacy", + "Yahoo": "Yahoo", + "Yandex": "Yandex", + "Yandex Web Search API Key": "Chave da API de pesquisa Yandex", + "Yandex Web Search config": "Configuração de pesquisa Yandex", + "Yandex Web Search URL": "URL de pesquisa Yandex", "Yesterday": "Ontem", "Yesterday at {{LOCALIZED_TIME}}": "Ontem às {{LOCALIZED_TIME}}", "You": "Você", @@ -2022,7 +2022,7 @@ "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Toda a sua contribuição irá diretamente para o desenvolvedor do plugin; o Open WebUI não retém nenhuma porcentagem. No entanto, a plataforma de financiamento escolhida pode ter suas próprias taxas.", "Your message text or inputs": "Seu texto de mensagem ou entradas", "Your usage stats have been successfully synced.": "Suas estatísticas de uso foram sincronizadas com sucesso.", - "YouTube": "", - "Youtube Language": "", - "Youtube Proxy URL": "" + "YouTube": "YouTube", + "Youtube Language": "Idioma do YouTube", + "Youtube Proxy URL": "URL de proxy do YouTube" } From 12bea8cd881219ca2a4116cecd8c8af475a1a29d Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Fri, 20 Feb 2026 06:06:23 +0800 Subject: [PATCH 22/70] i18n: improve Chinese translation (#21530) --- src/lib/i18n/locales/zh-CN/translation.json | 6 +++--- src/lib/i18n/locales/zh-TW/translation.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index b155142d87..40c26e84d2 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -345,7 +345,7 @@ "Completions": "续写", "Compress Images in Channels": "压缩频道中的图片", "Concurrent Requests": "并发请求", - "Config": "", + "Config": "配置", "Config imported successfully": "配置导入成功", "Configure": "配置", "Confirm": "确认", @@ -769,7 +769,7 @@ "Export All Chats (All Users)": "导出所有用户对话", "Export chat (.json)": "JSON 文件 (.json)", "Export Chats": "导出对话", - "Export Config": "", + "Export Config": "导出配置文件", "Export Models": "导出模型配置", "Export Presets": "导出预设", "Export Prompts": "导出提示词", @@ -976,7 +976,7 @@ "Images": "图片", "Import": "导入", "Import Chats": "导入对话记录", - "Import Config": "", + "Import Config": "导入配置文件", "Import From Link": "从链接导入", "Import Models": "导入模型配置", "Import Presets": "导入预设", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index b4f702c05e..467ce55e3b 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -345,7 +345,7 @@ "Completions": "自動完成", "Compress Images in Channels": "壓縮頻道中的圖片", "Concurrent Requests": "平行請求", - "Config": "", + "Config": "設定", "Config imported successfully": "成功匯入設定", "Configure": "設定", "Confirm": "確認", @@ -769,7 +769,7 @@ "Export All Chats (All Users)": "匯出所有對話紀錄(所有使用者)", "Export chat (.json)": "匯出對話紀錄(.json)", "Export Chats": "匯出對話紀錄", - "Export Config": "", + "Export Config": "匯出設定檔", "Export Models": "匯出模型", "Export Presets": "匯出預設集", "Export Prompts": "匯出提示詞", @@ -976,7 +976,7 @@ "Images": "圖片", "Import": "匯入", "Import Chats": "匯入對話紀錄", - "Import Config": "", + "Import Config": "匯入設定檔", "Import From Link": "從連結匯入", "Import Models": "匯入模型", "Import Presets": "匯入預設集", From 6ac593209c179bbe524e4d0b56886fcd833e9b26 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:09:54 -0600 Subject: [PATCH 23/70] refac --- src/lib/components/workspace/Skills.svelte | 79 ++++++++++++++++------ 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/src/lib/components/workspace/Skills.svelte b/src/lib/components/workspace/Skills.svelte index 2563ad33ac..63956f3cf2 100644 --- a/src/lib/components/workspace/Skills.svelte +++ b/src/lib/components/workspace/Skills.svelte @@ -13,6 +13,7 @@ getSkillById, getSkillItems, exportSkills, + createNewSkill, deleteSkillById, toggleSkillById } from '$lib/apis/skills'; @@ -198,30 +199,66 @@ bind:this={importInputElement} bind:files={importFiles} type="file" - accept=".md" + accept=".md,.json" hidden on:change={() => { if (importFiles && importFiles.length > 0) { - const reader = new FileReader(); - reader.onload = (event) => { - const mdContent = event.target?.result; - if (typeof mdContent === 'string') { - const fm = parseFrontmatter(mdContent); - const fileName = importFiles[0].name.replace(/\.md$/, ''); - const rawName = fm.name || fileName; - const displayName = formatSkillName(rawName); - sessionStorage.skill = JSON.stringify({ - name: displayName, - id: fm.name || '', // Use raw frontmatter name as ID if available - description: fm.description || '', - content: mdContent, - is_active: true, - access_grants: [] - }); - goto('/workspace/skills/create'); - } - }; - reader.readAsText(importFiles[0]); + const file = importFiles[0]; + const ext = file.name.split('.').pop()?.toLowerCase(); + + if (ext === 'json') { + // JSON import: create skills via API + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const content = event.target?.result; + if (typeof content !== 'string') return; + + const parsedSkills = JSON.parse(content); + const items = Array.isArray(parsedSkills) ? parsedSkills : [parsedSkills]; + + for (const skill of items) { + await createNewSkill(localStorage.token, skill).catch( + (error) => { + toast.error(`${error}`); + } + ); + } + + toast.success($i18n.t('Skill imported successfully')); + page = 1; + loadSkillItems(); + _skills.set(await getSkills(localStorage.token)); + } catch (e) { + toast.error($i18n.t('Invalid JSON file')); + } + }; + reader.readAsText(file); + } else { + // Markdown import: parse frontmatter and open in editor + const reader = new FileReader(); + reader.onload = (event) => { + const mdContent = event.target?.result; + if (typeof mdContent === 'string') { + const fm = parseFrontmatter(mdContent); + const fileName = file.name.replace(/\.md$/, ''); + const rawName = fm.name || fileName; + const displayName = formatSkillName(rawName); + sessionStorage.skill = JSON.stringify({ + name: displayName, + id: fm.name || '', + description: fm.description || '', + content: mdContent, + is_active: true, + access_grants: [] + }); + goto('/workspace/skills/create'); + } + }; + reader.readAsText(file); + } + + importInputElement.value = ''; } }} /> From 91a0301c9e22e93295a7c471d83592a802560795 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:29:19 -0600 Subject: [PATCH 24/70] refac --- backend/open_webui/main.py | 73 +++++ backend/open_webui/utils/anthropic.py | 370 ++++++++++++++++++++++++++ backend/open_webui/utils/auth.py | 4 + 3 files changed, 447 insertions(+) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 6d877e0ca1..3f1536244d 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1442,6 +1442,16 @@ async def check_url(request: Request, call_next): scheme="Bearer", credentials=request.cookies.get("token") ) + # Fallback to x-api-key header for Anthropic Messages API routes + if request.state.token is None and request.headers.get("x-api-key"): + request_path = request.url.path + if request_path in ("/api/message", "/api/v1/messages"): + from fastapi.security import HTTPAuthorizationCredentials + + request.state.token = HTTPAuthorizationCredentials( + scheme="Bearer", credentials=request.headers.get("x-api-key") + ) + request.state.enable_api_keys = app.state.config.ENABLE_API_KEYS response = await call_next(request) process_time = int(time.time()) - start_time @@ -1893,6 +1903,69 @@ generate_chat_completions = chat_completion generate_chat_completion = chat_completion +################################## +# +# Anthropic Messages API Compatible Endpoint +# +################################## + + +from open_webui.utils.anthropic import ( + convert_anthropic_to_openai_payload, + convert_openai_to_anthropic_response, + openai_stream_to_anthropic_stream, +) + + +@app.post("/api/message") +@app.post("/api/v1/messages") # Anthropic Messages API compatible endpoint +async def generate_messages( + request: Request, + form_data: dict, + user=Depends(get_verified_user), +): + """ + Anthropic Messages API compatible endpoint. + + Accepts the Anthropic Messages API format, converts internally to OpenAI + Chat Completions format, routes through the existing chat completion + pipeline, then converts the response back to Anthropic Messages format. + + Supports both streaming and non-streaming requests. + All models configured in Open WebUI are accessible via this endpoint. + + Authentication: Supports both standard Authorization header and + Anthropic's x-api-key header (via middleware translation). + """ + # Convert Anthropic payload to OpenAI format + requested_model = form_data.get("model", "") + + openai_payload = convert_anthropic_to_openai_payload(form_data) + + # Route through the existing chat_completion handler + response = await chat_completion(request, openai_payload, user) + + # Convert response back to Anthropic format + if isinstance(response, StreamingResponse): + # Streaming response: wrap the generator to convert SSE format + return StreamingResponse( + openai_stream_to_anthropic_stream( + response.body_iterator, model=requested_model + ), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + ) + elif isinstance(response, dict): + return convert_openai_to_anthropic_response(response, model=requested_model) + else: + # Passthrough for error responses (JSONResponse, PlainTextResponse, etc.) + return response + + + @app.post("/api/chat/completed") async def chat_completed( request: Request, form_data: dict, user=Depends(get_verified_user) diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py index 59236c32df..ba24984400 100644 --- a/backend/open_webui/utils/anthropic.py +++ b/backend/open_webui/utils/anthropic.py @@ -1,3 +1,4 @@ +import json import logging import aiohttp @@ -78,3 +79,372 @@ async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> di return None return {"object": "list", "data": all_models} + + +############################## +# +# Anthropic Messages API Conversion Utilities +# +############################## + + +def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict: + """ + Convert an Anthropic Messages API request to OpenAI Chat Completions format. + + Anthropic format: + {model, messages: [{role, content}], system, max_tokens, ...} + OpenAI format: + {model, messages: [{role, content}], max_tokens, ...} + """ + openai_payload = {} + + # Model + openai_payload["model"] = anthropic_payload.get("model", "") + + # Build messages list + messages = [] + + # System prompt (Anthropic has it as top-level, OpenAI as a system message) + system = anthropic_payload.get("system") + if system: + if isinstance(system, str): + messages.append({"role": "system", "content": system}) + elif isinstance(system, list): + # Anthropic supports system as list of content blocks + text_parts = [] + for block in system: + if isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + elif isinstance(block, str): + text_parts.append(block) + messages.append({"role": "system", "content": "\n".join(text_parts)}) + + # Convert messages + for msg in anthropic_payload.get("messages", []): + role = msg.get("role", "user") + content = msg.get("content") + + if isinstance(content, str): + messages.append({"role": role, "content": content}) + elif isinstance(content, list): + # Convert Anthropic content blocks to OpenAI format + openai_content = [] + tool_calls = [] + + for block in content: + block_type = block.get("type", "text") + + if block_type == "text": + openai_content.append({ + "type": "text", + "text": block.get("text", ""), + }) + elif block_type == "image": + source = block.get("source", {}) + if source.get("type") == "base64": + media_type = source.get("media_type", "image/png") + data = source.get("data", "") + openai_content.append({ + "type": "image_url", + "image_url": { + "url": f"data:{media_type};base64,{data}", + }, + }) + elif source.get("type") == "url": + openai_content.append({ + "type": "image_url", + "image_url": {"url": source.get("url", "")}, + }) + elif block_type == "tool_use": + tool_calls.append({ + "id": block.get("id", ""), + "type": "function", + "function": { + "name": block.get("name", ""), + "arguments": json.dumps(block.get("input", {})) + if isinstance(block.get("input"), dict) + else str(block.get("input", "{}")), + }, + }) + elif block_type == "tool_result": + # Tool results become separate tool messages in OpenAI format + tool_content = block.get("content", "") + if isinstance(tool_content, list): + tool_text_parts = [] + for tc in tool_content: + if isinstance(tc, dict) and tc.get("type") == "text": + tool_text_parts.append(tc.get("text", "")) + tool_content = "\n".join(tool_text_parts) + + messages.append({ + "role": "tool", + "tool_call_id": block.get("tool_use_id", ""), + "content": tool_content, + }) + + # Build the message + if tool_calls: + # Assistant message with tool calls + msg_dict = {"role": role} + if openai_content: + # If there's only text, flatten it + if len(openai_content) == 1 and openai_content[0]["type"] == "text": + msg_dict["content"] = openai_content[0]["text"] + else: + msg_dict["content"] = openai_content + else: + msg_dict["content"] = "" + msg_dict["tool_calls"] = tool_calls + messages.append(msg_dict) + elif openai_content: + # If there's only a single text block, flatten it to a string + if len(openai_content) == 1 and openai_content[0]["type"] == "text": + messages.append({"role": role, "content": openai_content[0]["text"]}) + else: + messages.append({"role": role, "content": openai_content}) + else: + messages.append({"role": role, "content": str(content) if content else ""}) + + openai_payload["messages"] = messages + + # max_tokens + if "max_tokens" in anthropic_payload: + openai_payload["max_tokens"] = anthropic_payload["max_tokens"] + + # Common parameters + for param in ("temperature", "top_p", "stop_sequences", "stream"): + if param in anthropic_payload: + if param == "stop_sequences": + openai_payload["stop"] = anthropic_payload[param] + else: + openai_payload[param] = anthropic_payload[param] + + # Tools conversion: Anthropic → OpenAI + if "tools" in anthropic_payload: + openai_tools = [] + for tool in anthropic_payload["tools"]: + openai_tools.append({ + "type": "function", + "function": { + "name": tool.get("name", ""), + "description": tool.get("description", ""), + "parameters": tool.get("input_schema", {}), + }, + }) + openai_payload["tools"] = openai_tools + + # tool_choice + if "tool_choice" in anthropic_payload: + tc = anthropic_payload["tool_choice"] + if isinstance(tc, dict): + tc_type = tc.get("type", "auto") + if tc_type == "auto": + openai_payload["tool_choice"] = "auto" + elif tc_type == "any": + openai_payload["tool_choice"] = "required" + elif tc_type == "tool": + openai_payload["tool_choice"] = { + "type": "function", + "function": {"name": tc.get("name", "")}, + } + + return openai_payload + + +def convert_openai_to_anthropic_response( + openai_response: dict, model: str = "" +) -> dict: + """ + Convert a non-streaming OpenAI Chat Completions response to Anthropic Messages format. + """ + import uuid as _uuid + + choice = {} + if openai_response.get("choices"): + choice = openai_response["choices"][0] + + message = choice.get("message", {}) + finish_reason = choice.get("finish_reason", "stop") + + # Map finish_reason to stop_reason + stop_reason_map = { + "stop": "end_turn", + "length": "max_tokens", + "tool_calls": "tool_use", + "content_filter": "end_turn", + } + stop_reason = stop_reason_map.get(finish_reason, "end_turn") + + # Build content blocks + content = [] + msg_content = message.get("content") + if msg_content: + content.append({"type": "text", "text": msg_content}) + + # Tool calls → tool_use blocks + tool_calls = message.get("tool_calls", []) + for tc in tool_calls: + func = tc.get("function", {}) + try: + tool_input = json.loads(func.get("arguments", "{}")) + except (json.JSONDecodeError, TypeError): + tool_input = {} + content.append({ + "type": "tool_use", + "id": tc.get("id", f"toolu_{_uuid.uuid4().hex[:24]}"), + "name": func.get("name", ""), + "input": tool_input, + }) + + # Usage + openai_usage = openai_response.get("usage", {}) + usage = { + "input_tokens": openai_usage.get("prompt_tokens", 0), + "output_tokens": openai_usage.get("completion_tokens", 0), + } + + return { + "id": openai_response.get("id", f"msg_{_uuid.uuid4().hex[:24]}"), + "type": "message", + "role": "assistant", + "content": content, + "model": model or openai_response.get("model", ""), + "stop_reason": stop_reason, + "stop_sequence": None, + "usage": usage, + } + + +async def openai_stream_to_anthropic_stream( + openai_stream_generator, model: str = "" +): + """ + Convert an OpenAI SSE streaming response to Anthropic Messages SSE format. + + OpenAI sends: data: {"choices": [{"delta": {"content": "..."}}]} + Anthropic sends: event: content_block_delta\\ndata: {"type": "content_block_delta", ...} + """ + import uuid as _uuid + + msg_id = f"msg_{_uuid.uuid4().hex[:24]}" + input_tokens = 0 + output_tokens = 0 + block_started = False + stop_reason = "end_turn" + + # Emit message_start + message_start = { + "type": "message_start", + "message": { + "id": msg_id, + "type": "message", + "role": "assistant", + "content": [], + "model": model, + "stop_reason": None, + "stop_sequence": None, + "usage": {"input_tokens": 0, "output_tokens": 0}, + }, + } + yield f"event: message_start\ndata: {json.dumps(message_start)}\n\n".encode() + + try: + async for chunk in openai_stream_generator: + if isinstance(chunk, bytes): + chunk = chunk.decode("utf-8", errors="ignore") + + for line in chunk.strip().split("\n"): + line = line.strip() + + if not line or not line.startswith("data:"): + continue + + data_str = line[5:].strip() + if data_str == "[DONE]": + continue + if data_str == "{}": + continue + + try: + data = json.loads(data_str) + except (json.JSONDecodeError, TypeError): + continue + + choices = data.get("choices", []) + if not choices: + # Check for usage in the final chunk + if data.get("usage"): + input_tokens = data["usage"].get("prompt_tokens", input_tokens) + output_tokens = data["usage"].get( + "completion_tokens", output_tokens + ) + continue + + delta = choices[0].get("delta", {}) + finish_reason = choices[0].get("finish_reason") + + # Update usage if present + if data.get("usage"): + input_tokens = data["usage"].get("prompt_tokens", input_tokens) + output_tokens = data["usage"].get( + "completion_tokens", output_tokens + ) + + content = delta.get("content") + if content is not None: + if not block_started: + # Start the content block + block_start = { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + } + yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n".encode() + block_started = True + + # Send content delta + block_delta = { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": content}, + } + yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n".encode() + + # Handle tool calls in streaming + tool_calls = delta.get("tool_calls") + if tool_calls: + # Tool calls in streaming are more complex; + # for now we pass through the text content + pass + + if finish_reason is not None: + stop_reason_map = { + "stop": "end_turn", + "length": "max_tokens", + "tool_calls": "tool_use", + } + stop_reason = stop_reason_map.get(finish_reason, "end_turn") + + except Exception as e: + log.error(f"Error in Anthropic stream conversion: {e}") + + # Close content block if one was started + if block_started: + block_stop = {"type": "content_block_stop", "index": 0} + yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode() + + # Emit message_delta with stop reason + message_delta = { + "type": "message_delta", + "delta": { + "stop_reason": stop_reason, + "stop_sequence": None, + }, + "usage": {"output_tokens": output_tokens}, + } + yield f"event: message_delta\ndata: {json.dumps(message_delta)}\n\n".encode() + + # Emit message_stop + yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n".encode() + diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 27af3631e4..a12c6db881 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -290,6 +290,10 @@ async def get_current_user( if token is None and "token" in request.cookies: token = request.cookies.get("token") + # Fallback to request.state.token (set by middleware, e.g. for x-api-key) + if token is None and hasattr(request.state, "token") and request.state.token: + token = request.state.token.credentials + if token is None: raise HTTPException(status_code=401, detail="Not authenticated") From 0a700aafe46dfea2cf9721bb81725d2582b0d781 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:32:41 -0600 Subject: [PATCH 25/70] refac --- backend/open_webui/socket/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 78df66b8dc..c1d7e5c29e 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -511,6 +511,8 @@ async def channel_events(sid, data): async def ydoc_document_join(sid, data): """Handle user joining a document""" user = SESSION_POOL.get(sid) + if not user: + return try: document_id = data["document_id"] @@ -683,11 +685,13 @@ async def yjs_document_update(sid, data): skip_sid=sid, ) + user = SESSION_POOL.get(sid) + if not user: + return + async def debounced_save(): await asyncio.sleep(0.5) - await document_save_handler( - document_id, data.get("data", {}), SESSION_POOL.get(sid) - ) + await document_save_handler(document_id, data.get("data", {}), user) if data.get("data"): await create_task(REDIS, debounced_save(), document_id) From 43eb2351d2f4ccd2fff59b1f7b2af3fa2d05c3f4 Mon Sep 17 00:00:00 2001 From: Kylapaallikko Date: Fri, 20 Feb 2026 00:36:32 +0200 Subject: [PATCH 26/70] Update fi-FI translation.json (#21538) Added missing translations. --- src/lib/i18n/locales/fi-FI/translation.json | 250 ++++++++++---------- 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/src/lib/i18n/locales/fi-FI/translation.json b/src/lib/i18n/locales/fi-FI/translation.json index c0396719a2..2e676da539 100644 --- a/src/lib/i18n/locales/fi-FI/translation.json +++ b/src/lib/i18n/locales/fi-FI/translation.json @@ -35,7 +35,7 @@ "Accept Autocomplete Generation\nJump to Prompt Variable": "Hyväksy automaattiset täydennykset\n Siirry kehoitteen muuttujaan", "Access": "Pääsy", "Access Control": "Käyttöoikeuksien hallinta", - "Access List": "", + "Access List": "Pääsylista", "Accessible to all users": "Käytettävissä kaikille käyttäjille", "Account": "Tili", "Account Activation Pending": "Tilin aktivointi odottaa", @@ -53,8 +53,8 @@ "Add a model ID": "Lisää mallitunnus", "Add a short description about what this model does": "Lisää lyhyt kuvaus siitä, mitä tämä malli tekee", "Add a tag": "Lisää tagi", - "Add a tag...": "", - "Add Access": "", + "Add a tag...": "Lisää tagi...", + "Add Access": "Lisää pääsy", "Add Arena Model": "Lisää Areena-malli", "Add Connection": "Lisää yhteys", "Add Content": "Lisää sisältöä", @@ -63,13 +63,13 @@ "Add Custom Prompt": "Lisää mukautettu kehoite", "Add Details": "Lisää yksityiskohtia", "Add Files": "Lisää tiedostoja", - "Add Image": "", + "Add Image": "Lisää kuva", "Add Member": "Lisää jäsen", "Add Members": "Lisää jäseniä", "Add Memory": "Lisää muistiin", "Add Model": "Lisää malli", "Add Reaction": "Lisää reaktio", - "Add tag": "", + "Add tag": "Lisää tagi", "Add Tag": "Lisää tagi", "Add text content": "Lisää tekstisisältöä", "Add User": "Lisää käyttäjä", @@ -94,7 +94,7 @@ "All": "Kaikki", "All chats have been unarchived.": "Kaikki keskustelut poistettu arkistosta.", "All models deleted successfully": "Kaikki mallit poistettu onnistuneesti", - "All Users": "", + "All Users": "Kaikki käyttäjät", "Allow Call": "Salli puhelut", "Allow Chat Controls": "Salli keskustelujen hallinta", "Allow Chat Delete": "Salli keskustelujen poisto", @@ -144,7 +144,7 @@ "API Keys": "API-avaimet", "API Mode": "API-moodi", "API Timeout": "API aikakatkaisu", - "API Type": "", + "API Type": "API-tyyppi", "API Version": "API-versio", "API Version is required": "API-versio vaaditaan", "Application DN": "Sovelluksen DN", @@ -152,17 +152,17 @@ "applies to all users with the \"user\" role": "koskee kaikkia käyttäjiä, joilla on \"käyttäjä\"-rooli", "April": "huhtikuu", "Archive": "Arkistoi", - "Archive All": "", + "Archive All": "Arkistoi kaikki", "Archive All Chats": "Arkistoi kaikki keskustelut", "Archived Chats": "Arkistoidut keskustelut", "archived-chat-export": "arkistoitu-keskustelu-vienti", - "Are you sure you want to archive all chats? This action cannot be undone.": "", + "Are you sure you want to archive all chats? This action cannot be undone.": "Haluatko varmasti arkistoida kaikki keskustelut? Tätä toimintoa ei voi peruuttaa.", "Are you sure you want to clear all memories? This action cannot be undone.": "Haluatko varmasti tyhjentää kaikki muistot? Tätä toimintoa ei voi peruuttaa.", "Are you sure you want to delete \"{{NAME}}\"?": "Haluatko varmasti poistaa \"{{NAME}}\"?", - "Are you sure you want to delete all chats? This action cannot be undone.": "", + "Are you sure you want to delete all chats? This action cannot be undone.": "Haluatko varamsti poistaa kaikki keskustelut? Tätä toimintoa ei voi peruuttaa.", "Are you sure you want to delete this channel?": "Haluatko varmasti poistaa tämän kanavan?", "Are you sure you want to delete this message?": "Haluatko varmasti poistaa tämän viestin?", - "Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "", + "Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "Haluatko varamsti poistaa tämän version? Alaversiot linkitetään uudelleen tämän version ylätason versioon.", "Are you sure you want to unarchive all archived chats?": "Haluatko varmasti purkaa kaikkien arkistoitujen keskustelujen arkistoinnin?", "Arena Models": "Arena-mallit", "Artifacts": "Artefaktit", @@ -193,7 +193,7 @@ "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 API:n todennusmerkkijono", "AUTOMATIC1111 Base URL": "AUTOMATIC1111 verkko-osoite", "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 verkko-osoite vaaditaan.", - "Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "", + "Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "Lisää järjestelmätyökaluja automaattisesti natiivissa toimintokutsutilassa (esim. aikaleimat, muisti, keskusteluhistoria, muistiinpanot jne.)", "Available list": "Käytettävissä oleva luettelo", "Available Tools": "Käytettävissä olevat työkalut", "available users": "käytettävissä olevat käyttäjät", @@ -223,7 +223,7 @@ "Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Tiettyjen tokeneiden tehostaminen tai rankaiseminen rajoitetuista vastauksista. Poikkeaman arvot rajoitetaan välille -100 ja 100 (mukaan lukien). (Oletus: ei mitään)", "Brave": "", "Brave Search API Key": "Brave Search API -avain", - "Browse and query knowledge bases": "", + "Browse and query knowledge bases": "Selaa ja hae tietokannoista", "Builtin Tools": "Sisäänrakennetut työkalut", "Bullet List": "Luettelo", "Button ID": "Painikkeen ID", @@ -240,7 +240,7 @@ "Camera": "Kamera", "Cancel": "Peruuta", "Cannot create an empty note.": "Tyhjää muistiinpanoa ei voi luoda.", - "Cannot delete the production version": "", + "Cannot delete the production version": "Tuotantoversiota ei voi poistaa", "Capabilities": "Ominaisuudet", "Capture": "Näyttökuva", "Capture Audio": "Kaappaa ääntä", @@ -264,15 +264,15 @@ "Chat Controls": "Keskustelun hallinta", "Chat Conversation": "Chat-keskustelut", "Chat direction": "Keskustelun suunta", - "Chat exported successfully": "", - "Chat History": "", + "Chat exported successfully": "Keskustelut viety onnistuneesti", + "Chat History": "Keskusteluhistoria", "Chat ID": "Keskustelu ID", "Chat moved successfully": "Keskustelu siirrettiin onnistuneesti", "Chat Overview": "Keskustelun yleiskatsaus", "Chat Permissions": "Keskustelun käyttöoikeudet", "Chat Tags Auto-Generation": "Keskustelutunnisteiden automaattinen luonti", - "Chat unshared successfully.": "", - "chats": "", + "Chat unshared successfully.": "Keskustelun jako lopetettiin onnistuneesti", + "chats": "keskustelut", "Chats": "Keskustelut", "Check Again": "Tarkista uudelleen", "Check for updates": "Tarkista päivitykset", @@ -301,7 +301,7 @@ "Click here to upload a workflow.json file.": "Klikkaa tästä ladataksesi workflow.json-tiedosto.", "click here.": "klikkaa tästä.", "Click on the user role button to change a user's role.": "Klikkaa käyttäjän roolipainiketta vaihtaaksesi käyttäjän roolia.", - "Click to copy ID": "", + "Click to copy ID": "Klikkaa kopioidaksesi ID", "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Leikepöydälle kirjoitusoikeus evätty. Tarkista selaimesi asetukset ja myönnä tarvittavat käyttöoikeudet.", "Clone": "Kloonaa", "Clone Chat": "Kloonaa keskustelu", @@ -337,15 +337,15 @@ "ComfyUI Workflow": "ComfyUI-työnkulku", "ComfyUI Workflow Nodes": "ComfyUI-työnkulun solmut", "Comma separated Node Ids (e.g. 1 or 1,2)": "Pilkulla erotellut Node Id:t (esim. 1 tai 1,2)", - "command": "", + "command": "komento", "Command": "Komento", "Comment": "Kommentti", - "Commit Message": "", - "Community Reviews": "", + "Commit Message": "Vahvistusviesti", + "Community Reviews": "Yhteisön arvostelut", "Completions": "Täydennykset", "Compress Images in Channels": "Pakkaa kuvat kanavissa", "Concurrent Requests": "Samanaikaiset pyynnöt", - "Config": "", + "Config": "Määritykset", "Config imported successfully": "Määritysten tuonti onnistui", "Configure": "Määritä", "Confirm": "Vahvista", @@ -385,8 +385,8 @@ "Copy Last Response": "Kopioi viimeisin vastaus", "Copy link": "Kopioi linkki", "Copy Link": "Kopioi linkki", - "Copy Prompt": "", - "Copy Share Link": "", + "Copy Prompt": "Kopioi kehoite", + "Copy Share Link": "Kopioi jakolinkki", "Copy to clipboard": "Kopioi leikepöydälle", "Copy URL": "Kopioi linkki", "Copying to clipboard was successful!": "Kopioiminen leikepöydälle onnistui!", @@ -453,13 +453,13 @@ "Default User Role": "Oletuskäyttäjärooli", "Delete": "Poista", "Delete a model": "Poista malli", - "Delete All": "", + "Delete All": "Poista kaikki", "Delete All Chats": "Poista kaikki keskustelut", "Delete all contents inside this folder": "Poista kaikki sisällöt tästä kansiosta", "Delete All Models": "Poista kaikki mallit", "Delete Chat": "Poista keskustelu", "Delete chat?": "Haluatko varmasti poistaa tämän keskustelun?", - "Delete File": "", + "Delete File": "Poista tiedosto", "Delete folder?": "Haluatko varmasti poistaa tämän kansion?", "Delete function?": "Haluatko varmasti poistaa tämän toiminnon?", "Delete Message": "Poista viesti", @@ -467,20 +467,20 @@ "Delete Model": "Poista malli", "Delete note?": "Poista muistiinpano?", "Delete prompt?": "Haluatko varmasti poistaa tämän kehotteen?", - "Delete skill?": "", + "Delete skill?": "Poista taito?", "delete this link": "poista tämä linkki", "Delete tool?": "Haluatko varmasti poistaa tämän työkalun?", "Delete User": "Poista käyttäjä", - "Delete Version": "", + "Delete Version": "Poista versio?", "Deleted": "Poistettu", "Deleted {{deleteModelTag}}": "Poistettu {{deleteModelTag}}", "Deleted {{name}}": "Poistettu {{nimi}}", "Deleted User": "Käyttäjä poistettu", "Deployment names are required for Azure OpenAI": "Azure OpenAI:lle vaaditaan käyttöönottojen nimet", "Desc": "Laskeva", - "Describe the edit...": "", - "Describe the image...": "", - "Describe what changed...": "", + "Describe the edit...": "Kuvaile muokkausta...", + "Describe the image...": "Kuvaile kuvaa...", + "Describe what changed...": "Kuvaile mikä muuttui...", "Describe your knowledge base and objectives": "Kuvaa tietokantasi ja tavoitteesi", "Description": "Kuvaus", "Detect Artifacts Automatically": "Tunnista artefaktit automaattisesti", @@ -492,7 +492,7 @@ "Direct Message": "Suora viesti", "Direct Tool Servers": "Suorat työkalu palvelimet", "Directory selection was cancelled": "Hakemiston valinta keskeytettiin", - "Disable All": "", + "Disable All": "Poista kaikki käytöstä", "Disable Code Interpreter": "Poista Koodin suoritus käytöstä", "Disable Image Extraction": "Poista kuvien poiminta käytöstä", "Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Poista kuvien poiminta käytöstä PDF tiedostoista. Jos LLM on käytössä, kuvat tekstitetään automaattisesti. Oletuksena ei käytössä.", @@ -552,15 +552,15 @@ "e.g. A filter to remove profanity from text": "esim. suodatin, joka poistaa kirosanoja tekstistä", "e.g. about the Roman Empire": "esim. Rooman valtakunta", "e.g. alloy, echo, shimmer": "esim. alloy, echo, shimmer", - "e.g. Code Review Guidelines": "", - "e.g. code-review-guidelines": "", + "e.g. Code Review Guidelines": "esim. koodin tarkistusohjeet", + "e.g. code-review-guidelines": "esim. koodin-tarkistusohjeet", "e.g. en": "esim. en", "e.g. My Filter": "esim. Oma suodatin", "e.g. My Tools": "esim. Omat työkalut", "e.g. my_filter": "esim. oma_suodatin", "e.g. my_tools": "esim. omat_työkalut", "e.g. pdf, docx, txt": "esim. pdf, docx, txt", - "e.g. Step-by-step instructions for code reviews": "", + "e.g. Step-by-step instructions for code reviews": "esim. vaiheittaiset ohjeet koodikatselmuksiin", "e.g. Tell me a fun fact": "esim. Kerro minulle hauska fakta", "e.g. Tell me a fun fact about the Roman Empire": "esim. Kerro minulle hauska fakta Rooman valtakunnasta", "e.g. Tools for performing various operations": "esim. työkaluja erilaisten toimenpiteiden suorittamiseen", @@ -577,7 +577,7 @@ "Edit Image": "Kuvan muokkaus", "Edit Last Message": "Muokkaa viimeisintä viestiä", "Edit Memory": "Muokkaa muistia", - "Edit Prompt": "", + "Edit Prompt": "Muokkaa kehoitetta", "Edit User": "Muokkaa käyttäjää", "Edit User Group": "Muokkaa käyttäjäryhmää", "Edit workflow.json content": "Muokkaa workflow.json sisältöä", @@ -592,7 +592,7 @@ "Embedding Batch Size": "Upotuksen eräkoko", "Embedding Model": "Upotusmalli", "Embedding Model Engine": "Upotusmallin moottori", - "Enable All": "", + "Enable All": "Ota kaikki käyttöön", "Enable API Keys": "Ota API-avaimet käyttöön", "Enable autocomplete generation for chat messages": "Ota automaattinen täydennys käyttöön keskusteluviesteissä", "Enable Code Execution": "Ota koodin suoritus käyttöön", @@ -600,7 +600,7 @@ "Enable Community Sharing": "Ota yhteisön jakaminen käyttöön", "Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "Ota Memory Locking (mlock) käyttöön estääksesi mallidatan vaihtamisen pois RAM-muistista. Tämä lukitsee mallin työsivut RAM-muistiin, varmistaen että niitä ei vaihdeta levylle. Tämä voi parantaa suorituskykyä välttämällä sivuvikoja ja varmistamalla nopean tietojen käytön.", "Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "Ota Memory Mapping (mmap) käyttöön ladataksesi mallidataa. Tämä vaihtoehto sallii järjestelmän käyttää levytilaa RAM-laajennuksena käsittelemällä levytiedostoja kuin ne olisivat RAM-muistissa. Tämä voi parantaa mallin suorituskykyä sallimalla nopeamman tietojen käytön. Kuitenkin se ei välttämättä toimi oikein kaikissa järjestelmissä ja voi kuluttaa huomattavasti levytilaa.", - "Enable Message Queue": "", + "Enable Message Queue": "Ota viestijono käyttöön", "Enable Message Rating": "Ota viestiarviointi käyttöön", "Enable Mirostat sampling for controlling perplexity.": "", "Enable New Sign Ups": "Salli uudet rekisteröitymiset", @@ -722,8 +722,8 @@ "Enter Yacy Password": "Kirjoita Yacy salasana", "Enter Yacy URL (e.g. http://yacy.example.com:8090)": "Kirjoita Yacy verkko-osoite (esim. http://yacy.example.com:8090)", "Enter Yacy Username": "Kirjoita Yacy käyttäjänimi", - "Enter Yandex Web Search API Key": "", - "Enter Yandex Web Search URL": "", + "Enter Yandex Web Search API Key": "Kirjoita Yandex Web Search API-avain", + "Enter Yandex Web Search URL": "Kirjoita Yandex Web Search verkko-osoite", "Enter your code here...": "Kirjoita koodisi tähän...", "Enter your current password": "Kirjoita nykyinen salasanasi", "Enter Your Email": "Kirjoita sähköpostiosoitteesi", @@ -757,7 +757,7 @@ "Example: sAMAccountName or uid or userPrincipalName": "Esimerkki: sAMAccountName tai uid tai userPrincipalName", "Exceeded the number of seats in your license. Please contact support to increase the number of seats.": "Lisenssiisi kuuluva paikkamäärä on ylittynyt. Ota yhteyttä tukeen, jos haluat lisätä paikkoja.", "Exclude": "Jätä pois", - "Execute code": "", + "Execute code": "Suorita koodi", "Execute code for analysis": "Suorita koodi analysointia varten", "Executing **{{NAME}}**...": "Suoritetaan **{{NAME}}**...", "Expand": "Laajenna", @@ -769,7 +769,7 @@ "Export All Chats (All Users)": "Vie kaikki keskustelut (kaikki käyttäjät)", "Export chat (.json)": "Vie keskustelu (.json)", "Export Chats": "Vie keskustelut", - "Export Config": "", + "Export Config": "Vie asetukset", "Export Models": "Vie mallit", "Export Presets": "Vie esiasetukset", "Export Prompts": "Vie kehoitteet", @@ -792,7 +792,7 @@ "Failed to copy link": "Linkin kopiointi epäonnistui", "Failed to create API Key.": "API-avaimen luonti epäonnistui.", "Failed to delete note": "Muistiinpanon poistaminen epäonnistui", - "Failed to download image": "", + "Failed to download image": "Kuvan lataaminen epäonnistui", "Failed to extract content from the file: {{error}}": "Tiedoston sisällön pomiminen epäonnistui: {{error}}", "Failed to extract content from the file.": "Tiedoston sisällön pomiminen epäonnistui.", "Failed to fetch models": "Mallien hakeminen epäonnistui", @@ -810,7 +810,7 @@ "Failed to save connections": "Yhteyksien tallentaminen epäonnistui", "Failed to save conversation": "Keskustelun tallentaminen epäonnistui", "Failed to save models configuration": "Mallien määrityksen tallentaminen epäonnistui", - "Failed to unshare chat.": "", + "Failed to unshare chat.": "Jaon lopettaminen epäonnistui.", "Failed to update settings": "Asetusten päivittäminen epäonnistui", "Failed to update status": "Tilan päivittäminen epäonnistui", "Failed to upload file.": "Tiedoston lataaminen epäonnistui.", @@ -818,7 +818,7 @@ "Features Permissions": "Ominaisuuksien käyttöoikeudet", "February": "helmikuu", "Feedback": "Palaute", - "Feedback Activity": "", + "Feedback Activity": "Palautetoiminta", "Feedback deleted successfully": "Palaute poistettu onnistuneesti", "Feedback Details": "Palautteen tiedot", "Feedback History": "Palautehistoria", @@ -828,7 +828,7 @@ "File added successfully.": "Tiedosto lisätty onnistuneesti.", "File content updated successfully.": "Tiedoston sisältö päivitetty onnistuneesti.", "File Context": "Tiedoston konteksti", - "File deleted successfully.": "", + "File deleted successfully.": "Tiedosto poistettiin onnistuneesti.", "File Mode": "Tiedostotila", "File not found.": "Tiedostoa ei löytynyt.", "File removed successfully.": "Tiedosto poistettu onnistuneesti.", @@ -836,7 +836,7 @@ "File Upload": "Tiedoston lataus", "File uploaded successfully": "Tiedosto ladattiin onnistuneesti", "File uploaded!": "Tiedosto ladattu!", - "Filename": "", + "Filename": "Tiedostonimi", "Files": "Tiedostot", "Filter": "Suodata", "Filter is now globally disabled": "Suodatin on nyt poistettu käytöstä globaalisti", @@ -897,13 +897,13 @@ "General": "Yleinen", "Generate": "Luo", "Generate an image": "Luo kuva", - "Generate and edit images": "", + "Generate and edit images": "Luo ja muokkaa kuvia", "Generate Message Pair": "Luo viesti pari", "Generated Image": "Luo kuva", - "Generated images will appear here": "", + "Generated images will appear here": "Luodut kuvat näkyvät tässä", "Generating search query": "Luodaan hakukyselyä", "Generating...": "Luodaan...", - "Get current time and perform date/time calculations": "", + "Get current time and perform date/time calculations": "Hanki nykyinen aika ja suorita päivämäärä-/aikalaskelmia", "Get information on {{name}} in the UI": "Hae tietoja {{name}} -nimestä käyttöliittymässä", "Get started": "Aloita", "Get started with {{WEBUI_NAME}}": "Aloita käyttämään {{WEBUI_NAME}}:iä", @@ -934,16 +934,16 @@ "Height": "Korkeus", "Hello, {{name}}": "Hei, {{name}}", "Help": "Ohje", - "Help the community discover great models": "", + "Help the community discover great models": "Auta yhteisöä löytämään upeita malleja", "Hex Color": "Heksadesimaaliväri", "Hex Color - Leave empty for default color": "Heksadesimaaliväri - Jätä tyhjäksi, jos haluat oletusvärin", - "Hidden": "", + "Hidden": "Piilossa", "Hide": "Piilota", "Hide from Sidebar": "Piilota sivupalkista", "Hide Model": "Piilota malli", "High": "Korkea", "High Contrast Mode": "Korkean kontrastin tila", - "History": "", + "History": "Historia", "Home": "Koti", "Host": "Palvelin", "How can I help you today?": "Miten voin auttaa sinua tänään?", @@ -955,7 +955,7 @@ "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Vahvistan, että olen lukenut ja ymmärrän toimintani seuraukset. Olen tietoinen mielivaltaisen koodin suorittamiseen liittyvistä riskeistä ja olen varmistanut lähteen luotettavuuden.", "ID": "Tunnus", "ID cannot contain \":\" or \"|\" characters": "ID ei voi sisältää \":\" tai \"|\" kirjaimia", - "ID copied to clipboard": "", + "ID copied to clipboard": "ID kopioitu leikepöydälle", "iframe Sandbox Allow Forms": "Salli lomakkeet iframe hiekkalaatikossa", "iframe Sandbox Allow Same Origin": "Salli iframe hiekkalaatikko samasta alkuperästä", "Ignite curiosity": "Sytytä uteliaisuus", @@ -976,7 +976,7 @@ "Images": "Kuvat", "Import": "Tuo", "Import Chats": "Tuo keskustelut", - "Import Config": "", + "Import Config": "Tuo asetukset", "Import From Link": "Tuo verkko-osoitteesta", "Import Models": "Tuo mallit", "Import Presets": "Tuo esiasetuksia", @@ -984,7 +984,7 @@ "Import successful": "Tuonti onnistui", "Import Tools": "Tuo työkalut", "Important Update": "Tärkeä päivitys", - "Inactive": "", + "Inactive": "Toimeton", "Include": "Sisällytä", "Include `--api-auth` flag when running stable-diffusion-webui": "Sisällytä `--api-auth`-lippu ajettaessa stable-diffusion-webui", "Include `--api` flag when running stable-diffusion-webui": "Sisällytä `--api`-lippu ajettaessa stable-diffusion-webui", @@ -1086,7 +1086,7 @@ "Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).": "Rajoita samanaikaisia hakukyselyitä. 0 = rajoittamaton (oletus). Aseta arvoon 1 peräkkäistä suoritusta varten (suositellaan API-rajapinnoille, joilla on tiukat nopeusrajoitukset, kuten Brave-ilmaistaso).", "List": "Lista", "Listening...": "Kuuntelee...", - "Live": "", + "Live": "Live", "Llama.cpp": "Llama.cpp", "LLMs can make mistakes. Verify important information.": "Kielimallit voivat tehdä virheitä. Tarkista tärkeät tiedot.", "Loader": "Lataaja", @@ -1105,7 +1105,7 @@ "Male": "Mies", "Manage": "Hallitse", "Manage Direct Connections": "Hallitse suoria yhteyksiä", - "Manage Files": "", + "Manage Files": "Hallitse tiedostoja", "Manage Models": "Hallitse malleja", "Manage Ollama": "Hallitse Ollamaa", "Manage Ollama API Connections": "Hallitse Ollama API -yhteyksiä", @@ -1128,7 +1128,7 @@ "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "MCP-tuki on kokeellinen ja sen määritykset muuttuvat usein, mikä voi johtaa yhteensopivuus ongelmiin. OpenAPI-määritysten tuesta vastaa suoraan Open WebUI -tiimi, joten se on luotettavampi vaihtoehto yhteensopivuuden kannalta.", "Medium": "Keskitaso", "Member removed successfully": "Jäsen poistettu onnistuneesti", - "members": "", + "members": "käyttäjät", "Members": "Jäsenet", "Members added successfully": "Jäsen lisätty onnistuneesti", "Memories": "Muistot", @@ -1142,10 +1142,10 @@ "Merged Response": "Yhdistetty vastaus", "Message": "Viesti", "Message counts and response timestamps": "Viestien määrä ja vastausten aikaleimat", - "Message counts are based on assistant responses.": "", + "Message counts are based on assistant responses.": "Viestien määrä perustuu avustajan vastauksiin.", "Message rating should be enabled to use this feature": "Tämän toiminnon käyttämiseksi viestiarviointi on otettava käyttöön", - "messages": "", - "Messages": "", + "messages": "viestit", + "Messages": "Viestit", "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Linkin luomisen jälkeen lähettämäsi viestit eivät ole jaettuja. Käyttäjät, joilla on verkko-osoite, voivat tarkastella jaettua keskustelua.", "Microsoft OneDrive": "", "Microsoft OneDrive (personal)": "", @@ -1178,14 +1178,14 @@ "Model name already exists, please choose a different one": "Mallin nimi on jo olemassa, valitse toinen nimi.", "Model Name is required.": "Mallin nimi on pakollinen", "Model names and usage frequency": "Mallien nimet ja käyttötiheys", - "Model not found": "", + "Model not found": "Mallia ei löytynyt", "Model not selected": "Mallia ei ole valittu", "Model Params": "Mallin parametrit", "Model Permissions": "Mallin käyttöoikeudet", "Model responses or outputs": "Mallin vastaukset tai tuotokset", "Model unloaded successfully": "Malli purettu onnistuneesti", "Model updated successfully": "Malli päivitetty onnistuneesti", - "Model Usage": "", + "Model Usage": "Mallin käyttö", "Model(s) do not support file upload": "Malli(t) ei tue tiedostojen lataamista", "Modelfile Content": "Mallitiedoston sisältö", "Models": "Mallit", @@ -1198,7 +1198,7 @@ "Mojeek Search API Key": "Mojeek Search API -avain", "More": "Lisää", "More Concise": "Ytimekkäämpi", - "More options": "", + "More options": "Lisää vaihtoehtoja", "More Options": "Lisää vaihtoehtoja", "Move": "Siirrä", "Name": "Nimi", @@ -1215,13 +1215,13 @@ "New Note": "Uusi muistiinpano", "New Password": "Uusi salasana", "New Prompt": "Uusi kehoite", - "New Skill": "", + "New Skill": "Uusi taito", "New Temporary Chat": "Uusi väliaikainen keskustelu", "New Tool": "Uusi työkalu", "New Webhook": "Uusi Webhook", "new-channel": "uusi-kanava", "Next message": "Seuraava viesti", - "No access grants. Private to you.": "", + "No access grants. Private to you.": "Ei käyttöoikeuksia. Yksityinen sinulle.", "No activity data": "Ei aktiivisuustietoja", "No authentication": "Ei todennusta", "No chats found": "Keskuteluja ei löytynyt", @@ -1231,16 +1231,16 @@ "No content found": "Sisältöä ei löytynyt", "No content to speak": "Ei puhuttavaa sisältöä", "No conversation to save": "Ei tallennettavaa keskustelua", - "No data": "", - "No data found": "", + "No data": "Ei dataa", + "No data found": "Dataa ei löytynyt", "No distance available": "Etäisyyttä ei saatavilla", "No expiration can pose security risks.": "Vanhenemisen laittamatta jättäminen voi altistaa tietoturvariskeille.", "No feedback found": "Ei palautetta", "No file selected": "Tiedostoa ei ole valittu", - "No files found": "", + "No files found": "Tiedostoja ei löytynyt", "No files in this knowledge base.": "Tässä tietokannassa ei ole tiedostoja.", "No functions found": "Funktioita ei löytynyt", - "No history available": "", + "No history available": "Historiaa ei saatavilla", "No HTML, CSS, or JavaScript content found.": "HTML-, CSS- tai JavaScript-sisältöä ei löytynyt.", "No inference engine with management support found": "", "No knowledge bases found.": "Tietokantoja ei löytynyt.", @@ -1257,7 +1257,7 @@ "No results": "Ei tuloksia", "No results found": "Ei tuloksia", "No search query generated": "Hakukyselyä ei luotu", - "No skills found": "", + "No skills found": "Taitoja ei löytynyt", "No source available": "Lähdettä ei saatavilla", "No sources found": "Lähteitä ei löytynyt", "No suggestion prompts": "Ei ehdotettuja promptteja", @@ -1316,7 +1316,7 @@ "Open modal to configure connection": "Avaa modaali yhteyden määrittämiseksi", "Open Modal To Manage Floating Quick Actions": "Avaa modaali kelluvien pikatoimintojen hallitsemiseksi", "Open Modal To Manage Image Compression": "Avaa kuvien pakkaus hallinta modaali", - "Open Model Selector": "", + "Open Model Selector": "Avaa mallinvalitsin", "Open Settings": "Avaa asetukset", "Open Sidebar": "Avaa sivupalkki", "Open User Profile Menu": "Avaa käyttäjäprofiili ikkuna", @@ -1336,19 +1336,19 @@ "OpenAPI": "OpenAPI", "OpenAPI Spec": "OpenAPI Spec", "openapi.json URL or Path": "openapi.json verkko-osoite tai polku", - "optional": "", + "optional": "vaihtoehtoinen", "Optional": "Valinnainen", "or": "tai", "Ordered List": "Järjestetty lista", "Organize your users": "Järjestä käyttäjäsi", "Other": "Muu", - "Output": "", + "Output": "Tuloste", "OUTPUT": "TULOSTE", "Output format": "Tulosteen muoto", "Output Format": "Tulosteen muoto", "Overview": "Yleiskatsaus", "page": "sivu", - "Page": "", + "Page": "Sivu", "Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "", "Paginate": "Sivutus", "Parameters": "Parametrit", @@ -1359,7 +1359,7 @@ "Paste Large Text as File": "Liitä suuri teksti tiedostona", "PDF document (.pdf)": "PDF-asiakirja (.pdf)", "PDF Extract Images (OCR)": "Poimi kuvat PDF:stä (OCR)", - "PDF Loader Mode": "", + "PDF Loader Mode": "PDF latausmoodi", "pending": "odottaa", "Pending": "Odottaa", "Pending User Overlay Content": "Odottavien käyttäjien sisältö", @@ -1419,13 +1419,13 @@ "Previous message": "Edellinen viesti", "Private": "Yksityinen", "Private conversation between selected users": "Yksityinen keskustelu käyttäjien välillä", - "Production version updated": "", + "Production version updated": "Tuotantoversio päivitetty", "Profile": "Profiili", "Prompt": "Kehote", "Prompt Autocompletion": "Kehotteen automaattinen täydennys", "Prompt Content": "Kehotteen sisältö", "Prompt created successfully": "Kehote luotu onnistuneesti", - "Prompt Name": "", + "Prompt Name": "Kehoiteen nimi", "Prompt updated successfully": "Kehote päivitetty onnistuneesti", "Prompts": "Kehotteet", "Prompts Access": "Kehoitteiden käyttöoikeudet", @@ -1479,7 +1479,7 @@ "Remove image": "Poista kuva", "Remove Model": "Poista malli", "Rename": "Nimeä uudelleen", - "Render Markdown in Previews": "", + "Render Markdown in Previews": "Renderöi Markdown esikatseluissa", "Reorder Models": "Uudelleenjärjestä malleja", "Reply": "Vastaa", "Reply in Thread": "Vastaa ketjussa", @@ -1525,13 +1525,13 @@ "Search": "Haku", "Search a model": "Hae mallia", "Search all emojis": "Hae emojeista", - "Search and manage user memories": "", - "Search and view user chat history": "", + "Search and manage user memories": "Hae ja hallinnoi käyttäjien muistoja", + "Search and view user chat history": "Hae ja tarkastele käyttäjän keskusteluhistoriaa", "Search Base": "Hakupohja", - "Search channels and channel messages": "", + "Search channels and channel messages": "Hae kanavia ja kanavaviestejä", "Search Chats": "Hae keskusteluja", "Search Collection": "Hae kokoelmaa", - "Search Files": "", + "Search Files": "Hae tiedostoja", "Search Filters": "Hakusuodattimet", "search for archived chats": "Hae arkistoiduista keskusteluista", "search for folders": "Hae kansioista", @@ -1546,11 +1546,11 @@ "Search options": "Hakuvaihtoehdot", "Search Prompts": "Hae kehotteita", "Search Result Count": "Hakutulosten määrä", - "Search Skills": "", + "Search Skills": "Etsi taitoja", "Search the internet": "Hae verkosta", - "Search the web and fetch URLs": "", + "Search the web and fetch URLs": "Hae verkosta ja hae URL-osoitteita", "Search Tools": "Hae työkaluja", - "Search, view, and manage user notes": "", + "Search, view, and manage user notes": "Hae, tarkastele ja hallinnoi käyttäjämuistiinpanoja", "SearchApi API Key": "SearchApi API -avain", "SearchApi Engine": "SearchApi-moottori", "Searched {{count}} sites": "Etsitty {{count}} sivulta", @@ -1559,7 +1559,7 @@ "Searching Knowledge for \"{{searchQuery}}\"": "Haetaan tietämystä \"{{searchQuery}}\"", "Searching the web": "Haetaan verkosta...", "Searxng Query URL": "Searxng-kyselyn verkko-osoite", - "Searxng search language (all, en, es, de, fr, etc.)": "", + "Searxng search language (all, en, es, de, fr, etc.)": "Hakukieli (all, en, es, de, fr jne.)", "See readme.md for instructions": "Katso ohjeet readme.md-tiedostosta", "See what's new": "Katso, mitä uutta", "Seed": "Siemenluku", @@ -1585,7 +1585,7 @@ "Select an embedding model engine": "Valitse upotusmallin moottori", "Select an engine": "Valitse moottori", "Select an Ollama instance": "Valitse Ollama instanssi", - "Select an option": "", + "Select an option": "Valitse vaihtoehto", "Select an output format": "Valitse tulostusmuoto", "Select dtype": "Valitse dtype", "Select Engine": "Valitse moottori", @@ -1599,7 +1599,7 @@ "Send": "Lähetä", "Send a Message": "Lähetä viesti", "Send message": "Lähetä viesti", - "Send now": "", + "Send now": "Lähetä nyt", "Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Lähettää `stream_options: { include_usage: true }` pyynnössä.\nTuetut tarjoajat palauttavat tokenkäyttötiedot vastauksessa, kun se on asetettu.", "September": "syyskuu", "SerpApi API Key": "SerpApi API -avain", @@ -1610,7 +1610,7 @@ "Server connection verified": "Palvelinyhteys vahvistettu", "Session": "Istunto", "Set as default": "Aseta oletukseksi", - "Set as Production": "", + "Set as Production": "Aseta tuotantokäyttöön", "Set embedding model": "Aseta upotelmamalli", "Set embedding model (e.g. {{model}})": "Aseta upotelmamalli (esim. {{model}})", "Set reranking model (e.g. {{model}})": "Aseta uudelleenpisteytymismalli (esim. {{model}})", @@ -1631,16 +1631,16 @@ "Settings saved successfully!": "Asetukset tallennettu onnistuneesti!", "Share": "Jaa", "Share Chat": "Jaa keskustelu", - "Share link copied to clipboard.": "", + "Share link copied to clipboard.": "Jakolinkki kopioitu leikepöydälle.", "Share to Open WebUI Community": "Jaa OpenWebUI-yhteisöön", "Share your background and interests": "Jaa taustasi ja kiinnostuksen kohteesi", - "Shared Chats": "", + "Shared Chats": "Jaetut keskustelut", "Shared with you": "Jaettu kanssasi", "Sharing Permissions": "Jako oikeudet", "Show": "Näytä", "Show \"What's New\" modal on login": "Näytä \"Mitä uutta\" -modaali kirjautumisen yhteydessä", "Show Admin Details in Account Pending Overlay": "Näytä ylläpitäjän tiedot odottavan tilin päällä", - "Show all ({{COUNT}} characters)": "", + "Show all ({{COUNT}} characters)": "Näytä kaikki ({{COUNT}} merkkiä)", "Show Files": "Näytä tiedostot", "Show Formatting Toolbar": "Näytä muotoilupalkki", "Show image preview": "Näytä kuvan esikatselu", @@ -1648,7 +1648,7 @@ "Show Shortcuts": "Näytä pikakomennot", "Show your support!": "Osoita tukesi!", "Showcased creativity": "Osoitti luovuutta", - "Showing all messages (user + assistant) per user.": "", + "Showing all messages (user + assistant) per user.": "Näytetään kaikki viestit (käyttäjä + avustaja) käyttäjää kohden.", "Sign in": "Kirjaudu sisään", "Sign in to {{WEBUI_NAME}}": "Kirjaudu sisään palveluun {{WEBUI_NAME}}", "Sign in to {{WEBUI_NAME}} with LDAP": "Kirjaudu sisään palveluun {{WEBUI_NAME}} LDAP:lla", @@ -1660,16 +1660,16 @@ "Single": "", "Sink List": "Upotuslista", "sk-1234": "sk-1234", - "Skill created successfully": "", - "Skill deleted successfully": "", - "Skill Description": "", - "Skill ID": "", - "Skill Name": "", - "Skill updated successfully": "", - "Skills": "", - "Skills Access": "", - "Skills Public Sharing": "", - "Skills Sharing": "", + "Skill created successfully": "Taito luotu onnistuneesti", + "Skill deleted successfully": "Taito poistettu onnistuneesti", + "Skill Description": "Taidon kuvaus", + "Skill ID": "Taidon ID", + "Skill Name": "Taidon nimi", + "Skill updated successfully": "Taito päivitetty onnistuneesti", + "Skills": "Taidot", + "Skills Access": "Pääsy taitoihin", + "Skills Public Sharing": "Taitojen julkinen jakaminen", + "Skills Sharing": "Taitojen jako", "Skip Cache": "Ohita välimuisti", "Skip the cache and re-run the inference. Defaults to False.": "Ohita välimuisti ja suorita päätelmä uudelleen. Oletusarvo ei käytössä.", "Something went wrong :/": "Jokin meni pieleen :/", @@ -1686,7 +1686,7 @@ "Speech recognition error: {{error}}": "Puheentunnistusvirhe: {{error}}", "Speech-to-Text": "Puheentunnistus", "Speech-to-Text Engine": "Puheentunnistusmoottori", - "Split documents by markdown headers before applying character/token splitting.": "", + "Split documents by markdown headers before applying character/token splitting.": "Jaa dokumentit Markdown-otsikoiden mukaan ennen merkki-/tunnusjaon käyttöä", "Start a new conversation": "Aloita uusi keskustelu", "Start of the channel": "Kanavan alku", "Start Tag": "Aloitus tagi", @@ -1797,11 +1797,11 @@ "Thought for {{DURATION}} seconds": "Ajatteli {{DURATION}} sekuntia", "Thought for less than a second": "Ajatteli alle sekunnin", "Thread": "Ketju", - "Thumbs up/down ratings from users on model responses": "", + "Thumbs up/down ratings from users on model responses": "Käyttäjien peukku-/alas-arviot mallien vastauksista", "Tika": "Tika", "Tika Server URL required.": "Tika palvelimen verkko-osoite vaaditaan.", "Tiktoken": "Tiktoken", - "Time & Calculation": "", + "Time & Calculation": "Aika ja laskenta", "Timeout": "Aikakatkaisu", "Title": "Otsikko", "Title Auto-Generation": "Otsikon automaattinen luonti", @@ -1814,7 +1814,7 @@ "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Päästäksesi käyttämään WebUI:ta, ota yhteyttä ylläpitäjään. Ylläpitäjät voivat hallita käyttäjien tiloja Ylläpitopaneelista.", "To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Liittääksesi tietokantasi tähän, lisää ne ensin \"Tietämys\"-työtilaan.", "To learn more about available endpoints, visit our documentation.": "Jos haluat lisätietoja käytettävissä olevista päätepisteistä, tutustu dokumentaatioomme.", - "To select skills here, add them to the \"Skills\" workspace first.": "", + "To select skills here, add them to the \"Skills\" workspace first.": "Jos haluat valita taitoja tässä, lisää ne ensin \"Taidot\"-työtilaan.", "To select toolkits here, add them to the \"Tools\" workspace first.": "Valitaksesi työkalusettejä tässä, lisää ne ensin \"Työkalut\"-työtilaan.", "Toast notifications for new updates": "Ilmoituspopuppien näyttäminen uusista päivityksistä", "Today": "Tänään", @@ -1822,9 +1822,9 @@ "Toggle Sidebar": "Näytä/piilota sivupalkki", "Toggle whether current connection is active.": "Vaihda, onko nykyinen yhteys aktiivinen", "Token": "Token", - "Token counts are estimates and may not reflect actual API usage": "", - "tokens": "", - "Tokens": "", + "Token counts are estimates and may not reflect actual API usage": "Tokenmäärät ovat arvioita eivätkä välttämättä vastaa todellista API-käyttöä", + "tokens": "tokenit", + "Tokens": "Tokenit", "Too verbose": "Liian puhelias", "Tool created successfully": "Työkalu luotu onnistuneesti", "Tool deleted successfully": "Työkalu poistettu onnistuneesti", @@ -1868,7 +1868,7 @@ "Unlock mysteries": "Selvitä arvoituksia", "Unpin": "Irrota kiinnitys", "Unravel secrets": "Avaa salaisuuksia", - "Unshare Chat": "", + "Unshare Chat": "Lopeta keskustelun jakaminen", "Unsupported file type.": "Ei tuettu tiedostotyyppi", "Untagged": "Ei tageja", "Untitled": "Nimetön", @@ -1897,7 +1897,7 @@ "URL is required": "URL vaaditaan", "URL Mode": "URL-tila", "Usage": "Käyttö", - "Use": "", + "Use": "Käytä", "Use '#' in the prompt input to load and include your knowledge.": "Käytä '#' -merkkiä kehotekenttään ladataksesi ja sisällyttääksesi tietämystäsi.", "Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Käytä /v1/chat/completions-päätteitä/v1/audio/transcriptions-pääteen sijaan mahdollisesti paremman tarkkuuden saavuttamiseksi.", "Use Chat Completions API": "Käytä Chat Completions API:a", @@ -1907,7 +1907,7 @@ "Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "Käytä http_proxy- ja https_proxy-ympäristömuuttujien määrittämää välityspalvelinta sivun sisällön hakemiseen.", "user": "käyttäjä", "User": "Käyttäjä", - "User Activity": "", + "User Activity": "Käyttäjätoiminta", "User Groups": "Käyttäjäryhmät", "User location successfully retrieved.": "Käyttäjän sijainti haettu onnistuneesti.", "User menu": "Käyttäjävalikko", @@ -1932,11 +1932,11 @@ "Verify SSL Certificate": "Tarkista SSL-varmenne", "Version": "Versio", "Version {{selectedVersion}} of {{totalVersions}}": "Versio {{selectedVersion}} / {{totalVersions}}", - "Version deleted": "", + "Version deleted": "Versio poistettu", "View Replies": "Näytä vastaukset", "View Result from **{{NAME}}**": "Näytä **{{NAME}}** tulokset", "Visibility": "Näkyvyys", - "Visible": "", + "Visible": "Näkyvillä", "Visible to all users": "Näkyy kaikille käyttäjille", "Vision": "Visio", "Voice": "Ääni", @@ -1992,9 +1992,9 @@ "Yacy Username": "Yacy käyttäjänimi", "Yahoo": "", "Yandex": "", - "Yandex Web Search API Key": "", - "Yandex Web Search config": "", - "Yandex Web Search URL": "", + "Yandex Web Search API Key": "Yandex Web Search API -avain", + "Yandex Web Search config": "Yandex Web Search asetukset", + "Yandex Web Search URL": "Yandex Web Search verkko-osoite", "Yesterday": "Eilen", "Yesterday at {{LOCALIZED_TIME}}": "Eilen {{LOCALIZED_TIME}}", "You": "Sinä", @@ -2004,15 +2004,15 @@ "You cannot upload an empty file.": "Et voi ladata tyhjää tiedostoa.", "You do not have permission to edit this model": "Sinulla ei ole oikeuksia muokata tätä mallinetta", "You do not have permission to edit this prompt.": "Sinulla ei ole oikeuksia muokata tätä kehotetta.", - "You do not have permission to edit this skill.": "", - "You do not have permission to edit this tool": "", - "You do not have permission to make this public": "", + "You do not have permission to edit this skill.": "Sinulla ei ole oikeutta muokata tätä taitoa.", + "You do not have permission to edit this tool": "Sinulla ei ole oikeutta muokata tätä työkalua", + "You do not have permission to make this public": "Sinulla ei ole lupaa tehdä tästä julkista", "You do not have permission to send messages in this channel.": "Sinulla ei ole oikeuksia lähettää viestejä tähän kanavaan.", "You do not have permission to send messages in this thread.": "Sinulla ei ole oikeuksia lähettää viestejä tähän ketjuun.", "You do not have permission to upload files to this knowledge base.": "Sinulla ei ole oikeuksia ladata tiedostoja tähän tietokantaan.", "You do not have permission to upload files.": "Sinulla ei ole lupaa ladata tiedostoja.", "You have no archived conversations.": "Sinulla ei ole arkistoituja keskusteluja.", - "You have no shared conversations.": "", + "You have no shared conversations.": "Sinulla ei ole jaettuja keskusteluja.", "You have shared this chat": "Olet jakanut tämän keskustelun", "You're a helpful assistant.": "Olet avulias avustaja.", "You're now logged in.": "Olet nyt kirjautunut sisään.", From c5c31ab7699aadb1d352e142626aa57c84d24fee Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:36:48 +0100 Subject: [PATCH 27/70] fix: respect BYPASS_ADMIN_ACCESS_CONTROL in file list/search endpoints (#21595) --- backend/open_webui/routers/files.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 4a12db5cd9..ad667533dd 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -47,6 +47,7 @@ from open_webui.routers.audio import transcribe from open_webui.storage.provider import Storage +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.misc import strict_match_mime_type from pydantic import BaseModel @@ -362,7 +363,7 @@ async def list_files( content: bool = Query(True), db: Session = Depends(get_session), ): - if user.role == "admin": + if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: files = Files.get_files(db=db) else: files = Files.get_files_by_user_id(user.id, db=db) @@ -398,8 +399,8 @@ async def search_files( Search for files by filename with support for wildcard patterns. Uses SQL-based filtering with pagination for better performance. """ - # Determine user_id: null for admin (search all), user.id for regular users - user_id = None if user.role == "admin" else user.id + # Determine user_id: null for admin with bypass (search all), user.id otherwise + user_id = None if (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) else user.id # Use optimized database query with pagination files = Files.search_files( From 326599b8db0f147918ddd8fb96cc7d98fc521c46 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:38:01 +0100 Subject: [PATCH 28/70] =?UTF-8?q?Fix=20O(n=C2=B2)=20performance=20in=20get?= =?UTF-8?q?=5Fmessage=5Flist=20by=20replacing=20insert(0)=20with=20append+?= =?UTF-8?q?reverse=20(#21588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jordan --- backend/open_webui/utils/misc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index a63d425ff5..a52dca7a88 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -93,12 +93,11 @@ def get_message_list(messages_map, message_id): message_list = [] while current_message: - message_list.insert( - 0, current_message - ) # Insert the message at the beginning of the list + message_list.append(current_message) parent_id = current_message.get("parentId") # Use .get() for safety current_message = messages_map.get(parent_id) if parent_id else None + message_list.reverse() return message_list From 6d67ac371d37757f01836acb4983f2455b07fea6 Mon Sep 17 00:00:00 2001 From: VasilyLebedev123 <150416542+VasilyLebedev123@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:08:40 +1030 Subject: [PATCH 29/70] fix: correct unpacking order of distances, documents, and metadatas in hybrid search query (#21562) Co-authored-by: Vasily Lebedev --- backend/open_webui/retrieval/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 96fd9d3f89..536ddb4c32 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -292,7 +292,7 @@ async def query_doc_with_hybrid_search( # retrieve only min(k, k_reranker) items, sort and cut by distance if k < k_reranker if k < k_reranker: sorted_items = sorted( - zip(distances, metadatas, documents), key=lambda x: x[0], reverse=True + zip(distances, documents, metadatas), key=lambda x: x[0], reverse=True ) sorted_items = sorted_items[:k] From 3db6d49e578db47bc38447fc058827d0e5e4d031 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:41:46 +0100 Subject: [PATCH 30/70] Query title column directly in get_chat_title_by_id instead of loading full chat (#157) (#21590) Previously loaded the entire ChatModel (including the full conversation JSON blob) just to extract the title string. Now queries only the Chat.title column directly, which is already a top-level DB column. --- backend/open_webui/models/chats.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index f579a11822..0ae88acffb 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -456,11 +456,11 @@ class ChatTable: return ChatModel.model_validate(chat) def get_chat_title_by_id(self, id: str) -> Optional[str]: - chat = self.get_chat_by_id(id) - if chat is None: - return None - - return chat.chat.get("title", "New Chat") + with get_db_context() as db: + result = db.query(Chat.title).filter_by(id=id).first() + if result is None: + return None + return result[0] or "New Chat" def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: chat = self.get_chat_by_id(id) From 9950cc8c2862896f9e40a59f3647a319bf185dea Mon Sep 17 00:00:00 2001 From: fchevallieratecna <119590452+fchevallieratecna@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:42:57 +0100 Subject: [PATCH 31/70] =?UTF-8?q?fix(i18n):=20correct=20French=20typo=20"A?= =?UTF-8?q?naltique"=20=E2=86=92=20"Analytique"=20(#21512)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From d664922febcb3f3555958744de7b14d1bc439fe9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:48:23 +0100 Subject: [PATCH 32/70] Avoid loading full chat JSON blob for pinned/archived/shared list endpoints (#21591) Co-authored-by: Tim Baek --- backend/open_webui/models/chats.py | 49 +++++++++++++++++++++++------ backend/open_webui/routers/chats.py | 24 +++++--------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 0ae88acffb..42c8cb4508 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -714,7 +714,7 @@ class ChatTable: skip: int = 0, limit: int = 50, db: Optional[Session] = None, - ) -> list[ChatModel]: + ) -> list[ChatTitleIdResponse]: with get_db_context(db) as db: query = db.query(Chat).filter_by(user_id=user_id, archived=True) @@ -740,13 +740,27 @@ class ChatTable: else: query = query.order_by(Chat.updated_at.desc()) + query = query.with_entities( + Chat.id, Chat.title, Chat.updated_at, Chat.created_at + ) + if skip: query = query.offset(skip) if limit: query = query.limit(limit) all_chats = query.all() - return [ChatModel.model_validate(chat) for chat in all_chats] + return [ + ChatTitleIdResponse.model_validate( + { + "id": chat[0], + "title": chat[1], + "updated_at": chat[2], + "created_at": chat[3], + } + ) + for chat in all_chats + ] def get_shared_chat_list_by_user_id( self, @@ -802,12 +816,14 @@ class ChatTable: all_chats = query.all() return [ - SharedChatResponse( - id=chat[0], - title=chat[1], - share_id=chat[2], - updated_at=chat[3], - created_at=chat[4], + SharedChatResponse.model_validate( + { + "id": chat[0], + "title": chat[1], + "share_id": chat[2], + "updated_at": chat[3], + "created_at": chat[4], + } ) for chat in all_chats ] @@ -1017,14 +1033,27 @@ class ChatTable: def get_pinned_chats_by_user_id( self, user_id: str, db: Optional[Session] = None - ) -> list[ChatModel]: + ) -> list[ChatTitleIdResponse]: with get_db_context(db) as db: all_chats = ( db.query(Chat) .filter_by(user_id=user_id, pinned=True, archived=False) .order_by(Chat.updated_at.desc()) + .with_entities( + Chat.id, Chat.title, Chat.updated_at, Chat.created_at + ) ) - return [ChatModel.model_validate(chat) for chat in all_chats] + return [ + ChatTitleIdResponse.model_validate( + { + "id": chat[0], + "title": chat[1], + "updated_at": chat[2], + "created_at": chat[3], + } + ) + for chat in all_chats + ] def get_archived_chats_by_user_id( self, user_id: str, db: Optional[Session] = None diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index e04c234966..ff3f6c78c7 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -723,10 +723,7 @@ async def get_chat_list_by_folder_id( async def get_user_pinned_chats( user=Depends(get_verified_user), db: Session = Depends(get_session) ): - return [ - ChatTitleIdResponse(**chat.model_dump()) - for chat in Chats.get_pinned_chats_by_user_id(user.id, db=db) - ] + return Chats.get_pinned_chats_by_user_id(user.id, db=db) ############################ @@ -821,18 +818,13 @@ async def get_archived_session_user_chat_list( if direction: filter["direction"] = direction - chat_list = [ - ChatTitleIdResponse(**chat.model_dump()) - for chat in Chats.get_archived_chat_list_by_user_id( - user.id, - filter=filter, - skip=skip, - limit=limit, - db=db, - ) - ] - - return chat_list + return Chats.get_archived_chat_list_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) ############################ From 8c485b260f5296a6c077c17ec347bb486979eb11 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:53:21 -0600 Subject: [PATCH 33/70] refac --- backend/open_webui/models/access_grants.py | 25 ++++++++ backend/open_webui/models/channels.py | 48 ++++++++++++-- backend/open_webui/models/knowledge.py | 42 ++++++++++-- backend/open_webui/models/models.py | 75 +++++++++++++++++++--- backend/open_webui/models/notes.py | 40 ++++++++++-- backend/open_webui/models/prompts.py | 28 ++++++-- backend/open_webui/models/skills.py | 30 +++++++-- backend/open_webui/models/tools.py | 21 +++++- 8 files changed, 271 insertions(+), 38 deletions(-) diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index a15e4f3c5d..227621becd 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -456,6 +456,31 @@ class AccessGrantsTable: ) return [AccessGrantModel.model_validate(g) for g in grants] + def get_grants_by_resources( + self, + resource_type: str, + resource_ids: list[str], + db: Optional[Session] = None, + ) -> dict[str, list[AccessGrantModel]]: + """Batch-fetch grants for multiple resources. Returns {resource_id: [grants]}.""" + if not resource_ids: + return {} + with get_db_context(db) as db: + grants = ( + db.query(AccessGrant) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + ) + .all() + ) + result: dict[str, list[AccessGrantModel]] = { + rid: [] for rid in resource_ids + } + for g in grants: + result[g.resource_id].append(AccessGrantModel.model_validate(g)) + return result + def has_access( self, user_id: str, diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 8a55da9345..2253d538c8 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -261,13 +261,19 @@ class ChannelTable: return AccessGrants.get_grants_by_resource("channel", channel_id, db=db) def _to_channel_model( - self, channel: Channel, db: Optional[Session] = None + self, + channel: Channel, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, ) -> ChannelModel: channel_data = ChannelModel.model_validate(channel).model_dump( exclude={"access_grants"} ) - access_grants = self._get_access_grants(channel_data["id"], db=db) - channel_data["access_grants"] = access_grants + channel_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(channel_data["id"], db=db) + ) return ChannelModel.model_validate(channel_data) def _collect_unique_user_ids( @@ -368,7 +374,18 @@ class ChannelTable: def get_channels(self, db: Optional[Session] = None) -> list[ChannelModel]: with get_db_context(db) as db: channels = db.query(Channel).all() - return [self._to_channel_model(channel, db=db) for channel in channels] + channel_ids = [channel.id for channel in channels] + grants_map = AccessGrants.get_grants_by_resources( + "channel", channel_ids, db=db + ) + return [ + self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels + ] def _has_permission(self, db, query, filter: dict, permission: str = "read"): return AccessGrants.has_permission_filter( @@ -417,7 +434,16 @@ class ChannelTable: standard_channels = query.all() all_channels = membership_channels + standard_channels - return [self._to_channel_model(c, db=db) for c in all_channels] + channel_ids = [c.id for c in all_channels] + grants_map = AccessGrants.get_grants_by_resources( + "channel", channel_ids, db=db + ) + return [ + self._to_channel_model( + c, access_grants=grants_map.get(c.id, []), db=db + ) + for c in all_channels + ] def get_dm_channel_by_user_ids( self, user_ids: list[str], db: Optional[Session] = None @@ -724,7 +750,17 @@ class ChannelTable: ) channel_ids = [cf.channel_id for cf in channel_files] channels = db.query(Channel).filter(Channel.id.in_(channel_ids)).all() - return [self._to_channel_model(channel, db=db) for channel in channels] + grants_map = AccessGrants.get_grants_by_resources( + "channel", channel_ids, db=db + ) + return [ + self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels + ] def get_channels_by_file_id_and_user_id( self, file_id: str, user_id: str, db: Optional[Session] = None diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 1d21d5d910..f9defae6ef 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -144,13 +144,18 @@ class KnowledgeTable: return AccessGrants.get_grants_by_resource("knowledge", knowledge_id, db=db) def _to_knowledge_model( - self, knowledge: Knowledge, db: Optional[Session] = None + self, + knowledge: Knowledge, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, ) -> KnowledgeModel: knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump( exclude={"access_grants"} ) - knowledge_data["access_grants"] = self._get_access_grants( - knowledge_data["id"], db=db + knowledge_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(knowledge_data["id"], db=db) ) return KnowledgeModel.model_validate(knowledge_data) @@ -192,9 +197,13 @@ class KnowledgeTable: db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all() ) user_ids = list(set(knowledge.user_id for knowledge in all_knowledge)) + knowledge_ids = [knowledge.id for knowledge in all_knowledge] users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = AccessGrants.get_grants_by_resources( + "knowledge", knowledge_ids, db=db + ) knowledge_bases = [] for knowledge in all_knowledge: @@ -202,7 +211,11 @@ class KnowledgeTable: knowledge_bases.append( KnowledgeUserModel.model_validate( { - **self._to_knowledge_model(knowledge, db=db).model_dump(), + **self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ).model_dump(), "user": user.model_dump() if user else None, } ) @@ -261,13 +274,22 @@ class KnowledgeTable: items = query.all() + knowledge_ids = [kb.id for kb, _ in items] + grants_map = AccessGrants.get_grants_by_resources( + "knowledge", knowledge_ids, db=db + ) + knowledge_bases = [] for knowledge_base, user in items: knowledge_bases.append( KnowledgeUserModel.model_validate( { **self._to_knowledge_model( - knowledge_base, db=db + knowledge_base, + access_grants=grants_map.get( + knowledge_base.id, [] + ), + db=db, ).model_dump(), "user": ( UserModel.model_validate(user).model_dump() @@ -440,8 +462,16 @@ class KnowledgeTable: .filter(KnowledgeFile.file_id == file_id) .all() ) + knowledge_ids = [k.id for k in knowledges] + grants_map = AccessGrants.get_grants_by_resources( + "knowledge", knowledge_ids, db=db + ) return [ - self._to_knowledge_model(knowledge, db=db) + self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ) for knowledge in knowledges ] except Exception: diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index cfece00e35..25b135c87f 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -144,11 +144,20 @@ class ModelsTable: ) -> list[AccessGrantModel]: return AccessGrants.get_grants_by_resource("model", model_id, db=db) - def _to_model_model(self, model: Model, db: Optional[Session] = None) -> ModelModel: + def _to_model_model( + self, + model: Model, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, + ) -> ModelModel: model_data = ModelModel.model_validate(model).model_dump( exclude={"access_grants"} ) - model_data["access_grants"] = self._get_access_grants(model_data["id"], db=db) + model_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(model_data["id"], db=db) + ) return ModelModel.model_validate(model_data) def insert_new_model( @@ -181,8 +190,14 @@ class ModelsTable: def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]: with get_db_context(db) as db: + all_models = db.query(Model).all() + model_ids = [model.id for model in all_models] + grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db) return [ - self._to_model_model(model, db=db) for model in db.query(Model).all() + self._to_model_model( + model, access_grants=grants_map.get(model.id, []), db=db + ) + for model in all_models ] def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]: @@ -190,9 +205,11 @@ class ModelsTable: all_models = db.query(Model).filter(Model.base_model_id != None).all() user_ids = list(set(model.user_id for model in all_models)) + model_ids = [model.id for model in all_models] users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db) models = [] for model in all_models: @@ -200,7 +217,11 @@ class ModelsTable: models.append( ModelUserResponse.model_validate( { - **self._to_model_model(model, db=db).model_dump(), + **self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ).model_dump(), "user": user.model_dump() if user else None, } ) @@ -209,9 +230,16 @@ class ModelsTable: def get_base_models(self, db: Optional[Session] = None) -> list[ModelModel]: with get_db_context(db) as db: + all_models = ( + db.query(Model).filter(Model.base_model_id == None).all() + ) + model_ids = [model.id for model in all_models] + grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db) return [ - self._to_model_model(model, db=db) - for model in db.query(Model).filter(Model.base_model_id == None).all() + self._to_model_model( + model, access_grants=grants_map.get(model.id, []), db=db + ) + for model in all_models ] def get_models_by_user_id( @@ -325,11 +353,18 @@ class ModelsTable: items = query.all() + model_ids = [model.id for model, _ in items] + grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db) + models = [] for model, user in items: models.append( ModelUserResponse( - **self._to_model_model(model, db=db).model_dump(), + **self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ).model_dump(), user=( UserResponse(**UserModel.model_validate(user).model_dump()) if user @@ -356,7 +391,18 @@ class ModelsTable: try: with get_db_context(db) as db: models = db.query(Model).filter(Model.id.in_(ids)).all() - return [self._to_model_model(model, db=db) for model in models] + model_ids = [model.id for model in models] + grants_map = AccessGrants.get_grants_by_resources( + "model", model_ids, db=db + ) + return [ + self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in models + ] except Exception: return [] @@ -465,9 +511,18 @@ class ModelsTable: db.commit() + all_models = db.query(Model).all() + model_ids = [model.id for model in all_models] + grants_map = AccessGrants.get_grants_by_resources( + "model", model_ids, db=db + ) return [ - self._to_model_model(model, db=db) - for model in db.query(Model).all() + self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in all_models ] except Exception as e: log.exception(f"Error syncing models for user {user_id}: {e}") diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index d17c749d1c..ff8a3ac635 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -93,9 +93,18 @@ class NoteTable: ) -> list[AccessGrantModel]: return AccessGrants.get_grants_by_resource("note", note_id, db=db) - def _to_note_model(self, note: Note, db: Optional[Session] = None) -> NoteModel: + def _to_note_model( + self, + note: Note, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, + ) -> NoteModel: note_data = NoteModel.model_validate(note).model_dump(exclude={"access_grants"}) - note_data["access_grants"] = self._get_access_grants(note_data["id"], db=db) + note_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(note_data["id"], db=db) + ) return NoteModel.model_validate(note_data) def _has_permission(self, db, query, filter: dict, permission: str = "read"): @@ -142,7 +151,14 @@ class NoteTable: if limit is not None: query = query.limit(limit) notes = query.all() - return [self._to_note_model(note, db=db) for note in notes] + note_ids = [note.id for note in notes] + grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db) + return [ + self._to_note_model( + note, access_grants=grants_map.get(note.id, []), db=db + ) + for note in notes + ] def search_notes( self, @@ -227,11 +243,18 @@ class NoteTable: items = query.all() + note_ids = [note.id for note, _ in items] + grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db) + notes = [] for note, user in items: notes.append( NoteUserResponse( - **self._to_note_model(note, db=db).model_dump(), + **self._to_note_model( + note, + access_grants=grants_map.get(note.id, []), + db=db, + ).model_dump(), user=( UserResponse(**UserModel.model_validate(user).model_dump()) if user @@ -266,7 +289,14 @@ class NoteTable: query = query.limit(limit) notes = query.all() - return [self._to_note_model(note, db=db) for note in notes] + note_ids = [note.id for note in notes] + grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db) + return [ + self._to_note_model( + note, access_grants=grants_map.get(note.id, []), db=db + ) + for note in notes + ] def get_note_by_id( self, id: str, db: Optional[Session] = None diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 80b7856f0f..dc83ee51dc 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -97,12 +97,19 @@ class PromptsTable: return AccessGrants.get_grants_by_resource("prompt", prompt_id, db=db) def _to_prompt_model( - self, prompt: Prompt, db: Optional[Session] = None + self, + prompt: Prompt, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, ) -> PromptModel: prompt_data = PromptModel.model_validate(prompt).model_dump( exclude={"access_grants"} ) - prompt_data["access_grants"] = self._get_access_grants(prompt_data["id"], db=db) + prompt_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(prompt_data["id"], db=db) + ) return PromptModel.model_validate(prompt_data) def insert_new_prompt( @@ -206,9 +213,11 @@ class PromptsTable: ) user_ids = list(set(prompt.user_id for prompt in all_prompts)) + prompt_ids = [prompt.id for prompt in all_prompts] users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = AccessGrants.get_grants_by_resources("prompt", prompt_ids, db=db) prompts = [] for prompt in all_prompts: @@ -216,7 +225,11 @@ class PromptsTable: prompts.append( PromptUserResponse.model_validate( { - **self._to_prompt_model(prompt, db=db).model_dump(), + **self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ).model_dump(), "user": user.model_dump() if user else None, } ) @@ -329,11 +342,18 @@ class PromptsTable: items = query.all() + prompt_ids = [prompt.id for prompt, _ in items] + grants_map = AccessGrants.get_grants_by_resources("prompt", prompt_ids, db=db) + prompts = [] for prompt, user in items: prompts.append( PromptUserResponse( - **self._to_prompt_model(prompt, db=db).model_dump(), + **self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ).model_dump(), user=( UserResponse(**UserModel.model_validate(user).model_dump()) if user diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py index 1262830153..13e62c6f19 100644 --- a/backend/open_webui/models/skills.py +++ b/backend/open_webui/models/skills.py @@ -110,11 +110,20 @@ class SkillsTable: ) -> list[AccessGrantModel]: return AccessGrants.get_grants_by_resource("skill", skill_id, db=db) - def _to_skill_model(self, skill: Skill, db: Optional[Session] = None) -> SkillModel: + def _to_skill_model( + self, + skill: Skill, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, + ) -> SkillModel: skill_data = SkillModel.model_validate(skill).model_dump( exclude={"access_grants"} ) - skill_data["access_grants"] = self._get_access_grants(skill_data["id"], db=db) + skill_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(skill_data["id"], db=db) + ) return SkillModel.model_validate(skill_data) def insert_new_skill( @@ -172,9 +181,11 @@ class SkillsTable: all_skills = db.query(Skill).order_by(Skill.updated_at.desc()).all() user_ids = list(set(skill.user_id for skill in all_skills)) + skill_ids = [skill.id for skill in all_skills] users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = AccessGrants.get_grants_by_resources("skill", skill_ids, db=db) skills = [] for skill in all_skills: @@ -182,7 +193,11 @@ class SkillsTable: skills.append( SkillUserModel.model_validate( { - **self._to_skill_model(skill, db=db).model_dump(), + **self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ).model_dump(), "user": user.model_dump() if user else None, } ) @@ -267,11 +282,18 @@ class SkillsTable: items = query.all() + skill_ids = [skill.id for skill, _ in items] + grants_map = AccessGrants.get_grants_by_resources("skill", skill_ids, db=db) + skills = [] for skill, user in items: skills.append( SkillUserResponse( - **self._to_skill_model(skill, db=db).model_dump(), + **self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ).model_dump(), user=( UserResponse( **UserModel.model_validate(user).model_dump() diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index eaac4c385d..7b66bb7b40 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -100,9 +100,18 @@ class ToolsTable: ) -> list[AccessGrantModel]: return AccessGrants.get_grants_by_resource("tool", tool_id, db=db) - def _to_tool_model(self, tool: Tool, db: Optional[Session] = None) -> ToolModel: + def _to_tool_model( + self, + tool: Tool, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[Session] = None, + ) -> ToolModel: tool_data = ToolModel.model_validate(tool).model_dump(exclude={"access_grants"}) - tool_data["access_grants"] = self._get_access_grants(tool_data["id"], db=db) + tool_data["access_grants"] = ( + access_grants + if access_grants is not None + else self._get_access_grants(tool_data["id"], db=db) + ) return ToolModel.model_validate(tool_data) def insert_new_tool( @@ -152,9 +161,11 @@ class ToolsTable: all_tools = db.query(Tool).order_by(Tool.updated_at.desc()).all() user_ids = list(set(tool.user_id for tool in all_tools)) + tool_ids = [tool.id for tool in all_tools] users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = AccessGrants.get_grants_by_resources("tool", tool_ids, db=db) tools = [] for tool in all_tools: @@ -162,7 +173,11 @@ class ToolsTable: tools.append( ToolUserModel.model_validate( { - **self._to_tool_model(tool, db=db).model_dump(), + **self._to_tool_model( + tool, + access_grants=grants_map.get(tool.id, []), + db=db, + ).model_dump(), "user": user.model_dump() if user else None, } ) From 2a804541e017b9092a7923e18dc7bd760fc6f81f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 19 Feb 2026 16:57:32 -0600 Subject: [PATCH 34/70] refac Co-Authored-By: Leandro Ygor Loli <77518998+leandroyloli@users.noreply.github.com> --- backend/open_webui/routers/notes.py | 14 +++++++++++++- src/lib/components/notes/Notes.svelte | 20 ++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 8d1a66c4af..41bb65f55a 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -36,6 +36,14 @@ log = logging.getLogger(__name__) router = APIRouter() + +def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optional[dict]: + if not data: + return data + md = (data.get("content") or {}).get("md") or "" + return {"content": {"md": md[:max_length]}} + + ############################ # GetNotes ############################ @@ -82,6 +90,7 @@ async def get_notes( NoteUserResponse( **{ **note.model_dump(), + "data": _truncate_note_data(note.data), "user": UserResponse(**users[note.user_id].model_dump()), } ) @@ -135,7 +144,10 @@ async def search_notes( filter["user_id"] = user.id - return Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) + result = Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) + for note in result.items: + note.data = _truncate_note_data(note.data) + return result ############################ diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 2cf4ed3f8d..dd4d66be0b 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -31,7 +31,7 @@ import { goto } from '$app/navigation'; import { WEBUI_NAME, config, prompts as _prompts, user } from '$lib/stores'; - import { createNewNote, deleteNoteById, getNoteList, searchNotes } from '$lib/apis/notes'; + import { createNewNote, deleteNoteById, getNoteById, getNoteList, searchNotes } from '$lib/apis/notes'; import { capitalizeFirstLetter, copyToClipboard, getTimeRange } from '$lib/utils'; import { downloadPdf, createNoteHandler } from './utils'; @@ -73,15 +73,23 @@ let allItemsLoaded = false; const downloadHandler = async (type) => { + // Fetch the full note since the list response may not contain full content + const note = await getNoteById(localStorage.token, selectedNote.id).catch((error) => { + toast.error(`${error}`); + return null; + }); + + if (!note) return; + if (type === 'txt') { - const blob = new Blob([selectedNote.data.content.md], { type: 'text/plain' }); - saveAs(blob, `${selectedNote.title}.txt`); + const blob = new Blob([note.data.content.md], { type: 'text/plain' }); + saveAs(blob, `${note.title}.txt`); } else if (type === 'md') { - const blob = new Blob([selectedNote.data.content.md], { type: 'text/markdown' }); - saveAs(blob, `${selectedNote.title}.md`); + const blob = new Blob([note.data.content.md], { type: 'text/markdown' }); + saveAs(blob, `${note.title}.md`); } else if (type === 'pdf') { try { - await downloadPdf(selectedNote); + await downloadPdf(note); } catch (error) { toast.error(`${error}`); } From f5e5632afcc0c1072f71eba30ccec9fc42c1d1d4 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:01:24 -0500 Subject: [PATCH 35/70] fix(chat): prevent stuck drop overlay when dragging outside window in firefox (#21664) --- src/lib/components/chat/MessageInput.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 47bdc68674..a0cc2dd010 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -797,7 +797,10 @@ } }; - const onDragLeave = () => { + const onDragLeave = (e) => { + if (e.currentTarget.contains(e.relatedTarget)) { + return; + } dragged = false; }; From ae05586fdabf318d551b53ede41575355d3b9e2b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Feb 2026 16:49:43 -0600 Subject: [PATCH 36/70] refac: oauth session management --- backend/open_webui/env.py | 6 ++++++ backend/open_webui/models/oauth_sessions.py | 1 + backend/open_webui/utils/oauth.py | 21 +++++++++++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 02891d6810..b18ba46658 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -557,6 +557,12 @@ OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get( "OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY ) +# Maximum number of concurrent OAuth sessions per user per provider +# This prevents unbounded session growth while allowing multi-device usage +OAUTH_MAX_SESSIONS_PER_USER = int( + os.environ.get("OAUTH_MAX_SESSIONS_PER_USER", "10") +) + # Token Exchange Configuration # Allows external apps to exchange OAuth tokens for OpenWebUI tokens ENABLE_OAUTH_TOKEN_EXCHANGE = ( diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index 538937483f..fbcd763f34 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -188,6 +188,7 @@ class OAuthSessionTable: session = ( db.query(OAuthSession) .filter_by(provider=provider, user_id=user_id) + .order_by(OAuthSession.created_at.desc()) .first() ) if session: diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index c849eb25a8..2cb552860c 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -69,6 +69,7 @@ from open_webui.env import ( ENABLE_OAUTH_ID_TOKEN_COOKIE, ENABLE_OAUTH_EMAIL_FALLBACK, OAUTH_CLIENT_INFO_ENCRYPTION_KEY, + OAUTH_MAX_SESSIONS_PER_USER, ) from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token @@ -1679,11 +1680,23 @@ class OAuthManager: if "expires_in" in token and "expires_at" not in token: token["expires_at"] = datetime.now().timestamp() + token["expires_in"] - # Clean up any existing sessions for this user/provider first + # Enforce max concurrent sessions per user/provider to prevent + # unbounded growth while allowing multi-device usage sessions = OAuthSessions.get_sessions_by_user_id(user.id, db=db) - for session in sessions: - if session.provider == provider: - OAuthSessions.delete_session_by_id(session.id, db=db) + provider_sessions = sorted( + [ + for session in sessions + if session.provider == provider + ], + key=lambda session: session.created_at, + reverse=True, + ) + # Keep the newest sessions up to the limit, prune the rest + if len(provider_sessions) >= OAUTH_MAX_SESSIONS_PER_USER: + for old_session in provider_sessions[ + OAUTH_MAX_SESSIONS_PER_USER - 1 : + ]: + OAuthSessions.delete_session_by_id(old_session.id, db=db) session = OAuthSessions.create_session( user_id=user.id, From 092a358b3c2846a2d6ebbc68b16bd4dc9b04d8f1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 20 Feb 2026 16:55:06 -0600 Subject: [PATCH 37/70] refac --- backend/open_webui/utils/oauth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 2cb552860c..17dac27094 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1685,6 +1685,7 @@ class OAuthManager: sessions = OAuthSessions.get_sessions_by_user_id(user.id, db=db) provider_sessions = sorted( [ + session for session in sessions if session.provider == provider ], From d650c987ecda25946f798052db9fc0c2e8b51712 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:04:36 -0500 Subject: [PATCH 38/70] fix: resolve backend execution deadlock when syncing stats with cyclic chat history (#21681) --- backend/open_webui/utils/misc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index a52dca7a88..f162f906f6 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -91,8 +91,17 @@ def get_message_list(messages_map, message_id): # Reconstruct the chain by following the parentId links message_list = [] + visited_message_ids = set() while current_message: + message_id = current_message.get("id") + if message_id in visited_message_ids: + # Cycle detected, break to prevent infinite loop + break + + if message_id is not None: + visited_message_ids.add(message_id) + message_list.append(current_message) parent_id = current_message.get("parentId") # Use .get() for safety current_message = messages_map.get(parent_id) if parent_id else None From ac620118c1876dc15369f8a3236aecd99f164023 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:53:47 +0100 Subject: [PATCH 39/70] fix group search (#21691) --- src/lib/components/workspace/common/MemberSelector.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/workspace/common/MemberSelector.svelte b/src/lib/components/workspace/common/MemberSelector.svelte index d3a49c73f4..5cc46f33e4 100644 --- a/src/lib/components/workspace/common/MemberSelector.svelte +++ b/src/lib/components/workspace/common/MemberSelector.svelte @@ -191,7 +191,7 @@ - {#if users.length > 0} + {#if users.length > 0 || filteredGroups.length > 0}
From 4228bf71c45cce42208288d34f0e525cdbb63b08 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:54:47 +0100 Subject: [PATCH 40/70] fix: gate model default features on global config and user permissions (#21690) fix: gate model default features on global config and user permissions If you disabled code interpreter globally and in user permissions but enabled it as a default feature on a model, the code interpreter pill still appeared in the chat input. Same issue for web search and image generation. The setDefaults function in Chat.svelte activated model default features based solely on the model's capability flag, ignoring whether the feature was globally enabled or allowed by user permissions. Added the same global config and user permission checks already used by the integrations menu visibility and the features object sent to the backend. --- src/lib/components/chat/Chat.svelte | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 06b63b074d..9a58a89490 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -329,15 +329,27 @@ // Set Default Features if (model?.info?.meta?.defaultFeatureIds) { - if (model.info?.meta?.capabilities?.['image_generation']) { + if ( + model.info?.meta?.capabilities?.['image_generation'] && + $config?.features?.enable_image_generation && + ($user?.role === 'admin' || $user?.permissions?.features?.image_generation) + ) { imageGenerationEnabled = model.info.meta.defaultFeatureIds.includes('image_generation'); } - if (model.info?.meta?.capabilities?.['web_search']) { + if ( + model.info?.meta?.capabilities?.['web_search'] && + $config?.features?.enable_web_search && + ($user?.role === 'admin' || $user?.permissions?.features?.web_search) + ) { webSearchEnabled = model.info.meta.defaultFeatureIds.includes('web_search'); } - if (model.info?.meta?.capabilities?.['code_interpreter']) { + if ( + model.info?.meta?.capabilities?.['code_interpreter'] && + $config?.features?.enable_code_interpreter && + ($user?.role === 'admin' || $user?.permissions?.features?.code_interpreter) + ) { codeInterpreterEnabled = model.info.meta.defaultFeatureIds.includes('code_interpreter'); } } From a9312d25373d3aa161788598f87180b8db11c5b6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:15:32 -0600 Subject: [PATCH 41/70] refac --- backend/open_webui/utils/anthropic.py | 107 ++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py index ba24984400..f567ad562e 100644 --- a/backend/open_webui/utils/anthropic.py +++ b/backend/open_webui/utils/anthropic.py @@ -177,6 +177,10 @@ def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict: tool_text_parts.append(tc.get("text", "")) tool_content = "\n".join(tool_text_parts) + # Propagate error status if present + if block.get("is_error"): + tool_content = f"Error: {tool_content}" + messages.append({ "role": "tool", "tool_call_id": block.get("tool_use_id", ""), @@ -324,15 +328,27 @@ async def openai_stream_to_anthropic_stream( OpenAI sends: data: {"choices": [{"delta": {"content": "..."}}]} Anthropic sends: event: content_block_delta\\ndata: {"type": "content_block_delta", ...} + + Handles text content, tool calls, and mixed content with proper + multi-block indexing as required by Anthropic's streaming protocol. """ import uuid as _uuid msg_id = f"msg_{_uuid.uuid4().hex[:24]}" input_tokens = 0 output_tokens = 0 - block_started = False stop_reason = "end_turn" + # Track content blocks with a running index. + # Each text block or tool_use block gets its own index. + current_block_index = 0 + text_block_open = False + + # Track tool call state: maps OpenAI tool_call index -> Anthropic block index + # This allows handling multiple concurrent tool calls. + tool_call_blocks = {} # {openai_tc_index: anthropic_block_index} + tool_call_started = {} # {openai_tc_index: bool} + # Emit message_start message_start = { "type": "message_start", @@ -375,7 +391,9 @@ async def openai_stream_to_anthropic_stream( if not choices: # Check for usage in the final chunk if data.get("usage"): - input_tokens = data["usage"].get("prompt_tokens", input_tokens) + input_tokens = data["usage"].get( + "prompt_tokens", input_tokens + ) output_tokens = data["usage"].get( "completion_tokens", output_tokens ) @@ -386,38 +404,90 @@ async def openai_stream_to_anthropic_stream( # Update usage if present if data.get("usage"): - input_tokens = data["usage"].get("prompt_tokens", input_tokens) + input_tokens = data["usage"].get( + "prompt_tokens", input_tokens + ) output_tokens = data["usage"].get( "completion_tokens", output_tokens ) + # --- Handle text content --- content = delta.get("content") if content is not None: - if not block_started: - # Start the content block + if not text_block_open: + # Start a new text content block block_start = { "type": "content_block_start", - "index": 0, + "index": current_block_index, "content_block": {"type": "text", "text": ""}, } yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n".encode() - block_started = True + text_block_open = True - # Send content delta + # Send text delta block_delta = { "type": "content_block_delta", - "index": 0, + "index": current_block_index, "delta": {"type": "text_delta", "text": content}, } yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n".encode() - # Handle tool calls in streaming + # --- Handle tool calls --- tool_calls = delta.get("tool_calls") if tool_calls: - # Tool calls in streaming are more complex; - # for now we pass through the text content - pass + # Close text block if one is open (text comes before tools) + if text_block_open: + block_stop = { + "type": "content_block_stop", + "index": current_block_index, + } + yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode() + text_block_open = False + current_block_index += 1 + for tc in tool_calls: + tc_index = tc.get("index", 0) + + if tc_index not in tool_call_started: + # First time seeing this tool call — emit content_block_start + tool_call_blocks[tc_index] = current_block_index + tool_call_started[tc_index] = True + + # Extract tool call ID and name from the first chunk + tc_id = tc.get( + "id", f"toolu_{_uuid.uuid4().hex[:24]}" + ) + tc_name = tc.get("function", {}).get("name", "") + + block_start = { + "type": "content_block_start", + "index": current_block_index, + "content_block": { + "type": "tool_use", + "id": tc_id, + "name": tc_name, + "input": {}, + }, + } + yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n".encode() + current_block_index += 1 + + # Emit argument chunks as input_json_delta + args_chunk = tc.get("function", {}).get( + "arguments", "" + ) + if args_chunk: + block_delta = { + "type": "content_block_delta", + "index": tool_call_blocks[tc_index], + "delta": { + "type": "input_json_delta", + "partial_json": args_chunk, + }, + } + yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n".encode() + + # --- Handle finish reason --- if finish_reason is not None: stop_reason_map = { "stop": "end_turn", @@ -429,9 +499,14 @@ async def openai_stream_to_anthropic_stream( except Exception as e: log.error(f"Error in Anthropic stream conversion: {e}") - # Close content block if one was started - if block_started: - block_stop = {"type": "content_block_stop", "index": 0} + # Close any open text block + if text_block_open: + block_stop = {"type": "content_block_stop", "index": current_block_index} + yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode() + + # Close any open tool call blocks + for tc_index, block_index in tool_call_blocks.items(): + block_stop = {"type": "content_block_stop", "index": block_index} yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode() # Emit message_delta with stop reason From b1dc58ddb7b420caeefe6ec066666dc1fb407d8e Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:18:37 +0100 Subject: [PATCH 42/70] feat: add sortable columns to groups admin panel (#21692) * feat: add sortable columns to groups admin panel Make the Group and Users column headers in the admin groups list clickable to sort groups alphabetically by name or numerically by member count. Clicking a column toggles ascending/descending order, indicated by a chevron icon. When no sort is active, the default API order (by updated_at) is preserved. * Update Groups.svelte * Update Groups.svelte --- src/lib/components/admin/Users/Groups.svelte | 73 ++++++++++++++++---- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/lib/components/admin/Users/Groups.svelte b/src/lib/components/admin/Users/Groups.svelte index fe0f7477eb..39304e4430 100644 --- a/src/lib/components/admin/Users/Groups.svelte +++ b/src/lib/components/admin/Users/Groups.svelte @@ -21,6 +21,8 @@ import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte'; import EditGroupModal from './Groups/EditGroupModal.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte'; + import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; + import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import GroupItem from './Groups/GroupItem.svelte'; import { createNewGroup, getGroups } from '$lib/apis/groups'; import { @@ -34,19 +36,42 @@ let loaded = false; let groups = []; - let filteredGroups; - $: filteredGroups = groups.filter((user) => { - if (search === '') { - return true; + let query = ''; + let orderBy = ''; + let direction = 'asc'; + + const setSortKey = (key) => { + if (orderBy === key) { + direction = direction === 'asc' ? 'desc' : 'asc'; } else { - let name = user.name.toLowerCase(); - const query = search.toLowerCase(); - return name.includes(query); + orderBy = key; + direction = 'asc'; } - }); + }; + + $: filteredGroups = groups + .filter((group) => { + if (query === '') { + return true; + } else { + let name = group.name.toLowerCase(); + const q = query.toLowerCase(); + return name.includes(q); + } + }) + .sort((a, b) => { + if (orderBy === 'name') { + const cmp = a.name.localeCompare(b.name); + return direction === 'asc' ? cmp : -cmp; + } else if (orderBy === 'members') { + const cmp = (a.member_count ?? 0) - (b.member_count ?? 0); + return direction === 'asc' ? cmp : -cmp; + } + return 0; + }); + - let search = ''; let defaultPermissions = {}; let showAddGroupModal = false; @@ -124,7 +149,7 @@
@@ -171,9 +196,33 @@ {:else}
-
{$i18n.t('Group')}
+ -
{$i18n.t('Users')}
+

From 10c13b686c034a53ff78fa4d8afd3d4b3c219fb8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:19:28 -0600 Subject: [PATCH 43/70] refac --- .../test/apps/webui/routers/test_prompts.py | 91 ------------------- src/lib/components/workspace/Prompts.svelte | 6 +- 2 files changed, 3 insertions(+), 94 deletions(-) delete mode 100644 backend/open_webui/test/apps/webui/routers/test_prompts.py diff --git a/backend/open_webui/test/apps/webui/routers/test_prompts.py b/backend/open_webui/test/apps/webui/routers/test_prompts.py deleted file mode 100644 index d91bf77dc5..0000000000 --- a/backend/open_webui/test/apps/webui/routers/test_prompts.py +++ /dev/null @@ -1,91 +0,0 @@ -from test.util.abstract_integration_test import AbstractPostgresTest -from test.util.mock_user import mock_webui_user - - -class TestPrompts(AbstractPostgresTest): - BASE_PATH = "/api/v1/prompts" - - def test_prompts(self): - # Get all prompts - with mock_webui_user(id="2"): - response = self.fast_api_client.get(self.create_url("/")) - assert response.status_code == 200 - assert len(response.json()) == 0 - - # Create a two new prompts - with mock_webui_user(id="2"): - response = self.fast_api_client.post( - self.create_url("/create"), - json={ - "command": "/my-command", - "title": "Hello World", - "content": "description", - }, - ) - assert response.status_code == 200 - with mock_webui_user(id="3"): - response = self.fast_api_client.post( - self.create_url("/create"), - json={ - "command": "/my-command2", - "title": "Hello World 2", - "content": "description 2", - }, - ) - assert response.status_code == 200 - - # Get all prompts - with mock_webui_user(id="2"): - response = self.fast_api_client.get(self.create_url("/")) - assert response.status_code == 200 - assert len(response.json()) == 2 - - # Get prompt by command - with mock_webui_user(id="2"): - response = self.fast_api_client.get(self.create_url("/command/my-command")) - assert response.status_code == 200 - data = response.json() - assert data["command"] == "/my-command" - assert data["title"] == "Hello World" - assert data["content"] == "description" - assert data["user_id"] == "2" - - # Update prompt - with mock_webui_user(id="2"): - response = self.fast_api_client.post( - self.create_url("/command/my-command2/update"), - json={ - "command": "irrelevant for request", - "title": "Hello World Updated", - "content": "description Updated", - }, - ) - assert response.status_code == 200 - data = response.json() - assert data["command"] == "/my-command2" - assert data["title"] == "Hello World Updated" - assert data["content"] == "description Updated" - assert data["user_id"] == "3" - - # Get prompt by command - with mock_webui_user(id="2"): - response = self.fast_api_client.get(self.create_url("/command/my-command2")) - assert response.status_code == 200 - data = response.json() - assert data["command"] == "/my-command2" - assert data["title"] == "Hello World Updated" - assert data["content"] == "description Updated" - assert data["user_id"] == "3" - - # Delete prompt - with mock_webui_user(id="2"): - response = self.fast_api_client.delete( - self.create_url("/command/my-command/delete") - ) - assert response.status_code == 200 - - # Get all prompts - with mock_webui_user(id="2"): - response = self.fast_api_client.get(self.create_url("/")) - assert response.status_code == 200 - assert len(response.json()) == 1 diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 726571d8a3..4eab4ef645 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -248,11 +248,11 @@ for (const prompt of savedPrompts) { await createNewPrompt(localStorage.token, { - command: prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command, - title: prompt.title, + command: prompt.command, + name: prompt.name, content: prompt.content }).catch((error) => { - toast.error(`${error}`); + toast.error(typeof error === 'string' ? error : JSON.stringify(error)); return null; }); } From 8265422ba0660e7ba2192eb19efd70f8be652748 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:22:20 -0600 Subject: [PATCH 44/70] refac --- src/lib/components/admin/Functions.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index 8cd283381a..57dba96af2 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -83,7 +83,7 @@ } const setFilteredItems = () => { - filteredItems = functions + filteredItems = (functions ?? []) .filter( (f) => (selectedType !== '' ? f.type === selectedType : true) && From 6d8a6e6d8bb81c1ad6858d44a6c97bc195b3873e Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:22:50 -0500 Subject: [PATCH 45/70] fix(model-selector): resolve virtual scroll bug when typing quickly (#21659) --- src/lib/components/chat/ModelSelector/Selector.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index 529bc44a44..b774f24217 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -159,9 +159,7 @@ }) ).filter((item) => !(item.model?.info?.meta?.hidden ?? false)); - $: if (selectedTag || selectedConnectionType) { - resetView(); - } else { + $: if (selectedTag !== undefined || selectedConnectionType !== undefined || searchValue !== undefined) { resetView(); } From 3242dad8aef2c747aa4f3a20f590d9fa4efaf118 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:29:40 -0600 Subject: [PATCH 46/70] refac --- src/lib/components/workspace/Models/ToolsSelector.svelte | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/components/workspace/Models/ToolsSelector.svelte b/src/lib/components/workspace/Models/ToolsSelector.svelte index d002ac6296..bbc484f907 100644 --- a/src/lib/components/workspace/Models/ToolsSelector.svelte +++ b/src/lib/components/workspace/Models/ToolsSelector.svelte @@ -12,13 +12,7 @@ const i18n = getContext('i18n'); onMount(() => { - // Filter out OAuth2.1 MCP tools - these cannot be set as model defaults - // because OAuth authentication is per-user and would fail for users - // who haven't completed the OAuth flow. - // The `authenticated` field only exists on OAuth2.1 MCP tools. - const availableTools = tools.filter((tool) => tool.authenticated === undefined); - - _tools = availableTools.reduce((acc, tool) => { + _tools = tools.reduce((acc, tool) => { acc[tool.id] = { ...tool, selected: selectedToolIds.includes(tool.id) From 5522b91c32703eb545e8990f3a317914eca7a125 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:31:35 -0500 Subject: [PATCH 47/70] fix(ui): align profile dropdown items and prevent phantom synthetic drag clicks (#21699) --- .../components/layout/Sidebar/UserMenu.svelte | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte index 502d09b344..c5eca5a60b 100644 --- a/src/lib/components/layout/Sidebar/UserMenu.svelte +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -201,7 +201,7 @@ {/if} { show = false; @@ -220,7 +220,7 @@ { show = false; @@ -243,7 +243,8 @@ { show = false; if ($mobile) { @@ -260,7 +261,8 @@ { show = false; if ($mobile) { @@ -284,36 +286,42 @@ {#if $user?.role === 'admin'} { show = false; }} - href="https://docs.openwebui.com" > - -
{$i18n.t('Documentation')}
+
+ +
+
{$i18n.t('Documentation')}
{ show = false; }} - href="https://github.com/open-webui/open-webui/releases" > - -
{$i18n.t('Releases')}
+
+ +
+
{$i18n.t('Releases')}
{/if} { show = false; @@ -325,15 +333,17 @@ } }} > - -
{$i18n.t('Keyboard shortcuts')}
+
+ +
+
{$i18n.t('Keyboard shortcuts')}
{/if}
{ const res = await userSignOut(); user.set(null); From 5d4547f934b6fbe751bb2041f9597fe11ddf8e43 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:33:48 -0600 Subject: [PATCH 48/70] enh: RAG_EMBEDDING_CONCURRENT_REQUESTS --- backend/open_webui/config.py | 6 ++++ backend/open_webui/main.py | 3 ++ backend/open_webui/retrieval/utils.py | 21 +++++++++++--- backend/open_webui/routers/retrieval.py | 13 +++++++-- .../admin/Settings/Documents.svelte | 28 ++++++++++++++++++- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 9ec08cc4c3..af7234bb2c 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2903,6 +2903,12 @@ ENABLE_ASYNC_EMBEDDING = PersistentConfig( os.environ.get("ENABLE_ASYNC_EMBEDDING", "True").lower() == "true", ) +RAG_EMBEDDING_CONCURRENT_REQUESTS = PersistentConfig( + "RAG_EMBEDDING_CONCURRENT_REQUESTS", + "rag.embedding_concurrent_requests", + int(os.getenv("RAG_EMBEDDING_CONCURRENT_REQUESTS", "0")), +) + RAG_EMBEDDING_QUERY_PREFIX = os.environ.get("RAG_EMBEDDING_QUERY_PREFIX", None) RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get("RAG_EMBEDDING_CONTENT_PREFIX", None) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 3f1536244d..693a611266 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -240,6 +240,7 @@ from open_webui.config import ( RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_BATCH_SIZE, ENABLE_ASYNC_EMBEDDING, + RAG_EMBEDDING_CONCURRENT_REQUESTS, RAG_TOP_K, RAG_TOP_K_RERANKER, RAG_RELEVANCE_THRESHOLD, @@ -980,6 +981,7 @@ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING +app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = RAG_EMBEDDING_CONCURRENT_REQUESTS app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL @@ -1133,6 +1135,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( else None ), enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) app.state.RERANKING_FUNCTION = get_reranking_function( diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 536ddb4c32..33d3779a0c 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -803,6 +803,7 @@ def get_embedding_function( embedding_batch_size, azure_api_version=None, enable_async=True, + concurrent_requests=0, ) -> Awaitable: if embedding_engine == "": # Sentence transformers: CPU-bound sync operation @@ -844,10 +845,22 @@ def get_embedding_function( log.debug( f"generate_multiple_async: Processing {len(batches)} batches in parallel" ) - # Execute all batches in parallel - tasks = [ - embedding_function(batch, prefix=prefix, user=user) - for batch in batches + # Use semaphore to limit concurrent embedding API requests + # 0 = unlimited (no semaphore) + if concurrent_requests: + semaphore = asyncio.Semaphore(concurrent_requests) + + async def generate_batch_with_semaphore(batch): + async with semaphore: + return await embedding_function( + batch, prefix=prefix, user=user + ) + + tasks = [generate_batch_with_semaphore(batch) for batch in batches] + else: + tasks = [ + embedding_function(batch, prefix=prefix, user=user) + for batch in batches ] batch_results = await asyncio.gather(*tasks) else: diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index b16bc022ee..9630462613 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -270,6 +270,7 @@ async def get_status(request: Request): "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, + "RAG_EMBEDDING_CONCURRENT_REQUESTS": request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, } @@ -281,6 +282,7 @@ async def get_embedding_config(request: Request, user=Depends(get_admin_user)): "RAG_EMBEDDING_MODEL": request.app.state.config.RAG_EMBEDDING_MODEL, "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, + "RAG_EMBEDDING_CONCURRENT_REQUESTS": request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, "openai_config": { "url": request.app.state.config.RAG_OPENAI_API_BASE_URL, "key": request.app.state.config.RAG_OPENAI_API_KEY, @@ -321,6 +323,7 @@ class EmbeddingModelUpdateForm(BaseModel): RAG_EMBEDDING_MODEL: str RAG_EMBEDDING_BATCH_SIZE: Optional[int] = 1 ENABLE_ASYNC_EMBEDDING: Optional[bool] = True + RAG_EMBEDDING_CONCURRENT_REQUESTS: Optional[int] = 0 def unload_embedding_model(request: Request): @@ -355,6 +358,9 @@ async def update_embedding_config( request.app.state.config.ENABLE_ASYNC_EMBEDDING = ( form_data.ENABLE_ASYNC_EMBEDDING ) + request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = ( + form_data.RAG_EMBEDDING_CONCURRENT_REQUESTS + ) if request.app.state.config.RAG_EMBEDDING_ENGINE in [ "ollama", @@ -422,6 +428,7 @@ async def update_embedding_config( else None ), enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) return { @@ -430,6 +437,7 @@ async def update_embedding_config( "RAG_EMBEDDING_MODEL": request.app.state.config.RAG_EMBEDDING_MODEL, "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, + "RAG_EMBEDDING_CONCURRENT_REQUESTS": request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, "openai_config": { "url": request.app.state.config.RAG_OPENAI_API_BASE_URL, "key": request.app.state.config.RAG_OPENAI_API_KEY, @@ -1589,6 +1597,7 @@ def save_docs_to_vector_db( else None ), enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) # Run async embedding in sync context using the main event loop @@ -2329,7 +2338,7 @@ async def process_web_search( # Limited concurrency with semaphore semaphore = asyncio.Semaphore(concurrent_limit) - async def search_with_limit(query): + async def search_query_with_semaphore(query): async with semaphore: return await run_in_threadpool( search_web, @@ -2339,7 +2348,7 @@ async def process_web_search( user, ) - search_tasks = [search_with_limit(query) for query in form_data.queries] + search_tasks = [search_query_with_semaphore(query) for query in form_data.queries] else: # Unlimited parallel execution (previous behavior) search_tasks = [ diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 3f4434084b..ca0d86ebbd 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -42,6 +42,7 @@ let RAG_EMBEDDING_MODEL = ''; let RAG_EMBEDDING_BATCH_SIZE = 1; let ENABLE_ASYNC_EMBEDDING = true; + let RAG_EMBEDDING_CONCURRENT_REQUESTS = 0; let rerankingModel = ''; @@ -104,7 +105,8 @@ RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_MODEL, RAG_EMBEDDING_BATCH_SIZE, - ENABLE_ASYNC_EMBEDDING + ENABLE_ASYNC_EMBEDDING, + RAG_EMBEDDING_CONCURRENT_REQUESTS }); updateEmbeddingModelLoading = true; @@ -113,6 +115,7 @@ RAG_EMBEDDING_MODEL: RAG_EMBEDDING_MODEL, RAG_EMBEDDING_BATCH_SIZE: RAG_EMBEDDING_BATCH_SIZE, ENABLE_ASYNC_EMBEDDING: ENABLE_ASYNC_EMBEDDING, + RAG_EMBEDDING_CONCURRENT_REQUESTS: RAG_EMBEDDING_CONCURRENT_REQUESTS, ollama_config: { key: OllamaKey, url: OllamaUrl @@ -241,6 +244,7 @@ RAG_EMBEDDING_MODEL = embeddingConfig.RAG_EMBEDDING_MODEL; RAG_EMBEDDING_BATCH_SIZE = embeddingConfig.RAG_EMBEDDING_BATCH_SIZE ?? 1; ENABLE_ASYNC_EMBEDDING = embeddingConfig.ENABLE_ASYNC_EMBEDDING ?? true; + RAG_EMBEDDING_CONCURRENT_REQUESTS = embeddingConfig.RAG_EMBEDDING_CONCURRENT_REQUESTS ?? 0; OpenAIKey = embeddingConfig.openai_config.key; OpenAIUrl = embeddingConfig.openai_config.url; @@ -1043,6 +1047,28 @@
+ +
+
+ + {$i18n.t('Embedding Concurrent Requests')} + +
+
+ +
+
{/if} From 9f7dd31e128955430f346f3cb96b9aaa012246c1 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:35:32 +0100 Subject: [PATCH 49/70] feat: scroll to rich ui once rendered (#21698) * Update Chat.svelte * Update Chat.svelte --- src/lib/components/chat/Chat.svelte | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 9a58a89490..7868564dc6 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -427,6 +427,15 @@ message.files = data.files; } else if (type === 'chat:message:embeds' || type === 'embeds') { message.embeds = data.embeds; + + // Auto-scroll to the embed once it's rendered in the DOM + await tick(); + setTimeout(() => { + const embedEl = document.getElementById(`${event.message_id}-embeds-0`); + if (embedEl) { + embedEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 100); } else if (type === 'chat:message:error') { message.error = data.error; } else if (type === 'chat:message:follow_ups') { From 7e42d727e8dfdf9ac3251eb713d2537a2600632a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:39:28 -0600 Subject: [PATCH 50/70] refac --- src/lib/components/chat/Chat.svelte | 2 +- src/lib/components/chat/Messages/ResponseMessage.svelte | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 7868564dc6..d4418a1d0e 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -431,7 +431,7 @@ // Auto-scroll to the embed once it's rendered in the DOM await tick(); setTimeout(() => { - const embedEl = document.getElementById(`${event.message_id}-embeds-0`); + const embedEl = document.getElementById(`${event.message_id}-embeds-container`); if (embedEl) { embedEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index ae55ca3495..fa0b967e00 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -679,7 +679,10 @@ {/if} {#if message?.embeds && message.embeds.length > 0} -
+
{#each message.embeds as embed, idx}
Date: Sat, 21 Feb 2026 15:48:51 -0500 Subject: [PATCH 51/70] fix(backend): catch 404 http exceptions before generalized exception block in files router (#21687) --- backend/open_webui/routers/files.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index ad667533dd..b9c9c73e8e 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -690,6 +690,8 @@ async def get_file_content_by_id( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) + except HTTPException as e: + raise e except Exception as e: log.exception(e) log.error("Error getting file content") @@ -741,6 +743,8 @@ async def get_html_file_content_by_id( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) + except HTTPException as e: + raise e except Exception as e: log.exception(e) log.error("Error getting file content") From d247adb60c06d4cfd9734cd244262f624cf9bcaa Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:49:19 +0100 Subject: [PATCH 52/70] feat: add citation sources for fetch_url tool results (#21669) feat: add citation sources for fetch_url tool results URL fetches now produce clickable citation sources in the UI, matching the existing behavior of search_web and knowledge file tools. When a model calls fetch_url during native tool calling, the fetched URL appears as a citable source with a content preview, giving users full transparency into what pages the model referenced. --- backend/open_webui/utils/middleware.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index ec7af7733b..c2b2b07cb1 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -171,7 +171,10 @@ def get_citation_source_from_tool_result( Returns a list of sources (usually one, but query_knowledge_files may return multiple). """ try: - tool_result = json.loads(tool_result) + try: + tool_result = json.loads(tool_result) + except (json.JSONDecodeError, TypeError): + pass # keep tool_result as-is (e.g. fetch_url returns plain text) if isinstance(tool_result, dict) and "error" in tool_result: return [] @@ -232,6 +235,25 @@ def get_citation_source_from_tool_result( } ] + elif tool_name == "fetch_url": + url = tool_params.get("url", "") + content = tool_result if isinstance(tool_result, str) else str(tool_result) + snippet = content[:500] + ("..." if len(content) > 500 else "") + + return [ + { + "source": {"name": url or "fetch_url", "id": url or "fetch_url"}, + "document": [snippet], + "metadata": [ + { + "source": url, + "name": url, + "url": url, + } + ], + } + ] + elif tool_name == "query_knowledge_files": chunks = tool_result @@ -4102,6 +4124,7 @@ async def streaming_chat_response_handler(response, ctx): tool_function_name in [ "search_web", + "fetch_url", "view_knowledge_file", "query_knowledge_files", ] From 5759917f54e0c655ad0e5ee33ff6ea8dfbc276dd Mon Sep 17 00:00:00 2001 From: lazariv Date: Sat, 21 Feb 2026 21:51:56 +0100 Subject: [PATCH 53/70] feat: Adding You.com as a web search provider (#21599) * Add ydc.py provider implementation * Add PersistentConfig entry for you.com * Add Youcom search function import * Update you.com configuration * Add you.com as a web search engine option in frontend * Add YOUCOM_API_KEY to main.py --- backend/open_webui/config.py | 6 ++ backend/open_webui/main.py | 2 + backend/open_webui/retrieval/web/ydc.py | 73 +++++++++++++++++++ backend/open_webui/routers/retrieval.py | 14 ++++ .../admin/Settings/WebSearch.svelte | 16 +++- 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 backend/open_webui/retrieval/web/ydc.py diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index af7234bb2c..00fc1b36f3 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -3491,6 +3491,12 @@ YANDEX_WEB_SEARCH_CONFIG = PersistentConfig( os.environ.get("YANDEX_WEB_SEARCH_CONFIG", ""), ) +YOUCOM_API_KEY = PersistentConfig( + "YOUCOM_API_KEY", + "rag.web.search.youcom_api_key", + os.environ.get("YOUCOM_API_KEY", ""), +) + #################################### # Images #################################### diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 693a611266..57660fe9e4 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -360,6 +360,7 @@ from open_webui.config import ( YANDEX_WEB_SEARCH_URL, YANDEX_WEB_SEARCH_API_KEY, YANDEX_WEB_SEARCH_CONFIG, + YOUCOM_API_KEY, # WebUI WEBUI_AUTH, WEBUI_NAME, @@ -1067,6 +1068,7 @@ app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG +app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL diff --git a/backend/open_webui/retrieval/web/ydc.py b/backend/open_webui/retrieval/web/ydc.py new file mode 100644 index 0000000000..21d725a895 --- /dev/null +++ b/backend/open_webui/retrieval/web/ydc.py @@ -0,0 +1,73 @@ +import logging +from typing import Optional, List + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_youcom( + api_key: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + language: str = "EN", +) -> List[SearchResult]: + """Search using You.com's YDC Index API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A You.com API key + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (list[str], optional): Domain filter list + language (str): Language code for search results (default: "EN") + """ + url = "https://ydc-index.io/v1/search" + headers = { + "Accept": "application/json", + "X-API-KEY": api_key, + } + params = { + "query": query, + "count": count, + "language": language, + } + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = json_response.get("results", {}).get("web", []) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result["url"], + title=result.get("title"), + snippet=_build_snippet(result), + ) + for result in results[:count] + ] + + +def _build_snippet(result: dict) -> str: + """Combine the description and snippets list into a single string. + + The You.com API returns a short ``description`` plus a ``snippets`` + list with richer passages. Merging them gives downstream retrieval + (embedding, BM25, bypass-loader context) the most content to work with. + """ + parts: list[str] = [] + + description = result.get("description") + if description: + parts.append(description) + + snippets = result.get("snippets") + if snippets and isinstance(snippets, list): + parts.extend(snippets) + + return "\n\n".join(parts) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 9630462613..2c8de8c8df 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -77,6 +77,7 @@ from open_webui.retrieval.web.sougou import search_sougou from open_webui.retrieval.web.firecrawl import search_firecrawl from open_webui.retrieval.web.external import search_external from open_webui.retrieval.web.yandex import search_yandex +from open_webui.retrieval.web.ydc import search_youcom from open_webui.retrieval.utils import ( get_content_from_url, @@ -591,6 +592,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "YANDEX_WEB_SEARCH_URL": request.app.state.config.YANDEX_WEB_SEARCH_URL, "YANDEX_WEB_SEARCH_API_KEY": request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, "YANDEX_WEB_SEARCH_CONFIG": request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + "YOUCOM_API_KEY": request.app.state.config.YOUCOM_API_KEY, }, } @@ -657,6 +659,7 @@ class WebConfig(BaseModel): YANDEX_WEB_SEARCH_URL: Optional[str] = None YANDEX_WEB_SEARCH_API_KEY: Optional[str] = None YANDEX_WEB_SEARCH_CONFIG: Optional[str] = None + YOUCOM_API_KEY: Optional[str] = None class ConfigForm(BaseModel): @@ -1213,6 +1216,9 @@ async def update_rag_config( request.app.state.config.YANDEX_WEB_SEARCH_CONFIG = ( form_data.web.YANDEX_WEB_SEARCH_CONFIG ) + request.app.state.config.YOUCOM_API_KEY = ( + form_data.web.YOUCOM_API_KEY + ) return { "status": True, @@ -1340,6 +1346,7 @@ async def update_rag_config( "YANDEX_WEB_SEARCH_URL": request.app.state.config.YANDEX_WEB_SEARCH_URL, "YANDEX_WEB_SEARCH_API_KEY": request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, "YANDEX_WEB_SEARCH_CONFIG": request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + "YOUCOM_API_KEY": request.app.state.config.YOUCOM_API_KEY, }, } @@ -2299,6 +2306,13 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, user=user, ) + elif engine == "youcom": + return search_youcom( + request.app.state.config.YOUCOM_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) else: raise Exception("No search engine API key found in environment variables") diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 4778b69ec5..4d5d167c98 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -37,7 +37,8 @@ 'sougou', 'firecrawl', 'external', - 'yandex' + 'yandex', + 'youcom' ]; let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'external']; @@ -786,6 +787,19 @@
+ {:else if webConfig.WEB_SEARCH_ENGINE === 'youcom'} +
+
+
+ {$i18n.t('You.com API Key')} +
+ + +
+
{/if} {#if webConfig.WEB_SEARCH_ENGINE === 'duckduckgo'} From 45e23c3ad0794d848c768f9f5f468776a68685ee Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:53:31 +0100 Subject: [PATCH 54/70] perf: eliminate 2 redundant full chat deserialization on every message send (#21596) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: eliminate 2 redundant full chat deserialization on every message send (#162) Problem: Every message send triggered get_chat_by_id_and_user_id which loads the entire Chat row — including the potentially massive JSON blob containing the full conversation history — even when the caller only needed a simple yes/no ownership check or a single column value. Two call sites in the message-send hot path were doing this: 1. main.py ownership verification: loaded the entire chat object including all message history JSON, then checked `if chat is None`. The JSON blob was immediately discarded — only the existence of the row mattered. 2. middleware.py folder check: loaded the entire chat object including all message history JSON, then read only `chat.folder_id` — a plain column on the chat table that requires zero JSON parsing. Fix: - Added `chat_exists_by_id_and_user_id()`: uses SQL EXISTS subquery which returns a boolean without loading any row data. The database can satisfy this from the primary key index alone. - Added `get_chat_folder_id()`: queries only the `folder_id` column via `db.query(Chat.folder_id)`, which tells SQLAlchemy to SELECT only that single column instead of the entire row. Both new methods preserve the same error handling semantics (return False/None on exception) and user_id filtering (ownership check) as the original get_chat_by_id_and_user_id. Impact: - Best case (typical): eliminates deserializing 2 full chat JSON blobs per message send. For long conversations (hundreds of messages with tool calls, images, file attachments), this blob can be multiple megabytes. - Worst case: no regression — the new queries are strictly cheaper than the old ones (less data transferred, less Python object construction, no Pydantic model_validate overhead). - The 3 remaining full chat loads in process_chat_payload (load_messages_from_db, add_file_context, chat_image_generation_handler) are left untouched as they genuinely need the full history and require separate analysis. * Address maintainer feedback: rename method and inline call (#166) - Rename chat_exists_by_id_and_user_id -> is_chat_owner - Remove intermediate chat_owned variable; call is_chat_owner directly in if condition --- backend/open_webui/main.py | 6 ++--- backend/open_webui/models/chats.py | 35 ++++++++++++++++++++++++++ backend/open_webui/utils/middleware.py | 7 +++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 57660fe9e4..b95166a9fd 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1760,9 +1760,9 @@ async def chat_completion( "local:" ): # temporary chats are not stored - # Verify chat ownership - chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id) - if chat is None and user.role != "admin": # admins can access any chat + # Verify chat ownership — lightweight EXISTS check avoids + # deserializing the full chat JSON blob just to confirm the row exists + if not Chats.is_chat_owner(metadata["chat_id"], user.id) and user.role != "admin": # admins can access any chat raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.DEFAULT(), diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 42c8cb4508..84c67a5b15 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -974,6 +974,41 @@ class ChatTable: except Exception: return None + def is_chat_owner( + self, id: str, user_id: str, db: Optional[Session] = None + ) -> bool: + """ + Lightweight ownership check — uses EXISTS subquery instead of loading + the full Chat row (which includes the potentially large JSON blob). + """ + try: + with get_db_context(db) as db: + return db.query( + exists().where( + and_(Chat.id == id, Chat.user_id == user_id) + ) + ).scalar() + except Exception: + return False + + def get_chat_folder_id( + self, id: str, user_id: str, db: Optional[Session] = None + ) -> Optional[str]: + """ + Fetch only the folder_id column for a chat, without loading the full + JSON blob. Returns None if chat doesn't exist or doesn't belong to user. + """ + try: + with get_db_context(db) as db: + result = ( + db.query(Chat.folder_id) + .filter_by(id=id, user_id=user_id) + .first() + ) + return result[0] if result else None + except Exception: + return None + def get_chats( self, skip: int = 0, limit: int = 50, db: Optional[Session] = None ) -> list[ChatModel]: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index c2b2b07cb1..218deed17e 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2078,11 +2078,12 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Folder "Project" handling # Check if the request has chat_id and is inside of a folder + # Uses lightweight column query — only fetches folder_id, not the full chat JSON blob chat_id = metadata.get("chat_id", None) if chat_id and user: - chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id) - if chat and chat.folder_id: - folder = Folders.get_folder_by_id_and_user_id(chat.folder_id, user.id) + folder_id = Chats.get_chat_folder_id(chat_id, user.id) + if folder_id: + folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id) if folder and folder.data: if "system_prompt" in folder.data: From 35598b8017557258b8c9ee3469d320adb0140751 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 14:56:19 -0600 Subject: [PATCH 55/70] enh: ENABLE_ADMIN_ANALYTICS --- backend/open_webui/config.py | 4 ++++ backend/open_webui/main.py | 5 ++++- src/lib/stores/index.ts | 1 + src/routes/(app)/admin/+layout.svelte | 4 +++- src/routes/(app)/admin/analytics/+page.svelte | 13 ++++++++++++- src/routes/(app)/admin/analytics/[tab]/+page.svelte | 13 ++++++++++++- 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 00fc1b36f3..9c458cba17 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1678,6 +1678,10 @@ ENABLE_ADMIN_CHAT_ACCESS = ( os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true" ) +ENABLE_ADMIN_ANALYTICS = ( + os.environ.get("ENABLE_ADMIN_ANALYTICS", "True").lower() == "true" +) + ENABLE_COMMUNITY_SHARING = PersistentConfig( "ENABLE_COMMUNITY_SHARING", "ui.enable_community_sharing", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index b95166a9fd..a99ea4b015 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -434,6 +434,7 @@ from open_webui.config import ( RESPONSE_WATERMARK, # Admin ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_ANALYTICS, BYPASS_ADMIN_ACCESS_CONTROL, ENABLE_ADMIN_EXPORT, # Tasks @@ -1530,7 +1531,8 @@ app.include_router(functions.router, prefix="/api/v1/functions", tags=["function app.include_router( evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"] ) -app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"]) +if ENABLE_ADMIN_ANALYTICS: + app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"]) app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) # SCIM 2.0 API for identity management @@ -2120,6 +2122,7 @@ async def get_app_config(request: Request): "enable_user_status": app.state.config.ENABLE_USER_STATUS, "enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, + "enable_admin_analytics": ENABLE_ADMIN_ANALYTICS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "enable_memories": app.state.config.ENABLE_MEMORIES, diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 5bde46b4a6..3056c08459 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -275,6 +275,7 @@ type Config = { enable_image_generation: boolean; enable_admin_export: boolean; enable_admin_chat_access: boolean; + enable_admin_analytics: boolean; enable_community_sharing: boolean; enable_memories: boolean; enable_autocomplete_generation: boolean; diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index 6577cdabef..eee197ce6e 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -2,7 +2,7 @@ import { onMount, getContext } from 'svelte'; import { goto } from '$app/navigation'; - import { WEBUI_NAME, mobile, showSidebar, user } from '$lib/stores'; + import { WEBUI_NAME, config, mobile, showSidebar, user } from '$lib/stores'; import { page } from '$app/stores'; import Tooltip from '$lib/components/common/Tooltip.svelte'; @@ -66,12 +66,14 @@ href="/admin">{$i18n.t('Users')} + {#if $config?.features.enable_admin_analytics ?? true} {$i18n.t('Analytics')} + {/if} Date: Sat, 21 Feb 2026 15:16:22 -0600 Subject: [PATCH 57/70] refac --- src/lib/components/workspace/Prompts.svelte | 36 +++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 4eab4ef645..98d36692d4 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -246,24 +246,26 @@ const savedPrompts = JSON.parse(event.target.result); console.log(savedPrompts); - for (const prompt of savedPrompts) { - await createNewPrompt(localStorage.token, { - command: prompt.command, - name: prompt.name, - content: prompt.content - }).catch((error) => { - toast.error(typeof error === 'string' ? error : JSON.stringify(error)); - return null; - }); + try { + for (const prompt of savedPrompts) { + await createNewPrompt(localStorage.token, { + command: prompt.command, + name: prompt.name, + content: prompt.content + }).catch((error) => { + toast.error(typeof error === 'string' ? error : JSON.stringify(error)); + return null; + }); + } + + prompts = null; + page = 1; + await getPromptList(); + await _prompts.set(await getPrompts(localStorage.token)); + } finally { + importFiles = []; + promptsImportInputElement.value = ''; } - - prompts = null; - page = 1; - getPromptList(); - await _prompts.set(await getPrompts(localStorage.token)); - - importFiles = []; - promptsImportInputElement.value = ''; }; reader.readAsText(importFiles[0]); From bc482b9cce7edb2714f0c14db0a2aa4986c44942 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 15:17:36 -0600 Subject: [PATCH 58/70] refac --- src/lib/components/workspace/Prompts.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 98d36692d4..67d529a4bc 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -402,9 +402,7 @@
/{prompt.command}
- {#if prompt.is_active === false} - - {/if} +
{#if !prompt.write_access} From ff7f38d3438034e1211988f3799ced7885a9a3f4 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 21 Feb 2026 15:20:31 -0600 Subject: [PATCH 59/70] refac --- src/lib/components/workspace/Prompts.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 67d529a4bc..da59258ee2 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -240,6 +240,7 @@ hidden on:change={() => { console.log(importFiles); + if (!importFiles || importFiles.length === 0) return; const reader = new FileReader(); reader.onload = async (event) => { @@ -390,7 +391,7 @@ {:else if (prompts ?? []).length !== 0}
- {#each prompts as prompt} + {#each prompts as prompt (prompt.id)} Date: Sat, 21 Feb 2026 15:22:50 -0600 Subject: [PATCH 60/70] refac --- src/lib/components/workspace/Prompts.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index da59258ee2..98c3128853 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -259,12 +259,11 @@ }); } - prompts = null; page = 1; await getPromptList(); await _prompts.set(await getPrompts(localStorage.token)); } finally { - importFiles = []; + importFiles = null; promptsImportInputElement.value = ''; } }; From c2172e43eb4ad55175b35e9766fd11b889c984e7 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:23:58 -0500 Subject: [PATCH 61/70] fix(ui): prevent drag-and-drop ghost cursors and text highlighting on admin and workspace navigation tabs (#21701) --- src/routes/(app)/admin/+layout.svelte | 17 +++++++++++------ src/routes/(app)/workspace/+layout.svelte | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index eee197ce6e..23e93a5bc8 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -32,7 +32,7 @@ ? 'md:max-w-[calc(100%-var(--sidebar-width))]' : ' md:max-w-[calc(100%-49px)]'} w-full max-w-full" > -