7169 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek 41f256321b refac 2026-06-29 05:46:47 -05:00
Timothy Jaeryang Baek 44b9463498 refac 2026-06-29 05:44:47 -05:00
Timothy Jaeryang Baek 396d9ac181 refac 2026-06-29 05:27:51 -05:00
Timothy Jaeryang Baek 0eba3df119 refac 2026-06-29 05:21:49 -05:00
Timothy Jaeryang Baek fa76764c3b refac 2026-06-29 05:16:57 -05:00
Timothy Jaeryang Baek 83ec36cd38 refac 2026-06-29 05:15:55 -05:00
Timothy Jaeryang Baek cdd7b88bec refac 2026-06-29 05:14:57 -05:00
Timothy Jaeryang Baek 8927c9bb3d refac 2026-06-29 05:14:34 -05:00
maco c65b29ec0f fix: handle MCP resource content type blob and URI variants (#25260) 2026-06-29 05:12:11 -05:00
Timothy Jaeryang Baek 0be069c165 refac 2026-06-29 05:04:00 -05:00
Timothy Jaeryang Baek 416baef813 refac 2026-06-29 04:57:28 -05:00
Timothy Jaeryang BaekandPedro Machado 45fea34bd0 refac
Co-Authored-By: Pedro Machado <45520277+pedrofcm@users.noreply.github.com>
2026-06-29 04:46:56 -05:00
Timothy Jaeryang Baek 953432b5fe refac 2026-06-29 04:43:40 -05:00
Timothy Jaeryang Baek e5b5e5917b refac 2026-06-29 04:43:08 -05:00
Timothy Jaeryang Baek 67c9de8efd refac 2026-06-29 04:42:59 -05:00
Timothy Jaeryang Baek a3b487422d refac 2026-06-29 04:42:02 -05:00
Alberto de la Cruz 718b226177 fix: discover MCP Protected Resource Metadata when server does not return 401 (#25980)
get_protected_resource_metadata() only attempted RFC 9728 discovery when
the anonymous `initialize` probe returned 401 with a WWW-Authenticate
header. Some remote MCP servers — notably Google's Gmail/Drive/Calendar
MCPs (gmailmcp.googleapis.com, etc.) — answer 200 to an anonymous
initialize, so OAuth scope and authorization-server discovery silently
failed and connections to them could not be established.

Run the discovery regardless of the probe's HTTP status: prefer the
resource_metadata URL from WWW-Authenticate when present, and otherwise
fall back to the RFC 9728 §4.2 well-known URIs. The trade-off is a couple
of extra well-known GETs during MCP connection setup for servers that
expose no PRM document; behavior for 401-responding servers is unchanged.
2026-06-29 04:34:36 -05:00
Timothy Jaeryang Baek f8ec63203c refac 2026-06-29 04:31:43 -05:00
Timothy Jaeryang Baek fd7a59d37a refac 2026-06-29 04:27:48 -05:00
Timothy Jaeryang Baek ab84bbf08c refac 2026-06-29 04:19:33 -05:00
Timothy Jaeryang Baek d3676b4f71 refac 2026-06-29 04:11:46 -05:00
Timothy Jaeryang Baek c4688b958d refac 2026-06-29 04:03:06 -05:00
Timothy Jaeryang Baek 33b91bd8ae refac 2026-06-29 03:58:00 -05:00
Classic298 0fc630b34b fix: per-user model cache used static key= (cross-user model exposure) (#25783)
routers/openai.py and routers/ollama.py decorated get_all_models with
`@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC
cache key: get_cache_key returns `self.key` verbatim (the lambda object)
without calling it, so every caller collides to ONE shared entry within
the TTL. The intended per-user namespacing never happened — one user's
permission-filtered model list could be served to another user (or an
anonymous caller) during the cache window.

The per-call hook is `key_builder=` (called as key_builder(func, *args,
**kwargs)). Switch both sites to key_builder with a (func, request,
user=None) signature so the key is built per call. user=None mirrors
ollama's optional-user signature and stays correct whether user is passed
positionally, as a kwarg, or omitted.

Verified offline: old form returns the same key object for distinct users
(collision); new form yields distinct openai_all_models_<id> /
ollama_all_models_<id> keys, and the unauthenticated base key when no user.
These two were the only @cached(key=lambda ...) sites in the backend.
2026-06-29 03:51:02 -05:00
G30 ee11069ef2 perf(ollama): offload multi-GB model file I/O with asyncio.to_thread (#25829)
The Ollama model upload and download handlers perform three stages
of sync file I/O on multi-GB model files inside async handlers:

1. Persist upload — file.file.read() + write() in a loop
2. SHA-256 hash — calculate_sha256() reads the file sequentially
3. Read for blob push — open().read() loads entire model into memory

All three stages block the event loop for the duration of the I/O.
For a 4GB model, each stage freezes the event loop for 10+ seconds
while every other user's request stalls.

Wrap each blocking stage in asyncio.to_thread() in both handlers:

- upload_model(): persist upload, calculate_sha256, blob read
- download_file_stream(): calculate_sha256, blob read

Benchmark (full pipeline: write + SHA-256 + read back, 3 trials):
- 200MB: max jitter 145ms → 1ms (145x improvement)
- 500MB: max jitter 1,026ms → 1ms (1,026x improvement)

File I/O is pure I/O — no GIL contention. asyncio.to_thread()
completely eliminates event loop blocking.
2026-06-29 03:48:48 -05:00
Classic298andClaude Opus 4.8 a66477b710 fix: bind channel thread parent/reply to the URL channel (#25766)
GET /api/v1/channels/{id}/messages/{message_id}/thread authorized only the URL channel, but get_messages_by_parent_id() appended the thread parent (loaded by id) without checking it belonged to that channel, so a caller could read a message from a channel they cannot access by passing its id as the thread root. Require the parent to be in the requested channel before returning it, and reject a posted parent_id/reply_to_id that does not belong to the channel.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:46:50 -05:00
G30 7b1aa749eb perf(retrieval): make URL fetch and file hash non-blocking with asyncio.to_thread (#25822)
- Wrap get_content_from_url() in asyncio.to_thread() inside
  get_sources_from_items() to prevent sync requests.get() from blocking
  the event loop when users attach URL sources to chat messages.
  The same function was already properly wrapped at retrieval.py:1839.

- Wrap hashlib.sha256(contents).hexdigest() in asyncio.to_thread()
  inside upload_file_handler() to prevent CPU-bound hashing from
  blocking the event loop during file uploads (44ms/100MB, scales
  linearly with file size).
2026-06-29 03:46:24 -05:00
Timothy Jaeryang Baek a70a6589af refac 2026-06-29 03:42:36 -05:00
Classic298 e98730b20d fix: pass web search results to model when embedding & retrieval enabled (#25600)
Web search results stored as a vector collection were silently dropped
before retrieval when BYPASS_RETRIEVAL_ACCESS_CONTROL is False (the
default). The server-generated web_search file item carries a
'collection_name' but its 'web_search' type is not matched by any
explicit dispatch branch in get_sources_from_items, so it fell through
to the untrusted client-supplied collection_name branch and was ignored.

Add an explicit branch for type == 'web_search' items so the collection
is queried again. Access control is preserved: the collection still
passes through filter_accessible_collections, which already allowlists
web-search-* and bypasses only for admins.

Regression introduced when the retrieval access-control hardening gated
the bare collection_name fallback behind BYPASS_RETRIEVAL_ACCESS_CONTROL.
2026-06-29 03:34:55 -05:00
Timothy Jaeryang Baek 3fd0384ffc refac 2026-06-29 03:21:41 -05:00
Classic298andClaude Opus 4.8 5796d44363 fix: defer missing-local-embedding error so it can't crash boot (#25683)
get_embedding_function runs at import time (main.py), so the empty-engine
+ no-model guard added in 55ca719b raised ValueError during startup and
bricked the instance: a blank embedding model set via the UI made the app
unbootable, with no way back into settings to undo it. Move the check into
the returned coroutine so construction always succeeds and the error only
fires when something actually embeds, surfacing as a clear request error
instead of the old cryptic 'NoneType has no attribute encode'.


Fixes #25634
Fixes #25165

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:21:09 -05:00
G30 6e14e446cb fix(api): handle orphaned shared_chat rows when unsharing (#25632) 2026-06-29 03:20:46 -05:00
Timothy Jaeryang Baek 33cd199e6d refac 2026-06-29 03:16:59 -05:00
Timothy Jaeryang Baek 390e200f76 refac 2026-06-29 03:13:17 -05:00
Timothy Jaeryang Baek 7be009649a refac 2026-06-29 02:57:58 -05:00
G30andTim Baek 6fdf9b4340 perf(auth): make password hashing non-blocking and batch CSV user import (#25804)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-29 02:45:39 -05:00
Timothy Jaeryang Baek 2bdd2ab94e refac 2026-06-29 02:43:14 -05:00
G30andTim Baek 6ea591491e perf(images): offload validate_url() DNS resolution with asyncio.to_thread (#25825)
validate_url() calls socket.getaddrinfo() for SSRF protection, which
blocks the event loop for 100-700ms per DNS lookup. This affects:

- Image generation (get_image_data) — every external image URL
- Image editing (load_url_image) — every external image URL
- OAuth profile pictures (_process_picture_url) — every login
- Webhook delivery (post_webhook) — every notification
- Image base64 conversion (get_image_base64_from_url) — chat images

Wrap all 5 async call sites in asyncio.to_thread() so DNS resolution
runs in the thread pool. The event loop remains free to serve other
requests during the lookup.

Benchmark (3 domains, 3 trials averaged):
- BEFORE: max jitter 479ms, 1 blocked ping per trial
- AFTER:  max jitter 1ms, 0 blocked pings (324x improvement)

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-29 02:31:24 -05:00
Timothy Jaeryang Baek ac3449cac9 refac 2026-06-29 02:26:27 -05:00
G30 71d6212ab8 fix(redis): use await asyncio.sleep() instead of time.sleep() in async generator (#25823)
time.sleep() inside the _wrap_async_gen() async generator blocks the
entire event loop during Sentinel failover retries. The sibling method
_wrap_async_call() already correctly uses await asyncio.sleep().
2026-06-29 02:25:46 -05:00
Timothy Jaeryang Baek 61a2672215 refac 2026-06-29 02:20:54 -05:00
Classic298andClaude Opus 4.8 386ac95814 fix: scope Socket.IO event-caller to the requesting user's own session (#25763)
get_event_call() routed execute:python / execute:tool events to a client-supplied session_id after only checking the session was connected, not that it belonged to the requester. Verify the target session is owned by the requesting user (metadata user_id) before delivering, so a client cannot route code/tool execution into another user's session.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:17:40 -05:00
914039ac81 Escape voice-derived attributes in Azure TTS SSML (#25776)
The Azure TTS handler (_tts_azure) interpolated the user-supplied voice,
and the locale derived from it, into the SSML xml:lang and <voice name>
attributes without XML-escaping, while the text body was already escaped
(2e75c6dbd). Escape both attributes too, so every user-derived value in
the SSML document is consistently encoded.

Co-authored-by: alanturing881 <alanturing881@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:16:37 -05:00
G30 ff25ccca65 feat: add repeat/recurrence dropdown to calendar event modal (#25865)
* feat(calendar): add repeat/recurrence dropdown to event modal

* fix(calendar): anchor recurring event expansion to event start date

The expand_recurring_event() utility used the view range start as dtstart,
causing FREQ=WEEKLY events to land on the wrong day of the week. Use the
event's original start date instead so recurrence patterns stay correct.
2026-06-29 02:16:19 -05:00
Classic298andClaude Opus 4.8 01198eaeef Close DNS-rebinding SSRF gap in get_content_from_url probe (#25775)
The web-ingest probe in get_content_from_url validated the URL at resolve
time (validate_url) but then fetched with a bare requests.get, which
re-resolves the hostname at connect time. An attacker-controlled name
server can answer with a public IP during validation and an internal IP
at connect, reaching cloud metadata / loopback / internal services (blind
always; binary content-types are read back to the caller). The
connection-layer guard (#24759) that closes this for the SafeWebBaseLoader
path was never mounted on this probe.

Route the probe through the same _SSRFSafeAdapter the loader uses, so the
resolution that feeds the TCP connect is re-validated against the
global-IP check.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:15:31 -05:00
Classic298 15d96b1f2a fix: chroma has_collection always returns False (name vs Collection) (#25780)
has_collection did `collection_name in self.client.list_collections()`,
but chromadb's list_collections() returns Collection objects (1.x), not
name strings — so the membership test is always False, even when the
collection exists. Compare against the collection names instead (with a
hasattr guard tolerating versions that yield plain names).

Found via the dependency-contract test suite (unit/deps/test_chromadb.py).
2026-06-29 02:15:05 -05:00
G30 342539f1e1 perf(audio): make ML model loading non-blocking with asyncio.to_thread (#25806) 2026-06-29 02:14:15 -05:00
Classic298 c3394288bb fix: repair Mistral OCR async upload (aiohttp.streams.FilePayload removed) (#25779)
_upload_file_async built its multipart body with
`aiohttp.streams.FilePayload(...)`, which no longer exists in aiohttp
(payload classes live in aiohttp.payload, and there is no FilePayload).
The reference sits inside a lazy closure, so import succeeds and only the
async Mistral OCR file-upload path blows up at runtime with
AttributeError on every call.

Mirror the working sync path: open the file in a context manager and let
MultipartWriter.append(file_obj, {...}) build a streaming
BufferedReaderPayload, with the POST issued inside the open() block so
the handle stays valid for the whole upload. Preserves the streaming /
memory-efficiency intent.

Found via the dependency-contract test suite (unit/deps/test_aiohttp.py),
which pins aiohttp.streams.FilePayload as absent.
2026-06-29 02:05:54 -05:00
Classic298 8a016931f1 refac(telemetry): drop deprecated semconv SpanAttributes subclass (#25784)
constants.py subclassed opentelemetry.semconv.trace.SpanAttributes, which
is deprecated since semconv 1.25.0 (emits a DeprecationWarning; the pinned
0.63b1 has it live). Source the legacy span-attribute keys from the
non-deprecated _incubating attribute modules instead:
  http.url / http.method / http.status_code  <- _incubating http_attributes
  db.name / db.statement / db.operation      <- _incubating db_attributes
The stable http module renamed these (http.request.method, ...), so only
the incubating module preserves the original values. Custom keys
(db.instance/type/ip/port, error.*, result.*) stay literals.

Verified: emitted attribute keys are byte-identical before/after for every
key instrumentors.py reads, and importing constants no longer emits a
DeprecationWarning.
2026-06-29 02:05:34 -05:00
G30 e69ce6e1c6 perf(channels): batch N+1 queries for reactions and thread replies (#25831)
Replace per-message database queries with batch IN-clause queries in
channel message handlers. This eliminates the N+1 query pattern that
caused ~102 queries per channel page load (50 messages × 2 queries each).

Changes:
- Add get_reactions_by_message_ids() to MessageTable: single query
  fetches all reactions for multiple messages using IN clause with
  User JOIN, returns dict[message_id, list[Reactions]]
- Add get_thread_reply_counts_by_message_ids() to MessageTable: single
  GROUP BY aggregate query returns (count, max_created_at) per parent,
  replacing full object loads just to call len()
- Refactor get_channel_messages(): 102 → 4 queries per page
- Refactor get_pinned_channel_messages(): 22 → 3 queries per page
- Refactor get_channel_thread_messages(): 53 → 4 queries per page
- Refactor send_notification(): N+1 membership check → batch set lookup
2026-06-29 02:05:16 -05:00