瀏覽代碼

Merge branch 'dev' into fix-9864

Feynman Liang 2 月之前
父節點
當前提交
113addef46
共有 100 個文件被更改,包括 3745 次插入1476 次删除
  1. 1 1
      README.md
  2. 74 3
      backend/open_webui/config.py
  3. 1 0
      backend/open_webui/env.py
  4. 50 2
      backend/open_webui/main.py
  5. 1 1
      backend/open_webui/models/chats.py
  6. 18 0
      backend/open_webui/models/users.py
  7. 28 0
      backend/open_webui/retrieval/vector/dbs/opensearch.py
  8. 72 0
      backend/open_webui/retrieval/web/bocha.py
  9. 32 16
      backend/open_webui/retrieval/web/google_pse.py
  10. 13 4
      backend/open_webui/retrieval/web/jina_search.py
  11. 68 0
      backend/open_webui/routers/audio.py
  12. 92 0
      backend/open_webui/routers/configs.py
  13. 16 20
      backend/open_webui/routers/files.py
  14. 46 69
      backend/open_webui/routers/images.py
  15. 73 46
      backend/open_webui/routers/ollama.py
  16. 6 6
      backend/open_webui/routers/openai.py
  17. 24 0
      backend/open_webui/routers/retrieval.py
  18. 1 1
      backend/open_webui/routers/users.py
  19. 21 7
      backend/open_webui/storage/provider.py
  20. 35 109
      backend/open_webui/utils/chat.py
  21. 148 0
      backend/open_webui/utils/code_interpreter.py
  22. 99 0
      backend/open_webui/utils/filter.py
  23. 1 1
      backend/open_webui/utils/images/comfyui.py
  24. 128 109
      backend/open_webui/utils/middleware.py
  25. 5 4
      backend/open_webui/utils/misc.py
  26. 46 11
      backend/open_webui/utils/oauth.py
  27. 6 6
      backend/open_webui/utils/payload.py
  28. 7 5
      backend/open_webui/utils/pdf_generator.py
  29. 3 1
      backend/open_webui/utils/response.py
  30. 5 3
      backend/requirements.txt
  31. 186 120
      package-lock.json
  32. 4 2
      package.json
  33. 5 2
      pyproject.toml
  34. 114 0
      src/lib/apis/configs/index.ts
  35. 97 1
      src/lib/apis/index.ts
  36. 78 22
      src/lib/apis/openai/index.ts
  37. 7 3
      src/lib/components/AddConnectionModal.svelte
  38. 2 2
      src/lib/components/admin/Evaluations/Feedbacks.svelte
  39. 3 1
      src/lib/components/admin/Evaluations/Leaderboard.svelte
  40. 34 8
      src/lib/components/admin/Functions.svelte
  41. 36 0
      src/lib/components/admin/Settings.svelte
  42. 41 3
      src/lib/components/admin/Settings/Audio.svelte
  43. 166 0
      src/lib/components/admin/Settings/CodeInterpreter.svelte
  44. 78 17
      src/lib/components/admin/Settings/Connections.svelte
  45. 1 1
      src/lib/components/admin/Settings/Connections/OllamaConnection.svelte
  46. 2 1
      src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte
  47. 39 19
      src/lib/components/admin/Settings/Evaluations.svelte
  48. 20 4
      src/lib/components/admin/Settings/Models.svelte
  49. 25 5
      src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
  50. 25 5
      src/lib/components/admin/Settings/Pipelines.svelte
  51. 42 0
      src/lib/components/admin/Settings/WebSearch.svelte
  52. 12 9
      src/lib/components/chat/Chat.svelte
  53. 19 7
      src/lib/components/chat/MessageInput.svelte
  54. 17 2
      src/lib/components/chat/MessageInput/CallOverlay.svelte
  55. 20 11
      src/lib/components/chat/Messages/Citations.svelte
  56. 17 2
      src/lib/components/chat/Messages/CodeBlock.svelte
  57. 98 61
      src/lib/components/chat/Messages/ResponseMessage.svelte
  58. 207 201
      src/lib/components/chat/Messages/UserMessage.svelte
  59. 33 3
      src/lib/components/chat/ModelSelector/Selector.svelte
  60. 2 2
      src/lib/components/chat/Navbar.svelte
  61. 8 2
      src/lib/components/chat/Settings/Account.svelte
  62. 162 16
      src/lib/components/chat/Settings/Audio.svelte
  63. 152 0
      src/lib/components/chat/Settings/Connections.svelte
  64. 83 0
      src/lib/components/chat/Settings/Connections/Connection.svelte
  65. 5 0
      src/lib/components/chat/Settings/General.svelte
  66. 44 2
      src/lib/components/chat/SettingsModal.svelte
  67. 2 2
      src/lib/components/chat/ShareChatModal.svelte
  68. 9 3
      src/lib/components/common/Collapsible.svelte
  69. 1 0
      src/lib/components/common/SensitiveInput.svelte
  70. 28 6
      src/lib/components/workspace/Models.svelte
  71. 2 2
      src/lib/components/workspace/Prompts.svelte
  72. 2 2
      src/lib/components/workspace/Tools.svelte
  73. 10 3
      src/lib/i18n/locales/ar-BH/translation.json
  74. 10 3
      src/lib/i18n/locales/bg-BG/translation.json
  75. 10 3
      src/lib/i18n/locales/bn-BD/translation.json
  76. 29 22
      src/lib/i18n/locales/ca-ES/translation.json
  77. 10 3
      src/lib/i18n/locales/ceb-PH/translation.json
  78. 10 3
      src/lib/i18n/locales/cs-CZ/translation.json
  79. 10 3
      src/lib/i18n/locales/da-DK/translation.json
  80. 111 104
      src/lib/i18n/locales/de-DE/translation.json
  81. 10 3
      src/lib/i18n/locales/dg-DG/translation.json
  82. 10 3
      src/lib/i18n/locales/el-GR/translation.json
  83. 10 3
      src/lib/i18n/locales/en-GB/translation.json
  84. 10 3
      src/lib/i18n/locales/en-US/translation.json
  85. 307 301
      src/lib/i18n/locales/es-ES/translation.json
  86. 10 3
      src/lib/i18n/locales/eu-ES/translation.json
  87. 10 3
      src/lib/i18n/locales/fa-IR/translation.json
  88. 10 3
      src/lib/i18n/locales/fi-FI/translation.json
  89. 10 3
      src/lib/i18n/locales/fr-CA/translation.json
  90. 10 3
      src/lib/i18n/locales/fr-FR/translation.json
  91. 10 3
      src/lib/i18n/locales/he-IL/translation.json
  92. 10 3
      src/lib/i18n/locales/hi-IN/translation.json
  93. 10 3
      src/lib/i18n/locales/hr-HR/translation.json
  94. 10 3
      src/lib/i18n/locales/hu-HU/translation.json
  95. 10 3
      src/lib/i18n/locales/id-ID/translation.json
  96. 10 3
      src/lib/i18n/locales/ie-GA/translation.json
  97. 10 3
      src/lib/i18n/locales/it-IT/translation.json
  98. 10 3
      src/lib/i18n/locales/ja-JP/translation.json
  99. 10 3
      src/lib/i18n/locales/ka-GE/translation.json
  100. 10 3
      src/lib/i18n/locales/ko-KR/translation.json

+ 1 - 1
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 🌙
 

+ 74 - 3
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)
@@ -682,6 +683,17 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
 CACHE_DIR = f"{DATA_DIR}/cache"
 Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
 
+
+####################################
+# DIRECT CONNECTIONS
+####################################
+
+ENABLE_DIRECT_CONNECTIONS = PersistentConfig(
+    "ENABLE_DIRECT_CONNECTIONS",
+    "direct.enable",
+    os.environ.get("ENABLE_DIRECT_CONNECTIONS", "True").lower() == "true",
+)
+
 ####################################
 # OLLAMA_BASE_URL
 ####################################
@@ -1325,6 +1337,54 @@ 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_PROMPT_TEMPLATE = PersistentConfig(
+    "CODE_INTERPRETER_PROMPT_TEMPLATE",
+    "code_interpreter.prompt_template",
+    os.environ.get("CODE_INTERPRETER_PROMPT_TEMPLATE", ""),
+)
+
+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
 
@@ -1335,9 +1395,8 @@ DEFAULT_CODE_INTERPRETER_PROMPT = """
    - When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user.  
    - After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.**  
    - If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary.  
-   - If a link is provided for an image, audio, or any file, include it in the response exactly as given to ensure the user has access to the original resource.  
+   - **If a link to an image, audio, or any file is provided in markdown format in the output, ALWAYS regurgitate word for word, explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.**
    - All responses should be communicated in the chat's primary language, ensuring seamless understanding. If the chat is multilingual, default to English for clarity.
-   - **If a link to an image, audio, or any file is provided in markdown format, ALWAYS regurgitate explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.**
 
 Ensure that the tools are effectively utilized to achieve the highest-quality analysis for the user."""
 
@@ -1645,7 +1704,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",
@@ -1690,6 +1749,12 @@ MOJEEK_SEARCH_API_KEY = PersistentConfig(
     os.getenv("MOJEEK_SEARCH_API_KEY", ""),
 )
 
+BOCHA_SEARCH_API_KEY = PersistentConfig(
+    "BOCHA_SEARCH_API_KEY",
+    "rag.web.search.bocha_search_api_key",
+    os.getenv("BOCHA_SEARCH_API_KEY", ""),
+)
+
 SERPSTACK_API_KEY = PersistentConfig(
     "SERPSTACK_API_KEY",
     "rag.web.search.serpstack_api_key",
@@ -2012,6 +2077,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",

+ 1 - 0
backend/open_webui/env.py

@@ -92,6 +92,7 @@ log_sources = [
     "RAG",
     "WEBHOOK",
     "SOCKET",
+    "OAUTH",
 ]
 
 SRC_LOG_LEVELS = {}

+ 50 - 2
backend/open_webui/main.py

@@ -97,6 +97,16 @@ from open_webui.config import (
     OPENAI_API_BASE_URLS,
     OPENAI_API_KEYS,
     OPENAI_API_CONFIGS,
+    # Direct Connections
+    ENABLE_DIRECT_CONNECTIONS,
+    # Code Interpreter
+    ENABLE_CODE_INTERPRETER,
+    CODE_INTERPRETER_ENGINE,
+    CODE_INTERPRETER_PROMPT_TEMPLATE,
+    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 +140,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
@@ -180,6 +191,7 @@ from open_webui.config import (
     EXA_API_KEY,
     KAGI_SEARCH_API_KEY,
     MOJEEK_SEARCH_API_KEY,
+    BOCHA_SEARCH_API_KEY,
     GOOGLE_PSE_API_KEY,
     GOOGLE_PSE_ENGINE_ID,
     GOOGLE_DRIVE_CLIENT_ID,
@@ -322,7 +334,11 @@ class SPAStaticFiles(StaticFiles):
             return await super().get_response(path, scope)
         except (HTTPException, StarletteHTTPException) as ex:
             if ex.status_code == 404:
-                return await super().get_response("index.html", scope)
+                if path.endswith(".js"):
+                    # Return 404 for javascript files
+                    raise ex
+                else:
+                    return await super().get_response("index.html", scope)
             else:
                 raise ex
 
@@ -389,6 +405,14 @@ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
 
 app.state.OPENAI_MODELS = {}
 
+########################################
+#
+# DIRECT CONNECTIONS
+#
+########################################
+
+app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
+
 ########################################
 #
 # WEBUI
@@ -514,6 +538,7 @@ app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
 app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
 app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY
 app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY
+app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY
 app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
 app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
 app.state.config.SERPER_API_KEY = SERPER_API_KEY
@@ -569,6 +594,24 @@ 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_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
+
+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 +654,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 +797,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"])
 
@@ -1011,14 +1056,17 @@ async def get_app_config(request: Request):
             "enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
             **(
                 {
+                    "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
                     "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 {}

+ 1 - 1
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:

+ 18 - 0
backend/open_webui/models/users.py

@@ -271,6 +271,24 @@ class UsersTable:
         except Exception:
             return None
 
+    def update_user_settings_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
+        try:
+            with get_db() as db:
+                user_settings = db.query(User).filter_by(id=id).first().settings
+
+                if user_settings is None:
+                    user_settings = {}
+
+                user_settings.update(updated)
+
+                db.query(User).filter_by(id=id).update({"settings": user_settings})
+                db.commit()
+
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
+        except Exception:
+            return None
+
     def delete_user_by_id(self, id: str) -> bool:
         try:
             # Remove User from Groups

+ 28 - 0
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)

+ 72 - 0
backend/open_webui/retrieval/web/bocha.py

@@ -0,0 +1,72 @@
+import logging
+from typing import Optional
+
+import requests
+import json
+from open_webui.retrieval.web.main import SearchResult, get_filtered_results
+from open_webui.env import SRC_LOG_LEVELS
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["RAG"])
+
+def _parse_response(response):
+    result = {}
+    if "data" in response:
+        data = response["data"]
+        if "webPages" in data:
+            webPages = data["webPages"]
+            if "value" in webPages:
+                result["webpage"] = [
+                    {
+                        "id": item.get("id", ""),
+                        "name": item.get("name", ""),
+                        "url": item.get("url", ""),
+                        "snippet": item.get("snippet", ""),
+                        "summary": item.get("summary", ""),
+                        "siteName": item.get("siteName", ""),
+                        "siteIcon": item.get("siteIcon", ""),
+                        "datePublished": item.get("datePublished", "") or item.get("dateLastCrawled", ""),
+                    }
+                    for item in webPages["value"]
+                ]
+    return result
+
+
+def search_bocha(
+    api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
+) -> list[SearchResult]:
+    """Search using Bocha's Search API and return the results as a list of SearchResult objects.
+
+    Args:
+        api_key (str): A Bocha Search API key
+        query (str): The query to search for
+    """
+    url = "https://api.bochaai.com/v1/web-search?utm_source=ollama"
+    headers = {
+        "Authorization": f"Bearer {api_key}",
+        "Content-Type": "application/json"
+    }
+    
+    payload = json.dumps({
+        "query": query,
+        "summary": True,
+        "freshness": "noLimit",
+        "count": count
+    })
+
+    response = requests.post(url, headers=headers, data=payload, timeout=5)
+    response.raise_for_status()
+    results = _parse_response(response.json())
+    print(results)
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
+
+    return [
+        SearchResult(
+            link=result["url"], 
+            title=result.get("name"), 
+            snippet=result.get("summary")
+        )
+        for result in results.get("webpage", [])[:count]  
+    ]
+

+ 32 - 16
backend/open_webui/retrieval/web/google_pse.py

@@ -8,7 +8,6 @@ from open_webui.env import SRC_LOG_LEVELS
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
-
 def search_google_pse(
     api_key: str,
     search_engine_id: str,
@@ -17,34 +16,51 @@ def search_google_pse(
     filter_list: Optional[list[str]] = None,
 ) -> list[SearchResult]:
     """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
+    Handles pagination for counts greater than 10.
 
     Args:
         api_key (str): A Programmable Search Engine API key
         search_engine_id (str): A Programmable Search Engine ID
         query (str): The query to search for
+        count (int): The number of results to return (max 100, as PSE max results per query is 10 and max page is 10)
+        filter_list (Optional[list[str]], optional): A list of keywords to filter out from results. Defaults to None.
+
+    Returns:
+        list[SearchResult]: A list of SearchResult objects.
     """
     url = "https://www.googleapis.com/customsearch/v1"
-
     headers = {"Content-Type": "application/json"}
-    params = {
-        "cx": search_engine_id,
-        "q": query,
-        "key": api_key,
-        "num": count,
-    }
-
-    response = requests.request("GET", url, headers=headers, params=params)
-    response.raise_for_status()
-
-    json_response = response.json()
-    results = json_response.get("items", [])
+    all_results = []
+    start_index = 1  # Google PSE start parameter is 1-based
+
+    while count > 0:
+        num_results_this_page = min(count, 10)  # Google PSE max results per page is 10
+        params = {
+            "cx": search_engine_id,
+            "q": query,
+            "key": api_key,
+            "num": num_results_this_page,
+            "start": start_index,
+        }
+        response = requests.request("GET", url, headers=headers, params=params)
+        response.raise_for_status()
+        json_response = response.json()
+        results = json_response.get("items", [])
+        if results: # check if results are returned. If not, no more pages to fetch.
+            all_results.extend(results)
+            count -= len(results) # Decrement count by the number of results fetched in this page.
+            start_index += 10 # Increment start index for the next page
+        else:
+            break # No more results from Google PSE, break the loop
+
     if filter_list:
-        results = get_filtered_results(results, filter_list)
+        all_results = get_filtered_results(all_results, filter_list)
+
     return [
         SearchResult(
             link=result["link"],
             title=result.get("title"),
             snippet=result.get("snippet"),
         )
-        for result in results
+        for result in all_results
     ]

+ 13 - 4
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"],

+ 68 - 0
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:

+ 92 - 0
backend/open_webui/routers/configs.py

@@ -36,6 +36,98 @@ async def export_config(user=Depends(get_admin_user)):
     return get_config()
 
 
+############################
+# Direct Connections Config
+############################
+
+
+class DirectConnectionsConfigForm(BaseModel):
+    ENABLE_DIRECT_CONNECTIONS: bool
+
+
+@router.get("/direct_connections", response_model=DirectConnectionsConfigForm)
+async def get_direct_connections_config(request: Request, user=Depends(get_admin_user)):
+    return {
+        "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
+    }
+
+
+@router.post("/direct_connections", response_model=DirectConnectionsConfigForm)
+async def set_direct_connections_config(
+    request: Request,
+    form_data: DirectConnectionsConfigForm,
+    user=Depends(get_admin_user),
+):
+    request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
+        form_data.ENABLE_DIRECT_CONNECTIONS
+    )
+    return {
+        "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
+    }
+
+
+############################
+# CodeInterpreterConfig
+############################
+class CodeInterpreterConfigForm(BaseModel):
+    ENABLE_CODE_INTERPRETER: bool
+    CODE_INTERPRETER_ENGINE: str
+    CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[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_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
+        "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_PROMPT_TEMPLATE = (
+        form_data.CODE_INTERPRETER_PROMPT_TEMPLATE
+    )
+
+    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_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
+        "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
 ############################

+ 16 - 20
backend/open_webui/routers/files.py

@@ -3,30 +3,22 @@ import os
 import uuid
 from pathlib import Path
 from typing import Optional
-from pydantic import BaseModel
-import mimetypes
 from urllib.parse import quote
 
-from open_webui.storage.provider import Storage
-
+from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
+from fastapi.responses import FileResponse, StreamingResponse
+from open_webui.constants import ERROR_MESSAGES
+from open_webui.env import SRC_LOG_LEVELS
 from open_webui.models.files import (
     FileForm,
     FileModel,
     FileModelResponse,
     Files,
 )
-from open_webui.routers.retrieval import process_file, ProcessFileForm
-
-from open_webui.config import UPLOAD_DIR
-from open_webui.env import SRC_LOG_LEVELS
-from open_webui.constants import ERROR_MESSAGES
-
-
-from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request
-from fastapi.responses import FileResponse, StreamingResponse
-
-
+from open_webui.routers.retrieval import ProcessFileForm, process_file
+from open_webui.storage.provider import Storage
 from open_webui.utils.auth import get_admin_user, get_verified_user
+from pydantic import BaseModel
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -41,7 +33,10 @@ router = APIRouter()
 
 @router.post("/", response_model=FileModelResponse)
 def upload_file(
-    request: Request, file: UploadFile = File(...), user=Depends(get_verified_user)
+    request: Request,
+    file: UploadFile = File(...),
+    user=Depends(get_verified_user),
+    file_metadata: dict = {},
 ):
     log.info(f"file.content_type: {file.content_type}")
     try:
@@ -65,6 +60,7 @@ def upload_file(
                         "name": name,
                         "content_type": file.content_type,
                         "size": len(contents),
+                        "data": file_metadata,
                     },
                 }
             ),
@@ -126,7 +122,7 @@ async def delete_all_files(user=Depends(get_admin_user)):
             Storage.delete_all_files()
         except Exception as e:
             log.exception(e)
-            log.error(f"Error deleting files")
+            log.error("Error deleting files")
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
@@ -248,7 +244,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
                 )
         except Exception as e:
             log.exception(e)
-            log.error(f"Error getting file content")
+            log.error("Error getting file content")
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
@@ -279,7 +275,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
                 )
         except Exception as e:
             log.exception(e)
-            log.error(f"Error getting file content")
+            log.error("Error getting file content")
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
@@ -355,7 +351,7 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
                 Storage.delete_file(file.path)
             except Exception as e:
                 log.exception(e)
-                log.error(f"Error deleting files")
+                log.error("Error deleting files")
                 raise HTTPException(
                     status_code=status.HTTP_400_BAD_REQUEST,
                     detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),

+ 46 - 69
backend/open_webui/routers/images.py

@@ -1,32 +1,26 @@
 import asyncio
 import base64
+import io
 import json
 import logging
 import mimetypes
 import re
-import uuid
 from pathlib import Path
 from typing import Optional
 
 import requests
-
-
-from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
-from fastapi.middleware.cors import CORSMiddleware
-from pydantic import BaseModel
-
-
+from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
 from open_webui.config import CACHE_DIR
 from open_webui.constants import ERROR_MESSAGES
-from open_webui.env import ENV, SRC_LOG_LEVELS, ENABLE_FORWARD_USER_INFO_HEADERS
-
+from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, SRC_LOG_LEVELS
+from open_webui.routers.files import upload_file
 from open_webui.utils.auth import get_admin_user, get_verified_user
 from open_webui.utils.images.comfyui import (
     ComfyUIGenerateImageForm,
     ComfyUIWorkflow,
     comfyui_generate_image,
 )
-
+from pydantic import BaseModel
 
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["IMAGES"])
@@ -271,7 +265,6 @@ async def get_image_config(request: Request, user=Depends(get_admin_user)):
 async def update_image_config(
     request: Request, form_data: ImageConfigForm, user=Depends(get_admin_user)
 ):
-
     set_image_model(request, form_data.MODEL)
 
     pattern = r"^\d+x\d+$"
@@ -383,40 +376,22 @@ class GenerateImageForm(BaseModel):
     negative_prompt: Optional[str] = None
 
 
-def save_b64_image(b64_str):
+def load_b64_image_data(b64_str):
     try:
-        image_id = str(uuid.uuid4())
-
         if "," in b64_str:
             header, encoded = b64_str.split(",", 1)
             mime_type = header.split(";")[0]
-
             img_data = base64.b64decode(encoded)
-            image_format = mimetypes.guess_extension(mime_type)
-
-            image_filename = f"{image_id}{image_format}"
-            file_path = IMAGE_CACHE_DIR / f"{image_filename}"
-            with open(file_path, "wb") as f:
-                f.write(img_data)
-            return image_filename
         else:
-            image_filename = f"{image_id}.png"
-            file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
-
+            mime_type = "image/png"
             img_data = base64.b64decode(b64_str)
-
-            # Write the image data to a file
-            with open(file_path, "wb") as f:
-                f.write(img_data)
-            return image_filename
-
+        return img_data, mime_type
     except Exception as e:
-        log.exception(f"Error saving image: {e}")
+        log.exception(f"Error loading image data: {e}")
         return None
 
 
-def save_url_image(url, headers=None):
-    image_id = str(uuid.uuid4())
+def load_url_image_data(url, headers=None):
     try:
         if headers:
             r = requests.get(url, headers=headers)
@@ -426,18 +401,7 @@ def save_url_image(url, headers=None):
         r.raise_for_status()
         if r.headers["content-type"].split("/")[0] == "image":
             mime_type = r.headers["content-type"]
-            image_format = mimetypes.guess_extension(mime_type)
-
-            if not image_format:
-                raise ValueError("Could not determine image type from MIME type")
-
-            image_filename = f"{image_id}{image_format}"
-
-            file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
-            with open(file_path, "wb") as image_file:
-                for chunk in r.iter_content(chunk_size=8192):
-                    image_file.write(chunk)
-            return image_filename
+            return r.content, mime_type
         else:
             log.error("Url does not point to an image.")
             return None
@@ -447,6 +411,20 @@ def save_url_image(url, headers=None):
         return None
 
 
+def upload_image(request, image_metadata, image_data, content_type, user):
+    image_format = mimetypes.guess_extension(content_type)
+    file = UploadFile(
+        file=io.BytesIO(image_data),
+        filename=f"generated-image{image_format}",  # will be converted to a unique ID on upload_file
+        headers={
+            "content-type": content_type,
+        },
+    )
+    file_item = upload_file(request, file, user, file_metadata=image_metadata)
+    url = request.app.url_path_for("get_file_content_by_id", id=file_item.id)
+    return url
+
+
 @router.post("/generations")
 async def image_generations(
     request: Request,
@@ -500,13 +478,9 @@ async def image_generations(
             images = []
 
             for image in res["data"]:
-                image_filename = save_b64_image(image["b64_json"])
-                images.append({"url": f"/cache/image/generations/{image_filename}"})
-                file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
-
-                with open(file_body_path, "w") as f:
-                    json.dump(data, f)
-
+                image_data, content_type = load_b64_image_data(image["b64_json"])
+                url = upload_image(request, data, image_data, content_type, user)
+                images.append({"url": url})
             return images
 
         elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
@@ -552,14 +526,15 @@ async def image_generations(
                         "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
                     }
 
-                image_filename = save_url_image(image["url"], headers)
-                images.append({"url": f"/cache/image/generations/{image_filename}"})
-                file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
-
-                with open(file_body_path, "w") as f:
-                    json.dump(form_data.model_dump(exclude_none=True), f)
-
-            log.debug(f"images: {images}")
+                image_data, content_type = load_url_image_data(image["url"], headers)
+                url = upload_image(
+                    request,
+                    form_data.model_dump(exclude_none=True),
+                    image_data,
+                    content_type,
+                    user,
+                )
+                images.append({"url": url})
             return images
         elif (
             request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111"
@@ -604,13 +579,15 @@ async def image_generations(
             images = []
 
             for image in res["images"]:
-                image_filename = save_b64_image(image)
-                images.append({"url": f"/cache/image/generations/{image_filename}"})
-                file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
-
-                with open(file_body_path, "w") as f:
-                    json.dump({**data, "info": res["info"]}, f)
-
+                image_data, content_type = load_b64_image_data(image)
+                url = upload_image(
+                    request,
+                    {**data, "info": res["info"]},
+                    image_data,
+                    content_type,
+                    user,
+                )
+                images.append({"url": url})
             return images
     except Exception as e:
         error = e

+ 73 - 46
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 = f"{UPLOAD_DIR}/{file.filename}"
-
-    # Save file in chunks
-    with open(file_path, "wb+") as f:
-        for chunk in file.file:
-            f.write(chunk)
-
-    def file_process_stream():
+    file_path = os.path.join(UPLOAD_DIR, file.filename)
+    os.makedirs(UPLOAD_DIR, exist_ok=True)
+
+    # --- 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)
+
+    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)
+
+            if response.ok:
+                log.info(f"Uploaded to /api/blobs")  # DEBUG
+                # Remove local file
+                os.remove(file_path)
+
+                # 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),
+                )
 
-                    url = f"{ollama_url}/api/blobs/sha256:{hashed}"
-                    response = requests.post(url, data=f)
+                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}"
+                    )
 
-                    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."
-                        )
+            else:
+                raise Exception("Ollama: Could not create blob, Please try again.")
 
         except Exception as e:
             res = {"error": str(e)}

+ 6 - 6
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:

+ 24 - 0
backend/open_webui/routers/retrieval.py

@@ -45,6 +45,7 @@ from open_webui.retrieval.web.utils import get_web_loader
 from open_webui.retrieval.web.brave import search_brave
 from open_webui.retrieval.web.kagi import search_kagi
 from open_webui.retrieval.web.mojeek import search_mojeek
+from open_webui.retrieval.web.bocha import search_bocha
 from open_webui.retrieval.web.duckduckgo import search_duckduckgo
 from open_webui.retrieval.web.google_pse import search_google_pse
 from open_webui.retrieval.web.jina_search import search_jina
@@ -379,6 +380,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
                 "brave_search_api_key": request.app.state.config.BRAVE_SEARCH_API_KEY,
                 "kagi_search_api_key": request.app.state.config.KAGI_SEARCH_API_KEY,
                 "mojeek_search_api_key": request.app.state.config.MOJEEK_SEARCH_API_KEY,
