Skip to content

Commit adc9076

Browse files
tjbckClassic298jmleksanathoikShirasawaSama
authored
* refac * refac * refac * Merge pull request open-webui#24356 from Classic298/patch-1 doc/chore: Update SECURITY.md * refac * refac * refac * refac * refac * chore: Update SECURITY.md (open-webui#24363) * Update SECURITY.md * Update SECURITY.md * Implement asynchronous database ping for health checks (open-webui#24380) * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * fix: prevent STT from blocking the uvicorn event loop (open-webui#24338) The transcription endpoint was async but called the synchronous transcribe() function directly, blocking the single-threaded uvicorn event loop for the entire duration of inference. This caused all HTTP and WebSocket connections to stall for every user on the instance during STT processing. - Add asyncio import - Use async UploadFile.read() instead of synchronous file.file.read() - Offload the blocking transcribe() call via asyncio.to_thread() Closes open-webui#24169 * refac * fix: open file content in new window when clicking file name in FileItemModal (open-webui#24125) Previously, clicking the file name link did not open the file content because the condition checked `!isPDF && item.url`, which failed for `type === 'file'` items that use an ID-based URL path. Update the condition to trigger on `item.type === 'file' || item.url`, and resolve the correct URL by extracting `fileId` from `item.id` or `item.tempId` instead of using `item.url` directly as the file identifier. * refac * refac * refac * refac * refac * refac * refac * refac * refac * Refactor file processing to use asyncio for transcribing, improving concurrency. (open-webui#24379) * Apply validate_profile_image_url to ChannelWebhookForm.profile_image_url (open-webui#24370) * refac * refac * refac * refac * refac * fix: stream GET /chats/all to prevent OOM on large chat histories (open-webui#24461) Convert the /chats/all endpoint from loading all user chats into memory at once to a streaming NDJSON response that fetches chats in batches of 100. This prevents Out-of-Memory crashes for users with large chat histories. Backend: Added async generator that paginates through chats with short-lived DB sessions per batch (critical for SQLite lock release). Frontend: Updated getAllChats to consume the NDJSON stream via ReadableStream reader, accumulating results for the export file. Ref: open-webui#22206 * refac * refac * refac * refac * refac * refac * Enhance CommitSessionMiddleware to allow health probes to bypass session management, ensuring faster and more reliable responses. (open-webui#24384) * refac * refac * refac * refac * refac * refac * refac: apply DOMPurify to excel and office HTML render assignments (open-webui#24468) * I18n/improve chinese translation (open-webui#24194) * i18n: improve zh-CN translation * i18n: improve zh-TW translation * perf(prompts): filter prompt list in SQL instead of N+1 has_access loop (open-webui#24288) get_prompts_by_user_id used to fetch every active prompt (with users + all access grants), then call AccessGrants.has_access() once per prompt that the user did not own. With 600+ prompts this issued ~600 extra round-trips per request and explained the multi-second delay reported in the GET /api/v1/prompts and /api/v1/prompts/tags endpoints for non-admin users. Push the access check into a single SQL query via the existing AccessGrants.has_permission_filter (EXISTS subquery), so only accessible rows come back from the DB. Users and access grants for the surviving rows are still batch-fetched, no N+1 anywhere on this path. Co-authored-by: Claude <noreply@anthropic.com> * refac * refac * Update catalan translation.json (open-webui#24174) * perf(prompts): make /tags fetch only the tags column with SQL access filter (open-webui#24287) Non-admin GET /api/v1/prompts/tags went through get_prompts_by_user_id, which loaded every active prompt with its full content/data/meta plus owner records and all access grants, then ran one has_access query per prompt that wasn't owned by the caller - all so the endpoint could collapse the result to a sorted tag list. With 600 prompts this took several seconds while the admin path (a single SELECT) returned in <1s. Add Prompts.get_tags_by_user_id which selects only the tags column and applies the same EXISTS-based access filter used by /list. Also tighten the admin get_tags to project just the tags column instead of full rows. The endpoint is now one DB query (plus one for groups), no row hydration, no N+1. Co-authored-by: Claude <noreply@anthropic.com> * refac * refac * i18n: Add Tagalog (Filipino) translation (open-webui#24254) Signed-off-by: Adam Tao <tcx4c70@gmail.com> Co-authored-by: Tim Baek <tim@openwebui.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: Teay <pythontogoplease@gmail.com> Co-authored-by: tcx4c70 <tcx4c70@gmail.com> Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: RomualdYT <romuald@gameurnews.fr> Co-authored-by: Lucas <lucas@vanosenbruggen.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: Constantine <Runixer@gmail.com> * refac * refac * refac * style(env): satisfy ruff (datetime alias, line length, identity check) (open-webui#24118) * Korean Translation Update (open-webui#24087) Signed-off-by: Adam Tao <tcx4c70@gmail.com> Co-authored-by: Tim Baek <tim@openwebui.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: Teay <pythontogoplease@gmail.com> Co-authored-by: tcx4c70 <tcx4c70@gmail.com> Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: RomualdYT <romuald@gameurnews.fr> Co-authored-by: Lucas <lucas@vanosenbruggen.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: Constantine <Runixer@gmail.com> * refac * refac * refac * refac * refac * refac * refac * refac * refac * feat: brave search llm context * refac * refac * refac * refac * refac * refac * refac * fix(mcp): remove asyncio.wait_for/shield from MCP cleanup in chat handler (open-webui#24105) asyncio.wait_for() and asyncio.shield() create new asyncio Tasks which violate anyio cancel-scope task-ownership rules. The MCPClient's exit_stack contains anyio resources (streamable_http transport) that use anyio cancel scopes. When exited from a different task, anyio raises 'Attempted to exit a cancel scope that isn't the current task's current cancel scope' as a BaseException. This BaseException propagates through the finally block, discards the completed response return value, and surfaces as a 500 Internal Server Error / 'No response returned.' - silently swallowing successful MCP tool calls and blocking the chat endpoint. Fix: call client.disconnect() directly in a simple loop. MCPClient.disconnect() already catches BaseException internally (see prior commit), so no wrapper is needed. Signed-off-by: Adam Tao <tcx4c70@gmail.com> Co-authored-by: Tim Baek <tim@openwebui.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: Teay <pythontogoplease@gmail.com> Co-authored-by: tcx4c70 <tcx4c70@gmail.com> Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: RomualdYT <romuald@gameurnews.fr> Co-authored-by: Lucas <lucas@vanosenbruggen.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: Constantine <Runixer@gmail.com> Co-authored-by: Circe (Claude Code Sonnet 4.6) <circe@athena-council.org> Co-authored-by: Claude <noreply@anthropic.com> * fix:image url validation and signout post (open-webui#24420) * refac(routers): reject external URLs in profile/model image handlers * refac(ui): centralize image URL validation in safeImageUrl helper * refac(auths): make signout POST-only * refac: gate external profile image redirect behind ENABLE_PROFILE_IMAGE_URL_FORWARDING Restore the 302 redirect for external http(s) profile image URLs in the user and model profile-image endpoints, but gate it behind a new ENABLE_PROFILE_IMAGE_URL_FORWARDING env flag (default: True). Existing deployments that rely on external profile image forwarding continue to work unchanged. Operators who want to suppress the redirect (to prevent client-side IP/UA/Referer leaks) can set the flag to False. * refac * refac * refac * refac * refac * refac * refac * refac * refac * chore: format * chore: changelog (open-webui#24358) * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * changelog * doc: changelog * refac * refac * refac * chore: format * refac --------- Signed-off-by: Adam Tao <tcx4c70@gmail.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: Athanasios Oikonomou <athoik@gmail.com> Co-authored-by: Shirasawa <764798966@qq.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Aleix Dorca <aleixdorca@mac.com> Co-authored-by: Vincent Agra <agravj007@gmail.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: Teay <pythontogoplease@gmail.com> Co-authored-by: tcx4c70 <tcx4c70@gmail.com> Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com> Co-authored-by: RomualdYT <romuald@gameurnews.fr> Co-authored-by: Lucas <lucas@vanosenbruggen.com> Co-authored-by: Constantine <Runixer@gmail.com> Co-authored-by: Shamil <ashm.tech@proton.me> Co-authored-by: Cyp <cypher9715@naver.com> Co-authored-by: looselyhuman <fieldian@gmail.com> Co-authored-by: Circe (Claude Code Sonnet 4.6) <circe@athena-council.org>
2 parents 8dae237 + 413dcae commit adc9076

