Commit Graph
227 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek 95d590b360 refac 2026-07-26 23:03:32 -04:00
Timothy Jaeryang Baek 8b206de48e refac 2026-07-26 22:59:23 -04:00
Classic298 fb1f1a3c92 perf: parse scraped web pages with lxml, not html.parser (#27439)
Every page pulled in by web search and web RAG is parsed with BeautifulSoup's `html.parser`, a pure-Python parser. It is the slowest option bs4 offers, and it is being handed 300 KiB to 1.5 MiB documents, several per query. `SafeWebBaseLoader` inherits `default_parser = "html.parser"` from langchain's `WebBaseLoader` and never overrides it, so this is an upstream default carried by accident, not a decision anyone made for Open WebUI.

`default_parser` is the single chokepoint for both the sync `_scrape()` path and the async `ascrape_all()` path, so one `setdefault` covers everything and an explicit caller override still wins.

lxml is already in the tree as a transitive hard dependency of ddgs, python-pptx and unstructured, so nothing new enters the image and `uv.lock` already resolves it at 6.1.1. The pin makes it explicit and closes a latent failure: bs4's `"xml"` feature, already used for `.xml` URLs in `_unpack_fetch_results()`, requires lxml and would raise `FeatureNotFound` the day that transitive dependency moves.

## Benchmarks

37 real pages, 13.8 MiB of HTML, median of 5 runs each. The timed operation is `BeautifulSoup(html, parser)` plus `get_text()` plus `extract_metadata()`, which is exactly what the loader does per page. bs4 4.14.3, lxml 6.1.1, CPython 3.12.

| | html.parser | lxml | |
|---|---|---|---|
| 37 pages, 13.8 MiB total | 1611.0ms | 1151.3ms | 1.4x faster, 460ms saved |

Largest pages:

| page | size | html.parser | lxml | speedup |
|---|---|---|---|---|
| pypi.org/project/aiohttp/ | 1259 KiB | 243.75ms | 180.51ms | 1.4x |
| gnu.org/software/bash/manual/bash.html | 1017 KiB | 257.97ms | 178.99ms | 1.4x |
| rfc-editor.org/rfc/rfc9110.html | 1157 KiB | 205.94ms | 154.87ms | 1.3x |
| docs.aiohttp.org/en/stable/client_reference.html | 403 KiB | 108.62ms | 84.93ms | 1.3x |
| ollama.com/library | 779 KiB | 117.55ms | 73.64ms | 1.6x |
| theregister.com | 1052 KiB | 88.32ms | 60.12ms | 1.5x |
| kubernetes.io/docs/concepts/services-networking/service/ | 563 KiB | 72.18ms | 43.43ms | 1.7x |
| docs.python.org/3/library/socket.html | 301 KiB | 71.88ms | 49.04ms | 1.5x |

Ranges from 1.1x to 1.7x, and the win grows with page size. A ten result web search sheds roughly 125ms of parsing. Because the async path builds its soups inline in `_unpack_fetch_results()`, that is 125ms the event loop spends parsing HTML instead of serving other users' streams. Pages under about 10 KiB are marginally slower under lxml due to fixed setup cost, which is worth nothing either way.

## Output verification

The risk in changing parser is silently different extracted text, so that was measured rather than assumed. Across all 37 real pages:

- **Zero characters of text were lost.** Every diff opcode against html.parser output was an insertion. Not one page dropped content under lxml.
- 659 characters were added, all on one page (docs.docker.com), where an inline Alpine.js `@click` handler containing a regex confuses libxml2's attribute handling and leaks a 73-character JS fragment into the text nine times. That is 659 characters of script noise in 27,206 characters of extracted text, with no content affected.
- Metadata (`title`, `description`, `language`) was identical on 35 of 37 pages. The two exceptions are 141-byte Wikipedia bot-block stubs with no `<html>` element, where lxml's fragment auto-wrapping adds `language: "No language found."`. Both parsers extract the same text from them.

Large documents were checked separately because libxml2 carries internal size caps. A 12 MiB single text node, 12 MiB spread across 400k nodes, a 3 MiB attribute value and 50k sibling elements with a trailing marker all produced byte-identical text under both parsers, with no truncation.

Malformed markup was checked too. lxml and html.parser diverge on unterminated comments, bare CDATA and duplicated `<html>` elements, all cases where both parsers are guessing and neither is correct. None of those shapes appeared in the 37 page corpus.

`backend/open_webui/env.py:184` also uses `html.parser`, on the local CHANGELOG at import time. That is trivial input on a startup path and is deliberately left alone.
2026-07-26 18:19:09 -04:00
Classic298 18d004cabe chore: drop python-jose, nothing imports it (#27444)
The migration to joserfc completed the job but left the old dependency pinned. `python-jose` now has zero imports anywhere in the backend: the only `jose` references left are `joserfc` in `utils/oauth.py`, and a repo-wide search for `from jose`, `import jose` or `python_jose` returns nothing outside the three pin files.

Removing it also removes `ecdsa` and `rsa` from the image, which were pulled in only by python-jose. `uv lock` confirms that: it drops exactly those three packages and nothing else, because google-auth 2.55 depends on cryptography and pyasn1-modules rather than rsa. That is worth having beyond the size saving, since `ecdsa` ships a documented Minerva-style timing side-channel in its P-256 signing path that upstream has declined to fix, so keeping it in the image means shipping a flagged crypto library that nothing calls.

Verified by blocking the `jose` module at import time and importing the backend anyway:

```
PASS  import open_webui.utils.auth
PASS  import open_webui.utils.oauth
PASS  import open_webui.main
jose in sys.modules: False
PASS  create_token/decode_token round trip
```

One user-visible consequence worth stating: Tools and Functions run in the same interpreter, so a third-party plugin that imports `jose` directly stops working after this. Nothing in Open WebUI itself does, and PyJWT remains a dependency, but a plugin relying on a library the application never declared for that purpose is the only thing this can break.

`uv.lock` was edited surgically rather than regenerated, to avoid the unrelated whole-file churn a newer uv version introduces. The result was diffed against real `uv lock` output and matches it exactly apart from that version's cosmetic fields.
2026-07-26 18:18:17 -04:00
Classic298 3ce734c6c6 fix: bump uvicorn to 0.51.0 to move off the legacy websocket implementation (#27553)
Uvicorn's `--ws auto` selected its `websockets_impl` protocol on 0.41.0, which is built on `websockets.legacy`. That module raises `AssertionError` in `_drain_helper` during keepalive pings and kills the websocket connection. Each crash runs the Socket.IO `disconnect` handler and drops the session from `SESSION_POOL`, so every subsequent server-to-browser call fails. The most visible symptom is the Pyodide code execution tool, which reaches the browser through `sio.call('events', ...)` and returns `{"stderr": "Client session disconnected."}` on every run.

Uvicorn 0.50.0 changed `--ws auto` to select the sans-io implementation whenever websockets is installed, and deprecated the legacy one. Bumping the pin therefore fixes this on every launch path at once, without adding a `--ws` flag to the startup scripts. Doing nothing is not stable either: websockets is unpinned apart from uvicorn's own `>=13.0` floor, and `websockets.legacy` is removed outright in websockets 17, which turns the current AssertionError into an ImportError on a fresh install.

Bumping to 0.51.0 rather than the minimum 0.50.0 also picks up the sans-io keepalive pings added in 0.44.0, so raw websocket endpoints keep the idle-timeout behaviour they have today behind a reverse proxy. Uvicorn 0.51.0 drops colorama from its `standard` extra and raises the httptools floor to 0.8.0, which the lockfile already satisfies.

Verified on the bumped pin: the backend boots, `/health` returns 200, `--ws auto` resolves to `WebSocketsSansIOProtocol`, a Socket.IO client completes a websocket handshake against the running app, and a bidirectional `sio.call` round trip succeeds. The unit test suite reports an identical 2273 passed / 7 failed on 0.41.0 and 0.51.0, with the 7 failures unrelated to uvicorn.

Fixes #27550
2026-07-26 17:55:44 -04:00
Timothy Jaeryang Baek 7b12fd677f refac 2026-07-24 01:23:39 -04:00
Timothy Jaeryang Baek ff11ff5a3e refac 2026-07-24 01:11:23 -04:00
G30 4f823774ab fix: migrate deprecated authlib.jose import to joserfc in oauth utils (#27310) 2026-07-23 21:54:59 -04:00
Classic298andClaude Fable 5 32242a6788 perf: 40% LESS CPU usage: cut per-instance CPU cost of shared socket.io Redis pub/sub channel (#27282)
* perf: cut per-instance CPU cost of shared socket.io Redis pub/sub channel

Profiling a multi-instance deployment (py-spy --gil) showed ~44% of worker
CPU in the socket.io pub/sub listener. Two causes, two fixes:

- Add hiredis so redis-py parses the RESP protocol in C instead of pure
  Python (redis/_parsers/resp3.py alone accounted for ~28% of GIL samples;
  redis-py auto-selects the hiredis parser when importable).

- Subclass AsyncRedisManager to drop emits whose target room has no local
  participants before upstream _handle_emit re-encodes the full packet.
  Every instance receives every emit published on the shared channel, so
  with N instances all but the hosting one were paying full packet
  re-serialization per message just to deliver it to nobody. Broadcasts
  (room=None) are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* fix: restrict pub/sub emit early-out to string rooms

Adversarial review against python-socketio 5.16.2 found one divergence
from upstream: for a degenerate empty-sequence room (emit to room=[]) on
an instance whose namespace has no local clients, the filter's
get_participants probe raises IndexError from room[0] where upstream
returns silently at the namespace guard and still publishes to Redis.
Open WebUI only ever emits to scalar string rooms or room=None, so the
case is unreachable today; guard on isinstance(room, str) anyway so any
non-string room shape passes through to upstream behavior unchanged.

Every open-webui emit uses a string room, so the fast path still covers
all real traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* Update requirements.txt

* Update pyproject.toml

* Update requirements-min.txt

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:11:38 -04:00
Timothy Jaeryang Baek 30415c925a refac 2026-07-23 03:11:38 -04:00
Classic298andClaude Opus 4.8 b295a20b9d chore: bump Python backend dependencies, drop unused peewee (#25786)
* chore: bump Python backend dependencies, drop unused peewee

Minor/patch + reviewed major bumps across requirements.txt,
requirements-min.txt, pyproject.toml and uv.lock; playwright image bumped in
docker-compose.playwright.yaml. peewee/peewee-migrate removed (zero imports).

Security-relevant: cryptography 46->48, authlib 1.6.10->1.7.2, PyJWT 2.11->2.13,
requests 2.33.1->2.34.2, RestrictedPython 8.1->8.2, pillow 12.1.1->12.2.0.
Reviewed majors: redis 7->8, pymilvus ->2.6.14, azure-search-documents 11->12,
chardet 5->7, unstructured 0.18->0.22, pycrdt 0.12->0.13.

Testing:
- Resolution: `uv lock` resolves the full bumped set with no conflicts; uv.lock
  regenerated to match (peewee dropped, every pin including
  azure-search-documents==12.0.0 resolves).
- Per-dependency contract tests (external tests repo, unit/deps/): 105 files,
  2205 passed / 6 skipped, ruff-clean. One file per dependency pins the symbols,
  signatures and behaviour the backend actually uses, so an API removal/rename in
  a bumped version fails loudly instead of at runtime. Offline/deterministic.
- End-to-end embed->retrieve test driving transformers + sentence-transformers +
  chromadb together through Open WebUI's real RAG path (cached model, in-memory
  chroma, semantic retrieval asserted).
- Install/startup/health resolution gate added to the dep-bump workflow and the
  integration suite (uv/pip resolve + uvicorn /health + Playwright dev visibility).
- Bugs surfaced while testing each got an isolated fix branch + regression test:
  Mistral OCR aiohttp FilePayload (#25779), chroma has_collection (#25780),
  aiocache per-user model-cache key (security), otel semconv deprecation,
  pydub/audioop <3.13 note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Bump python-multipart 0.0.22 -> 0.0.27 (CVE-2026-42561, CVE-2026-40347)

0.0.22 is affected by two DoS CVEs in the multipart parser that
Starlette/FastAPI run for every multipart/form-data request, so any
authenticated user hitting an upload endpoint can trigger them:
- CVE-2026-42561: unbounded part-header count/size -> CPU exhaustion (fixed 0.0.27)
- CVE-2026-40347: large multipart preamble/epilogue DoS (fixed 0.0.26)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:02:18 -05:00
Timothy Jaeryang Baek c0f1aa2919 refac 2026-06-01 12:24:45 -07:00
rileydes-improving 567c4aabe9 feat: add support for Valkey vector database (#24769)
* feat: add support for Valkey vector database

Signed-off-by: Riley Des <riley.desserre@improving.com>

* feat: add CLIENT SETNAME to Valkey vector store connections

Set client_name on GlideClientConfiguration for both the main client
and batch client so connections are identifiable in CLIENT LIST,
monitoring dashboards, and CloudWatch metrics.

Signed-off-by: Riley Des <riley.desserre@improving.com>

---------

Signed-off-by: Riley Des <riley.desserre@improving.com>
2026-06-01 12:20:01 -07:00
RomualdYTandTim Baek e0d6074cd2 refactor(firecrawl): use v2 API directly (#23934)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-04-24 18:32:08 +09:00
Timothy Jaeryang Baek 3d1e355df7 refac 2026-04-24 18:20:10 +09:00
Timothy Jaeryang Baek 752238247c refac 2026-04-24 16:47:30 +09:00
Timothy Jaeryang Baek 89669f3fa1 refac 2026-04-24 14:40:17 +09:00
Timothy Jaeryang Baek d47993385a refac 2026-04-24 14:35:15 +09:00
Tim Baek 9f61a6f13c fix 2026-04-21 19:37:22 +09:00
Timothy Jaeryang Baek 9a8c4da67d chore: deps bump 2026-04-13 21:46:32 -05:00
Timothy Jaeryang Baek 39ea7bf63d chore: dep bump 2026-04-13 21:45:17 -05:00
Timothy Jaeryang Baek 45f45f5bba chore: bump dep 2026-04-13 13:49:28 -05:00
Timothy Jaeryang Baek d1a0fbe292 refac 2026-04-13 13:36:54 -05:00
Timothy Jaeryang Baek 1994d65306 fix: bs4 pip install 2026-03-26 17:26:06 -05:00
Timothy Jaeryang Baek 58dcc1b33f refac 2026-03-24 20:06:43 -05:00
Timothy Jaeryang Baek cb83831041 chore: pin deps 2026-03-24 20:04:12 -05:00
Shamil 85411e4867 chore: align black with Ruff backend formatting (#22766) 2026-03-21 17:42:37 -05:00
Shamil b73010bb36 feat: add ruff linter & formatter (#22576)
* chore: add ruff linter with base config

* ci: add ruff pre-commit hook

* ci: add ruff github actions workflow

* ci: run ruff only on changed files
2026-03-15 17:22:27 -05:00
Timothy Jaeryang Baek 6862d618ee refac 2026-03-13 20:57:12 -05:00
Timothy Jaeryang Baek f1c1004225 fix: ddgs 2026-03-11 14:25:18 -05:00
Code with love 265d1b2824 Add support for mariadb-vector as backing vector DB (#21931) 2026-03-08 17:13:14 -05:00
Classic298 2108f420ea chore: dep bump (#22305)
* chore: dep bump

* revert: Brotli dependency bump (1.2.0 -> 1.1.0)
2026-03-07 17:12:22 -06:00
Timothy Jaeryang Baek 850a864b02 refac 2026-02-12 16:54:32 -06:00
Timothy Jaeryang Baek 3567054325 refac 2026-02-09 18:22:28 -06:00
Classic298 55169e69c0 chore: Dep bump (#21261)
* Update pyproject.toml

* Update requirements-min.txt

* Update package versions in requirements.txt

Updated various package versions in requirements.txt to latest releases.
2026-02-09 16:15:39 -06:00
Classic298 60f2f8c1c4 Update pyproject.toml (#21087) 2026-02-02 09:21:39 -06:00
Classic298 643c661a6f chore: Updates minor/patch versions for 21 Python backend dependencies (#21059) 2026-01-31 16:51:08 -06:00
Classic298 716f2986b9 dep bump (#20735) 2026-01-17 21:44:32 +04:00
Timothy Jaeryang Baek 3a57233dd4 chore: aiohttp 2026-01-09 18:10:27 +04:00
Classic298 f5455d48c4 Chore: dup bump for NPM and PIP (#20386)
* Update pyproject.toml

* Update aiohttp version to 3.13.3

* Update aiohttp version to 3.13.3

* Update pyproject.toml

* Update requirements.txt

* Update package.json
2026-01-05 22:34:25 +04:00
Classic298 a2f8e41fbc chore: dep bump (#20315)
* dep bump

* update
2026-01-03 18:14:25 +04:00
Classic298 b3371033be chore(deps): update and synchronize backend dependencies (#20225)
* chore(deps): update and synchronize backend dependencies

- Updated dependencies in requirements files and pyproject.toml to latest versions.
- Preserved pinned versions for av, pinecone, and pyarrow.
- Added missing dependencies to pyproject.toml to match requirements.txt.
- Ensured all dependency versions are synchronized across files.

* Update pyproject.toml
2025-12-30 14:05:56 +04:00
Timothy Jaeryang Baek 267cc14d2d chore: rm google-generativeai 2025-12-29 00:42:49 +04:00
Timothy Jaeryang Baek 117b1bfa65 refac 2025-12-22 19:06:15 +04:00
Timothy Jaeryang Baek 5c5f87a63c fix: python pip install dep issue 2025-12-22 09:42:50 +04:00
Classic298 60c93b4ccc chore: dep bump (#20077)
* Update pyproject.toml

* Update requirements-min.txt

* Update requirements.txt
2025-12-21 10:52:06 -05:00
Classic298 cd170735c5 chore: dep bump (#20012)
* Update requirements-min.txt

* Update pyproject.toml

* Update requirements.txt

* Update pyproject.toml

* Update requirements.txt

* Update requirements-min.txt
2025-12-20 07:30:30 -05:00
Classic298 dce52f33c0 chore: small dep bump (#19972)
* Update requirements.txt

* Update pyproject.toml
2025-12-15 13:31:40 -05:00
Classic298 1c62be4406 chore: dep bump (#19937)
* Update requirements.txt

* Update requirements-min.txt

* Update pyproject.toml
2025-12-13 14:42:08 -05:00
Timothy Jaeryang Baek db95e96688 chore: dep 2025-12-10 23:59:52 -05:00