+                "bocha_search_api_key": request.app.state.config.BOCHA_SEARCH_API_KEY,
                 "serpstack_api_key": request.app.state.config.SERPSTACK_API_KEY,
                 "serpstack_https": request.app.state.config.SERPSTACK_HTTPS,
                 "serper_api_key": request.app.state.config.SERPER_API_KEY,
@@ -392,6 +394,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,
             },
         },
     }
@@ -428,6 +431,7 @@ class WebSearchConfig(BaseModel):
     brave_search_api_key: Optional[str] = None
     kagi_search_api_key: Optional[str] = None
     mojeek_search_api_key: Optional[str] = None
+    bocha_search_api_key: Optional[str] = None
     serpstack_api_key: Optional[str] = None
     serpstack_https: Optional[bool] = None
     serper_api_key: Optional[str] = None
@@ -441,6 +445,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):
@@ -523,6 +528,9 @@ async def update_rag_config(
         request.app.state.config.MOJEEK_SEARCH_API_KEY = (
             form_data.web.search.mojeek_search_api_key
         )
+        request.app.state.config.BOCHA_SEARCH_API_KEY = (
+            form_data.web.search.bocha_search_api_key
+        )
         request.app.state.config.SERPSTACK_API_KEY = (
             form_data.web.search.serpstack_api_key
         )
@@ -553,6 +561,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,
@@ -586,6 +597,7 @@ async def update_rag_config(
                 "brave_search_api_key": request.app.state.config.BRAVE_SEARCH_API_KEY,
                 "kagi_search_api_key": request.app.state.config.KAGI_SEARCH_API_KEY,
                 "mojeek_search_api_key": request.app.state.config.MOJEEK_SEARCH_API_KEY,
+                "bocha_search_api_key": request.app.state.config.BOCHA_SEARCH_API_KEY,
                 "serpstack_api_key": request.app.state.config.SERPSTACK_API_KEY,
                 "serpstack_https": request.app.state.config.SERPSTACK_HTTPS,
                 "serper_api_key": request.app.state.config.SERPER_API_KEY,
@@ -599,6 +611,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,
             },
         },
     }
@@ -1107,6 +1120,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
     - BRAVE_SEARCH_API_KEY
     - KAGI_SEARCH_API_KEY
     - MOJEEK_SEARCH_API_KEY
+    - BOCHA_SEARCH_API_KEY
     - SERPSTACK_API_KEY
     - SERPER_API_KEY
     - SERPLY_API_KEY
@@ -1174,6 +1188,16 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
             )
         else:
             raise Exception("No MOJEEK_SEARCH_API_KEY found in environment variables")
+    elif engine == "bocha":
+        if request.app.state.config.BOCHA_SEARCH_API_KEY:
+            return search_bocha(
+                request.app.state.config.BOCHA_SEARCH_API_KEY,
+                query,
+                request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+            )
+        else:
+            raise Exception("No BOCHA_SEARCH_API_KEY found in environment variables")
     elif engine == "serpstack":
         if request.app.state.config.SERPSTACK_API_KEY:
             return search_serpstack(

+ 1 - 1
backend/open_webui/routers/users.py

@@ -153,7 +153,7 @@ async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
 async def update_user_settings_by_session_user(
     form_data: UserSettings, user=Depends(get_verified_user)
 ):
-    user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()})
+    user = Users.update_user_settings_by_id(user.id, form_data.model_dump())
     if user:
         return user.settings
     else:

+ 21 - 7
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):

+ 35 - 109
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,
-        }
-    )
-
-    __event_call__ = get_event_call(
-        {
-            "chat_id": data["chat_id"],
-            "message_id": data["id"],
-            "session_id": data["session_id"],
-            "user_id": user.id,
-        }
-    )
-
-    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,
-            }
+    metadata = {
+        "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,
+    }
 
-            # 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):

+ 148 - 0
backend/open_webui/utils/code_interpreter.py

@@ -0,0 +1,148 @@
+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
+             - Images are prefixed with "base64:image/png," and separated by newlines if multiple.
+    """
+    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()
+            xsrf_token = session.cookies.get("_xsrf")
+            if not xsrf_token:
+                raise ValueError("Failed to fetch _xsrf token")
+
+            login_data = {"_xsrf": xsrf_token, "password": password}
+            login_response = session.post(
+                login_url, data=login_data, cookies=session.cookies
+            )
+            login_response.raise_for_status()
+            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:
+        response = session.post(kernel_url, headers=headers, cookies=session.cookies)
+        response.raise_for_status()
+        kernel_id = response.json()["id"]
+
+        websocket_url = urljoin(
+            jupyter_url.replace("http", "ws"),
+            f"/api/kernels/{kernel_id}/channels{params}",
+        )
+
+        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()]
+            )
+
+        async with websockets.connect(
+            websocket_url, additional_headers=ws_headers
+        ) as ws:
+            msg_id = str(uuid.uuid4())
+            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))
+
+            stdout, stderr, result = "", "", []
+
+            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"):
+                            data = message_data["content"]["data"]
+                            if "image/png" in data:
+                                result.append(
+                                    f"data:image/png;base64,{data['image/png']}"
+                                )
+                            elif "text/plain" in data:
+                                result.append(data["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:
+        if kernel_id:
+            requests.delete(
+                f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies
+            )
+
+    return {
+        "stdout": stdout.strip(),
+        "stderr": stderr.strip(),
+        "result": "\n".join(result).strip() if result else "",
+    }

+ 99 - 0
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, {}

+ 1 - 1
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

+ 128 - 109
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 = "<context>Unable to generate an image, tell the user that an error occured</context>"
+        system_message_content = "<context>Unable to generate an image, tell the user that an error occurred</context>"
 
     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
@@ -778,12 +690,21 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
         if "code_interpreter" in features and features["code_interpreter"]:
             form_data["messages"] = add_or_update_user_message(
-                DEFAULT_CODE_INTERPRETER_PROMPT, form_data["messages"]
+                (
+                    request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
+                    if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE != ""
+                    else DEFAULT_CODE_INTERPRETER_PROMPT
+                ),
+                form_data["messages"],
             )
 
     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 +1043,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 +1123,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 +1190,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
@@ -1702,21 +1650,60 @@ async def process_chat_response(
                         content_blocks[-1]["type"] == "code_interpreter"
                         and retries < MAX_RETRIES
                     ):
+                        await event_emitter(
+                            {
+                                "type": "chat:completion",
+                                "data": {
+                                    "content": serialize_content_blocks(content_blocks),
+                                },
+                            }
+                        )
+
                         retries += 1
                         log.debug(f"Attempt count: {retries}")
 
                         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", "")
@@ -1750,6 +1737,38 @@ async def process_chat_response(
                                                 )
 
                                         output["stdout"] = "\n".join(stdoutLines)
+
+                                    result = output.get("result", "")
+
+                                    if result:
+                                        resultLines = result.split("\n")
+                                        for idx, line in enumerate(resultLines):
+                                            if "data:image/png;base64" in line:
+                                                id = str(uuid4())
+
+                                                # ensure the path exists
+                                                os.makedirs(
+                                                    os.path.join(CACHE_DIR, "images"),
+                                                    exist_ok=True,
+                                                )
+
+                                                image_path = os.path.join(
+                                                    CACHE_DIR,
+                                                    f"images/{id}.png",
+                                                )
+
+                                                with open(image_path, "wb") as f:
+                                                    f.write(
+                                                        base64.b64decode(
+                                                            line.split(",")[1]
+                                                        )
+                                                    )
+
+                                                resultLines[idx] = (
+                                                    f"![Output Image {idx}](/cache/images/{id}.png)"
+                                                )
+
+                                        output["result"] = "\n".join(resultLines)
                         except Exception as e:
                             output = str(e)
 

+ 5 - 4
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()
 
 

+ 46 - 11
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}"
@@ -281,7 +315,8 @@ class OAuthManager:
                 username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
 
                 name = user_data.get(username_claim)
-                if not isinstance(user, str):
+                if not name:
+                    log.warning("Username claim is missing, using email as name")
                     name = email
 
                 role = self.get_user_role(None, user_data)

+ 6 - 6
backend/open_webui/utils/payload.py

@@ -14,6 +14,12 @@ def apply_model_system_prompt_to_body(
     if not system:
         return form_data
 
+    # Metadata (WebUI Usage)
+    if metadata:
+        variables = metadata.get("variables", {})
+        if variables:
+            system = prompt_variables_template(system, variables)
+
     # Legacy (API Usage)
     if user:
         template_params = {
@@ -25,12 +31,6 @@ def apply_model_system_prompt_to_body(
 
     system = prompt_template(system, **template_params)
 
-    # Metadata (WebUI Usage)
-    if metadata:
-        variables = metadata.get("variables", {})
-        if variables:
-            system = prompt_variables_template(system, variables)
-
     form_data["messages"] = add_or_update_system_message(
         system, form_data.get("messages", [])
     )

+ 7 - 5
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"""
         <html>
             <head>
@@ -84,7 +86,7 @@ class PDFGenerator:
             <body>
             <div>
                 <div>
-                    <h2>{self.form_data.title}</h2>
+                    <h2>{escaped_title}</h2>
                     {self.messages_html}
                 </div>
             </div>

+ 3 - 1
backend/open_webui/utils/response.py

@@ -73,7 +73,9 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
                     "type": "function",
                     "function": {
                         "name": tool_call.get("function", {}).get("name", ""),
-                        "arguments": f"{tool_call.get('function', {}).get('arguments', {})}",
+                        "arguments": json.dumps(
+                            tool_call.get("function", {}).get("arguments", {})
+                        ),
                     },
                 }
                 openai_tool_calls.append(openai_tool_call)

+ 5 - 3
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
@@ -89,7 +91,7 @@ pytube==15.0.0
 
 extract_msg
 pydub
-duckduckgo-search~=7.3.0
+duckduckgo-search~=7.3.2
 
 ## Google Drive
 google-api-python-client

+ 186 - 120
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"
 			}

+ 4 - 2
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",

