diff --git a/README.md b/README.md index 0fb03537df..56ab09b05d 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa In the last part of the command, replace `open-webui` with your container name if it is different. -Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/). +Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating). ### Using the Dev Branch 🌙 diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bf6f1d0256..6993b28011 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -660,6 +660,7 @@ S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None) S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None) S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None) S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) +S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) @@ -1325,6 +1326,48 @@ Your task is to synthesize these responses into a single, high-quality response. Responses from models: {{responses}}""" +#################################### +# Code Interpreter +#################################### + +ENABLE_CODE_INTERPRETER = PersistentConfig( + "ENABLE_CODE_INTERPRETER", + "code_interpreter.enable", + os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true", +) + +CODE_INTERPRETER_ENGINE = PersistentConfig( + "CODE_INTERPRETER_ENGINE", + "code_interpreter.engine", + os.environ.get("CODE_INTERPRETER_ENGINE", "pyodide"), +) + +CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( + "CODE_INTERPRETER_JUPYTER_URL", + "code_interpreter.jupyter.url", + os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""), +) + +CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( + "CODE_INTERPRETER_JUPYTER_AUTH", + "code_interpreter.jupyter.auth", + os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""), +) + +CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( + "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", + "code_interpreter.jupyter.auth_token", + os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""), +) + + +CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( + "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", + "code_interpreter.jupyter.auth_password", + os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""), +) + + DEFAULT_CODE_INTERPRETER_PROMPT = """ #### Tools Available @@ -1645,7 +1688,7 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig( # This ensures the highest level of safety and reliability of the information sources. RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST", - "rag.rag.web.search.domain.filter_list", + "rag.web.search.domain.filter_list", [ # "wikipedia.com", # "wikimedia.org", @@ -2012,6 +2055,12 @@ WHISPER_MODEL_AUTO_UPDATE = ( and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true" ) +# Add Deepgram configuration +DEEPGRAM_API_KEY = PersistentConfig( + "DEEPGRAM_API_KEY", + "audio.stt.deepgram.api_key", + os.getenv("DEEPGRAM_API_KEY", ""), +) AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( "AUDIO_STT_OPENAI_API_BASE_URL", diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 00605e15dc..0be3887f82 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -92,6 +92,7 @@ log_sources = [ "RAG", "WEBHOOK", "SOCKET", + "OAUTH", ] SRC_LOG_LEVELS = {} diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 863f58dea5..2b741a2bce 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -97,6 +97,13 @@ from open_webui.config import ( OPENAI_API_BASE_URLS, OPENAI_API_KEYS, OPENAI_API_CONFIGS, + # Code Interpreter + ENABLE_CODE_INTERPRETER, + CODE_INTERPRETER_ENGINE, + CODE_INTERPRETER_JUPYTER_URL, + CODE_INTERPRETER_JUPYTER_AUTH, + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, # Image AUTOMATIC1111_API_AUTH, AUTOMATIC1111_BASE_URL, @@ -130,6 +137,7 @@ from open_webui.config import ( AUDIO_TTS_AZURE_SPEECH_REGION, AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, WHISPER_MODEL, + DEEPGRAM_API_KEY, WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_DIR, # Retrieval @@ -569,6 +577,23 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( app.state.config.RAG_EMBEDDING_BATCH_SIZE, ) +######################################## +# +# CODE INTERPRETER +# +######################################## + +app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER +app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE + +app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ( + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN +) +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD +) ######################################## # @@ -611,6 +636,7 @@ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE app.state.config.STT_MODEL = AUDIO_STT_MODEL app.state.config.WHISPER_MODEL = WHISPER_MODEL +app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY @@ -753,6 +779,7 @@ app.include_router(openai.router, prefix="/openai", tags=["openai"]) app.include_router(pipelines.router, prefix="/api/v1/pipelines", tags=["pipelines"]) app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) app.include_router(images.router, prefix="/api/v1/images", tags=["images"]) + app.include_router(audio.router, prefix="/api/v1/audio", tags=["audio"]) app.include_router(retrieval.router, prefix="/api/v1/retrieval", tags=["retrieval"]) @@ -1013,12 +1040,14 @@ async def get_app_config(request: Request): { "enable_channels": app.state.config.ENABLE_CHANNELS, "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH, - "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER, "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION, + "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING, "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING, "enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, + "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, } if user is not None else {} diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 73ff6c102d..9e0a5865e9 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -470,7 +470,7 @@ class ChatTable: try: with get_db() as db: # it is possible that the shared link was deleted. hence, - # we check if the chat is still shared by checkng if a chat with the share_id exists + # we check if the chat is still shared by checking if a chat with the share_id exists chat = db.query(Chat).filter_by(share_id=id).first() if chat: diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index b3d8b5eb8a..b8186b3f93 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -113,6 +113,34 @@ class OpenSearchClient: return self._result_to_search_result(result) + def query( + self, collection_name: str, filter: dict, limit: Optional[int] = None + ) -> Optional[GetResult]: + if not self.has_collection(collection_name): + return None + + query_body = { + "query": {"bool": {"filter": []}}, + "_source": ["text", "metadata"], + } + + for field, value in filter.items(): + query_body["query"]["bool"]["filter"].append({"term": {field: value}}) + + size = limit if limit else 10 + + try: + result = self.client.search( + index=f"{self.index_prefix}_{collection_name}", + body=query_body, + size=size, + ) + + return self._result_to_get_result(result) + + except Exception as e: + return None + def get_or_create_index(self, index_name: str, dimension: int): if not self.has_index(index_name): self._create_index(index_name, dimension) diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py index 3de6c18077..a87293db5c 100644 --- a/backend/open_webui/retrieval/web/jina_search.py +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -20,14 +20,23 @@ def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]: list[SearchResult]: A list of search results """ jina_search_endpoint = "https://s.jina.ai/" - headers = {"Accept": "application/json", "Authorization": f"Bearer {api_key}"} - url = str(URL(jina_search_endpoint + query)) - response = requests.get(url, headers=headers) + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": api_key, + "X-Retain-Images": "none", + } + + payload = {"q": query, "count": count if count <= 10 else 10} + + url = str(URL(jina_search_endpoint)) + response = requests.post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() results = [] - for result in data["data"][:count]: + for result in data["data"]: results.append( SearchResult( link=result["url"], diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index c1b15772bd..e2d05ba908 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -11,6 +11,7 @@ from pydub.silence import split_on_silence import aiohttp import aiofiles import requests +import mimetypes from fastapi import ( Depends, @@ -138,6 +139,7 @@ class STTConfigForm(BaseModel): ENGINE: str MODEL: str WHISPER_MODEL: str + DEEPGRAM_API_KEY: str class AudioConfigUpdateForm(BaseModel): @@ -165,6 +167,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)): "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -190,6 +193,7 @@ async def update_audio_config( request.app.state.config.STT_ENGINE = form_data.stt.ENGINE request.app.state.config.STT_MODEL = form_data.stt.MODEL request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL + request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY if request.app.state.config.STT_ENGINE == "": request.app.state.faster_whisper_model = set_faster_whisper_model( @@ -214,6 +218,7 @@ async def update_audio_config( "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -521,6 +526,69 @@ def transcribe(request: Request, file_path): raise Exception(detail if detail else "Open WebUI: Server Connection Error") + elif request.app.state.config.STT_ENGINE == "deepgram": + try: + # Determine the MIME type of the file + mime, _ = mimetypes.guess_type(file_path) + if not mime: + mime = "audio/wav" # fallback to wav if undetectable + + # Read the audio file + with open(file_path, "rb") as f: + file_data = f.read() + + # Build headers and parameters + headers = { + "Authorization": f"Token {request.app.state.config.DEEPGRAM_API_KEY}", + "Content-Type": mime, + } + + # Add model if specified + params = {} + if request.app.state.config.STT_MODEL: + params["model"] = request.app.state.config.STT_MODEL + + # Make request to Deepgram API + r = requests.post( + "https://api.deepgram.com/v1/listen", + headers=headers, + params=params, + data=file_data, + ) + r.raise_for_status() + response_data = r.json() + + # Extract transcript from Deepgram response + try: + transcript = response_data["results"]["channels"][0]["alternatives"][ + 0 + ].get("transcript", "") + except (KeyError, IndexError) as e: + log.error(f"Malformed response from Deepgram: {str(e)}") + raise Exception( + "Failed to parse Deepgram response - unexpected response format" + ) + data = {"text": transcript.strip()} + + # Save transcript + transcript_file = f"{file_dir}/{id}.json" + with open(transcript_file, "w") as f: + json.dump(data, f) + + return data + + except Exception as e: + log.exception(e) + detail = None + if r is not None: + try: + res = r.json() + if "error" in res: + detail = f"External: {res['error'].get('message', '')}" + except Exception: + detail = f"External: {e}" + raise Exception(detail if detail else "Open WebUI: Server Connection Error") + def compress_audio(file_path): if os.path.getsize(file_path) > MAX_FILE_SIZE: diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index ef6c4d8c1f..4fe7dbf4d4 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -36,6 +36,61 @@ async def export_config(user=Depends(get_admin_user)): return get_config() +############################ +# CodeInterpreterConfig +############################ +class CodeInterpreterConfigForm(BaseModel): + ENABLE_CODE_INTERPRETER: bool + CODE_INTERPRETER_ENGINE: str + CODE_INTERPRETER_JUPYTER_URL: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] + + +@router.get("/code_interpreter", response_model=CodeInterpreterConfigForm) +async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)): + return { + "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, + "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, + "CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + } + + +@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm) +async def set_code_interpreter_config( + request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) +): + request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER + request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE + request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = ( + form_data.CODE_INTERPRETER_JUPYTER_URL + ) + + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = ( + form_data.CODE_INTERPRETER_JUPYTER_AUTH + ) + + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ( + form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + ) + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( + form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + ) + + return { + "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, + "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, + "CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + } + + ############################ # SetDefaultModels ############################ diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 2ab06eb95e..64373c616c 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -11,10 +11,8 @@ import re import time from typing import Optional, Union from urllib.parse import urlparse - import aiohttp from aiocache import cached - import requests from fastapi import ( @@ -990,6 +988,8 @@ async def generate_chat_completion( ) payload = {**form_data.model_dump(exclude_none=True)} + if "metadata" in payload: + del payload["metadata"] model_id = payload["model"] model_info = Models.get_model_by_id(model_id) @@ -1408,9 +1408,10 @@ async def download_model( return None +# TODO: Progress bar does not reflect size & duration of upload. @router.post("/models/upload") @router.post("/models/upload/{url_idx}") -def upload_model( +async def upload_model( request: Request, file: UploadFile = File(...), url_idx: Optional[int] = None, @@ -1419,59 +1420,85 @@ def upload_model( if url_idx is None: url_idx = 0 ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + file_path = os.path.join(UPLOAD_DIR, file.filename) + os.makedirs(UPLOAD_DIR, exist_ok=True) - file_path = f"{UPLOAD_DIR}/{file.filename}" + # --- P1: save file locally --- + chunk_size = 1024 * 1024 * 2 # 2 MB chunks + with open(file_path, "wb") as out_f: + while True: + chunk = file.file.read(chunk_size) + # log.info(f"Chunk: {str(chunk)}") # DEBUG + if not chunk: + break + out_f.write(chunk) - # Save file in chunks - with open(file_path, "wb+") as f: - for chunk in file.file: - f.write(chunk) - - def file_process_stream(): + async def file_process_stream(): nonlocal ollama_url total_size = os.path.getsize(file_path) - chunk_size = 1024 * 1024 + log.info(f"Total Model Size: {str(total_size)}") # DEBUG + + # --- P2: SSE progress + calculate sha256 hash --- + file_hash = calculate_sha256(file_path, chunk_size) + log.info(f"Model Hash: {str(file_hash)}") # DEBUG try: with open(file_path, "rb") as f: - total = 0 - done = False - - while not done: - chunk = f.read(chunk_size) - if not chunk: - done = True - continue - - total += len(chunk) - progress = round((total / total_size) * 100, 2) - - res = { + bytes_read = 0 + while chunk := f.read(chunk_size): + bytes_read += len(chunk) + progress = round(bytes_read / total_size * 100, 2) + data_msg = { "progress": progress, "total": total_size, - "completed": total, + "completed": bytes_read, } - yield f"data: {json.dumps(res)}\n\n" + yield f"data: {json.dumps(data_msg)}\n\n" - if done: - f.seek(0) - hashed = calculate_sha256(f) - f.seek(0) + # --- P3: Upload to ollama /api/blobs --- + with open(file_path, "rb") as f: + url = f"{ollama_url}/api/blobs/sha256:{file_hash}" + response = requests.post(url, data=f) - url = f"{ollama_url}/api/blobs/sha256:{hashed}" - response = requests.post(url, data=f) + if response.ok: + log.info(f"Uploaded to /api/blobs") # DEBUG + # Remove local file + os.remove(file_path) - if response.ok: - res = { - "done": done, - "blob": f"sha256:{hashed}", - "name": file.filename, - } - os.remove(file_path) - yield f"data: {json.dumps(res)}\n\n" - else: - raise Exception( - "Ollama: Could not create blob, Please try again." - ) + # Create model in ollama + model_name, ext = os.path.splitext(file.filename) + log.info(f"Created Model: {model_name}") # DEBUG + + create_payload = { + "model": model_name, + # Reference the file by its original name => the uploaded blob's digest + "files": {file.filename: f"sha256:{file_hash}"}, + } + log.info(f"Model Payload: {create_payload}") # DEBUG + + # Call ollama /api/create + # https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model + create_resp = requests.post( + url=f"{ollama_url}/api/create", + headers={"Content-Type": "application/json"}, + data=json.dumps(create_payload), + ) + + if create_resp.ok: + log.info(f"API SUCCESS!") # DEBUG + done_msg = { + "done": True, + "blob": f"sha256:{file_hash}", + "name": file.filename, + "model_created": model_name, + } + yield f"data: {json.dumps(done_msg)}\n\n" + else: + raise Exception( + f"Failed to create model in Ollama. {create_resp.text}" + ) + + else: + raise Exception("Ollama: Could not create blob, Please try again.") except Exception as e: res = {"error": str(e)} diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index d18f2a8ffc..afda362373 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -75,9 +75,9 @@ async def cleanup_response( await session.close() -def openai_o1_handler(payload): +def openai_o1_o3_handler(payload): """ - Handle O1 specific parameters + Handle o1, o3 specific parameters """ if "max_tokens" in payload: # Remove "max_tokens" from the payload @@ -621,10 +621,10 @@ async def generate_chat_completion( url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] - # Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" - is_o1 = payload["model"].lower().startswith("o1-") - if is_o1: - payload = openai_o1_handler(payload) + # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" + is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-")) + if is_o1_o3: + payload = openai_o1_o3_handler(payload) elif "api.openai.com" not in url: # Remove "max_completion_tokens" from the payload for backward compatibility if "max_completion_tokens" in payload: diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 77f04a4be5..ff61f2cead 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -392,6 +392,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } @@ -441,6 +442,7 @@ class WebSearchConfig(BaseModel): exa_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None + domain_filter_list: Optional[List[str]] = [] class WebConfig(BaseModel): @@ -553,6 +555,9 @@ async def update_rag_config( request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests ) + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = ( + form_data.web.search.domain_filter_list + ) return { "status": True, @@ -599,6 +604,7 @@ async def update_rag_config( "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 0c0a8aacfc..b03cf0a7ec 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -10,6 +10,7 @@ from open_webui.config import ( S3_ACCESS_KEY_ID, S3_BUCKET_NAME, S3_ENDPOINT_URL, + S3_KEY_PREFIX, S3_REGION_NAME, S3_SECRET_ACCESS_KEY, GCS_BUCKET_NAME, @@ -93,15 +94,17 @@ class S3StorageProvider(StorageProvider): aws_secret_access_key=S3_SECRET_ACCESS_KEY, ) self.bucket_name = S3_BUCKET_NAME + self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]: """Handles uploading of the file to S3 storage.""" _, file_path = LocalStorageProvider.upload_file(file, filename) try: - self.s3_client.upload_file(file_path, self.bucket_name, filename) + s3_key = os.path.join(self.key_prefix, filename) + self.s3_client.upload_file(file_path, self.bucket_name, s3_key) return ( open(file_path, "rb").read(), - "s3://" + self.bucket_name + "/" + filename, + "s3://" + self.bucket_name + "/" + s3_key, ) except ClientError as e: raise RuntimeError(f"Error uploading file to S3: {e}") @@ -109,18 +112,18 @@ class S3StorageProvider(StorageProvider): def get_file(self, file_path: str) -> str: """Handles downloading of the file from S3 storage.""" try: - bucket_name, key = file_path.split("//")[1].split("/") - local_file_path = f"{UPLOAD_DIR}/{key}" - self.s3_client.download_file(bucket_name, key, local_file_path) + s3_key = self._extract_s3_key(file_path) + local_file_path = self._get_local_file_path(s3_key) + self.s3_client.download_file(self.bucket_name, s3_key, local_file_path) return local_file_path except ClientError as e: raise RuntimeError(f"Error downloading file from S3: {e}") def delete_file(self, file_path: str) -> None: """Handles deletion of the file from S3 storage.""" - filename = file_path.split("/")[-1] try: - self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename) + s3_key = self._extract_s3_key(file_path) + self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) except ClientError as e: raise RuntimeError(f"Error deleting file from S3: {e}") @@ -133,6 +136,10 @@ class S3StorageProvider(StorageProvider): response = self.s3_client.list_objects_v2(Bucket=self.bucket_name) if "Contents" in response: for content in response["Contents"]: + # Skip objects that were not uploaded from open-webui in the first place + if not content["Key"].startswith(self.key_prefix): + continue + self.s3_client.delete_object( Bucket=self.bucket_name, Key=content["Key"] ) @@ -142,6 +149,13 @@ class S3StorageProvider(StorageProvider): # Always delete from local storage LocalStorageProvider.delete_all_files() + # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. + def _extract_s3_key(self, full_file_path: str) -> str: + return "/".join(full_file_path.split("//")[1].split("/")[1:]) + + def _get_local_file_path(self, s3_key: str) -> str: + return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}" + class GCSStorageProvider(StorageProvider): def __init__(self): diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 0719f6af5b..3b6d5ea042 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -44,6 +44,10 @@ from open_webui.utils.response import ( convert_response_ollama_to_openai, convert_streaming_response_ollama_to_openai, ) +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL @@ -177,116 +181,38 @@ async def chat_completed(request: Request, form_data: dict, user: Any): except Exception as e: return Exception(f"Error: {e}") - __event_emitter__ = get_event_emitter( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + metadata = { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + "user_id": user.id, + } - __event_call__ = get_event_call( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + extra_params = { + "__event_emitter__": get_event_emitter(metadata), + "__event_call__": get_event_call(metadata), + "__user__": { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + }, + "__metadata__": metadata, + "__request__": request, + "__model__": model, + } - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel to include vavles - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [function.id for function in Functions.get_global_filter_functions()] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - # Sort filter_ids by priority, using the get_priority function - filter_ids.sort(key=get_priority) - - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if not hasattr(function_module, "outlet"): - continue - try: - outlet = function_module.outlet - - # Get the signature of the function - sig = inspect.signature(outlet) - params = {"body": data} - - # Extra parameters to be passed to the function - extra_params = { - "__model__": model, - "__id__": filter_id, - "__event_emitter__": __event_emitter__, - "__event_call__": __event_call__, - "__request__": request, - } - - # Add extra params in contained in function signature - for key, value in extra_params.items(): - if key in sig.parameters: - params[key] = value - - if "__user__" in sig.parameters: - __user__ = { - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - } - - try: - if hasattr(function_module, "UserValves"): - __user__["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, user.id - ) - ) - except Exception as e: - print(e) - - params = {**params, "__user__": __user__} - - if inspect.iscoroutinefunction(outlet): - data = await outlet(**params) - else: - data = outlet(**params) - - except Exception as e: - return Exception(f"Error: {e}") - - return data + try: + result, _ = await process_filter_functions( + request=request, + filter_ids=get_sorted_filter_ids(model), + filter_type="outlet", + form_data=data, + extra_params=extra_params, + ) + return result + except Exception as e: + return Exception(f"Error: {e}") async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): diff --git a/backend/open_webui/utils/code_interpreter.py b/backend/open_webui/utils/code_interpreter.py new file mode 100644 index 0000000000..34daa71c96 --- /dev/null +++ b/backend/open_webui/utils/code_interpreter.py @@ -0,0 +1,153 @@ +import asyncio +import json +import uuid +import websockets +import requests +from urllib.parse import urljoin + + +async def execute_code_jupyter( + jupyter_url, code, token=None, password=None, timeout=10 +): + """ + Executes Python code in a Jupyter kernel. + Supports authentication with a token or password. + :param jupyter_url: Jupyter server URL (e.g., "http://localhost:8888") + :param code: Code to execute + :param token: Jupyter authentication token (optional) + :param password: Jupyter password (optional) + :param timeout: WebSocket timeout in seconds (default: 10s) + :return: Dictionary with stdout, stderr, and result + """ + session = requests.Session() # Maintain cookies + headers = {} # Headers for requests + + # Authenticate using password + if password and not token: + try: + login_url = urljoin(jupyter_url, "/login") + response = session.get(login_url) + response.raise_for_status() + + # Retrieve `_xsrf` token + xsrf_token = session.cookies.get("_xsrf") + if not xsrf_token: + raise ValueError("Failed to fetch _xsrf token") + + # Send login request + login_data = {"_xsrf": xsrf_token, "password": password} + login_response = session.post( + login_url, data=login_data, cookies=session.cookies + ) + login_response.raise_for_status() + + # Update headers with `_xsrf` + headers["X-XSRFToken"] = xsrf_token + except Exception as e: + return { + "stdout": "", + "stderr": f"Authentication Error: {str(e)}", + "result": "", + } + + # Construct API URLs with authentication token if provided + params = f"?token={token}" if token else "" + kernel_url = urljoin(jupyter_url, f"/api/kernels{params}") + + try: + # Include cookies if authenticating with password + response = session.post(kernel_url, headers=headers, cookies=session.cookies) + response.raise_for_status() + kernel_id = response.json()["id"] + + # Construct WebSocket URL + websocket_url = urljoin( + jupyter_url.replace("http", "ws"), + f"/api/kernels/{kernel_id}/channels{params}", + ) + + # **IMPORTANT:** Include authentication cookies for WebSockets + ws_headers = {} + if password and not token: + ws_headers["X-XSRFToken"] = session.cookies.get("_xsrf") + cookies = {name: value for name, value in session.cookies.items()} + ws_headers["Cookie"] = "; ".join( + [f"{name}={value}" for name, value in cookies.items()] + ) + + # Connect to the WebSocket + async with websockets.connect( + websocket_url, additional_headers=ws_headers + ) as ws: + msg_id = str(uuid.uuid4()) + + # Send execution request + execute_request = { + "header": { + "msg_id": msg_id, + "msg_type": "execute_request", + "username": "user", + "session": str(uuid.uuid4()), + "date": "", + "version": "5.3", + }, + "parent_header": {}, + "metadata": {}, + "content": { + "code": code, + "silent": False, + "store_history": True, + "user_expressions": {}, + "allow_stdin": False, + "stop_on_error": True, + }, + "channel": "shell", + } + await ws.send(json.dumps(execute_request)) + + # Collect execution results + stdout, stderr, result = "", "", None + while True: + try: + message = await asyncio.wait_for(ws.recv(), timeout) + message_data = json.loads(message) + if message_data.get("parent_header", {}).get("msg_id") == msg_id: + msg_type = message_data.get("msg_type") + if msg_type == "stream": + if message_data["content"]["name"] == "stdout": + stdout += message_data["content"]["text"] + elif message_data["content"]["name"] == "stderr": + stderr += message_data["content"]["text"] + elif msg_type in ("execute_result", "display_data"): + result = message_data["content"]["data"].get( + "text/plain", "" + ) + elif msg_type == "error": + stderr += "\n".join(message_data["content"]["traceback"]) + elif ( + msg_type == "status" + and message_data["content"]["execution_state"] == "idle" + ): + break + except asyncio.TimeoutError: + stderr += "\nExecution timed out." + break + except Exception as e: + return {"stdout": "", "stderr": f"Error: {str(e)}", "result": ""} + finally: + # Shutdown the kernel + if kernel_id: + requests.delete( + f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies + ) + + return { + "stdout": stdout.strip(), + "stderr": stderr.strip(), + "result": result.strip() if result else "", + } + + +# Example Usage +# asyncio.run(execute_code_jupyter("http://localhost:8888", "print('Hello, world!')", token="your-token")) +# asyncio.run(execute_code_jupyter("http://localhost:8888", "print('Hello, world!')", password="your-password")) diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py new file mode 100644 index 0000000000..de51bd46e5 --- /dev/null +++ b/backend/open_webui/utils/filter.py @@ -0,0 +1,99 @@ +import inspect +from open_webui.utils.plugin import load_function_module_by_id +from open_webui.models.functions import Functions + + +def get_sorted_filter_ids(model): + def get_priority(function_id): + function = Functions.get_function_by_id(function_id) + if function is not None and hasattr(function, "valves"): + # TODO: Fix FunctionModel to include vavles + return (function.valves if function.valves else {}).get("priority", 0) + return 0 + + filter_ids = [function.id for function in Functions.get_global_filter_functions()] + if "info" in model and "meta" in model["info"]: + filter_ids.extend(model["info"]["meta"].get("filterIds", [])) + filter_ids = list(set(filter_ids)) + + enabled_filter_ids = [ + function.id + for function in Functions.get_functions_by_type("filter", active_only=True) + ] + + filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] + filter_ids.sort(key=get_priority) + return filter_ids + + +async def process_filter_functions( + request, filter_ids, filter_type, form_data, extra_params +): + skip_files = None + + for filter_id in filter_ids: + filter = Functions.get_function_by_id(filter_id) + if not filter: + continue + + if filter_id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[filter_id] + else: + function_module, _, _ = load_function_module_by_id(filter_id) + request.app.state.FUNCTIONS[filter_id] = function_module + + # Check if the function has a file_handler variable + if filter_type == "inlet" and hasattr(function_module, "file_handler"): + skip_files = function_module.file_handler + + # Apply valves to the function + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + # Prepare handler function + handler = getattr(function_module, filter_type, None) + if not handler: + continue + + try: + # Prepare parameters + sig = inspect.signature(handler) + params = {"body": form_data} | { + k: v + for k, v in { + **extra_params, + "__id__": filter_id, + }.items() + if k in sig.parameters + } + + # Handle user parameters + if "__user__" in sig.parameters: + if hasattr(function_module, "UserValves"): + try: + params["__user__"]["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + filter_id, params["__user__"]["id"] + ) + ) + except Exception as e: + print(e) + + # Execute handler + if inspect.iscoroutinefunction(handler): + form_data = await handler(**params) + else: + form_data = handler(**params) + + except Exception as e: + print(f"Error in {filter_type} handler {filter_id}: {e}") + raise e + + # Handle file cleanup for inlet + if skip_files and "files" in form_data.get("metadata", {}): + del form_data["metadata"]["files"] + + return form_data, {} diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py index 679fff9f64..b86c257591 100644 --- a/backend/open_webui/utils/images/comfyui.py +++ b/backend/open_webui/utils/images/comfyui.py @@ -161,7 +161,7 @@ async def comfyui_generate_image( seed = ( payload.seed if payload.seed - else random.randint(0, 18446744073709551614) + else random.randint(0, 1125899906842624) ) for node_id in node.node_ids: workflow[node_id]["inputs"][node.key] = seed diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 06763483cb..aba2a5e4fe 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -68,7 +68,11 @@ from open_webui.utils.misc import ( ) from open_webui.utils.tools import get_tools from open_webui.utils.plugin import load_function_module_by_id - +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) +from open_webui.utils.code_interpreter import execute_code_jupyter from open_webui.tasks import create_task @@ -91,99 +95,6 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) -async def chat_completion_filter_functions_handler(request, body, model, extra_params): - skip_files = None - - def get_filter_function_ids(model): - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [ - function.id for function in Functions.get_global_filter_functions() - ] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - filter_ids.sort(key=get_priority) - return filter_ids - - filter_ids = get_filter_function_ids(model) - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - # Check if the function has a file_handler variable - if hasattr(function_module, "file_handler"): - skip_files = function_module.file_handler - - # Apply valves to the function - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if hasattr(function_module, "inlet"): - try: - inlet = function_module.inlet - - # Create a dictionary of parameters to be passed to the function - params = {"body": body} | { - k: v - for k, v in { - **extra_params, - "__model__": model, - "__id__": filter_id, - }.items() - if k in inspect.signature(inlet).parameters - } - - if "__user__" in params and hasattr(function_module, "UserValves"): - try: - params["__user__"]["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, params["__user__"]["id"] - ) - ) - except Exception as e: - print(e) - - if inspect.iscoroutinefunction(inlet): - body = await inlet(**params) - else: - body = inlet(**params) - - except Exception as e: - print(f"Error: {e}") - raise e - - if skip_files and "files" in body.get("metadata", {}): - del body["metadata"]["files"] - - return body, {} - - async def chat_completion_tools_handler( request: Request, body: dict, user: UserModel, models, tools ) -> tuple[dict, dict]: @@ -572,13 +483,13 @@ async def chat_image_generation_handler( { "type": "status", "data": { - "description": f"An error occured while generating an image", + "description": f"An error occurred while generating an image", "done": True, }, } ) - system_message_content = "Unable to generate an image, tell the user that an error occured" + system_message_content = "Unable to generate an image, tell the user that an error occurred" if system_message_content: form_data["messages"] = add_or_update_system_message( @@ -706,6 +617,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): }, "__metadata__": metadata, "__request__": request, + "__model__": model, } # Initialize events to store additional event to be sent to the client @@ -782,8 +694,12 @@ async def process_chat_payload(request, form_data, metadata, user, model): ) try: - form_data, flags = await chat_completion_filter_functions_handler( - request, form_data, model, extra_params + form_data, flags = await process_filter_functions( + request=request, + filter_ids=get_sorted_filter_ids(model), + filter_type="inlet", + form_data=form_data, + extra_params=extra_params, ) except Exception as e: raise Exception(f"Error: {e}") @@ -1122,6 +1038,20 @@ async def process_chat_response( }, ) + def split_content_and_whitespace(content): + content_stripped = content.rstrip() + original_whitespace = ( + content[len(content_stripped) :] + if len(content) > len(content_stripped) + else "" + ) + return content_stripped, original_whitespace + + def is_opening_code_block(content): + backtick_segments = content.split("```") + # Even number of segments means the last backticks are opening a new block + return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 + # Handle as a background task async def post_response_handler(response, events): def serialize_content_blocks(content_blocks, raw=False): @@ -1188,6 +1118,19 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") + content_stripped, original_whitespace = ( + split_content_and_whitespace(content) + ) + if is_opening_code_block(content_stripped): + # Remove trailing backticks that would open a new block + content = ( + content_stripped.rstrip("`").rstrip() + + original_whitespace + ) + else: + # Keep content as is - either closing backticks or no backticks + content = content_stripped + original_whitespace + if output: output = html.escape(json.dumps(output)) @@ -1242,10 +1185,10 @@ async def process_chat_response( match.end() : ] # Content after opening tag - # Remove the start tag from the currently handling text block + # Remove the start tag and after from the currently handling text block content_blocks[-1]["content"] = content_blocks[-1][ "content" - ].replace(match.group(0), "") + ].replace(match.group(0) + after_tag, "") if before_tag: content_blocks[-1]["content"] = before_tag @@ -1708,15 +1651,45 @@ async def process_chat_response( output = "" try: if content_blocks[-1]["attributes"].get("type") == "code": - output = await event_caller( - { - "type": "execute:python", - "data": { - "id": str(uuid4()), - "code": content_blocks[-1]["content"], - }, + code = content_blocks[-1]["content"] + + if ( + request.app.state.config.CODE_INTERPRETER_ENGINE + == "pyodide" + ): + output = await event_caller( + { + "type": "execute:python", + "data": { + "id": str(uuid4()), + "code": code, + }, + } + ) + elif ( + request.app.state.config.CODE_INTERPRETER_ENGINE + == "jupyter" + ): + output = await execute_code_jupyter( + request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + code, + ( + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH + == "token" + else None + ), + ( + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH + == "password" + else None + ), + ) + else: + output = { + "stdout": "Code interpreter engine not configured." } - ) if isinstance(output, dict): stdout = output.get("stdout", "") diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index b073939219..99e6d9c392 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -244,11 +244,12 @@ def get_gravatar_url(email): return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp" -def calculate_sha256(file): +def calculate_sha256(file_path, chunk_size): + # Compute SHA-256 hash of a file efficiently in chunks sha256 = hashlib.sha256() - # Read the file in chunks to efficiently handle large files - for chunk in iter(lambda: file.read(8192), b""): - sha256.update(chunk) + with open(file_path, "rb") as f: + while chunk := f.read(chunk_size): + sha256.update(chunk) return sha256.hexdigest() diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 20c112ebbf..463f67adcc 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1,6 +1,7 @@ import base64 import logging import mimetypes +import sys import uuid import aiohttp @@ -40,7 +41,11 @@ from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook +from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OAUTH"]) auth_manager_config = AppConfig() auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE @@ -72,12 +77,15 @@ class OAuthManager: def get_user_role(self, user, user_data): if user and Users.get_num_users() == 1: # If the user is the only user, assign the role "admin" - actually repairs role for single user on login + log.debug("Assigning the only user the admin role") return "admin" if not user and Users.get_num_users() == 0: # If there are no users, assign the role "admin", as the first user will be an admin + log.debug("Assigning the first user the admin role") return "admin" if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT: + log.debug("Running OAUTH Role management") oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES @@ -93,17 +101,24 @@ class OAuthManager: claim_data = claim_data.get(nested_claim, {}) oauth_roles = claim_data if isinstance(claim_data, list) else None + log.debug(f"Oauth Roles claim: {oauth_claim}") + log.debug(f"User roles from oauth: {oauth_roles}") + log.debug(f"Accepted user roles: {oauth_allowed_roles}") + log.debug(f"Accepted admin roles: {oauth_admin_roles}") + # If any roles are found, check if they match the allowed or admin roles if oauth_roles: # If role management is enabled, and matching roles are provided, use the roles for allowed_role in oauth_allowed_roles: # If the user has any of the allowed roles, assign the role "user" if allowed_role in oauth_roles: + log.debug("Assigned user the user role") role = "user" break for admin_role in oauth_admin_roles: # If the user has any of the admin roles, assign the role "admin" if admin_role in oauth_roles: + log.debug("Assigned user the admin role") role = "admin" break else: @@ -117,16 +132,27 @@ class OAuthManager: return role def update_user_groups(self, user, user_data, default_permissions): + log.debug("Running OAUTH Group management") oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM user_oauth_groups: list[str] = user_data.get(oauth_claim, list()) user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) all_available_groups: list[GroupModel] = Groups.get_groups() + log.debug(f"Oauth Groups claim: {oauth_claim}") + log.debug(f"User oauth groups: {user_oauth_groups}") + log.debug(f"User's current groups: {[g.name for g in user_current_groups]}") + log.debug( + f"All groups available in OpenWebUI: {[g.name for g in all_available_groups]}" + ) + # Remove groups that user is no longer a part of for group_model in user_current_groups: if group_model.name not in user_oauth_groups: # Remove group from user + log.debug( + f"Removing user from group {group_model.name} as it is no longer in their oauth groups" + ) user_ids = group_model.user_ids user_ids = [i for i in user_ids if i != user.id] @@ -152,6 +178,9 @@ class OAuthManager: gm.name == group_model.name for gm in user_current_groups ): # Add user to group + log.debug( + f"Adding user to group {group_model.name} as it was found in their oauth groups" + ) user_ids = group_model.user_ids user_ids.append(user.id) @@ -193,7 +222,7 @@ class OAuthManager: log.warning(f"OAuth callback error: {e}") raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) user_data: UserInfo = token.get("userinfo") - if not user_data: + if not user_data or "email" not in user_data: user_data: UserInfo = await client.userinfo(token=token) if not user_data: log.warning(f"OAuth callback failed, user data is missing: {token}") @@ -261,15 +290,20 @@ class OAuthManager: } async with aiohttp.ClientSession() as session: async with session.get(picture_url, **get_kwargs) as resp: - picture = await resp.read() - base64_encoded_picture = base64.b64encode( - picture - ).decode("utf-8") - guessed_mime_type = mimetypes.guess_type(picture_url)[0] - if guessed_mime_type is None: - # assume JPG, browsers are tolerant enough of image formats - guessed_mime_type = "image/jpeg" - picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + if resp.ok: + picture = await resp.read() + base64_encoded_picture = base64.b64encode( + picture + ).decode("utf-8") + guessed_mime_type = mimetypes.guess_type( + picture_url + )[0] + if guessed_mime_type is None: + # assume JPG, browsers are tolerant enough of image formats + guessed_mime_type = "image/jpeg" + picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + else: + picture_url = "/user.png" except Exception as e: log.error( f"Error downloading profile image '{picture_url}': {e}" diff --git a/backend/open_webui/utils/pdf_generator.py b/backend/open_webui/utils/pdf_generator.py index 1bb9f76b30..8b04dd81bc 100644 --- a/backend/open_webui/utils/pdf_generator.py +++ b/backend/open_webui/utils/pdf_generator.py @@ -2,6 +2,7 @@ from datetime import datetime from io import BytesIO from pathlib import Path from typing import Dict, Any, List +from html import escape from markdown import markdown @@ -41,13 +42,13 @@ class PDFGenerator: def _build_html_message(self, message: Dict[str, Any]) -> str: """Build HTML for a single message.""" - role = message.get("role", "user") - content = message.get("content", "") + role = escape(message.get("role", "user")) + content = escape(message.get("content", "")) timestamp = message.get("timestamp") - model = message.get("model") if role == "assistant" else "" + model = escape(message.get("model") if role == "assistant" else "") - date_str = self.format_timestamp(timestamp) if timestamp else "" + date_str = escape(self.format_timestamp(timestamp) if timestamp else "") # extends pymdownx extension to convert markdown to html. # - https://facelessuser.github.io/pymdown-extensions/usage_notes/ @@ -76,6 +77,7 @@ class PDFGenerator: def _generate_html_body(self) -> str: """Generate the full HTML body for the PDF.""" + escaped_title = escape(self.form_data.title) return f""" @@ -84,7 +86,7 @@ class PDFGenerator:
-

