From 89777891770b4fb82587cca413e4ab6bcf28ae61 Mon Sep 17 00:00:00 2001
From: G30 <50341825+silentoplayz@users.noreply.github.com>
Date: Wed, 8 Apr 2026 15:48:51 -0400
Subject: [PATCH 01/14] fix(ui): remove capitalize class from terminal menu
selected label (#23518)
---
src/lib/components/chat/MessageInput/TerminalMenu.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib/components/chat/MessageInput/TerminalMenu.svelte b/src/lib/components/chat/MessageInput/TerminalMenu.svelte
index aeb46a5d4e..c7458ff156 100644
--- a/src/lib/components/chat/MessageInput/TerminalMenu.svelte
+++ b/src/lib/components/chat/MessageInput/TerminalMenu.svelte
@@ -102,7 +102,7 @@
{#if $selectedTerminalId && selectedLabel}
- {selectedLabel}
{/if}
From 435efa31ce04f703ed983c480fd2f46fd20b41c9 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:10:35 +0200
Subject: [PATCH 02/14] fix: add SSRF protection to OAuth profile picture URL
fetching (#23356)
---
backend/open_webui/utils/oauth.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py
index bf6be461e6..8aaadc271a 100644
--- a/backend/open_webui/utils/oauth.py
+++ b/backend/open_webui/utils/oauth.py
@@ -81,6 +81,7 @@ from open_webui.utils.misc import parse_duration
from open_webui.utils.auth import get_password_hash, create_token
from open_webui.utils.webhook import post_webhook
from open_webui.utils.groups import apply_default_group_assignment
+from open_webui.retrieval.web.utils import validate_url
from mcp.shared.auth import (
OAuthClientMetadata as MCPOAuthClientMetadata,
@@ -1330,6 +1331,8 @@ class OAuthManager:
return '/user.png'
try:
+ validate_url(picture_url)
+
get_kwargs = {}
if access_token:
get_kwargs['headers'] = {
From 803d833908638bf7cac71c394c7b797912d537d0 Mon Sep 17 00:00:00 2001
From: Aleix Dorca
Date: Wed, 8 Apr 2026 22:11:25 +0200
Subject: [PATCH 03/14] i18n: Update catalan translation.json (#23506)
---
src/lib/i18n/locales/ca-ES/translation.json | 96 ++++++++++-----------
1 file changed, 48 insertions(+), 48 deletions(-)
diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json
index 6d4c101f29..fad81172aa 100644
--- a/src/lib/i18n/locales/ca-ES/translation.json
+++ b/src/lib/i18n/locales/ca-ES/translation.json
@@ -183,7 +183,7 @@
"Are you sure you want to archive all chats? This action cannot be undone.": "Estàs segur que vols arxivar tots els xats? Aquesta acció no es pot desfer.",
"Are you sure you want to clear all memories? This action cannot be undone.": "Estàs segur que vols netejar totes les memòries? Aquesta acció no es pot desfer.",
"Are you sure you want to delete \"{{NAME}}\"?": "Estàs segur que vols eliminar \"{{NAME}}\"?",
- "Are you sure you want to delete **{{modelName}}**?": "",
+ "Are you sure you want to delete **{{modelName}}**?": "Estàs segur que vols suprimir **{{modelName}}**?",
"Are you sure you want to delete all chats? This action cannot be undone.": "Estàs segur que vols suprimir tots els xats? Aquesta acció no es pot desfer.",
"Are you sure you want to delete this channel?": "Estàs segur que vols eliminar aquest canal?",
"Are you sure you want to delete this connection? This action cannot be undone.": "Estàs segur que vols suprimir aquesta connexió? Aquesta acció no es pot desfer.",
@@ -200,7 +200,7 @@
"Assistant": "Assistent",
"Async Embedding Processing": "Procés d'incrustat asíncron",
"Attach File From Knowledge": "Adjuntar arxiu del coneixement",
- "Attach Files": "",
+ "Attach Files": "Adjuntar arxius",
"Attach Knowledge": "Adjuntar coneixement",
"Attach Notes": "Adjuntar notes",
"Attach Webpage": "Adjuntar pàgina web",
@@ -223,13 +223,13 @@
"AUTOMATIC1111 Base URL": "URL Base d'AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "Es requereix la URL Base d'AUTOMATIC1111.",
"Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "Injecta automàticament les eines del sistema en el mode de crida de funcions natives (per exemple, marques de temps, memòria, historial de xat, notes, etc.)",
- "Automation": "",
- "Automation created": "",
- "Automation Name": "",
- "Automation title": "",
- "Automation triggered": "",
- "Automation updated": "",
- "Automations": "",
+ "Automation": "Automatització",
+ "Automation created": "Automatització creada",
+ "Automation Name": "Nom de l'automatització",
+ "Automation title": "Títol de l'automatització",
+ "Automation triggered": "Automatització disparada",
+ "Automation updated": "Automatització actualitzada",
+ "Automations": "Automatització",
"Available list": "Llista de disponibles",
"Available models": "Models disponibles",
"Available Tools": "Eines disponibles",
@@ -260,7 +260,7 @@
"Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Potenciar o penalitzar tokens específics per a respostes limitades. Els valors de biaix es fixaran entre -100 i 100 (inclosos). (Per defecte: cap)",
"Brave": "Brave",
"Brave Search API Key": "Clau API de Brave Search",
- "Break down complex requests into trackable steps": "",
+ "Break down complex requests into trackable steps": "Dividir les sol·licituds complexes en passos rastrejables",
"Browse and query knowledge bases": "Cerca i fes preguntes a una base de coneixement",
"Builtin Tools": "Eines integrades",
"Bullet List": "Llista indexada",
@@ -394,7 +394,7 @@
"Concurrent Requests": "Peticions simultànies",
"Config": "Configuració",
"Config imported successfully": "Configuració importada correctament",
- "Configuration": "",
+ "Configuration": "Configuració",
"Configure": "Configurar",
"Confirm": "Confirmar",
"Confirm Password": "Confirmar la contrasenya",
@@ -464,7 +464,7 @@
"Create new secret key": "Crear una nova clau secreta",
"Create note": "Crear una nota",
"Create Note": "Crea nota",
- "Create scheduled prompts that run automatically on a recurring basis.": "",
+ "Create scheduled prompts that run automatically on a recurring basis.": "Crea sol·licituds programades que s'executin automàticament de manera recurrent.",
"Create your first note by clicking on the plus button below.": "Crea la teva primera nota prement sobre el botó 'més' inferior",
"Created at": "Creat el",
"Created At": "Creat el",
@@ -486,7 +486,7 @@
"Data Controls": "Controls de dades",
"Database": "Base de dades",
"Datalab Marker API": "API de Datalab Marker",
- "Day": "",
+ "Day": "Dia",
"DD/MM/YYYY": "DD/MM/YYYY",
"DDGS Backend": "Backend DDGS",
"December": "Desembre",
@@ -517,7 +517,7 @@
"Delete All": "Eliminar tot",
"Delete All Chats": "Eliminar tots els xats",
"Delete all contents inside this folder": "Eliminar tot el contingut d'aquesta carpeta",
- "Delete automation?": "",
+ "Delete automation?": "Eliminar l'automatització",
"Delete Chat": "Eliminar xat",
"Delete chat?": "Eliminar el xat?",
"Delete File": "Eliminar el fitxer",
@@ -662,7 +662,7 @@
"Embedding Concurrent Requests": "Peticions concurrents d'incrustació",
"Embedding Model": "Model d'incrustació",
"Embedding Model Engine": "Motor de model d'incrustació",
- "Emojis": "",
+ "Emojis": "Emojis",
"Empty message": "Missatge buit",
"Enable All": "Habilitar tot",
"Enable API Keys": "Permetre claus API",
@@ -754,7 +754,7 @@
"Enter Perplexity Search API URL": "Introduïu l'URL de l'API de cerca de Perplexity",
"Enter Playwright Timeout": "Introdueix el temps d'espera de Playwright",
"Enter Playwright WebSocket URL": "Introdueix la URL de Playwright WebSocket",
- "Enter prompt here.": "",
+ "Enter prompt here.": "Introdueix el prompt aquí.",
"Enter proxy URL (e.g. https://user:password@host:port)": "Entra la URL (p. ex. https://user:password@host:port)",
"Enter reasoning effort": "Introdueix l'esforç de raonament",
"Enter Score": "Introdueix la puntuació",
@@ -779,7 +779,7 @@
"Enter system prompt here": "Entra la indicació de sistema aquí",
"Enter Tavily API Key": "Introdueix la clau API de Tavily",
"Enter Tavily Extract Depth": "Introdueix la profunditat d'extracció de Tavily",
- "Enter the prompt instructions for this automation...": "",
+ "Enter the prompt instructions for this automation...": "Introdueix les instruccions de la indicació per a aquesta automatització...",
"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Entra la URL pública de WebUI. Aquesta URL s'utilitzarà per generar els enllaços en les notificacions.",
"Enter the URL of the function to import": "Introdueix la URL de la funció a importar",
"Enter the URL to import": "Introdueix la URL a importar",
@@ -819,7 +819,7 @@
"Error accessing directory": "Error en accedir al directori",
"Error accessing Google Drive: {{error}}": "Error en accedir a Google Drive: {{error}}",
"Error accessing media devices.": "Error en accedir als dispositius multimèdia",
- "Error deleting model: {{error}}": "",
+ "Error deleting model: {{error}}": "Error en eliminar el model: {{error}}",
"Error starting recording.": "Error en començar a enregistrar",
"Error unloading model: {{error}}": "Error en descarregar el model: {{error}}",
"Error uploading file: {{error}}": "Error en pujar l'arxiu: {{error}}",
@@ -837,7 +837,7 @@
"Execute code": "Executa el codi",
"Execute code for analysis": "Executar el codi per analitzar-lo",
"Executing **{{NAME}}**...": "Executant **{{NAME}}**...",
- "Execution Logs": "",
+ "Execution Logs": "Registre de l'execució",
"Expand": "Expandir",
"Experimental": "Experimental",
"Explain": "Explicar",
@@ -847,8 +847,8 @@
"Export": "Exportar",
"Export All Archived Chats": "Exportar tots els xats arxivats",
"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
- "Export as CSV": "",
- "Export as JSON": "",
+ "Export as CSV": "Exportar com a CSV",
+ "Export as JSON": "Exportar com a JSON",
"Export chat (.json)": "Exportar el xat (.json)",
"Export Chats": "Exportar els xats",
"Export Config": "Exportar la configuració",
@@ -1103,7 +1103,7 @@
"Insert Suggestion Prompt to Input": "Insereix un suggeriment per introduir",
"Install from Github URL": "Instal·lar des de la URL de Github",
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
- "Instructions": "",
+ "Instructions": "Instruccions",
"Integration": "Integració",
"Integrations": "Integracions",
"Interface": "Interfície",
@@ -1163,7 +1163,7 @@
"Last 90 days": "Darrers 90 dies",
"Last Active": "Activitat recent",
"Last Modified": "Modificació",
- "Last ran": "",
+ "Last ran": "Darrera execució",
"Last reply": "Darrera resposta",
"LDAP": "LDAP",
"LDAP server updated": "Servidor LDAP actualitzat",
@@ -1270,7 +1270,7 @@
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat correctament.",
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
"Model {{modelId}} not found": "No s'ha trobat el model {{modelId}}",
- "Model {{modelName}} deleted successfully": "",
+ "Model {{modelName}} deleted successfully": "El model {{modelName}} s'ha eliminat correctament",
"Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió",
"Model {{name}} is now {{status}}": "El model {{name}} ara és {{status}}",
"Model {{name}} is now hidden": "El model {{name}} està ara amagat",
@@ -1320,11 +1320,11 @@
"Name": "Nom",
"Name and ID are required, please fill them out": "El nom i l'ID són necessaris, emplena'ls, si us plau",
"Name your knowledge base": "Anomena la teva base de coneixement",
- "Name, prompt, and model are required": "",
+ "Name, prompt, and model are required": "Nom, indicació i model són necessaris",
"Native": "Natiu",
- "Never": "",
+ "Never": "Mai",
"New": "Nou",
- "New Automation": "",
+ "New Automation": "Nova automatització",
"New Button": "Botó nou",
"New Chat": "Nou xat",
"New File": "Nou arxiu",
@@ -1343,11 +1343,11 @@
"New Webhook": "Nou webhook",
"new-channel": "nou-canal",
"Next message": "Missatge següent",
- "Next run": "",
+ "Next run": "Següent execució",
"No access grants. Private to you.": "Sense permisos d'accés. Privat per a tu.",
"No activity data": "No hi ha dades d'activitat",
"No authentication": "Sense autenticació",
- "No automations found": "",
+ "No automations found": "No s'ha trobat cap automatització",
"No chats found": "No s'han trobat xats",
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
"No chats found.": "No s'ha trobat xats.",
@@ -1358,7 +1358,7 @@
"No data": "No hi ha dades",
"No data found": "No s'han trobat dades",
"No distance available": "No hi ha distància disponible",
- "No execution logs available yet": "",
+ "No execution logs available yet": "No hi ha registres d'execució encara",
"No expiration can pose security risks.": "No posar expiració pot suposar problemes de seguretat.",
"No feedback found": "No s'ha trobat cap retorn",
"No file selected": "No s'ha escollit cap fitxer",
@@ -1405,7 +1405,7 @@
"Not factually correct": "No és clarament correcte",
"Not helpful": "No ajuda",
"Not Registered": "No registrat",
- "Not scheduled": "",
+ "Not scheduled": "No s'ha programat",
"Note": "Nota",
"Note deleted successfully": "La nota s'ha eliminat correctament",
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
@@ -1479,7 +1479,7 @@
"or": "o",
"Ordered List": "Llista ordenada",
"Other": "Altres",
- "out of": "",
+ "out of": "de",
"Output": "Sortida",
"OUTPUT": "SORTIDA",
"Output format": "Format de sortida",
@@ -1495,7 +1495,7 @@
"Password": "Contrasenya",
"Passwords do not match.": "Les contrasenyes no coincideixen",
"Paste Large Text as File": "Enganxa un text llarg com a fitxer",
- "Paused": "",
+ "Paused": "Pausat",
"PDF document (.pdf)": "Document PDF (.pdf)",
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
"PDF Loader Mode": "Mode de càrrega de PDF",
@@ -1600,7 +1600,7 @@
"Reason": "Raó",
"Reasoning Effort": "Esforç de raonament",
"Reasoning Tags": "Etiqueta de raonament",
- "Recently Used": "",
+ "Recently Used": "Recentment utilitzat",
"Record": "Enregistrar",
"Record voice": "Enregistrar la veu",
"Redirecting you to Open WebUI Community": "Redirigint-te a la comunitat OpenWebUI",
@@ -1636,7 +1636,7 @@
"Renamed to {{name}}": "S'ha renombrat a {{name}}",
"Render Markdown in Previews": "Compila el Markdown a les previsualitzacions",
"Reorder Models": "Reordenar els models",
- "Repeats": "",
+ "Repeats": "Repeticions",
"Reply": "Respondre",
"Reply in Thread": "Respondre al fil",
"Reply to thread...": "Respondra al fil...",
@@ -1670,8 +1670,8 @@
"RTL": "RTL",
"Run": "Executar",
"Run All": "Executar tot",
- "Run now": "",
- "Run Now": "",
+ "Run now": "Executar ara",
+ "Run Now": "Executar ara",
"Running": "S'està executant",
"Running...": "S'està executant...",
"Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.": "Executa tasques d'incrustació simultàniament per accelerar el processament. Desactiva-ho si els límits de velocitat es converteixen en un problema.",
@@ -1682,15 +1682,15 @@
"Save Chat": "Dear el xat",
"Saved": "Desat",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
- "Schedule": "",
- "Scheduled time must be in the future": "",
+ "Schedule": "Programar",
+ "Scheduled time must be in the future": "La data d'execucuío ha de ser en el futur",
"Scroll On Branch Change": "Fer scroll en canviar de branca",
"Search": "Cercar",
"Search a model": "Cercar un model",
"Search all emojis": "Cercar tots els emojis",
"Search and manage user memories": "Cerca i gestiona les memòries d'usuari",
"Search and view user chat history": "Cerca i mostra l'historial de xats",
- "Search Automations": "",
+ "Search Automations": "Cercar automatitzacions",
"Search Base": "Base de cerca",
"Search channels and channel messages": "Cerca els canals i els missatges als canals",
"Search Chats": "Cercar xats",
@@ -1760,7 +1760,7 @@
"Select how to split message text for TTS requests": "Seleccionar com separar un missatge per a peticions TTS",
"Select Knowledge": "Seleccionar coneixement",
"Select Method": "Escollir el mètode",
- "Select model": "",
+ "Select model": "Escollir el model",
"Select only one model to call": "Seleccionar només un model per trucar",
"Select view": "Seleccionar una vista",
"Selected model: {{modelName}}": "Model seleccionat: {{modelName}}",
@@ -1870,7 +1870,7 @@
"Start of the channel": "Inici del canal",
"Start Tag": "Etiqueta d'inici",
"Starting kernel...": "Iniciant el kernel...",
- "State": "",
+ "State": "Estat",
"Status": "Estat",
"Status cleared successfully": "S'ha eliminat correctament el teu estat",
"Status updated successfully": "S'ha actualitzat correctament el teu estat",
@@ -1921,10 +1921,10 @@
"Talk to Model": "Parlar amb el model",
"Tap to interrupt": "Prem per interrompre",
"Task List": "Llista de tasques",
- "Task Management": "",
+ "Task Management": "Gestió de tasques",
"Task Model": "Model de tasques",
"Tasks": "Tasques",
- "tasks completed": "",
+ "tasks completed": "tasques completades",
"Tavily API Key": "Clau API de Tavily",
"Tavily Extract Depth": "Profunditat d'extracció de Tavily",
"Tell us more:": "Dona'ns més informació:",
@@ -1991,7 +1991,7 @@
"Tika": "Tika",
"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
"Tiktoken": "Tiktoken",
- "Time": "",
+ "Time": "Temps",
"Time & Calculation": "Temps i càlculs",
"Timeout": "Temps d'espera",
"Title": "Títol",
@@ -2009,7 +2009,7 @@
"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
"Today": "Avui",
- "Today at": "",
+ "Today at": "Avui a les",
"Today at {{LOCALIZED_TIME}}": "Avui a les {{LOCALIZED_TIME}}",
"Toggle {{COUNT}} sources": "Activa/Desactiva {{COUNT}} fonts",
"Toggle 1 source": "Activa/Desactiva 1 font",
@@ -2148,7 +2148,7 @@
"Waiting for upload...": "Esperant per pujar...",
"Warning": "Avís",
"Warning:": "Avís:",
- "Warning: Enabling this will allow users to run scheduled prompts automatically.": "",
+ "Warning: Enabling this will allow users to run scheduled prompts automatically.": "Avís: Si actives aquesta opció, els usuaris podran executar sol·licituds programades automàticament.",
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "Avís: Habilitar això permetrà als usuaris penjar codi arbitrari al servidor.",
"Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.": "Avís: l'execució de Jupyter permet l'execució de codi arbitrari, la qual cosa comporta greus riscos de seguretat; procediu amb extrema precaució.",
"Web": "Web",
@@ -2183,7 +2183,7 @@
"Width": "amplada",
"Wikipedia": "Wikipedia",
"Won": "Ha guanyat",
- "Working Directory": "",
+ "Working Directory": "Carpeta de treball",
"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 juntament amb top-k. Un valor més alt (p. ex., 0,95) donarà lloc a un text més divers, mentre que un valor més baix (p. ex., 0,5) generarà un text més concentrat i conservador.",
"Workspace": "Espai de treball",
"Workspace Permissions": "Permisos de l'espai de treball",
From e7e006e78156288dc4308ea018dee1611019ac30 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:13:44 +0200
Subject: [PATCH 04/14] fix: use admin-configured WEB_SEARCH_RESULT_COUNT as
default (#23488)
The built-in search_web tool hardcoded count=5 as the default,
ignoring the admin-configured WEB_SEARCH_RESULT_COUNT setting.
When the LLM did not specify a count, the tool always returned 5
results regardless of admin configuration.
Now the tool defaults to the admin-configured value when the LLM
omits the count parameter, while still capping LLM-requested
values at the admin maximum to prevent abuse.
Closes #23485
---
backend/open_webui/tools/builtin.py | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py
index 3c63d318c3..1823e71df2 100644
--- a/backend/open_webui/tools/builtin.py
+++ b/backend/open_webui/tools/builtin.py
@@ -149,7 +149,7 @@ async def calculate_timestamp(
async def search_web(
query: str,
- count: int = 5,
+ count: Optional[int] = None,
__request__: Request = None,
__user__: dict = None,
) -> str:
@@ -158,7 +158,7 @@ async def search_web(
or topics not covered in internal documents.
:param query: The search query to look up
- :param count: Number of results to return (default: 5)
+ :param count: Number of results to return (default: admin-configured value)
:return: JSON with search results containing title, link, and snippet for each result
"""
if __request__ is None:
@@ -168,12 +168,9 @@ async def search_web(
engine = __request__.app.state.config.WEB_SEARCH_ENGINE
user = UserModel(**__user__) if __user__ else None
- # Enforce maximum result count from config to prevent abuse
- count = (
- count
- if count < __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
- else __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
- )
+ configured = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
+ max_count = 5 if configured is None else configured
+ count = max(1, min(count, max_count)) if count is not None else max_count
results = await asyncio.to_thread(_search_web, __request__, engine, query, user)
From 99f3c554c8595bb0c94b8f7bac66d61386cfeae1 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:14:46 +0200
Subject: [PATCH 05/14] feat: support Azure v1 endpoint format (/openai/v1)
(#23484)
Azure offers two URL formats: the legacy deployment-based format
(/openai/deployments/{model}/...) and the newer v1 format
(/openai/v1/...) where the model stays in the payload body and no
api-version query parameter is needed.
Previously, the code always ran convert_to_azure_payload which
rewrites the URL to the deployment format, causing 404 errors for
users with v1-style base URLs. Now, when the base URL contains
'/openai/v1', we skip deployment URL construction and route
directly.
Applied consistently across all three Azure routing paths:
generate_chat_completion, /responses proxy, and generic proxy.
---
backend/open_webui/routers/openai.py | 58 ++++++++++++++++++----------
1 file changed, 38 insertions(+), 20 deletions(-)
diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py
index 0e7c67c1f6..836517df9d 100644
--- a/backend/open_webui/routers/openai.py
+++ b/backend/open_webui/routers/openai.py
@@ -1131,21 +1131,31 @@ async def generate_chat_completion(
is_responses = api_config.get('api_type') == 'responses'
if api_config.get('azure', False):
- api_version = api_config.get('api_version', '2023-03-15-preview')
- request_url, payload = convert_to_azure_payload(url, payload, api_version)
-
# Only set api-key header if not using Azure Entra ID authentication
auth_type = api_config.get('auth_type', 'bearer')
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
headers['api-key'] = key
- headers['api-version'] = api_version
+ # Azure v1 format: base URL already ends with /openai/v1,
+ # model stays in the payload, no deployment URL rewriting.
+ is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
- if is_responses:
- payload = convert_to_responses_payload(payload)
- request_url = f'{request_url}/responses?api-version={api_version}'
+ if is_azure_v1:
+ if is_responses:
+ payload = convert_to_responses_payload(payload)
+ request_url = f'{url.rstrip("/")}/responses'
+ else:
+ request_url = f'{url.rstrip("/")}/chat/completions'
else:
- request_url = f'{request_url}/chat/completions?api-version={api_version}'
+ api_version = api_config.get('api_version', '2023-03-15-preview')
+ request_url, payload = convert_to_azure_payload(url, payload, api_version)
+ headers['api-version'] = api_version
+
+ if is_responses:
+ payload = convert_to_responses_payload(payload)
+ request_url = f'{request_url}/responses?api-version={api_version}'
+ else:
+ request_url = f'{request_url}/chat/completions?api-version={api_version}'
else:
if is_responses:
payload = convert_to_responses_payload(payload)
@@ -1357,16 +1367,19 @@ async def responses(
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
if api_config.get('azure', False):
- api_version = api_config.get('api_version', '2023-03-15-preview')
-
auth_type = api_config.get('auth_type', 'bearer')
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
headers['api-key'] = key
- headers['api-version'] = api_version
+ is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
- model = payload.get('model', '')
- request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}'
+ if is_azure_v1:
+ request_url = f'{url.rstrip("/")}/responses'
+ else:
+ api_version = api_config.get('api_version', '2023-03-15-preview')
+ headers['api-version'] = api_version
+ model = payload.get('model', '')
+ request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}'
else:
request_url = f'{url}/responses'
@@ -1459,20 +1472,25 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
if api_config.get('azure', False):
- api_version = api_config.get('api_version', '2023-03-15-preview')
-
# Only set api-key header if not using Azure Entra ID authentication
auth_type = api_config.get('auth_type', 'bearer')
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
headers['api-key'] = key
- headers['api-version'] = api_version
+ is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
- payload = json.loads(body)
- url, payload = convert_to_azure_payload(url, payload, api_version)
- body = json.dumps(payload).encode()
+ if is_azure_v1:
+ qs = request.url.query
+ request_url = f'{url.rstrip("/")}/{path}' + (f'?{qs}' if qs else '')
+ else:
+ api_version = api_config.get('api_version', '2023-03-15-preview')
+ headers['api-version'] = api_version
- request_url = f'{url}/{path}?api-version={api_version}'
+ payload = json.loads(body)
+ url, payload = convert_to_azure_payload(url, payload, api_version)
+ body = json.dumps(payload).encode()
+
+ request_url = f'{url}/{path}?api-version={api_version}'
else:
request_url = f'{url}/{path}'
From fcedeb903401fc33ca999a4e8b014f65b867e756 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:15:21 +0200
Subject: [PATCH 06/14] feat: add /v1/responses proxy endpoint for Ollama
(#23483)
Ollama recently added Responses API support via its OpenAI-compatible
endpoint (/v1/responses). This adds a proxy endpoint to the Ollama
router that forwards requests to Ollama's /v1/responses, applying
the same model resolution, access control, and prefix_id handling
used by the existing /v1/chat/completions and /v1/messages proxies.
Uses a typed ResponsesForm Pydantic model with required model field
and extra='allow' for forward compatibility, consistent with other
endpoint schemas in the file.
This allows API consumers (Codex, Claude Code, etc.) to use the
Responses API directly with Ollama-hosted models without requiring
a separate OpenAI-compatible connection.
---
backend/open_webui/routers/ollama.py | 78 ++++++++++++++++++++++++++++
1 file changed, 78 insertions(+)
diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py
index d03c37ae1a..fd2a088a61 100644
--- a/backend/open_webui/routers/ollama.py
+++ b/backend/open_webui/routers/ollama.py
@@ -1596,6 +1596,84 @@ async def generate_anthropic_messages(
)
+class ResponsesForm(BaseModel):
+ model: str
+
+ model_config = ConfigDict(extra='allow')
+
+
+@router.post('/v1/responses')
+@router.post('/v1/responses/{url_idx}')
+async def generate_responses(
+ request: Request,
+ form_data: ResponsesForm,
+ url_idx: Optional[int] = None,
+ user=Depends(get_verified_user),
+):
+ """
+ Proxy for Ollama's OpenAI-compatible /v1/responses endpoint.
+
+ Forwards the request as-is to the Ollama backend, applying the same
+ model resolution, access control, and prefix_id handling used by
+ the OpenAI-compatible /v1/chat/completions proxy.
+
+ See https://ollama.com/blog/responses-api
+ """
+ if not request.app.state.config.ENABLE_OLLAMA_API:
+ raise HTTPException(status_code=503, detail='Ollama API is disabled')
+
+ payload = form_data.model_dump()
+ model_id = form_data.model
+
+ model_info = Models.get_model_by_id(model_id)
+ if model_info:
+ if model_info.base_model_id:
+ payload['model'] = model_info.base_model_id
+
+ # Check if user has access to the model
+ if user.role == 'user':
+ user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
+ if not (
+ user.id == model_info.user_id
+ or AccessGrants.has_access(
+ user_id=user.id,
+ resource_type='model',
+ resource_id=model_info.id,
+ permission='read',
+ user_group_ids=user_group_ids,
+ )
+ ):
+ raise HTTPException(
+ status_code=403,
+ detail='Model not found',
+ )
+ else:
+ if user.role != 'admin':
+ raise HTTPException(
+ status_code=403,
+ detail='Model not found',
+ )
+
+ url, url_idx = await get_ollama_url(request, payload['model'], url_idx)
+ api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
+ str(url_idx),
+ request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
+ )
+
+ prefix_id = api_config.get('prefix_id', None)
+ if prefix_id:
+ payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
+
+ return await send_post_request(
+ url=f'{url}/v1/responses',
+ payload=json.dumps(payload),
+ stream=payload.get('stream', False),
+ content_type='text/event-stream' if payload.get('stream', False) else None,
+ key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
+ user=user,
+ )
+
+
@router.get('/v1/models')
@router.get('/v1/models/{url_idx}')
async def get_openai_models(
From 736a800c5f3e6d6055ecd661b51d624b0ae8ec86 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Wed, 8 Apr 2026 13:20:11 -0700
Subject: [PATCH 07/14] refac
---
src/lib/components/AddToolServerModal.svelte | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte
index 2237c1afc6..d146aeca0c 100644
--- a/src/lib/components/AddToolServerModal.svelte
+++ b/src/lib/components/AddToolServerModal.svelte
@@ -917,10 +917,8 @@
'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.'
)}
- {$i18n.t('Read more →')}{$i18n.t('Read more →')}
{/if}
From 2112a99b36fb99d1c3cfe00b6ea86ff2f0e430b6 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Wed, 8 Apr 2026 13:21:01 -0700
Subject: [PATCH 08/14] refac
---
README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/README.md b/README.md
index a178b3271e..1885f4f6f1 100644
--- a/README.md
+++ b/README.md
@@ -172,8 +172,6 @@ After installation, you can access Open WebUI at [http://localhost:3000](http://
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
-Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/development) for instructions on setting up a local development environment.
-
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
From f9ceb7fa899aea2780b2c8a970681e5418d3902f Mon Sep 17 00:00:00 2001
From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
Date: Wed, 8 Apr 2026 23:21:55 +0300
Subject: [PATCH 09/14] perf: skip torch import on non-macOS (#23438)
---
backend/open_webui/env.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py
index e0dcaff276..53737a1f2f 100644
--- a/backend/open_webui/env.py
+++ b/backend/open_webui/env.py
@@ -60,13 +60,14 @@ if USE_CUDA.lower() == 'true':
else:
DEVICE_TYPE = 'cpu'
-try:
- import torch
+if sys.platform == 'darwin':
+ try:
+ import torch
- if torch.backends.mps.is_available() and torch.backends.mps.is_built():
- DEVICE_TYPE = 'mps'
-except Exception:
- pass
+ if torch.backends.mps.is_available() and torch.backends.mps.is_built():
+ DEVICE_TYPE = 'mps'
+ except Exception:
+ pass
####################################
# LOGGING
From a775fc9b5087a06bb3b5d765cde4654d201f6861 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Wed, 8 Apr 2026 13:34:23 -0700
Subject: [PATCH 10/14] refac
---
backend/open_webui/functions.py | 13 +------------
backend/open_webui/utils/middleware.py | 5 ++++-
2 files changed, 5 insertions(+), 13 deletions(-)
diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py
index 9bfe77c41e..6a1fe22149 100644
--- a/backend/open_webui/functions.py
+++ b/backend/open_webui/functions.py
@@ -34,7 +34,6 @@ from open_webui.utils.plugin import (
load_function_module_by_id,
get_function_module_from_cache,
)
-from open_webui.utils.tools import get_tools
from open_webui.env import GLOBAL_LOG_LEVEL
@@ -255,17 +254,7 @@ async def generate_function_chat_completion(request, form_data, user, models: di
'__oauth_token__': oauth_token,
'__request__': request,
}
- extra_params['__tools__'] = await get_tools(
- request,
- tool_ids,
- user,
- {
- **extra_params,
- '__model__': models.get(form_data['model'], None),
- '__messages__': form_data['messages'],
- '__files__': files,
- },
- )
+ extra_params['__tools__'] = metadata.get('tools', {})
if model_info:
if model_info.base_model_id:
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index 54df422c4b..0dedd7f2f6 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -2680,9 +2680,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
tools_dict[name] = tool_dict
if tools_dict:
+ # Always store resolved tools in metadata so downstream consumers
+ # (e.g. pipe functions) can access all tools including MCP and builtins.
+ metadata['tools'] = tools_dict
+
if metadata.get('params', {}).get('function_calling') == 'native':
# If the function calling is native, then call the tools function calling handler
- metadata['tools'] = tools_dict
form_data['tools'] = [
{'type': 'function', 'function': tool.get('spec', {})} for tool in tools_dict.values()
]
From e51b661af0e71a24f041428f328fcc6e97a15262 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Wed, 8 Apr 2026 14:32:34 -0700
Subject: [PATCH 11/14] refac: ollama
---
backend/open_webui/routers/ollama.py | 508 ++++++++-------------------
1 file changed, 155 insertions(+), 353 deletions(-)
diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py
index fd2a088a61..93745440c4 100644
--- a/backend/open_webui/routers/ollama.py
+++ b/backend/open_webui/routers/ollama.py
@@ -15,7 +15,7 @@ from typing import Optional, Union
from urllib.parse import urlparse
import aiohttp
from aiocache import cached
-import requests
+
from open_webui.utils.headers import include_user_info_headers
from open_webui.models.chats import Chats
@@ -107,19 +107,24 @@ async def send_get_request(url, key=None, user: UserModel = None):
return None
-async def send_post_request(
+async def send_request(
url: str,
- payload: Union[str, bytes],
- stream: bool = True,
+ method: str = 'POST',
+ *,
+ payload: Optional[Union[str, bytes]] = None,
key: Optional[str] = None,
- content_type: Optional[str] = None,
user: UserModel = None,
+ stream: bool = False,
+ content_type: Optional[str] = None,
metadata: Optional[dict] = None,
):
r = None
streaming = False
try:
- session = aiohttp.ClientSession(trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT))
+ session = aiohttp.ClientSession(
+ trust_env=True,
+ timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
+ )
headers = {
'Content-Type': 'application/json',
@@ -131,32 +136,29 @@ async def send_post_request(
if metadata and metadata.get('chat_id'):
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id')
- r = await session.post(
- url,
- data=payload,
- headers=headers,
+ r = await session.request(
+ method, url, data=payload, headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
- if r.ok is False:
+ if not r.ok:
try:
res = await r.json()
- await cleanup_response(r, session)
if 'error' in res:
raise HTTPException(status_code=r.status, detail=res['error'])
- except HTTPException as e:
- raise e # Re-raise HTTPException to be handled by FastAPI
+ except HTTPException:
+ raise
except Exception as e:
log.error(f'Failed to parse error response: {e}')
- raise HTTPException(
- status_code=r.status,
- detail=f'Open WebUI: Server Connection Error',
- )
+ raise HTTPException(
+ status_code=r.status,
+ detail='Open WebUI: Server Connection Error',
+ )
+
+ r.raise_for_status()
- r.raise_for_status() # Raises an error for bad responses (4xx, 5xx)
if stream:
response_headers = dict(r.headers)
-
if content_type:
response_headers['Content-Type'] = content_type
@@ -167,17 +169,17 @@ async def send_post_request(
headers=response_headers,
)
else:
- res = await r.json()
- return res
+ try:
+ return await r.json()
+ except Exception:
+ return None
- except HTTPException as e:
- raise e # Re-raise HTTPException to be handled by FastAPI
+ except HTTPException:
+ raise
except Exception as e:
- detail = f'Ollama: {e}'
-
raise HTTPException(
status_code=r.status if r else 500,
- detail=detail if e else 'Open WebUI: Server Connection Error',
+ detail=f'Ollama: {e}' if str(e) else 'Open WebUI: Server Connection Error',
)
finally:
if not streaming:
@@ -430,40 +432,7 @@ async def get_ollama_tags(request: Request, url_idx: Optional[int] = None, user=
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
-
- r = None
- try:
- headers = {
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(
- method='GET',
- url=f'{url}/api/tags',
- headers=headers,
- )
- r.raise_for_status()
-
- models = r.json()
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ models = await send_request(f'{url}/api/tags', 'GET', key=key, user=user)
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
models['models'] = await get_filtered_models(models, user)
@@ -569,29 +538,7 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
)
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
-
- r = None
- try:
- r = requests.request(method='GET', url=f'{url}/api/version')
- r.raise_for_status()
-
- return r.json()
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ return await send_request(f'{url}/api/version', 'GET')
else:
return {'version': False}
@@ -640,10 +587,9 @@ async def unload_model(
payload = {'model': model_name, 'keep_alive': 0, 'prompt': ''}
try:
- res = await send_post_request(
- url=f'{url}/api/generate',
+ res = await send_request(
+ f'{url}/api/generate',
payload=json.dumps(payload),
- stream=False,
key=key,
user=user,
)
@@ -681,11 +627,12 @@ async def pull_model(
# Admin should be able to pull models from any source
payload = {**form_data, 'insecure': True}
- return await send_post_request(
- url=f'{url}/api/pull',
+ return await send_request(
+ f'{url}/api/pull',
payload=json.dumps(payload),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=True,
)
@@ -721,11 +668,12 @@ async def push_model(
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
log.debug(f'url: {url}')
- return await send_post_request(
- url=f'{url}/api/push',
+ return await send_request(
+ f'{url}/api/push',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=True,
)
@@ -751,11 +699,12 @@ async def create_model(
log.debug(f'form_data: {form_data}')
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
- return await send_post_request(
- url=f'{url}/api/create',
+ return await send_request(
+ f'{url}/api/create',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=True,
)
@@ -790,41 +739,13 @@ async def copy_model(
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
- try:
- headers = {
- 'Content-Type': 'application/json',
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(
- method='POST',
- url=f'{url}/api/copy',
- headers=headers,
- data=form_data.model_dump_json(exclude_none=True).encode(),
- )
- r.raise_for_status()
-
- log.debug(f'r.text: {r.text}')
- return True
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ await send_request(
+ f'{url}/api/copy',
+ payload=form_data.model_dump_json(exclude_none=True).encode(),
+ key=key,
+ user=user,
+ )
+ return True
@router.delete('/api/delete')
@@ -858,42 +779,13 @@ async def delete_model(
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
- r = None
- try:
- headers = {
- 'Content-Type': 'application/json',
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(
- method='DELETE',
- url=f'{url}/api/delete',
- headers=headers,
- json=form_data,
- )
- r.raise_for_status()
-
- log.debug(f'r.text: {r.text}')
- return True
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ await send_request(
+ f'{url}/api/delete', 'DELETE',
+ payload=json.dumps(form_data),
+ key=key,
+ user=user,
+ )
+ return True
@router.post('/api/show')
@@ -920,35 +812,12 @@ async def show_model_info(request: Request, form_data: ModelNameForm, user=Depen
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
- try:
- headers = {
- 'Content-Type': 'application/json',
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(method='POST', url=f'{url}/api/show', headers=headers, json=form_data)
- r.raise_for_status()
-
- return r.json()
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ return await send_request(
+ f'{url}/api/show',
+ payload=json.dumps(form_data),
+ key=key,
+ user=user,
+ )
class GenerateEmbedForm(BaseModel):
@@ -1004,41 +873,12 @@ async def embed(
if prefix_id:
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
- try:
- headers = {
- 'Content-Type': 'application/json',
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(
- method='POST',
- url=f'{url}/api/embed',
- headers=headers,
- data=form_data.model_dump_json(exclude_none=True).encode(),
- )
- r.raise_for_status()
-
- data = r.json()
- return data
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ return await send_request(
+ f'{url}/api/embed',
+ payload=form_data.model_dump_json(exclude_none=True).encode(),
+ key=key,
+ user=user,
+ )
class GenerateEmbeddingsForm(BaseModel):
@@ -1089,41 +929,12 @@ async def embeddings(
if prefix_id:
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
- try:
- headers = {
- 'Content-Type': 'application/json',
- **({'Authorization': f'Bearer {key}'} if key else {}),
- }
-
- if ENABLE_FORWARD_USER_INFO_HEADERS and user:
- headers = include_user_info_headers(headers, user)
-
- r = requests.request(
- method='POST',
- url=f'{url}/api/embeddings',
- headers=headers,
- data=form_data.model_dump_json(exclude_none=True).encode(),
- )
- r.raise_for_status()
-
- data = r.json()
- return data
- except Exception as e:
- log.exception(e)
-
- detail = None
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- detail = f'Ollama: {res["error"]}'
- except Exception:
- detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=detail if detail else 'Open WebUI: Server Connection Error',
- )
+ return await send_request(
+ f'{url}/api/embeddings',
+ payload=form_data.model_dump_json(exclude_none=True).encode(),
+ key=key,
+ user=user,
+ )
class GenerateCompletionForm(BaseModel):
@@ -1175,11 +986,12 @@ async def generate_completion(
if prefix_id:
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/api/generate',
+ return await send_request(
+ f'{url}/api/generate',
payload=form_data.model_dump_json(exclude_none=True).encode(),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=True,
)
@@ -1319,13 +1131,13 @@ async def generate_chat_completion(
if prefix_id:
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/api/chat',
+ return await send_request(
+ f'{url}/api/chat',
payload=json.dumps(payload),
- stream=form_data.stream,
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
- content_type='application/x-ndjson',
user=user,
+ stream=form_data.stream,
+ content_type='application/x-ndjson',
metadata=metadata,
)
@@ -1429,12 +1241,12 @@ async def generate_openai_completion(
if prefix_id:
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/v1/completions',
+ return await send_request(
+ f'{url}/v1/completions',
payload=json.dumps(payload),
- stream=payload.get('stream', False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=payload.get('stream', False),
metadata=metadata,
)
@@ -1514,12 +1326,12 @@ async def generate_openai_chat_completion(
if prefix_id:
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/v1/chat/completions',
+ return await send_request(
+ f'{url}/v1/chat/completions',
payload=json.dumps(payload),
- stream=payload.get('stream', False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=payload.get('stream', False),
metadata=metadata,
)
@@ -1586,13 +1398,13 @@ async def generate_anthropic_messages(
if prefix_id:
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/v1/messages',
+ return await send_request(
+ f'{url}/v1/messages',
payload=json.dumps(payload),
- stream=payload.get('stream', False),
- content_type='text/event-stream' if payload.get('stream', False) else None,
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=payload.get('stream', False),
+ content_type='text/event-stream' if payload.get('stream', False) else None,
)
@@ -1664,13 +1476,13 @@ async def generate_responses(
if prefix_id:
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
- return await send_post_request(
- url=f'{url}/v1/responses',
+ return await send_request(
+ f'{url}/v1/responses',
payload=json.dumps(payload),
- stream=payload.get('stream', False),
- content_type='text/event-stream' if payload.get('stream', False) else None,
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
+ stream=payload.get('stream', False),
+ content_type='text/event-stream' if payload.get('stream', False) else None,
)
@@ -1697,36 +1509,17 @@ async def get_openai_models(
else:
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
- try:
- r = requests.request(method='GET', url=f'{url}/api/tags')
- r.raise_for_status()
+ model_list = await send_request(f'{url}/api/tags', 'GET')
- model_list = r.json()
-
- models = [
- {
- 'id': model['model'],
- 'object': 'model',
- 'created': int(time.time()),
- 'owned_by': 'openai',
- }
- for model in models['models']
- ]
- except Exception as e:
- log.exception(e)
- error_detail = 'Open WebUI: Server Connection Error'
- if r is not None:
- try:
- res = r.json()
- if 'error' in res:
- error_detail = f'Ollama: {res["error"]}'
- except Exception:
- error_detail = f'Ollama: {e}'
-
- raise HTTPException(
- status_code=r.status_code if r else 500,
- detail=error_detail,
- )
+ models = [
+ {
+ 'id': model['model'],
+ 'object': 'model',
+ 'created': int(time.time()),
+ 'owned_by': 'openai',
+ }
+ for model in model_list.get('models', [])
+ ]
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
# Filter models based on user access control
@@ -1812,13 +1605,16 @@ async def download_file_stream(ollama_url, file_url, file_path, file_name, chunk
file.close()
hashed = calculate_sha256(file_path, chunk_size)
- with open(file_path, 'rb') as file:
- chunk_size = 1024 * 1024 * 2
- url = f'{ollama_url}/api/blobs/sha256:{hashed}'
- with requests.Session() as session:
- response = session.post(url, data=file, timeout=30)
+ with open(file_path, 'rb') as f:
+ blob_data = f.read()
- if response.ok:
+ url = f'{ollama_url}/api/blobs/sha256:{hashed}'
+ blob_timeout = aiohttp.ClientTimeout(total=30)
+ async with aiohttp.ClientSession(timeout=blob_timeout, trust_env=True) as blob_session:
+ async with blob_session.post(
+ url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
+ ) as blob_response:
+ if blob_response.ok:
res = {
'done': done,
'blob': f'sha256:{hashed}',
@@ -1914,47 +1710,53 @@ async def upload_model(
# --- P3: Upload to ollama /api/blobs ---
with open(file_path, 'rb') as f:
- url = f'{ollama_url}/api/blobs/sha256:{file_hash}'
- response = requests.post(url, data=f)
+ blob_data = f.read()
- if response.ok:
- log.info(f'Uploaded to /api/blobs') # DEBUG
- # Remove local file
- os.remove(file_path)
+ url = f'{ollama_url}/api/blobs/sha256:{file_hash}'
+ upload_timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
+ async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as upload_session:
+ async with upload_session.post(
+ url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
+ ) as response:
+ if not response.ok:
+ raise Exception('Ollama: Could not create blob, Please try again.')
- # Create model in ollama
- model_name, ext = os.path.splitext(filename)
- log.info(f'Created Model: {model_name}') # DEBUG
+ log.info(f'Uploaded to /api/blobs') # DEBUG
+ # Remove local file
+ os.remove(file_path)
- create_payload = {
- 'model': model_name,
- # Reference the file by its original name => the uploaded blob's digest
- 'files': {filename: f'sha256:{file_hash}'},
- }
- log.info(f'Model Payload: {create_payload}') # DEBUG
+ # Create model in ollama
+ model_name, ext = os.path.splitext(filename)
+ log.info(f'Created Model: {model_name}') # DEBUG
- # Call ollama /api/create
- # https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model
- create_resp = requests.post(
- url=f'{ollama_url}/api/create',
+ create_payload = {
+ 'model': model_name,
+ # Reference the file by its original name => the uploaded blob's digest
+ 'files': {filename: f'sha256:{file_hash}'},
+ }
+ log.info(f'Model Payload: {create_payload}') # DEBUG
+
+ # Call ollama /api/create
+ # https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model
+ async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as create_session:
+ async with create_session.post(
+ f'{ollama_url}/api/create',
headers={'Content-Type': 'application/json'},
data=json.dumps(create_payload),
- )
-
- if create_resp.ok:
- log.info(f'API SUCCESS!') # DEBUG
- done_msg = {
- 'done': True,
- 'blob': f'sha256:{file_hash}',
- 'name': filename,
- 'model_created': model_name,
- }
- yield f'data: {json.dumps(done_msg)}\n\n'
- else:
- raise Exception(f'Failed to create model in Ollama. {create_resp.text}')
-
- else:
- raise Exception('Ollama: Could not create blob, Please try again.')
+ ssl=AIOHTTP_CLIENT_SESSION_SSL,
+ ) as create_resp:
+ if create_resp.ok:
+ log.info(f'API SUCCESS!') # DEBUG
+ done_msg = {
+ 'done': True,
+ 'blob': f'sha256:{file_hash}',
+ 'name': filename,
+ 'model_created': model_name,
+ }
+ yield f'data: {json.dumps(done_msg)}\n\n'
+ else:
+ resp_text = await create_resp.text()
+ raise Exception(f'Failed to create model in Ollama. {resp_text}')
except Exception as e:
res = {'error': str(e)}
From cb0fd6ed41fd57ad3243119a68a64b410b21b65d Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Wed, 8 Apr 2026 14:55:04 -0700
Subject: [PATCH 12/14] refac
---
.../chat/FileNav/FilePreview.svelte | 21 ++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte
index 0bfa4fe504..b5a3a30cc4 100644
--- a/src/lib/components/chat/FileNav/FilePreview.svelte
+++ b/src/lib/components/chat/FileNav/FilePreview.svelte
@@ -103,6 +103,13 @@
$: isNotebook = getExt(selectedFile) === 'ipynb';
$: isCode = isCodeFile(selectedFile);
$: csvDelimiter = getExt(selectedFile) === 'tsv' ? '\t' : ',';
+
+ // For HTML files on system terminals (proxy URL), use path-based serving
+ // so the iframe can resolve relative CSS/JS/image references via cookie auth.
+ $: serveUrl =
+ isHtml && selectedFile && baseUrl && baseUrl.includes('/api/v1/terminals/')
+ ? `${baseUrl}/files/serve/${selectedFile.replace(/^\//, '')}`
+ : null;
$: renderedHtml =
isMarkdown && fileContent
? DOMPurify.sanitize(marked.parse(fileContent, { async: false }) as string)
@@ -386,7 +393,19 @@
{/if}
{:else if fileContent !== null}
- {#if isHtml && !showRaw}
+ {#if isHtml && !showRaw && serveUrl}
+ {#if overlay}
+
+ {/if}
+
+ {:else if isHtml && !showRaw}
{#if overlay}
{/if}
From 4b8e33133354b4dd6813c16ed5601cdaffe80cc4 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Thu, 9 Apr 2026 11:13:09 -0700
Subject: [PATCH 13/14] refac
---
src/lib/components/chat/Chat.svelte | 17 +++++++++++++++--
src/lib/stores/index.ts | 9 +++++++++
src/routes/+layout.svelte | 6 ++++--
3 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 381d26cd53..768975dacb 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -46,7 +46,8 @@
selectedTerminalId,
showFileNavPath,
showFileNavDir,
- chatRequestQueues
+ chatRequestQueues,
+ desktopEvent
} from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
@@ -1225,7 +1226,19 @@
showControls.set(true);
}
- if ($page.url.searchParams.get('q')) {
+ // Consume one-shot desktop event (e.g. Spotlight query + attachments)
+ if ($desktopEvent) {
+ const { query, files: eventFiles } = $desktopEvent;
+ desktopEvent.set(null);
+
+ // TODO: handle eventFiles (attach to chat)
+
+ if (query) {
+ messageInput?.setText(query);
+ await tick();
+ submitHandler(query);
+ }
+ } else if ($page.url.searchParams.get('q')) {
const q = $page.url.searchParams.get('q') ?? '';
messageInput?.setText(q);
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts
index 5cd7f0d632..edcea62137 100644
--- a/src/lib/stores/index.ts
+++ b/src/lib/stores/index.ts
@@ -111,6 +111,15 @@ export const artifactContents = writable(null);
export const embed = writable(null);
export const temporaryChatEnabled = writable(false);
+
+// Transient one-shot event from the desktop shell (Spotlight, drag-and-drop, etc.).
+// Set by +layout.svelte, consumed and cleared by Chat.svelte.
+export type DesktopEventFile = { name: string; mimeType: string; dataUrl: string };
+export type DesktopEvent = {
+ query?: string;
+ files?: DesktopEventFile[];
+};
+export const desktopEvent: Writable = writable(null);
export const scrollPaginationEnabled = writable(false);
export const currentChatPage = writable(1);
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 75da2acd23..0f032c6642 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -35,7 +35,8 @@
showControls,
showFileNavPath,
showFileNavDir,
- pyodideWorker
+ pyodideWorker,
+ desktopEvent
} from '$lib/stores';
import { getFileContentById } from '$lib/apis/files';
import { goto } from '$app/navigation';
@@ -711,7 +712,8 @@
return;
}
if (event.type === 'query' && event.data?.query) {
- await goto(`/?q=${encodeURIComponent(event.data.query)}`);
+ desktopEvent.set({ query: event.data.query });
+ await goto('/');
return;
}
if (event.type === 'theme:update' && event.data?.theme) {
From 1dcbfd47fb97655725aadaa25b23a0dceaf3e152 Mon Sep 17 00:00:00 2001
From: Timothy Jaeryang Baek
Date: Thu, 9 Apr 2026 11:36:06 -0700
Subject: [PATCH 14/14] refac
---
src/lib/components/chat/Chat.svelte | 14 +++++++++++++-
src/routes/+layout.svelte | 4 ++--
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 768975dacb..3f0f8d13e4 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -1231,7 +1231,19 @@
const { query, files: eventFiles } = $desktopEvent;
desktopEvent.set(null);
- // TODO: handle eventFiles (attach to chat)
+ // Attach screenshot images from desktop (e.g. Spotlight region capture)
+ if (eventFiles?.length) {
+ for (const ef of eventFiles) {
+ files = [
+ ...files,
+ {
+ type: 'image',
+ url: ef.dataUrl,
+ name: ef.name
+ }
+ ];
+ }
+ }
if (query) {
messageInput?.setText(query);
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 0f032c6642..b028236533 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -711,8 +711,8 @@
await goto(event.data.path);
return;
}
- if (event.type === 'query' && event.data?.query) {
- desktopEvent.set({ query: event.data.query });
+ if (event.type === 'query' && (event.data?.query || event.data?.files?.length)) {
+ desktopEvent.set({ query: event.data.query, files: event.data.files });
await goto('/');
return;
}