+ 5 - 2
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",
@@ -94,7 +97,7 @@ dependencies = [
 
     "extract_msg",
     "pydub",
-    "duckduckgo-search~=7.3.0",
+    "duckduckgo-search~=7.3.2",
 
     "google-api-python-client",
     "google-auth-httplib2",

+ 114 - 0
src/lib/apis/configs/index.ts

@@ -58,6 +58,120 @@ export const exportConfig = async (token: string) => {
 	return res;
 };
 
+export const getDirectConnectionsConfig = async (token: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
+		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 setDirectConnectionsConfig = async (token: string, config: object) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
+		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 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;
 

+ 97 - 1
src/lib/apis/index.ts

@@ -1,6 +1,11 @@
 import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
+import { getOpenAIModelsDirect } from './openai';
 
-export const getModels = async (token: string = '', base: boolean = false) => {
+export const getModels = async (
+	token: string = '',
+	connections: object | null = null,
+	base: boolean = false
+) => {
 	let error = null;
 	const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
 		method: 'GET',
@@ -25,6 +30,97 @@ export const getModels = async (token: string = '', base: boolean = false) => {
 	}
 
 	let models = res?.data ?? [];
+
+	if (connections && !base) {
+		let localModels = [];
+
+		if (connections) {
+			const OPENAI_API_BASE_URLS = connections.OPENAI_API_BASE_URLS;
+			const OPENAI_API_KEYS = connections.OPENAI_API_KEYS;
+			const OPENAI_API_CONFIGS = connections.OPENAI_API_CONFIGS;
+
+			const requests = [];
+			for (const idx in OPENAI_API_BASE_URLS) {
+				const url = OPENAI_API_BASE_URLS[idx];
+
+				if (idx.toString() in OPENAI_API_CONFIGS) {
+					const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {};
+
+					const enable = apiConfig?.enable ?? true;
+					const modelIds = apiConfig?.model_ids ?? [];
+
+					if (enable) {
+						if (modelIds.length > 0) {
+							const modelList = {
+								object: 'list',
+								data: modelIds.map((modelId) => ({
+									id: modelId,
+									name: modelId,
+									owned_by: 'openai',
+									openai: { id: modelId },
+									urlIdx: idx
+								}))
+							};
+
+							requests.push(
+								(async () => {
+									return modelList;
+								})()
+							);
+						} else {
+							requests.push(getOpenAIModelsDirect(url, OPENAI_API_KEYS[idx]));
+						}
+					} else {
+						requests.push(
+							(async () => {
+								return {
+									object: 'list',
+									data: [],
+									urlIdx: idx
+								};
+							})()
+						);
+					}
+				}
+			}
+
+			const responses = await Promise.all(requests);
+
+			for (const idx in responses) {
+				const response = responses[idx];
+				const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {};
+
+				let models = Array.isArray(response) ? response : (response?.data ?? []);
+				models = models.map((model) => ({ ...model, openai: { id: model.id }, urlIdx: idx }));
+
+				const prefixId = apiConfig.prefix_id;
+				if (prefixId) {
+					for (const model of models) {
+						model.id = `${prefixId}.${model.id}`;
+					}
+				}
+
+				localModels = localModels.concat(models);
+			}
+		}
+
+		models = models.concat(
+			localModels.map((model) => ({
+				...model,
+				name: model?.name ?? model?.id,
+				direct: true
+			}))
+		);
+
+		// Remove duplicates
+		const modelsMap = {};
+		for (const model of models) {
+			modelsMap[model.id] = model;
+		}
+
+		models = Object.values(modelsMap);
+	}
+
 	return models;
 };
 

+ 78 - 22
src/lib/apis/openai/index.ts

@@ -208,6 +208,33 @@ export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
 	return res.OPENAI_API_KEYS;
 };
 
+export const getOpenAIModelsDirect = async (url: string, key: string) => {
+	let error = null;
+
+	const res = await fetch(`${url}/models`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(key && { authorization: `Bearer ${key}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
+			return [];
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getOpenAIModels = async (token: string, urlIdx?: number) => {
 	let error = null;
 
@@ -241,33 +268,62 @@ export const getOpenAIModels = async (token: string, urlIdx?: number) => {
 export const verifyOpenAIConnection = async (
 	token: string = '',
 	url: string = 'https://api.openai.com/v1',
-	key: string = ''
+	key: string = '',
+	direct: boolean = false
 ) => {
+	if (!url) {
+		throw 'OpenAI: URL is required';
+	}
+
 	let error = null;
+	let res = null;
 
-	const res = await fetch(`${OPENAI_API_BASE_URL}/verify`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			Authorization: `Bearer ${token}`,
-			'Content-Type': 'application/json'
-		},
-		body: JSON.stringify({
-			url,
-			key
+	if (direct) {
+		res = await fetch(`${url}/models`, {
+			method: 'GET',
+			headers: {
+				Accept: 'application/json',
+				Authorization: `Bearer ${key}`,
+				'Content-Type': 'application/json'
+			}
 		})
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return res.json();
+			.then(async (res) => {
+				if (!res.ok) throw await res.json();
+				return res.json();
+			})
+			.catch((err) => {
+				error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
+				return [];
+			});
+
+		if (error) {
+			throw error;
+		}
+	} else {
+		res = await fetch(`${OPENAI_API_BASE_URL}/verify`, {
+			method: 'POST',
+			headers: {
+				Accept: 'application/json',
+				Authorization: `Bearer ${token}`,
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({
+				url,
+				key
+			})
 		})
-		.catch((err) => {
-			error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
-			return [];
-		});
-
-	if (error) {
-		throw error;
+			.then(async (res) => {
+				if (!res.ok) throw await res.json();
+				return res.json();
+			})
+			.catch((err) => {
+				error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
+				return [];
+			});
+
+		if (error) {
+			throw error;
+		}
 	}
 
 	return res;

+ 7 - 3
src/lib/components/admin/Settings/Connections/AddConnectionModal.svelte → src/lib/components/AddConnectionModal.svelte

@@ -20,7 +20,9 @@
 
 	export let show = false;
 	export let edit = false;
+
 	export let ollama = false;
+	export let direct = false;
 
 	export let connection = null;
 
@@ -46,9 +48,11 @@
 	};
 
 	const verifyOpenAIHandler = async () => {
-		const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
-			toast.error(`${error}`);
-		});
+		const res = await verifyOpenAIConnection(localStorage.token, url, key, direct).catch(
+			(error) => {
+				toast.error(`${error}`);
+			}
+		);
 
 		if (res) {
 			toast.success($i18n.t('Server connection verified'));

+ 2 - 2
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 @@
 					}}
 				>
 					<div class=" self-center mr-2 font-medium line-clamp-1">
-						{$i18n.t('Share to OpenWebUI Community')}
+						{$i18n.t('Share to Open WebUI Community')}
 					</div>
 
 					<div class=" self-center">

+ 3 - 1
src/lib/components/admin/Evaluations/Leaderboard.svelte

@@ -1,6 +1,8 @@
 <script lang="ts">
 	import * as ort from 'onnxruntime-web';
-	import { AutoModel, AutoTokenizer } from '@huggingface/transformers';
+	import { env, AutoModel, AutoTokenizer } from '@huggingface/transformers';
+
+	env.backends.onnx.wasm.wasmPaths = '/wasm/';
 
 	import { onMount, getContext } from 'svelte';
 	import { models } from '$lib/stores';

+ 34 - 8
src/lib/components/admin/Functions.svelte

@@ -3,7 +3,7 @@
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 
-	import { WEBUI_NAME, config, functions, models } from '$lib/stores';
+	import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores';
 	import { onMount, getContext, tick } from 'svelte';
 
 	import { goto } from '$app/navigation';
@@ -65,7 +65,7 @@
 			return null;
 		});
 
-		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+		toast.success($i18n.t('Redirecting you to Open WebUI Community'));
 
 		const url = 'https://openwebui.com';
 
@@ -126,7 +126,12 @@
 			toast.success($i18n.t('Function deleted successfully'));
 
 			functions.set(await getFunctions(localStorage.token));
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		}
 	};
 
@@ -147,7 +152,12 @@
 			}
 
 			functions.set(await getFunctions(localStorage.token));
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		}
 	};
 
@@ -359,7 +369,13 @@
 							bind:state={func.is_active}
 							on:change={async (e) => {
 								toggleFunctionById(localStorage.token, func.id);
-								models.set(await getModels(localStorage.token));
+								models.set(
+									await getModels(
+										localStorage.token,
+										$config?.features?.enable_direct_connections &&
+											($settings?.directConnections ?? null)
+									)
+								);
 							}}
 						/>
 					</Tooltip>
@@ -453,7 +469,7 @@
 {#if $config?.features.enable_community_sharing}
 	<div class=" my-16">
 		<div class=" text-xl font-medium mb-1 line-clamp-1">
-			{$i18n.t('Made by OpenWebUI Community')}
+			{$i18n.t('Made by Open WebUI Community')}
 		</div>
 
 		<a
@@ -496,7 +512,12 @@
 	id={selectedFunction?.id ?? null}
 	on:save={async () => {
 		await tick();
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	}}
 />
 
@@ -517,7 +538,12 @@
 
 			toast.success($i18n.t('Functions imported successfully'));
 			functions.set(await getFunctions(localStorage.token));
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		};
 
 		reader.readAsText(importFiles[0]);

+ 36 - 0
src/lib/components/admin/Settings.svelte

@@ -19,6 +19,7 @@
 	import ChartBar from '../icons/ChartBar.svelte';
 	import DocumentChartBar from '../icons/DocumentChartBar.svelte';
 	import Evaluations from './Settings/Evaluations.svelte';
+	import CodeInterpreter from './Settings/CodeInterpreter.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -188,6 +189,32 @@
 			<div class=" self-center">{$i18n.t('Web Search')}</div>
 		</button>
 
+		<button
+			class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+			'code-interpreter'
+				? ''
+				: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+			on:click={() => {
+				selectedTab = 'code-interpreter';
+			}}
+		>
+			<div class=" self-center mr-2">
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 16 16"
+					fill="currentColor"
+					class="size-4"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm2.22 1.97a.75.75 0 0 0 0 1.06l.97.97-.97.97a.75.75 0 1 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06l-1.5-1.5a.75.75 0 0 0-1.06 0ZM8.75 8.5a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z"
+						clip-rule="evenodd"
+					/>
+				</svg>
+			</div>
+			<div class=" self-center">{$i18n.t('Code Interpreter')}</div>
+		</button>
+
 		<button
 			class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'interface'
@@ -364,6 +391,15 @@
 					await config.set(await getBackendConfig());
 				}}
 			/>
+		{:else if selectedTab === 'code-interpreter'}
+			<CodeInterpreter
+				saveHandler={async () => {
+					toast.success($i18n.t('Settings saved successfully!'));
+
+					await tick();
+					await config.set(await getBackendConfig());
+				}}
+			/>
 		{:else if selectedTab === 'interface'}
 			<Interface
 				on:save={() => {

+ 41 - 3
src/lib/components/admin/Settings/Audio.svelte

@@ -10,7 +10,7 @@
 		getModels as _getModels,
 		getVoices as _getVoices
 	} from '$lib/apis/audio';
-	import { config } from '$lib/stores';
+	import { config, settings } from '$lib/stores';
 
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 
@@ -39,6 +39,7 @@
 	let STT_ENGINE = '';
 	let STT_MODEL = '';
 	let STT_WHISPER_MODEL = '';
+	let STT_DEEPGRAM_API_KEY = '';
 
 	let STT_WHISPER_MODEL_LOADING = false;
 
@@ -50,7 +51,10 @@
 		if (TTS_ENGINE === '') {
 			models = [];
 		} else {
-			const res = await _getModels(localStorage.token).catch((e) => {
+			const res = await _getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			).catch((e) => {
 				toast.error(`${e}`);
 			});
 
@@ -103,7 +107,8 @@
 				OPENAI_API_KEY: STT_OPENAI_API_KEY,
 				ENGINE: STT_ENGINE,
 				MODEL: STT_MODEL,
-				WHISPER_MODEL: STT_WHISPER_MODEL
+				WHISPER_MODEL: STT_WHISPER_MODEL,
+				DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY
 			}
 		});
 
@@ -143,6 +148,7 @@
 			STT_ENGINE = res.stt.ENGINE;
 			STT_MODEL = res.stt.MODEL;
 			STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
+			STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
 		}
 
 		await getVoices();
@@ -173,6 +179,7 @@
 							<option value="">{$i18n.t('Whisper (Local)')}</option>
 							<option value="openai">OpenAI</option>
 							<option value="web">{$i18n.t('Web API')}</option>
+							<option value="deepgram">Deepgram</option>
 						</select>
 					</div>
 				</div>
@@ -210,6 +217,37 @@
 							</div>
 						</div>
 					</div>
+				{:else if STT_ENGINE === 'deepgram'}
+					<div>
+						<div class="mt-1 flex gap-2 mb-1">
+							<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_DEEPGRAM_API_KEY} />
+						</div>
+					</div>
+
+					<hr class=" dark:border-gray-850 my-2" />
+
+					<div>
+						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
+						<div class="flex w-full">
+							<div class="flex-1">
+								<input
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									bind:value={STT_MODEL}
+									placeholder="Select a model (optional)"
+								/>
+							</div>
+						</div>
+						<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t('Leave model field empty to use the default model.')}
+							<a
+								class=" hover:underline dark:text-gray-200 text-gray-800"
+								href="https://developers.deepgram.com/docs/models"
+								target="_blank"
+							>
+								{$i18n.t('Click here to see available models.')}
+							</a>
+						</div>
+					</div>
 				{:else if STT_ENGINE === ''}
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>

+ 166 - 0
src/lib/components/admin/Settings/CodeInterpreter.svelte

@@ -0,0 +1,166 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { onMount, getContext } from 'svelte';
+	import { getCodeInterpreterConfig, setCodeInterpreterConfig } from '$lib/apis/configs';
+
+	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
+	import Switch from '$lib/components/common/Switch.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let saveHandler: Function;
+
+	let config = null;
+
+	let engines = ['pyodide', 'jupyter'];
+
+	const submitHandler = async () => {
+		const res = await setCodeInterpreterConfig(localStorage.token, config);
+	};
+
+	onMount(async () => {
+		const res = await getCodeInterpreterConfig(localStorage.token);
+
+		if (res) {
+			config = res;
+		}
+	});
+</script>
+
+<form
+	class="flex flex-col h-full justify-between space-y-3 text-sm"
+	on:submit|preventDefault={async () => {
+		await submitHandler();
+		saveHandler();
+	}}
+>
+	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
+		{#if config}
+			<div>
+				<div class=" mb-1 text-sm font-medium">
+					{$i18n.t('Code Interpreter')}
+				</div>
+
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Enable Code Interpreter')}
+						</div>
+
+						<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
+					</div>
+				</div>
+
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">{$i18n.t('Code Interpreter Engine')}</div>
+					<div class="flex items-center relative">
+						<select
+							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							bind:value={config.CODE_INTERPRETER_ENGINE}
+							placeholder={$i18n.t('Select a engine')}
+							required
+						>
+							<option disabled selected value="">{$i18n.t('Select a engine')}</option>
+							{#each engines as engine}
+								<option value={engine}>{engine}</option>
+							{/each}
+						</select>
+					</div>
+				</div>
+
+				{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
+					<div class="mt-1 flex flex-col gap-1.5 mb-1 w-full">
+						<div class="text-xs font-medium">
+							{$i18n.t('Jupyter URL')}
+						</div>
+
+						<div class="flex w-full">
+							<div class="flex-1">
+								<input
+									class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-none"
+									type="text"
+									placeholder={$i18n.t('Enter Jupyter URL')}
+									bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
+									autocomplete="off"
+								/>
+							</div>
+						</div>
+					</div>
+
+					<div class="mt-1 flex gap-2 mb-1 w-full items-center justify-between">
+						<div class="text-xs font-medium">
+							{$i18n.t('Jupyter Auth')}
+						</div>
+
+						<div>
+							<select
+								class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
+								bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
+								placeholder={$i18n.t('Select an auth method')}
+							>
+								<option selected value="">{$i18n.t('None')}</option>
+								<option value="token">{$i18n.t('Token')}</option>
+								<option value="password">{$i18n.t('Password')}</option>
+							</select>
+						</div>
+					</div>
+
+					{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
+						<div class="flex w-full gap-2">
+							<div class="flex-1">
+								{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
+									<SensitiveInput
+										type="text"
+										placeholder={$i18n.t('Enter Jupyter Password')}
+										bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
+										autocomplete="off"
+									/>
+								{:else}
+									<SensitiveInput
+										type="text"
+										placeholder={$i18n.t('Enter Jupyter Token')}
+										bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
+										autocomplete="off"
+									/>
+								{/if}
+							</div>
+						</div>
+					{/if}
+				{/if}
+			</div>
+
+			<hr class=" dark:border-gray-850 my-2" />
+
+			<div>
+				<div class="py-0.5 w-full">
+					<div class=" mb-2.5 text-xs font-medium">
+						{$i18n.t('Code Interpreter Prompt Template')}
+					</div>
+
+					<Tooltip
+						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+						placement="top-start"
+					>
+						<Textarea
+							bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
+							placeholder={$i18n.t(
+								'Leave empty to use the default prompt, or enter a custom prompt'
+							)}
+						/>
+					</Tooltip>
+				</div>
+			</div>
+		{/if}
+	</div>
+	<div class="flex justify-end pt-3 text-sm font-medium">
+		<button
+			class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
+			type="submit"
+		>
+			{$i18n.t('Save')}
+		</button>
+	</div>
+</form>

+ 78 - 17
src/lib/components/admin/Settings/Connections.svelte

@@ -7,8 +7,9 @@
 	import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
 	import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
 	import { getModels as _getModels } from '$lib/apis';
+	import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
 
-	import { models, user } from '$lib/stores';
+	import { config, models, settings, user } from '$lib/stores';
 
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
@@ -16,13 +17,16 @@
 	import Plus from '$lib/components/icons/Plus.svelte';
 
 	import OpenAIConnection from './Connections/OpenAIConnection.svelte';
-	import AddConnectionModal from './Connections/AddConnectionModal.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
 	import OllamaConnection from './Connections/OllamaConnection.svelte';
 
 	const i18n = getContext('i18n');
 
 	const getModels = async () => {
-		const models = await _getModels(localStorage.token);
+		const models = await _getModels(
+			localStorage.token,
+			$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+		);
 		return models;
 	};
 
@@ -37,6 +41,8 @@
 	let ENABLE_OPENAI_API: null | boolean = null;
 	let ENABLE_OLLAMA_API: null | boolean = null;
 
+	let directConnectionsConfig = null;
+
 	let pipelineUrls = {};
 	let showAddOpenAIConnectionModal = false;
 	let showAddOllamaConnectionModal = false;
@@ -98,17 +104,33 @@
 		}
 	};
 
+	const updateDirectConnectionsHandler = async () => {
+		const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
+			(error) => {
+				toast.error(`${error}`);
+			}
+		);
+
+		if (res) {
+			toast.success($i18n.t('Direct Connections settings updated'));
+			await models.set(await getModels());
+		}
+	};
+
 	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();
 	};
@@ -124,6 +146,9 @@
 				})(),
 				(async () => {
 					openaiConfig = await getOpenAIConfig(localStorage.token);
+				})(),
+				(async () => {
+					directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
 				})()
 			]);
 
@@ -167,6 +192,14 @@
 			}
 		}
 	});
+
+	const submitHandler = async () => {
+		updateOpenAIHandler();
+		updateOllamaHandler();
+		updateDirectConnectionsHandler();
+
+		dispatch('save');
+	};
 </script>
 
 <AddConnectionModal
@@ -180,17 +213,9 @@
 	onSubmit={addOllamaConnectionHandler}
 />
 
-<form
-	class="flex flex-col h-full justify-between text-sm"
-	on:submit|preventDefault={() => {
-		updateOpenAIHandler();
-		updateOllamaHandler();
-
-		dispatch('save');
-	}}
->
+<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
-		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
+		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
 			<div class="my-2">
 				<div class="mt-2 space-y-2 pr-1.5">
 					<div class="flex justify-between items-center text-sm">
@@ -244,7 +269,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 +331,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}
@@ -322,6 +356,33 @@
 					</div>
 				{/if}
 			</div>
+
+			<hr class=" border-gray-50 dark:border-gray-850" />
+
+			<div class="pr-1.5 my-2">
+				<div class="flex justify-between items-center text-sm">
+					<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
+
+					<div class="flex items-center">
+						<div class="">
+							<Switch
+								bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
+								on:change={async () => {
+									updateDirectConnectionsHandler();
+								}}
+							/>
+						</div>
+					</div>
+				</div>
+
+				<div class="mt-1.5">
+					<div class="text-xs text-gray-500">
+						{$i18n.t(
+							'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
+						)}
+					</div>
+				</div>
+			</div>
 		{:else}
 			<div class="flex h-full justify-center">
 				<div class="my-auto">

+ 1 - 1
src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

@@ -4,7 +4,7 @@
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-	import AddConnectionModal from './AddConnectionModal.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
 
 	import Cog6 from '$lib/components/icons/Cog6.svelte';
 	import Wrench from '$lib/components/icons/Wrench.svelte';

+ 2 - 1
src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte

@@ -5,7 +5,8 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Cog6 from '$lib/components/icons/Cog6.svelte';
-	import AddConnectionModal from './AddConnectionModal.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
+
 	import { connect } from 'socket.io-client';
 
 	export let onDelete = () => {};

+ 39 - 19
src/lib/components/admin/Settings/Evaluations.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
-	import { models, user } from '$lib/stores';
+	import { models, settings, user, config } from '$lib/stores';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
 
 	const dispatch = createEventDispatcher();
@@ -16,49 +16,69 @@
 
 	const i18n = getContext('i18n');
 
-	let config = null;
+	let evaluationConfig = null;
 	let showAddModel = false;
 
 	const submitHandler = async () => {
-		config = await updateConfig(localStorage.token, config).catch((err) => {
+		evaluationConfig = await updateConfig(localStorage.token, evaluationConfig).catch((err) => {
 			toast.error(err);
 			return null;
 		});
 
-		if (config) {
+		if (evaluationConfig) {
 			toast.success('Settings saved successfully');
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		}
 	};
 
 	const addModelHandler = async (model) => {
-		config.EVALUATION_ARENA_MODELS.push(model);
-		config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
+		evaluationConfig.EVALUATION_ARENA_MODELS.push(model);
+		evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
 
 		await submitHandler();
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	const editModelHandler = async (model, modelIdx) => {
-		config.EVALUATION_ARENA_MODELS[modelIdx] = model;
-		config.EVALUATION_ARENA_MODELS = [...config.EVALUATION_ARENA_MODELS];
+		evaluationConfig.EVALUATION_ARENA_MODELS[modelIdx] = model;
+		evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
 
 		await submitHandler();
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	const deleteModelHandler = async (modelIdx) => {
-		config.EVALUATION_ARENA_MODELS = config.EVALUATION_ARENA_MODELS.filter(
+		evaluationConfig.EVALUATION_ARENA_MODELS = evaluationConfig.EVALUATION_ARENA_MODELS.filter(
 			(m, mIdx) => mIdx !== modelIdx
 		);
 
 		await submitHandler();
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	onMount(async () => {
 		if ($user.role === 'admin') {
-			config = await getConfig(localStorage.token).catch((err) => {
+			evaluationConfig = await getConfig(localStorage.token).catch((err) => {
 				toast.error(err);
 				return null;
 			});
@@ -81,7 +101,7 @@
 	}}
 >
 	<div class="overflow-y-scroll scrollbar-hidden h-full">
-		{#if config !== null}
+		{#if evaluationConfig !== null}
 			<div class="">
 				<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div>
 
@@ -90,12 +110,12 @@
 						<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
 
 						<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
-							<Switch bind:state={config.ENABLE_EVALUATION_ARENA_MODELS} />
+							<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
 						</Tooltip>
 					</div>
 				</div>
 
-				{#if config.ENABLE_EVALUATION_ARENA_MODELS}
+				{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
 					<hr class=" border-gray-50 dark:border-gray-700/10 my-2" />
 
 					<div class="flex justify-between items-center mb-2">
@@ -117,8 +137,8 @@
 					</div>
 
 					<div class="flex flex-col gap-2">
-						{#if (config?.EVALUATION_ARENA_MODELS ?? []).length > 0}
-							{#each config.EVALUATION_ARENA_MODELS as model, index}
+						{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
+							{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
 								<Model
 									{model}
 									on:edit={(e) => {

+ 20 - 4
src/lib/components/admin/Settings/Models.svelte

@@ -68,7 +68,7 @@
 
 	const init = async () => {
 		workspaceModels = await getBaseModels(localStorage.token);
-		baseModels = await getModels(localStorage.token, true);
+		baseModels = await getModels(localStorage.token, null, true);
 
 		models = baseModels.map((m) => {
 			const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
@@ -111,7 +111,12 @@
 			}
 		}
 
-		_models.set(await getModels(localStorage.token));
+		_models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 		await init();
 	};
 
@@ -133,7 +138,12 @@
 		}
 
 		// await init();
-		_models.set(await getModels(localStorage.token));
+		_models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	onMount(async () => {
@@ -330,7 +340,13 @@
 									}
 								}
 
-								await _models.set(await getModels(localStorage.token));
+								await _models.set(
+									await getModels(
+										localStorage.token,
+										$config?.features?.enable_direct_connections &&
+											($settings?.directConnections ?? null)
+									)
+								);
 								init();
 							};
 

+ 25 - 5
src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte

@@ -3,7 +3,7 @@
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 
-	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
+	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config, settings } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 
 	import {
@@ -235,7 +235,12 @@
 					})
 				);
 
-				models.set(await getModels(localStorage.token));
+				models.set(
+					await getModels(
+						localStorage.token,
+						$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					)
+				);
 			} else {
 				toast.error($i18n.t('Download canceled'));
 			}
@@ -394,7 +399,12 @@
 		modelTransferring = false;
 		uploadProgress = null;
 
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	const deleteModelHandler = async () => {
@@ -407,7 +417,12 @@
 		}
 
 		deleteModelTag = '';
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 	};
 
 	const cancelModelPullHandler = async (model: string) => {
@@ -506,7 +521,12 @@
 			}
 		}
 
-		models.set(await getModels(localStorage.token));
+		models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 
 		createModelLoading = false;
 

+ 25 - 5
src/lib/components/admin/Settings/Pipelines.svelte

@@ -2,7 +2,7 @@
 	import { v4 as uuidv4 } from 'uuid';
 
 	import { toast } from 'svelte-sonner';
-	import { models } from '$lib/stores';
+	import { config, models, settings } from '$lib/stores';
 	import { getContext, onMount, tick } from 'svelte';
 	import type { Writable } from 'svelte/store';
 	import type { i18n as i18nType } from 'i18next';
@@ -63,7 +63,12 @@
 			if (res) {
 				toast.success($i18n.t('Valves updated successfully'));
 				setPipelines();
-				models.set(await getModels(localStorage.token));
+				models.set(
+					await getModels(
+						localStorage.token,
+						$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					)
+				);
 				saveHandler();
 			}
 		} else {
@@ -125,7 +130,12 @@
 		if (res) {
 			toast.success($i18n.t('Pipeline downloaded successfully'));
 			setPipelines();
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		}
 
 		downloading = false;
@@ -150,7 +160,12 @@
 			if (res) {
 				toast.success($i18n.t('Pipeline downloaded successfully'));
 				setPipelines();
-				models.set(await getModels(localStorage.token));
+				models.set(
+					await getModels(
+						localStorage.token,
+						$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					)
+				);
 			}
 		} else {
 			toast.error($i18n.t('No file selected'));
@@ -179,7 +194,12 @@
 		if (res) {
 			toast.success($i18n.t('Pipeline deleted successfully'));
 			setPipelines();
-			models.set(await getModels(localStorage.token));
+			models.set(
+				await getModels(
+					localStorage.token,
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				)
+			);
 		}
 	};
 

+ 42 - 0
src/lib/components/admin/Settings/WebSearch.svelte

@@ -18,6 +18,7 @@
 		'brave',
 		'kagi',
 		'mojeek',
+		'bocha',
 		'serpstack',
 		'serper',
 		'serply',
@@ -34,6 +35,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: {
@@ -42,6 +53,8 @@
 				proxy_url: youtubeProxyUrl
 			}
 		});
+
+		webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
 	};
 
 	onMount(async () => {
@@ -49,6 +62,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;
@@ -179,6 +196,17 @@
 									bind:value={webConfig.search.mojeek_search_api_key}
 								/>
 							</div>
+						{:else if webConfig.search.engine === 'bocha'}
+							<div>
+								<div class=" self-center text-xs font-medium mb-1">
+									{$i18n.t('Bocha Search API Key')}
+								</div>
+
+								<SensitiveInput
+									placeholder={$i18n.t('Enter Bocha Search API Key')}
+									bind:value={webConfig.search.bocha_search_api_key}
+								/>
+							</div>
 						{:else if webConfig.search.engine === 'serpstack'}
 							<div>
 								<div class=" self-center text-xs font-medium mb-1">
@@ -334,6 +362,20 @@
 							/>
 						</div>
 					</div>
+
+					<div class="mt-2">
+						<div class=" self-center text-xs font-medium mb-1">
+							{$i18n.t('Domain Filter List')}
+						</div>
+
+						<input
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							placeholder={$i18n.t(
+								'Enter domains separated by commas (e.g., example.com,site.org)'
+							)}
+							bind:value={webConfig.search.domain_filter_list}
+						/>
+					</div>
 				{/if}
 			</div>
 

+ 12 - 9
src/lib/components/chat/Chat.svelte

@@ -1226,7 +1226,7 @@
 			selectedModels = _selectedModels;
 		}
 
-		if (userPrompt === '') {
+		if (userPrompt === '' && files.length === 0) {
 			toast.error($i18n.t('Please enter a prompt'));
 			return;
 		}
@@ -1478,7 +1478,7 @@
 			params?.stream_response ??
 			true;
 
-		const messages = [
+		let messages = [
 			params?.system || $settings.system || (responseMessage?.userContext ?? null)
 				? {
 						role: 'system',
@@ -1499,8 +1499,9 @@
 				...message,
 				content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
 			}))
-		]
-			.filter((message) => message?.content?.trim())
+		].filter((message) => message);
+
+		messages = messages
 			.map((message, idx, arr) => ({
 				role: message.role,
 				...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
@@ -1524,7 +1525,8 @@
 					: {
 							content: message?.merged?.content ?? message.content
 						})
-			}));
+			}))
+			.filter((message) => message?.role === 'user' || message?.content?.trim());
 
 		const res = await generateOpenAIChatCompletion(
 			localStorage.token,
@@ -1556,7 +1558,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:
@@ -1581,7 +1584,7 @@
 					(messages.length == 2 &&
 						messages.at(0)?.role === 'system' &&
 						messages.at(1)?.role === 'user')) &&
-				selectedModels[0] === model.id
+				(selectedModels[0] === model.id || atSelectedModel !== undefined)
 					? {
 							background_tasks: {
 								title_generation: $settings?.title?.auto ?? true,
@@ -2006,7 +2009,7 @@
 									}
 								}}
 								on:submit={async (e) => {
-									if (e.detail) {
+									if (e.detail || files.length > 0) {
 										await tick();
 										submitPrompt(
 											($settings?.richTextInput ?? true)
@@ -2049,7 +2052,7 @@
 									}
 								}}
 								on:submit={async (e) => {
-									if (e.detail) {
+									if (e.detail || files.length > 0) {
 										await tick();
 										submitPrompt(
 											($settings?.richTextInput ?? true)

+ 19 - 7
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"
 															>
 																<path
 																	d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
@@ -695,7 +697,7 @@
 													)}
 												placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
 												largeTextAsFile={$settings?.largeTextAsFile ?? false}
-												autocomplete={true}
+												autocomplete={$config?.features.enable_autocomplete_generation}
 												generateAutoCompletion={async (text) => {
 													if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
 														toast.error($i18n.t('Please select a model first.'));
@@ -1169,7 +1171,7 @@
 													</Tooltip>
 												{/if}
 
-												{#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter}
+												{#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
 													<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
 														<button
 															on:click|preventDefault={() =>
@@ -1242,7 +1244,7 @@
 										{/if}
 
 										{#if !history.currentId || history.messages[history.currentId]?.done == true}
-											{#if prompt === ''}
+											{#if prompt === '' && files.length === 0}
 												<div class=" flex items-center">
 													<Tooltip content={$i18n.t('Call')}>
 														<button
@@ -1281,6 +1283,16 @@
 
 																	stream = null;
 
+																	if (!$TTSWorker) {
+																		await TTSWorker.set(
+																			new KokoroWorker({
+																				dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
+																			})
+																		);
+
+																		await $TTSWorker.init();
+																	}
+
 																	showCallOverlay.set(true);
 																	showControls.set(true);
 																} catch (err) {
@@ -1301,13 +1313,13 @@
 													<Tooltip content={$i18n.t('Send message')}>
 														<button
 															id="send-message-button"
-															class="{prompt !== ''
+															class="{!(prompt === '' && files.length === 0)
 																? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
 																	? 'bg-blue-500 text-white hover:bg-blue-400 '
 																	: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
 																: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
 															type="submit"
-															disabled={prompt === ''}
+															disabled={prompt === '' && files.length === 0}
 														>
 															<svg
 																xmlns="http://www.w3.org/2000/svg"

+ 17 - 2
src/lib/components/chat/MessageInput/CallOverlay.svelte

@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { config, models, settings, showCallOverlay } from '$lib/stores';
+	import { config, models, settings, showCallOverlay, TTSWorker } from '$lib/stores';
 	import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
 
 	const dispatch = createEventDispatcher();
@@ -12,6 +12,7 @@
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
+	import { KokoroWorker } from '$lib/workers/KokoroWorker';
 
 	const i18n = getContext('i18n');
 
@@ -459,7 +460,21 @@
 					}
 				}
 
-				if ($config.audio.tts.engine !== '') {
+				if ($settings.audio?.tts?.engine === 'browser-kokoro') {
+					const blob = await $TTSWorker
+						.generate({
+							text: content,
+							voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
+						})
+						.catch((error) => {
+							console.error(error);
+							toast.error(`${error}`);
+						});
+
+					if (blob) {
+						audioCache.set(content, new Audio(blob));
+					}
+				} else if ($config.audio.tts.engine !== '') {
 					const res = await synthesizeOpenAISpeech(
 						localStorage.token,
 						$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice

+ 20 - 11
src/lib/components/chat/Messages/Citations.svelte

@@ -97,7 +97,7 @@
 {#if citations.length > 0}
 	<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
 		{#if citations.length <= 3}
-			<div class="flex text-xs font-medium">
+			<div class="flex text-xs font-medium flex-wrap">
 				{#each citations as citation, idx}
 					<button
 						id={`source-${citation.source.name}`}
@@ -113,7 +113,7 @@
 							</div>
 						{/if}
 						<div
-							class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
+							class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
 						>
 							{citation.source.name}
 						</div>
@@ -121,13 +121,22 @@
 				{/each}
 			</div>
 		{:else}
-			<Collapsible id="collapsible-sources" bind:open={isCollapsibleOpen} className="w-full">
+			<Collapsible
+				id="collapsible-sources"
+				bind:open={isCollapsibleOpen}
+				className="w-full max-w-full "
+				buttonClassName="w-fit max-w-full"
+			>
 				<div
-					class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
+					class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
 				>
-					<div class="flex-grow flex items-center gap-1 overflow-hidden">
-						<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
-						<div class="flex items-center">
+					<div
+						class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
+					>
+						<span class="whitespace-nowrap hidden sm:inline flex-shrink-0"
+							>{$i18n.t('References from')}</span
+						>
+						<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
 							<div class="flex text-xs font-medium items-center">
 								{#each citations.slice(0, 2) as citation, idx}
 									<button
@@ -145,14 +154,14 @@
 												{idx + 1}
 											</div>
 										{/if}
-										<div class="flex-1 mx-1 line-clamp-1 truncate">
+										<div class="flex-1 mx-1 truncate">
 											{citation.source.name}
 										</div>
 									</button>
 								{/each}
 							</div>
 						</div>
-						<div class="flex items-center gap-1 whitespace-nowrap">
+						<div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0">
 							<span class="hidden sm:inline">{$i18n.t('and')}</span>
 							{citations.length - 2}
 							<span>{$i18n.t('more')}</span>
@@ -167,7 +176,7 @@
 					</div>
 				</div>
 				<div slot="content">
-					<div class="flex text-xs font-medium">
+					<div class="flex text-xs font-medium flex-wrap">
 						{#each citations as citation, idx}
 							<button
 								id={`source-${citation.source.name}`}
@@ -182,7 +191,7 @@
 										{idx + 1}
 									</div>
 								{/if}
-								<div class="flex-1 mx-1 line-clamp-1 truncate">
+								<div class="flex-1 mx-1 truncate">
 									{citation.source.name}
 								</div>
 							</button>

+ 17 - 2
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();
+		}
+	});
 </script>
 
 <div>

+ 98 - 61
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<Writable<i18nType>>('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,7 +194,42 @@
 
 		speaking = true;
 
-		if ($config.audio.tts.engine !== '') {
+		if ($config.audio.tts.engine === '') {
+			let voices = [];
+			const getVoicesLoop = setInterval(() => {
+				voices = speechSynthesis.getVoices();
+				if (voices.length > 0) {
+					clearInterval(getVoicesLoop);
+
+					const voice =
+						voices
+							?.filter(
+								(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
+							)
+							?.at(0) ?? undefined;
+
+					console.log(voice);
+
+					const speak = new SpeechSynthesisUtterance(message.content);
+					speak.rate = $settings.audio?.tts?.playbackRate ?? 1;
+
+					console.log(speak);
+
+					speak.onend = () => {
+						speaking = false;
+						if ($settings.conversationMode) {
+							document.getElementById('voice-input-button')?.click();
+						}
+					};
+
+					if (voice) {
+						speak.voice = voice;
+					}
+
+					speechSynthesis.speak(speak);
+				}
+			}, 100);
+		} else {
 			loadingSpeech = true;
 
 			const messageContentParts: string[] = getMessageContentParts(
@@ -222,67 +258,68 @@
 
 			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}`);
+			if ($settings.audio?.tts?.engine === 'browser-kokoro') {
+				if (!$TTSWorker) {
+					await TTSWorker.set(
+						new KokoroWorker({
+							dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
+						})
+					);
 
-					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));
+					await $TTSWorker.init();
 				}
-			}
-		} else {
-			let voices = [];
-			const getVoicesLoop = setInterval(() => {
-				voices = speechSynthesis.getVoices();
-				if (voices.length > 0) {
-					clearInterval(getVoicesLoop);
-
-					const voice =
-						voices
-							?.filter(
-								(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
-							)
-							?.at(0) ?? undefined;
-
-					console.log(voice);
-
-					const speak = new SpeechSynthesisUtterance(message.content);
-					speak.rate = $settings.audio?.tts?.playbackRate ?? 1;
 
-					console.log(speak);
+				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}`);
 
-					speak.onend = () => {
 						speaking = false;
-						if ($settings.conversationMode) {
-							document.getElementById('voice-input-button')?.click();
-						}
-					};
+						loadingSpeech = false;
+					});
 
-					if (voice) {
-						speak.voice = voice;
-					}
+					if (res) {
+						const blob = await res.blob();
+						const blobUrl = URL.createObjectURL(blob);
+						const audio = new Audio(blobUrl);
+						audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
 
-					speechSynthesis.speak(speak);
+						audioParts[idx] = audio;
+						loadingSpeech = false;
+						lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
+					}
 				}
-			}, 100);
+			}
 		}
 	};
 

+ 207 - 201
src/lib/components/chat/Messages/UserMessage.svelte

@@ -145,274 +145,280 @@
 				</div>
 			{/if}
 
-			{#if edit === true}
-				<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
-					<div class="max-h-96 overflow-auto">
-						<textarea
-							id="message-edit-{message.id}"
-							bind:this={messageEditTextAreaElement}
-							class=" bg-transparent outline-none w-full resize-none"
-							bind:value={editedContent}
-							on:input={(e) => {
-								e.target.style.height = '';
-								e.target.style.height = `${e.target.scrollHeight}px`;
-							}}
-							on:keydown={(e) => {
-								if (e.key === 'Escape') {
-									document.getElementById('close-edit-message-button')?.click();
-								}
-
-								const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
-								const isEnterPressed = e.key === 'Enter';
-
-								if (isCmdOrCtrlPressed && isEnterPressed) {
-									document.getElementById('confirm-edit-message-button')?.click();
-								}
-							}}
-						/>
-					</div>
+			{#if message.content !== ''}
+				{#if edit === true}
+					<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
+						<div class="max-h-96 overflow-auto">
+							<textarea
+								id="message-edit-{message.id}"
+								bind:this={messageEditTextAreaElement}
+								class=" bg-transparent outline-none w-full resize-none"
+								bind:value={editedContent}
+								on:input={(e) => {
+									e.target.style.height = '';
+									e.target.style.height = `${e.target.scrollHeight}px`;
+								}}
+								on:keydown={(e) => {
+									if (e.key === 'Escape') {
+										document.getElementById('close-edit-message-button')?.click();
+									}
 
-					<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
-						<div>
-							<button
-								id="save-edit-message-button"
-								class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
-								on:click={() => {
-									editMessageConfirmHandler(false);
+									const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
+									const isEnterPressed = e.key === 'Enter';
+
+									if (isCmdOrCtrlPressed && isEnterPressed) {
+										document.getElementById('confirm-edit-message-button')?.click();
+									}
 								}}
-							>
-								{$i18n.t('Save')}
-							</button>
+							/>
 						</div>
 
-						<div class="flex space-x-1.5">
-							<button
-								id="close-edit-message-button"
-								class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
-								on:click={() => {
-									cancelEditMessage();
-								}}
-							>
-								{$i18n.t('Cancel')}
-							</button>
-
-							<button
-								id="confirm-edit-message-button"
-								class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
-								on:click={() => {
-									editMessageConfirmHandler();
-								}}
-							>
-								{$i18n.t('Send')}
-							</button>
+						<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
+							<div>
+								<button
+									id="save-edit-message-button"
+									class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
+									on:click={() => {
+										editMessageConfirmHandler(false);
+									}}
+								>
+									{$i18n.t('Save')}
+								</button>
+							</div>
+
+							<div class="flex space-x-1.5">
+								<button
+									id="close-edit-message-button"
+									class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
+									on:click={() => {
+										cancelEditMessage();
+									}}
+								>
+									{$i18n.t('Cancel')}
+								</button>
+
+								<button
+									id="confirm-edit-message-button"
+									class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
+									on:click={() => {
+										editMessageConfirmHandler();
+									}}
+								>
+									{$i18n.t('Send')}
+								</button>
+							</div>
 						</div>
 					</div>
-				</div>
-			{:else}
-				<div class="w-full">
-					<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
-						<div
-							class="rounded-3xl {($settings?.chatBubble ?? true)
-								? `max-w-[90%] px-5 py-2  bg-gray-50 dark:bg-gray-850 ${
-										message.files ? 'rounded-tr-lg' : ''
-									}`
-								: ' w-full'}"
-						>
-							{#if message.content}
-								<Markdown id={message.id} content={message.content} />
-							{/if}
+				{:else}
+					<div class="w-full">
+						<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
+							<div
+								class="rounded-3xl {($settings?.chatBubble ?? true)
+									? `max-w-[90%] px-5 py-2  bg-gray-50 dark:bg-gray-850 ${
+											message.files ? 'rounded-tr-lg' : ''
+										}`
+									: ' w-full'}"
+							>
+								{#if message.content}
+									<Markdown id={message.id} content={message.content} />
+								{/if}
+							</div>
 						</div>
-					</div>
 
-					<div
-						class=" flex {($settings?.chatBubble ?? true)
-							? 'justify-end'
-							: ''}  text-gray-600 dark:text-gray-500"
-					>
-						{#if !($settings?.chatBubble ?? true)}
-							{#if siblings.length > 1}
-								<div class="flex self-center" dir="ltr">
-									<button
-										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
-										on:click={() => {
-											showPreviousMessage(message);
-										}}
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											fill="none"
-											viewBox="0 0 24 24"
-											stroke="currentColor"
-											stroke-width="2.5"
-											class="size-3.5"
+						<div
+							class=" flex {($settings?.chatBubble ?? true)
+								? 'justify-end'
+								: ''}  text-gray-600 dark:text-gray-500"
+						>
+							{#if !($settings?.chatBubble ?? true)}
+								{#if siblings.length > 1}
+									<div class="flex self-center" dir="ltr">
+										<button
+											class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+											on:click={() => {
+												showPreviousMessage(message);
+											}}
 										>
-											<path
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												d="M15.75 19.5 8.25 12l7.5-7.5"
-											/>
-										</svg>
-									</button>
-
-									<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
-										{siblings.indexOf(message.id) + 1}/{siblings.length}
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke="currentColor"
+												stroke-width="2.5"
+												class="size-3.5"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M15.75 19.5 8.25 12l7.5-7.5"
+												/>
+											</svg>
+										</button>
+
+										<div
+											class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
+										>
+											{siblings.indexOf(message.id) + 1}/{siblings.length}
+										</div>
+
+										<button
+											class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+											on:click={() => {
+												showNextMessage(message);
+											}}
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke="currentColor"
+												stroke-width="2.5"
+												class="size-3.5"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="m8.25 4.5 7.5 7.5-7.5 7.5"
+												/>
+											</svg>
+										</button>
 									</div>
-
+								{/if}
+							{/if}
+							{#if !readOnly}
+								<Tooltip content={$i18n.t('Edit')} placement="bottom">
 									<button
-										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
 										on:click={() => {
-											showNextMessage(message);
+											editMessageHandler();
 										}}
 									>
 										<svg
 											xmlns="http://www.w3.org/2000/svg"
 											fill="none"
 											viewBox="0 0 24 24"
+											stroke-width="2.3"
 											stroke="currentColor"
-											stroke-width="2.5"
-											class="size-3.5"
+											class="w-4 h-4"
 										>
 											<path
 												stroke-linecap="round"
 												stroke-linejoin="round"
-												d="m8.25 4.5 7.5 7.5-7.5 7.5"
+												d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 											/>
 										</svg>
 									</button>
-								</div>
+								</Tooltip>
 							{/if}
-						{/if}
-						{#if !readOnly}
-							<Tooltip content={$i18n.t('Edit')} placement="bottom">
-								<button
-									class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
-									on:click={() => {
-										editMessageHandler();
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										fill="none"
-										viewBox="0 0 24 24"
-										stroke-width="2.3"
-										stroke="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											stroke-linecap="round"
-											stroke-linejoin="round"
-											d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
-										/>
-									</svg>
-								</button>
-							</Tooltip>
-						{/if}
 
-						<Tooltip content={$i18n.t('Copy')} placement="bottom">
-							<button
-								class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
-								on:click={() => {
-									copyToClipboard(message.content);
-								}}
-							>
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									fill="none"
-									viewBox="0 0 24 24"
-									stroke-width="2.3"
-									stroke="currentColor"
-									class="w-4 h-4"
-								>
-									<path
-										stroke-linecap="round"
-										stroke-linejoin="round"
-										d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
-									/>
-								</svg>
-							</button>
-						</Tooltip>
-
-						{#if !isFirstMessage && !readOnly}
-							<Tooltip content={$i18n.t('Delete')} placement="bottom">
+							<Tooltip content={$i18n.t('Copy')} placement="bottom">
 								<button
-									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+									class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
 									on:click={() => {
-										deleteMessageHandler();
+										copyToClipboard(message.content);
 									}}
 								>
 									<svg
 										xmlns="http://www.w3.org/2000/svg"
 										fill="none"
 										viewBox="0 0 24 24"
-										stroke-width="2"
+										stroke-width="2.3"
 										stroke="currentColor"
 										class="w-4 h-4"
 									>
 										<path
 											stroke-linecap="round"
 											stroke-linejoin="round"
-											d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+											d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
 										/>
 									</svg>
 								</button>
 							</Tooltip>
-						{/if}
 
-						{#if $settings?.chatBubble ?? true}
-							{#if siblings.length > 1}
-								<div class="flex self-center" dir="ltr">
+							{#if !isFirstMessage && !readOnly}
+								<Tooltip content={$i18n.t('Delete')} placement="bottom">
 									<button
-										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+										class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 										on:click={() => {
-											showPreviousMessage(message);
+											deleteMessageHandler();
 										}}
 									>
 										<svg
 											xmlns="http://www.w3.org/2000/svg"
 											fill="none"
 											viewBox="0 0 24 24"
+											stroke-width="2"
 											stroke="currentColor"
-											stroke-width="2.5"
-											class="size-3.5"
+											class="w-4 h-4"
 										>
 											<path
 												stroke-linecap="round"
 												stroke-linejoin="round"
-												d="M15.75 19.5 8.25 12l7.5-7.5"
+												d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
 											/>
 										</svg>
 									</button>
+								</Tooltip>
+							{/if}
 
-									<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
-										{siblings.indexOf(message.id) + 1}/{siblings.length}
-									</div>
-
-									<button
-										class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
-										on:click={() => {
-											showNextMessage(message);
-										}}
-									>
-										<svg
-											xmlns="http://www.w3.org/2000/svg"
-											fill="none"
-											viewBox="0 0 24 24"
-											stroke="currentColor"
-											stroke-width="2.5"
-											class="size-3.5"
+							{#if $settings?.chatBubble ?? true}
+								{#if siblings.length > 1}
+									<div class="flex self-center" dir="ltr">
+										<button
+											class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+											on:click={() => {
+												showPreviousMessage(message);
+											}}
 										>
-											<path
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												d="m8.25 4.5 7.5 7.5-7.5 7.5"
-											/>
-										</svg>
-									</button>
-								</div>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke="currentColor"
+												stroke-width="2.5"
+												class="size-3.5"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="M15.75 19.5 8.25 12l7.5-7.5"
+												/>
+											</svg>
+										</button>
+
+										<div
+											class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
+										>
+											{siblings.indexOf(message.id) + 1}/{siblings.length}
+										</div>
+
+										<button
+											class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
+											on:click={() => {
+												showNextMessage(message);
+											}}
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												fill="none"
+												viewBox="0 0 24 24"
+												stroke="currentColor"
+												stroke-width="2.5"
+												class="size-3.5"
+											>
+												<path
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													d="m8.25 4.5 7.5 7.5-7.5 7.5"
+												/>
+											</svg>
+										</button>
+									</div>
+								{/if}
 							{/if}
-						{/if}
+						</div>
 					</div>
-				</div>
+				{/if}
 			{/if}
 		</div>
 	</div>

+ 33 - 3
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -12,7 +12,15 @@
 
 	import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
 
-	import { user, MODEL_DOWNLOAD_POOL, models, mobile, temporaryChatEnabled } from '$lib/stores';
+	import {
+		user,
+		MODEL_DOWNLOAD_POOL,
+		models,
+		mobile,
+		temporaryChatEnabled,
+		settings,
+		config
+	} from '$lib/stores';
 	import { toast } from 'svelte-sonner';
 	import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
 	import { getModels } from '$lib/apis';
@@ -186,7 +194,12 @@
 					})
 				);
 
-				models.set(await getModels(localStorage.token));
+				models.set(
+					await getModels(
+						localStorage.token,
+						$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					)
+				);
 			} else {
 				toast.error($i18n.t('Download canceled'));
 			}
@@ -358,7 +371,24 @@
 
 								<!-- {JSON.stringify(item.info)} -->
 
-								{#if item.model.owned_by === 'openai'}
+								{#if item.model?.direct}
+									<Tooltip content={`${'Direct'}`}>
+										<div class="translate-y-[1px]">
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="size-3"
+											>
+												<path
+													fill-rule="evenodd"
+													d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
+													clip-rule="evenodd"
+												/>
+											</svg>
+										</div>
+									</Tooltip>
+								{:else if item.model.owned_by === 'openai'}
 									<Tooltip content={`${'External'}`}>
 										<div class="translate-y-[1px]">
 											<svg

+ 2 - 2
src/lib/components/chat/Navbar.svelte

@@ -114,7 +114,7 @@
 							</div>
 						</button>
 					</Menu>
-				{:else if $mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
+				{:else if $mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
 					<Tooltip content={$i18n.t('Controls')}>
 						<button
 							class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
@@ -130,7 +130,7 @@
 					</Tooltip>
 				{/if}
 
-				{#if !$mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)}
+				{#if !$mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)}
 					<Tooltip content={$i18n.t('Controls')}>
 						<button
 							class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"

+ 8 - 2
src/lib/components/chat/Settings/Account.svelte

@@ -3,7 +3,7 @@
 	import { onMount, getContext } from 'svelte';
 
 	import { user, config, settings } from '$lib/stores';
-	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
+	import { updateUserProfile, createAPIKey, getAPIKey, getSessionUser } from '$lib/apis/auths';
 
 	import UpdatePassword from './Account/UpdatePassword.svelte';
 	import { getGravatarUrl } from '$lib/apis/utils';
@@ -53,7 +53,13 @@
 		);
 
 		if (updatedUser) {
-			await user.set(updatedUser);
+			// Get Session User Info
+			const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
+				toast.error(`${error}`);
+				return null;
+			});
+
+			await user.set(sessionUser);
 			return true;
 		}
 		return false;

+ 162 - 16
src/lib/components/chat/Settings/Audio.svelte

@@ -1,11 +1,14 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
+	import { KokoroTTS } from 'kokoro-js';
 
 	import { user, settings, config } from '$lib/stores';
 	import { getVoices as _getVoices } from '$lib/apis/audio';
 
 	import Switch from '$lib/components/common/Switch.svelte';
+	import { round } from '@huggingface/transformers';
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	const dispatch = createEventDispatcher();
 
 	const i18n = getContext('i18n');
@@ -20,6 +23,13 @@
 
 	let STTEngine = '';
 
+	let TTSEngine = '';
+	let TTSEngineConfig = {};
+
+	let TTSModel = null;
+	let TTSModelProgress = null;
+	let TTSModelLoading = false;
+
 	let voices = [];
 	let voice = '';
 
@@ -28,23 +38,37 @@
 	const speedOptions = [2, 1.75, 1.5, 1.25, 1, 0.75, 0.5];
 
 	const getVoices = async () => {
-		if ($config.audio.tts.engine === '') {
-			const getVoicesLoop = setInterval(async () => {
-				voices = await speechSynthesis.getVoices();
+		if (TTSEngine === 'browser-kokoro') {
+			if (!TTSModel) {
+				await loadKokoro();
+			}
 
-				// do your loop
-				if (voices.length > 0) {
-					clearInterval(getVoicesLoop);
-				}
-			}, 100);
-		} else {
-			const res = await _getVoices(localStorage.token).catch((e) => {
-				toast.error(`${e}`);
+			voices = Object.entries(TTSModel.voices).map(([key, value]) => {
+				return {
+					id: key,
+					name: value.name,
+					localService: false
+				};
 			});
-
-			if (res) {
-				console.log(res);
-				voices = res.voices;
+		} else {
+			if ($config.audio.tts.engine === '') {
+				const getVoicesLoop = setInterval(async () => {
+					voices = await speechSynthesis.getVoices();
+
+					// do your loop
+					if (voices.length > 0) {
+						clearInterval(getVoicesLoop);
+					}
+				}, 100);
+			} else {
+				const res = await _getVoices(localStorage.token).catch((e) => {
+					toast.error(`${e}`);
+				});
+
+				if (res) {
+					console.log(res);
+					voices = res.voices;
+				}
 			}
 		}
 	};
@@ -67,6 +91,9 @@
 
 		STTEngine = $settings?.audio?.stt?.engine ?? '';
 
+		TTSEngine = $settings?.audio?.tts?.engine ?? '';
+		TTSEngineConfig = $settings?.audio?.tts?.engineConfig ?? {};
+
 		if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
 			voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
 		} else {
@@ -77,6 +104,51 @@
 
 		await getVoices();
 	});
+
+	$: if (TTSEngine && TTSEngineConfig) {
+		onTTSEngineChange();
+	}
+
+	const onTTSEngineChange = async () => {
+		if (TTSEngine === 'browser-kokoro') {
+			await loadKokoro();
+		}
+	};
+
+	const loadKokoro = async () => {
+		if (TTSEngine === 'browser-kokoro') {
+			voices = [];
+
+			if (TTSEngineConfig?.dtype) {
+				TTSModel = null;
+				TTSModelProgress = null;
+				TTSModelLoading = true;
+
+				const model_id = 'onnx-community/Kokoro-82M-v1.0-ONNX';
+
+				TTSModel = await KokoroTTS.from_pretrained(model_id, {
+					dtype: TTSEngineConfig.dtype, // Options: "fp32", "fp16", "q8", "q4", "q4f16"
+					device: !!navigator?.gpu ? 'webgpu' : 'wasm', // Detect WebGPU
+					progress_callback: (e) => {
+						TTSModelProgress = e;
+						console.log(e);
+					}
+				});
+
+				await getVoices();
+
+				// const rawAudio = await tts.generate(inputText, {
+				// 	// Use `tts.list_voices()` to list all available voices
+				// 	voice: voice
+				// });
+
+				// const blobUrl = URL.createObjectURL(await rawAudio.toBlob());
+				// const audio = new Audio(blobUrl);
+
+				// audio.play();
+			}
+		}
+	};
 </script>
 
 <form
@@ -88,6 +160,8 @@
 					engine: STTEngine !== '' ? STTEngine : undefined
 				},
 				tts: {
+					engine: TTSEngine !== '' ? TTSEngine : undefined,
+					engineConfig: TTSEngineConfig,
 					playbackRate: playbackRate,
 					voice: voice !== '' ? voice : undefined,
 					defaultVoice: $config?.audio?.tts?.voice ?? '',
@@ -142,6 +216,39 @@
 		<div>
 			<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
 
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
+				<div class="flex items-center relative">
+					<select
+						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+						bind:value={TTSEngine}
+						placeholder="Select an engine"
+					>
+						<option value="">{$i18n.t('Default')}</option>
+						<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
+					</select>
+				</div>
+			</div>
+
+			{#if TTSEngine === 'browser-kokoro'}
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">{$i18n.t('Kokoro.js Dtype')}</div>
+					<div class="flex items-center relative">
+						<select
+							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							bind:value={TTSEngineConfig.dtype}
+							placeholder="Select dtype"
+						>
+							<option value="" disabled selected>Select dtype</option>
+							<option value="fp32">fp32</option>
+							<option value="fp16">fp16</option>
+							<option value="q8">q8</option>
+							<option value="q4">q4</option>
+						</select>
+					</div>
+				</div>
+			{/if}
+
 			<div class=" py-0.5 flex w-full justify-between">
 				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
 
@@ -178,7 +285,46 @@
 
 		<hr class=" dark:border-gray-850" />
 
-		{#if $config.audio.tts.engine === ''}
+		{#if TTSEngine === 'browser-kokoro'}
+			{#if TTSModel}
+				<div>
+					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
+					<div class="flex w-full">
+						<div class="flex-1">
+							<input
+								list="voice-list"
+								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								bind:value={voice}
+								placeholder="Select a voice"
+							/>
+
+							<datalist id="voice-list">
+								{#each voices as voice}
+									<option value={voice.id}>{voice.name}</option>
+								{/each}
+							</datalist>
+						</div>
+					</div>
+				</div>
+			{:else}
+				<div>
+					<div class=" mb-2.5 text-sm font-medium flex gap-2 items-center">
+						<Spinner className="size-4" />
+
+						<div class=" text-sm font-medium shimmer">
+							{$i18n.t('Loading Kokoro.js...')}
+							{TTSModelProgress && TTSModelProgress.status === 'progress'
+								? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)`
+								: ''}
+						</div>
+					</div>
+
+					<div class="text-xs text-gray-500">
+						{$i18n.t('Please do not close the settings page while loading the model.')}
+					</div>
+				</div>
+			{/if}
+		{:else if $config.audio.tts.engine === ''}
 			<div>
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
 				<div class="flex w-full">

+ 152 - 0
src/lib/components/chat/Settings/Connections.svelte

@@ -0,0 +1,152 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
+	import { getModels as _getModels } from '$lib/apis';
+
+	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
+
+	import { models, settings, user } from '$lib/stores';
+
+	import Switch from '$lib/components/common/Switch.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Plus from '$lib/components/icons/Plus.svelte';
+	import Connection from './Connections/Connection.svelte';
+
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
+
+	export let saveSettings: Function;
+
+	let config = null;
+
+	let showConnectionModal = false;
+
+	const addConnectionHandler = async (connection) => {
+		config.OPENAI_API_BASE_URLS.push(connection.url);
+		config.OPENAI_API_KEYS.push(connection.key);
+		config.OPENAI_API_CONFIGS[config.OPENAI_API_BASE_URLS.length - 1] = connection.config;
+
+		await updateHandler();
+	};
+
+	const updateHandler = async () => {
+		// Remove trailing slashes
+		config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
+
+		// Check if API KEYS length is same than API URLS length
+		if (config.OPENAI_API_KEYS.length !== config.OPENAI_API_BASE_URLS.length) {
+			// if there are more keys than urls, remove the extra keys
+			if (config.OPENAI_API_KEYS.length > config.OPENAI_API_BASE_URLS.length) {
+				config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.slice(
+					0,
+					config.OPENAI_API_BASE_URLS.length
+				);
+			}
+
+			// if there are more urls than keys, add empty keys
+			if (config.OPENAI_API_KEYS.length < config.OPENAI_API_BASE_URLS.length) {
+				const diff = config.OPENAI_API_BASE_URLS.length - config.OPENAI_API_KEYS.length;
+				for (let i = 0; i < diff; i++) {
+					config.OPENAI_API_KEYS.push('');
+				}
+			}
+		}
+
+		await saveSettings({
+			directConnections: config
+		});
+	};
+
+	onMount(async () => {
+		config = $settings?.directConnections ?? {
+			OPENAI_API_BASE_URLS: [],
+			OPENAI_API_KEYS: [],
+			OPENAI_API_CONFIGS: {}
+		};
+	});
+</script>
+
+<AddConnectionModal direct bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
+
+<form
+	class="flex flex-col h-full justify-between text-sm"
+	on:submit|preventDefault={() => {
+		updateHandler();
+	}}
+>
+	<div class=" overflow-y-scroll scrollbar-hidden h-full">
+		{#if config !== null}
+			<div class="">
+				<div class="pr-1.5">
+					<div class="">
+						<div class="flex justify-between items-center mb-0.5">
+							<div class="font-medium">{$i18n.t('Manage Direct Connections')}</div>
+
+							<Tooltip content={$i18n.t(`Add Connection`)}>
+								<button
+									class="px-1"
+									on:click={() => {
+										showConnectionModal = true;
+									}}
+									type="button"
+								>
+									<Plus />
+								</button>
+							</Tooltip>
+						</div>
+
+						<div class="flex flex-col gap-1.5">
+							{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
+								<Connection
+									bind:url
+									bind:key={config.OPENAI_API_KEYS[idx]}
+									bind:config={config.OPENAI_API_CONFIGS[idx]}
+									onSubmit={() => {
+										updateHandler();
+									}}
+									onDelete={() => {
+										config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
+											(url, urlIdx) => idx !== urlIdx
+										);
+										config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
+											(key, keyIdx) => idx !== keyIdx
+										);
+
+										let newConfig = {};
+										config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
+											newConfig[newIdx] =
+												config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+										});
+										config.OPENAI_API_CONFIGS = newConfig;
+									}}
+								/>
+							{/each}
+						</div>
+					</div>
+
+					<div class="my-1.5">
+						<div class="text-xs text-gray-500">
+							{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
+						</div>
+					</div>
+				</div>
+			</div>
+		{:else}
+			<div class="flex h-full justify-center">
+				<div class="my-auto">
+					<Spinner className="size-6" />
+				</div>
+			</div>
+		{/if}
+	</div>
+
+	<div class="flex justify-end pt-3 text-sm font-medium">
+		<button
+			class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
+			type="submit"
+		>
+			{$i18n.t('Save')}
+		</button>
+	</div>
+</form>

+ 83 - 0
src/lib/components/chat/Settings/Connections/Connection.svelte

@@ -0,0 +1,83 @@
+<script lang="ts">
+	import { getContext, tick } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+	import Cog6 from '$lib/components/icons/Cog6.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
+
+	export let onDelete = () => {};
+	export let onSubmit = () => {};
+
+	export let pipeline = false;
+
+	export let url = '';
+	export let key = '';
+	export let config = {};
+
+	let showConfigModal = false;
+</script>
+
+<AddConnectionModal
+	edit
+	bind:show={showConfigModal}
+	connection={{
+		url,
+		key,
+		config
+	}}
+	{onDelete}
+	onSubmit={(connection) => {
+		url = connection.url;
+		key = connection.key;
+		config = connection.config;
+		onSubmit(connection);
+	}}
+/>
+
+<div class="flex w-full gap-2 items-center">
+	<Tooltip
+		className="w-full relative"
+		content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
+			url
+		})}
+		placement="top-start"
+	>
+		{#if !(config?.enable ?? true)}
+			<div
+				class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
+			></div>
+		{/if}
+		<div class="flex w-full">
+			<div class="flex-1 relative">
+				<input
+					class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
+					placeholder={$i18n.t('API Base URL')}
+					bind:value={url}
+					autocomplete="off"
+				/>
+			</div>
+
+			<SensitiveInput
+				inputClassName=" outline-none bg-transparent w-full"
+				placeholder={$i18n.t('API Key')}
+				bind:value={key}
+			/>
+		</div>
+	</Tooltip>
+
+	<div class="flex gap-1">
+		<Tooltip content={$i18n.t('Configure')} className="self-start">
+			<button
+				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
+				on:click={() => {
+					showConfigModal = true;
+				}}
+				type="button"
+			>
+				<Cog6 />
+			</button>
+		</Tooltip>
+	</div>
+</div>

+ 5 - 0
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,

+ 44 - 2
src/lib/components/chat/SettingsModal.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { getContext, tick } from 'svelte';
 	import { toast } from 'svelte-sonner';
-	import { models, settings, user } from '$lib/stores';
+	import { config, models, settings, user } from '$lib/stores';
 	import { updateUserSettings } from '$lib/apis/users';
 	import { getModels as _getModels } from '$lib/apis';
 	import { goto } from '$app/navigation';
@@ -17,6 +17,7 @@
 	import Personalization from './Settings/Personalization.svelte';
 	import SearchInput from '../layout/Sidebar/SearchInput.svelte';
 	import Search from '../icons/Search.svelte';
+	import Connections from './Settings/Connections.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -122,6 +123,11 @@
 				'alwaysonwebsearch'
 			]
 		},