169 files changed

Lines changed: 9981 additions & 3337 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 98 additions & 0 deletions
Large diffs are not rendered by default.

backend/open_webui/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import os
33
import random
4+
import sys
45
from pathlib import Path
56
from typing import Annotated
67

@@ -68,12 +69,18 @@ def serve(
6869
import open_webui.main # noqa: F401
6970
from open_webui.env import UVICORN_WORKERS # Import the workers setting
7071

72+
# On Windows, uvicorn's default loop factory hardcodes ProactorEventLoop,
73+
# which is incompatible with psycopg v3 async. Setting loop='none' lets
74+
# asyncio.run() respect the WindowsSelectorEventLoopPolicy set in db.py.
75+
loop = 'none' if sys.platform == 'win32' else 'auto'
76+
7177
uvicorn.run(
7278
'open_webui.main:app',
7379
host=host,
7480
port=port,
7581
forwarded_allow_ips='*',
7682
workers=UVICORN_WORKERS,
83+
loop=loop,
7784
)
7885

7986

backend/open_webui/config.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,17 @@ async def _async_persist(self, key):
328328
except Exception as e:
329329
log.error(f'Failed to async-persist config key {key}: {e}')
330330

331+
def _sync_to_redis(self):
332+
"""Push all in-memory config values to Redis, e.g. after a bulk import."""
333+
if not self._redis or not ENABLE_PERSISTENT_CONFIG:
334+
return
335+
for key, pc in self._state.items():
336+
redis_key = f'{self._redis_key_prefix}:config:{key}'
337+
try:
338+
self._redis.set(redis_key, json.dumps(pc.value))
339+
except Exception as e:
340+
log.error(f'Failed to sync config key {key} to Redis: {e}')
341+
331342
def __getattr__(self, key):
332343
if key not in self._state:
333344
raise AttributeError(f"Config key '{key}' not found")
@@ -1197,6 +1208,12 @@ def reachable(host: str, port: int) -> bool:
11971208
tool_server_connections,
11981209
)
11991210

