diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 2a326f65e4..4f159f4faa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -8,8 +8,9 @@ body: value: | ## Important Notes ### Before submitting - Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted. + Please check the open AND closed [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. + If your feature request might impact others in the community, consider opening a discussion instead and evaluate whether and how to implement it. This will help us efficiently focus on improving the project. ### Collaborate respectfully @@ -35,7 +36,7 @@ body: label: Check Existing Issues description: Please confirm that you've checked for existing similar requests options: - - label: I have searched the existing issues and discussions. + - label: I have searched all existing open AND closed issues and discussions for similar requests. I have found none that is comparable to my request. required: true - type: textarea id: problem-description diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4938dfe1..38e3e2be4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ 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.6.34] - 2025-10-16 + +### Added + +- 📄 MinerU is now supported as a document parser backend, with support for both local and managed API deployments. [#18306](https://github.com/open-webui/open-webui/pull/18306) +- 🔒 JWT token expiration default is now set to 4 weeks instead of never expiring, with security warnings displayed in backend logs and admin UI when set to unlimited. [#18261](https://github.com/open-webui/open-webui/pull/18261), [#18262](https://github.com/open-webui/open-webui/pull/18262) +- ⚡ Page loading performance is improved by preventing unnecessary API requests when sidebar folders are not expanded. [#18179](https://github.com/open-webui/open-webui/pull/18179), [#17476](https://github.com/open-webui/open-webui/issues/17476) +- 📁 File hash values are now included in the knowledge endpoint response, enabling efficient file synchronization through hash comparison. [#18284](https://github.com/open-webui/open-webui/pull/18284), [#18283](https://github.com/open-webui/open-webui/issues/18283) +- 🎨 Chat dialog scrollbar visibility is improved by increasing its width, making it easier to use for navigation. [#18369](https://github.com/open-webui/open-webui/pull/18369), [#11782](https://github.com/open-webui/open-webui/issues/11782) +- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security. +- 🌐 Translations for Catalan, Chinese, Czech, Finnish, German, Kabyle, Korean, Portuguese (Brazil), Spanish, Thai, and Turkish were enhanced and expanded. + +### Fixed + +- 📚 Focused retrieval mode now works correctly, preventing the system from forcing full context mode and loading all documents in a knowledge base regardless of settings. [#18133](https://github.com/open-webui/open-webui/issues/18133) +- 🔧 Filter inlet functions now correctly execute on tool call continuations, ensuring parameter persistence throughout tool interactions. [#18222](https://github.com/open-webui/open-webui/issues/18222) +- 🛠️ External tool servers now properly support DELETE requests with body data. [#18289](https://github.com/open-webui/open-webui/pull/18289), [#18287](https://github.com/open-webui/open-webui/issues/18287) +- 🗄️ Oracle23ai vector database client now correctly handles variable initialization, resolving UnboundLocalError when retrieving items from collections. [#18356](https://github.com/open-webui/open-webui/issues/18356) +- 🔧 Model auto-pull functionality now works correctly even when user settings remain unmodified. [#18324](https://github.com/open-webui/open-webui/pull/18324) +- 🎨 Duplicate HTML content in artifacts is now prevented by improving code block detection logic. [#18195](https://github.com/open-webui/open-webui/pull/18195), [#6154](https://github.com/open-webui/open-webui/issues/6154) +- 💬 Pinned chats now appear in the Reference Chats list and can be referenced in conversations. [#18288](https://github.com/open-webui/open-webui/issues/18288) +- 📝 Misleading knowledge base warning text in documents settings is clarified to correctly instruct users about reindexing vectors. [#18263](https://github.com/open-webui/open-webui/pull/18263) +- 🔔 Toast notifications can now be dismissed even when a modal is open. [#18260](https://github.com/open-webui/open-webui/pull/18260) +- 🔘 The "Chats" button in the sidebar now correctly toggles chat list visibility without navigating away from the current page. [#18232](https://github.com/open-webui/open-webui/pull/18232) +- 🎯 The Integrations menu no longer closes prematurely when clicking outside the Valves modal. [#18310](https://github.com/open-webui/open-webui/pull/18310) +- 🛠️ Tool ID display issues where "undefined" was incorrectly shown in the interface are now resolved. [#18178](https://github.com/open-webui/open-webui/pull/18178) +- 🛠️ Model management issues caused by excessively long model IDs are now prevented through validation that limits model IDs to 256 characters. [#18125](https://github.com/open-webui/open-webui/issues/18125) + ## [0.6.33] - 2025-10-08 ### Added diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bd73807621..f7926abe85 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -307,9 +307,15 @@ API_KEY_ALLOWED_ENDPOINTS = PersistentConfig( JWT_EXPIRES_IN = PersistentConfig( - "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1") + "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "4w") ) +if JWT_EXPIRES_IN.value == "-1": + log.warning( + "⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n" + " See: https://docs.openwebui.com/getting-started/env-configuration\n" + ) + #################################### # OAuth config #################################### @@ -2291,6 +2297,36 @@ DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig( os.environ.get("DATALAB_MARKER_OUTPUT_FORMAT", "markdown"), ) +MINERU_API_MODE = PersistentConfig( + "MINERU_API_MODE", + "rag.mineru_api_mode", + os.environ.get("MINERU_API_MODE", "local"), # "local" or "cloud" +) + +MINERU_API_URL = PersistentConfig( + "MINERU_API_URL", + "rag.mineru_api_url", + os.environ.get("MINERU_API_URL", "http://localhost:8000"), +) + +MINERU_API_KEY = PersistentConfig( + "MINERU_API_KEY", + "rag.mineru_api_key", + os.environ.get("MINERU_API_KEY", ""), +) + +mineru_params = os.getenv("MINERU_PARAMS", "") +try: + mineru_params = json.loads(mineru_params) +except json.JSONDecodeError: + mineru_params = {} + +MINERU_PARAMS = PersistentConfig( + "MINERU_PARAMS", + "rag.mineru_params", + mineru_params, +) + EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig( "EXTERNAL_DOCUMENT_LOADER_URL", "rag.external_document_loader_url", diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index 59ee6aaacb..6d63295ab8 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -38,6 +38,7 @@ class ERROR_MESSAGES(str, Enum): ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string." MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string." NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string." + MODEL_ID_TOO_LONG = "The model id is too long. Please make sure your model id is less than 256 characters long." INVALID_TOKEN = ( "Your session has expired or the token is invalid. Please sign in again." diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 221c20f305..9998af0e73 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -243,6 +243,10 @@ from open_webui.config import ( DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, DATALAB_MARKER_FORMAT_LINES, DATALAB_MARKER_OUTPUT_FORMAT, + MINERU_API_MODE, + MINERU_API_URL, + MINERU_API_KEY, + MINERU_PARAMS, DATALAB_MARKER_USE_LLM, EXTERNAL_DOCUMENT_LOADER_URL, EXTERNAL_DOCUMENT_LOADER_API_KEY, @@ -853,6 +857,10 @@ app.state.config.DOCLING_PICTURE_DESCRIPTION_API = DOCLING_PICTURE_DESCRIPTION_A app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY +app.state.config.MINERU_API_MODE = MINERU_API_MODE +app.state.config.MINERU_API_URL = MINERU_API_URL +app.state.config.MINERU_API_KEY = MINERU_API_KEY +app.state.config.MINERU_PARAMS = MINERU_PARAMS app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 98b1166ce4..cfcbc004b7 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -502,6 +502,7 @@ class ChatTable: user_id: str, include_archived: bool = False, include_folders: bool = False, + include_pinned: bool = False, skip: Optional[int] = None, limit: Optional[int] = None, ) -> list[ChatTitleIdResponse]: @@ -511,7 +512,8 @@ class ChatTable: if not include_folders: query = query.filter_by(folder_id=None) - query = query.filter(or_(Chat.pinned == False, Chat.pinned == None)) + if not include_pinned: + query = query.filter(or_(Chat.pinned == False, Chat.pinned == None)) if not include_archived: query = query.filter_by(archived=False) diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index c5cbaf91f8..171810fde7 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -82,6 +82,7 @@ class FileModelResponse(BaseModel): class FileMetadataResponse(BaseModel): id: str + hash: Optional[str] = None meta: dict created_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -147,6 +148,7 @@ class FilesTable: file = db.get(File, id) return FileMetadataResponse( id=file.id, + hash=file.hash, meta=file.meta, created_at=file.created_at, updated_at=file.updated_at, @@ -182,12 +184,13 @@ class FilesTable: return [ FileMetadataResponse( id=file.id, + hash=file.hash, meta=file.meta, created_at=file.created_at, updated_at=file.updated_at, ) for file in db.query( - File.id, File.meta, File.created_at, File.updated_at + File.id, File.hash, File.meta, File.created_at, File.updated_at ) .filter(File.id.in_(ids)) .order_by(File.updated_at.desc()) diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index b3d90cc8f3..2ef1d75e02 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -27,6 +27,7 @@ from open_webui.retrieval.loaders.external_document import ExternalDocumentLoade from open_webui.retrieval.loaders.mistral import MistralLoader from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader +from open_webui.retrieval.loaders.mineru import MinerULoader from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL @@ -367,6 +368,22 @@ class Loader: api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), azure_credential=DefaultAzureCredential(), ) + elif self.engine == "mineru" and file_ext in [ + "pdf", + "doc", + "docx", + "ppt", + "pptx", + "xls", + "xlsx", + ]: + loader = MinerULoader( + file_path=file_path, + api_mode=self.kwargs.get("MINERU_API_MODE", "local"), + api_url=self.kwargs.get("MINERU_API_URL", "http://localhost:8000"), + api_key=self.kwargs.get("MINERU_API_KEY", ""), + params=self.kwargs.get("MINERU_PARAMS", {}), + ) elif ( self.engine == "mistral_ocr" and self.kwargs.get("MISTRAL_OCR_API_KEY") != "" diff --git a/backend/open_webui/retrieval/loaders/mineru.py b/backend/open_webui/retrieval/loaders/mineru.py new file mode 100644 index 0000000000..437f44ae6b --- /dev/null +++ b/backend/open_webui/retrieval/loaders/mineru.py @@ -0,0 +1,541 @@ +import os +import time +import requests +import logging +import tempfile +import zipfile +from typing import List, Optional +from langchain_core.documents import Document +from fastapi import HTTPException, status + +log = logging.getLogger(__name__) + + +class MinerULoader: + """ + MinerU document parser loader supporting both Cloud API and Local API modes. + + Cloud API: Uses MinerU managed service with async task-based processing + Local API: Uses self-hosted MinerU API with synchronous processing + """ + + def __init__( + self, + file_path: str, + api_mode: str = "local", + api_url: str = "http://localhost:8000", + api_key: str = "", + params: dict = None, + ): + self.file_path = file_path + self.api_mode = api_mode.lower() + self.api_url = api_url.rstrip("/") + self.api_key = api_key + + # Parse params dict with defaults + params = params or {} + self.enable_ocr = params.get("enable_ocr", False) + self.enable_formula = params.get("enable_formula", True) + self.enable_table = params.get("enable_table", True) + self.language = params.get("language", "en") + self.model_version = params.get("model_version", "pipeline") + self.page_ranges = params.get("page_ranges", "") + + # Validate API mode + if self.api_mode not in ["local", "cloud"]: + raise ValueError( + f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'" + ) + + # Validate Cloud API requirements + if self.api_mode == "cloud" and not self.api_key: + raise ValueError("API key is required for Cloud API mode") + + def load(self) -> List[Document]: + """ + Main entry point for loading and parsing the document. + Routes to Cloud or Local API based on api_mode. + """ + try: + if self.api_mode == "cloud": + return self._load_cloud_api() + else: + return self._load_local_api() + except Exception as e: + log.error(f"Error loading document with MinerU: {e}") + raise + + def _load_local_api(self) -> List[Document]: + """ + Load document using Local API (synchronous). + Posts file to /file_parse endpoint and gets immediate response. + """ + log.info(f"Using MinerU Local API at {self.api_url}") + + filename = os.path.basename(self.file_path) + + # Build form data for Local API + form_data = { + "return_md": "true", + "formula_enable": str(self.enable_formula).lower(), + "table_enable": str(self.enable_table).lower(), + } + + # Parse method based on OCR setting + if self.enable_ocr: + form_data["parse_method"] = "ocr" + else: + form_data["parse_method"] = "auto" + + # Language configuration (Local API uses lang_list array) + if self.language: + form_data["lang_list"] = self.language + + # Backend/model version (Local API uses "backend" parameter) + if self.model_version == "vlm": + form_data["backend"] = "vlm-vllm-engine" + else: + form_data["backend"] = "pipeline" + + # Page ranges (Local API uses start_page_id and end_page_id) + if self.page_ranges: + # For simplicity, if page_ranges is specified, log a warning + # Full page range parsing would require parsing the string + log.warning( + f"Page ranges '{self.page_ranges}' specified but Local API uses different format. " + "Consider using start_page_id/end_page_id parameters if needed." + ) + + try: + with open(self.file_path, "rb") as f: + files = {"files": (filename, f, "application/octet-stream")} + + log.info(f"Sending file to MinerU Local API: {filename}") + log.debug(f"Local API parameters: {form_data}") + + response = requests.post( + f"{self.api_url}/file_parse", + data=form_data, + files=files, + timeout=300, # 5 minute timeout for large documents + ) + response.raise_for_status() + + except FileNotFoundError: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" + ) + except requests.Timeout: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail="MinerU Local API request timed out", + ) + except requests.HTTPError as e: + error_detail = f"MinerU Local API request failed: {e}" + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f" - {error_data}" + except: + error_detail += f" - {e.response.text}" + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error calling MinerU Local API: {str(e)}", + ) + + # Parse response + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Invalid JSON response from MinerU Local API: {e}", + ) + + # Extract markdown content from response + if "results" not in result: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail="MinerU Local API response missing 'results' field", + ) + + results = result["results"] + if not results: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail="MinerU returned empty results", + ) + + # Get the first (and typically only) result + file_result = list(results.values())[0] + markdown_content = file_result.get("md_content", "") + + if not markdown_content: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail="MinerU returned empty markdown content", + ) + + log.info(f"Successfully parsed document with MinerU Local API: {filename}") + + # Create metadata + metadata = { + "source": filename, + "api_mode": "local", + "backend": result.get("backend", "unknown"), + "version": result.get("version", "unknown"), + } + + return [Document(page_content=markdown_content, metadata=metadata)] + + def _load_cloud_api(self) -> List[Document]: + """ + Load document using Cloud API (asynchronous). + Uses batch upload endpoint to avoid need for public file URLs. + """ + log.info(f"Using MinerU Cloud API at {self.api_url}") + + filename = os.path.basename(self.file_path) + + # Step 1: Request presigned upload URL + batch_id, upload_url = self._request_upload_url(filename) + + # Step 2: Upload file to presigned URL + self._upload_to_presigned_url(upload_url) + + # Step 3: Poll for results + result = self._poll_batch_status(batch_id, filename) + + # Step 4: Download and extract markdown from ZIP + markdown_content = self._download_and_extract_zip( + result["full_zip_url"], filename + ) + + log.info(f"Successfully parsed document with MinerU Cloud API: {filename}") + + # Create metadata + metadata = { + "source": filename, + "api_mode": "cloud", + "batch_id": batch_id, + } + + return [Document(page_content=markdown_content, metadata=metadata)] + + def _request_upload_url(self, filename: str) -> tuple: + """ + Request presigned upload URL from Cloud API. + Returns (batch_id, upload_url). + """ + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + # Build request body + request_body = { + "enable_formula": self.enable_formula, + "enable_table": self.enable_table, + "language": self.language, + "model_version": self.model_version, + "files": [ + { + "name": filename, + "is_ocr": self.enable_ocr, + } + ], + } + + # Add page ranges if specified + if self.page_ranges: + request_body["files"][0]["page_ranges"] = self.page_ranges + + log.info(f"Requesting upload URL for: {filename}") + log.debug(f"Cloud API request body: {request_body}") + + try: + response = requests.post( + f"{self.api_url}/file-urls/batch", + headers=headers, + json=request_body, + timeout=30, + ) + response.raise_for_status() + except requests.HTTPError as e: + error_detail = f"Failed to request upload URL: {e}" + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f" - {error_data.get('msg', error_data)}" + except: + error_detail += f" - {e.response.text}" + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error requesting upload URL: {str(e)}", + ) + + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Invalid JSON response: {e}", + ) + + # Check for API error response + if result.get("code") != 0: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}", + ) + + data = result.get("data", {}) + batch_id = data.get("batch_id") + file_urls = data.get("file_urls", []) + + if not batch_id or not file_urls: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail="MinerU Cloud API response missing batch_id or file_urls", + ) + + upload_url = file_urls[0] + log.info(f"Received upload URL for batch: {batch_id}") + + return batch_id, upload_url + + def _upload_to_presigned_url(self, upload_url: str) -> None: + """ + Upload file to presigned URL (no authentication needed). + """ + log.info(f"Uploading file to presigned URL") + + try: + with open(self.file_path, "rb") as f: + response = requests.put( + upload_url, + data=f, + timeout=300, # 5 minute timeout for large files + ) + response.raise_for_status() + except FileNotFoundError: + raise HTTPException( + status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" + ) + except requests.Timeout: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail="File upload to presigned URL timed out", + ) + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Failed to upload file to presigned URL: {e}", + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading file: {str(e)}", + ) + + log.info("File uploaded successfully") + + def _poll_batch_status(self, batch_id: str, filename: str) -> dict: + """ + Poll batch status until completion. + Returns the result dict for the file. + """ + headers = { + "Authorization": f"Bearer {self.api_key}", + } + + max_iterations = 300 # 10 minutes max (2 seconds per iteration) + poll_interval = 2 # seconds + + log.info(f"Polling batch status: {batch_id}") + + for iteration in range(max_iterations): + try: + response = requests.get( + f"{self.api_url}/extract-results/batch/{batch_id}", + headers=headers, + timeout=30, + ) + response.raise_for_status() + except requests.HTTPError as e: + error_detail = f"Failed to poll batch status: {e}" + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f" - {error_data.get('msg', error_data)}" + except: + error_detail += f" - {e.response.text}" + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error polling batch status: {str(e)}", + ) + + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Invalid JSON response while polling: {e}", + ) + + # Check for API error response + if result.get("code") != 0: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}", + ) + + data = result.get("data", {}) + extract_result = data.get("extract_result", []) + + # Find our file in the batch results + file_result = None + for item in extract_result: + if item.get("file_name") == filename: + file_result = item + break + + if not file_result: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"File {filename} not found in batch results", + ) + + state = file_result.get("state") + + if state == "done": + log.info(f"Processing complete for {filename}") + return file_result + elif state == "failed": + error_msg = file_result.get("err_msg", "Unknown error") + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"MinerU processing failed: {error_msg}", + ) + elif state in ["waiting-file", "pending", "running", "converting"]: + # Still processing + if iteration % 10 == 0: # Log every 20 seconds + log.info( + f"Processing status: {state} (iteration {iteration + 1}/{max_iterations})" + ) + time.sleep(poll_interval) + else: + log.warning(f"Unknown state: {state}") + time.sleep(poll_interval) + + # Timeout + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail="MinerU processing timed out after 10 minutes", + ) + + def _download_and_extract_zip(self, zip_url: str, filename: str) -> str: + """ + Download ZIP file from CDN and extract markdown content. + Returns the markdown content as a string. + """ + log.info(f"Downloading results from: {zip_url}") + + try: + response = requests.get(zip_url, timeout=60) + response.raise_for_status() + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"Failed to download results ZIP: {e}", + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error downloading results: {str(e)}", + ) + + # Save ZIP to temporary file and extract + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_zip: + tmp_zip.write(response.content) + tmp_zip_path = tmp_zip.name + + with tempfile.TemporaryDirectory() as tmp_dir: + # Extract ZIP + with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref: + zip_ref.extractall(tmp_dir) + + # Find markdown file - search recursively for any .md file + markdown_content = None + found_md_path = None + + # First, list all files in the ZIP for debugging + all_files = [] + for root, dirs, files in os.walk(tmp_dir): + for file in files: + full_path = os.path.join(root, file) + all_files.append(full_path) + # Look for any .md file + if file.endswith(".md"): + found_md_path = full_path + log.info(f"Found markdown file at: {full_path}") + try: + with open(full_path, "r", encoding="utf-8") as f: + markdown_content = f.read() + if ( + markdown_content + ): # Use the first non-empty markdown file + break + except Exception as e: + log.warning(f"Failed to read {full_path}: {e}") + if markdown_content: + break + + if markdown_content is None: + log.error(f"Available files in ZIP: {all_files}") + # Try to provide more helpful error message + md_files = [f for f in all_files if f.endswith(".md")] + if md_files: + error_msg = ( + f"Found .md files but couldn't read them: {md_files}" + ) + else: + error_msg = ( + f"No .md files found in ZIP. Available files: {all_files}" + ) + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=error_msg, + ) + + # Clean up temporary ZIP file + os.unlink(tmp_zip_path) + + except zipfile.BadZipFile as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Invalid ZIP file received: {e}", + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error extracting ZIP: {str(e)}", + ) + + if not markdown_content: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail="Extracted markdown content is empty", + ) + + log.info( + f"Successfully extracted markdown content ({len(markdown_content)} characters)" + ) + return markdown_content diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 66a7fa2ddf..69aee29ac2 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -268,6 +268,13 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict: combined = dict() # To store documents with unique document hashes for data in query_results: + if ( + len(data.get("distances", [])) == 0 + or len(data.get("documents", [])) == 0 + or len(data.get("metadatas", [])) == 0 + ): + continue + distances = data["distances"][0] documents = data["documents"][0] metadatas = data["metadatas"][0] diff --git a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py index 07b014681a..b714588bdc 100644 --- a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py +++ b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py @@ -717,7 +717,7 @@ class Oracle23aiClient(VectorDBBase): ) try: - limit = limit or 1000 + limit = 1000 # Hardcoded limit for get operation with self.get_connection() as connection: with connection.cursor() as cursor: diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 1f065988fe..2587c5ff8e 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -39,6 +39,7 @@ router = APIRouter() def get_session_user_chat_list( user=Depends(get_verified_user), page: Optional[int] = None, + include_pinned: Optional[bool] = False, include_folders: Optional[bool] = False, ): try: @@ -47,11 +48,15 @@ def get_session_user_chat_list( skip = (page - 1) * limit return Chats.get_chat_title_id_list_by_user_id( - user.id, include_folders=include_folders, skip=skip, limit=limit + user.id, + include_folders=include_folders, + include_pinned=include_pinned, + skip=skip, + limit=limit, ) else: return Chats.get_chat_title_id_list_by_user_id( - user.id, include_folders=include_folders + user.id, include_folders=include_folders, include_pinned=include_pinned ) except Exception as e: log.exception(e) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 10577e26d6..e7fa13d1ff 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -183,7 +183,7 @@ async def set_tool_servers_config( ) oauth_client_info = decrypt_data(oauth_client_info) - await request.app.state.oauth_client_manager.add_client( + request.app.state.oauth_client_manager.add_client( f"{server_type}:{server_id}", OAuthClientInformationFull(**oauth_client_info), ) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 5c5a2dcd90..215cd8426c 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -35,6 +35,10 @@ log = logging.getLogger(__name__) router = APIRouter() +def validate_model_id(model_id: str) -> bool: + return model_id and len(model_id) <= 256 + + ########################### # GetModels ########################### @@ -84,6 +88,12 @@ async def create_new_model( detail=ERROR_MESSAGES.MODEL_ID_TAKEN, ) + if not validate_model_id(form_data.id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.MODEL_ID_TOO_LONG, + ) + else: model = Models.insert_new_model(form_data, user.id) if model: @@ -124,7 +134,8 @@ async def import_models( for model_data in data: # Here, you can add logic to validate model_data if needed model_id = model_data.get("id") - if model_id: + + if model_id and validate_model_id(model_id): existing_model = Models.get_model_by_id(model_id) if existing_model: # Update existing model diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index c79d3ce656..cb66e8926e 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -466,6 +466,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, + # MinerU settings + "MINERU_API_MODE": request.app.state.config.MINERU_API_MODE, + "MINERU_API_URL": request.app.state.config.MINERU_API_URL, + "MINERU_API_KEY": request.app.state.config.MINERU_API_KEY, + "MINERU_PARAMS": request.app.state.config.MINERU_PARAMS, # Reranking settings "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, "RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE, @@ -647,6 +652,12 @@ class ConfigForm(BaseModel): DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None MISTRAL_OCR_API_KEY: Optional[str] = None + # MinerU settings + MINERU_API_MODE: Optional[str] = None + MINERU_API_URL: Optional[str] = None + MINERU_API_KEY: Optional[str] = None + MINERU_PARAMS: Optional[dict] = None + # Reranking settings RAG_RERANKING_MODEL: Optional[str] = None RAG_RERANKING_ENGINE: Optional[str] = None @@ -886,6 +897,28 @@ async def update_rag_config( else request.app.state.config.MISTRAL_OCR_API_KEY ) + # MinerU settings + request.app.state.config.MINERU_API_MODE = ( + form_data.MINERU_API_MODE + if form_data.MINERU_API_MODE is not None + else request.app.state.config.MINERU_API_MODE + ) + request.app.state.config.MINERU_API_URL = ( + form_data.MINERU_API_URL + if form_data.MINERU_API_URL is not None + else request.app.state.config.MINERU_API_URL + ) + request.app.state.config.MINERU_API_KEY = ( + form_data.MINERU_API_KEY + if form_data.MINERU_API_KEY is not None + else request.app.state.config.MINERU_API_KEY + ) + request.app.state.config.MINERU_PARAMS = ( + form_data.MINERU_PARAMS + if form_data.MINERU_PARAMS is not None + else request.app.state.config.MINERU_PARAMS + ) + # Reranking settings if request.app.state.config.RAG_RERANKING_ENGINE == "": # Unloading the internal reranker and clear VRAM memory @@ -1150,6 +1183,11 @@ async def update_rag_config( "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, + # MinerU settings + "MINERU_API_MODE": request.app.state.config.MINERU_API_MODE, + "MINERU_API_URL": request.app.state.config.MINERU_API_URL, + "MINERU_API_KEY": request.app.state.config.MINERU_API_KEY, + "MINERU_PARAMS": request.app.state.config.MINERU_PARAMS, # Reranking settings "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, "RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE, @@ -1560,6 +1598,10 @@ def process_file( DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY, + MINERU_API_MODE=request.app.state.config.MINERU_API_MODE, + MINERU_API_URL=request.app.state.config.MINERU_API_URL, + MINERU_API_KEY=request.app.state.config.MINERU_API_KEY, + MINERU_PARAMS=request.app.state.config.MINERU_PARAMS, ) docs = loader.load( file.filename, file.meta.get("content_type"), file_path diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index bbfdd6a368..dd42612eee 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -827,11 +827,7 @@ async def chat_completion_files_handler( if files := body.get("metadata", {}).get("files", None): # Check if all files are in full context mode - all_full_context = all( - item.get("context") == "full" - for item in files - if item.get("type") == "file" - ) + all_full_context = all(item.get("context") == "full" for item in files) queries = [] if not all_full_context: @@ -1509,8 +1505,8 @@ async def process_chat_response( ) follow_ups_string = response_message.get( - "content", response_message.get("reasoning_content", "") - ) + "content" + ) or response_message.get("reasoning_content", "") else: follow_ups_string = "" @@ -1573,12 +1569,12 @@ async def process_chat_response( "message", {} ) - title_string = response_message.get( - "content", - response_message.get( + title_string = ( + response_message.get("content") + or response_message.get( "reasoning_content", - message.get("content", user_message), - ), + ) + or message.get("content", user_message) ) else: title_string = "" @@ -1637,9 +1633,8 @@ async def process_chat_response( ) tags_string = response_message.get( - "content", - response_message.get("reasoning_content", ""), - ) + "content" + ) or response_message.get("reasoning_content", "") else: tags_string = "" @@ -2671,8 +2666,6 @@ async def process_chat_response( results = [] for tool_call in response_tool_calls: - - print("tool_call", tool_call) tool_call_id = tool_call.get("id", "") tool_function_name = tool_call.get("function", {}).get( "name", "" @@ -2803,9 +2796,9 @@ async def process_chat_response( try: new_form_data = { + **form_data, "model": model_id, "stream": True, - "tools": form_data["tools"], "messages": [ *form_data["messages"], *convert_content_blocks_to_messages( @@ -2979,6 +2972,7 @@ async def process_chat_response( try: new_form_data = { + **form_data, "model": model_id, "stream": True, "messages": [ diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 6a0a7346bb..16f50dfb20 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -754,7 +754,7 @@ async def execute_tool_server( ) as session: request_method = getattr(session, http_method.lower()) - if http_method in ["post", "put", "patch"]: + if http_method in ["post", "put", "patch", "delete"]: async with request_method( final_url, json=body_params, diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b348bad89..8876f5d8ac 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,7 +10,7 @@ cryptography bcrypt==5.0.0 argon2-cffi==25.1.0 PyJWT[crypto]==2.10.1 -authlib==1.6.3 +authlib==1.6.5 requests==2.32.5 aiohttp==3.12.15 diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 507e3c6069..f08c465471 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -4,10 +4,11 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| main | :white_check_mark: | -| others | :x: | +| Version (Branch) | Supported | +| ---------------- | ------------------ | +| main | :white_check_mark: | +| dev | :x: | +| others | :x: | ## Zero Tolerance for External Platforms @@ -17,28 +18,101 @@ Any reports or solicitations arriving from sources other than our designated Git ## Reporting a Vulnerability -We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines: - Reports not submitted through our designated GitHub repository will be disregarded, and we will categorically reject invitations to collaborate on external platforms. Our aggressive stance on this matter underscores our commitment to a secure, transparent, and open community where all operations are visible and contributors are accountable. -1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted. +We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. Ensure that **submissions are constructive, actionable, reproducible, well documented and adhere to the following guidelines**: -2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts. +1. **Report MUST be a vulnerability:** A security vulnerability is an exploitable weakness where the system behaves in an unintended way, allowing attackers to bypass security controls, gain unauthorized access, execute arbitrary code, or escalate privileges. Configuration options, missing features, and expected protocol behavior are **not vulnerabilities**. -3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded. +2. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted. -4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly. +3. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts. -5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security. +4. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence may be disregarded. -**Non-compliant submissions will be closed, and repeat violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users. +> [!NOTE] +> A PoC (Proof of Concept) is a **demonstration of exploitation of a vulnerability**. Your PoC must show: +> +> 1. What security boundary was crossed (Confidentiality, Integrity, Availability, Authenticity, Non-repudiation) +> 2. How this vulnerability was abused +> 3. What actions the attacker can now perform +> +> **Examples of valid PoCs:** +> +> - Step-by-step reproduction instructions with exact commands +> - Complete exploit code with detailed execution instructions +> - Screenshots/videos demonstrating the exploit (supplementary to written steps) +> +> **Failure to provide a reproducible PoC may lead to closure of the report** +> We will notify you, if we struggle to reproduce the exploit using your PoC to allow you to improve your PoC +> However, if we repeatedly cannot reproduce the exploit using the PoC, the report may be closed -## Product Security +5. **Required Patch or Actionable Remediation Plan Submission**: Along with the PoC, reporters must provide a patch or some actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly. + +6. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider provided patches for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security. + +7. **Default Configuration Testing**: All vulnerability reports MUST be tested and reproducible using Open WebUI's out-of-the-box default configuration. Claims of vulnerabilities that only manifest with explicitly weakened security settings may be discarded, unless they are covered by the following exception: + +> [!NOTE] +> **Note**: If you believe you have found a security issue that +> +> 1. affects default configurations **or** +> 2. represents a genuine bypass of intended security controls **or** +> 3. works only with non-default configurations **but the configuration in question is likely to be used by production deployments** > **then we absolutely want to hear about it.** This policy is intended to filter configuration issues and deployment problems, not to discourage legitimate security research. + +8. **Threat Model Understanding Required**: Reports must demonstrate understanding of Open WebUI's self-hosted, authenticated, role-based access control architecture. Comparing Open WebUI to services with fundamentally different security models without acknowledging the architectural differences may result in report rejection. + +9. **CVSS Scoring Accuracy:** If you include a CVSS score with your report, it must accurately reflect the vulnerability according to CVSS methodology. Common errors include 1) rating PR:N (None) when authentication is required, 2) scoring hypothetical attack chains instead of the actual vulnerability, or 3) inflating severity without evidence. **We will adjust inaccurate CVSS scores.** Intentionally inflated scores may result in report rejection. + +> [!WARNING] > **Using CVE Precedents:** If you cite other CVEs to support your report, ensure they are **genuinely comparable** in vulnerability type, threat model, and attack vector. Citing CVEs from different product categories, different vulnerability classes or different deployment models will lead us to suspect the use of AI in your report. + +11. **Admin Actions Are Out of Scope:** Vulnerabilities that require an administrator to actively perform unsafe actions are **not considered valid vulnerabilities**. Admins have full system control and are expected to understand the security implications of their actions and configurations. This includes but is not limited to: adding malicious external servers (models, tools, webhooks), pasting untrusted code into Functions/Tools, or intentionally weakening security settings. **Reports requiring admin negligence or social engineering of admins may be rejected.** + +12. **AI report transparency:** Due to an extreme spike in AI-aided vulnerability reports **YOU MUST DISCLOSE if AI was used in any capacity** - whether for writing the report, generating the PoC, or identifying the vulnerability. If AI helped you in any way shape or form in the creation of the report, PoC or finding the vulnerability, you MUST disclose it. + +> [!NOTE] +> AI-aided vulnerability reports **will not be rejected by us by default.** But: +> +> - If we suspect you used AI (but you did not disclose it to us), we will be asking tough follow-up questions to validate your understanding of the reported vulnerability and Open WebUI itself. +> - If we suspect you used AI (but you did not disclose it to us) **and** your report ends up being invalid/not a vulnerability/not reproducible, then you **may be banned** from reporting future vulnerabilities. +> +> This measure was necessary due to the extreme rise in clearly AI written vulnerability reports, where the vast majority of them +> +> - were not a vulnerability +> - were faulty configurations rather than a real vulnerability +> - did not provide a PoC +> - violated any of the rules outlined here +> - had a clear lack of understanding of Open WebUI +> - wrote comments with conflicting information +> - used illogical arguments + +**Non-compliant submissions will be closed, and repeat extreme violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users. + +If you want to report a vulnerability and can meet the outlined requirements, [open a vulnerability report here](https://github.com/open-webui/open-webui/security/advisories/new). + +## Product Security And For Non-Vulnerability Security Concerns: + +If your concern does not meet the vulnerability requirements outlined above, such as: + +- Suggestions for better default configuration values +- Security hardening recommendations +- Deployment best practices guidance +- Unclear configuration instructions +- Need for additional security documentation +- Feature requests for optional security enhancements (2FA, audit logging, etc.) +- General security questions about production deployment + +**then use one of the following channels instead:** + +- **Documentation issues/improvement ideas:** Open an issue on our [Documentation Repository](https://github.com/open-webui/docs) +- **Feature requests:** Create a discussion in [GitHub Discussions - Ideas](https://github.com/open-webui/open-webui/discussions/) to discuss with the community if this feature request is wanted by multiple people +- **Configuration help:** Ask the community for help and guidance on our [Discord Server](https://discord.gg/5rJgQTnV4s) or on [Reddit](https://www.reddit.com/r/OpenWebUI/) +- **General issues:** Use our [Issue Tracker](https://github.com/open-webui/open-webui/issues) We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon. -For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s). +For any other immediate concerns, please create an issue in our [issue tracker](https://github.com/open-webui/open-webui/issues) or contact our team on [Discord](https://discord.gg/5rJgQTnV4s). --- -_Last updated on **2024-08-19**._ +_Last updated on **2025-10-12**._ diff --git a/package-lock.json b/package-lock.json index 38cf347d3e..4b3183e318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.33", + "version": "0.6.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.33", + "version": "0.6.34", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index bfb7b0d871..6d0aa8f5d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.33", + "version": "0.6.34", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 4452fff89a..cd5a08fba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "bcrypt==5.0.0", "argon2-cffi==25.1.0", "PyJWT[crypto]==2.10.1", - "authlib==1.6.3", + "authlib==1.6.5", "requests==2.32.5", "aiohttp==3.12.15", diff --git a/src/app.css b/src/app.css index e8f4ee137b..49bcebbd97 100644 --- a/src/app.css +++ b/src/app.css @@ -129,8 +129,8 @@ li p { } ::-webkit-scrollbar { - height: 0.4rem; - width: 0.4rem; + height: 0.8rem; + width: 0.8rem; } ::-webkit-scrollbar-track { diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index b8073d94fa..c548a71dc2 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -112,6 +112,7 @@ export const importChat = async ( export const getChatList = async ( token: string = '', page: number | null = null, + include_pinned: boolean = false, include_folders: boolean = false ) => { let error = null; @@ -125,6 +126,10 @@ export const getChatList = async ( searchParams.append('include_folders', 'true'); } + if (include_pinned) { + searchParams.append('include_pinned', 'true'); + } + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, { method: 'GET', headers: { diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 43a5936a23..75a289d631 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -494,7 +494,10 @@ export const executeToolServer = async ( headers }; - if (['post', 'put', 'patch'].includes(httpMethod.toLowerCase()) && operation.requestBody) { + if ( + ['post', 'put', 'patch', 'delete'].includes(httpMethod.toLowerCase()) && + operation.requestBody + ) { requestOptions.body = JSON.stringify(bodyParams); } diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 77b3903740..efa63d89b9 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -207,6 +207,15 @@ return; } + if ( + RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru' && + RAGConfig.MINERU_API_MODE === 'cloud' && + RAGConfig.MINERU_API_KEY === '' + ) { + toast.error($i18n.t('MinerU API Key required for Cloud API mode.')); + return; + } + if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) { await embeddingModelUpdateHandler(); } @@ -337,6 +346,7 @@ + @@ -749,6 +759,92 @@ bind:value={RAGConfig.MISTRAL_OCR_API_KEY} /> + {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru'} + +
+
+
+ {$i18n.t('API Mode')} +
+ +
+
+ + +
+ +
+ + + {#if RAGConfig.MINERU_API_MODE === 'cloud'} +
+ +
+ {/if} + + +
+
+ + {$i18n.t('Parameters')} + +
+
+