+		{
+			id: 'connections',
+			title: 'Connections',
+			keywords: []
+		},
 		{
 			id: 'personalization',
 			title: 'Personalization',
@@ -316,7 +322,10 @@
 	};
 
 	const getModels = async () => {
-		return await _getModels(localStorage.token);
+		return await _getModels(
+			localStorage.token,
+			$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+		);
 	};
 
 	let selectedTab = 'general';
@@ -447,6 +456,32 @@
 								</div>
 								<div class=" self-center">{$i18n.t('Interface')}</div>
 							</button>
+						{:else if tabId === 'connections'}
+							{#if $user.role === 'admin' || ($user.role === 'user' && $config?.features?.enable_direct_connections)}
+								<button
+									class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+									'connections'
+										? ''
+										: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+									on:click={() => {
+										selectedTab = 'connections';
+									}}
+								>
+									<div class=" self-center mr-2">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
+											/>
+										</svg>
+									</div>
+									<div class=" self-center">{$i18n.t('Connections')}</div>
+								</button>
+							{/if}
 						{:else if tabId === 'personalization'}
 							<button
 								class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
@@ -620,6 +655,13 @@
 							toast.success($i18n.t('Settings saved successfully!'));
 						}}
 					/>
+				{:else if selectedTab === 'connections'}
+					<Connections
+						saveSettings={async (updated) => {
+							await saveSettings(updated);
+							toast.success($i18n.t('Settings saved successfully!'));
+						}}
+					/>
 				{:else if selectedTab === 'personalization'}
 					<Personalization
 						{saveSettings}

+ 2 - 2
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')}
 								</button>
 							{/if}
 

+ 9 - 3
src/lib/components/common/Collapsible.svelte

@@ -75,9 +75,15 @@
 				<div class="">
 					{#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}

+ 1 - 0
src/lib/components/common/SensitiveInput.svelte

@@ -23,6 +23,7 @@
 	/>
 	<button
 		class={showButtonClassName}
+		type="button"
 		on:click={(e) => {
 			e.preventDefault();
 			show = !show;

+ 28 - 6
src/lib/components/workspace/Models.svelte

@@ -68,7 +68,12 @@
 			toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
 		}
 
-		await _models.set(await getModels(localStorage.token));
+		await _models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 		models = await getWorkspaceModels(localStorage.token);
 	};
 
@@ -82,7 +87,7 @@
 	};
 
 	const shareModelHandler = async (model) => {
-		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+		toast.success($i18n.t('Redirecting you to Open WebUI Community'));
 
 		const url = 'https://openwebui.com';
 
@@ -134,7 +139,12 @@
 			);
 		}
 
-		await _models.set(await getModels(localStorage.token));
+		await _models.set(
+			await getModels(
+				localStorage.token,
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			)
+		);
 		models = await getWorkspaceModels(localStorage.token);
 	};
 
@@ -371,7 +381,13 @@
 										bind:state={model.is_active}
 										on:change={async (e) => {
 											toggleModelById(localStorage.token, model.id);
-											_models.set(await getModels(localStorage.token));
+											_models.set(
+												await getModels(
+													localStorage.token,
+													$config?.features?.enable_direct_connections &&
+														($settings?.directConnections ?? null)
+												)
+											);
 										}}
 									/>
 								</Tooltip>
@@ -417,7 +433,13 @@
 								}
 							}
 
-							await _models.set(await getModels(localStorage.token));
+							await _models.set(
+								await getModels(
+									localStorage.token,
+									$config?.features?.enable_direct_connections &&
+										($settings?.directConnections ?? null)
+								)
+							);
 							models = await getWorkspaceModels(localStorage.token);
 						};
 