{self.form_data.title}

+

{escaped_title}

{self.messages_html}
diff --git a/backend/requirements.txt b/backend/requirements.txt index 14ad4b9cdf..ba277a071d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -32,6 +32,8 @@ boto3==1.35.53 argon2-cffi==23.1.0 APScheduler==3.10.4 +RestrictedPython==8.0 + # AI libraries openai anthropic @@ -45,7 +47,7 @@ fake-useragent==1.5.1 chromadb==0.6.2 pymilvus==2.5.0 qdrant-client~=1.12.0 -opensearch-py==2.7.1 +opensearch-py==2.8.0 transformers @@ -77,7 +79,7 @@ opencv-python-headless==4.11.0.86 rapidocr-onnxruntime==1.3.24 rank-bm25==0.2.2 -faster-whisper==1.0.3 +faster-whisper==1.1.1 PyJWT[crypto]==2.10.1 authlib==1.4.1 diff --git a/package-lock.json b/package-lock.json index e079e15364..e5c18101bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -62,7 +63,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "devDependencies": { "@sveltejs/adapter-auto": "3.2.2", @@ -91,7 +93,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "engines": { "node": ">=18.13.0 <=22.x.x", @@ -1078,21 +1080,23 @@ } }, "node_modules/@huggingface/jinja": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.1.tgz", - "integrity": "sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.3.tgz", + "integrity": "sha512-vQQr2JyWvVFba3Lj9es4q9vCl1sAc74fdgnEMoX8qHrXtswap9ge9uO3ONDzQB0cQ0PUyaKY2N6HaVbTBvSXvw==", + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@huggingface/transformers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.0.0.tgz", - "integrity": "sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.3.3.tgz", + "integrity": "sha512-OcMubhBjW6u1xnp0zSt5SvCxdGHuhP2k+w2Vlm3i0vNcTJhJTZWxxYQmPBfcb7PX+Q6c43lGSzWD6tsJFwka4Q==", + "license": "Apache-2.0", "dependencies": { - "@huggingface/jinja": "^0.3.0", - "onnxruntime-node": "1.19.2", - "onnxruntime-web": "1.20.0-dev.20241016-2b8fc5529b", + "@huggingface/jinja": "^0.3.3", + "onnxruntime-node": "1.20.1", + "onnxruntime-web": "1.21.0-dev.20250206-d981b153d3", "sharp": "^0.33.5" } }, @@ -1546,6 +1550,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -1558,6 +1563,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -1798,7 +1804,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1811,7 +1816,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1820,7 +1824,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1856,27 +1859,32 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1885,27 +1893,32 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" }, "node_modules/@pyscript/core": { "version": "0.4.32", @@ -2210,7 +2223,8 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", @@ -3146,13 +3160,14 @@ "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "chai": "^4.3.10" }, "funding": { @@ -3160,12 +3175,13 @@ } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.0", + "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3178,6 +3194,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^1.0.0" }, @@ -3189,10 +3206,11 @@ } }, "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" }, @@ -3201,10 +3219,11 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "dev": true, + "license": "MIT", "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", @@ -3215,10 +3234,11 @@ } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^2.2.0" }, @@ -3227,10 +3247,11 @@ } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", "dev": true, + "license": "MIT", "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", @@ -3246,6 +3267,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -3416,7 +3438,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3496,6 +3517,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -3644,7 +3666,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "engines": { "node": ">=8" }, @@ -3720,7 +3741,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -3895,6 +3915,7 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3972,10 +3993,11 @@ "dev": true }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -3983,7 +4005,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -4019,6 +4041,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -4077,7 +4100,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4101,7 +4123,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4113,6 +4134,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -5135,10 +5157,11 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -5257,6 +5280,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -5899,7 +5923,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -5915,7 +5938,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5939,7 +5961,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -5998,7 +6019,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6037,9 +6057,10 @@ } }, "node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==" + "version": "25.1.24", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.1.24.tgz", + "integrity": "sha512-Ni+KCqYquU30UEgGkrrwpbYtUcUmNuLFcQ5Xdy9DK7WUaji+AAov+Bf12FEYmu0eI15y31oD38utnBexe0cAYA==", + "license": "Apache-2.0" }, "node_modules/flatted": { "version": "3.3.1", @@ -6110,7 +6131,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6238,6 +6258,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -6429,8 +6450,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -6441,7 +6461,8 @@ "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" }, "node_modules/gulp-sort": { "version": "2.0.0", @@ -6809,7 +6830,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6858,7 +6878,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6875,7 +6894,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6917,7 +6935,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -7097,7 +7114,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -7172,6 +7188,16 @@ "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true }, + "node_modules/kokoro-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.1.1.tgz", + "integrity": "sha512-cyLO34iI8nBJXPnd3fI4fGeQGS+a6Uatg7eXNL6QS8TLSxaa30WD6Fj7/XoIZYaHg8q6d+TCrui/f74MTY2g1g==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/transformers": "^3.3.3", + "phonemizer": "^1.2.1" + } + }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -7455,15 +7481,17 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -7609,7 +7637,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -8066,7 +8093,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8150,6 +8176,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "license": "MIT", "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" @@ -8162,6 +8189,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -8181,6 +8209,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8195,6 +8224,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8209,6 +8239,7 @@ "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, @@ -8329,7 +8360,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8433,42 +8463,46 @@ } }, "node_modules/onnxruntime-common": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.19.2.tgz", - "integrity": "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q==" + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz", + "integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==", + "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.19.2.tgz", - "integrity": "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz", + "integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==", "hasInstallScript": true, + "license": "MIT", "os": [ "win32", "darwin", "linux" ], "dependencies": { - "onnxruntime-common": "1.19.2", + "onnxruntime-common": "1.20.1", "tar": "^7.0.1" } }, "node_modules/onnxruntime-web": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ==", + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-esDVQdRic6J44VBMFLumYvcGfioMh80ceLmzF1yheJyuLKq/Th8VT2aj42XWQst+2bcWnAhw4IKmRQaqzU8ugg==", + "license": "MIT", "dependencies": { - "flatbuffers": "^1.12.0", + "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", - "onnxruntime-common": "1.20.0-dev.20241016-2b8fc5529b", + "onnxruntime-common": "1.21.0-dev.20250206-d981b153d3", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ==" + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-TwaE51xV9q2y8pM61q73rbywJnusw9ivTEHAJ39GVWNZqxCoDBpe/tQkh/w9S+o/g+zS7YeeL0I/2mEWd+dgyA==", + "license": "MIT" }, "node_modules/optionator": { "version": "0.9.3", @@ -8546,7 +8580,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" }, "node_modules/paneforge": { "version": "0.0.6", @@ -8686,6 +8721,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -8728,6 +8764,12 @@ "@types/estree": "*" } }, + "node_modules/phonemizer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz", + "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==", + "license": "Apache-2.0" + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -8781,7 +8823,8 @@ "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" }, "node_modules/polyscript": { "version": "0.12.8", @@ -9064,6 +9107,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -9078,6 +9122,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9314,6 +9359,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -9413,7 +9459,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -9504,7 +9549,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -9534,7 +9580,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9637,7 +9682,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -9763,7 +9807,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11131,6 +11174,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -11147,6 +11191,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -11247,6 +11292,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -11277,7 +11323,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -11406,10 +11451,11 @@ "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11484,7 +11530,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -11749,10 +11794,11 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", @@ -11770,6 +11816,24 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.2.0.tgz", + "integrity": "sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -12166,16 +12230,17 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -12189,7 +12254,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.6.0", + "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -12204,8 +12269,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, @@ -12567,6 +12632,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } diff --git a/package.json b/package.json index 02228ee8bc..aa43f6a754 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "type": "module", "dependencies": { @@ -85,6 +85,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -105,7 +106,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "engines": { "node": ">=18.13.0 <=22.x.x", diff --git a/pyproject.toml b/pyproject.toml index f121089e8f..03c5c00052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ dependencies = [ "argon2-cffi==23.1.0", "APScheduler==3.10.4", + + "RestrictedPython==8.0", + "openai", "anthropic", "google-generativeai==0.7.2", @@ -82,7 +85,7 @@ dependencies = [ "rapidocr-onnxruntime==1.3.24", "rank-bm25==0.2.2", - "faster-whisper==1.0.3", + "faster-whisper==1.1.1", "PyJWT[crypto]==2.10.1", "authlib==1.4.1", diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index e9faf346bc..999842b269 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -58,6 +58,63 @@ export const exportConfig = async (token: string) => { return res; }; +export const getCodeInterpreterConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { + 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.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setCodeInterpreterConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(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/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index e43081302c..e73adb027b 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -65,7 +65,7 @@ }; const shareHandler = async () => { - toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + toast.success($i18n.t('Redirecting you to Open WebUI Community')); // remove snapshot from feedbacks const feedbacksToShare = feedbacks.map((f) => { @@ -266,7 +266,7 @@ }} >
- {$i18n.t('Share to OpenWebUI Community')} + {$i18n.t('Share to Open WebUI Community')}
diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index 59f6df916a..07e19e9792 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -1,6 +1,8 @@ + +
{ + await submitHandler(); + saveHandler(); + }} +> +
+ {#if config} +
+
+ {$i18n.t('Code Interpreter')} +
+ +
+
+
+ {$i18n.t('Enable Code Interpreter')} +
+ + +
+
+ +
+
{$i18n.t('Code Interpreter Engine')}
+
+ +
+
+ + {#if config.CODE_INTERPRETER_ENGINE === 'jupyter'} +
+
+ {$i18n.t('Jupyter Kernel Gateway URL')} +
+ +
+
+ +
+
+
+ +
+
+ {$i18n.t('Jupyter Kernel Gateway Auth')} +
+ +
+ +
+
+ + {#if config.CODE_INTERPRETER_JUPYTER_AUTH} +
+
+ {#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'} + + {:else} + + {/if} +
+
+ {/if} + {/if} +
+ + + {/if} +
+
+ +
+
diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte index 35e6e0293a..2d7c8675bb 100644 --- a/src/lib/components/admin/Settings/Connections.svelte +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -101,14 +101,17 @@ const addOpenAIConnectionHandler = async (connection) => { OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url]; OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key]; - OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config; + OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config; await updateOpenAIHandler(); }; const addOllamaConnectionHandler = async (connection) => { OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url]; - OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config; + OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = { + ...connection.config, + key: connection.key + }; await updateOllamaHandler(); }; @@ -244,7 +247,11 @@ ); OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx); - delete OPENAI_API_CONFIGS[idx]; + let newConfig = {}; + OPENAI_API_BASE_URLS.forEach((url, newIdx) => { + newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; + }); + OPENAI_API_CONFIGS = newConfig; }} /> {/each} @@ -302,7 +309,12 @@ }} onDelete={() => { OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx); - delete OLLAMA_API_CONFIGS[idx]; + + let newConfig = {}; + OLLAMA_BASE_URLS.forEach((url, newIdx) => { + newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; + }); + OLLAMA_API_CONFIGS = newConfig; }} /> {/each} diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index b2accbf1d7..f1482e6a25 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -34,6 +34,16 @@ let youtubeProxyUrl = ''; const submitHandler = async () => { + // Convert domain filter string to array before sending + if (webConfig.search.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list + .split(',') + .map((domain) => domain.trim()) + .filter((domain) => domain.length > 0); + } else { + webConfig.search.domain_filter_list = []; + } + const res = await updateRAGConfig(localStorage.token, { web: webConfig, youtube: { @@ -49,6 +59,10 @@ if (res) { webConfig = res.web; + // Convert array back to comma-separated string for display + if (webConfig?.search?.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', '); + } youtubeLanguage = res.youtube.language.join(','); youtubeTranslation = res.youtube.translation; @@ -334,6 +348,20 @@ />
+ +
+
+ {$i18n.t('Domain Filter List')} +
+ + +
{/if} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index cc14f2c6fe..2348d42fe4 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1556,7 +1556,8 @@ ? imageGenerationEnabled : false, code_interpreter: - $user.role === 'admin' || $user?.permissions?.features?.code_interpreter + $config?.features?.enable_code_interpreter && + ($user.role === 'admin' || $user?.permissions?.features?.code_interpreter) ? codeInterpreterEnabled : false, web_search: diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 41ff0b64be..3ab5d05fed 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -16,7 +16,8 @@ showCallOverlay, tools, user as _user, - showControls + showControls, + TTSWorker } from '$lib/stores'; import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils'; @@ -43,6 +44,7 @@ import PhotoSolid from '../icons/PhotoSolid.svelte'; import Photo from '../icons/Photo.svelte'; import CommandLine from '../icons/CommandLine.svelte'; + import { KokoroWorker } from '$lib/workers/KokoroWorker'; const i18n = getContext('i18n'); @@ -638,7 +640,7 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" - class="w-4 h-4" + class="size-4" > { if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) { toast.error($i18n.t('Please select a model first.')); @@ -1169,7 +1171,7 @@ {/if} - {#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter} + {#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)} {/each} -
+
{citations.length - 2} {$i18n.t('more')} @@ -167,7 +175,7 @@
-
+
{#each citations as citation, idx} diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index f4efaba734..7a17ee607d 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -5,7 +5,14 @@ import { v4 as uuidv4 } from 'uuid'; - import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte'; + import { + getContext, + getAllContexts, + onMount, + tick, + createEventDispatcher, + onDestroy + } from 'svelte'; import { copyToClipboard } from '$lib/utils'; import 'highlight.js/styles/github-dark.min.css'; @@ -31,6 +38,8 @@ export let editorClassName = ''; export let stickyButtonsClassName = 'top-8'; + let pyodideWorker = null; + let _code = ''; $: if (code) { updateCode(); @@ -138,7 +147,7 @@ console.log(packages); - const pyodideWorker = new PyodideWorker(); + pyodideWorker = new PyodideWorker(); pyodideWorker.postMessage({ id: id, @@ -280,6 +289,12 @@ }); } }); + + onDestroy(() => { + if (pyodideWorker) { + pyodideWorker.terminate(); + } + });
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index f6a4b0bc0a..95cefda56e 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -4,12 +4,18 @@ import { createEventDispatcher } from 'svelte'; import { onMount, tick, getContext } from 'svelte'; + import type { Writable } from 'svelte/store'; + import type { i18n as i18nType } from 'i18next'; const i18n = getContext>('i18n'); const dispatch = createEventDispatcher(); - import { config, models, settings, user } from '$lib/stores'; + import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations'; + import { getChatById } from '$lib/apis/chats'; + import { generateTags } from '$lib/apis'; + + import { config, models, settings, TTSWorker, user } from '$lib/stores'; import { synthesizeOpenAISpeech } from '$lib/apis/audio'; import { imageGenerations } from '$lib/apis/images'; import { @@ -34,13 +40,8 @@ import Error from './Error.svelte'; import Citations from './Citations.svelte'; import CodeExecutions from './CodeExecutions.svelte'; - - import type { Writable } from 'svelte/store'; - import type { i18n as i18nType } from 'i18next'; import ContentRenderer from './ContentRenderer.svelte'; - import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations'; - import { getChatById } from '$lib/apis/chats'; - import { generateTags } from '$lib/apis'; + import { KokoroWorker } from '$lib/workers/KokoroWorker'; interface MessageType { id: string; @@ -193,62 +194,7 @@ speaking = true; - if ($config.audio.tts.engine !== '') { - loadingSpeech = true; - - const messageContentParts: string[] = getMessageContentParts( - message.content, - $config?.audio?.tts?.split_on ?? 'punctuation' - ); - - if (!messageContentParts.length) { - console.log('No content to speak'); - toast.info($i18n.t('No content to speak')); - - speaking = false; - loadingSpeech = false; - return; - } - - console.debug('Prepared message content for TTS', messageContentParts); - - audioParts = messageContentParts.reduce( - (acc, _sentence, idx) => { - acc[idx] = null; - return acc; - }, - {} as typeof audioParts - ); - - let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately - - for (const [idx, sentence] of messageContentParts.entries()) { - const res = await synthesizeOpenAISpeech( - localStorage.token, - $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice - ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) - : $config?.audio?.tts?.voice, - sentence - ).catch((error) => { - console.error(error); - toast.error(`${error}`); - - speaking = false; - loadingSpeech = false; - }); - - if (res) { - const blob = await res.blob(); - const blobUrl = URL.createObjectURL(blob); - const audio = new Audio(blobUrl); - audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; - - audioParts[idx] = audio; - loadingSpeech = false; - lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); - } - } - } else { + if ($config.audio.tts.engine === '') { let voices = []; const getVoicesLoop = setInterval(() => { voices = speechSynthesis.getVoices(); @@ -283,6 +229,97 @@ speechSynthesis.speak(speak); } }, 100); + } else { + loadingSpeech = true; + + const messageContentParts: string[] = getMessageContentParts( + message.content, + $config?.audio?.tts?.split_on ?? 'punctuation' + ); + + if (!messageContentParts.length) { + console.log('No content to speak'); + toast.info($i18n.t('No content to speak')); + + speaking = false; + loadingSpeech = false; + return; + } + + console.debug('Prepared message content for TTS', messageContentParts); + + audioParts = messageContentParts.reduce( + (acc, _sentence, idx) => { + acc[idx] = null; + return acc; + }, + {} as typeof audioParts + ); + + let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately + + if ($settings.audio?.tts?.engine === 'browser-kokoro') { + if (!$TTSWorker) { + await TTSWorker.set( + new KokoroWorker({ + dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32' + }) + ); + + await $TTSWorker.init(); + } + + for (const [idx, sentence] of messageContentParts.entries()) { + const blob = await $TTSWorker + .generate({ + text: sentence, + voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice + }) + .catch((error) => { + console.error(error); + toast.error(`${error}`); + + speaking = false; + loadingSpeech = false; + }); + + if (blob) { + const audio = new Audio(blob); + audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; + + audioParts[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } + } + } else { + for (const [idx, sentence] of messageContentParts.entries()) { + const res = await synthesizeOpenAISpeech( + localStorage.token, + $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice + ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) + : $config?.audio?.tts?.voice, + sentence + ).catch((error) => { + console.error(error); + toast.error(`${error}`); + + speaking = false; + loadingSpeech = false; + }); + + if (res) { + const blob = await res.blob(); + const blobUrl = URL.createObjectURL(blob); + const audio = new Audio(blobUrl); + audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; + + audioParts[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } + } + } } }; diff --git a/src/lib/components/chat/Settings/Audio.svelte b/src/lib/components/chat/Settings/Audio.svelte index 3f9fa9335b..ce9cae7717 100644 --- a/src/lib/components/chat/Settings/Audio.svelte +++ b/src/lib/components/chat/Settings/Audio.svelte @@ -1,11 +1,14 @@
{$i18n.t('TTS Settings')}
+
+
{$i18n.t('Text-to-Speech Engine')}
+
+ +
+
+ + {#if TTSEngine === 'browser-kokoro'} +
+
{$i18n.t('Kokoro.js Dtype')}
+
+ +
+
+ {/if} +
{$i18n.t('Auto-playback response')}
@@ -178,7 +285,46 @@
- {#if $config.audio.tts.engine === ''} + {#if TTSEngine === 'browser-kokoro'} + {#if TTSModel} +
+
{$i18n.t('Set Voice')}
+
+
+ + + + {#each voices as voice} + + {/each} + +
+
+
+ {:else} +
+
+ + +
+ {$i18n.t('Loading Kokoro.js...')} + {TTSModelProgress && TTSModelProgress.status === 'progress' + ? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)` + : ''} +
+
+ +
+ {$i18n.t('Please do not close the settings page while loading the model.')} +
+
+ {/if} + {:else if $config.audio.tts.engine === ''}
{$i18n.t('Set Voice')}
diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index ce8ed80e22..cb7843c694 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -49,6 +49,7 @@ function_calling: null, seed: null, temperature: null, + reasoning_effort: null, frequency_penalty: null, repeat_last_n: null, mirostat: null, @@ -333,9 +334,13 @@ system: system !== '' ? system : undefined, params: { stream_response: params.stream_response !== null ? params.stream_response : undefined, + function_calling: + params.function_calling !== null ? params.function_calling : undefined, seed: (params.seed !== null ? params.seed : undefined) ?? undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, temperature: params.temperature !== null ? params.temperature : undefined, + reasoning_effort: + params.reasoning_effort !== null ? params.reasoning_effort : undefined, frequency_penalty: params.frequency_penalty !== null ? params.frequency_penalty : undefined, repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined, diff --git a/src/lib/components/chat/ShareChatModal.svelte b/src/lib/components/chat/ShareChatModal.svelte index b30c168d66..f3f6213248 100644 --- a/src/lib/components/chat/ShareChatModal.svelte +++ b/src/lib/components/chat/ShareChatModal.svelte @@ -30,7 +30,7 @@ const _chat = chat.chat; console.log('share', _chat); - toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + toast.success($i18n.t('Redirecting you to Open WebUI Community')); const url = 'https://openwebui.com'; // const url = 'http://localhost:5173'; @@ -143,7 +143,7 @@ show = false; }} > - {$i18n.t('Share to OpenWebUI Community')} + {$i18n.t('Share to Open WebUI Community')} {/if} diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte index 4f6b1ea958..4ee4c1b11e 100644 --- a/src/lib/components/common/Collapsible.svelte +++ b/src/lib/components/common/Collapsible.svelte @@ -74,9 +74,15 @@
{#if attributes?.type === 'reasoning'} {#if attributes?.done === 'true' && attributes?.duration} - {$i18n.t('Thought for {{DURATION}}', { - DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() - })} + {#if attributes.duration < 60} + {$i18n.t('Thought for {{DURATION}} seconds', { + DURATION: attributes.duration + })} + {:else} + {$i18n.t('Thought for {{DURATION}}', { + DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() + })} + {/if} {:else} {$i18n.t('Thinking...')} {/if} diff --git a/src/lib/components/common/SensitiveInput.svelte b/src/lib/components/common/SensitiveInput.svelte index cb6484a0ba..6b30bef536 100644 --- a/src/lib/components/common/SensitiveInput.svelte +++ b/src/lib/components/common/SensitiveInput.svelte @@ -23,6 +23,7 @@ />