1211+
OAUTH_CLIENT_TIMEOUT = PersistentConfig(
1212+
'OAUTH_CLIENT_TIMEOUT',
1213+
'oauth.client.timeout',
1214+
os.environ.get('OAUTH_CLIENT_TIMEOUT', ''),
1215+
)
1216+
12001217
####################################
12011218
# TERMINAL_SERVER
12021219
####################################
@@ -1299,10 +1316,16 @@ def reachable(host: str, port: int) -> bool:
12991316
[],
13001317
)
13011318

1319+
try:
1320+
default_model_metadata = json.loads(os.environ.get('DEFAULT_MODEL_METADATA', '{}'))
1321+
except Exception as e:
1322+
log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}')
1323+
default_model_metadata = {}
1324+
13021325
DEFAULT_MODEL_METADATA = PersistentConfig(
13031326
'DEFAULT_MODEL_METADATA',
13041327
'models.default_metadata',
1305-
{},
1328+
default_model_metadata,
13061329
)
13071330

13081331
try:
@@ -1477,6 +1500,10 @@ def reachable(host: str, port: int) -> bool:
14771500

14781501
USER_PERMISSIONS_CHAT_SHARE = os.environ.get('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true'
14791502

1503+
USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING = (
1504+
os.environ.get('USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
1505+
)
1506+
14801507
USER_PERMISSIONS_CHAT_EXPORT = os.environ.get('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true'
14811508

14821509
USER_PERMISSIONS_CHAT_STT = os.environ.get('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true'
@@ -1557,6 +1584,7 @@ def reachable(host: str, port: int) -> bool:
15571584
'public_skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING,
15581585
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
15591586
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
1587+
'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING,
15601588
},
15611589
'access_grants': {
15621590
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
@@ -2051,6 +2079,12 @@ class BannerModel(BaseModel):
20512079
os.environ.get('VOICE_MODE_PROMPT_TEMPLATE', ''),
20522080
)
20532081

2082+
ENABLE_VOICE_MODE_PROMPT = PersistentConfig(
2083+
'ENABLE_VOICE_MODE_PROMPT',
2084+
'task.voice.prompt.enable',
2085+
os.environ.get('ENABLE_VOICE_MODE_PROMPT', 'True').lower() == 'true',
2086+
)
2087+
20542088
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant.
20552089
20562090
Everything you say will be spoken aloud.
@@ -2627,13 +2661,17 @@ class BannerModel(BaseModel):
26272661
)
26282662

26292663

2630-
ENABLE_ONEDRIVE_PERSONAL = os.environ.get('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true'
2631-
ENABLE_ONEDRIVE_BUSINESS = os.environ.get('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true'
2632-
26332664
ONEDRIVE_CLIENT_ID = os.environ.get('ONEDRIVE_CLIENT_ID', '')
26342665
ONEDRIVE_CLIENT_ID_PERSONAL = os.environ.get('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID)
26352666
ONEDRIVE_CLIENT_ID_BUSINESS = os.environ.get('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID)
26362667

2668+
ENABLE_ONEDRIVE_PERSONAL = os.environ.get('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' and bool(
2669+
ONEDRIVE_CLIENT_ID_PERSONAL
2670+
)
2671+
ENABLE_ONEDRIVE_BUSINESS = os.environ.get('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' and bool(
2672+
ONEDRIVE_CLIENT_ID_BUSINESS
2673+
)
2674+
26372675
ONEDRIVE_SHAREPOINT_URL = PersistentConfig(
26382676
'ONEDRIVE_SHAREPOINT_URL',
26392677
'onedrive.sharepoint_url',
@@ -3252,7 +3290,7 @@ class BannerModel(BaseModel):
32523290
WEB_SEARCH_TRUST_ENV = PersistentConfig(
32533291
'WEB_SEARCH_TRUST_ENV',
32543292
'rag.web.search.trust_env',
3255-
os.getenv('WEB_SEARCH_TRUST_ENV', 'False').lower() == 'true',
3293+
os.getenv('WEB_SEARCH_TRUST_ENV', 'True').lower() == 'true',
32563294
)
32573295

32583296

@@ -3310,6 +3348,12 @@ class BannerModel(BaseModel):
33103348
os.getenv('BRAVE_SEARCH_API_KEY', ''),
33113349
)
33123350

3351+
BRAVE_SEARCH_CONTEXT_TOKENS = PersistentConfig(
3352+
'BRAVE_SEARCH_CONTEXT_TOKENS',
3353+
'rag.web.search.brave_search_context_tokens',
3354+
int(os.getenv('BRAVE_SEARCH_CONTEXT_TOKENS', '8192')),
3355+
)
3356+
33133357
KAGI_SEARCH_API_KEY = PersistentConfig(
33143358
'KAGI_SEARCH_API_KEY',
33153359
'rag.web.search.kagi_search_api_key',
@@ -3947,6 +3991,19 @@ class BannerModel(BaseModel):
39473991
],
39483992
)
39493993

3994+
AUDIO_STT_ALLOWED_EXTENSIONS = PersistentConfig(
3995+
'AUDIO_STT_ALLOWED_EXTENSIONS',
3996+
'audio.stt.allowed_extensions',
3997+
[
3998+
ext.strip()
3999+
for ext in os.environ.get(
4000+
'AUDIO_STT_ALLOWED_EXTENSIONS',
4001+
'mp3,wav,m4a,webm,ogg,flac,mp4,mpga,mpeg',
4002+
).split(',')
4003+
if ext.strip()
4004+
],
4005+
)
4006+
39504007
AUDIO_STT_AZURE_API_KEY = PersistentConfig(
39514008
'AUDIO_STT_AZURE_API_KEY',
39524009
'audio.stt.azure.api_key',

backend/open_webui/env.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1+
import datetime as dt
12
import importlib.metadata
23
import json
34
import logging
45
import os
56
import pkgutil
6-
import sys
7+
import re
78
import shutil
9+
import sys
810
import traceback
9-
from datetime import datetime, timezone
11+
from pathlib import Path
1012
from typing import Any
1113
from uuid import uuid4
12-
from pathlib import Path
13-
from cryptography.hazmat.primitives import serialization
14-
import re
15-
1614

1715
import markdown
1816
from bs4 import BeautifulSoup
17+
from cryptography.hazmat.primitives import serialization
18+
1919
from open_webui.constants import ERROR_MESSAGES
2020

2121
####################################
@@ -43,7 +43,8 @@
4343

4444
DOCKER = os.environ.get('DOCKER', 'False').lower() == 'true'
4545

46-
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
46+
# device type for embedding models - "cpu" (default), "cuda" (nvidia gpu required), or "mps" (apple silicon)
47+
# choosing this correctly can lead to better performance
4748
USE_CUDA = os.environ.get('USE_CUDA_DOCKER', 'false')
4849

4950
if USE_CUDA.lower() == 'true':
@@ -87,7 +88,7 @@ class JSONFormatter(logging.Formatter):
8788

8889
def format(self, record: logging.LogRecord) -> str:
8990
log_entry: dict[str, Any] = {
90-
'ts': datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(timespec='milliseconds'),
91+
'ts': dt.datetime.fromtimestamp(record.created, tz=dt.UTC).isoformat(timespec='milliseconds'),
9192
'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()),
9293
'msg': record.getMessage(),
9394
'caller': record.name,
@@ -180,7 +181,7 @@ def parse_section(section):
180181

181182
try:
182183
changelog_path = BASE_DIR / 'CHANGELOG.md'
183-
with open(str(changelog_path.absolute()), 'r', encoding='utf8') as file:
184+
with open(str(changelog_path.absolute()), encoding='utf8') as file:
184185
changelog_content = file.read()
185186

186187
except Exception:
@@ -248,6 +249,17 @@ def parse_section(section):
248249

249250
ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'true'
250251

252+
####################################
253+
# ENABLE_PROFILE_IMAGE_URL_FORWARDING
254+
####################################
255+
256+
# When True (default), the user and model profile-image endpoints
257+
# honour external http(s) URLs stored in profile_image_url by issuing a
258+
# 302 redirect to the original origin. Set to False to suppress the
259+
# redirect (prevents client-side IP/UA/Referer leaks to attacker-
260+
# controlled origins) and fall through to the default image instead.
261+
ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
262+
251263
####################################
252264
# WEBUI_BUILD_HASH
253265
####################################
@@ -339,7 +351,7 @@ def parse_section(section):
339351

340352
DATABASE_POOL_SIZE = os.environ.get('DATABASE_POOL_SIZE', None)
341353

342-
if DATABASE_POOL_SIZE != None:
354+
if DATABASE_POOL_SIZE is not None:
343355
try:
344356
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
345357
except Exception:
@@ -652,7 +664,7 @@ def parse_section(section):
652664
-----BEGIN PUBLIC KEY-----
653665
{LICENSE_PUBLIC_KEY}
654666
-----END PUBLIC KEY-----
655-
""".encode('utf-8')
667+
""".encode()
656668
)
657669

658670

backend/open_webui/functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ async def get_function_params(function_module, form_data, user, extra_params=Non
284284
if params:
285285
system = params.pop('system', None)
286286
form_data = apply_model_params_to_body_openai(params, form_data)
287-
form_data = apply_system_prompt_to_body(system, form_data, metadata, user)
287+
form_data = await apply_system_prompt_to_body(system, form_data, metadata, user)
288288

289289
pipe_id = get_pipe_id(form_data)
290290
function_module = await get_function_module_by_id(request, pipe_id)

backend/open_webui/internal/db.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
import json
34
import logging
45
from contextlib import asynccontextmanager, contextmanager
@@ -332,6 +333,15 @@ def get_session():
332333
# all work without any stripping or translation.
333334
ASYNC_SQLALCHEMY_DATABASE_URL = _make_async_url(SQLALCHEMY_DATABASE_URL)
334335

336+
# psycopg v3 cannot run in async mode under Windows' default
337+
# ProactorEventLoop — switch to SelectorEventLoop before creating
338+
# the async engine. This runs at import time, which is early enough
339+
# to cover every entry point (workers, reload, direct invocations).
340+
if sys.platform == 'win32' and _is_postgres_url(DATABASE_URL):
341+
import asyncio
342+
343+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
344+
335345
if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL:
336346
# Generous default — async coroutines + no session sharing = high connection demand.
337347
_sqlite_pool_size = DATABASE_POOL_SIZE if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0 else 512

0 commit comments

Comments
 (0)