@@ -479,7 +501,7 @@
 	{#if $config?.features.enable_community_sharing}
 		<div class=" my-16">
 			<div class=" text-xl font-medium mb-1 line-clamp-1">
-				{$i18n.t('Made by OpenWebUI Community')}
+				{$i18n.t('Made by Open WebUI Community')}
 			</div>
 
 			<a

+ 2 - 2
src/lib/components/workspace/Prompts.svelte

@@ -40,7 +40,7 @@
 	$: filteredItems = prompts.filter((p) => query === '' || p.command.includes(query));
 
 	const shareHandler = async (prompt) => {
-		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+		toast.success($i18n.t('Redirecting you to Open WebUI Community'));
 
 		const url = 'https://openwebui.com';
 
@@ -319,7 +319,7 @@
 	{#if $config?.features.enable_community_sharing}
 		<div class=" my-16">
 			<div class=" text-xl font-medium mb-1 line-clamp-1">
-				{$i18n.t('Made by OpenWebUI Community')}
+				{$i18n.t('Made by Open WebUI Community')}
 			</div>
 
 			<a

+ 2 - 2
src/lib/components/workspace/Tools.svelte

@@ -65,7 +65,7 @@
 			return null;
 		});
 
-		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
+		toast.success($i18n.t('Redirecting you to Open WebUI Community'));
 
 		const url = 'https://openwebui.com';
 
@@ -438,7 +438,7 @@
 	{#if $config?.features.enable_community_sharing}
 		<div class=" my-16">
 			<div class=" text-xl font-medium mb-1 line-clamp-1">
-				{$i18n.t('Made by OpenWebUI Community')}
+				{$i18n.t('Made by Open WebUI Community')}
 			</div>
 
 			<a

+ 10 - 3
src/lib/i18n/locales/ar-BH/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "أضغط هنا الانتقال",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "أضغط هنا للاختيار",
 	"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "مستندات",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
+	"Domain Filter List": "",
 	"Don't have an account?": "ليس لديك حساب؟",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "أدخل الChunk Overlap",
 	"Enter Chunk Size": "أدخل Chunk الحجم",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
 	"Enter Google PSE API Key": "أدخل مفتاح واجهة برمجة تطبيقات PSE من Google",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "اللغة",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "فاتح",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "من جهة اليسار إلى اليمين",
-	"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
+	"Made by Open WebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
 	"Make sure to enclose them with": "تأكد من إرفاقها",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "أقراء لي",
 	"Reasoning Effort": "",
 	"Record voice": "سجل صوت",
-	"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
+	"Redirecting you to Open WebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
 	"Share": "كشاركة",
 	"Share Chat": "مشاركة الدردشة",
-	"Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع",
+	"Share to Open WebUI Community": "OpenWebUI شارك في مجتمع",
 	"Show": "عرض",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "شرح شامل",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/bg-BG/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Натиснете тук за",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Натиснете тук, за да изберете",
 	"Click here to select a csv file.": "Натиснете тук, за да изберете csv файл.",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "Документи",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Нямате акаунт?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Въведете Chunk Overlap",
 	"Enter Chunk Size": "Въведете Chunk Size",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Въведете URL адреса на Github Raw",
 	"Enter Google PSE API Key": "Въведете Google PSE API ключ",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Език",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Светъл",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
+	"Made by Open WebUI Community": "Направено от OpenWebUI общността",
 	"Make sure to enclose them with": "Уверете се, че са заключени с",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Прочети на Голос",
 	"Reasoning Effort": "",
 	"Record voice": "Записване на глас",
-	"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
+	"Redirecting you to Open WebUI Community": "Пренасочване към OpenWebUI общността",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Настройките са запазени успешно!",
 	"Share": "Подели",
 	"Share Chat": "Подели Чат",
-	"Share to OpenWebUI Community": "Споделите с OpenWebUI Общността",
+	"Share to Open WebUI Community": "Споделите с OpenWebUI Общността",
 	"Show": "Покажи",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "Това е подробно описание.",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/bn-BD/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "এখানে ক্লিক করুন",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "নির্বাচন করার জন্য এখানে ক্লিক করুন",
 	"Click here to select a csv file.": "একটি csv ফাইল নির্বাচন করার জন্য এখানে ক্লিক করুন",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "ডকুমেন্টসমূহ",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "কোন এক্সটার্নাল কানেকশন তৈরি করে না, এবং আপনার ডেটা আর লোকালি হোস্টেড সার্ভারেই নিরাপদে থাকে।",
+	"Domain Filter List": "",
 	"Don't have an account?": "একাউন্ট নেই?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
 	"Enter Chunk Size": "চাংক সাইজ লিখুন",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
 	"Enter Google PSE API Key": "গুগল পিএসই এপিআই কী লিখুন",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "ভাষা",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "লাইট",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
+	"Made by Open WebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
 	"Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "পড়াশোনা করুন",
 	"Reasoning Effort": "",
 	"Record voice": "ভয়েস রেকর্ড করুন",
-	"Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
+	"Redirecting you to Open WebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "সেটিংগুলো সফলভাবে সংরক্ষিত হয়েছে",
 	"Share": "শেয়ার করুন",
 	"Share Chat": "চ্যাট শেয়ার করুন",
-	"Share to OpenWebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
+	"Share to Open WebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
 	"Show": "দেখান",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 29 - 22
src/lib/i18n/locales/ca-ES/translation.json

@@ -63,11 +63,11 @@
 	"Allowed Endpoints": "Punts d'accés permesos",
 	"Already have an account?": "Ja tens un compte?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Alternativa al top_p, i pretén garantir un equilibri de qualitat i varietat. El paràmetre p representa la probabilitat mínima que es consideri un token, en relació amb la probabilitat del token més probable. Per exemple, amb p=0,05 i el token més probable amb una probabilitat de 0,9, es filtren els logits amb un valor inferior a 0,045. (Per defecte: 0.0)",
-	"Always": "",
+	"Always": "Sempre",
 	"Amazing": "Al·lucinant",
 	"an assistant": "un assistent",
-	"Analyzed": "",
-	"Analyzing...": "",
+	"Analyzed": "Analitzat",
+	"Analyzing...": "Analitzant...",
 	"and": "i",
 	"and {{COUNT}} more": "i {{COUNT}} més",
 	"and create a new shared link.": "i crear un nou enllaç compartit.",
@@ -163,6 +163,7 @@
 	"Click here to": "Clic aquí per",
 	"Click here to download user import template file.": "Fes clic aquí per descarregar l'arxiu de plantilla d'importació d'usuaris",
 	"Click here to learn more about faster-whisper and see the available models.": "Clica aquí per obtenir més informació sobre faster-whisper i veure els models disponibles.",
+	"Click here to see available models.": "Clica aquí per veure els models disponibles.",
 	"Click here to select": "Clica aquí per seleccionar",
 	"Click here to select a csv file.": "Clica aquí per seleccionar un fitxer csv.",
 	"Click here to select a py file.": "Clica aquí per seleccionar un fitxer py.",
@@ -172,11 +173,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.",
 	"Clone": "Clonar",
 	"Clone Chat": "Clonar el xat",
-	"Clone of {{TITLE}}": "",
+	"Clone of {{TITLE}}": "Clon de {{TITLE}}",
 	"Close": "Tancar",
 	"Code execution": "Execució de codi",
 	"Code formatted successfully": "Codi formatat correctament",
-	"Code Interpreter": "",
+	"Code Interpreter": "Intèrpret de codi",
 	"Collection": "Col·lecció",
 	"Color": "Color",
 	"ComfyUI": "ComfyUI",
@@ -238,7 +239,7 @@
 	"Default": "Per defecte",
 	"Default (Open AI)": "Per defecte (Open AI)",
 	"Default (SentenceTransformers)": "Per defecte (SentenceTransformers)",
-	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "El mode predeterminat funciona amb una gamma més àmplia de models cridant a les eines una vegada abans de l'execució. El mode natiu aprofita les capacitats de crida d'eines integrades del model, però requereix que el model admeti aquesta funció de manera inherent.",
 	"Default Model": "Model per defecte",
 	"Default model updated": "Model per defecte actualitzat",
 	"Default Models": "Models per defecte",
@@ -290,6 +291,7 @@
 	"Documentation": "Documentació",
 	"Documents": "Documents",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.",
+	"Domain Filter List": "Llista de filtre de dominis",
 	"Don't have an account?": "No tens un compte?",
 	"don't install random functions from sources you don't trust.": "no instal·lis funcions aleatòries de fonts en què no confiïs.",
 	"don't install random tools from sources you don't trust.": "no instal·lis eines aleatòries de fonts en què no confiïs.",
@@ -349,7 +351,8 @@
 	"Enter Chunk Overlap": "Introdueix la mida de solapament de blocs",
 	"Enter Chunk Size": "Introdueix la mida del bloc",
 	"Enter description": "Introdueix la descripció",
-	"Enter Exa API Key": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "Introdueix els dominis separats per comes (p. ex. example.com,site.org)",
+	"Enter Exa API Key": "Introdueix la clau API de d'EXA",
 	"Enter Github Raw URL": "Introdueix l'URL en brut de Github",
 	"Enter Google PSE API Key": "Introdueix la clau API de Google PSE",
 	"Enter Google PSE Engine Id": "Introdueix l'identificador del motor PSE de Google",
@@ -398,14 +401,14 @@
 	"Error accessing Google Drive: {{error}}": "Error en accedir a Google Drive: {{error}}",
 	"Error uploading file: {{error}}": "Error en pujar l'arxiu: {{error}}",
 	"Evaluations": "Avaluacions",
-	"Exa API Key": "",
+	"Exa API Key": "Clau API d'EXA",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Exemple: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Exemple: TOTS",
 	"Example: mail": "Exemple: mail",
 	"Example: ou=users,dc=foo,dc=example": "Exemple: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Exemple: sAMAccountName o uid o userPrincipalName",
 	"Exclude": "Excloure",
-	"Execute code for analysis": "",
+	"Execute code for analysis": "Executa el codi per analitzar-lo",
 	"Experimental": "Experimental",
 	"Explore the cosmos": "Explorar el cosmos",
 	"Export": "Exportar",
@@ -458,7 +461,7 @@
 	"Format your variables using brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
 	"Frequency Penalty": "Penalització per freqüència",
 	"Function": "Funció",
-	"Function Calling": "",
+	"Function Calling": "Crida a funcions",
 	"Function created successfully": "La funció s'ha creat correctament",
 	"Function deleted successfully": "La funció s'ha eliminat correctament",
 	"Function Description": "Descripció de la funció",
@@ -473,7 +476,7 @@
 	"Functions imported successfully": "Les funcions s'han importat correctament",
 	"General": "General",
 	"General Settings": "Preferències generals",
-	"Generate an image": "",
+	"Generate an image": "Generar una imatge",
 	"Generate Image": "Generar imatge",
 	"Generating search query": "Generant consulta",
 	"Get started": "Començar",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Coneixement eliminat correctament.",
 	"Knowledge reset successfully.": "Coneixement restablert correctament.",
 	"Knowledge updated successfully": "Coneixement actualitzat correctament.",
+	"Kokoro.js (Browser)": "Kokoro.js (Navegador)",
+	"Kokoro.js Dtype": "Kokoro.js Dtype",
 	"Label": "Etiqueta",
 	"Landing Page Mode": "Mode de la pàgina d'entrada",
 	"Language": "Idioma",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Deixar-ho buit per incloure tots els models del punt de connexió \"{{URL}}/models\"",
 	"Leave empty to include all models or select specific models": "Deixa-ho en blanc per incloure tots els models o selecciona models específics",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
+	"Leave model field empty to use the default model.": "Deixa el camp de model buit per utilitzar el model per defecte.",
 	"Light": "Clar",
 	"Listening...": "Escoltant...",
 	"Llama.cpp": "Llama.cpp",
@@ -574,7 +580,7 @@
 	"Local Models": "Models locals",
 	"Lost": "Perdut",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
+	"Made by Open WebUI Community": "Creat per la Comunitat OpenWebUI",
 	"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Assegura't d'exportar un fitxer workflow.json com a format API des de ComfyUI.",
 	"Manage": "Gestionar",
@@ -630,7 +636,7 @@
 	"More": "Més",
 	"Name": "Nom",
 	"Name your knowledge base": "Anomena la teva base de coneixement",
-	"Native": "",
+	"Native": "Natiu",
 	"New Chat": "Nou xat",
 	"New Folder": "Nova carpeta",
 	"New Password": "Nova contrasenya",
@@ -725,7 +731,7 @@
 	"Please enter a prompt": "Si us plau, entra una indicació",
 	"Please fill in all fields.": "Emplena tots els camps, si us plau.",
 	"Please select a model first.": "Si us plau, selecciona un model primer",
-	"Please select a model.": "",
+	"Please select a model.": "Si us plau, selecciona un model.",
 	"Please select a reason": "Si us plau, selecciona una raó",
 	"Port": "Port",
 	"Positive attitude": "Actitud positiva",
@@ -734,7 +740,7 @@
 	"Previous 30 days": "30 dies anteriors",
 	"Previous 7 days": "7 dies anteriors",
 	"Profile Image": "Imatge de perfil",
-	"Prompt": "",
+	"Prompt": "Indicació",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Indicació (p.ex. Digues-me quelcom divertit sobre l'Imperi Romà)",
 	"Prompt Content": "Contingut de la indicació",
 	"Prompt created successfully": "Indicació creada correctament",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Llegir en veu alta",
 	"Reasoning Effort": "Esforç de raonament",
 	"Record voice": "Enregistrar la veu",
-	"Redirecting you to OpenWebUI Community": "Redirigint-te a la comunitat OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Redirigint-te a la comunitat OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Redueix la probabilitat de generar ximpleries. Un valor més alt (p. ex. 100) donarà respostes més diverses, mentre que un valor més baix (p. ex. 10) serà més conservador. (Per defecte: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")",
 	"References from": "Referències de",
@@ -810,7 +816,7 @@
 	"Search options": "Opcions de cerca",
 	"Search Prompts": "Cercar indicacions",
 	"Search Result Count": "Recompte de resultats de cerca",
-	"Search the internet": "",
+	"Search the internet": "Cerca a internet",
 	"Search Tools": "Cercar eines",
 	"SearchApi API Key": "Clau API de SearchApi",
 	"SearchApi Engine": "Motor de SearchApi",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Les preferències s'han desat correctament",
 	"Share": "Compartir",
 	"Share Chat": "Compartir el xat",
-	"Share to OpenWebUI Community": "Compartir amb la comunitat OpenWebUI",
+	"Share to Open WebUI Community": "Compartir amb la comunitat OpenWebUI",
 	"Show": "Mostrar",
 	"Show \"What's New\" modal on login": "Veure 'Què hi ha de nou' a l'entrada",
 	"Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent",
@@ -943,7 +949,8 @@
 	"This will delete all models including custom models and cannot be undone.": "Això eliminarà tots els models incloent els personalitzats i no es pot desfer",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Això restablirà la base de coneixement i sincronitzarà tots els fitxers. Vols continuar?",
 	"Thorough explanation": "Explicació en detall",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "He pensat durant {{DURATION}}",
+	"Thought for {{DURATION}} seconds": "He pensat durant {{DURATION}} segons",
 	"Tika": "Tika",
 	"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
 	"Tiktoken": "Tiktoken",
@@ -980,7 +987,7 @@
 	"Tools": "Eines",
 	"Tools Access": "Accés a les eines",
 	"Tools are a function calling system with arbitrary code execution": "Les eines són un sistema de crida a funcions amb execució de codi arbitrari",
-	"Tools Function Calling Prompt": "",
+	"Tools Function Calling Prompt": "Indicació per a la crida de funcions",
 	"Tools have a function calling system that allows arbitrary code execution": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari",
 	"Tools have a function calling system that allows arbitrary code execution.": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari.",
 	"Top K": "Top K",
@@ -1051,7 +1058,7 @@
 	"Web Loader Settings": "Preferències del carregador web",
 	"Web Search": "Cerca la web",
 	"Web Search Engine": "Motor de cerca de la web",
-	"Web Search in Chat": "",
+	"Web Search in Chat": "Cerca a internet al xat",
 	"Web Search Query Generation": "Generació de consultes per a la cerca de la web",
 	"Webhook URL": "URL del webhook",
 	"WebUI Settings": "Preferències de WebUI",
@@ -1081,7 +1088,7 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Pots personalitzar les teves interaccions amb els models de llenguatge afegint memòries mitjançant el botó 'Gestiona' que hi ha a continuació, fent-les més útils i adaptades a tu.",
 	"You cannot upload an empty file.": "No es pot pujar un ariux buit.",
 	"You do not have permission to access this feature.": "No tens permís per accedir a aquesta funcionalitat",
-	"You do not have permission to upload files": "",
+	"You do not have permission to upload files": "No tens permisos per pujar arxius",
 	"You do not have permission to upload files.": "No tens permisos per pujar arxius.",
 	"You have no archived conversations.": "No tens converses arxivades.",
 	"You have shared this chat": "Has compartit aquest xat",

+ 10 - 3
src/lib/i18n/locales/ceb-PH/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "I-klik dinhi aron makapili",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "Mga dokumento",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "wala maghimo ug eksternal nga koneksyon, ug ang imong data nagpabiling luwas sa imong lokal nga host server.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Wala kay account ?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Pagsulod sa block overlap",
 	"Enter Chunk Size": "Isulod ang block size",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Pinulongan",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Kahayag",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "",
-	"Made by OpenWebUI Community": "Gihimo sa komunidad sa OpenWebUI",
+	"Made by Open WebUI Community": "Gihimo sa komunidad sa OpenWebUI",
 	"Make sure to enclose them with": "Siguruha nga palibutan sila",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "",
 	"Reasoning Effort": "",
 	"Record voice": "Irekord ang tingog",
-	"Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Malampuson nga na-save ang mga setting!",
 	"Share": "",
 	"Share Chat": "",
-	"Share to OpenWebUI Community": "Ipakigbahin sa komunidad sa OpenWebUI",
+	"Share to Open WebUI Community": "Ipakigbahin sa komunidad sa OpenWebUI",
 	"Show": "Pagpakita",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/cs-CZ/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Klikněte sem pro",
 	"Click here to download user import template file.": "Klikněte zde pro stažení šablony souboru pro import uživatelů.",
 	"Click here to learn more about faster-whisper and see the available models.": "Klikněte sem a zjistěte více o faster-whisper a podívejte se na dostupné modely.",
+	"Click here to see available models.": "",
 	"Click here to select": "Klikněte sem pro výběr",
 	"Click here to select a csv file.": "Klikněte zde pro výběr souboru typu csv.",
 	"Click here to select a py file.": "Klikněte sem pro výběr {{py}} souboru.",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentace",
 	"Documents": "Dokumenty",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "nevytváří žádná externí připojení a vaše data zůstávají bezpečně na vašem lokálním serveru.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Nemáte účet?",
 	"don't install random functions from sources you don't trust.": "Neinstalujte náhodné funkce ze zdrojů, kterým nedůvěřujete.",
 	"don't install random tools from sources you don't trust.": "Neinstalujte náhodné nástroje ze zdrojů, kterým nedůvěřujete.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Zadejte překryv části",
 	"Enter Chunk Size": "Zadejte velikost bloku",
 	"Enter description": "Zadejte popis",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Zadejte URL adresu Github Raw",
 	"Enter Google PSE API Key": "Zadejte klíč rozhraní API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Znalosti byly úspěšně odstraněny.",
 	"Knowledge reset successfully.": "Úspěšné obnovení znalostí.",
 	"Knowledge updated successfully": "Znalosti úspěšně aktualizovány",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "Režim vstupní stránky",
 	"Language": "Jazyk",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "Nechte prázdné pro zahrnutí všech modelů nebo vyberte konkrétní modely.",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Nechte prázdné pro použití výchozího podnětu, nebo zadejte vlastní podnět.",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Světlo",
 	"Listening...": "Poslouchání...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Lokální modely",
 	"Lost": "Ztracený",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Vytvořeno komunitou OpenWebUI",
+	"Made by Open WebUI Community": "Vytvořeno komunitou OpenWebUI",
 	"Make sure to enclose them with": "Ujistěte se, že jsou uzavřeny pomocí",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Ujistěte se, že exportujete soubor workflow.json ve formátu API z ComfyUI.",
 	"Manage": "Spravovat",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Číst nahlas",
 	"Reasoning Effort": "",
 	"Record voice": "Nahrát hlas",
-	"Redirecting you to OpenWebUI Community": "Přesměrování na komunitu OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Přesměrování na komunitu OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Odkazujte na sebe jako na \"uživatele\" (např. \"Uživatel se učí španělsky\").",
 	"References from": "Reference z",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Nastavení byla úspěšně uložena!",
 	"Share": "Sdílet",
 	"Share Chat": "Sdílet chat",
-	"Share to OpenWebUI Community": "Sdílet s komunitou OpenWebUI",
+	"Share to Open WebUI Community": "Sdílet s komunitou OpenWebUI",
 	"Show": "Zobrazit",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "Zobrazit podrobnosti administrátora v překryvném okně s čekajícím účtem",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Toto obnoví znalostní databázi a synchronizuje všechny soubory. Přejete si pokračovat?",
 	"Thorough explanation": "Obsáhlé vysvětlení",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Je vyžadována URL adresa serveru Tika.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/da-DK/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Klik her for at",
 	"Click here to download user import template file.": "Klik her for at downloade bruger import template fil.",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Klik her for at vælge",
 	"Click here to select a csv file.": "Klik her for at vælge en csv fil",
 	"Click here to select a py file.": "Klik her for at vælge en py fil",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentation",
 	"Documents": "Dokumenter",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "laver ikke eksterne kald, og din data bliver sikkert på din egen lokalt hostede server.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Har du ikke en profil?",
 	"don't install random functions from sources you don't trust.": "lad være med at installere tilfældige funktioner fra kilder, som du ikke stoler på.",
 	"don't install random tools from sources you don't trust.": "lad være med at installere tilfældige værktøjer fra kilder, som du ikke stoler på.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Indtast overlapning af tekststykker",
 	"Enter Chunk Size": "Indtast størrelse af tekststykker",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Indtast Github Raw URL",
 	"Enter Google PSE API Key": "Indtast Google PSE API-nøgle",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Viden slettet.",
 	"Knowledge reset successfully.": "Viden nulstillet.",
 	"Knowledge updated successfully": "Viden opdateret.",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "Landing Page-tilstand",
 	"Language": "Sprog",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Lad stå tomt for at bruge standardprompten, eller indtast en brugerdefineret prompt",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Lys",
 	"Listening...": "Lytter...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Lokale modeller",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Lavet af OpenWebUI Community",
+	"Made by Open WebUI Community": "Lavet af OpenWebUI Community",
 	"Make sure to enclose them with": "Sørg for at omslutte dem med",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Sørg for at eksportere en workflow.json-fil som API-format fra ComfyUI.",
 	"Manage": "Administrer",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Læs højt",
 	"Reasoning Effort": "",
 	"Record voice": "Optag stemme",
-	"Redirecting you to OpenWebUI Community": "Omdirigerer dig til OpenWebUI Community",
+	"Redirecting you to Open WebUI Community": "Omdirigerer dig til OpenWebUI Community",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referer til dig selv som \"Bruger\" (f.eks. \"Bruger lærer spansk\")",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Indstillinger gemt!",
 	"Share": "Del",
 	"Share Chat": "Del chat",
-	"Share to OpenWebUI Community": "Del til OpenWebUI Community",
+	"Share to Open WebUI Community": "Del til OpenWebUI Community",
 	"Show": "Vis",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "Vis administratordetaljer i overlay for ventende konto",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Dette vil nulstille vidensbasen og synkronisere alle filer. Vil du fortsætte?",
 	"Thorough explanation": "Grundig forklaring",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika-server-URL påkrævet.",
 	"Tiktoken": "",

+ 111 - 104
src/lib/i18n/locales/de-DE/translation.json

@@ -1,11 +1,11 @@
 {
-	"-1 for no limit, or a positive integer for a specific limit": "",
+	"-1 for no limit, or a positive integer for a specific limit": "-1 für kein Limit oder eine positive Zahl für ein spezifisches Limit",
 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' oder '-1' für keine Ablaufzeit.",
 	"(e.g. `sh webui.sh --api --api-auth username_password`)": "(z. B. `sh webui.sh --api --api-auth username_password`)",
 	"(e.g. `sh webui.sh --api`)": "(z. B. `sh webui.sh --api`)",
 	"(latest)": "(neueste)",
 	"{{ models }}": "{{ Modelle }}",
-	"{{COUNT}} Replies": "",
+	"{{COUNT}} Replies": "{{COUNT}} Antworten",
 	"{{user}}'s Chats": "{{user}}s Unterhaltungen",
 	"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
 	"*Prompt node ID(s) are required for image generation": "*Prompt-Node-ID(s) sind für die Bildgenerierung erforderlich",
@@ -35,7 +35,7 @@
 	"Add Group": "Gruppe hinzufügen",
 	"Add Memory": "Erinnerung hinzufügen",
 	"Add Model": "Modell hinzufügen",
-	"Add Reaction": "",
+	"Add Reaction": "Reaktion hinzufügen",
 	"Add Tag": "Tag hinzufügen",
 	"Add Tags": "Tags hinzufügen",
 	"Add text content": "Textinhalt hinzufügen",
@@ -51,7 +51,7 @@
 	"Advanced Params": "Erweiterte Parameter",
 	"All Documents": "Alle Dokumente",
 	"All models deleted successfully": "Alle Modelle erfolgreich gelöscht",
-	"Allow Chat Controls": "",
+	"Allow Chat Controls": "Chat-Steuerung erlauben",
 	"Allow Chat Delete": "Löschen von Unterhaltungen erlauben",
 	"Allow Chat Deletion": "Löschen von Unterhaltungen erlauben",
 	"Allow Chat Edit": "Bearbeiten von Unterhaltungen erlauben",
@@ -60,21 +60,21 @@
 	"Allow Temporary Chat": "Temporäre Unterhaltungen erlauben",
 	"Allow User Location": "Standort freigeben",
 	"Allow Voice Interruption in Call": "Unterbrechung durch Stimme im Anruf zulassen",
-	"Allowed Endpoints": "",
+	"Allowed Endpoints": "Erlaubte Endpunkte",
 	"Already have an account?": "Haben Sie bereits einen Account?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Alternative zu top_p und zielt darauf ab, ein Gleichgewicht zwischen Qualität und Vielfalt zu gewährleisten. Der Parameter p repräsentiert die Mindestwahrscheinlichkeit für ein Token, um berücksichtigt zu werden, relativ zur Wahrscheinlichkeit des wahrscheinlichsten Tokens. Zum Beispiel, bei p=0.05 und das wahrscheinlichste Token hat eine Wahrscheinlichkeit von 0.9, werden Logits mit einem Wert von weniger als 0.045 herausgefiltert. (Standard: 0.0)",
-	"Always": "",
+	"Always": "Immer",
 	"Amazing": "Fantastisch",
 	"an assistant": "ein Assistent",
-	"Analyzed": "",
-	"Analyzing...": "",
+	"Analyzed": "Analysiert",
+	"Analyzing...": "Analysiere...",
 	"and": "und",
 	"and {{COUNT}} more": "und {{COUNT}} mehr",
 	"and create a new shared link.": "und erstellen Sie einen neuen freigegebenen Link.",
 	"API Base URL": "API-Basis-URL",
 	"API Key": "API-Schlüssel",
 	"API Key created.": "API-Schlüssel erstellt.",
-	"API Key Endpoint Restrictions": "",
+	"API Key Endpoint Restrictions": "API-Schlüssel Endpunkteinschränkungen",
 	"API keys": "API-Schlüssel",
 	"Application DN": "Anwendungs-DN",
 	"Application DN Password": "Anwendungs-DN-Passwort",
@@ -84,8 +84,8 @@
 	"Archive All Chats": "Alle Unterhaltungen archivieren",
 	"Archived Chats": "Archivierte Unterhaltungen",
 	"archived-chat-export": "archivierter-chat-export",
-	"Are you sure you want to delete this channel?": "",
-	"Are you sure you want to delete this message?": "",
+	"Are you sure you want to delete this channel?": "Sind Sie sicher, dass Sie diesen Kanal löschen möchten?",
+	"Are you sure you want to delete this message?": "Sind Sie sicher, dass Sie diese Nachricht löschen möchten?",
 	"Are you sure you want to unarchive all archived chats?": "Sind Sie sicher, dass Sie alle archivierten Unterhaltungen wiederherstellen möchten?",
 	"Are you sure?": "Sind Sie sicher?",
 	"Arena Models": "Arena-Modelle",
@@ -94,15 +94,15 @@
 	"Assistant": "Assistent",
 	"Attach file": "Datei anhängen",
 	"Attention to detail": "Aufmerksamkeit für Details",
-	"Attribute for Mail": "",
+	"Attribute for Mail": "Attribut für E-Mail",
 	"Attribute for Username": "Attribut für Benutzername",
 	"Audio": "Audio",
 	"August": "August",
 	"Authenticate": "Authentifizieren",
 	"Auto-Copy Response to Clipboard": "Antwort automatisch in die Zwischenablage kopieren",
 	"Auto-playback response": "Antwort automatisch abspielen",
-	"Autocomplete Generation": "",
-	"Autocomplete Generation Input Max Length": "",
+	"Autocomplete Generation": "Automatische Vervollständigung",
+	"Autocomplete Generation Input Max Length": "Maximale Eingabenlände für automatische Vervollständigung",
 	"Automatic1111": "Automatic1111",
 	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111-API-Authentifizierungszeichenfolge",
 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111-Basis-URL",
@@ -119,7 +119,7 @@
 	"Batch Size (num_batch)": "Stapelgröße (num_batch)",
 	"before": "bereits geteilt",
 	"Being lazy": "Faulheit",
-	"Beta": "",
+	"Beta": "Beta",
 	"Bing Search V7 Endpoint": "Bing Search V7-Endpunkt",
 	"Bing Search V7 Subscription Key": "Bing Search V7-Abonnement-Schlüssel",
 	"Brave Search API Key": "Brave Search API-Schlüssel",
@@ -130,13 +130,13 @@
 	"Camera": "Kamera",
 	"Cancel": "Abbrechen",
 	"Capabilities": "Fähigkeiten",
-	"Capture": "",
+	"Capture": "Aufnehmen",
 	"Certificate Path": "Zertifikatpfad",
 	"Change Password": "Passwort ändern",
-	"Channel Name": "",
-	"Channels": "",
+	"Channel Name": "Kanalname",
+	"Channels": "Kanäle",
 	"Character": "Zeichen",
-	"Character limit for autocomplete generation input": "",
+	"Character limit for autocomplete generation input": "Zeichenlimit für die Eingabe der automatischen Vervollständigung",
 	"Chart new frontiers": "Neue Wege beschreiten",
 	"Chat": "Gespräch",
 	"Chat Background Image": "Hintergrundbild des Unterhaltungsfensters",
@@ -163,6 +163,7 @@
 	"Click here to": "Klicken Sie hier, um",
 	"Click here to download user import template file.": "Klicken Sie hier, um die Vorlage für den Benutzerimport herunterzuladen.",
 	"Click here to learn more about faster-whisper and see the available models.": "Klicken Sie hier, um mehr über faster-whisper zu erfahren und die verfügbaren Modelle zu sehen.",
+	"Click here to see available models.": "",
 	"Click here to select": "Klicke Sie zum Auswählen hier",
 	"Click here to select a csv file.": "Klicken Sie zum Auswählen einer CSV-Datei hier.",
 	"Click here to select a py file.": "Klicken Sie zum Auswählen einer py-Datei hier.",
@@ -171,16 +172,16 @@
 	"Click on the user role button to change a user's role.": "Klicken Sie auf die Benutzerrolle, um sie zu ändern.",
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Schreibberechtigung für die Zwischenablage verweigert. Bitte überprüfen Sie Ihre Browsereinstellungen, um den erforderlichen Zugriff zu erlauben.",
 	"Clone": "Klonen",
-	"Clone Chat": "",
-	"Clone of {{TITLE}}": "",
+	"Clone Chat": "Konversation klonen",
+	"Clone of {{TITLE}}": "Klon von {{TITLE}}",
 	"Close": "Schließen",
 	"Code execution": "Codeausführung",
 	"Code formatted successfully": "Code erfolgreich formatiert",
-	"Code Interpreter": "",
+	"Code Interpreter": "Code-Interpreter",
 	"Collection": "Kollektion",
 	"Color": "Farbe",
 	"ComfyUI": "ComfyUI",
-	"ComfyUI API Key": "",
+	"ComfyUI API Key": "ComfyUI-API-Schlüssel",
 	"ComfyUI Base URL": "ComfyUI-Basis-URL",
 	"ComfyUI Base URL is required.": "ComfyUI-Basis-URL wird benötigt.",
 	"ComfyUI Workflow": "ComfyUI-Workflow",
@@ -192,9 +193,9 @@
 	"Confirm": "Bestätigen",
 	"Confirm Password": "Passwort bestätigen",
 	"Confirm your action": "Bestätigen Sie Ihre Aktion.",
-	"Confirm your new password": "",
+	"Confirm your new password": "Neues Passwort bestätigen",
 	"Connections": "Verbindungen",
-	"Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "",
+	"Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "Beschränkt den Aufwand für das Schlussfolgern bei Schlussfolgerungsmodellen. Nur anwendbar auf Schlussfolgerungsmodelle von spezifischen Anbietern, die den Schlussfolgerungsaufwand unterstützen. (Standard: medium)",
 	"Contact Admin for WebUI Access": "Kontaktieren Sie den Administrator für den Zugriff auf die Weboberfläche",
 	"Content": "Info",
 	"Content Extraction": "Inhaltsextraktion",
@@ -220,7 +221,7 @@
 	"Create a model": "Modell erstellen",
 	"Create Account": "Konto erstellen",
 	"Create Admin Account": "Administrator-Account erstellen",
-	"Create Channel": "",
+	"Create Channel": "Kanal erstellen",
 	"Create Group": "Gruppe erstellen",
 	"Create Knowledge": "Wissen erstellen",
 	"Create new key": "Neuen Schlüssel erstellen",
@@ -238,10 +239,10 @@
 	"Default": "Standard",
 	"Default (Open AI)": "Standard (Open AI)",
 	"Default (SentenceTransformers)": "Standard (SentenceTransformers)",
-	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "Der Standardmodus funktioniert mit einer breiteren Auswahl von Modellen, indem er Werkzeuge einmal vor der Ausführung aufruft. Der native Modus nutzt die integrierten Tool-Aufrufmöglichkeiten des Modells, erfordert jedoch, dass das Modell diese Funktion von Natur aus unterstützt.",
 	"Default Model": "Standardmodell",
 	"Default model updated": "Standardmodell aktualisiert",
-	"Default Models": "",
+	"Default Models": "Standardmodelle",
 	"Default permissions": "Standardberechtigungen",
 	"Default permissions updated successfully": "Standardberechtigungen erfolgreich aktualisiert",
 	"Default Prompt Suggestions": "Prompt-Vorschläge",
@@ -257,7 +258,7 @@
 	"Delete chat?": "Unterhaltung löschen?",
 	"Delete folder?": "Ordner löschen?",
 	"Delete function?": "Funktion löschen?",
-	"Delete Message": "",
+	"Delete Message": "Nachricht löschen",
 	"Delete prompt?": "Prompt löschen?",
 	"delete this link": "diesen Link löschen",
 	"Delete tool?": "Werkzeug löschen?",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentation",
 	"Documents": "Dokumente",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "stellt keine externen Verbindungen her, und Ihre Daten bleiben sicher auf Ihrem lokal gehosteten Server.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Haben Sie noch kein Benutzerkonto?",
 	"don't install random functions from sources you don't trust.": "installieren Sie keine Funktionen aus Quellen, denen Sie nicht vertrauen.",
 	"don't install random tools from sources you don't trust.": "installieren Sie keine Werkzeuge aus Quellen, denen Sie nicht vertrauen.",
@@ -310,7 +312,7 @@
 	"e.g. Tools for performing various operations": "z. B. Werkzeuge für verschiedene Operationen",
 	"Edit": "Bearbeiten",
 	"Edit Arena Model": "Arena-Modell bearbeiten",
-	"Edit Channel": "",
+	"Edit Channel": "Kanal bearbeiten",
 	"Edit Connection": "Verbindung bearbeiten",
 	"Edit Default Permissions": "Standardberechtigungen bearbeiten",
 	"Edit Memory": "Erinnerungen bearbeiten",
@@ -323,10 +325,10 @@
 	"Embedding Model": "Embedding-Modell",
 	"Embedding Model Engine": "Embedding-Modell-Engine",
 	"Embedding model set to \"{{embedding_model}}\"": "Embedding-Modell auf \"{{embedding_model}}\" gesetzt",
-	"Enable API Key": "",
-	"Enable autocomplete generation for chat messages": "",
+	"Enable API Key": "API-Schlüssel aktivieren",
+	"Enable autocomplete generation for chat messages": "Automatische Vervollständigung für Chat-Nachrichten aktivieren",
 	"Enable Community Sharing": "Community-Freigabe aktivieren",
-	"Enable Google Drive": "",
+	"Enable Google Drive": "Google Drive aktivieren",
 	"Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "Aktiviere Memory Locking (mlock), um zu verhindern, dass Modelldaten aus dem RAM ausgelagert werden. Diese Option sperrt die Arbeitsseiten des Modells im RAM, um sicherzustellen, dass sie nicht auf die Festplatte ausgelagert werden. Dies kann die Leistung verbessern, indem Page Faults vermieden und ein schneller Datenzugriff sichergestellt werden.",
 	"Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "Aktiviere Memory Mapping (mmap), um Modelldaten zu laden. Diese Option ermöglicht es dem System, den Festplattenspeicher als Erweiterung des RAM zu verwenden, indem Festplattendateien so behandelt werden, als ob sie im RAM wären. Dies kann die Modellleistung verbessern, indem ein schnellerer Datenzugriff ermöglicht wird. Es kann jedoch nicht auf allen Systemen korrekt funktionieren und einen erheblichen Teil des Festplattenspeichers beanspruchen.",
 	"Enable Message Rating": "Nachrichtenbewertung aktivieren",
@@ -349,20 +351,21 @@
 	"Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein",
 	"Enter Chunk Size": "Geben Sie die Blockgröße ein",
 	"Enter description": "Geben Sie eine Beschreibung ein",
-	"Enter Exa API Key": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
+	"Enter Exa API Key": "Geben Sie den Exa-API-Schlüssel ein",
 	"Enter Github Raw URL": "Geben Sie die Github Raw-URL ein",
 	"Enter Google PSE API Key": "Geben Sie den Google PSE-API-Schlüssel ein",
 	"Enter Google PSE Engine Id": "Geben Sie die Google PSE-Engine-ID ein",
 	"Enter Image Size (e.g. 512x512)": "Geben Sie die Bildgröße ein (z. B. 512x512)",
 	"Enter Jina API Key": "Geben Sie den Jina-API-Schlüssel ein",
-	"Enter Kagi Search API Key": "",
+	"Enter Kagi Search API Key": "Geben sie den Kagi Search API-Schlüssel ein",
 	"Enter language codes": "Geben Sie die Sprachcodes ein",
 	"Enter Model ID": "Geben Sie die Modell-ID ein",
 	"Enter model tag (e.g. {{modelTag}})": "Geben Sie den Model-Tag ein",
 	"Enter Mojeek Search API Key": "Geben Sie den Mojeek Search API-Schlüssel ein",
 	"Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
-	"Enter proxy URL (e.g. https://user:password@host:port)": "",
-	"Enter reasoning effort": "",
+	"Enter proxy URL (e.g. https://user:password@host:port)": "Geben sie die Proxy-URL ein (z. B. https://user:password@host:port)",
+	"Enter reasoning effort": "Geben Sie den Schlussfolgerungsaufwand ein",
 	"Enter Sampler (e.g. Euler a)": "Geben Sie den Sampler ein (z. B. Euler a)",
 	"Enter Scheduler (e.g. Karras)": "Geben Sie den Scheduler ein (z. B. Karras)",
 	"Enter Score": "Punktzahl eingeben",
@@ -379,33 +382,33 @@
 	"Enter stop sequence": "Stop-Sequenz eingeben",
 	"Enter system prompt": "Systemprompt eingeben",
 	"Enter Tavily API Key": "Geben Sie den Tavily-API-Schlüssel ein",
-	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "",
+	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Geben sie die öffentliche URL Ihrer WebUI ein. Diese URL wird verwendet, um Links in den Benachrichtigungen zu generieren.",
 	"Enter Tika Server URL": "Geben Sie die Tika-Server-URL ein",
 	"Enter Top K": "Geben Sie Top K ein",
 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Geben Sie die URL ein (z. B. http://127.0.0.1:7860/)",
 	"Enter URL (e.g. http://localhost:11434)": "Geben Sie die URL ein (z. B. http://localhost:11434)",
-	"Enter your current password": "",
+	"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
 	"Enter Your Email": "Geben Sie Ihre E-Mail-Adresse ein",
 	"Enter Your Full Name": "Geben Sie Ihren vollständigen Namen ein",
 	"Enter your message": "Geben Sie Ihre Nachricht ein",
-	"Enter your new password": "",
+	"Enter your new password": "Geben Sie Ihr neues Passwort ein",
 	"Enter Your Password": "Geben Sie Ihr Passwort ein",
 	"Enter Your Role": "Geben Sie Ihre Rolle ein",
 	"Enter Your Username": "Geben Sie Ihren Benutzernamen ein",
-	"Enter your webhook URL": "",
+	"Enter your webhook URL": "Geben Sie Ihre Webhook-URL ein",
 	"Error": "Fehler",
 	"ERROR": "FEHLER",
-	"Error accessing Google Drive: {{error}}": "",
-	"Error uploading file: {{error}}": "",
+	"Error accessing Google Drive: {{error}}": "Fehler beim Zugriff auf Google Drive: {{error}}",
+	"Error uploading file: {{error}}": "Fehler beim Hochladen der Datei: {{error}}",
 	"Evaluations": "Evaluationen",
-	"Exa API Key": "",
+	"Exa API Key": "Exa-API-Schlüssel",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Beispiel: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Beispiel: ALL",
-	"Example: mail": "",
+	"Example: mail": "Beispiel: mail",
 	"Example: ou=users,dc=foo,dc=example": "Beispiel: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Beispiel: sAMAccountName or uid or userPrincipalName",
 	"Exclude": "Ausschließen",
-	"Execute code for analysis": "",
+	"Execute code for analysis": "Code für Analyse ausführen",
 	"Experimental": "Experimentell",
 	"Explore the cosmos": "Erforschen Sie das Universum",
 	"Export": "Exportieren",
@@ -423,12 +426,12 @@
 	"External Models": "Externe Modelle",
 	"Failed to add file.": "Fehler beim Hinzufügen der Datei.",
 	"Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.",
-	"Failed to fetch models": "",
+	"Failed to fetch models": "Fehler beim Abrufen der Modelle",
 	"Failed to read clipboard contents": "Fehler beim Abruf der Zwischenablage",
-	"Failed to save models configuration": "",
+	"Failed to save models configuration": "Fehler beim Speichern der Modellkonfiguration",
 	"Failed to update settings": "Fehler beim Aktualisieren der Einstellungen",
 	"Failed to upload file.": "Fehler beim Hochladen der Datei.",
-	"Features Permissions": "",
+	"Features Permissions": "Funktionen-Berechtigungen",
 	"February": "Februar",
 	"Feedback History": "Feedback-Verlauf",
 	"Feedbacks": "Feedbacks",
@@ -440,7 +443,7 @@
 	"File not found.": "Datei nicht gefunden.",
 	"File removed successfully.": "Datei erfolgreich entfernt.",
 	"File size should not exceed {{maxSize}} MB.": "Datei darf nicht größer als {{maxSize}} MB sein.",
-	"File uploaded successfully": "",
+	"File uploaded successfully": "Datei erfolgreich hochgeladen",
 	"Files": "Dateien",
 	"Filter is now globally disabled": "Filter ist jetzt global deaktiviert",
 	"Filter is now globally enabled": "Filter ist jetzt global aktiviert",
@@ -458,7 +461,7 @@
 	"Format your variables using brackets like this:": "Formatieren Sie Ihre Variablen mit Klammern, wie hier:",
 	"Frequency Penalty": "Frequenzstrafe",
 	"Function": "Funktion",
-	"Function Calling": "",
+	"Function Calling": "Funktionsaufruf",
 	"Function created successfully": "Funktion erfolgreich erstellt",
 	"Function deleted successfully": "Funktion erfolgreich gelöscht",
 	"Function Description": "Funktionsbeschreibung",
@@ -473,14 +476,14 @@
 	"Functions imported successfully": "Funktionen erfolgreich importiert",
 	"General": "Allgemein",
 	"General Settings": "Allgemeine Einstellungen",
-	"Generate an image": "",
+	"Generate an image": "Bild erzeugen",
 	"Generate Image": "Bild erzeugen",
 	"Generating search query": "Suchanfrage wird erstellt",
 	"Get started": "Loslegen",
 	"Get started with {{WEBUI_NAME}}": "Loslegen mit {{WEBUI_NAME}}",
 	"Global": "Global",
 	"Good Response": "Gute Antwort",
-	"Google Drive": "",
+	"Google Drive": "Google Drive",
 	"Google PSE API Key": "Google PSE-API-Schlüssel",
 	"Google PSE Engine Id": "Google PSE-Engine-ID",
 	"Group created successfully": "Gruppe erfolgreich erstellt",
@@ -504,14 +507,14 @@
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Ich bestätige, dass ich gelesen habe und die Auswirkungen meiner Aktion verstehe. Mir sind die Risiken bewusst, die mit der Ausführung beliebigen Codes verbunden sind, und ich habe die Vertrauenswürdigkeit der Quelle überprüft.",
 	"ID": "ID",
 	"Ignite curiosity": "Neugier entfachen",
-	"Image": "",
-	"Image Compression": "",
-	"Image Generation": "",
+	"Image": "Bild",
+	"Image Compression": "Bildkomprimierung",
+	"Image Generation": "Bildgenerierung",
 	"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
 	"Image Generation Engine": "Bildgenerierungs-Engine",
-	"Image Max Compression Size": "",
-	"Image Prompt Generation": "",
-	"Image Prompt Generation Prompt": "",
+	"Image Max Compression Size": "Maximale Bildkomprimierungsgröße",
+	"Image Prompt Generation": "Bild-Prompt-Generierung",
+	"Image Prompt Generation Prompt": "Prompt für die Bild-Prompt-Generierung",
 	"Image Settings": "Bildeinstellungen",
 	"Images": "Bilder",
 	"Import Chats": "Unterhaltungen importieren",
@@ -532,7 +535,7 @@
 	"Interface": "Benutzeroberfläche",
 	"Invalid file format.": "Ungültiges Dateiformat.",
 	"Invalid Tag": "Ungültiger Tag",
-	"is typing...": "",
+	"is typing...": "schreibt ...",
 	"January": "Januar",
 	"Jina API Key": "Jina-API-Schlüssel",
 	"join our Discord for help.": "Treten Sie unserem Discord bei, um Hilfe zu erhalten.",
@@ -542,7 +545,7 @@
 	"June": "Juni",
 	"JWT Expiration": "JWT-Ablauf",
 	"JWT Token": "JWT-Token",
-	"Kagi Search API Key": "",
+	"Kagi Search API Key": "Kagi Search API-Schlüssel",
 	"Keep Alive": "Verbindung aufrechterhalten",
 	"Key": "Schlüssel",
 	"Keyboard shortcuts": "Tastenkombinationen",
@@ -552,12 +555,14 @@
 	"Knowledge deleted successfully.": "Wissen erfolgreich gelöscht.",
 	"Knowledge reset successfully.": "Wissen erfolgreich zurückgesetzt.",
 	"Knowledge updated successfully": "Wissen erfolgreich aktualisiert",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Label",
 	"Landing Page Mode": "Startseitenmodus",
 	"Language": "Sprache",
 	"Last Active": "Zuletzt aktiv",
 	"Last Modified": "Zuletzt bearbeitet",
-	"Last reply": "",
+	"Last reply": "Letzte Antwort",
 	"LDAP": "LDAP",
 	"LDAP server updated": "LDAP-Server aktualisiert",
 	"Leaderboard": "Bestenliste",
@@ -566,20 +571,21 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Leer lassen, um alle Modelle vom \"{{URL}}/models\"-Endpunkt einzuschließen",
 	"Leave empty to include all models or select specific models": "Leer lassen, um alle Modelle einzuschließen oder spezifische Modelle auszuwählen",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Leer lassen, um den Standardprompt zu verwenden, oder geben Sie einen benutzerdefinierten Prompt ein",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Hell",
 	"Listening...": "Höre zu...",
-	"Llama.cpp": "",
+	"Llama.cpp": "Llama.cpp",
 	"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
 	"Local": "Lokal",
 	"Local Models": "Lokale Modelle",
 	"Lost": "Verloren",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Von der OpenWebUI-Community",
+	"Made by Open WebUI Community": "Von der OpenWebUI-Community",
 	"Make sure to enclose them with": "Umschließe Variablen mit",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Stellen Sie sicher, dass sie eine workflow.json-Datei im API-Format von ComfyUI exportieren.",
 	"Manage": "Verwalten",
 	"Manage Arena Models": "Arena-Modelle verwalten",
-	"Manage Models": "",
+	"Manage Models": "Modelle verwalten",
 	"Manage Ollama": "Ollama verwalten",
 	"Manage Ollama API Connections": "Ollama-API-Verbindungen verwalten",
 	"Manage OpenAI API Connections": "OpenAI-API-Verbindungen verwalten",
@@ -624,17 +630,17 @@
 	"Modelfile Content": "Modelfile-Inhalt",
 	"Models": "Modelle",
 	"Models Access": "Modell-Zugriff",
-	"Models configuration saved successfully": "",
+	"Models configuration saved successfully": "Modellkonfiguration erfolgreich gespeichert",
 	"Mojeek Search API Key": "Mojeek Search API-Schlüssel",
 	"more": "mehr",
 	"More": "Mehr",
 	"Name": "Name",
 	"Name your knowledge base": "Benennen Sie Ihren Wissensspeicher",
-	"Native": "",
+	"Native": "Nativ",
 	"New Chat": "Neue Unterhaltung",
-	"New Folder": "",
+	"New Folder": "Neuer Ordner",
 	"New Password": "Neues Passwort",
-	"new-channel": "",
+	"new-channel": "neuer-kanal",
 	"No content found": "Kein Inhalt gefunden",
 	"No content to speak": "Kein Inhalt zum Vorlesen",
 	"No distance available": "Keine Distanz verfügbar",
@@ -643,11 +649,11 @@
 	"No files found.": "Keine Dateien gefunden.",
 	"No groups with access, add a group to grant access": "Keine Gruppen mit Zugriff, fügen Sie eine Gruppe hinzu, um Zugriff zu gewähren",
 	"No HTML, CSS, or JavaScript content found.": "Keine HTML-, CSS- oder JavaScript-Inhalte gefunden.",
-	"No inference engine with management support found": "",
+	"No inference engine with management support found": "Keine Inferenz-Engine mit Management-Unterstützung gefunden",
 	"No knowledge found": "Kein Wissen gefunden",
 	"No model IDs": "Keine Modell-IDs",
 	"No models found": "Keine Modelle gefunden",
-	"No models selected": "",
+	"No models selected": "Keine Modelle ausgewählt",
 	"No results found": "Keine Ergebnisse gefunden",
 	"No search query generated": "Keine Suchanfrage generiert",
 	"No source available": "Keine Quelle verfügbar",
@@ -658,8 +664,8 @@
 	"Not helpful": "Nich hilfreich",
 	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Hinweis: Wenn Sie eine Mindestpunktzahl festlegen, werden in der Suche nur Dokumente mit einer Punktzahl größer oder gleich der Mindestpunktzahl zurückgegeben.",
 	"Notes": "Notizen",
-	"Notification Sound": "",
-	"Notification Webhook": "",
+	"Notification Sound": "Benachrichtigungston",
+	"Notification Webhook": "Benachrichtigungs-Webhook",
 	"Notifications": "Benachrichtigungen",
 	"November": "November",
 	"num_gpu (Ollama)": "num_gpu (Ollama)",
@@ -724,8 +730,8 @@
 	"Please carefully review the following warnings:": "Bitte überprüfen Sie die folgenden Warnungen sorgfältig:",
 	"Please enter a prompt": "Bitte geben Sie einen Prompt ein",
 	"Please fill in all fields.": "Bitte füllen Sie alle Felder aus.",
-	"Please select a model first.": "",
-	"Please select a model.": "",
+	"Please select a model first.": "Bitte wählen Sie zuerst ein Modell aus.",
+	"Please select a model.": "Bitte wählen Sie ein Modell aus.",
 	"Please select a reason": "Bitte wählen Sie einen Grund aus",
 	"Port": "Port",
 	"Positive attitude": "Positive Einstellung",
@@ -734,7 +740,7 @@
 	"Previous 30 days": "Vorherige 30 Tage",
 	"Previous 7 days": "Vorherige 7 Tage",
 	"Profile Image": "Profilbild",
-	"Prompt": "",
+	"Prompt": "Prompt",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (z. B. \"Erzähle mir eine interessante Tatsache über das Römische Reich\")",
 	"Prompt Content": "Prompt-Inhalt",
 	"Prompt created successfully": "Prompt erfolgreich erstellt",
@@ -742,7 +748,7 @@
 	"Prompt updated successfully": "Prompt erfolgreich aktualisiert",
 	"Prompts": "Prompts",
 	"Prompts Access": "Prompt-Zugriff",
-	"Proxy URL": "",
+	"Proxy URL": "Proxy-URL",
 	"Pull \"{{searchValue}}\" from Ollama.com": "\"{{searchValue}}\" von Ollama.com beziehen",
 	"Pull a model from Ollama.com": "Modell von Ollama.com beziehen",
 	"Query Generation Prompt": "Abfragegenerierungsprompt",
@@ -750,11 +756,11 @@
 	"RAG Template": "RAG-Vorlage",
 	"Rating": "Bewertung",
 	"Re-rank models by topic similarity": "Modelle nach thematischer Ähnlichkeit neu ordnen",
-	"Read": "",
+	"Read": "Lesen",
 	"Read Aloud": "Vorlesen",
-	"Reasoning Effort": "",
+	"Reasoning Effort": "Schlussfolgerungsaufwand",
 	"Record voice": "Stimme aufnehmen",
-	"Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
+	"Redirecting you to Open WebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Reduziert die Wahrscheinlichkeit, Unsinn zu generieren. Ein höherer Wert (z.B. 100) liefert vielfältigere Antworten, während ein niedrigerer Wert (z.B. 10) konservativer ist. (Standard: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Beziehen Sie sich auf sich selbst als \"Benutzer\" (z. B. \"Benutzer lernt Spanisch\")",
 	"References from": "Referenzen aus",
@@ -765,22 +771,22 @@
 	"Remove": "Entfernen",
 	"Remove Model": "Modell entfernen",
 	"Rename": "Umbenennen",
-	"Reorder Models": "",
+	"Reorder Models": "Modelle neu anordnen",
 	"Repeat Last N": "Wiederhole die letzten N",
-	"Reply in Thread": "",
+	"Reply in Thread": "Im Thread antworten",
 	"Request Mode": "Anforderungsmodus",
 	"Reranking Model": "Reranking-Modell",
 	"Reranking model disabled": "Reranking-Modell deaktiviert",
 	"Reranking model set to \"{{reranking_model}}\"": "Reranking-Modell \"{{reranking_model}}\" fesgelegt",
 	"Reset": "Zurücksetzen",
-	"Reset All Models": "",
+	"Reset All Models": "Alle Modelle zurücksetzen",
 	"Reset Upload Directory": "Upload-Verzeichnis zurücksetzen",
 	"Reset Vector Storage/Knowledge": "Vektorspeicher/Wissen zurücksetzen",
-	"Reset view": "",
+	"Reset view": "Ansicht zurücksetzen",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.",
 	"Response splitting": "Antwortaufteilung",
 	"Result": "Ergebnis",
-	"Retrieval Query Generation": "",
+	"Retrieval Query Generation": "Abfragegenerierung",
 	"Rich Text Input for Chat": "Rich-Text-Eingabe für Unterhaltungen",
 	"RK": "RK",
 	"Role": "Rolle",
@@ -810,11 +816,11 @@
 	"Search options": "Suchoptionen",
 	"Search Prompts": "Prompts durchsuchen...",
 	"Search Result Count": "Anzahl der Suchergebnisse",
-	"Search the internet": "",
+	"Search the internet": "Im Internet suchen",
 	"Search Tools": "Werkzeuge durchsuchen...",
 	"SearchApi API Key": "SearchApi-API-Schlüssel",
 	"SearchApi Engine": "SearchApi-Engine",
-	"Searched {{count}} sites": "",
+	"Searched {{count}} sites": "{{count}} Websites durchsucht",
 	"Searching \"{{searchQuery}}\"": "Suche nach \"{{searchQuery}}\"",
 	"Searching Knowledge for \"{{searchQuery}}\"": "Suche im Wissen nach \"{{searchQuery}}\"",
 	"Searxng Query URL": "Searxng-Abfrage-URL",
@@ -829,7 +835,7 @@
 	"Select a pipeline": "Wählen Sie eine Pipeline",
 	"Select a pipeline url": "Wählen Sie eine Pipeline-URL",
 	"Select a tool": "Wählen Sie ein Werkzeug",
-	"Select an Ollama instance": "",
+	"Select an Ollama instance": "Wählen Sie eine Ollama-Instanz",
 	"Select Engine": "Engine auswählen",
 	"Select Knowledge": "Wissensdatenbank auswählen",
 	"Select model": "Modell auswählen",
@@ -856,7 +862,7 @@
 	"Set Scheduler": "Scheduler festlegen",
 	"Set Steps": "Schrittgröße festlegen",
 	"Set Task Model": "Aufgabenmodell festlegen",
-	"Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "",
+	"Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "Legen Sie die Anzahl der Schichten fest, die auf die GPU ausgelagert werden. Eine Erhöhung dieses Wertes kann die Leistung für Modelle, die für die GPU-Beschleunigung optimiert sind, erheblich verbessern, kann jedoch auch mehr Strom und GPU-Ressourcen verbrauchen.",
 	"Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Legt die Anzahl der für die Berechnung verwendeten GPU-Geräte fest. Diese Option steuert, wie viele GPU-Geräte (falls verfügbar) zur Verarbeitung eingehender Anfragen verwendet werden. Eine Erhöhung dieses Wertes kann die Leistung für Modelle, die für GPU-Beschleunigung optimiert sind, erheblich verbessern, kann jedoch auch mehr Strom und GPU-Ressourcen verbrauchen.",
 	"Set Voice": "Stimme festlegen",
 	"Set whisper model": "Whisper-Modell festlegen",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Einstellungen erfolgreich gespeichert!",
 	"Share": "Teilen",
 	"Share Chat": "Unterhaltung teilen",
-	"Share to OpenWebUI Community": "Mit OpenWebUI Community teilen",
+	"Share to Open WebUI Community": "Mit OpenWebUI Community teilen",
 	"Show": "Anzeigen",
 	"Show \"What's New\" modal on login": "\"Was gibt's Neues\"-Modal beim Anmelden anzeigen",
 	"Show Admin Details in Account Pending Overlay": "Admin-Details im Account-Pending-Overlay anzeigen",
@@ -883,7 +889,7 @@
 	"Sign up": "Registrieren",
 	"Sign up to {{WEBUI_NAME}}": "Bei {{WEBUI_NAME}} registrieren",
 	"Signing in to {{WEBUI_NAME}}": "Wird bei {{WEBUI_NAME}} angemeldet",
-	"sk-1234": "",
+	"sk-1234": "sk-1234",
 	"Source": "Quelle",
 	"Speech Playback Speed": "Sprachwiedergabegeschwindigkeit",
 	"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
@@ -903,7 +909,7 @@
 	"System": "System",
 	"System Instructions": "Systemanweisungen",
 	"System Prompt": "System-Prompt",
-	"Tags Generation": "",
+	"Tags Generation": "Tag-Generierung",
 	"Tags Generation Prompt": "Prompt für Tag-Generierung",
 	"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "Tail-Free Sampling wird verwendet, um den Einfluss weniger wahrscheinlicher Tokens auf die Ausgabe zu reduzieren. Ein höherer Wert (z.B. 2.0) reduziert den Einfluss stärker, während ein Wert von 1.0 diese Einstellung deaktiviert. (Standard: 1)",
 	"Tap to interrupt": "Zum Unterbrechen tippen",
@@ -921,7 +927,7 @@
 	"The batch size determines how many text requests are processed together at once. A higher batch size can increase the performance and speed of the model, but it also requires more memory.  (Default: 512)": "Die Batch-Größe bestimmt, wie viele Textanfragen gleichzeitig verarbeitet werden. Eine größere Batch-Größe kann die Leistung und Geschwindigkeit des Modells erhöhen, erfordert jedoch auch mehr Speicher. (Standard: 512)",
 	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Die Entwickler hinter diesem Plugin sind leidenschaftliche Freiwillige aus der Community. Wenn Sie dieses Plugin hilfreich finden, erwägen Sie bitte, zu seiner Entwicklung beizutragen.",
 	"The evaluation leaderboard is based on the Elo rating system and is updated in real-time.": "Die Bewertungs-Bestenliste basiert auf dem Elo-Bewertungssystem und wird in Echtzeit aktualisiert.",
-	"The LDAP attribute that maps to the mail that users use to sign in.": "",
+	"The LDAP attribute that maps to the mail that users use to sign in.": "Das LDAP-Attribut, das der Mail zugeordnet ist, die Benutzer zum Anmelden verwenden.",
 	"The LDAP attribute that maps to the username that users use to sign in.": "Das LDAP-Attribut, das dem Benutzernamen zugeordnet ist, den Benutzer zum Anmelden verwenden.",
 	"The leaderboard is currently in beta, and we may adjust the rating calculations as we refine the algorithm.": "Die Bestenliste befindet sich derzeit in der Beta-Phase, und es ist möglich, dass wir die Bewertungsberechnungen anpassen, während wir den Algorithmus verfeinern.",
 	"The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "Die maximale Dateigröße in MB. Wenn die Dateigröße dieses Limit überschreitet, wird die Datei nicht hochgeladen.",
@@ -943,7 +949,8 @@
 	"This will delete all models including custom models and cannot be undone.": "Dies wird alle Modelle einschließlich benutzerdefinierter Modelle löschen und kann nicht rückgängig gemacht werden.",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Dadurch wird die Wissensdatenbank zurückgesetzt und alle Dateien synchronisiert. Möchten Sie fortfahren?",
 	"Thorough explanation": "Ausführliche Erklärung",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "Nachgedacht für {{DURATION}}",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika-Server-URL erforderlich.",
 	"Tiktoken": "Tiktoken",
@@ -958,7 +965,7 @@
 	"To access the GGUF models available for downloading,": "Um auf die verfügbaren GGUF-Modelle zuzugreifen,",
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Um auf das WebUI zugreifen zu können, wenden Sie sich bitte an einen Administrator. Administratoren können den Benutzerstatus über das Admin-Panel verwalten.",
 	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Um Wissensdatenbanken hier anzuhängen, fügen Sie sie zunächst dem Arbeitsbereich \"Wissen\" hinzu.",
-	"To learn more about available endpoints, visit our documentation.": "",
+	"To learn more about available endpoints, visit our documentation.": "Um mehr über verfügbare Endpunkte zu erfahren, besuchen Sie unsere Dokumentation.",
 	"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Um Ihre Privatsphäre zu schützen, werden nur Bewertungen, Modell-IDs, Tags und Metadaten aus Ihrem Feedback geteilt – Ihre Unterhaltungen bleiben privat und werden nicht einbezogen.",
 	"To select actions here, add them to the \"Functions\" workspace first.": "Um Aktionen auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
 	"To select filters here, add them to the \"Functions\" workspace first.": "Um Filter auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
@@ -980,7 +987,7 @@
 	"Tools": "Werkzeuge",
 	"Tools Access": "Werkzeugzugriff",
 	"Tools are a function calling system with arbitrary code execution": "Wekzeuge sind ein Funktionssystem mit beliebiger Codeausführung",
-	"Tools Function Calling Prompt": "",
+	"Tools Function Calling Prompt": "Prompt für Funktionssystemaufrufe",
 	"Tools have a function calling system that allows arbitrary code execution": "Werkezuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht",
 	"Tools have a function calling system that allows arbitrary code execution.": "Werkzeuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht.",
 	"Top K": "Top K",
@@ -992,7 +999,7 @@
 	"TTS Voice": "TTS-Stimme",
 	"Type": "Art",
 	"Type Hugging Face Resolve (Download) URL": "Geben Sie die Hugging Face Resolve-URL ein",
-	"Uh-oh! There was an issue with the response.": "",
+	"Uh-oh! There was an issue with the response.": "Oh nein! Es gab ein Problem mit der Antwort.",
 	"UI": "Oberfläche",
 	"Unarchive All": "Alle wiederherstellen",
 	"Unarchive All Archived Chats": "Alle archivierten Unterhaltungen wiederherstellen",
@@ -1038,7 +1045,7 @@
 	"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
 	"Version": "Version",
 	"Version {{selectedVersion}} of {{totalVersions}}": "Version {{selectedVersion}} von {{totalVersions}}",
-	"View Replies": "",
+	"View Replies": "Antworten anzeigen",
 	"Visibility": "Sichtbarkeit",
 	"Voice": "Stimme",
 	"Voice Input": "Spracheingabe",
@@ -1051,11 +1058,11 @@
 	"Web Loader Settings": "Web Loader Einstellungen",
 	"Web Search": "Websuche",
 	"Web Search Engine": "Suchmaschine",
-	"Web Search in Chat": "",
-	"Web Search Query Generation": "",
+	"Web Search in Chat": "Websuche im Chat",
+	"Web Search Query Generation": "Abfragegenerierung für Websuche",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "WebUI-Einstellungen",
-	"WebUI URL": "",
+	"WebUI URL": "WebUI-URL",
 	"WebUI will make requests to \"{{url}}/api/chat\"": "WebUI wird Anfragen an \"{{url}}/api/chat\" senden",
 	"WebUI will make requests to \"{{url}}/chat/completions\"": "WebUI wird Anfragen an \"{{url}}/chat/completions\" senden",
 	"What are you trying to achieve?": "Was versuchen Sie zu erreichen?",
@@ -1070,7 +1077,7 @@
 	"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)": "Funktioniert zusammen mit top-k. Ein höherer Wert (z.B. 0,95) führt zu vielfältigerem Text, während ein niedrigerer Wert (z.B. 0,5) fokussierteren und konservativeren Text erzeugt. (Standard: 0,9)",
 	"Workspace": "Arbeitsbereich",
 	"Workspace Permissions": "Arbeitsbereichsberechtigungen",
-	"Write": "",
+	"Write": "Schreiben",
 	"Write a prompt suggestion (e.g. Who are you?)": "Schreiben Sie einen Promptvorschlag (z. B. Wer sind Sie?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.",
 	"Write something...": "Schreiben Sie etwas...",
@@ -1080,8 +1087,8 @@
 	"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Sie können nur mit maximal {{maxCount}} Datei(en) gleichzeitig chatten.",
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Personalisieren Sie Interaktionen mit LLMs, indem Sie über die Schaltfläche \"Verwalten\" Erinnerungen hinzufügen.",
 	"You cannot upload an empty file.": "Sie können keine leere Datei hochladen.",
-	"You do not have permission to access this feature.": "",
-	"You do not have permission to upload files": "",
+	"You do not have permission to access this feature.": "Sie haben keine Berechtigung, auf diese Funktion zuzugreifen.",
+	"You do not have permission to upload files": "Sie haben keine Berechtigung, Dateien hochzuladen",
 	"You do not have permission to upload files.": "Sie haben keine Berechtigung zum Hochladen von Dateien.",
 	"You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.",
 	"You have shared this chat": "Sie haben diese Unterhaltung geteilt",

+ 10 - 3
src/lib/i18n/locales/dg-DG/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Click to select",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "Documents",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "does not connect external, data stays safe locally.",
+	"Domain Filter List": "",
 	"Don't have an account?": "No account? Much sad.",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Enter Overlap of Chunks",
 	"Enter Chunk Size": "Enter Size of Chunk",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Doge Speak",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Light",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "",
-	"Made by OpenWebUI Community": "Made by OpenWebUI Community",
+	"Made by Open WebUI Community": "Made by Open WebUI Community",
 	"Make sure to enclose them with": "Make sure to enclose them with",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "",
 	"Reasoning Effort": "",
 	"Record voice": "Record Bark",
-	"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
+	"Redirecting you to Open WebUI Community": "Redirecting you to Open WebUI Community",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Settings saved successfully! Very success!",
 	"Share": "",
 	"Share Chat": "",
-	"Share to OpenWebUI Community": "Share to OpenWebUI Community much community",
+	"Share to Open WebUI Community": "Share to Open WebUI Community much community",
 	"Show": "Show much show",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/el-GR/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Κάντε κλικ εδώ για να",
 	"Click here to download user import template file.": "Κάντε κλικ εδώ για να κατεβάσετε το αρχείο προτύπου εισαγωγής χρήστη.",
 	"Click here to learn more about faster-whisper and see the available models.": "Κάντε κλικ εδώ για να μάθετε περισσότερα σχετικά με το faster-whisper και να δείτε τα διαθέσιμα μοντέλα.",
+	"Click here to see available models.": "",
 	"Click here to select": "Κάντε κλικ εδώ για επιλογή",
 	"Click here to select a csv file.": "Κάντε κλικ εδώ για να επιλέξετε ένα αρχείο csv.",
 	"Click here to select a py file.": "Κάντε κλικ εδώ για να επιλέξετε ένα αρχείο py.",
@@ -290,6 +291,7 @@
 	"Documentation": "Τεκμηρίωση",
 	"Documents": "Έγγραφα",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "δεν κάνει καμία εξωτερική σύνδεση, και τα δεδομένα σας παραμένουν ασφαλή στον τοπικά φιλοξενούμενο διακομιστή σας.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Δεν έχετε λογαριασμό;",
 	"don't install random functions from sources you don't trust.": "μην εγκαθιστάτε τυχαίες λειτουργίες από πηγές που δεν εμπιστεύεστε.",
 	"don't install random tools from sources you don't trust.": "μην εγκαθιστάτε τυχαία εργαλεία από πηγές που δεν εμπιστεύεστε.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Εισάγετε την Επικάλυψη Τμημάτων",
 	"Enter Chunk Size": "Εισάγετε το Μέγεθος Τμημάτων",
 	"Enter description": "Εισάγετε την περιγραφή",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Εισάγετε το Github Raw URL",
 	"Enter Google PSE API Key": "Εισάγετε το Κλειδί API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Η γνώση διαγράφηκε με επιτυχία.",
 	"Knowledge reset successfully.": "Η γνώση επαναφέρθηκε με επιτυχία.",
 	"Knowledge updated successfully": "Η γνώση ενημερώθηκε με επιτυχία",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Ετικέτα",
 	"Landing Page Mode": "Λειτουργία Σελίδας Άφιξης",
 	"Language": "Γλώσσα",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Αφήστε κενό για να συμπεριλάβετε όλα τα μοντέλα από το endpoint \"{{URL}}/models\"",
 	"Leave empty to include all models or select specific models": "Αφήστε κενό για να χρησιμοποιήσετε όλα τα μοντέλα ή επιλέξτε συγκεκριμένα μοντέλα",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Αφήστε κενό για να χρησιμοποιήσετε την προεπιλεγμένη προτροπή, ή εισάγετε μια προσαρμοσμένη προτροπή",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Φως",
 	"Listening...": "Ακούγεται...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Τοπικά Μοντέλα",
 	"Lost": "Χαμένος",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Δημιουργήθηκε από την Κοινότητα OpenWebUI",
+	"Made by Open WebUI Community": "Δημιουργήθηκε από την Κοινότητα OpenWebUI",
 	"Make sure to enclose them with": "Βεβαιωθείτε ότι τα περικλείετε με",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Βεβαιωθείτε ότι εξάγετε ένα αρχείο workflow.json ως μορφή API από το ComfyUI.",
 	"Manage": "Διαχείριση",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Ανάγνωση Φωναχτά",
 	"Reasoning Effort": "",
 	"Record voice": "Εγγραφή φωνής",
-	"Redirecting you to OpenWebUI Community": "Μετακατεύθυνση στην Κοινότητα OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Μετακατεύθυνση στην Κοινότητα OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Μειώνει την πιθανότητα δημιουργίας ανοησιών. Μια υψηλότερη τιμή (π.χ. 100) θα δώσει πιο ποικίλες απαντήσεις, ενώ μια χαμηλότερη τιμή (π.χ. 10) θα δημιουργήσει πιο συντηρητικές απαντήσεις. (Προεπιλογή: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Αναφέρεστε στον εαυτό σας ως \"User\" (π.χ., \"User μαθαίνει Ισπανικά\")",
 	"References from": "Αναφορές από",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Οι Ρυθμίσεις αποθηκεύτηκαν με επιτυχία!",
 	"Share": "Κοινή Χρήση",
 	"Share Chat": "Κοινή Χρήση Συνομιλίας",
-	"Share to OpenWebUI Community": "Κοινή Χρήση στην Κοινότητα OpenWebUI",
+	"Share to Open WebUI Community": "Κοινή Χρήση στην Κοινότητα OpenWebUI",
 	"Show": "Εμφάνιση",
 	"Show \"What's New\" modal on login": "Εμφάνιση του παράθυρου \"Τι νέο υπάρχει\" κατά την είσοδο",
 	"Show Admin Details in Account Pending Overlay": "Εμφάνιση Λεπτομερειών Διαχειριστή στο Υπέρθεση Εκκρεμής Λογαριασμού",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Αυτό θα επαναφέρει τη βάση γνώσης και θα συγχρονίσει όλα τα αρχεία. Θέλετε να συνεχίσετε;",
 	"Thorough explanation": "Λεπτομερής εξήγηση",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Απαιτείται το URL διακομιστή Tika.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/en-GB/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "",
+	"Domain Filter List": "",
 	"Don't have an account?": "",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "",
-	"Made by OpenWebUI Community": "",
+	"Made by Open WebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "",
 	"Reasoning Effort": "",
 	"Record voice": "",
-	"Redirecting you to OpenWebUI Community": "",
+	"Redirecting you to Open WebUI Community": "",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "",
 	"Share": "",
 	"Share Chat": "",
-	"Share to OpenWebUI Community": "",
+	"Share to Open WebUI Community": "",
 	"Show": "",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/en-US/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "",
 	"Click here to select a csv file.": "",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "",
+	"Domain Filter List": "",
 	"Don't have an account?": "",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "",
 	"Enter Chunk Size": "",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "",
 	"Enter Google PSE API Key": "",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "",
-	"Made by OpenWebUI Community": "",
+	"Made by Open WebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "",
 	"Reasoning Effort": "",
 	"Record voice": "",
-	"Redirecting you to OpenWebUI Community": "",
+	"Redirecting you to Open WebUI Community": "",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "",
 	"Share": "",
 	"Share Chat": "",
-	"Share to OpenWebUI Community": "",
+	"Share to Open WebUI Community": "",
 	"Show": "",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

文件差異過大導致無法顯示
+ 307 - 301
src/lib/i18n/locales/es-ES/translation.json


+ 10 - 3
src/lib/i18n/locales/eu-ES/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Klikatu hemen",
 	"Click here to download user import template file.": "Klikatu hemen erabiltzaileen inportazio txantiloia deskargatzeko.",
 	"Click here to learn more about faster-whisper and see the available models.": "Klikatu hemen faster-whisper-i buruz gehiago ikasteko eta eredu erabilgarriak ikusteko.",
+	"Click here to see available models.": "",
 	"Click here to select": "Klikatu hemen hautatzeko",
 	"Click here to select a csv file.": "Klikatu hemen csv fitxategi bat hautatzeko.",
 	"Click here to select a py file.": "Klikatu hemen py fitxategi bat hautatzeko.",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentazioa",
 	"Documents": "Dokumentuak",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "ez du kanpo konexiorik egiten, eta zure datuak modu seguruan mantentzen dira zure zerbitzari lokalean.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Ez duzu konturik?",
 	"don't install random functions from sources you don't trust.": "ez instalatu fidagarriak ez diren iturrietatik datozen ausazko funtzioak.",
 	"don't install random tools from sources you don't trust.": "ez instalatu fidagarriak ez diren iturrietatik datozen ausazko tresnak.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Sartu Zatien Gainjartzea (chunk overlap)",
 	"Enter Chunk Size": "Sartu Zati Tamaina",
 	"Enter description": "Sartu deskribapena",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Sartu Github Raw URLa",
 	"Enter Google PSE API Key": "Sartu Google PSE API Gakoa",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Ezagutza ongi ezabatu da.",
 	"Knowledge reset successfully.": "Ezagutza ongi berrezarri da.",
 	"Knowledge updated successfully": "Ezagutza ongi eguneratu da.",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Etiketa",
 	"Landing Page Mode": "Hasiera Orriaren Modua",
 	"Language": "Hizkuntza",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Utzi hutsik \"{{URL}}/models\" endpointuko eredu guztiak sartzeko",
 	"Leave empty to include all models or select specific models": "Utzi hutsik eredu guztiak sartzeko edo hautatu eredu zehatzak",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Utzi hutsik prompt lehenetsia erabiltzeko, edo sartu prompt pertsonalizatu bat",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Argia",
 	"Listening...": "Entzuten...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Modelo lokalak",
 	"Lost": "Galduta",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "OpenWebUI Komunitateak egina",
+	"Made by Open WebUI Community": "OpenWebUI Komunitateak egina",
 	"Make sure to enclose them with": "Ziurtatu hauek gehitzen dituzula",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Ziurtatu workflow.json fitxategia API formatu gisa esportatzen duzula ComfyUI-tik.",
 	"Manage": "Kudeatu",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Irakurri ozen",
 	"Reasoning Effort": "",
 	"Record voice": "Grabatu ahotsa",
-	"Redirecting you to OpenWebUI Community": "OpenWebUI Komunitatera berbideratzen",
+	"Redirecting you to Open WebUI Community": "OpenWebUI Komunitatera berbideratzen",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Zentzugabekeriak sortzeko probabilitatea murrizten du. Balio altuago batek (adib. 100) erantzun anitzagoak emango ditu, balio baxuago batek (adib. 10) kontserbadoreagoa izango den bitartean. (Lehenetsia: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Egin erreferentzia zure buruari \"Erabiltzaile\" gisa (adib., \"Erabiltzailea gaztelania ikasten ari da\")",
 	"References from": "Erreferentziak hemendik",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Ezarpenak ongi gorde dira!",
 	"Share": "Partekatu",
 	"Share Chat": "Partekatu txata",
-	"Share to OpenWebUI Community": "Partekatu OpenWebUI komunitatearekin",
+	"Share to Open WebUI Community": "Partekatu OpenWebUI komunitatearekin",
 	"Show": "Erakutsi",
 	"Show \"What's New\" modal on login": "Erakutsi \"Berritasunak\" modala saioa hastean",
 	"Show Admin Details in Account Pending Overlay": "Erakutsi administratzaile xehetasunak kontu zain geruzan",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Honek ezagutza-basea berrezarri eta fitxategi guztiak sinkronizatuko ditu. Jarraitu nahi duzu?",
 	"Thorough explanation": "Azalpen sakona",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika zerbitzariaren URLa beharrezkoa da.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/fa-IR/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "برای کمک اینجا را کلیک کنید.",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "برای انتخاب اینجا کلیک کنید",
 	"Click here to select a csv file.": "برای انتخاب یک فایل csv اینجا را کلیک کنید.",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "اسناد",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "هیچ اتصال خارجی ایجاد نمی کند و داده های شما به طور ایمن در سرور میزبان محلی شما باقی می ماند.",
+	"Domain Filter List": "",
 	"Don't have an account?": "حساب کاربری ندارید؟",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "مقدار Chunk Overlap را وارد کنید",
 	"Enter Chunk Size": "مقدار Chunk Size را وارد کنید",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "ادرس Github Raw را وارد کنید",
 	"Enter Google PSE API Key": "کلید API گوگل PSE را وارد کنید",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "زبان",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "روشن",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "ساخته شده توسط OpenWebUI Community",
+	"Made by Open WebUI Community": "ساخته شده توسط OpenWebUI Community",
 	"Make sure to enclose them with": "مطمئن شوید که آنها را با این محصور کنید:",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "خواندن به صورت صوتی",
 	"Reasoning Effort": "",
 	"Record voice": "ضبط صدا",
-	"Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community",
+	"Redirecting you to Open WebUI Community": "در حال هدایت به OpenWebUI Community",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "تنظیمات با موفقیت ذخیره شد!",
 	"Share": "اشتراک\u200cگذاری",
 	"Share Chat": "اشتراک\u200cگذاری چت",
-	"Share to OpenWebUI Community": "اشتراک گذاری با OpenWebUI Community",
+	"Share to Open WebUI Community": "اشتراک گذاری با OpenWebUI Community",
 	"Show": "نمایش",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "توضیح کامل",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/fi-FI/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Klikkaa tästä",
 	"Click here to download user import template file.": "Lataa käyttäjien tuontipohjatiedosto klikkaamalla tästä.",
 	"Click here to learn more about faster-whisper and see the available models.": "Klikkaa tästä oppiaksesi lisää faster-whisperista ja nähdäksesi saatavilla olevat mallit.",
+	"Click here to see available models.": "",
 	"Click here to select": "Klikkaa tästä valitaksesi",
 	"Click here to select a csv file.": "Klikkaa tästä valitaksesi CSV-tiedosto.",
 	"Click here to select a py file.": "Klikkaa tästä valitaksesi py-tiedosto.",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentaatio",
 	"Documents": "Asiakirjat",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "ei tee ulkoisia yhteyksiä, ja tietosi pysyvät turvallisesti paikallisesti isännöidyllä palvelimellasi.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Eikö sinulla ole tiliä?",
 	"don't install random functions from sources you don't trust.": "älä asenna satunnaisia toimintoja lähteistä, joihin et luota.",
 	"don't install random tools from sources you don't trust.": "älä asenna satunnaisia työkaluja lähteistä, joihin et luota.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Syötä osien päällekkäisyys",
 	"Enter Chunk Size": "Syötä osien koko",
 	"Enter description": "Kirjoita kuvaus",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Kirjoita Github Raw -URL-osoite",
 	"Enter Google PSE API Key": "Kirjoita Google PSE API -avain",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Tietokanta poistettu onnistuneesti.",
 	"Knowledge reset successfully.": "Tietokanta nollattu onnistuneesti.",
 	"Knowledge updated successfully": "Tietokanta päivitetty onnistuneesti",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Tunniste",
 	"Landing Page Mode": "Etusivun tila",
 	"Language": "Kieli",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Jätä tyhjäksi, jos haluat sisällyttää kaikki mallit \"{{URL}}/models\" -päätepistestä",
 	"Leave empty to include all models or select specific models": "Jätä tyhjäksi, jos haluat sisällyttää kaikki mallit tai valitse tietyt mallit",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Jätä tyhjäksi käyttääksesi oletuskehotetta tai kirjoita mukautettu kehote",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Vaalea",
 	"Listening...": "Kuuntelee...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Paikalliset mallit",
 	"Lost": "Mennyt",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Tehnyt OpenWebUI-yhteisö",
+	"Made by Open WebUI Community": "Tehnyt OpenWebUI-yhteisö",
 	"Make sure to enclose them with": "Varmista, että suljet ne",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Muista viedä workflow.json-tiedosto API-muodossa ComfyUI:sta.",
 	"Manage": "Hallitse",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Lue ääneen",
 	"Reasoning Effort": "",
 	"Record voice": "Nauhoita ääni",
-	"Redirecting you to OpenWebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön",
+	"Redirecting you to Open WebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Vähentää merkityksetöntä sisältöä tuottavan todennäköisyyttä. Korkeampi arvo (esim. 100) antaa monipuolisempia vastauksia, kun taas alhaisempi arvo (esim. 10) on konservatiivisempi. (Oletus: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Viittaa itseen \"Käyttäjänä\" (esim. \"Käyttäjä opiskelee espanjaa\")",
 	"References from": "Viitteet lähteistä",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Asetukset tallennettu onnistuneesti!",
 	"Share": "Jaa",
 	"Share Chat": "Jaa keskustelu",
-	"Share to OpenWebUI Community": "Jaa OpenWebUI-yhteisöön",
+	"Share to Open WebUI Community": "Jaa OpenWebUI-yhteisöön",
 	"Show": "Näytä",
 	"Show \"What's New\" modal on login": "Näytä \"Mitä uutta\" -modaali kirjautumisen yhteydessä",
 	"Show Admin Details in Account Pending Overlay": "Näytä ylläpitäjän tiedot odottavan tilin päällä",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Tämä nollaa tietokannan ja synkronoi kaikki tiedostot. Haluatko jatkaa?",
 	"Thorough explanation": "Perusteellinen selitys",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika Server URL vaaditaan.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/fr-CA/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Cliquez ici pour",
 	"Click here to download user import template file.": "Cliquez ici pour télécharger le fichier modèle d'importation utilisateur.",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Cliquez ici pour sélectionner",
 	"Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier CSV.",
 	"Click here to select a py file.": "Cliquez ici pour sélectionner un fichier .py.",
@@ -290,6 +291,7 @@
 	"Documentation": "Documentation",
 	"Documents": "Documents",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "ne fait aucune connexion externe et garde vos données en sécurité sur votre serveur local.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Vous n'avez pas de compte ?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Entrez le chevauchement de chunk",
 	"Enter Chunk Size": "Entrez la taille de bloc",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Entrez l'URL brute de GitHub",
 	"Enter Google PSE API Key": "Entrez la clé API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Langue",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Lumineux",
 	"Listening...": "En train d'écouter...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Modèles locaux",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
+	"Made by Open WebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les inclure dans",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "Gérer",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Lire à haute voix",
 	"Reasoning Effort": "",
 	"Record voice": "Enregistrer la voix",
-	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Paramètres enregistrés avec succès !",
 	"Share": "Partager",
 	"Share Chat": "Partage de conversation",
-	"Share to OpenWebUI Community": "Partager avec la communauté OpenWebUI",
+	"Share to Open WebUI Community": "Partager avec la communauté OpenWebUI",
 	"Show": "Montrer",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "Afficher les détails de l'administrateur dans la superposition en attente du compte",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "Explication approfondie",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "URL du serveur Tika requise.",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/fr-FR/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Cliquez ici pour",
 	"Click here to download user import template file.": "Cliquez ici pour télécharger le fichier modèle d'importation des utilisateurs.",
 	"Click here to learn more about faster-whisper and see the available models.": "Cliquez ici pour en savoir plus sur faster-whisper et voir les modèles disponibles.",
+	"Click here to see available models.": "",
 	"Click here to select": "Cliquez ici pour sélectionner",
 	"Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier .csv.",
 	"Click here to select a py file.": "Cliquez ici pour sélectionner un fichier .py.",
@@ -290,6 +291,7 @@
 	"Documentation": "Documentation",
 	"Documents": "Documents",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "n'établit aucune connexion externe et garde vos données en sécurité sur votre serveur local.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Vous n'avez pas de compte ?",
 	"don't install random functions from sources you don't trust.": "n'installez pas de fonctions aléatoires provenant de sources auxquelles vous ne faites pas confiance.",
 	"don't install random tools from sources you don't trust.": "n'installez pas d'outils aléatoires provenant de sources auxquelles vous ne faites pas confiance.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Entrez le chevauchement des chunks",
 	"Enter Chunk Size": "Entrez la taille des chunks",
 	"Enter description": "Entrez la description",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Entrez l'URL brute de GitHub",
 	"Enter Google PSE API Key": "Entrez la clé API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Connaissance supprimée avec succès.",
 	"Knowledge reset successfully.": "Connaissance réinitialisée avec succès.",
 	"Knowledge updated successfully": "Connaissance mise à jour avec succès",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Étiquette",
 	"Landing Page Mode": "Mode de la page d'accueil",
 	"Language": "Langue",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Laissez vide pour inclure tous les modèles depuis le point de terminaison \"{{URL}}/models\"",
 	"Leave empty to include all models or select specific models": "Laissez vide pour inclure tous les modèles ou sélectionnez des modèles spécifiques",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Laissez vide pour utiliser le prompt par défaut, ou entrez un prompt personnalisé",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Clair",
 	"Listening...": "Écoute en cours...",
 	"Llama.cpp": "Llama.cpp",
@@ -574,7 +580,7 @@
 	"Local Models": "Modèles locaux",
 	"Lost": "Perdu",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
+	"Made by Open WebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les inclure dans",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Veillez à exporter un fichier workflow.json au format API depuis ComfyUI.",
 	"Manage": "Gérer",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Lire à haute voix",
 	"Reasoning Effort": "Effort de raisonnement",
 	"Record voice": "Enregistrer la voix",
-	"Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Redirection vers la communauté OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Réduit la probabilité de générer des non-sens. Une valeur plus élevée (par exemple 100) donnera des réponses plus diversifiées, tandis qu'une valeur plus basse (par exemple 10) sera plus conservatrice. (Par défaut : 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)",
 	"References from": "Références de",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Paramètres enregistrés avec succès !",
 	"Share": "Partager",
 	"Share Chat": "Partage de conversation",
-	"Share to OpenWebUI Community": "Partager avec la communauté OpenWebUI",
+	"Share to Open WebUI Community": "Partager avec la communauté OpenWebUI",
 	"Show": "Afficher",
 	"Show \"What's New\" modal on login": "Afficher la fenêtre modale \"Quoi de neuf\" lors de la connexion",
 	"Show Admin Details in Account Pending Overlay": "Afficher les coordonnées de l'administrateur aux comptes en attente",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Cela réinitialisera la base de connaissances et synchronisera tous les fichiers. Souhaitez-vous continuer ?",
 	"Thorough explanation": "Explication approfondie",
 	"Thought for {{DURATION}}": "Réflexion de {{DURATION}}",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "URL du serveur Tika requise.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/he-IL/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "לחץ כאן כדי",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "לחץ כאן לבחירה",
 	"Click here to select a csv file.": "לחץ כאן לבחירת קובץ csv.",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "מסמכים",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "לא מבצע חיבורים חיצוניים, והנתונים שלך נשמרים באופן מאובטח בשרת המקומי שלך.",
+	"Domain Filter List": "",
 	"Don't have an account?": "אין לך חשבון?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "הזן חפיפת נתונים",
 	"Enter Chunk Size": "הזן גודל נתונים",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "הזן כתובת URL של Github Raw",
 	"Enter Google PSE API Key": "הזן מפתח API של Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "שפה",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "בהיר",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "נוצר על ידי קהילת OpenWebUI",
+	"Made by Open WebUI Community": "נוצר על ידי קהילת OpenWebUI",
 	"Make sure to enclose them with": "ודא להקיף אותם עם",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "קרא בקול",
 	"Reasoning Effort": "",
 	"Record voice": "הקלט קול",
-	"Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI",
+	"Redirecting you to Open WebUI Community": "מפנה אותך לקהילת OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "ההגדרות נשמרו בהצלחה!",
 	"Share": "שתף",
 	"Share Chat": "שתף צ'אט",
-	"Share to OpenWebUI Community": "שתף לקהילת OpenWebUI",
+	"Share to Open WebUI Community": "שתף לקהילת OpenWebUI",
 	"Show": "הצג",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "תיאור מפורט",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/hi-IN/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "यहां क्लिक करें",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "चयन करने के लिए यहां क्लिक करें।",
 	"Click here to select a csv file.": "सीएसवी फ़ाइल का चयन करने के लिए यहां क्लिक करें।",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "दस्तावेज़",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "कोई बाहरी कनेक्शन नहीं बनाता है, और आपका डेटा आपके स्थानीय रूप से होस्ट किए गए सर्वर पर सुरक्षित रूप से रहता है।",
+	"Domain Filter List": "",
 	"Don't have an account?": "कोई खाता नहीं है?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "चंक ओवरलैप दर्ज करें",
 	"Enter Chunk Size": "खंड आकार दर्ज करें",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Github Raw URL दर्ज करें",
 	"Enter Google PSE API Key": "Google PSE API कुंजी दर्ज करें",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "भाषा",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "सुन",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "OpenWebUI समुदाय द्वारा निर्मित",
+	"Made by Open WebUI Community": "OpenWebUI समुदाय द्वारा निर्मित",
 	"Make sure to enclose them with": "उन्हें संलग्न करना सुनिश्चित करें",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "जोर से पढ़ें",
 	"Reasoning Effort": "",
 	"Record voice": "आवाज रिकॉर्ड करना",
-	"Redirecting you to OpenWebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है",
+	"Redirecting you to Open WebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "सेटिंग्स सफलतापूर्वक सहेजी गईं!",
 	"Share": "साझा करें",
 	"Share Chat": "चैट साझा करें",
-	"Share to OpenWebUI Community": "OpenWebUI समुदाय में साझा करें",
+	"Share to Open WebUI Community": "OpenWebUI समुदाय में साझा करें",
 	"Show": "दिखाओ",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "विस्तृत व्याख्या",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/hr-HR/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Kliknite ovdje za",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Kliknite ovdje za odabir",
 	"Click here to select a csv file.": "Kliknite ovdje da odaberete csv datoteku.",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentacija",
 	"Documents": "Dokumenti",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "ne uspostavlja vanjske veze, a vaši podaci ostaju sigurno na vašem lokalno hostiranom poslužitelju.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Nemate račun?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Unesite preklapanje dijelova",
 	"Enter Chunk Size": "Unesite veličinu dijela",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Unesite Github sirovi URL",
 	"Enter Google PSE API Key": "Unesite Google PSE API ključ",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Jezik",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Svijetlo",
 	"Listening...": "Slušam...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Lokalni modeli",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Izradio OpenWebUI Community",
+	"Made by Open WebUI Community": "Izradio OpenWebUI Community",
 	"Make sure to enclose them with": "Provjerite da ih zatvorite s",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "Upravljaj",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Čitaj naglas",
 	"Reasoning Effort": "",
 	"Record voice": "Snimanje glasa",
-	"Redirecting you to OpenWebUI Community": "Preusmjeravanje na OpenWebUI zajednicu",
+	"Redirecting you to Open WebUI Community": "Preusmjeravanje na OpenWebUI zajednicu",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Nazivajte se \"Korisnik\" (npr. \"Korisnik uči španjolski\")",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Postavke su uspješno spremljene!",
 	"Share": "Podijeli",
 	"Share Chat": "Podijeli razgovor",
-	"Share to OpenWebUI Community": "Podijeli u OpenWebUI zajednici",
+	"Share to Open WebUI Community": "Podijeli u OpenWebUI zajednici",
 	"Show": "Pokaži",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "Detaljno objašnjenje",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/hu-HU/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Kattints ide",
 	"Click here to download user import template file.": "Kattints ide a felhasználó importálási sablon letöltéséhez.",
 	"Click here to learn more about faster-whisper and see the available models.": "Kattints ide, hogy többet tudj meg a faster-whisperről és lásd az elérhető modelleket.",
+	"Click here to see available models.": "",
 	"Click here to select": "Kattints ide a kiválasztáshoz",
 	"Click here to select a csv file.": "Kattints ide egy CSV fájl kiválasztásához.",
 	"Click here to select a py file.": "Kattints ide egy py fájl kiválasztásához.",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentáció",
 	"Documents": "Dokumentumok",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "nem létesít külső kapcsolatokat, és az adataid biztonságban maradnak a helyileg hosztolt szervereden.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Nincs még fiókod?",
 	"don't install random functions from sources you don't trust.": "ne telepíts véletlenszerű funkciókat olyan forrásokból, amelyekben nem bízol.",
 	"don't install random tools from sources you don't trust.": "ne telepíts véletlenszerű eszközöket olyan forrásokból, amelyekben nem bízol.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Add meg a darab átfedést",
 	"Enter Chunk Size": "Add meg a darab méretet",
 	"Enter description": "Add meg a leírást",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Add meg a Github Raw URL-t",
 	"Enter Google PSE API Key": "Add meg a Google PSE API kulcsot",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "Tudásbázis sikeresen törölve.",
 	"Knowledge reset successfully.": "Tudásbázis sikeresen visszaállítva.",
 	"Knowledge updated successfully": "Tudásbázis sikeresen frissítve",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "Kezdőlap mód",
 	"Language": "Nyelv",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "Hagyja üresen az összes modell használatához, vagy válasszon ki konkrét modelleket",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Hagyja üresen az alapértelmezett prompt használatához, vagy adjon meg egyéni promptot",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Világos",
 	"Listening...": "Hallgatás...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Helyi modellek",
 	"Lost": "Elveszett",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Az OpenWebUI közösség által készítve",
+	"Made by Open WebUI Community": "Az OpenWebUI közösség által készítve",
 	"Make sure to enclose them with": "Győződjön meg róla, hogy körülveszi őket",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Győződjön meg róla, hogy exportál egy workflow.json fájlt API formátumban a ComfyUI-ból.",
 	"Manage": "Kezelés",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Felolvasás",
 	"Reasoning Effort": "",
 	"Record voice": "Hang rögzítése",
-	"Redirecting you to OpenWebUI Community": "Átirányítás az OpenWebUI közösséghez",
+	"Redirecting you to Open WebUI Community": "Átirányítás az OpenWebUI közösséghez",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Hivatkozzon magára \"Felhasználó\"-ként (pl. \"A Felhasználó spanyolul tanul\")",
 	"References from": "Hivatkozások innen",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Beállítások sikeresen mentve!",
 	"Share": "Megosztás",
 	"Share Chat": "Beszélgetés megosztása",
-	"Share to OpenWebUI Community": "Megosztás az OpenWebUI közösséggel",
+	"Share to Open WebUI Community": "Megosztás az OpenWebUI közösséggel",
 	"Show": "Mutat",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "Admin részletek megjelenítése a függő fiók átfedésben",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Ez visszaállítja a tudásbázist és szinkronizálja az összes fájlt. Szeretné folytatni?",
 	"Thorough explanation": "Alapos magyarázat",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika szerver URL szükséges.",
 	"Tiktoken": "Tiktoken",

+ 10 - 3
src/lib/i18n/locales/id-ID/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Klik di sini untuk",
 	"Click here to download user import template file.": "Klik di sini untuk mengunduh file templat impor pengguna.",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Klik di sini untuk memilih",
 	"Click here to select a csv file.": "Klik di sini untuk memilih file csv.",
 	"Click here to select a py file.": "Klik di sini untuk memilih file py.",
@@ -290,6 +291,7 @@
 	"Documentation": "Dokumentasi",
 	"Documents": "Dokumen",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "tidak membuat koneksi eksternal apa pun, dan data Anda tetap aman di server yang dihosting secara lokal.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Tidak memiliki akun?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Masukkan Tumpang Tindih Chunk",
 	"Enter Chunk Size": "Masukkan Ukuran Potongan",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Masukkan URL Mentah Github",
 	"Enter Google PSE API Key": "Masukkan Kunci API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Bahasa",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Cahaya",
 	"Listening...": "Mendengarkan",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "Model Lokal",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Dibuat oleh Komunitas OpenWebUI",
+	"Made by Open WebUI Community": "Dibuat oleh Komunitas OpenWebUI",
 	"Make sure to enclose them with": "Pastikan untuk melampirkannya dengan",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "Mengelola",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Baca dengan Keras",
 	"Reasoning Effort": "",
 	"Record voice": "Rekam suara",
-	"Redirecting you to OpenWebUI Community": "Mengarahkan Anda ke Komunitas OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Mengarahkan Anda ke Komunitas OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Merujuk diri Anda sebagai \"Pengguna\" (misalnya, \"Pengguna sedang belajar bahasa Spanyol\")",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Pengaturan berhasil disimpan!",
 	"Share": "Berbagi",
 	"Share Chat": "Bagikan Obrolan",
-	"Share to OpenWebUI Community": "Bagikan ke Komunitas OpenWebUI",
+	"Share to Open WebUI Community": "Bagikan ke Komunitas OpenWebUI",
 	"Show": "Tampilkan",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "Tampilkan Detail Admin di Hamparan Akun Tertunda",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "Penjelasan menyeluruh",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/ie-GA/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Cliceáil anseo chun",
 	"Click here to download user import template file.": "Cliceáil anseo chun an comhad iompórtála úsáideora a íoslódáil.",
 	"Click here to learn more about faster-whisper and see the available models.": "Cliceáil anseo chun níos mó a fhoghlaim faoi cogar níos tapúla agus na múnlaí atá ar fáil a fheiceáil.",
+	"Click here to see available models.": "",
 	"Click here to select": "Cliceáil anseo chun roghnú",
 	"Click here to select a csv file.": "Cliceáil anseo chun comhad csv a roghnú.",
 	"Click here to select a py file.": "Cliceáil anseo chun comhad py a roghnú.",
@@ -290,6 +291,7 @@
 	"Documentation": "Doiciméadú",
 	"Documents": "Doiciméid",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "ní dhéanann sé aon naisc sheachtracha, agus fanann do chuid sonraí go slán ar do fhreastalaí a óstáiltear go háitiúil.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Níl cuntas agat?",
 	"don't install random functions from sources you don't trust.": "ná suiteáil feidhmeanna randamacha ó fhoinsí nach bhfuil muinín agat.",
 	"don't install random tools from sources you don't trust.": "ná suiteáil uirlisí randamacha ó fhoinsí nach bhfuil muinín agat.",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Cuir isteach Chunk Forluí",
 	"Enter Chunk Size": "Cuir isteach Méid an Chunc",
 	"Enter description": "Iontráil cur síos",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "Cuir isteach Eochair Exa API",
 	"Enter Github Raw URL": "Cuir isteach URL Github Raw",
 	"Enter Google PSE API Key": "Cuir isteach Eochair API Google PSE",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "D'éirigh leis an eolas a scriosadh.",
 	"Knowledge reset successfully.": "D'éirigh le hathshocrú eolais.",
 	"Knowledge updated successfully": "D'éirigh leis an eolas a nuashonrú",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "Lipéad",
 	"Landing Page Mode": "Mód Leathanach Tuirlingthe",
 	"Language": "Teanga",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "Fág folamh chun gach múnla ón gcríochphointe \"{{URL}}/models\" a chur san áireamh",
 	"Leave empty to include all models or select specific models": "Fág folamh chun gach múnla a chur san áireamh nó roghnaigh múnlaí sonracha",
 	"Leave empty to use the default prompt, or enter a custom prompt": "Fág folamh chun an leid réamhshocraithe a úsáid, nó cuir isteach leid saincheaptha",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Solas",
 	"Listening...": "Éisteacht...",
 	"Llama.cpp": "Llama.cpp",
@@ -574,7 +580,7 @@
 	"Local Models": "Múnlaí Áitiúla",
 	"Lost": "Cailleadh",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Déanta ag OpenWebUI Community",
+	"Made by Open WebUI Community": "Déanta ag OpenWebUI Community",
 	"Make sure to enclose them with": "Déan cinnte iad a cheangal le",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Déan cinnte comhad workflow.json a onnmhairiú mar fhormáid API ó ComfyUI.",
 	"Manage": "Bainistiú",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Léigh Ard",
 	"Reasoning Effort": "Iarracht Réasúnúcháin",
 	"Record voice": "Taifead guth",
-	"Redirecting you to OpenWebUI Community": "Tú a atreorú chuig OpenWebUI Community",
+	"Redirecting you to Open WebUI Community": "Tú a atreorú chuig OpenWebUI Community",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Laghdaíonn sé an dóchúlacht go giniúint nonsense. Tabharfaidh luach níos airde (m.sh. 100) freagraí níos éagsúla, agus beidh luach níos ísle (m.sh. 10) níos coimeádaí. (Réamhshocrú: 40)",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Tagairt duit féin mar \"Úsáideoir\" (m.sh., \"Tá an úsáideoir ag foghlaim Spáinnis\")",
 	"References from": "Tagairtí ó",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Socruithe sábhálta go rathúil!",
 	"Share": "Comhroinn",
 	"Share Chat": "Comhroinn Comhrá",
-	"Share to OpenWebUI Community": "Comhroinn le Pobal OpenWebUI",
+	"Share to Open WebUI Community": "Comhroinn le Pobal OpenWebUI",
 	"Show": "Taispeáin",
 	"Show \"What's New\" modal on login": "Taispeáin módúil \"Cad atá Nua\" ar logáil isteach",
 	"Show Admin Details in Account Pending Overlay": "Taispeáin Sonraí Riaracháin sa Chuntas ar Feitheamh Forleagan",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Déanfaidh sé seo an bonn eolais a athshocrú agus gach comhad a shioncronú. Ar mhaith leat leanúint ar aghaidh?",
 	"Thorough explanation": "Míniú críochnúil",
 	"Thought for {{DURATION}}": "Smaoineamh ar {{DURATION}}",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Teastaíonn URL Freastalaí Tika.",
 	"Tiktoken": "Tictoken",

+ 10 - 3
src/lib/i18n/locales/it-IT/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "Clicca qui per",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "Clicca qui per selezionare",
 	"Click here to select a csv file.": "Clicca qui per selezionare un file csv.",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "Documenti",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "non effettua connessioni esterne e i tuoi dati rimangono al sicuro sul tuo server ospitato localmente.",
+	"Domain Filter List": "",
 	"Don't have an account?": "Non hai un account?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "Inserisci la sovrapposizione chunk",
 	"Enter Chunk Size": "Inserisci la dimensione chunk",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Immettere l'URL grezzo di Github",
 	"Enter Google PSE API Key": "Inserisci la chiave API PSE di Google",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "Lingua",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "Chiaro",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "Realizzato dalla comunità OpenWebUI",
+	"Made by Open WebUI Community": "Realizzato dalla comunità OpenWebUI",
 	"Make sure to enclose them with": "Assicurati di racchiuderli con",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "Leggi ad alta voce",
 	"Reasoning Effort": "",
 	"Record voice": "Registra voce",
-	"Redirecting you to OpenWebUI Community": "Reindirizzamento alla comunità OpenWebUI",
+	"Redirecting you to Open WebUI Community": "Reindirizzamento alla comunità OpenWebUI",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "Impostazioni salvate con successo!",
 	"Share": "Condividi",
 	"Share Chat": "Condividi chat",
-	"Share to OpenWebUI Community": "Condividi con la comunità OpenWebUI",
+	"Share to Open WebUI Community": "Condividi con la comunità OpenWebUI",
 	"Show": "Mostra",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "Spiegazione dettagliata",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/ja-JP/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "ここをクリックして",
 	"Click here to download user import template file.": "ユーザーテンプレートをインポートするにはここをクリックしてください。",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "選択するにはここをクリックしてください",
 	"Click here to select a csv file.": "CSVファイルを選択するにはここをクリックしてください。",
 	"Click here to select a py file.": "Pythonスクリプトファイルを選択するにはここをクリックしてください。",
@@ -290,6 +291,7 @@
 	"Documentation": "ドキュメント",
 	"Documents": "ドキュメント",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "外部接続を行わず、データはローカルでホストされているサーバー上に安全に保持されます。",
+	"Domain Filter List": "",
 	"Don't have an account?": "アカウントをお持ちではありませんか?",
 	"don't install random functions from sources you don't trust.": "信頼出来ないソースからランダムFunctionをインストールしないでください。",
 	"don't install random tools from sources you don't trust.": "信頼出来ないソースからランダムツールをインストールしないでください。",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "チャンクオーバーラップを入力してください",
 	"Enter Chunk Size": "チャンクサイズを入力してください",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Github Raw URLを入力",
 	"Enter Google PSE API Key": "Google PSE APIキーの入力",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "ナレッジベースの削除に成功しました",
 	"Knowledge reset successfully.": "ナレッジベースのリセットに成功しました",
 	"Knowledge updated successfully": "ナレッジベースのアップデートに成功しました",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "ランディングページモード",
 	"Language": "言語",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "カスタムプロンプトを入力。空欄ならデフォルトプロンプト",
+	"Leave model field empty to use the default model.": "",
 	"Light": "ライト",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "ローカルモデル",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "OpenWebUI コミュニティによって作成",
+	"Made by Open WebUI Community": "OpenWebUI コミュニティによって作成",
 	"Make sure to enclose them with": "必ず次で囲んでください",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "管理",
@@ -754,7 +760,7 @@
 	"Read Aloud": "読み上げ",
 	"Reasoning Effort": "",
 	"Record voice": "音声を録音",
-	"Redirecting you to OpenWebUI Community": "OpenWebUI コミュニティにリダイレクトしています",
+	"Redirecting you to Open WebUI Community": "OpenWebUI コミュニティにリダイレクトしています",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "設定が正常に保存されました!",
 	"Share": "共有",
 	"Share Chat": "チャットを共有",
-	"Share to OpenWebUI Community": "OpenWebUI コミュニティに共有",
+	"Share to Open WebUI Community": "OpenWebUI コミュニティに共有",
 	"Show": "表示",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "詳細な説明",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/ka-GE/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "დააკლიკე აქ",
 	"Click here to download user import template file.": "",
 	"Click here to learn more about faster-whisper and see the available models.": "",
+	"Click here to see available models.": "",
 	"Click here to select": "ასარჩევად, დააკლიკე აქ",
 	"Click here to select a csv file.": "ასარჩევად, დააკლიკე აქ",
 	"Click here to select a py file.": "",
@@ -290,6 +291,7 @@
 	"Documentation": "",
 	"Documents": "დოკუმენტები",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "არ ამყარებს გარე კავშირებს და თქვენი მონაცემები უსაფრთხოდ რჩება თქვენს ადგილობრივ სერვერზე.",
+	"Domain Filter List": "",
 	"Don't have an account?": "არ გაქვს ანგარიში?",
 	"don't install random functions from sources you don't trust.": "",
 	"don't install random tools from sources you don't trust.": "",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "შეიყვანეთ ნაწილის გადახურვა",
 	"Enter Chunk Size": "შეიყვანე ბლოკის ზომა",
 	"Enter description": "",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "შეიყვანეთ Github Raw URL",
 	"Enter Google PSE API Key": "შეიყვანეთ Google PSE API გასაღები",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "",
 	"Knowledge reset successfully.": "",
 	"Knowledge updated successfully": "",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "",
 	"Language": "ენა",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "",
 	"Leave empty to use the default prompt, or enter a custom prompt": "",
+	"Leave model field empty to use the default model.": "",
 	"Light": "მსუბუქი",
 	"Listening...": "",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "",
 	"Lost": "",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "დამზადებულია OpenWebUI საზოგადოების მიერ",
+	"Made by Open WebUI Community": "დამზადებულია OpenWebUI საზოგადოების მიერ",
 	"Make sure to enclose them with": "დარწმუნდით, რომ დაურთეთ ისინი",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "",
 	"Manage": "",
@@ -754,7 +760,7 @@
 	"Read Aloud": "ხმის ჩაწერა",
 	"Reasoning Effort": "",
 	"Record voice": "ხმის ჩაწერა",
-	"Redirecting you to OpenWebUI Community": "გადამისამართდებით OpenWebUI საზოგადოებაში",
+	"Redirecting you to Open WebUI Community": "გადამისამართდებით OpenWebUI საზოგადოებაში",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
 	"References from": "",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "პარამეტრები წარმატებით განახლდა!",
 	"Share": "გაზიარება",
 	"Share Chat": "გაზიარება",
-	"Share to OpenWebUI Community": "გააზიარე OpenWebUI საზოგადოებაში ",
+	"Share to Open WebUI Community": "გააზიარე OpenWebUI საზოგადოებაში ",
 	"Show": "ჩვენება",
 	"Show \"What's New\" modal on login": "",
 	"Show Admin Details in Account Pending Overlay": "",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "ვრცლად აღწერა",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 10 - 3
src/lib/i18n/locales/ko-KR/translation.json

@@ -163,6 +163,7 @@
 	"Click here to": "여기를 클릭하면",
 	"Click here to download user import template file.": "사용자 삽입 템플렛 파일을 다운받으려면 여기를 클릭하세요",
 	"Click here to learn more about faster-whisper and see the available models.": "빠른 속삭임에 대해 배우거나 가능한 모델을 보려면 여기를 클릭하세요",
+	"Click here to see available models.": "",
 	"Click here to select": "선택하려면 여기를 클릭하세요.",
 	"Click here to select a csv file.": "csv 파일을 선택하려면 여기를 클릭하세요.",
 	"Click here to select a py file.": "py 파일을 선택하려면 여기를 클릭하세요.",
@@ -290,6 +291,7 @@
 	"Documentation": "문서 조사",
 	"Documents": "문서",
 	"does not make any external connections, and your data stays securely on your locally hosted server.": "외부와 어떠한 연결도 하지 않으며, 데이터는 로컬에서 호스팅되는 서버에 안전하게 유지됩니다.",
+	"Domain Filter List": "",
 	"Don't have an account?": "계정이 없으신가요?",
 	"don't install random functions from sources you don't trust.": "불분명한 출처를 가진 임의의 함수를 설치하지마세요",
 	"don't install random tools from sources you don't trust.": "불분명한 출처를 가진 임의의 도구를 설치하지마세요",
@@ -349,6 +351,7 @@
 	"Enter Chunk Overlap": "청크 오버랩 입력",
 	"Enter Chunk Size": "청크 크기 입력",
 	"Enter description": "설명 입력",
+	"Enter domains separated by commas (e.g., example.com,site.org)": "",
 	"Enter Exa API Key": "",
 	"Enter Github Raw URL": "Github Raw URL 입력",
 	"Enter Google PSE API Key": "Google PSE API 키 입력",
@@ -552,6 +555,8 @@
 	"Knowledge deleted successfully.": "성공적으로 지식 기반이 삭제되었습니다",
 	"Knowledge reset successfully.": "성공적으로 지식 기반이 초기화되었습니다",
 	"Knowledge updated successfully": "성공적으로 지식 기반이 업데이트되었습니다",
+	"Kokoro.js (Browser)": "",
+	"Kokoro.js Dtype": "",
 	"Label": "",
 	"Landing Page Mode": "랜딩페이지 모드",
 	"Language": "언어",
@@ -566,6 +571,7 @@
 	"Leave empty to include all models from \"{{URL}}/models\" endpoint": "",
 	"Leave empty to include all models or select specific models": "특정 모델을 선택하거나 모든 모델을 포함하고 싶으면 빈칸으로 남겨두세요",
 	"Leave empty to use the default prompt, or enter a custom prompt": "기본 프롬프트를 사용하기 위해 빈칸으로 남겨두거나, 커스텀 프롬프트를 입력하세요",
+	"Leave model field empty to use the default model.": "",
 	"Light": "라이트",
 	"Listening...": "듣는 중...",
 	"Llama.cpp": "",
@@ -574,7 +580,7 @@
 	"Local Models": "로컬 모델",
 	"Lost": "패배",
 	"LTR": "LTR",
-	"Made by OpenWebUI Community": "OpenWebUI 커뮤니티에 의해 개발됨",
+	"Made by Open WebUI Community": "OpenWebUI 커뮤니티에 의해 개발됨",
 	"Make sure to enclose them with": "꼭 다음으로 감싸세요:",
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "꼭 workflow.json 파일을 ComfyUI의 API 형식대로 내보내세요",
 	"Manage": "관리",
@@ -754,7 +760,7 @@
 	"Read Aloud": "읽어주기",
 	"Reasoning Effort": "",
 	"Record voice": "음성 녹음",
-	"Redirecting you to OpenWebUI Community": "OpenWebUI 커뮤니티로 리디렉션 중",
+	"Redirecting you to Open WebUI Community": "OpenWebUI 커뮤니티로 리디렉션 중",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "",
 	"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "스스로를 \"사용자\" 라고 지칭하세요. (예: \"사용자는 영어를 배우고 있습니다\")",
 	"References from": "출처",
@@ -869,7 +875,7 @@
 	"Settings saved successfully!": "설정이 성공적으로 저장되었습니다!",
 	"Share": "공유",
 	"Share Chat": "채팅 공유",
-	"Share to OpenWebUI Community": "OpenWebUI 커뮤니티에 공유",
+	"Share to Open WebUI Community": "OpenWebUI 커뮤니티에 공유",
 	"Show": "보기",
 	"Show \"What's New\" modal on login": "로그인시 \"새로운 기능\" 모달 보기",
 	"Show Admin Details in Account Pending Overlay": "사용자용 계정 보류 설명창에, 관리자 상세 정보 노출",
@@ -944,6 +950,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "지식 기반과 모든 파일 연동을 초기화합니다. 계속 하시겠습니까?",
 	"Thorough explanation": "완전한 설명",
 	"Thought for {{DURATION}}": "{{DURATION}} 동안 생각함",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "티카(Tika)",
 	"Tika Server URL required.": "티카 서버 URL이 필요합니다",
 	"Tiktoken": "틱토큰 (Tiktoken)",

部分文件因文件數量過多而無法顯示