* refac * refac * refac: reorganize scripts and ci workflows * chore: remove unused cypress tests * chore: remove unused legacy test suite * refac: deprecate peewee migration layer The Alembic init migration (7e5b5dc7342b) already creates the equivalent schema. Peewee migrations are no longer needed for any version >= 0.3.6. * refac: remove dead peewee connection wrappers * refac * refac * style: ruff format * style: standardize os.environ.get to os.getenv * refac: modernize imports, standardize type hints and docstrings * refac: modernize type annotations (PEP 604 / PEP 585) * refac * feat: kb_exec * refac * refac * refac * refac * upd:i18n: es-ES Translation update v0.9.5 (#24651) es-ES Translation. Update v0.9.5 Added translation of new strings. * feat: knowledge directory * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * fix: German i18n translations (#24668) * i18n: Update Swedish (sv-SE) translation — merge with latest dev (#24665) Co-authored-by: Daniel Nylander <daniel@danielnylander.se> * refac * Fix translation for 'Authentication' in Danish (#24645) * refac * fix: korean i18n * i18n: Update catalan translation.json (#24569) * refac * fix: validate folder_id ownership on chat create + folder-update endpoints (#24588) POST /api/v1/chats/new and POST /api/v1/chats/{id}/folder accepted a caller-supplied folder_id with no validation — neither ownership, nor existence, nor UUID format. The row was persisted with the supplied value verbatim, so the DB ended up with chat rows whose folder_id referenced another user's folder, a non-existent UUID, or even a non-UUID string. No read path surfaces this across users — every chat-folder read is user_id-filtered on both sides — so this is referential-integrity hardening rather than a security boundary fix. But there's no reason to accept dangling references either, and the downstream consumers shouldn't have to assume the column is clean. Add a Folders.get_folder_by_id_and_user_id() lookup at both writers: if a folder_id is supplied, it must match a folder owned by the caller. None remains allowed (chat-without-folder is the default). Non-existent and non-UUID values fall through to 404. Reported by ShigekiTsuchiyama in GHSA-4vrg-2vcq-q7jc. Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com> * refac * refac * refac * refac * fix: enforce features.direct_tool_servers on chat-completion tool_servers (#24693) * fix: enforce features.direct_tool_servers on chat-completion tool_servers The features.direct_tool_servers per-user permission was correctly enforced on the storage path (routers/users.py user/settings/update, which strips toolServers from saved settings when the caller lacks the permission), but the inference path (/api/chat/completions) popped tool_servers straight from the request body into metadata with no permission check. The middleware (utils/middleware.py:2799) then consumed direct_tool_servers to inject system_prompt into the message array and register external tool specs that get invoked during the completion. End result: any authenticated user could bypass the admin-set per-user feature toggle and use inline tool_servers in their chat-completion requests, even when admin had explicitly denied the permission. Default for USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS is False (config.py:2750), so under default config no regular user is supposed to be able to use direct tool servers — making this a real boundary bypass on out-of-the-box deployments rather than a corner case. Mirror the storage-side behaviour at the inference entry point: pop tool_servers from the request body, then silently drop the value if the caller is non-admin and lacks features.direct_tool_servers. Admins always pass; users with the explicit grant always pass; everyone else gets None propagated into metadata, which the middleware already handles as the no-tool-servers case. Reported by berkant-koc in GHSA-f582-c373-jjf6. Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com> * chore: trim verbose comment on tool_servers permission check --------- Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com> * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * refac * fix: legacy peewee tables fk * refac Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com> * fix: default optional env vars used with bash `,,` in start.sh (#24683) start.sh runs with `set -euo pipefail`, but three call sites added in070ab2650(refac: reorganize scripts and ci workflows) reference optional env vars via bash's `,,` lowercase expansion without any default. Containers that don't set these vars — the default for every deployment that isn't explicitly opting into Playwright / bundled Ollama / CUDA — crash on startup with: start.sh: line 15: WEB_LOADER_ENGINE: unbound variable (and the same for USE_OLLAMA_DOCKER, USE_CUDA_DOCKER once the first were set in turn.) Reported in open-webui#24560 by urbenlegend. The same refactor correctly defaulted every other optional env var with `${VAR:-…}`. The three `,,` references slipped through because bash can't combine `:-default` with `,,` in a single substitution — `${VAR:-default,,}` makes the default literal `,,`, not what's wanted. Fix: normalise the three vars in a one-line preamble with `${VAR:=}`, which assigns an empty default if unset. The downstream `${VAR,,}` expressions stay exactly as Tim wrote them, preserving the file's visual style and matching the existing `${VAR:-…}` idiom for "this variable is optional". * i18n: update Russian translations (#24728) * Update SECURITY.md (#24726) * fix: tag composite pk in migration (#24722) * feat(ui): add emoji picker to rich text formatting toolbar (#24704) * fix: wire workspace.skills into the sidebar + workspace-index gates (#24729) Reported by bwgabrielsusai on #24719: granting a user only `workspace.skills` doesn't show the Workspace menu, and visiting `/workspace` directly bounces them to `/`. The per-route guard in `/workspace/+layout.svelte` already covered skills, but two earlier gates in the chain didn't: * `Sidebar.svelte` case 'workspace' OR'd models/knowledge/prompts/tools to decide menu visibility — skills was missing, so the entry never rendered for skills-only users. * `/workspace/+page.svelte` redirect chain picked the first available section — skills was missing, so the fallback `goto('/')` fired. Adding skills to both. * refac * refac * chore: pyodide * fix: get_image_base64_from_file_id * refac * i18n: update Irish translation (#24883) * refac * refactor: remove dead frontend API wrappers with no backend route (#24792) These 19 exported wrappers are dead: each appears exactly once in the codebase (its own definition), nothing imports or calls any of them, and none has a corresponding backend route. They are leftovers from settings that were consolidated server-side into /auths/admin/config, /openai/config, /ollama/config and /api/config: - index.ts: getModelFilterConfig, updateModelFilterConfig, getCommunitySharingEnabledStatus, toggleCommunitySharingEnabledStatus, getModelConfig, updateModelConfig (+ orphaned GlobalModelConfig type) - auths: getSignUpEnabledStatus, toggleSignUpEnabledStatus, getDefaultUserRole, updateDefaultUserRole, getJWTExpiresDuration, updateJWTExpiresDuration - openai: getOpenAIUrls, updateOpenAIUrls, getOpenAIKeys, updateOpenAIKeys - ollama: updateOllamaUrls - prompts: restorePromptFromHistory - folders: updateFolderItemsById (+ orphaned FolderItems type) Shared types (ModelConfig/ModelMeta/ModelParams) and all live wrappers are untouched. Removal is import-safe: nothing referenced these. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: remove unused GET /evaluations/feedbacks/all endpoint (#24778) GET /evaluations/feedbacks/all returned the entire feedback table in a single response (flagged as a Medium OOM risk for admins in open-webui#22206). Its only frontend wrapper, getAllFeedbacks in src/lib/apis/evaluations/index.ts, is dead: nothing imports or calls it anywhere in the codebase. The endpoint is a redundant view-only twin of GET /evaluations/feedbacks/all/export, which is what the admin Feedbacks UI actually uses. Removes the endpoint, the now-unused FeedbackResponse import in the evaluations router, and the dead getAllFeedbacks frontend wrapper. The shared Feedbacks.get_all_feedbacks data-layer method is kept, since the live /feedbacks/all/export endpoint still uses it. Ref: open-webui#22206 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: remove unused POST /api/v1/utils/markdown endpoint (#24779) POST /utils/markdown rendered a markdown string to HTML server-side. Its only frontend wrapper, getHTMLFromMarkdown in src/lib/apis/utils/index.ts, is dead: nothing imports or calls it, the route is hit by no other code path, and the path string appears nowhere else in the repo (no direct fetch, no test, no docs). Markdown is rendered client-side in the UI, so this endpoint was redundant. Fully self-contained removal: the endpoint, its MarkdownForm model, the now-orphaned 'import markdown' in the utils router (used only here), and the dead getHTMLFromMarkdown wrapper. Nothing else depends on any of them. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * refac * refac * refac Co-Authored-By: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> * refactor: remove unused DELETE /chats/{id}/tags/all endpoint (#24785) The bulk-clear-chat-tags endpoint's only frontend wrapper, deleteTagsById in src/lib/apis/chats/index.ts, is dead: nothing imports or calls it, the path is referenced nowhere else, and the route handler has no internal caller. Removes the route handler, the dead wrapper, and the now-orphaned Chats.delete_all_tags_by_id_and_user_id model method (its sole caller was this route). The shared Chats.delete_orphan_tags_for_user method is untouched. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac: audio * fix: pass subscription_key and endpoint in bing.py CLI search_bing() call (#24768) The __main__ block called search_bing() with 4 positional arguments, but the function requires 5 (subscription_key, endpoint, locale, query, count). Running `python -m open_webui.retrieval.web.bing` raised a TypeError and, before failing, silently misrouted every argument. Read the key/endpoint from environment variables, matching config.py defaults. Closes #24765 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: check destination calendar write access on event update (#24764) update_event only verified write access on the event's source calendar. CalendarEventUpdateForm accepts a new calendar_id which the model layer applies unconditionally, so a user with write access to their own calendar could move (inject) an event into any other user's calendar. Mirror the destination check create_event already performs. * refactor: remove dead generateFollowUps frontend wrapper (#24794) generateFollowUps in src/lib/apis/index.ts is dead: it appears only at its own definition, nothing imports or calls it, and it targets a non-existent path (/api/v1/tasks/follow_ups/completions, plural) while the real route is /tasks/follow_up/completions (singular). Follow-up suggestions are generated server-side in the chat-completion middleware and delivered over the chat:message:follow_ups websocket event, so this wrapper was never on the live path. Removes only the dead wrapper. The backend POST /tasks/follow_up/completions endpoint is intentionally kept: it is a member of the actively-used /tasks/*/completions family (title, tags, emoji, queries, moa) and its handler delegates to the core generate_follow_ups function. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: validate Playwright navigations and gate redirects in web loader (#24756) SafePlaywrightURLLoader validated only the initially submitted URL and then let the browser follow HTTP redirects and client-side navigations without re-checking them, so a public URL could redirect into the internal network (cloud metadata, RFC1918, loopback). Intercept document-type requests, re-run validate_url on each, and apply the same redirect policy as the requests loader (blocked unless AIOHTTP_CLIENT_ALLOW_REDIRECTS). Sub-resource requests pass through unchanged so page rendering performance is unaffected. Co-authored-by: POV9en <POV9en@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * refac * refac * enh: linkup * fix: log expected fetch/transcript/tool-server failures as warnings (#24903) * fix: emit [DONE] for AsyncGenerator pipe returns (#24763) * fix: respect access_type in shared-chat file authorization branch (#24755) has_access_to_file granted access whenever the file was attached to a shared chat the user could read, ignoring the requested access_type. A read-only shared-chat recipient therefore satisfied write and delete checks and could delete or mutate the chat owner's attached file. Gate the shared-chat branch on read access, matching the channels branch directly above it. Co-authored-by: oxsignal <oxsignal@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * refac * fix: mitigate DNS rebinding in web loader fetch paths (#24759) validate_url() resolves DNS to check IPs but discards the result; the HTTP client resolves again independently. Between those two lookups an attacker can swap the DNS record from a public IP to an internal one (DNS rebinding). Push the IP-is-global check into the actual connection layer so the validated resolution is the one used for the TCP connect: - aiohttp (_fetch): _SSRFSafeResolver wraps DefaultResolver and rejects non-global IPs at resolve time (zero TOCTOU window). - requests (_scrape): _SSRFSafeAdapter mounts custom urllib3 connection classes whose _new_conn resolves, validates, and connects to the validated IP in one shot (zero TOCTOU window). Both paths respect ENABLE_RAG_LOCAL_WEB_FETCH (skip validation when on). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: disable redirect following in OAuth picture fetch (SSRF) (#24809) _process_picture_url validated the initial picture URL with validate_url() but then aiohttp followed 3xx redirects without re-validating the target, so a validate_url-passing public URL could 302 to an internal address and the body was base64-stored in the user's profile_image_url. This is the sixth call site of the CVE-2026-45401 redirect-bypass cohort; the other five already pass allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS. Apply the same. * refac Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com> * Run transcode_audio_to_mp3 in a thread to avoid blocking (#24876) This incorporates the transcoding implementation in #24145. * refac Co-Authored-By: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com> * refactor: remove unused GET /prompts/command/{command} endpoint (#24782) The lookup-prompt-by-command endpoint's only frontend wrapper, getPromptByCommand, is dead: nothing imports or calls it, the path is referenced nowhere else, and the route handler has no internal caller (slash-command resolution happens client-side from the loaded prompt list). Removes the route handler, its section header, and the dead wrapper. The shared Prompts.get_prompt_by_command data-layer method is kept: it is still used by create/update prompt validation (prompts.py:175, 275, 340). PromptAccessResponse and AccessGrants are untouched. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * refac: kb sync * refac * refac * refac * refac * refac * refac: clean up Redis sentinel utilities and import grouping * fix: resolve NameError for redis_sentinels in session_cleanup_lock The variable was renamed to ws_sentinels but session_cleanup_lock still referenced the old name, causing a startup crash. * refac * refac * refac * refac * refac * refac * refac * Update knowledge.py (#25053) * refac * refac * fix(prompts): resolve undefined session variable in _get_access_grants and _to_prompt_model (#25129) Both _get_access_grants and _to_prompt_model referenced an undefined local variable 'session' instead of the 'db' parameter passed to each method. Because these helpers are called outside of any 'async with get_async_db_context()' block, 'session' did not exist in their scope, causing a NameError on every prompt fetch. The NameError was silently swallowed by the broad 'except Exception' clause in get_prompt_by_id, which returned None — causing the frontend [id]/+page.svelte to immediately redirect back to /workspace/prompts rather than rendering the prompt editor. Also adds the missing 'logging' import and module-level 'log' logger, which was referenced (but never imported) in insert_new_prompt, update_prompt_version, and delete_prompt_by_id. * fix: add knowledge_id access check in search_knowledge_files (BOLA) (#25113) When called without attached model knowledge and given a caller-supplied knowledge_id, search_knowledge_files passed it straight to Knowledges.search_files_by_id, which does not enforce ownership on knowledge_id. An authenticated user who happened to know a target UUID could enumerate file metadata (filename, file id, KB id, KB name, updated_at) from any knowledge base, bypassing the AccessGrants permission model. Mirror the same admin/owner/AccessGrants check the attached-KB branch already uses, matching the sibling query_knowledge_files function. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * fix(auth): use request.scope["path"] to prevent CVE-2026-48710 (BadHost) (#25123) Starlette reconstructs request.url.path from the HTTP Host header without validation. An attacker can inject a path into the Host header to make request.url.path return a different value than the path Starlette routes on. The API key endpoint restriction check was using request.url.path to decide whether to allow or deny access — making it bypassable via a crafted Host header on any Starlette version prior to 1.0.1. Fix: replace request.url.path with request.scope["path"], which reads the raw ASGI scope path that Starlette uses for routing. This value is set by the ASGI server from the actual request path and cannot be injected via HTTP headers, making it safe regardless of Starlette version. Affected code path: get_current_user_by_api_key() in backend/open_webui/utils/auth.py (only triggered when ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS is enabled) References: CVE-2026-48710 / BadHost https://arstechnica.com/information-technology/2026/05/millions-of-ai-agents-imperiled-by-critical-vulnerability-in-open-source-package/ * I18n/improve chinese translation (#25114) * i18n: improve zh-CN translation * i18n: improve zh-TW translation * refac * refac * refac * refac * fix: harden model profile image against SVG stored XSS (#25060) ModelMeta.profile_image_url now runs validate_profile_image_url, rejecting SVG/script data URIs (matching UserUpdateForm and ChannelWebhookForm). The /model/profile/image endpoint enforces the PROFILE_IMAGE_ALLOWED_MIME_TYPES allowlist and sets X-Content-Type-Options: nosniff, so an SVG data URI can no longer be served inline on-origin. Closes the fourth profile-image XSS sink missed by the user and webhook fixes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: gate chat-file links by caller access + repair insert_chat_files db arg (#25054) insert_chat_files() stored any caller-supplied file_id with no ownership check, so a user could attach another user's file to their own chat and then read it through the shared-chat access path in has_access_to_file(). Filter file_ids to those the caller owns, is admin for, or can read. Also repairs an UnboundLocalError introduced in260ead64d: the existing duplicate-check referenced `session` before it was assigned (db=session), so the function threw on every call and no chat_file rows were persisted. * Update fi-FI translation.json (#24963) Added missing translations and improved existing ones. * Update main.py (#25271) * fix(i18n): add missing Korean plural _one keys for selected/sources/minutes (#25228) * fix: sanitize mermaid SVG output to prevent stored XSS in file preview (#25219) renderMermaidDiagram returned raw mermaid SVG, which FilePreview.svelte injects via wrapper.innerHTML = svg. Mermaid runs with securityLevel: 'loose', so it neither sanitizes click hrefs (formatUrl skips sanitizeUrl) nor DOMPurifies its output; a .md file with a click X href "javascript:..." directive (or an HTML-label payload) therefore executes script in the app origin when previewed. The chat path was already safe because SVGPanZoom DOMPurifies before rendering; file preview was not. Sanitize at the source: renderMermaidDiagram now returns DOMPurify-cleaned SVG via a shared sanitizeSvg helper (same policy as SVGPanZoom), so every consumer including the FilePreview innerHTML sink receives safe output. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix: preserve parent_id on chat_message upsert (#25205) * fix(ui): include reasoning_tags in user settings advanced params save handler (#25204) * fix(ui): prevent long usernames from overflowing Edit User modal, User Preview modal, and sidebar (#25185) Long usernames overflow the Edit User modal, User Preview modal header, and the sidebar user area because the flex containers lack width constraints. - EditUserModal: add min-w-0 to the flex-1 container so the existing truncate class takes effect - UserPreviewModal: add min-w-0 and truncate to the title container, flex-shrink-0 to the close button so it stays visible - Sidebar: add truncate to the username display and flex-shrink-0 to the avatar container to prevent it from being squeezed * fix(ui): add voice mode mute shortcut to keyboard shortcuts modal (#25193) * fix(types): add missing markdown rendering settings to Settings type (#25198) * i18n: complete Turkish (tr-TR) translation (#25210) * fix: remove hardcoded WEBUI_SECRET_KEY fallback, require key explicitly (#25218) The 't0p-s3cr3t' default was dead code on every supported startup path: start.sh, start_windows.bat and `open-webui serve` all set or auto-generate WEBUI_SECRET_KEY before the backend imports env.py. It was only ever reachable by invoking uvicorn directly, which is unsupported and unsafe (the app would then sign tokens/cookies with a public, hardcoded key). It also keeps getting reported as a vulnerability because it looks dangerous, even though it is unreachable in practice. Drop the fallback (default to '') so an unset key is caught by the existing WEBUI_AUTH guard, and replace the vague error with a clear, actionable message explaining that the key is a hard requirement and how the supported start methods provide it. Exit cleanly via SystemExit instead of raising a ValueError traceback. WEBUI_AUTH=False keeps working unchanged (key defaults to ''). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: move bypass_system_prompt off query parameter onto request.state (#25156) bypass_system_prompt is an internal flag used by utils/middleware.py and utils/chat.py to skip applying the model system prompt on recursive base-model calls, but it was still declared as a positional argument on the openai/ollama chat-completion route handlers, so FastAPI bound it from the query string. Move it to request.state so external clients cannot set it, matching how bypass_filter is handled. Drop the argument from both route signatures and read getattr(request.state, 'bypass_system_prompt', False); utils/chat.py sets request.state.bypass_system_prompt alongside bypass_filter and drops the kwarg from the two route-handler calls (the recursive self-calls keep it). Mirrorsc0385f60b. Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n(pl-PL): add missing polish translations (#25176) * fix(models): gracefully handle legacy svg profile_image_url in ModelMeta validator (#25173) The SVG-XSS hardening introduced inf5f4b5895correctly rejects data:image/svg+xml URIs on new input, but also caused a pydantic_core.ValidationError when reading pre-existing models from the database that had SVG data URIs stored as their profile images. This ValidationError propagated unhandled through _to_model_model and get_all_models, crashing the entire /api/models endpoint with HTTP 500 and leaving users with no models available in the UI. Fix: - Wrap validate_profile_image_url() in a try/except ValueError inside ModelMeta.check_profile_image_url. Legacy entries are cleared to None with a warning log instead of raising — the /model/profile/image API endpoint already falls back to /static/favicon.png when the value is empty. - Default ModelMeta.profile_image_url to None instead of hardcoding /static/favicon.png, since the serving endpoint handles the fallback. - Add a per-model try/except in ModelsTable.get_all_models so that any future unexpected validation failure on a single record skips that model with an error log rather than aborting the entire list. * refac * fix: add null guards to channel Thread and PinnedMessagesModal components (#25209) Thread.svelte: Add null check for messagesContainerElement in scrollToBottom() to match the existing pattern in Channel.svelte. Prevents potential TypeError when the DOM element is not yet bound during rapid thread switches. PinnedMessagesModal.svelte: Move res.length check inside the if (res) block. Previously, res.length was accessed unconditionally after a guarded block, causing TypeError when the API call fails and the .catch() returns null. * fix(settings): correct presence_penalty and repeat_penalty saving wrong values (#25183) Both presence_penalty and repeat_penalty in the saveHandler read from params.frequency_penalty instead of their own values due to a copy-paste error. This causes users adjusting either parameter to silently save the frequency_penalty value instead. * fix(ui): enable custom parameters in user settings and admin model settings (#25200) Add missing custom={true} prop to AdvancedParams in General.svelte and ModelSettingsModal.svelte so the 'Add Custom Parameter' option appears consistently across all advanced parameter surfaces. Also forward custom_params in the General.svelte save handler so custom parameters are persisted instead of silently dropped on save. * i18n : (ms-MY) refine translation and standardise terminology (#25164) * refac * refac * fix: decode terminal proxy path until stable to block multi-encoded traversal (#25157) _sanitize_proxy_path decoded the proxy path once before the '..' check, so a double-encoded payload (%252e%252e) survived the check as %2e%2e and was then re-decoded into '..' by the upstream terminal server, defeating the traversal guard. Decode until stable so no encoded traversal sequence can reach the upstream. Single-encoded payloads were already rejected; this closes the double (and deeper) encoding bypass. Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac * refac * chore: Update CHANGELOG.md (#24680) * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * fix: use db instead of undefined session in chats model (#25455) * fix(ui): use correct 'blur' event name instead of 'blur-sm' in window listeners (#25459) * fix(ui): correct inverted high-contrast text colors for user message timestamp (#25461) * refac * refac * refac * refac * fix: clear usage interval in finally so it cannot leak on send failure (#25478) getChatEventEmitter starts a setInterval emitting a `usage` socket event every second; clearInterval ran only after sendMessageSocket resolved, so any throw/reject left the interval firing for the page lifetime. Each failed send added another orphaned interval, inflating server-side usage accounting and growing CPU/memory over a session. Wrap the send in try/finally so the interval is always cleared, on both the happy path and any thrown/rejected path. The exception still propagates unchanged. Fixes #25465 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: cap profile image data URI size to bound model/avatar bloat (#25476) * feat: cap profile image data URI size to bound model/avatar bloat validate_profile_image_url() validated data-URI format (MIME allowlist, SVG rejection, scheme checks) but never its length, so a valid data:image/...;base64,<huge> passed for both custom-model icons and user avatars. Large inline images bloat Postgres and the Redis MODELS hash and degrade model-list latency. Add PROFILE_IMAGE_MAX_DATA_URI_SIZE (default 256 KiB, 0 disables) and reject oversized data URIs in the shared validator, so both model meta (ModelMeta.profile_image_url) and user avatars (UpdateProfileForm) are bounded at one chokepoint. ModelMeta already clears invalid values to None on read, so existing oversized icons stop propagating into the MODELS hash on the next refresh. Fixes #25468 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: default PROFILE_IMAGE_MAX_DATA_URI_SIZE to None (no cap) Per review: opt-in rather than a 256 KiB default. Unset leaves data URIs uncapped; the validator already skips the check on a falsy value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refac * fix: don't hang terminal proxy when one forwarding pump exits first (#25479) The proxy gathered _client_to_upstream and _upstream_to_client with return_exceptions=True. When upstream sends a graceful CLOSE, _upstream_to_client returns but gather keeps waiting on _client_to_upstream, which is blocked in ws.receive() until the browser disconnects. The handler stays pending and the finally: session.close() cleanup is deferred, leaking a ClientSession and an open browser socket. Use asyncio.wait(return_when=FIRST_COMPLETED) and cancel the pending sibling, so the proxy unwinds as soon as either direction finishes. The pumps' bare except Exception already lets CancelledError (a BaseException) propagate, so cancellation is clean and they need no change. Fixes #25464 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refac Co-Authored-By: bannert <58707896+bannert1337@users.noreply.github.com> * refac Co-Authored-By: Boris Rybalkin <ribalkin@gmail.com> * fix: cache path traversal via sibling-prefix bypass in serve_cache_file (#25086) serve_cache_file gated the resolved path with file_path.startswith(os.path.abspath(CACHE_DIR)) without a trailing os.sep, so any path resolving to a sibling whose name starts with the cache-dir basename (e.g. cache_backup, cached_models) passed the prefix check. Authenticated users could read files from such siblings via /cache/../<sibling>/<file>. Appending os.sep to the prefix closes the bypass; deep traversal and absolute paths were already correctly blocked. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: persist outlet filter changes to message output (#24884) * refac * fix(images): pass content_type=None to r.json() to accept non-standard MIME types (#24838) aiohttp's ClientResponse.json() validates the Content-Type header against 'application/json' by default and raises ContentTypeError for any other value — including 'application/x-ndjson', which Ollama returns for its OpenAI-compatible /v1/images/generations endpoint. Pass content_type=None to skip this check while keeping all other parsing behaviour unchanged. The fix covers image generation (openai, gemini, automatic1111 engines) and image editing (openai, gemini engines). * fix(ui): guard JSON.parse(localStorage) calls with try/catch to prevent UI crashes (#25481) * fix: don't crash on startup when stdout can't encode the banner (#25482) The startup banner uses Unicode box-drawing characters. On a stdout that can't encode them (Windows cp1252, or redirected/headless/pythonw output) print() raises UnicodeEncodeError and aborts startup. This blocks running open-webui serve headless on Windows. Guard the banner print and fall back to a plain ASCII line so startup always proceeds regardless of the console encoding. Fixes #24965 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refac * fix(knowledge): remove premature drag-and-drop upload toast (#25484) * refac * fix: don't block first-admin signup on stale ENABLE_SIGNUP (#24821) Symptom On a fresh install (zero users) the frontend shows the mandatory "Create Admin Account" onboarding screen, but POST /api/v1/auths/signup returns 403 ACCESS_PROHIBITED ("You do not have permission to access this resource."). Wiping the database does not help when the config layer is backed by Redis (the value survives in the Redis/valkey volume), or when only the user table is cleared (the config row survives in Postgres). The instance is then unrecoverable through the UI. Root cause signup_handler() auto-sets request.app.state.config.ENABLE_SIGNUP = False immediately after the first admin is created. That value is persisted by the config layer (the Postgres config table, and Redis when REDIS_URL is set). On a later zero-user database the persisted False is read back, so ENABLE_SIGNUP resolves False even though no users exist. The old gate was: if WEBUI_AUTH: if not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM: if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP: 403 ENABLE_INITIAL_ADMIN_SIGNUP defaults to False, so with zero users the inner test (has_users or not ENABLE_INITIAL_ADMIN_SIGNUP) is True, and a stale ENABLE_SIGNUP=False trips the outer test, producing a 403 on the only UI path that can create the first admin. The frontend decides to show onboarding purely from user_count == 0, so frontend and backend disagree and the instance bricks. Change Split the gate by has_users. Subsequent signups (has_users True) are unchanged: still gated by ENABLE_SIGNUP and ENABLE_LOGIN_FORM. The first user (has_users False, the bootstrap admin the onboarding screen invites) is gated only by the admin-chosen ENABLE_LOGIN_FORM (the documented SSO-only hard-disable) unless ENABLE_INITIAL_ADMIN_SIGNUP is set. It is no longer gated by ENABLE_SIGNUP, which in the zero-user state is never an admin decision but the post-first-admin auto-disable leaking across a database reset. Why this is safe (full case analysis) For WEBUI_AUTH the gate has 16 input combinations over (has_users, ENABLE_SIGNUP, ENABLE_LOGIN_FORM, ENABLE_INITIAL_ADMIN_SIGNUP). Old and new are identical in 15 of them: * All 8 has_users=True cases: both reduce to "403 iff not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM". Unchanged. * 7 of the 8 has_users=False cases: identical. The only changed case is has_users=False, ENABLE_SIGNUP=False, ENABLE_LOGIN_FORM=True, ENABLE_INITIAL_ADMIN_SIGNUP=False: old behaviour 403, new behaviour allow. The new condition is a strict subset of the old (new-403 implies old-403), so the change never newly blocks any request that previously succeeded; it only stops blocking that one bootstrap state. That state has no legitimate deployment. With the login form enabled and zero users the onboarding form is already served, and the only operator-configurable way to keep the first signup closed (SSO-only: ENABLE_LOGIN_FORM=False, optionally with ENABLE_INITIAL_ADMIN_SIGNUP) is preserved byte for byte. ENABLE_SIGNUP=False with zero users is not an operator choice, it is the automatic post-first-admin disable, so the old behaviour there was purely a brick with no recovery path. No security control is weakened: ENABLE_LOGIN_FORM and ENABLE_INITIAL_ADMIN_SIGNUP keep their exact meaning, and the WEBUI_AUTH=False path is untouched. This is not Redis-specific: it reproduces with Redis disabled through the Postgres config table alone (clear the user table, keep the config row). Verification Drove the real signup endpoint across a 10-case matrix on freshly migrated databases, including the full end-to-end first-admin creation (returns role=admin, row persisted as admin) and the preserved SSO-only, subsequent-signup and no-auth behaviours. All pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: don't revert replace/outlet content on chat save (#25485) update_chat_by_id re-derived assistant content from `output` on every save (serialize_output) so frontend edits to output items reflect in content. But it ran unconditionally, so content set independently of output — an __event_emitter__ {"type":"replace"} from an Action, or an outlet filter footer — was reverted to the original output-derived text on the next save. The reload reads chat.chat directly, so the change vanished after navigating away (regression vs 0.9.2, which predates the output mechanism). Re-derive only when the message's `output` actually changed versus what's stored, which still reflects genuine output edits but leaves independently-set content intact. Fixes #24585 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: reject collection names with unsafe characters in RAG ACL (#24982) Open WebUI's collection ACL accepted any unknown name as a legacy/ephemeral collection. In Milvus multi-tenancy mode that name becomes the `resource_id` and is interpolated unescaped into a SQL-like Milvus expression — `resource_id == '<name>'` — so a name like x' or resource_id != '' or resource_id == 'x turns the filter into a tautology and returns every tenant's chunks from the shared collection. All collection names Open WebUI generates are UUIDs, SHA-256 hex digests, or fixed-prefix variants of those — they all fit [A-Za-z0-9_-]. Add a strict format check in filter_accessible_collections (utils.py) that drops any name outside that set before any ACL or vector-store lookup, applied even on the admin bypass path. _validate_collection_access then surfaces the dropped name as a 403. As defense in depth, MilvusClient now validates resource_id at every expression-construction site and escapes single quotes / backslashes in any other string interpolated into a filter (delete ids, metadata filter values). Non-string filter values are typed-checked instead of str()-formatted. Co-authored-by: Claude <noreply@anthropic.com> * refac * refac: mirror native FC code_interpreter authz gates onto legacy XML-tag path (#24724) The native function-calling tool resolver in utils/tools.py applies five gates before exposing execute_code as a builtin tool: builtin-category enable, ENABLE_CODE_INTERPRETER global config, model capability, features.code_interpreter request flag, and the per-user features.code_interpreter permission. The legacy XML-tag detection path in streaming_chat_response_handler applied only the request-flag gate. Brings the legacy path to parity by running the same five-gate check before activating tag detection. Behaviour change is limited to deployments that previously relied on the asymmetry — admins who set ENABLE_CODE_INTERPRETER=False or revoked the per-user permission, on the legacy tool-calling mode, with the client supplying features.code_interpreter=true. Any of those three conditions met now correctly disables tag detection. Co-authored-by: sfwani <sfwani@users.noreply.github.com> * fix: handle list-shape data in Firecrawl /search response (#24712) Firecrawl /search returns either `{"data": [...]}` (flat list — v1, and what frost19k reported on #23966) or `{"data": {"web": [...]}}` (v2, current production). The parser only handled the dict shape: data = response.get('data') or {} results = data.get('web') or [] On a list-shape response, `data.get('web')` raised AttributeError, caught by the function's outer try/except, and `search_firecrawl` silently returned []. Web search worked against v2 endpoints but is one upstream-format-change away from failing closed again. Accept either. * fix: bind prompt history/version ops to the authorized prompt (#25056) The history diff, delete, and version-restore routes authorize the URL prompt_id but then act on a caller-supplied history/version id without checking it belongs to that prompt (IDOR). Filter by prompt_id in compute_diff and delete_history_entry, and reject a cross-prompt version_id in update_prompt_version. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refac Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com> * 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> * refac * refac Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com> * refac Co-Authored-By: Zixin Yu <183055163+ivvi0927@users.noreply.github.com> * refac * refac * refac * Update Kagi API endpoint and request method (#25015) Co-authored-by: russelg <russelg@users.noreply.github.com> * feat: add skills management to chat component (#25037) - Introduced skills functionality in Chat.svelte, MessageInput.svelte, and related components. - Added SkillsModal for displaying and managing available skills. - Updated state management to include selectedSkillIds and integrate skills API. - Enhanced UI to show available skills and their descriptions. - Updated translations to support skills-related text. * refac * refac * refac Co-Authored-By: Zaid Marji <91486926+zaid-marji@users.noreply.github.com> * refac * refac * refac * refac * refac * fix: apply RAG_EMBEDDING_QUERY_PREFIX to memory search queries (#24921) The query_memory endpoint embeds the search query without the configured RAG_EMBEDDING_QUERY_PREFIX, while every RAG retrieval path in retrieval/utils.py correctly passes it. Instruction-tuned embedding models (e.g. Qwen3-Embedding) produce poor results without the prefix, causing memory search to return semantically unrelated results. * refac * fix: gate chat_completion channel: branch on channel access + message scoping (#24725) * fix: gate chat_completion channel: branch on channel access + message scoping When chat_id starts with 'channel:' the chat-completion handler skips the chat ownership / storage block below it. Nothing replaced that gate. The downstream channel emitter in socket/main.py:_make_channel_ emitter writes to Messages.update_message_by_id using a caller-supplied message_id pulled from form_data['id'], with no membership check, no write-access check, and no validation that the message_id belongs to the channel. Net effect: any authenticated user could submit chat_id='channel:<any-channel-uuid>' + id='<any-message-uuid>' and overwrite that message with attacker-controlled LLM output. Cross- channel writes worked too — private channels, DMs, channels the caller has no access to. Original author attribution stayed intact on the overwritten row. Add the missing checks at the channel: branch: 1. Channel must exist (404 otherwise). 2. Non-admin caller must have write access to the channel — membership for group/dm channels, AccessGrants permission='write' for others. 3. The message_id (if supplied) must belong to the same channel — a caller with write access to channel A cannot use this path to overwrite a message in channel B. Behaviour change is limited to callers who were exploiting the gap: legitimate flows that supply a message_id under their own channel membership continue to work unchanged. Co-authored-by: sfwani <sfwani@users.noreply.github.com> * chore: trim verbose comment on channel: branch gate --------- Co-authored-by: sfwani <sfwani@users.noreply.github.com> * refac * feat(retrieval): add Perplexity attribution header (#24833) Signed-off-by: James Liounis <james.liounis@perplexity.ai> * refac * refac * Update CHANGELOG.md (#25453) * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * fix: polyfill readable stream async iteration for Safari PDF extraction (#25473) * refactor: move background tasks handler call to ensure consistent execution in chat response handlers (#24717) * refac * fix(oauth): use Protected Resource Metadata scopes in static OAuth 2.1 flow (#24690) The static credentials OAuth flow currently sets scope=None, relying on the OAuth provider's default scopes. This breaks providers like GitHub that default to minimal/public-only access when no scope is requested. This change reads scopes_supported from the Protected Resource Metadata document (RFC 9728) and uses them in the authorization request. Unlike the Authorization Server's scopes_supported (a full catalog of every scope the AS can grant), the PRM scopes_supported represents what the specific resource requires — making it safe to request without breaking providers like Entra ID that reject broad scope requests. Fixes the regression introduced in349ea4eawhere all scope handling was removed from the static flow. * refac * chore: bump * chore * chore: format * Update CHANGELOG.md (#25491) * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * refac * refac Co-Authored-By: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com> * chore: format * refac Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> * fix: delete Qdrant points by ID so memory deletions don't orphan vectors (#25495) The Qdrant backends implemented delete(ids=...) as a payload filter on metadata.id, but points are stored with the item id as the Qdrant point id (see _create_points), and not every point carries an id in its payload. Memory points store only {created_at} in metadata (KB metadata embeddings likewise), so deleting a single memory matched nothing and left an orphaned vector that kept being injected into RAG context. Delete by point id instead: PointIdsList for the standard backend, and a tenant-scoped HasIdCondition for multitenancy (point ids are unique, so tenant isolation is preserved). Filter-based deletion is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(fr-fr): update frensh translations (#24614) Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch> Co-authored-by: Tim Baek <tim@openwebui.com> * fix: block private-IP webhook URLs to close SSRF on caller-controlled URL (#24587) * fix: block private-IP webhook URLs to close SSRF on caller-controlled URL post_webhook(url, ...) in utils/webhook.py forwards the URL straight to aiohttp.ClientSession.post with no SSRF gate. The URL is caller-controlled on two surfaces: - User notification settings under ENABLE_USER_WEBHOOKS=true — any authenticated user can set the URL their notifications POST to. - Automation notification triggers (calendar alerts, etc.). Without a gate, the URL can target cloud metadata (169.254.169.254 / fd00:ec2::254), localhost-bound services, RFC1918 internal hosts, or any other private address reachable from the server process. Blind SSRF — no response body returned to the caller — but enough to enumerate internal services via response timing / status codes, and on cloud deployments enough to issue requests against IMDSv1 if available. Call validate_url() at the top of post_webhook. The function blocks private/reserved IPs when ENABLE_RAG_LOCAL_WEB_FETCH is False (the default), is the project's chosen SSRF gate, and is already applied to the equivalent fetch surfaces (retrieval, image-load, OAuth profile picture). Operators who legitimately need to webhook to private IPs (internal monitoring, self-hosted Slack alternatives, etc.) can set ENABLE_RAG_LOCAL_WEB_FETCH=True — same opt-out as the other gated surfaces. Scope intentionally limited to webhooks. The OAuth discovery and external reranker paths cwanglab also flagged are admin-configured with intentional private-IP defaults (reranker defaults to http://localhost:8080/v1/rerank) and are out of scope per Rule 9 — the admin owns the URL choice and the operator opt-out exists for them too. Reported by cwanglab in GHSA-5x9f-85cg-w3hf (cluster canonical with six closed siblings: g36v-23gj-j69x, 6j8f-h58v-xgmw, xpwv-52pm-p8hj, v9gp-hv2c-9qv8, fw7w-jrw7-p3v9, x7xq-74rg-m8mf). Co-authored-by: cwanglab <cwanglab@users.noreply.github.com> * fix: also pass allow_redirects=False on webhook post_webhook session.post Companion to the previous commit. validate_url() only validates the initial URL; aiohttp's default allow_redirects=True would still follow a 302 to a private-IP target. Same redirect-bypass class as the rh5x cluster's five call sites, sixth call site to receive the same gate. Co-authored-by: cwanglab <cwanglab@users.noreply.github.com> --------- Co-authored-by: cwanglab <cwanglab@users.noreply.github.com> * refac * chore: format * refac * refac * refac --------- Signed-off-by: Riley Des <riley.desserre@improving.com> Signed-off-by: James Liounis <james.liounis@perplexity.ai> Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com> Co-authored-by: Taey <pythontogoplease@gmail.com> Co-authored-by: Daniel Nylander <po@danielnylander.se> Co-authored-by: Daniel Nylander <daniel@danielnylander.se> Co-authored-by: Asbjørn Dyhrberg Thegler <asbjoern@dyhrbergthegler.dk> Co-authored-by: Aleix Dorca <aleixdorca@mac.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com> Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com> Co-authored-by: mayamsin <58122670+mayamsin@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: G30 <50341825+silentoplayz@users.noreply.github.com> Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: POV9en <POV9en@users.noreply.github.com> Co-authored-by: oxsignal <oxsignal@users.noreply.github.com> Co-authored-by: Dara Adib <dara@quietapple.org> Co-authored-by: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com> Co-authored-by: Shirasawa <764798966@qq.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: maco <gosarmarcel7@gmail.com> Co-authored-by: Sakıp Han Dursun <100518315+sakiphan@users.noreply.github.com> Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com> Co-authored-by: Mateusz Hajder <6783135+mhajder@users.noreply.github.com> Co-authored-by: Amir Subhi <amirsubhi@hotmail.com> Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com> Co-authored-by: bannert <58707896+bannert1337@users.noreply.github.com> Co-authored-by: Boris Rybalkin <ribalkin@gmail.com> Co-authored-by: HW <hwinkler@first-it-consulting.de> Co-authored-by: sfwani <sfwani@users.noreply.github.com> Co-authored-by: rileydes-improving <riley.desserre@improving.com> Co-authored-by: Zixin Yu <183055163+ivvi0927@users.noreply.github.com> Co-authored-by: Lukáš Kucharczyk <lukas@kucharczyk.xyz> Co-authored-by: russelg <russelg@users.noreply.github.com> Co-authored-by: Mr. Meowgi <ovehbe@gmail.com> Co-authored-by: Zaid Marji <91486926+zaid-marji@users.noreply.github.com> Co-authored-by: Craig <66838006+TipKnuckle@users.noreply.github.com> Co-authored-by: James Liounis <james.liounis@perplexity.ai> Co-authored-by: Chane Lu <2522992009@qq.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: Justin Williams <justinjohnwilliams@gmail.com> Co-authored-by: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com> Co-authored-by: hungryBird <pantazis.marina@gmail.com> Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch> Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>
Open WebUI 👋
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline. It supports various LLM runners like Ollama and OpenAI-compatible APIs, with built-in inference engine for RAG, making it a powerful AI deployment solution.
Passionate about open-source AI? Join our team →
Tip
Looking for an Enterprise Plan? – Speak with Our Sales Team Today!
Get enhanced capabilities, including custom theming and branding, Service Level Agreement (SLA) support, Long-Term Support (LTS) versions, and more!
For more information, be sure to check out our Open WebUI Documentation.
Key Features of Open WebUI ⭐
-
🚀 Effortless Setup: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both
:ollamaand:cudatagged images. -
🤝 Ollama/OpenAI API Integration: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with LMStudio, GroqCloud, Mistral, OpenRouter, and more.
-
🛡️ Granular Permissions and User Groups: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
-
📱 Responsive Design: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
-
📱 Progressive Web App (PWA) for Mobile: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
-
✒️🔢 Full Markdown and LaTeX Support: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
-
🎤📹 Hands-Free Voice/Video Call: Experience seamless communication with integrated hands-free voice and video call features using multiple Speech-to-Text providers (Local Whisper, OpenAI, Deepgram, Azure) and Text-to-Speech engines (Azure, ElevenLabs, OpenAI, Transformers, WebAPI), allowing for dynamic and interactive chat environments.
-
🛠️ Model Builder: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through Open WebUI Community integration.
-
🐍 Native Python Function Calling Tool: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
-
💾 Persistent Artifact Storage: Built-in key-value storage API for artifacts, enabling features like journals, trackers, leaderboards, and collaborative tools with both personal and shared data scopes across sessions.
-
📚 Local RAG Integration: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support using your choice of 9 vector databases and multiple content extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, PaddleOCR-vl, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the
#command before a query. -
🔍 Web Search for RAG: Perform web searches using 15+ providers including
SearXNG,Google PSE,Brave Search,Kagi,Mojeek,Tavily,Perplexity,serpstack,serper,Serply,DuckDuckGo,SearchApi,SerpApi,Bing,Jina,Exa,Sougou,Azure AI Search, andOllama Cloud, injecting results directly into your chat experience. -
🌐 Web Browsing Capability: Seamlessly integrate websites into your chat experience using the
#command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions. -
🎨 Image Generation & Editing Integration: Create and edit images using multiple engines including OpenAI's DALL-E, Gemini, ComfyUI (local), and AUTOMATIC1111 (local), with support for both generation and prompt-based editing workflows.
-
⚙️ Many Models Conversations: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
-
🔐 Role-Based Access Control (RBAC): Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
-
🗄️ Flexible Database & Storage Options: Choose from SQLite (with optional encryption), PostgreSQL, or configure cloud storage backends (S3, Google Cloud Storage, Azure Blob Storage) for scalable deployments.
-
🔍 Advanced Vector Database Support: Select from 9 vector database options including ChromaDB, PGVector, Qdrant, Milvus, Elasticsearch, OpenSearch, Pinecone, S3Vector, and Oracle 23ai for optimal RAG performance.
-
🔐 Enterprise Authentication: Full support for LDAP/Active Directory integration, SCIM 2.0 automated provisioning, and SSO via trusted headers alongside OAuth providers. Enterprise-grade user and group provisioning through SCIM 2.0 protocol, enabling seamless integration with identity providers like Okta, Azure AD, and Google Workspace for automated user lifecycle management.
-
☁️ Cloud-Native Integration: Native support for Google Drive and OneDrive/SharePoint file picking, enabling seamless document import from enterprise cloud storage.
-
📊 Production Observability: Built-in OpenTelemetry support for traces, metrics, and logs, enabling comprehensive monitoring with your existing observability stack.
-
⚖️ Horizontal Scalability: Redis-backed session management and WebSocket support for multi-worker and multi-node deployments behind load balancers.
-
🌐🌍 Multilingual Support: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
-
🧩 Pipelines, Open WebUI Plugin Support: Seamlessly integrate custom logic and Python libraries into Open WebUI using Pipelines Plugin Framework. Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. Examples include Function Calling, User Rate Limiting to control access, Usage Monitoring with tools like Langfuse, Live Translation with LibreTranslate for multilingual support, Toxic Message Filtering and much more.
-
🌟 Continuous Updates: We are committed to improving Open WebUI with regular updates, fixes, and new features.
Want to learn more about Open WebUI's features? Check out our Open WebUI documentation for a comprehensive overview!
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
How to Install 🚀
Installation via Python pip 🐍
Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using Python 3.11 to avoid compatibility issues.
-
Install Open WebUI: Open your terminal and run the following command to install Open WebUI:
pip install open-webui -
Running Open WebUI: After installation, you can start Open WebUI by executing:
open-webui serve
This will start the Open WebUI server, which you can access at http://localhost:8080
Quick Start with Docker 🐳
Note
Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on Open WebUI Documentation is ready to assist you.
Warning
When using Docker to install Open WebUI, make sure to include the
-v open-webui:/app/backend/datain your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
Tip
If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either
:cudaor:ollama. To enable CUDA, you must install the Nvidia CUDA container toolkit on your Linux/WSL system.
Installation with Default Configuration
-
If Ollama is on your computer, use this command:
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main -
If Ollama is on a Different Server, use this command:
To connect to Ollama on another server, change the
OLLAMA_BASE_URLto the server's URL:docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main -
To run Open WebUI with Nvidia GPU support, use this command:
docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
Installation for OpenAI API Usage Only
-
If you're only using OpenAI API, use this command:
docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
Installing Open WebUI with Bundled Ollama Support
This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup:
-
With GPU Support: Utilize GPU resources by running the following command:
docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama -
For CPU Only: If you're not using a GPU, use this command instead:
docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly.
After installation, you can access Open WebUI at http://localhost:3000. Enjoy! 😄
Other Installation Methods
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our Open WebUI Documentation or join our Discord community for comprehensive guidance.
Troubleshooting
Encountering connection issues? Our Open WebUI Documentation has got you covered. For further assistance and to join our vibrant community, visit the Open WebUI Discord.
Open WebUI: Server Connection Error
If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the --network=host flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: http://localhost:8080.
Example Docker Command:
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
Keeping Your Docker Installation Up-to-Date
Check our Updating Guide available in our Open WebUI Documentation.
Using the Dev Branch 🌙
Warning
The
:devbranch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the :dev tag like this:
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
Offline Mode
If you are running Open WebUI in an offline environment, you can set the HF_HUB_OFFLINE environment variable to 1 to prevent attempts to download models from the internet.
export HF_HUB_OFFLINE=1
What's Next? 🌟
Discover upcoming features on our roadmap in the Open WebUI Documentation.
License 📜
This project contains code under multiple licenses. The current codebase includes components licensed under the Open WebUI License with an additional requirement to preserve the "Open WebUI" branding, as well as prior contributions under their respective original licenses. For a detailed record of license changes and the applicable terms for each section of the code, please refer to LICENSE_HISTORY. For complete and updated licensing details, please see the LICENSE and LICENSE_HISTORY files.
Support 💬
If you have any questions, suggestions, or need assistance, please open an issue or join our Open WebUI Discord community to connect with us! 🤝
Star History
Created by Timothy Jaeryang Baek - Let's make Open WebUI even more amazing together! 💪

