diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml index 562e6aa1c1..ee2d689d89 100644 --- a/.github/workflows/format-backend.yaml +++ b/.github/workflows/format-backend.yaml @@ -40,10 +40,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black + pip install "ruff>=0.15.5" - - name: Format backend - run: npm run format:backend - - - name: Check for changes after format - run: git diff --exit-code + - name: Ruff format check + run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml deleted file mode 100644 index bfbb892959..0000000000 --- a/.github/workflows/ruff.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Ruff - -on: - push: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - pull_request: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - -jobs: - ruff: - name: 'Lint & Format Backend' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get changed Python files - id: changed - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - BASE=${{ github.event.pull_request.base.sha }} - else - BASE=${{ github.event.before }} - fi - FILES=$(git diff --name-only --diff-filter=ACMR "$BASE" HEAD -- 'backend/**.py' | xargs) - echo "files=$FILES" >> "$GITHUB_OUTPUT" - - - name: Ruff check - if: steps.changed.outputs.files != '' - uses: astral-sh/ruff-action@v3 - with: - args: check ${{ steps.changed.outputs.files }} - - - name: Ruff format - if: steps.changed.outputs.files != '' - uses: astral-sh/ruff-action@v3 - with: - args: format --check ${{ steps.changed.outputs.files }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d82a7d2275..126b19e028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,168 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.12] - 2026-03-26 + +### Added + +- 🌐 **Translation updates.** Translations for Simplified Chinese, Catalan, Portuguese (Brazil), Finnish, and Lithuanian were enhanced and expanded. + +### Fixed + +- 🔒 **Terminal server connection security.** Terminal server verification and policy saving now proxy through the backend, preventing API key exposure and CORS errors when connecting to in-cluster services. [Commit](https://github.com/open-webui/open-webui/commit/a6413257079a52fa4487eda36543f3955d0fbd53), [Commit](https://github.com/open-webui/open-webui/commit/4567cdc0d9cb7b42b6eba7b676c0ced3f4850d31) +- 🛠️ **Terminal tools exception handling.** Exceptions in middleware.py due to invalid return values from get_terminal_tools() have been resolved. [Commit](https://github.com/open-webui/open-webui/commit/52a06bd48aff34fb2211aac2879f0cd028129267) +- 📦 **Missing beautifulsoup4 dependency.** Users can now start Open WebUI using uvx without encountering the "bs4 module missing" error. [Commit](https://github.com/open-webui/open-webui/commit/1994d65306bbcc7406584e1bfef82f5d353fc91c) +- 🔌 **API files list error.** The /api/v1/files/ endpoint no longer returns a 500 error, fixing a regression that prevented file listing via the API. [Commit](https://github.com/open-webui/open-webui/commit/11f52921dc21c2dc61c03f12bcdf6f19140a350c) +- 📜 **License data loading.** License data now loads correctly, displaying the expected color and logo in the interface. [Commit](https://github.com/open-webui/open-webui/commit/16335f866ea4cedf00c4971963622fcc1fe02d82) +- 👑 **Admin model visibility.** Administrators can now see models even when no access control is configured yet, allowing them to manage all available models. [Commit](https://github.com/open-webui/open-webui/commit/f3f8f9874f55282603c2650b91801640cb3f69cb) +- 📊 **Tool call embed visibility.** Rich UI embeds from tool calls (like visualizations) are now rendered outside collapsed groups and remain visible without requiring manual expansion. [Commit](https://github.com/open-webui/open-webui/commit/4c872a8d128757d4a6f311fb86bc382af2ba5d0d), [Commit](https://github.com/open-webui/open-webui/commit/308fa924a5b2b7e08cd1e8f15b9c8c96e1de8f02) + +## [0.8.11] - 2026-03-25 + +### Added + +- 🔀 **Responses API streaming improvements.** The OpenAI proxy now properly handles tool call streaming and re-invocations in the Responses API, preventing duplicate tool calls and preserving output during model re-invocations. [Commit](https://github.com/open-webui/open-webui/commit/93415a48e8893139db13d02d0a6d24e8604a2ac5), [Commit](https://github.com/open-webui/open-webui/commit/f8b3a32caf00dad76687fd8fe698b86f304f3997), [Commit](https://github.com/open-webui/open-webui/commit/2ae47cf20057e92a83fd618b938f3ee9bb124e5b), [Commit](https://github.com/open-webui/open-webui/commit/adcbba34f8bbfbab3e4041269a084f2b71c076d9) +- 🔀 **Responses API stateful sessions.** Administrators can now enable experimental stateful session support via the ENABLE_RESPONSES_API_STATEFUL environment variable, allowing compatible backends to store responses server-side with previous_response_id anchoring for improved multi-turn conversations. [Commit](https://github.com/open-webui/open-webui/commit/dfc2dc2c0bd298cb4bfcf212ef11223586aa54f1) +- 📄 **File viewing pagination.** The view_file and view_knowledge_file tools now support pagination with offset and max_chars parameters, allowing models to read large files in chunks. [Commit](https://github.com/open-webui/open-webui/commit/5d7766e1b6f7ca7749c5a5a780d7b1bb2da28a2f) +- 🗺️ **Knowledge search scoping.** The search_knowledge_files tool now respects model-attached knowledge, searching only within attached knowledge bases and files when available. [Commit](https://github.com/open-webui/open-webui/commit/0f0ba7dadd043460d205477fd3b57556aa970847) +- 🛠️ **Tool HTML embed context.** Tools can now return custom context alongside HTML embeds by using a tuple format, providing the LLM with actionable information instead of a generic message. [#22691](https://github.com/open-webui/open-webui/pull/22691) +- 🔒 **Trusted role header configuration.** Administrators can now configure the WEBUI_AUTH_TRUSTED_ROLE_HEADER environment variable to set user roles (admin, user, or pending) via a trusted header from their identity provider or reverse proxy. [#22523](https://github.com/open-webui/open-webui/pull/22523) +- 🔑 **OIDC authorization parameter injection.** Administrators can now inject extra parameters into the OIDC authorization redirect URL via the OAUTH_AUTHORIZE_PARAMS environment variable, enabling IdP pre-selection for brokers like CILogon and Keycloak. [#22863](https://github.com/open-webui/open-webui/issues/22863), [Commit](https://github.com/open-webui/open-webui/commit/69171a4c8bb7f995461b4a2feef194f112b32004) +- 🔑 **Google OAuth session persistence.** Administrators can now configure Google OAuth to issue refresh tokens via the GOOGLE_OAUTH_AUTHORIZE_PARAMS environment variable, preventing OAuth sessions from expiring after one hour and ensuring tools and integrations that rely on OAuth tokens remain functional. [#22652](https://github.com/open-webui/open-webui/pull/22652) +- 🔌 **Embed prompt confirmation.** Interactive tool embeds can now submit prompts to the chat without requiring same-origin access, showing a confirmation dialog for cross-origin requests to prevent abuse. [#22908](https://github.com/open-webui/open-webui/pull/22908) +- 🏮 **Tool binary response handling.** Tool servers can now return binary data such as images, which are properly processed and displayed in chat for both multimodal and non-multimodal models. [Commit](https://github.com/open-webui/open-webui/commit/1c25b06dca83ad491b4dc3d373b1c215a7a8fd3e), [Commit](https://github.com/open-webui/open-webui/commit/108a019cb8e63a533250abe84f2b6f2b7c2131c4) +- ⚡ **Svelte upgrade performance.** Page and markdown rendering are now approximately 25% faster across the board, with significantly less memory usage for smoother UI interactions. [#22611](https://github.com/open-webui/open-webui/issues/22611) +- 🧩 **Model and filter lookup optimization.** Model and filter membership lookups are now faster thanks to optimized data structure operations during model list loading. [Commit](https://github.com/open-webui/open-webui/commit/7eae377c01f8d2de94a694b72279f769c82658cd) +- 💨 **Chat render throttling.** Chat message rendering now uses requestAnimationFrame batching to stay smooth during rapid model responses, preventing dropped frames when fast models send many events per second. [#22947](https://github.com/open-webui/open-webui/pull/22947) +- 🚀 **Function list API optimization.** The functions list API now returns only essential metadata without function source code, reducing payload sizes by over 99% and making the Functions admin page load significantly faster. [#22788](https://github.com/open-webui/open-webui/pull/22788) +- ✨ **Smoother loading animation.** The loading shimmer animation now looks smoother and more natural, with softer highlight colors. [#22516](https://github.com/open-webui/open-webui/pull/22516) +- 🧪 **Terminal connection verification.** Users can now verify their terminal server connection is working before saving the configuration, making setup more reliable. [#22567](https://github.com/open-webui/open-webui/pull/22567) +- 📁 **Chat folder emoji reset.** Users can now reset chat folder emojis back to the default icon using a "Reset to Default" button in the emoji picker, making it easier to revert custom icons. [#22554](https://github.com/open-webui/open-webui/pull/22554) +- 📊 **Metrics export interval configuration.** Administrators can now control OpenTelemetry metrics export frequency via the OTEL_METRICS_EXPORT_INTERVAL_MILLIS environment variable, enabling cost optimization for metrics services like Grafana Cloud. [#22529](https://github.com/open-webui/open-webui/pull/22529) +- 🏥 **Readiness probe endpoint.** A new /ready endpoint is now available for Kubernetes deployments, returning 200 only after startup completes and database/Redis are reachable, enabling more reliable container orchestration. [#22507](https://github.com/open-webui/open-webui/pull/22507) +- 🔩 **Tool server timeout configuration.** Administrators can now configure a separate HTTP timeout for tool server requests via the AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER environment variable, enabling fine-tuned control over how long tool calls are allowed to take. [Commit](https://github.com/open-webui/open-webui/commit/a3238aa79f344765f5b62cb64eba71ffd001abaf) +- 📌 **Knowledge file previews.** Knowledge base files can now be opened in a new tab directly from the file list, making it easier to view content without downloading. [#22629](https://github.com/open-webui/open-webui/pull/22629) +- 🎯 **Knowledge tool hybrid search support.** The built-in query_knowledge_files tool now respects hybrid search and reranking settings, matching the behavior of the middleware RAG pipeline. [Commit](https://github.com/open-webui/open-webui/commit/9a2c60d5954ecbc172d09e9955d52a07d135dcbc) +- 🗣️ **Temporary chat folder support.** Temporary chats can now use folder-level system prompts and knowledge files, making them more powerful for quick explorations. [Commit](https://github.com/open-webui/open-webui/commit/adcc50d3370301afd5561e0f58ff6f3ab3750818) +- 📡 **Terminal port previews.** Detected ports in the File Navigator can now be previewed inline with a browser-style view, navigation controls, and an address bar, instead of only opening in a new tab. [Commit](https://github.com/open-webui/open-webui/commit/689061822173e561a153290b2bb816f4cb6f4959), [Commit](https://github.com/open-webui/open-webui/commit/1dc647f43b1929f5c4d1af393a90a47f56cb745e) +- ✏️ **File renaming.** Files and folders in the File Navigator can now be renamed by double-clicking or using the context menu, with Enter to confirm and Escape to cancel. [Commit](https://github.com/open-webui/open-webui/commit/637cd136c2271baf4787815bc8bc25241626a943) +- 🧭 **File Navigator navigation history.** The File Navigator toolbar now includes Back and Forward buttons for navigating through folder and file history, similar to a web browser. [Commit](https://github.com/open-webui/open-webui/commit/3a4b862e818c69fff6f3a3c67b50c51aa00c03e9) +- 🗑️ **Delete connection confirmations.** Users are now prompted with a confirmation dialog before deleting connections, preventing accidental deletions. [Commit](https://github.com/open-webui/open-webui/commit/157ff57c40bc40c53bc608828dac3779e95c2ffa) +- 📦 **Document loader fallbacks.** Excel and PowerPoint files can now be processed even when the unstructured package is not installed, using pandas and python-pptx as fallback loaders. [Commit](https://github.com/open-webui/open-webui/commit/6862d618ee17f95d3cae78819ed993e7fbc7e632) +- 🧠 **Memory management search and sort.** Users can now search and sort their personal memories in the Memory management modal, making it easier to find specific memories. [Commit](https://github.com/open-webui/open-webui/commit/47ab4c71d50fd631b04c95f2febb085dd0a13083) +- 📦 **SBOM generation script.** A new script for generating CycloneDX Software Bill of Materials is now available in the scripts directory. [Commit](https://github.com/open-webui/open-webui/commit/39100eca4915e4fe86a6912aa97dde86ed72e015) +- ⚙️ **Ruff linter and formatter.** Added Ruff as the Python linter and formatter, replacing the black-based workflow for better code quality with near-instant execution. [#22576](https://github.com/open-webui/open-webui/pull/22576), [#22462](https://github.com/open-webui/open-webui/discussions/22462) +- 🖥️ **Offline code formatting support.** The black formatter for Python code editing is now bundled locally in the Docker image, enabling code formatting to work in air-gapped deployments where client browsers cannot reach PyPI. Formatting failures no longer block saves, allowing code to be preserved even when offline. [#22509](https://github.com/open-webui/open-webui/issues/22509), [Commit](https://github.com/open-webui/open-webui/commit/8507e5eb0d18896f1bbf990a00a4361aec171a30) +- ✏️ **Markdown file editing.** Users can now edit and save Markdown files directly in the file navigator, with empty files automatically switching to editor mode for immediate editing. [Commit](https://github.com/open-webui/open-webui/commit/47e47e42af682e7f75c8359999f7cdf969bf903e) +- 🍔 **Model bulk actions menu.** Users can now quickly enable, disable, show, or hide multiple models at once using a new hamburger menu on the workspace Models page filter bar, with actions respecting the current search and filter settings. [#22484](https://github.com/open-webui/open-webui/pull/22484) +- 📂 **Files list pagination.** The files list API now supports pagination, returning paginated results with a total count for easier navigation through large file collections. [Commit](https://github.com/open-webui/open-webui/commit/f9756de693a93e918c037d757afddb7defc847e4) +- 🖇 **Web fetch content length config.** Administrators can now configure the maximum characters to return from fetched URLs via WEB_FETCH_MAX_CONTENT_LENGTH environment variable or the admin settings page, instead of the previous hardcoded 50K limit. [Commit](https://github.com/open-webui/open-webui/commit/b171b0216b916745420c7caf513093a315ed9560), [#22774](https://github.com/open-webui/open-webui/issues/22774) +- 🤖 **Ollama Anthropic endpoint support.** The Ollama proxy now supports the Anthropic-compatible /v1/messages endpoint, allowing clients using the Anthropic API format to work through Open WebUI with proper authentication and model access controls. [Commit](https://github.com/open-webui/open-webui/commit/f23296b22d3304e5bfcd19151e5802eec55bd98f), [#22861](https://github.com/open-webui/open-webui/issues/22861) +- 📝 **Writing block rendering.** Responses from OpenAI models that include :::writing blocks are now rendered as formatted content in a styled container with a copy button, instead of displaying raw marker text. [#22672](https://github.com/open-webui/open-webui/issues/22672), [Commit](https://github.com/open-webui/open-webui/commit/53b8a1f71bd0cb0a0122175ad5210da492018728) +- 💡 **Memory deletion confirmation.** Users are now asked to confirm before deleting individual memory entries, with the memory content displayed for review. [#22888](https://github.com/open-webui/open-webui/pull/22888) +- 📓 **Multi-artifact HTML rendering.** Code blocks with multiple HTML sections now render as separate artifacts instead of merging into one, allowing models to display distinct interactive components. [Commit](https://github.com/open-webui/open-webui/commit/9a6bf78e14a13864e72db87426da4f5996abe716) +- 🚩 **Drag chats as references.** Users can now drag chats from the sidebar and drop them into the message input to add them as Reference Chats. [Commit](https://github.com/open-webui/open-webui/commit/ebb7ce2092efc8d78da4974623647dbd18b6e372) +- ⌨️ **Terminal system prompts.** Terminal servers can now provide custom system prompts that are automatically included when their tools are used. [Commit](https://github.com/open-webui/open-webui/commit/6a9d67b5bb4c93fd343b334bee3e37703dff59f6) +- 💾 **Terminal state persistence.** The selected terminal server and its enabled state now persist across page loads, making terminal usage more seamless. [Commit](https://github.com/open-webui/open-webui/commit/d577ff1e4af750dda09e558dac7edb8dd2470850) +- 💾 **Terminal folder downloads.** Users can now download folders as ZIP archives and bulk-download multiple selected files as a single ZIP directly from the File Navigator toolbar, making file exports faster and more convenient. [Commit](https://github.com/open-webui/open-webui/commit/3841e85abb3ea3e8d8b364dff0102f0124844d22), [Commit](https://github.com/open-webui/open-webui/commit/cf60b1882f1929200649b59f867289dea54e4210) +- 🔐 **MCP OAuth 2.1 static credentials.** MCP servers that require static client_id and client_secret can now be connected using a new OAuth 2.1 Static auth type, enabling integration with MCP servers that don't support dynamic client registration. [#22266](https://github.com/open-webui/open-webui/pull/22266), [Commit](https://github.com/open-webui/open-webui/commit/601bb783587a3e965cf88c148e4856b988655b13) +- 🎪 **Collapsible tool and thinking groups.** Consecutive tool calls and reasoning blocks are now grouped into a single collapsible summary (e.g., "Explored tool1, tool2"), keeping chat responses clean and readable while preserving full detail on expand. [#21604](https://github.com/open-webui/open-webui/issues/21604), [Commit](https://github.com/open-webui/open-webui/commit/261aec8c864646eb7215be0d5c14a79cad3cb93f) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Finnish, Portuguese (Portugal), Catalan, Turkish, Japanese, Simplified Chinese, Traditional Chinese, Estonian, Spanish, Azerbaijani, and German were enhanced and expanded. + +### Fixed + +- 🔒 **Model access control bypass.** Fixed a security vulnerability where external clients could bypass model access controls by setting a URL parameter, preventing unauthorized access to restricted models. [Commit](https://github.com/open-webui/open-webui/commit/c0385f60ba049da48d2d5452068586d375303c37) +- 🛡️ **Terminal proxy path sanitization.** The terminal server proxy now properly sanitizes paths to prevent directory traversal and SSRF attacks, protecting against security vulnerabilities. [Commit](https://github.com/open-webui/open-webui/commit/f9d38a073fae32032ed44073cf2817cba20210bb) +- 🛡️ **Tool configuration access control.** Tool configuration endpoints now properly verify user permissions, preventing unauthorized access to tool settings. [Commit](https://github.com/open-webui/open-webui/commit/bc5b3ec6b8ec0fef894eb8046c636ee33688b8c4) +- 🗝️ **Tool valves access control.** The tool user valves endpoints now properly verify ownership and access grants before returning or updating configuration, with appropriate 404 responses for missing tools and 401 for unauthorized access. [Commit](https://github.com/open-webui/open-webui/commit/f949d17db1e62e0b79aecbbcbcabe3d57d8d4af6) +- 🔐 **Collaborative document authorization.** Fixed a security vulnerability in collaborative documents where authorization could be bypassed using alternative document ID formats, preventing unauthorized access to notes. [Commit](https://github.com/open-webui/open-webui/commit/3107a5363d13c899a995c930cbb1121a80f754f9) +- 🔏 **OAuth session persistence.** Users logging in via OAuth or OIDC providers now stay logged in for the configured JWT expiry duration instead of being logged out when closing the browser. [#22809](https://github.com/open-webui/open-webui/pull/22809) +- 🚪 **OAuth sub claim configuration crash.** Using the OAUTH_SUB_CLAIM environment variable no longer causes crashes during token exchange requests, fixing a missing configuration registration. [#22865](https://github.com/open-webui/open-webui/pull/22865) +- 🔍 **OAuth discovery header parsing.** The OAuth protected resource discovery now correctly handles both quoted and unquoted values in the WWW-Authenticate header, fixing compatibility with MCP servers that return unquoted metadata. [#22646](https://github.com/open-webui/open-webui/discussions/22646), [Commit](https://github.com/open-webui/open-webui/commit/fe7e002fea7283abcf901e22de5c8a7d86e336ea) +- 👤 **Admin OAuth group sync.** Admin user group memberships from OAuth and LDAP providers are now properly synced to Open WebUI, fixing a limitation where admin role excluded users from group updates. [#22537](https://github.com/open-webui/open-webui/pull/22537), [Commit](https://github.com/open-webui/open-webui/commit/a1aceb5f879abd130ef83085d98a0d51316a8fc3) +- 🎫 **Password change complexity validation.** Password complexity rules are now properly enforced when users change their password, closing a security gap where new passwords could bypass configured complexity requirements. [Commit](https://github.com/open-webui/open-webui/commit/bd8aa3b6a0b6a2320f41b20a51b9842f39aadb7f) +- 🔏 **OAuth role enforcement.** OAuth role management now properly denies access when a user's roles don't match any configured OAUTH_ALLOWED_ROLES or OAUTH_ADMIN_ROLES, instead of silently bypassing the restriction. [#13676](https://github.com/open-webui/open-webui/issues/13676), [#15551](https://github.com/open-webui/open-webui/issues/15551), [Commit](https://github.com/open-webui/open-webui/commit/6d7744c21903ec5a9ad951770dea76e9ba19cbcc) +- 🔑 **Microsoft Entra ID role claim preservation.** Role claims from Microsoft Entra ID tokens are now preserved during OAuth login, fixing ENABLE_OAUTH_ROLE_MANAGEMENT for Microsoft OAuth which was previously ignored because the userinfo endpoint stripped the roles claim. [#20518](https://github.com/open-webui/open-webui/issues/20518), [Commit](https://github.com/open-webui/open-webui/commit/aa2f7fbe5229c3985ce427602069cdeababda481) +- 🔍 **SCIM group filtering.** The SCIM endpoint now properly handles displayName and externalId filters when provisioning groups from identity providers like Microsoft Entra ID, preventing all groups from being returned instead of the filtered subset. [#21543](https://github.com/open-webui/open-webui/pull/21543) +- 🔐 **Forwarded allow IPs configuration.** The FORWARDED_ALLOW_IPS environment variable is now properly respected by the startup scripts instead of being hardcoded to '\*', allowing administrators to restrict which proxies are trusted for request forwarding. [#22539](https://github.com/open-webui/open-webui/issues/22539), [Commit](https://github.com/open-webui/open-webui/commit/0aebdd5f83cd1d811009edcbb2bec432a34e7c81) +- 🍪 **Model list auth cookie forwarding.** Model list requests to backends that require cookie-based authentication now properly forward auth headers and cookies, preventing "Unauthorized" errors when loading models. [Commit](https://github.com/open-webui/open-webui/commit/76ece4049e96bd6890593f17a946a9af6b082fab) +- 🔱 **Model lookup race condition.** Fixed a race condition in Redis model storage that caused intermittent "model not found" errors in multi-replica deployments under heavy load, by eliminating the window between hash deletion and updates. [Commit](https://github.com/open-webui/open-webui/commit/ee901fcd2ca82d7a7dad48170c64df782d3e040a) +- 🎚️ **Bulk model action reliability.** Bulk enable, disable, show, and hide operations in the admin Models settings now properly refresh the model list after completion, ensuring changes are reflected immediately and correct toast notifications are shown. [#22962](https://github.com/open-webui/open-webui/pull/22962), [Commit](https://github.com/open-webui/open-webui/commit/75932be880f3b86f78f00b4352b9f1350b8f53fa), [Commit](https://github.com/open-webui/open-webui/commit/15ae3f588b1aa4ddb686ae68afebd6064036a201) +- 🔄 **Paginated list duplicates.** Fixed duplicate items appearing in paginated lists when loading more items in chats, knowledge, notes, and search across the UI. [Commit](https://github.com/open-webui/open-webui/commit/58e78e8946fb3644107489fe8e01b17709302b2f) +- 🧽 **Duplicate chat list refresh.** Sending messages no longer triggers duplicate sidebar chat list refreshes, eliminating an unnecessary database query that was already handled by the save and completion handlers. [#22982](https://github.com/open-webui/open-webui/pull/22982) +- 🧹 **Chat history save optimization.** The chat list is no longer refreshed on every chat history save, branch navigation, or edit — only on meaningful state changes like new chat creation, title generation, and response completion. [#22983](https://github.com/open-webui/open-webui/pull/22983) +- 💬 **Message queue responsiveness.** The message queue no longer waits for background tasks like title generation and follow-up suggestions to complete, allowing users to send new messages immediately after a response finishes without unnecessary delays. [Commit](https://github.com/open-webui/open-webui/commit/486c004cbb43f15d5c3e31561f51f22effff1f6c), [#22565](https://github.com/open-webui/open-webui/issues/22565) +- 🗄️ **Migration reliability.** Database migrations no longer fail when chat data has unexpected format, making upgrades more reliable. [#22588](https://github.com/open-webui/open-webui/pull/22588), [#22568](https://github.com/open-webui/open-webui/issues/22568) +- 🫧 **Memory modal event bubbling.** Fixed an issue where clicking the Delete button in the Memory management modal would also open the Edit Memory modal due to event bubbling. [#22783](https://github.com/open-webui/open-webui/issues/22783) +- 🧩 **Memory tool registration.** Models with capabilities.memory: true now correctly have memory tools available for execution, fixing a retry loop where add_memory appeared in the tool schema but was not registered for backend execution. [#22666](https://github.com/open-webui/open-webui/issues/22666), [#22675](https://github.com/open-webui/open-webui/pull/22675), [Commit](https://github.com/open-webui/open-webui/commit/d9339919046c3e977f313f603782d220aab4257f) +- 📝 **Input variables modal crash.** Fixed a crash that occurred when selecting custom prompts with prompt variables, causing the Input Variables modal to display an infinite loading spinner instead of the variable input fields. [#22748](https://github.com/open-webui/open-webui/issues/22748), [Commit](https://github.com/open-webui/open-webui/commit/0dcd6ac983bede06b8477179192154467f5b24a2) +- 🪛 **Function list API crash fix.** Fixed a 500 error on the functions list API endpoint that was introduced by the recent optimization, by adding proper model configuration for SQLAlchemy ORM objects. [#22924](https://github.com/open-webui/open-webui/pull/22924) +- 🗂️ **Sidebar chat menu closure.** Sidebar chat dropdown menus now close properly after clicking "Clone", "Share", "Download", "Rename", "Pin", "Move", "Archive", or "Delete", instead of remaining visible. [#22884](https://github.com/open-webui/open-webui/pull/22884), [#22784](https://github.com/open-webui/open-webui/issues/22784) +- 🧭 **Chat deletion and archive redirection.** Users are now redirected to the chat list when deleting or archiving the currently active chat, instead of being left on a stale chat page. [#22755](https://github.com/open-webui/open-webui/pull/22755) +- 🚩 **User menu navigation fix.** Clicking Playground or Admin Panel from the user menu now uses client-side routing instead of causing full page reloads, restoring smooth SPA navigation. [Commit](https://github.com/open-webui/open-webui/commit/7ffcd3908ee90f88a4c4684d6cd6e75efd117461) +- 🔧 **Tool server connection persistence.** Fixed a bug where tool server connection updates were not being saved to persistent storage, ensuring OAuth client information is now properly preserved. [Commit](https://github.com/open-webui/open-webui/commit/b8ea267f8ec3931de55db7801156b9c07d3ad5f6) +- 🔩 **Tool server index bounds checking.** Tool servers with invalid indices no longer crash the application with IndexError after upgrades, preventing tool server configuration loss. [#22490](https://github.com/open-webui/open-webui/issues/22490), [Commit](https://github.com/open-webui/open-webui/commit/8da29566a1f81c38e80009bdea3ce4d9be860605) +- 🔌 **Tool server frontend timeout.** Fetch requests to external tool servers now time out after 10 seconds, preventing the UI from hanging indefinitely when a configured tool server is unreachable. [#22543](https://github.com/open-webui/open-webui/issues/22543), [Commit](https://github.com/open-webui/open-webui/commit/adf7af34ff934319a35470c572237d2d08f1de0b) +- 🔌 **MCP OAuth tool auto-selection.** MCP tools requiring OAuth authentication are now automatically re-selected after completing the auth flow, instead of leaving users to manually re-enable the tool on return to the chat. [#22994](https://github.com/open-webui/open-webui/issues/22994), [#22995](https://github.com/open-webui/open-webui/pull/22995), [Commit](https://github.com/open-webui/open-webui/commit/4d50001c4192c609b1010626ebb6496692823873) +- 🏷️ **Channel @mentions.** Direct connection models no longer appear in channel @mention suggestions, preventing confusion since they don't work in channels. [#22553](https://github.com/open-webui/open-webui/issues/22553), [Commit](https://github.com/open-webui/open-webui/commit/0a87c1ecd078320a08c4cc62d41fe8727fb3b5f7) +- 📎 **Channel message attachments.** Users can now press Enter to send messages with only file or image attachments in channels, direct messages, and threads, aligning with the behavior of the Send button. [#22752](https://github.com/open-webui/open-webui/pull/22752) +- 🗣️ **Image-only message handling.** Models like Gemini and Claude no longer fail when receiving messages with only file or image attachments and no text, by stripping empty text content blocks before sending to the API. [Commit](https://github.com/open-webui/open-webui/commit/ea515fa26e11faac146c48a5e3a2a284e1792bb3), [#22880](https://github.com/open-webui/open-webui/issues/22880) +- 🧹 **Channel thread sidebar cleanup.** The thread sidebar in channels and direct messages now automatically closes when the parent message is deleted, preventing orphaned threads. [#22890](https://github.com/open-webui/open-webui/pull/22890) +- 💡 **Chat input suggestion modal.** The suggestion modal for tags, mentions, and commands now correctly reappears when backspacing into a trigger character after it was dismissed. [#22899](https://github.com/open-webui/open-webui/pull/22899) +- ⏱️ **Chat action button timing.** Action buttons under assistant messages no longer appear prematurely when switching chats while a response is still streaming. [Commit](https://github.com/open-webui/open-webui/commit/ecba37070d6eb3cb033195a070b6c4ab5f396415), [#22891](https://github.com/open-webui/open-webui/issues/22891) +- 💬 **Skill and model mention persistence.** Skills selected via $ and models selected via @ in the chat input are now properly restored after a page refresh, instead of reverting to plain text while losing their interactive state. [#22913](https://github.com/open-webui/open-webui/issues/22913), [Commit](https://github.com/open-webui/open-webui/commit/be21db706993c0db95ac09509dfdb023de64daff) +- 🧹 **Webhook profile image errors.** Fixed 404 errors appearing in the browser console when scrolling through channel messages sent by webhooks, by skipping the user profile preview for webhook senders. [#22893](https://github.com/open-webui/open-webui/pull/22893) +- 🧮 **Logit bias parameter handling.** Using logit_bias parameters no longer causes errors when the input is already in dictionary format. [#22597](https://github.com/open-webui/open-webui/issues/22597), [Commit](https://github.com/open-webui/open-webui/commit/e34ed72e1e958505e940b74bf1c6a4808640bd17) +- 🪛 **Temp chat tool calling.** Temporary chats now properly preserve tool call information, fixing native tool calling with JSON schema that was previously broken. [#22475](https://github.com/open-webui/open-webui/pull/22475), [Commit](https://github.com/open-webui/open-webui/commit/bcd313c363ca50d71aa80bcb2f29c81fad3dff37) +- 🔗 **Multi-system message merging.** Models with strict chat templates like Qwen no longer fail when multiple pipeline stages inject separate system messages, as all system messages are now merged into one at the start. [#22505](https://github.com/open-webui/open-webui/issues/22505), [Commit](https://github.com/open-webui/open-webui/commit/631bd20c3537ce85bbaec02f9e0049c88fa8fdd4) +- 📜 **Public note access.** Opening public notes via direct share link no longer returns a 500 error caused by a missing function import. [#22680](https://github.com/open-webui/open-webui/issues/22680), [Commit](https://github.com/open-webui/open-webui/commit/566e25569e5e7d9c1e42db840ba4ba578887d208) +- 👤 **Terminal access user visibility.** The terminal connection access dialog now shows the currently logged-in user when searching for users to grant access, fixing an issue where users with identical display names were filtered incorrectly. [#22491](https://github.com/open-webui/open-webui/issues/22491), [Commit](https://github.com/open-webui/open-webui/commit/4a8f995c3fd4602ec2aaccc07efc4e8504dda84d) +- 👥 **User groups display.** User groups in the admin panel profile preview now wrap properly instead of overflowing horizontally, with a scrollbar when the list is long. [#22547](https://github.com/open-webui/open-webui/pull/22547) +- 🔧 **Model list drag-and-drop.** Fixed drag-and-drop reordering of models in admin settings, preventing UI glitches and state synchronization issues. [Commit](https://github.com/open-webui/open-webui/commit/753589e51ccbbe5c4f78a7d13e19c67e6c0000d7) +- 🖼️ **Model profile image fallbacks.** Model profile images now display a fallback icon when they fail to load, and model icons no longer disappear on paginated Models pages in admin and workspace settings. [#22485](https://github.com/open-webui/open-webui/pull/22485) +- 🖼️ **Profile image fallbacks.** Added fallback handlers for model and user profile images throughout the chat interface, preventing broken image icons when avatars fail to load. [#22486](https://github.com/open-webui/open-webui/pull/22486) +- 🧲 **RAG thinking model support.** Knowledge base queries now correctly parse JSON responses from thinking models like GLM-5 and DeepSeek-R1 by stripping their reasoning blocks before JSON extraction. [#22400](https://github.com/open-webui/open-webui/pull/22400) +- 🔍 **RAG query generation robustness.** The RAG query generation, web search, and image generation handlers now correctly extract JSON from model responses containing thinking tags by finding the last JSON block instead of the first, preventing "No sources found" errors with thinking models. [#21888](https://github.com/open-webui/open-webui/issues/21888), [Commit](https://github.com/open-webui/open-webui/commit/c0fcbc5b4cb29012e2913983c632edc5d24b9aea) +- 🔍 **Ollama embedding robustness.** Ollama embedding requests now include the truncate parameter to handle inputs exceeding the context window, preventing 500 errors when processing long documents. Error messages from failed embedding requests are also now properly surfaced instead of being silently swallowed. [#22671](https://github.com/open-webui/open-webui/issues/22671), [Commit](https://github.com/open-webui/open-webui/commit/d738044f47c70c755bec9bf244aa11878fe98d9c) +- 🔄 **Ollama embedding retry logic.** Embedding requests to Ollama now retry with exponential backoff when encountering 503 errors (such as when the model reloads mid-processing), preventing files from being silently dropped from knowledge bases. [#22571](https://github.com/open-webui/open-webui/issues/22571), [Commit](https://github.com/open-webui/open-webui/commit/8b6fa1f4ab6099a305de08706621075c205f65c4) +- 🗄️ **Oracle 23AI hybrid search.** Fixed an UnboundLocalError that occurred when using hybrid search with Oracle 23AI as the vector store, preventing knowledge base queries from failing. [Commit](https://github.com/open-webui/open-webui/commit/fcf720835285a4cea10fc1ebed0b454971463b20), [#22616](https://github.com/open-webui/open-webui/issues/22616) +- 🌐 **Dynamic HTML language attribute.** The HTML lang attribute now dynamically updates when users change their interface language, preventing browsers from triggering unwanted translation popups. [Commit](https://github.com/open-webui/open-webui/commit/de5e0fbc00e7abcd84e1272c301b0707f8ea5ac6) +- 📐 **File upload deduplication.** Attaching files that are already in the chat no longer triggers duplicate uploads. [Commit](https://github.com/open-webui/open-webui/commit/10f06a64fed474e9958b96295a953e0eebf9e4be) +- 🕵️ **Serper.dev search results.** Fixed web search results not displaying properly when using the Serper.dev provider by using the correct API response field. [#22869](https://github.com/open-webui/open-webui/pull/22869) +- 🔲 **Markdown task list checkbox styling.** Fixed task list checkboxes in markdown rendering to display consistently without shrinking in narrow layouts. [#22886](https://github.com/open-webui/open-webui/pull/22886) +- 🎨 **Artifacts sidebar tab background fix.** The Artifacts sidebar now correctly updates and displays when switching back to a browser tab that was in the background, ensuring artifacts are visible without requiring a manual refresh. [#22889](https://github.com/open-webui/open-webui/issues/22889) +- 🔃 **Chat input URL indexing fix.** Fixed an issue where URLs could be indexed twice when using multiple triggers followed by backspace and re-entering a URL. [#22749](https://github.com/open-webui/open-webui/issues/22749) +- 🔎 **Search modal chat preview avatars.** Fixed assistant profile images not displaying in the chat preview pane of the Search Modal. [#22782](https://github.com/open-webui/open-webui/pull/22782) +- 📋 **Prompts search pagination fix.** Fixed a bug where searching prompts from a paginated page would incorrectly use the current page number, resulting in "No prompts found" even when matching results existed. [#22912](https://github.com/open-webui/open-webui/pull/22912) +- 🗂️ **Reasoning block copy cleanup.** Copied chat responses no longer include reasoning block content or excess whitespace, ensuring only the intended message text is captured. [#22786](https://github.com/open-webui/open-webui/issues/22786), [Commit](https://github.com/open-webui/open-webui/commit/4f0e57420154800946394bc986b2c691462b2782) +- 🔤 **Emoji removal for text normalization.** Fixed the emoji removal function used in search and title generation to correctly handle all emoji types, including those with variation selectors (❤️, ☀️, ✅), keycap sequences (1️⃣), and ZWJ family sequences (👨‍👩‍👧‍👦). [#22915](https://github.com/open-webui/open-webui/pull/22915) +- ⏹️ **Task cancellation status tracking.** Cancelled tasks now correctly mark only the affected messages as done instead of clearing all task statuses for the chat, ensuring proper status tracking when multiple messages have pending tasks. [#22743](https://github.com/open-webui/open-webui/pull/22743) +- 🎨 **Filter icon display fix.** Fixed filter icons showing the wrong icon after removing one of multiple active filters below the chat input. [#22862](https://github.com/open-webui/open-webui/pull/22862) +- 📊 **Channel message data loading.** Fixed redundant 404 API calls that occurred when rendering channel messages, preventing unnecessary requests and console errors. [#22894](https://github.com/open-webui/open-webui/pull/22894) +- 👻 **Response message skeleton display.** Fixed an issue where the skeleton loader would incorrectly show or hide based on complex status history conditions, by extracting the visibility logic into a cleaner reactive variable. [Commit](https://github.com/open-webui/open-webui/commit/5df4277216fbb9de603fdf4289f8366292568234) +- 🐛 **Shared chat viewing crash.** Shared chats can now be viewed by unauthenticated users without crashing, with proper fallback handling for missing user profile information. [#22751](https://github.com/open-webui/open-webui/pull/22751), [#22742](https://github.com/open-webui/open-webui/issues/22742) +- 🛠️ **Plugin ID sanitization.** Creating Functions or Tools with emojis or special characters in their names now generates valid IDs that pass backend validation, instead of failing with an error. [#22695](https://github.com/open-webui/open-webui/pull/22695) +- 📋 **Chat title preservation.** Regenerating responses or using branches no longer overwrites user-specified chat titles when auto-naming is disabled, by checking the full chat message count instead of just the current branch. [#22754](https://github.com/open-webui/open-webui/pull/22754) +- 🎧 **Read Aloud in chat preview.** The Read Aloud button in the Search Chats modal preview no longer causes crashes, and TTS functionality is now properly hidden in read-only chat contexts. [Commit](https://github.com/open-webui/open-webui/commit/d8fa0f426a88f5c27b3216b7db35e1db47bbba28) +- 📡 **Heartbeat event loop blocking.** The WebSocket heartbeat handler no longer blocks the event loop when updating user activity, improving responsiveness under heavy load with many concurrent connections. [#22980](https://github.com/open-webui/open-webui/pull/22980) +- 🗝️ **Message upsert API reliability.** The message upsert API endpoint no longer crashes when called, fixing an error where a database session was incorrectly passed to a function that doesn't accept it. [#22959](https://github.com/open-webui/open-webui/issues/22959), [Commit](https://github.com/open-webui/open-webui/commit/70285fb6cad26b50d783583b68be5227ace16055) +- 🔓 **Forward auth proxy compatibility.** Fixed error pages that could appear when using authenticating reverse-proxies by properly handling 401 responses from background API requests, allowing the browser to re-authenticate with the identity provider. [#22942](https://github.com/open-webui/open-webui/pull/22942) +- 🔃 **Tool call streaming display.** Sequential tool calls are now properly accumulated during streaming, fixing an issue where completed tool calls could disappear from the display before the next tool call finished streaming. [Commit](https://github.com/open-webui/open-webui/commit/a9c5c787b9f6b10491924d38645042064b3c941e) +- 🧠 **Reasoning spinner content preservation.** Prior assistant content and tool call blocks no longer disappear during the reasoning spinner when responding after tool execution. [#23001](https://github.com/open-webui/open-webui/pull/23001) +- 🖥️ **Pyodide file list refresh.** Files created or modified during manual code execution now appear immediately in the pyodide files list without requiring a browser tab refresh. [Commit](https://github.com/open-webui/open-webui/commit/5c4062c64841974bf193ff321d92d10f28a09746) +- 🖱️ **Dropdown submenu hover stability.** Secondary hover menus like Download and Move now remain open while navigating into them, fixing an issue where an 8px gap between the trigger and submenu would cause the menu to disappear before a selection could be made. [#22744](https://github.com/open-webui/open-webui/issues/22744), [Commit](https://github.com/open-webui/open-webui/commit/cffbc3558e911abd6c4780cd028794b2f7282cd7) +- 📊 **Model tag normalization.** Model tags from backends that return them as string arrays are now properly normalized to object format, preventing crashes when filtering models by tag in the admin and workspace models pages. [#20819](https://github.com/open-webui/open-webui/issues/20819), [Commit](https://github.com/open-webui/open-webui/commit/90ca2e9b0f15cc9be7cf298fbefacaa45074cae9) +- 🎯 **Arena model sub-model settings.** Arena models now properly use the selected sub-model's settings — including RAG knowledge bases, web access, code interpreter, and tool capabilities — instead of the arena wrapper's empty defaults. [#16950](https://github.com/open-webui/open-webui/issues/16950), [Commit](https://github.com/open-webui/open-webui/commit/857d7e6f373d26a7a8989417c3a7fe99cdc03f20) +- 🧩 **Model editor default metadata.** The Model Editor now loads admin-configured default model metadata instead of hardcoded values, preventing admin defaults from being silently overwritten when users save models without realizing they were overriding system-wide settings. [#22996](https://github.com/open-webui/open-webui/issues/22996), [Commit](https://github.com/open-webui/open-webui/commit/cdc2b3bf850044051aafcd46f22fb25a1899788c) +- ✏️ **Rich text paste sanitization.** Copying and pasting text with HTML characters (like `<` or `>`) no longer corrupts the editor content, as the paste handler now properly escapes HTML entities before processing mentions and special syntax. [Commit](https://github.com/open-webui/open-webui/commit/94f877ff328d410339308ad2c566c9afcdf43014) + +### Changed + +- 🪝 **User webhooks disabled by default.** User webhook notifications are now disabled by default and properly gated by the ENABLE_USER_WEBHOOKS configuration, ensuring webhooks only fire when explicitly enabled. [Commit](https://github.com/open-webui/open-webui/commit/c24a4da17dbaddf47e2e0f865c1d602d0ff36ee6) +- 🧩 **MCP integration visibility.** MCP (Streamable HTTP) integrations are now hidden from user-level settings, matching the intended behavior where only administrators can configure MCP connections through the admin panel. User-level connections now show the connection type as read-only. [#22615](https://github.com/open-webui/open-webui/issues/22615), [Commit](https://github.com/open-webui/open-webui/commit/1eef5b4f6a718c0fcf3605f1ed62669aca07b454) +- 🧲 **Web search result limit.** The configured web search result count now acts as a maximum limit, preventing models from requesting more results than administrators allow. [#22577](https://github.com/open-webui/open-webui/pull/22577) + ## [0.8.10] - 2026-03-08 ### Added diff --git a/backend/dev.sh b/backend/dev.sh index 042fbd9efa..838b93f653 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1,3 +1,3 @@ export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080" PORT="${PORT:-8080}" -uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload +uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" --reload diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 19778dd81f..0d792e78b2 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -682,9 +682,7 @@ def load_oauth_providers(): **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), }, redirect_uri=GOOGLE_REDIRECT_URI.value, - **({ - 'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS - } if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}), + **({'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS} if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}), ) return client diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index ec1d0c6047..c0c79fdf50 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -9,7 +9,7 @@ class MESSAGES(str, Enum): class WEBHOOK_MESSAGES(str, Enum): DEFAULT = lambda msg='': f'{msg if msg else ""}' - USER_SIGNUP = lambda username='': (f'New user signed up: {username}' if username else 'New user signed up') + USER_SIGNUP = lambda username='': f'New user signed up: {username}' if username else 'New user signed up' class ERROR_MESSAGES(str, Enum): @@ -80,8 +80,8 @@ class ERROR_MESSAGES(str, Enum): OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.' - FILE_TOO_LARGE = ( - lambda size='': f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." + FILE_TOO_LARGE = lambda size='': ( + f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." ) DUPLICATE_CONTENT = 'Duplicate content detected. Please provide unique content to proceed.' @@ -89,7 +89,7 @@ class ERROR_MESSAGES(str, Enum): 'Extracted content is not available for this file. Please ensure that the file is processed before proceeding.' ) - INVALID_PASSWORD = lambda err='': (err if err else 'The password does not meet the required validation criteria.') + INVALID_PASSWORD = lambda err='': err if err else 'The password does not meet the required validation criteria.' class TASKS(str, Enum): diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 2ebda909f6..2caa392563 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -545,6 +545,7 @@ from open_webui.utils.auth import ( from open_webui.utils.plugin import install_tool_and_function_dependencies from open_webui.utils.oauth import ( get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, encrypt_data, decrypt_data, OAuthManager, @@ -2264,7 +2265,7 @@ if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: server_id = tool_server_connection.get('info', {}).get('id') auth_type = tool_server_connection.get('auth_type', 'none') - if server_id and auth_type == 'oauth_2.1': + if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'): oauth_client_info = tool_server_connection.get('info', {}).get('oauth_client_info', '') try: @@ -2324,17 +2325,33 @@ async def register_client(request, client_id: str) -> bool: return False server_url = connection.get('url') + auth_type = connection.get('auth_type', 'none') oauth_server_key = (connection.get('config') or {}).get('oauth_server_key') try: - oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( - request, - client_id, - server_url, - oauth_server_key, - ) + if auth_type == 'oauth_2.1_static': + # Static credentials: rebuild from stored credentials + fresh metadata + existing_client_info = connection.get('info', {}).get('oauth_client_info', '') + if not existing_client_info: + log.error(f'No stored OAuth client info for static client {client_id}') + return False + existing_data = decrypt_data(existing_client_info) + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + client_id, + server_url, + oauth_client_id=existing_data.get('client_id', ''), + oauth_client_secret=existing_data.get('client_secret', ''), + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, + client_id, + server_url, + oauth_server_key, + ) except Exception as e: - log.error(f'Dynamic client re-registration failed for {client_id}: {e}') + log.error(f'OAuth client re-registration failed for {client_id}: {e}') return False try: diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index 9a5b8fa400..7a9f77a3b0 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -87,7 +87,7 @@ class FileModelResponse(BaseModel): filename: str data: Optional[dict] = None - meta: FileMeta + meta: Optional[FileMeta] = None created_at: int # timestamp in epoch updated_at: Optional[int] = None # timestamp in epoch, optional for legacy files @@ -246,7 +246,7 @@ class FilesTable: total = query.count() items = [ - FileModel.model_validate(file) + FileModelResponse.model_validate(file, from_attributes=True) for file in query.order_by(File.updated_at.desc(), File.id.desc()).offset(skip).limit(limit).all() ] diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index 5ed4b6b00d..fc4cfb0d31 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -176,6 +176,11 @@ class GroupTable: groups = db.query(Group).order_by(Group.updated_at.desc()).all() return [GroupModel.model_validate(group) for group in groups] + def get_group_by_name(self, name: str, db: Optional[Session] = None) -> Optional[GroupModel]: + with get_db_context(db) as db: + group = db.query(Group).filter(Group.name == name).first() + return GroupModel.model_validate(group) if group else None + def get_groups(self, filter, db: Optional[Session] = None) -> list[GroupResponse]: with get_db_context(db) as db: member_count = ( diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 1069d93708..9d2b5819bc 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -10,7 +10,7 @@ from open_webui.models.users import User, UserModel, Users, UserResponse from open_webui.models.access_grants import AccessGrantModel, AccessGrants -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator from sqlalchemy import String, cast, or_, and_, func from sqlalchemy.dialects import postgresql, sqlite @@ -47,7 +47,20 @@ class ModelMeta(BaseModel): model_config = ConfigDict(extra='allow') - pass + @model_validator(mode='before') + @classmethod + def normalize_tags(cls, data): + if isinstance(data, dict) and 'tags' in data: + raw_tags = data['tags'] + if isinstance(raw_tags, list): + normalized = [] + for tag in raw_tags: + if isinstance(tag, str): + normalized.append({'name': tag}) + elif isinstance(tag, dict) and 'name' in tag: + normalized.append(tag) + data['tags'] = normalized + return data class Model(Base): diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index dcd0671cd5..4ab8bdf7c0 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -30,6 +30,7 @@ from open_webui.models.knowledge import Knowledges from open_webui.models.chats import Chats from open_webui.models.notes import Notes from open_webui.models.access_grants import AccessGrants +from open_webui.utils.access_control.files import has_access_to_file from open_webui.retrieval.vector.main import GetResult from open_webui.utils.headers import include_user_info_headers @@ -550,34 +551,30 @@ def generate_openai_batch_embeddings( key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'generate_openai_batch_embeddings:model {model} batch size: {len(texts)}') - json_data = {'input': texts, 'model': model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'generate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {key}', - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - r = requests.post( - f'{url}/embeddings', - headers=headers, - json=json_data, - ) - r.raise_for_status() - data = r.json() - if 'data' in data: - return [elem['embedding'] for elem in data['data']] - else: - raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") - except Exception as e: - log.exception(f'Error generating openai batch embeddings: {e}') - return None + r = requests.post( + f'{url}/embeddings', + headers=headers, + json=json_data, + ) + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") async def agenerate_openai_batch_embeddings( @@ -587,38 +584,34 @@ async def agenerate_openai_batch_embeddings( key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'agenerate_openai_batch_embeddings:model {model} batch size: {len(texts)}') - form_data = {'input': texts, 'model': model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {key}', - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post( - f'{url}/embeddings', - headers=headers, - json=form_data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() - data = await r.json() - if 'data' in data: - return [item['embedding'] for item in data['data']] - else: - raise Exception('Something went wrong :/') - except Exception as e: - log.exception(f'Error generating openai batch embeddings: {e}') - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/embeddings', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") def generate_azure_openai_batch_embeddings( @@ -629,42 +622,38 @@ def generate_azure_openai_batch_embeddings( version: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') - json_data = {'input': texts} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + json_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' + url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' - for _ in range(5): - headers = { - 'Content-Type': 'application/json', - 'api-key': key, - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + for _ in range(5): + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - r = requests.post( - url, - headers=headers, - json=json_data, - ) - if r.status_code == 429: - retry = float(r.headers.get('Retry-After', '1')) - time.sleep(retry) - continue - r.raise_for_status() - data = r.json() - if 'data' in data: - return [elem['embedding'] for elem in data['data']] - else: - raise Exception('Something went wrong :/') - return None - except Exception as e: - log.exception(f'Error generating azure openai batch embeddings: {e}') - return None + r = requests.post( + url, + headers=headers, + json=json_data, + ) + if r.status_code == 429: + retry = float(r.headers.get('Retry-After', '1')) + time.sleep(retry) + continue + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") + raise Exception('Azure OpenAI embedding request failed: max retries (429) exceeded') async def agenerate_azure_openai_batch_embeddings( @@ -675,40 +664,36 @@ async def agenerate_azure_openai_batch_embeddings( version: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'agenerate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') - form_data = {'input': texts} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + form_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - full_url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' + full_url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' - headers = { - 'Content-Type': 'application/json', - 'api-key': key, - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post( - full_url, - headers=headers, - json=form_data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() - data = await r.json() - if 'data' in data: - return [item['embedding'] for item in data['data']] - else: - raise Exception('Something went wrong :/') - except Exception as e: - log.exception(f'Error generating azure openai batch embeddings: {e}') - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + full_url, + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") def generate_ollama_batch_embeddings( @@ -718,35 +703,33 @@ def generate_ollama_batch_embeddings( key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') - json_data = {'input': texts, 'model': model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {key}', - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - r = requests.post( - f'{url}/api/embed', - headers=headers, - json=json_data, - ) - r.raise_for_status() - data = r.json() + r = requests.post( + f'{url}/api/embed', + headers=headers, + json=json_data, + ) + if r.status_code != 200: + error_detail = r.json().get('error', r.text) + raise Exception(f'Ollama embed error ({r.status_code}): {error_detail}') + data = r.json() - if 'embeddings' in data: - return data['embeddings'] - else: - raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") - except Exception as e: - log.exception(f'Error generating ollama batch embeddings: {e}') - return None + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") async def agenerate_ollama_batch_embeddings( @@ -756,38 +739,37 @@ async def agenerate_ollama_batch_embeddings( key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug(f'agenerate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') - form_data = {'input': texts, 'model': model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {key}', - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post( - f'{url}/api/embed', - headers=headers, - json=form_data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() - data = await r.json() - if 'embeddings' in data: - return data['embeddings'] - else: - raise Exception('Something went wrong :/') - except Exception as e: - log.exception(f'Error generating ollama batch embeddings: {e}') - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/api/embed', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status != 200: + error_data = await r.json() + error_detail = error_data.get('error', str(error_data)) + raise Exception(f'Ollama embed error ({r.status}): {error_detail}') + data = await r.json() + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") def get_embedding_function( @@ -855,11 +837,12 @@ def get_embedding_function( for batch in batches: batch_results.append(await embedding_function(batch, prefix=prefix, user=user)) - # Flatten results + # Flatten results — raise if any batch failed embeddings = [] - for batch_embeddings in batch_results: - if isinstance(batch_embeddings, list): - embeddings.extend(batch_embeddings) + for i, batch_embeddings in enumerate(batch_results): + if batch_embeddings is None: + raise Exception(f'Embedding generation failed for batch {i + 1}/{len(batches)}') + embeddings.extend(batch_embeddings) log.debug( f'generate_multiple_async: Generated {len(embeddings)} embeddings from {len(batches)} parallel batches' @@ -901,11 +884,15 @@ async def generate_embeddings( 'user': user, } ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings elif engine == 'openai': embeddings = await agenerate_openai_batch_embeddings( model, text if isinstance(text, list) else [text], url, key, prefix, user ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings elif engine == 'azure_openai': azure_api_version = kwargs.get('azure_api_version', '') @@ -918,6 +905,8 @@ async def generate_embeddings( prefix, user, ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings @@ -1054,7 +1043,11 @@ async def get_sources_from_items( } elif item.get('id'): file_object = Files.get_file_by_id(item.get('id')) - if file_object: + if file_object and ( + user.role == 'admin' + or file_object.user_id == user.id + or has_access_to_file(item.get('id'), 'read', user) + ): query_result = { 'documents': [[file_object.data.get('content', '')]], 'metadatas': [ diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 367ea4478c..88f0fe69fb 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -1155,8 +1155,9 @@ async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=D # create api key @router.post('/api_key', response_model=ApiKey) async def generate_api_key(request: Request, user=Depends(get_current_user), db: Session = Depends(get_session)): - if not request.app.state.config.ENABLE_API_KEYS or not has_permission( - user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS + if not request.app.state.config.ENABLE_API_KEYS or ( + user.role != 'admin' + and not has_permission(user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS) ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 0bfcf76d9d..041c4ad935 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -24,6 +24,7 @@ from open_webui.models.oauth_sessions import OAuthSessions from open_webui.utils.oauth import ( get_discovery_urls, get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, encrypt_data, decrypt_data, OAuthClientInformationFull, @@ -99,6 +100,7 @@ class OAuthClientRegistrationForm(BaseModel): url: str client_id: str client_name: Optional[str] = None + client_secret: Optional[str] = None @router.post('/oauth/clients/register') @@ -113,9 +115,19 @@ async def register_oauth_client( if type: oauth_client_id = f'{type}:{form_data.client_id}' - oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( - request, oauth_client_id, form_data.url - ) + if form_data.client_secret: + # Static credentials: skip dynamic registration, build from provided credentials + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + oauth_client_id, + form_data.url, + oauth_client_id=form_data.client_id, + oauth_client_secret=form_data.client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, oauth_client_id, form_data.url + ) return { 'status': True, 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), @@ -166,7 +178,7 @@ async def set_tool_servers_config( server_type = connection.get('type', 'openapi') auth_type = connection.get('auth_type', 'none') - if auth_type == 'oauth_2.1': + if auth_type in ('oauth_2.1', 'oauth_2.1_static'): # Remove existing OAuth clients for tool servers server_id = connection.get('info', {}).get('id') client_key = f'{server_type}:{server_id}' @@ -189,7 +201,7 @@ async def set_tool_servers_config( server_id = connection.get('info', {}).get('id') auth_type = connection.get('auth_type', 'none') - if auth_type == 'oauth_2.1' and server_id: + if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id: try: oauth_client_info = connection.get('info', {}).get('oauth_client_info', '') oauth_client_info = decrypt_data(oauth_client_info) @@ -257,6 +269,92 @@ async def set_terminal_servers_config( } +@router.post('/terminal_servers/verify') +async def verify_terminal_server_connection( + request: Request, form_data: TerminalServerConnection, user=Depends(get_admin_user) +): + """ + Verify the connection to a terminal server by detecting its type. + + Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config + (plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + # Orchestrators expose a policies API; plain terminals don't. + try: + async with session.get(f'{base_url}/api/v1/policies', headers=headers) as resp: + if resp.ok: + return {'status': True, 'type': 'orchestrator'} + except Exception: + pass + + # Fall back to open-terminal config endpoint. + try: + async with session.get(f'{base_url}/api/config', headers=headers) as resp: + if resp.ok: + return {'status': True, 'type': 'terminal'} + except Exception: + pass + + except Exception as e: + log.debug(f'Failed to connect to the terminal server: {e}') + + raise HTTPException(status_code=400, detail='Failed to connect to the terminal server') + + +class TerminalServerPolicyForm(BaseModel): + url: str + key: Optional[str] = '' + auth_type: Optional[str] = 'bearer' + policy_id: str + policy_data: dict + + +@router.post('/terminal_servers/policy') +async def put_terminal_server_policy( + request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user) +): + """ + Proxy a policy PUT to an orchestrator terminal server. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {'Content-Type': 'application/json'} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + policy_url = f'{base_url}/api/v1/policies/{form_data.policy_id}' + async with session.put(policy_url, headers=headers, json=form_data.policy_data) as resp: + if resp.ok: + return await resp.json() + detail = await resp.text() + raise HTTPException(status_code=resp.status, detail=detail) + except HTTPException: + raise + except Exception as e: + log.debug(f'Failed to save policy to terminal server: {e}') + raise HTTPException(status_code=400, detail='Failed to save policy to terminal server') + + @router.post('/tool_servers/verify') async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): """ @@ -264,7 +362,7 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn """ try: if form_data.type == 'mcp': - if form_data.auth_type == 'oauth_2.1': + if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'): discovery_urls = await get_discovery_urls(form_data.url) for discovery_url in discovery_urls: log.debug(f'Trying to fetch OAuth 2.1 discovery document from {discovery_url}') @@ -477,6 +575,13 @@ class ModelsConfigForm(BaseModel): DEFAULT_MODEL_PARAMS: Optional[dict] = None +@router.get('/models/defaults') +async def get_models_defaults(request: Request, user=Depends(get_verified_user)): + return { + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + } + + @router.get('/models', response_model=ModelsConfigForm) async def get_models_config(request: Request, user=Depends(get_admin_user)): return { diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 21b80e36c2..5a56e11b68 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -151,10 +151,14 @@ async def get_model_tags(user=Depends(get_verified_user), db: Session = Depends( if model.meta: meta = model.meta.model_dump() for tag in meta.get('tags', []): - tags_set.add((tag.get('name'))) + try: + name = tag.get('name') if isinstance(tag, dict) else str(tag) + if name: + tags_set.add(name) + except Exception: + continue - tags = [tag for tag in tags_set] - tags.sort() + tags = sorted(tags_set) return tags diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index e7d2b0593f..0e7c67c1f6 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -72,20 +72,31 @@ log = logging.getLogger(__name__) ########################################## -async def send_get_request(url, key=None, user: UserModel = None): +async def send_get_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, +): timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) try: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - headers = { - **({'Authorization': f'Bearer {key}'} if key else {}), - } + if request and config: + headers, cookies = await get_headers_and_cookies(request, url, key, config, user=user) + else: + headers = { + **({'Authorization': f'Bearer {key}'} if key else {}), + } + cookies = None - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) async with session.get( url, headers=headers, + cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: return await response.json() @@ -95,10 +106,16 @@ async def send_get_request(url, key=None, user: UserModel = None): return None -async def get_models_request(url, key=None, user: UserModel = None): +async def get_models_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, +): if is_anthropic_url(url): return await get_anthropic_models(url, key, user=user) - return await send_get_request(f'{url}/models', key, user=user) + return await send_get_request(request, f'{url}/models', key, user=user, config=config) def openai_reasoning_model_handler(payload): @@ -360,7 +377,7 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: request_tasks = [] for idx, url in enumerate(api_base_urls): if (str(idx) not in api_configs) and (url not in api_configs): # Legacy support - request_tasks.append(get_models_request(url, api_keys[idx], user=user)) + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user)) else: api_config = api_configs.get( str(idx), @@ -372,7 +389,7 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: if enable: if len(model_ids) == 0: - request_tasks.append(get_models_request(url, api_keys[idx], user=user)) + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user, config=api_config)) else: model_list = { 'object': 'list', diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index ed721ce335..56923bc447 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -790,8 +790,17 @@ async def get_groups( startIndex = max(1, startIndex) count = max(0, min(100, count)) - # Get all groups - groups_list = Groups.get_all_groups(db=db) + # Get groups, applying filter if provided + if filter: + if 'displayName eq' in filter: + display_name = filter.split('"')[1] + group = Groups.get_group_by_name(display_name, db=db) + groups_list = [group] if group else [] + else: + # Unrecognized filter — fall back to all groups + groups_list = Groups.get_all_groups(db=db) + else: + groups_list = Groups.get_all_groups(db=db) # Apply pagination total = len(groups_list) diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index 7ea4150021..c79d8fe5d8 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -42,6 +42,12 @@ async def format_code(form_data: CodeForm, user=Depends(get_admin_user)): @router.post('/code/execute') async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_CODE_EXECUTION: + raise HTTPException( + status_code=403, + detail='Code execution is disabled', + ) + if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter': output = await execute_code_jupyter( request.app.state.config.CODE_EXECUTION_JUPYTER_URL, diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 280666ff07..34412d6041 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -143,8 +143,15 @@ def get_license_data(app, key): pn, pt = nt(pb) data = json.loads(aesgcm.decrypt(pn, pt, None).decode()) - if not data.get('exp') or data.get('exp') < datetime.now().date(): - return False + + exp = data.get('exp') + if exp: + if isinstance(exp, str): + from datetime import date + + exp = date.fromisoformat(exp) + if exp < datetime.now().date(): + return False data_handler(data) return True diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index fcd8151022..5ce6fffec6 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -203,27 +203,41 @@ async def generate_chat_completion( except Exception as e: raise e - if model.get('owned_by') == 'arena': + # Arena model — sub-model was already resolved by process_chat_payload. + # Inject selected_model_id into the response for the frontend. + metadata = form_data.get('metadata', {}) + selected_model_id = metadata.pop('selected_model_id', None) + # Also clear from request.state.metadata to prevent the merge at + # lines 177-179 from re-adding it on the recursive call. + if hasattr(request.state, 'metadata'): + request.state.metadata.pop('selected_model_id', None) + + # Fallback: if generate_chat_completion is called with an arena model + # from a path that did NOT go through process_chat_payload (e.g., + # background tasks for title/follow-up/tags generation), resolve now. + if not selected_model_id and model.get('owned_by') == 'arena': model_ids = model.get('info', {}).get('meta', {}).get('model_ids') filter_mode = model.get('info', {}).get('meta', {}).get('filter_mode') if model_ids and filter_mode == 'exclude': model_ids = [ - model['id'] - for model in list(request.app.state.MODELS.values()) - if model.get('owned_by') != 'arena' and model['id'] not in model_ids + available_model['id'] + for available_model in list(request.app.state.MODELS.values()) + if available_model.get('owned_by') != 'arena' and available_model['id'] not in model_ids ] - selected_model_id = None if isinstance(model_ids, list) and model_ids: selected_model_id = random.choice(model_ids) else: model_ids = [ - model['id'] for model in list(request.app.state.MODELS.values()) if model.get('owned_by') != 'arena' + available_model['id'] + for available_model in list(request.app.state.MODELS.values()) + if available_model.get('owned_by') != 'arena' ] selected_model_id = random.choice(model_ids) form_data['model'] = selected_model_id + if selected_model_id: if form_data.get('stream') == True: async def stream_wrapper(stream): diff --git a/backend/open_webui/utils/logger.py b/backend/open_webui/utils/logger.py index 5cc34fe923..49b7973c57 100644 --- a/backend/open_webui/utils/logger.py +++ b/backend/open_webui/utils/logger.py @@ -150,7 +150,7 @@ def start_logger(): """ logger.remove() - audit_filter = lambda record: (True if ENABLE_AUDIT_STDOUT else 'auditable' not in record['extra']) + audit_filter = lambda record: True if ENABLE_AUDIT_STDOUT else 'auditable' not in record['extra'] if LOG_FORMAT == 'json': logger.add( _json_sink, diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index f6582bf239..b64febd673 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -92,6 +92,7 @@ from open_webui.utils.misc import ( get_last_user_message_item, get_last_assistant_message, get_system_message, + merge_system_messages, replace_system_message_content, prepend_to_first_user_message_content, convert_logit_bias_input_to_json, @@ -1457,7 +1458,7 @@ async def chat_web_search_handler(request: Request, form_data: dict, extra_param response = res['choices'][0]['message']['content'] try: - bracket_start = response.find('{') + bracket_start = response.rfind('{') bracket_end = response.rfind('}') + 1 if bracket_start == -1 or bracket_end == -1: @@ -1644,7 +1645,16 @@ def add_file_context(messages: list, chat_id: str, user) -> list: attrs += f' name="{file["name"]}"' return f'' - for message, stored_message in zip(messages, stored_messages): + # Pair only user-role messages from both lists to avoid misalignment. + # After process_messages_with_output(), assistant messages with tool calls + # are expanded into multiple messages (assistant + tool results), making + # the payload message list longer than the stored message list. A naive + # positional zip() would pair user messages with wrong stored messages, + # causing later images to lose their file context (see #21878). + user_messages = [m for m in messages if m.get('role') == 'user'] + stored_user_messages = [m for m in stored_messages if m.get('role') == 'user'] + + for message, stored_message in zip(user_messages, stored_user_messages): files_with_urls = [ file for file in stored_message.get('files', []) @@ -1779,7 +1789,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra response = res['choices'][0]['message']['content'] try: - bracket_start = response.find('{') + bracket_start = response.rfind('{') bracket_end = response.rfind('}') + 1 if bracket_start == -1 or bracket_end == -1: @@ -1883,7 +1893,7 @@ async def chat_completion_files_handler( queries_response = queries_response['choices'][0]['message']['content'] try: - bracket_start = queries_response.find('{') + bracket_start = queries_response.rfind('{') bracket_end = queries_response.rfind('}') + 1 if bracket_start == -1 or bracket_end == -1: @@ -2101,6 +2111,35 @@ async def process_chat_payload(request, form_data, user, metadata, model): # -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling # -> Chat Files + # Arena model resolution — pick the sub-model now so all downstream + # processing (knowledge, capabilities, tools, params) uses its settings + # instead of the empty arena wrapper. + if model.get('owned_by') == 'arena': + arena_model_ids = model.get('info', {}).get('meta', {}).get('model_ids') + arena_filter_mode = model.get('info', {}).get('meta', {}).get('filter_mode') + if arena_model_ids and arena_filter_mode == 'exclude': + arena_model_ids = [ + available_model['id'] + for available_model in request.app.state.MODELS.values() + if available_model.get('owned_by') != 'arena' and available_model['id'] not in arena_model_ids + ] + + if isinstance(arena_model_ids, list) and arena_model_ids: + selected_model_id = random.choice(arena_model_ids) + else: + arena_model_ids = [ + available_model['id'] + for available_model in request.app.state.MODELS.values() + if available_model.get('owned_by') != 'arena' + ] + selected_model_id = random.choice(arena_model_ids) + + selected_model = request.app.state.MODELS.get(selected_model_id) + if selected_model: + model = selected_model + form_data['model'] = selected_model_id + metadata['selected_model_id'] = selected_model_id + form_data = apply_params_to_form_data(form_data, model) log.debug(f'form_data: {form_data}') @@ -2568,12 +2607,17 @@ async def process_chat_payload(request, form_data, user, metadata, model): # so system terminals work even when no other tools are selected) if terminal_id: try: - terminal_tools, system_prompt = await get_terminal_tools( + terminal_result = await get_terminal_tools( request, terminal_id, user, extra_params, ) + if isinstance(terminal_result, tuple): + terminal_tools, system_prompt = terminal_result + else: + terminal_tools = terminal_result + system_prompt = None if terminal_tools: tools_dict = {**tools_dict, **terminal_tools} if system_prompt: @@ -2696,6 +2740,10 @@ async def process_chat_payload(request, form_data, user, metadata, model): # to prevent errors from providers like Gemini and Claude form_data['messages'] = strip_empty_content_blocks(form_data.get('messages', [])) + # Merge any duplicate system messages into a single message at position 0 + # to prevent template parsing errors with strict chat templates (e.g. Qwen) + form_data['messages'] = merge_system_messages(form_data.get('messages', [])) + return form_data, metadata, events @@ -3658,9 +3706,9 @@ async def streaming_chat_response_handler(response, ctx): current_response_tool_call['function']['name'] = delta_name if delta_arguments: - current_response_tool_call['function'][ - 'arguments' - ] += delta_arguments + current_response_tool_call['function']['arguments'] += ( + delta_arguments + ) # Emit pending tool calls in real-time if response_tool_calls: @@ -3746,7 +3794,7 @@ async def streaming_chat_response_handler(response, ctx): } ] - data = {'content': serialize_output(output)} + data = {'content': serialize_output(full_output())} if value: if ( diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index e6b686071d..bfa934c8b5 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -330,6 +330,33 @@ def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict] return get_system_message(messages), remove_system_message(messages) +def merge_system_messages(messages: list[dict]) -> list[dict]: + """ + Merge all system messages into one at position 0. + + Some chat templates (e.g. Qwen) require exactly one system + message at the start. Multiple pipeline stages may each + insert their own system message; this function consolidates + them. + """ + system_contents: list[str] = [] + other_messages: list[dict] = [] + + for message in messages: + if message.get('role') == 'system': + content = get_content_from_message(message) + if content: + system_contents.append(content) + else: + other_messages.append(message) + + if not system_contents: + return other_messages + + merged = {'role': 'system', 'content': '\n'.join(system_contents)} + return [merged, *other_messages] + + def update_message_content(message: dict, content: str, append: bool = True) -> dict: if isinstance(message['content'], list): for item in message['content']: diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 60ef87e5f5..b57c74744c 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -452,6 +452,10 @@ def get_filtered_models(models, user, db=None): or model['id'] in accessible_model_ids ): filtered_models.append(model) + elif user.role == 'admin': + # No DB entry means no access control configured yet; + # only admins can see unconfigured models. + filtered_models.append(model) return filtered_models else: diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 1a4ed3871c..e4a97327a2 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -442,6 +442,74 @@ async def get_oauth_client_info_with_dynamic_client_registration( raise e +async def get_oauth_client_info_with_static_credentials( + request, + client_id: str, + oauth_server_url: str, + oauth_client_id: str, + oauth_client_secret: str, +) -> OAuthClientInformationFull: + """ + Build an OAuthClientInformationFull from user-provided static credentials. + Performs server metadata discovery to resolve authorization/token endpoints, + but skips dynamic client registration entirely. + """ + try: + oauth_server_metadata = None + oauth_server_metadata_url = None + + redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/') + redirect_uri = f'{redirect_base_url}/oauth/clients/{client_id}/callback' + + # Discover server metadata (authorization endpoint, token endpoint, scopes, etc.) + discovery_urls = await get_discovery_urls(oauth_server_url) + for url in discovery_urls: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp: + if resp.status == 200: + try: + oauth_server_metadata = OAuthMetadata.model_validate(await resp.json()) + oauth_server_metadata_url = url + break + except Exception as e: + log.error(f'Error parsing OAuth metadata from {url}: {e}') + continue + + # Determine scope from server metadata if available + scope = None + if oauth_server_metadata and oauth_server_metadata.scopes_supported: + scope = ' '.join(oauth_server_metadata.scopes_supported) + + # Determine token_endpoint_auth_method + token_endpoint_auth_method = 'client_secret_post' + if ( + oauth_server_metadata + and oauth_server_metadata.token_endpoint_auth_methods_supported + and token_endpoint_auth_method not in oauth_server_metadata.token_endpoint_auth_methods_supported + ): + token_endpoint_auth_method = oauth_server_metadata.token_endpoint_auth_methods_supported[0] + + oauth_client_info = OAuthClientInformationFull( + client_id=oauth_client_id, + client_secret=oauth_client_secret, + redirect_uris=[redirect_uri], + grant_types=['authorization_code', 'refresh_token'], + response_types=['code'], + scope=scope, + token_endpoint_auth_method=token_endpoint_auth_method, + issuer=oauth_server_metadata_url, + server_metadata=oauth_server_metadata, + ) + + log.info( + f'Static OAuth client info built for {oauth_client_id} using metadata from {oauth_server_metadata_url}' + ) + return oauth_client_info + except Exception as e: + log.error(f'Exception building static OAuth client info: {e}') + raise e + + class OAuthClientManager: def __init__(self, app): self.oauth = OAuth() @@ -496,7 +564,7 @@ class OAuthClientManager: for connection in connections or []: if connection.get('type', 'openapi') != 'mcp': continue - if connection.get('auth_type', 'none') != 'oauth_2.1': + if connection.get('auth_type', 'none') not in ('oauth_2.1', 'oauth_2.1_static'): continue server_id = connection.get('info', {}).get('id') @@ -1329,12 +1397,21 @@ class OAuthManager: # Try to get userinfo from the token first, some providers include it there user_data: UserInfo = token.get('userinfo') + # Preserve extra claims from the ID token (e.g. roles, groups for + # Microsoft Entra ID) before the userinfo endpoint possibly overwrites them. + id_token_claims = dict(user_data) if user_data else {} if ( (not user_data) or (auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data) or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data) ): user_data: UserInfo = await client.userinfo(token=token) + # Merge back ID token claims that the userinfo endpoint doesn't + # return. Only backfill missing keys so userinfo always wins. + if user_data and id_token_claims: + for key, value in id_token_claims.items(): + if key not in user_data: + user_data[key] = value if provider == 'feishu' and isinstance(user_data, dict) and 'data' in user_data: user_data = user_data['data'] if not user_data: diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 86d086efd3..226830a1fa 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -439,7 +439,7 @@ def get_builtin_tools( builtin_functions.extend([search_chats, view_chat]) # Add memory tools if builtin category enabled AND enabled for this chat - if is_builtin_tool_enabled('memory') and features.get('memory'): + if is_builtin_tool_enabled('memory') and (features.get('memory') or get_model_capability('memory', False)): builtin_functions.extend( [ search_memories, @@ -956,7 +956,7 @@ async def get_terminal_tools( terminal_id: str, user: UserModel, extra_params: dict, -) -> tuple[dict[str, dict], Optional[str]]: +) -> dict[str, dict] | tuple[dict[str, dict], Optional[str]]: """Resolve tools for a terminal server identified by terminal_id. - Finds the connection in TERMINAL_SERVER_CONNECTIONS diff --git a/backend/requirements.txt b/backend/requirements.txt index 103a3bd868..a9275beaf3 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,7 +6,7 @@ itsdangerous==2.2.0 python-socketio==5.16.1 python-jose==3.5.0 -cryptography +cryptography==46.0.5 bcrypt==5.0.0 argon2-cffi==25.1.0 PyJWT[crypto]==2.11.0 @@ -14,9 +14,9 @@ authlib==1.6.9 requests==2.32.5 aiohttp==3.13.2 # do not update to 3.13.3 - broken -async-timeout -aiocache -aiofiles +async-timeout==5.0.1 +aiocache==0.12.3 +aiofiles==25.1.0 starlette-compress==1.7.0 Brotli==1.1.0 httpx[socks,http2,zstd,cli,brotli]==0.28.1 @@ -29,7 +29,7 @@ peewee==3.19.0 peewee-migrate==1.14.3 pycrdt==0.12.47 -redis +redis==7.4.0 APScheduler==3.11.2 RestrictedPython==8.1 @@ -39,11 +39,11 @@ loguru==0.7.3 asgiref==3.11.1 # AI libraries -tiktoken +tiktoken==0.12.0 mcp==1.26.0 -openai -anthropic +openai==2.29.0 +anthropic==0.86.0 google-genai==1.66.0 langchain==1.2.10 @@ -58,7 +58,7 @@ opensearch-py==3.1.0 transformers==5.3.0 sentence-transformers==5.2.3 -accelerate +accelerate==1.13.0 pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897 einops==0.8.2 @@ -74,15 +74,15 @@ unstructured==0.18.31 nltk==3.9.3 Markdown==3.10.2 -beautifulsoup4 +beautifulsoup4==4.14.3 pypandoc==1.16.2 pandas==3.0.1 openpyxl==3.1.5 pyxlsb==1.0.10 xlrd==2.0.2 validators==0.35.0 -psutil -sentencepiece +psutil==7.2.2 +sentencepiece==0.2.1 soundfile==0.13.1 pillow==12.1.1 @@ -97,7 +97,7 @@ black==26.1.0 youtube-transcript-api==1.2.4 pytube==15.0.0 -pydub +pydub==0.25.1 ddgs==9.11.3 azure-ai-documentintelligence==1.0.2 @@ -106,15 +106,15 @@ azure-storage-blob==12.28.0 azure-search-documents==11.6.0 ## Google Drive -google-api-python-client -google-auth-httplib2 -google-auth-oauthlib +google-api-python-client==2.193.0 +google-auth-httplib2==0.3.0 +google-auth-oauthlib==1.3.0 googleapis-common-protos==1.72.0 google-cloud-storage==3.9.0 ## Databases -pymongo +pymongo==4.16.0 psycopg2-binary==2.9.11 pgvector==0.4.2 diff --git a/backend/start.sh b/backend/start.sh index 31e87c9557..00d02f326b 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -50,7 +50,7 @@ if [ -n "$SPACE_ID" ]; then echo "Configuring for HuggingFace Space deployment" if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then echo "Admin user configured, creating" - WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' & + WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" & webui_pid=$! echo "Waiting for webui to start..." while ! curl -s "http://localhost:${PORT}/health" > /dev/null; do @@ -83,5 +83,5 @@ fi WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app \ --host "$HOST" \ --port "$PORT" \ - --forwarded-allow-ips '*' \ + --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \ "${ARGS[@]}" \ No newline at end of file diff --git a/backend/start_windows.bat b/backend/start_windows.bat index f350d11cd1..c8587c3c6d 100644 --- a/backend/start_windows.bat +++ b/backend/start_windows.bat @@ -24,6 +24,7 @@ IF NOT "%WEBUI_SECRET_KEY_FILE%" == "" ( IF "%PORT%"=="" SET PORT=8080 IF "%HOST%"=="" SET HOST=0.0.0.0 +IF "%FORWARDED_ALLOW_IPS%"=="" SET "FORWARDED_ALLOW_IPS=*" SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%" @@ -46,5 +47,5 @@ IF "%WEBUI_SECRET_KEY% %WEBUI_JWT_SECRET_KEY%" == " " ( :: Execute uvicorn SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" IF "%UVICORN_WORKERS%"=="" SET UVICORN_WORKERS=1 -uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --workers %UVICORN_WORKERS% --ws auto +uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips "%FORWARDED_ALLOW_IPS%" --workers %UVICORN_WORKERS% --ws auto :: For ssl user uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ssl-keyfile "key.pem" --ssl-certfile "cert.pem" --ws auto diff --git a/package-lock.json b/package-lock.json index 54458e1c29..538792470e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.8.10", + "version": "0.8.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.8.10", + "version": "0.8.12", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", @@ -8664,9 +8664,9 @@ "license": "Apache-2.0" }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -10035,9 +10035,9 @@ } }, "node_modules/jspdf": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz", - "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -13614,9 +13614,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", diff --git a/package.json b/package.json index 887781d325..ab0cf78afc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.8.10", + "version": "0.8.12", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 65a2366230..7a546e935f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "python-socketio==5.16.1", "python-jose==3.5.0", - "cryptography", + "cryptography==46.0.5", "bcrypt==5.0.0", "argon2-cffi==25.1.0", "PyJWT[crypto]==2.11.0", @@ -22,9 +22,9 @@ dependencies = [ "requests==2.32.5", "aiohttp==3.13.2", # do not update to 3.13.3 - broken - "async-timeout", - "aiocache", - "aiofiles", + "async-timeout==5.0.1", + "aiocache==0.12.3", + "aiofiles==25.1.0", "starlette-compress==1.7.0", "Brotli==1.1.0", "httpx[socks,http2,zstd,cli,brotli]==0.28.1", @@ -37,7 +37,7 @@ dependencies = [ "peewee-migrate==1.14.3", "pycrdt==0.12.47", - "redis", + "redis==7.4.0", "pytz==2026.1.post1", "APScheduler==3.11.2", @@ -46,11 +46,11 @@ dependencies = [ "loguru==0.7.3", "asgiref==3.11.1", - "tiktoken", + "tiktoken==0.12.0", "mcp==1.26.0", - "openai", - "anthropic", + "openai==2.29.0", + "anthropic==0.86.0", "google-genai==1.66.0", "langchain==1.2.10", @@ -66,7 +66,7 @@ dependencies = [ "transformers==5.3.0", "sentence-transformers==5.2.3", - "accelerate", + "accelerate==1.13.0", "pyarrow==20.0.0", # fix: pin pyarrow version to 20 for rpi compatibility #15897 "einops==0.8.2", @@ -80,14 +80,15 @@ dependencies = [ "msoffcrypto-tool==6.0.0", "nltk==3.9.3", "Markdown==3.10.2", + "beautifulsoup4==4.14.3", "pypandoc==1.16.2", "pandas==3.0.1", "openpyxl==3.1.5", "pyxlsb==1.0.10", "xlrd==2.0.2", "validators==0.35.0", - "psutil", - "sentencepiece", + "psutil==7.2.2", + "sentencepiece==0.2.1", "soundfile==0.13.1", "azure-ai-documentintelligence==1.0.2", @@ -103,12 +104,12 @@ dependencies = [ "youtube-transcript-api==1.2.4", "pytube==15.0.0", - "pydub", + "pydub==0.25.1", "ddgs==9.11.3", - "google-api-python-client", - "google-auth-httplib2", - "google-auth-oauthlib", + "google-api-python-client==2.193.0", + "google-auth-httplib2==0.3.0", + "google-auth-oauthlib==1.3.0", "googleapis-common-protos==1.72.0", "google-cloud-storage==3.9.0", @@ -144,7 +145,7 @@ unstructured = [ ] all = [ - "pymongo", + "pymongo==4.16.0", "psycopg2-binary==2.9.11", "pgvector==0.4.2", "moto[s3]>=5.0.26", diff --git a/scripts/prepare-pyodide.js b/scripts/prepare-pyodide.js index a1b027d157..d83598343e 100644 --- a/scripts/prepare-pyodide.js +++ b/scripts/prepare-pyodide.js @@ -18,9 +18,15 @@ const packages = [ 'openpyxl' ]; +// Pure-Python packages whose wheels must be downloaded from PyPI and saved into +// static/pyodide/ so that the browser can install them offline via micropip. +// Packages already provided by the Pyodide distribution (click, platformdirs, +// typing_extensions, etc.) do NOT need to be listed here. +const pypiPackages = ['black', 'pathspec', 'mypy_extensions']; + import { loadPyodide } from 'pyodide'; import { setGlobalDispatcher, ProxyAgent } from 'undici'; -import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises'; +import { writeFile, readFile, copyFile, readdir, rmdir, access } from 'fs/promises'; /** * Loading network proxy configurations from the environment variables. @@ -118,6 +124,78 @@ async function copyPyodide() { } } +/** + * Download pure-Python wheels from PyPI and save them into static/pyodide/. + * Also injects entries into pyodide-lock.json so that micropip resolves these + * packages from the local server instead of fetching them from the internet. + */ +async function downloadPyPIWheels() { + const lockPath = 'static/pyodide/pyodide-lock.json'; + let lockData; + try { + lockData = JSON.parse(await readFile(lockPath, 'utf-8')); + } catch { + console.warn('Could not read pyodide-lock.json, skipping PyPI wheel download'); + return; + } + + for (const pkg of pypiPackages) { + console.log(`Fetching PyPI metadata for: ${pkg}`); + const res = await fetch(`https://pypi.org/pypi/${pkg}/json`); + if (!res.ok) { + console.error(`Failed to fetch PyPI metadata for ${pkg}: ${res.status}`); + continue; + } + const meta = await res.json(); + const version = meta.info.version; + const files = meta.urls || []; + // Find the pure-Python wheel (py3-none-any) + const wheel = files.find( + (f) => f.filename.endsWith('.whl') && f.filename.includes('py3-none-any') + ); + if (!wheel) { + console.warn(`No pure-Python wheel found for ${pkg}==${version}, skipping`); + continue; + } + const dest = `static/pyodide/${wheel.filename}`; + // Download wheel if not already present + try { + await access(dest); + console.log(` Already exists: ${wheel.filename}`); + } catch { + console.log(` Downloading: ${wheel.filename}`); + const wheelRes = await fetch(wheel.url); + if (!wheelRes.ok) { + console.error(` Failed to download ${wheel.filename}: ${wheelRes.status}`); + continue; + } + const buffer = Buffer.from(await wheelRes.arrayBuffer()); + await writeFile(dest, buffer); + console.log(` Saved: ${dest} (${buffer.length} bytes)`); + } + + // Inject into pyodide-lock.json so micropip resolves locally + const normalizedName = pkg.replace(/-/g, '_'); + if (!lockData.packages[normalizedName]) { + lockData.packages[normalizedName] = { + name: normalizedName, + version: version, + file_name: wheel.filename, + install_dir: 'site', + sha256: wheel.digests?.sha256 || '', + package_type: 'package', + imports: [normalizedName], + depends: [] + }; + console.log(` Added ${normalizedName}==${version} to pyodide-lock.json`); + } + } + + await writeFile(lockPath, JSON.stringify(lockData, null, 2)); + console.log('Updated pyodide-lock.json with PyPI packages'); +} + initNetworkProxyFromEnv(); await downloadPackages(); await copyPyodide(); +await downloadPyPIWheels(); diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 51236ff528..6b7bf6f47b 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -268,9 +268,10 @@ export const detectTerminalServerType = async ( /** * Create or update a policy on the orchestrator. - * PUT {url}/api/v1/policies/{policyId} + * Proxied through the Open WebUI backend to keep API keys server-side. */ export const putOrchestratorPolicy = async ( + token: string, url: string, key: string, policyId: string, @@ -278,18 +279,52 @@ export const putOrchestratorPolicy = async ( ): Promise => { let error = null; - const baseUrl = url.replace(/\/$/, ''); - const headers: Record = { - 'Content-Type': 'application/json' - }; - if (key) { - headers['Authorization'] = `Bearer ${key}`; + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/policy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url.replace(/\/$/, ''), + key, + policy_id: policyId, + policy_data: policyData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; } - const res = await fetch(`${baseUrl}/api/v1/policies/${encodeURIComponent(policyId)}`, { - method: 'PUT', - headers, - body: JSON.stringify(policyData) + return res; +}; + +/** + * Verify a terminal server connection via the backend proxy. + * Used for system/admin connections to avoid CORS issues and API key exposure. + */ +export const verifyTerminalServerConnection = async (token: string, connection: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connection + }) }) .then(async (res) => { if (!res.ok) throw await res.json(); @@ -342,6 +377,7 @@ type RegisterOAuthClientForm = { url: string; client_id: string; client_name?: string; + client_secret?: string; }; export const registerOAuthClient = async ( @@ -441,6 +477,33 @@ export const setCodeExecutionConfig = async (token: string, config: object) => { return res; }; +export const getModelsDefaults = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models/defaults`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getModelsConfig = async (token: string) => { let error = null; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 71af8f793c..b07d524cba 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -2,6 +2,8 @@ import { WEBUI_BASE_URL } from '$lib/constants'; import { convertOpenApiToToolPayload } from '$lib/utils'; import { getOpenAIModelsDirect } from './openai'; +const TOOL_SERVER_FETCH_TIMEOUT = 10000; + // Every request sent from here is a petition. May it reach // the one for whom it was intended, and return answered. export const getModels = async ( @@ -304,6 +306,7 @@ export const getToolServerData = async (token: string, url: string) => { let error = null; const res = await fetch(`${url}`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT), method: 'GET', headers: { Accept: 'application/json', @@ -324,7 +327,9 @@ export const getToolServerData = async (token: string, url: string) => { }) .catch((err) => { console.error(err); - if ('detail' in err) { + if (err?.name === 'TimeoutError') { + error = `Connection to ${url} timed out`; + } else if ('detail' in err) { error = err.detail; } else { error = err; @@ -404,7 +409,9 @@ export const getToolServersData = async (servers: object[]) => { // Fetch system prompt if the server supports it try { const baseUrl = (server?.url ?? '').replace(/\/$/, ''); - const configRes = await fetch(`${baseUrl}/api/config`); + const configRes = await fetch(`${baseUrl}/api/config`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT) + }); if (configRes.ok) { const config = await configRes.json(); if (config?.features?.system) { @@ -412,7 +419,10 @@ export const getToolServersData = async (servers: object[]) => { if (toolServerToken) { headers['Authorization'] = `Bearer ${toolServerToken}`; } - const systemRes = await fetch(`${baseUrl}/system`, { headers }); + const systemRes = await fetch(`${baseUrl}/system`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT), + headers + }); if (systemRes.ok) { const systemData = await systemRes.json(); if (systemData?.prompt) { diff --git a/src/lib/components/AddTerminalServerModal.svelte b/src/lib/components/AddTerminalServerModal.svelte index b70a8b8233..e7c0236eb6 100644 --- a/src/lib/components/AddTerminalServerModal.svelte +++ b/src/lib/components/AddTerminalServerModal.svelte @@ -12,12 +12,16 @@ import LockClosed from '$lib/components/icons/LockClosed.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; - import { detectTerminalServerType, putOrchestratorPolicy } from '$lib/apis/configs'; + import { + detectTerminalServerType, + verifyTerminalServerConnection, + putOrchestratorPolicy + } from '$lib/apis/configs'; import { getTerminalConfig } from '$lib/apis/terminal'; export let show = false; export let edit = false; - export let admin = false; + export let direct = false; export let connection = null; export let onSubmit: Function = () => {}; @@ -110,9 +114,14 @@ verifying = true; try { - if (admin) { - // Admin: detect orchestrator vs terminal - const type = await detectTerminalServerType(_url, key); + if (!direct) { + // System connection: proxy through backend to avoid CORS / key exposure + const result = await verifyTerminalServerConnection(localStorage.token, { + url: _url, + key, + auth_type + }); + const type = result?.type ?? null; if (type) { serverType = type; @@ -137,7 +146,7 @@ toast.error($i18n.t('Server connection failed')); } } else { - // Non-admin: simple terminal verification + // Direct connection: verify from browser const res = await getTerminalConfig(_url, key); if (res) { toast.success($i18n.t('Server connection verified')); @@ -192,9 +201,9 @@ url = url.replace(/\/$/, ''); // Save policy to orchestrator if applicable - if (serverType === 'orchestrator' && admin && policyId) { + if (serverType === 'orchestrator' && !direct && policyId) { try { - await putOrchestratorPolicy(url, key, policyId, buildPolicyData()); + await putOrchestratorPolicy(localStorage.token, url, key, policyId, buildPolicyData()); } catch (err) { toast.error($i18n.t('Failed to save policy: {{error}}', { error: err })); return; @@ -202,7 +211,7 @@ } const result = { - ...(admin && id.trim() ? { id: id.trim() } : {}), + ...(!direct && id.trim() ? { id: id.trim() } : {}), url, key, name, @@ -210,7 +219,7 @@ auth_type, enabled: enabled, config: { - ...(admin ? { access_grants: accessGrants } : {}) + ...(!direct ? { access_grants: accessGrants } : {}) }, // Policy fields ...(serverType ? { server_type: serverType } : {}), @@ -270,7 +279,7 @@ /> - {#if admin} + {#if !direct}
@@ -1834,7 +1858,7 @@ + + {#if open} +
+
+ +
+
+ {/if} + + {#if allEmbeds.length > 0} + {#each allEmbeds as embedItem, idx} +
+ +
+ {/each} + {/if} +
diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte index cac7cdde66..0bd15fc2e5 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte @@ -20,6 +20,7 @@ import ToolCallDisplay from '$lib/components/common/ToolCallDisplay.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; + import ConsecutiveDetailsGroup from './ConsecutiveDetailsGroup.svelte'; import HtmlToken from './HTMLToken.svelte'; import Clipboard from '$lib/components/icons/Clipboard.svelte'; @@ -52,6 +53,51 @@ return 'h' + depth; }; + const GROUPABLE_DETAIL_TYPES = new Set(['tool_calls', 'reasoning', 'code_interpreter']); + + const isGroupableDetailToken = (token: Token & { attributes?: { type?: string } }) => { + return token?.type === 'details' && GROUPABLE_DETAIL_TYPES.has(token?.attributes?.type ?? ''); + }; + + const getDisplayTokens = (tokenList: Token[] = []) => { + const displayTokens = []; + let detailGroup = []; + + const flushDetailGroup = () => { + if (detailGroup.length > 1) { + displayTokens.push({ + type: 'detail_group', + items: [...detailGroup] + }); + } else if (detailGroup.length === 1) { + displayTokens.push(detailGroup[0]); + } + + detailGroup = []; + }; + + for (const token of tokenList) { + if (isGroupableDetailToken(token)) { + detailGroup.push(token); + } else { + flushDetailGroup(); + displayTokens.push(token); + } + } + + flushDetailGroup(); + + return displayTokens; + }; + + const getDetailTextContent = (token) => { + return decode(token?.text || '') + .replace(/.*?<\/summary>/gi, '') + .trim(); + }; + + $: displayTokens = getDisplayTokens(tokens); + const exportTableToCSVHandler = (token, tokenIdx = 0) => { console.log('Exporting table to CSV'); @@ -92,7 +138,7 @@ -{#each tokens as token, tokenIdx (tokenIdx)} +{#each displayTokens as token, tokenIdx (tokenIdx)} {#if token.type === 'hr'}
{:else if token.type === 'heading'} @@ -320,10 +366,62 @@ {/each} {/if} + {:else if token.type === 'detail_group'} + +
+ {#each token.items as detailToken, detailIdx} + {@const textContent = getDetailTextContent(detailToken)} + + {#if detailToken?.attributes?.type === 'tool_calls'} + + {:else if textContent.length > 0} + +
+ +
+
+ {:else} + + {/if} + {/each} +
+
{:else if token.type === 'details'} - {@const textContent = decode(token.text || '') - .replace(/.*?<\/summary>/gi, '') - .trim()} + {@const textContent = getDetailTextContent(token)} {#if token?.attributes?.type === 'tool_calls'} @@ -338,6 +436,7 @@ title={token.summary} open={$settings?.expandDetails ?? false} attributes={token?.attributes} + messageDone={done} className="w-full space-y-1" dir="auto" > @@ -360,6 +459,7 @@ open={false} disabled={true} attributes={token?.attributes} + messageDone={done} className="w-full space-y-1" dir="auto" /> diff --git a/src/lib/components/chat/Placeholder.svelte b/src/lib/components/chat/Placeholder.svelte index b7236dc60e..c9816b7fb1 100644 --- a/src/lib/components/chat/Placeholder.svelte +++ b/src/lib/components/chat/Placeholder.svelte @@ -47,6 +47,7 @@ export let selectedToolIds = []; export let selectedFilterIds = []; + export let pendingOAuthTools = []; export let showCommands = false; @@ -218,6 +219,7 @@ bind:atSelectedModel bind:showCommands bind:dragged + {pendingOAuthTools} {toolServers} {stopResponse} {createMessagePair} diff --git a/src/lib/components/chat/Settings/DataControls.svelte b/src/lib/components/chat/Settings/DataControls.svelte index e20aa72df2..2507191bd4 100644 --- a/src/lib/components/chat/Settings/DataControls.svelte +++ b/src/lib/components/chat/Settings/DataControls.svelte @@ -3,6 +3,7 @@ const { saveAs } = fileSaver; import { + chatId, chats, user, settings, @@ -139,7 +140,16 @@ }; - + { + if ($chatId === id) { + goto('/'); + chatId.set(''); + } + }} +/> diff --git a/src/lib/components/chat/Settings/Integrations/Terminals.svelte b/src/lib/components/chat/Settings/Integrations/Terminals.svelte index 8c637d8b2a..423fc79561 100644 --- a/src/lib/components/chat/Settings/Integrations/Terminals.svelte +++ b/src/lib/components/chat/Settings/Integrations/Terminals.svelte @@ -38,7 +38,7 @@ }; - addServer(server)} /> + addServer(server)} />
diff --git a/src/lib/components/chat/Settings/Integrations/Terminals/Connection.svelte b/src/lib/components/chat/Settings/Integrations/Terminals/Connection.svelte index 1be15783af..4f23bf94d5 100644 --- a/src/lib/components/chat/Settings/Integrations/Terminals/Connection.svelte +++ b/src/lib/components/chat/Settings/Integrations/Terminals/Connection.svelte @@ -20,6 +20,7 @@ {}; @@ -73,12 +74,13 @@ >
- {#if attributes?.done && attributes?.done !== 'true'} + {#if attributes?.done && attributes?.done !== 'true' && !messageDone}
@@ -86,7 +88,7 @@
{#if attributes?.type === 'reasoning'} - {#if attributes?.done === 'true' && attributes?.duration} + {#if (attributes?.done === 'true' || messageDone) && attributes?.duration} {#if attributes.duration < 1} {$i18n.t('Thought for less than a second')} {:else if attributes.duration < 60} @@ -98,11 +100,13 @@ DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() })} {/if} + {:else if attributes?.done === 'true' || messageDone} + {$i18n.t('Thought')} {:else} {$i18n.t('Thinking...')} {/if} {:else if attributes?.type === 'code_interpreter'} - {#if attributes?.done === 'true'} + {#if attributes?.done === 'true' || messageDone} {$i18n.t('Analyzed')} {:else} {$i18n.t('Analyzing...')} diff --git a/src/lib/components/common/ConfirmDialog.svelte b/src/lib/components/common/ConfirmDialog.svelte index 3814ab81a4..9a7462e5a7 100644 --- a/src/lib/components/common/ConfirmDialog.svelte +++ b/src/lib/components/common/ConfirmDialog.svelte @@ -25,6 +25,8 @@ export let inputValue = ''; export let inputType = ''; + let _inputValue = inputValue; + export let show = false; $: if (show) { @@ -37,7 +39,7 @@ let focusTrap: FocusTrap.FocusTrap | null = null; const init = () => { - inputValue = ''; + _inputValue = inputValue; }; const handleKeyDown = (event: KeyboardEvent) => { @@ -58,7 +60,7 @@ show = false; await tick(); await onConfirm(); - dispatch('confirm', inputValue); + dispatch('confirm', _inputValue); }; onMount(() => { @@ -141,13 +143,13 @@ placeholder={inputPlaceholder ? inputPlaceholder : $i18n.t('Enter your message')} - bind:value={inputValue} + bind:value={_inputValue} required={true} />
{:else}