Browse Source

Merge pull request #2505 from open-webui/dev-models

feat: openai api compatible model presets (profiles/modelfiles)
Timothy Jaeryang Baek 11 tháng trước cách đây
mục cha
commit
67a5020cdc
88 tập tin đã thay đổi với 3354 bổ sung2503 xóa
  1. 11 2
      backend/apps/litellm/main.py
  2. 150 26
      backend/apps/ollama/main.py
  3. 89 30
      backend/apps/openai/main.py
  4. 12 0
      backend/apps/web/internal/db.py
  5. 61 0
      backend/apps/web/internal/migrations/009_add_models.py
  6. 130 0
      backend/apps/web/internal/migrations/010_migrate_modelfiles_to_models.py
  7. 5 3
      backend/apps/web/main.py
  8. 8 0
      backend/apps/web/models/modelfiles.py
  9. 179 0
      backend/apps/web/models/models.py
  10. 0 124
      backend/apps/web/routers/modelfiles.py
  11. 108 0
      backend/apps/web/routers/models.py
  12. 2 0
      backend/constants.py
  13. 104 6
      backend/main.py
  14. 74 0
      backend/utils/misc.py
  15. 10 0
      backend/utils/models.py
  16. 123 0
      src/lib/apis/index.ts
  17. 2 1
      src/lib/apis/litellm/index.ts
  18. 17 32
      src/lib/apis/models/index.ts
  19. 6 1
      src/lib/apis/openai/index.ts
  20. 104 106
      src/lib/components/chat/Chat.svelte
  21. 60 13
      src/lib/components/chat/MessageInput.svelte
  22. 2 8
      src/lib/components/chat/Messages.svelte
  23. 54 53
      src/lib/components/chat/Messages/CodeBlock.svelte
  24. 0 3
      src/lib/components/chat/Messages/CompareMessages.svelte
  25. 36 37
      src/lib/components/chat/Messages/Placeholder.svelte
  26. 8 10
      src/lib/components/chat/Messages/ResponseMessage.svelte
  27. 4 9
      src/lib/components/chat/Messages/UserMessage.svelte
  28. 2 0
      src/lib/components/chat/Messages/test.json
  29. 5 7
      src/lib/components/chat/ModelSelector.svelte
  30. 43 18
      src/lib/components/chat/ModelSelector/Selector.svelte
  31. 0 155
      src/lib/components/chat/Settings/Advanced.svelte
  32. 177 97
      src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
  33. 28 27
      src/lib/components/chat/Settings/General.svelte
  34. 37 287
      src/lib/components/chat/Settings/Models.svelte
  35. 1 1
      src/lib/components/chat/SettingsModal.svelte
  36. 2 4
      src/lib/components/chat/ShareChatModal.svelte
  37. 1 0
      src/lib/components/common/Checkbox.svelte
  38. 2 1
      src/lib/components/common/Tooltip.svelte
  39. 0 1
      src/lib/components/layout/Navbar.svelte
  40. 73 92
      src/lib/components/workspace/Models.svelte
  41. 5 7
      src/lib/components/workspace/Playground.svelte
  42. 1 1
      src/lib/constants.ts
  43. 14 0
      src/lib/i18n/locales/ar-BH/translation.json
  44. 14 0
      src/lib/i18n/locales/bg-BG/translation.json
  45. 14 0
      src/lib/i18n/locales/bn-BD/translation.json
  46. 14 0
      src/lib/i18n/locales/ca-ES/translation.json
  47. 14 0
      src/lib/i18n/locales/de-DE/translation.json
  48. 14 0
      src/lib/i18n/locales/dg-DG/translation.json
  49. 14 0
      src/lib/i18n/locales/en-GB/translation.json
  50. 14 0
      src/lib/i18n/locales/en-US/translation.json
  51. 14 0
      src/lib/i18n/locales/es-ES/translation.json
  52. 14 0
      src/lib/i18n/locales/fa-IR/translation.json
  53. 14 0
      src/lib/i18n/locales/fi-FI/translation.json
  54. 14 0
      src/lib/i18n/locales/fr-CA/translation.json
  55. 14 0
      src/lib/i18n/locales/fr-FR/translation.json
  56. 14 0
      src/lib/i18n/locales/he-IL/translation.json
  57. 14 0
      src/lib/i18n/locales/hi-IN/translation.json
  58. 14 0
      src/lib/i18n/locales/hr-HR/translation.json
  59. 14 0
      src/lib/i18n/locales/it-IT/translation.json
  60. 14 0
      src/lib/i18n/locales/ja-JP/translation.json
  61. 14 0
      src/lib/i18n/locales/ka-GE/translation.json
  62. 14 0
      src/lib/i18n/locales/ko-KR/translation.json
  63. 14 0
      src/lib/i18n/locales/nl-NL/translation.json
  64. 14 0
      src/lib/i18n/locales/pa-IN/translation.json
  65. 14 0
      src/lib/i18n/locales/pl-PL/translation.json
  66. 14 0
      src/lib/i18n/locales/pt-BR/translation.json
  67. 14 0
      src/lib/i18n/locales/pt-PT/translation.json
  68. 14 0
      src/lib/i18n/locales/ru-RU/translation.json
  69. 14 0
      src/lib/i18n/locales/sr-RS/translation.json
  70. 14 0
      src/lib/i18n/locales/sv-SE/translation.json
  71. 14 0
      src/lib/i18n/locales/tr-TR/translation.json
  72. 14 0
      src/lib/i18n/locales/uk-UA/translation.json
  73. 14 0
      src/lib/i18n/locales/vi-VN/translation.json
  74. 14 0
      src/lib/i18n/locales/zh-CN/translation.json
  75. 14 0
      src/lib/i18n/locales/zh-TW/translation.json
  76. 11 9
      src/lib/stores/index.ts
  77. 0 24
      src/lib/utils/index.ts
  78. 3 28
      src/routes/(app)/+layout.svelte
  79. 2 2
      src/routes/(app)/workspace/+layout.svelte
  80. 1 1
      src/routes/(app)/workspace/+page.svelte
  81. 0 5
      src/routes/(app)/workspace/modelfiles/+page.svelte
  82. 0 721
      src/routes/(app)/workspace/modelfiles/create/+page.svelte
  83. 0 507
      src/routes/(app)/workspace/modelfiles/edit/+page.svelte
  84. 5 0
      src/routes/(app)/workspace/models/+page.svelte
  85. 576 0
      src/routes/(app)/workspace/models/create/+page.svelte
  86. 555 0
      src/routes/(app)/workspace/models/edit/+page.svelte
  87. 0 27
      src/routes/modelfiles/create/+page.svelte
  88. 3 17
      src/routes/s/[id]/+page.svelte

+ 11 - 2
backend/apps/litellm/main.py

@@ -18,8 +18,9 @@ import requests
 from pydantic import BaseModel, ConfigDict
 from typing import Optional, List
 
+from apps.web.models.models import Models
 from utils.utils import get_verified_user, get_current_user, get_admin_user
-from config import SRC_LOG_LEVELS, ENV
+from config import SRC_LOG_LEVELS
 from constants import MESSAGES
 
 import os
@@ -77,7 +78,7 @@ with open(LITELLM_CONFIG_DIR, "r") as file:
 
 app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
 app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
-
+app.state.MODEL_CONFIG = Models.get_all_models()
 
 app.state.ENABLE = ENABLE_LITELLM
 app.state.CONFIG = litellm_config
@@ -261,6 +262,14 @@ async def get_models(user=Depends(get_current_user)):
                         "object": "model",
                         "created": int(time.time()),
                         "owned_by": "openai",
+                        "custom_info": next(
+                            (
+                                item
+                                for item in app.state.MODEL_CONFIG
+                                if item.id == model["model_name"]
+                            ),
+                            None,
+                        ),
                     }
                     for model in app.state.CONFIG["model_list"]
                 ],

+ 150 - 26
backend/apps/ollama/main.py

@@ -29,7 +29,7 @@ import time
 from urllib.parse import urlparse
 from typing import Optional, List, Union
 
-
+from apps.web.models.models import Models
 from apps.web.models.users import Users
 from constants import ERROR_MESSAGES
 from utils.utils import (
@@ -39,6 +39,8 @@ from utils.utils import (
     get_admin_user,
 )
 
+from utils.models import get_model_id_from_custom_model_id
+
 
 from config import (
     SRC_LOG_LEVELS,
@@ -68,7 +70,6 @@ app.state.config = AppConfig()
 app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
 app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
-
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.MODELS = {}
@@ -875,14 +876,93 @@ async def generate_chat_completion(
     user=Depends(get_verified_user),
 ):
 
-    if url_idx == None:
-        model = form_data.model
+    log.debug(
+        "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
+            form_data.model_dump_json(exclude_none=True).encode()
+        )
+    )
 
-        if ":" not in model:
-            model = f"{model}:latest"
+    payload = {
+        **form_data.model_dump(exclude_none=True),
+    }
 
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
+    model_id = form_data.model
+    model_info = Models.get_model_by_id(model_id)
+
+    if model_info:
+        print(model_info)
+        if model_info.base_model_id:
+            payload["model"] = model_info.base_model_id
+
+        model_info.params = model_info.params.model_dump()
+
+        if model_info.params:
+            payload["options"] = {}
+
+            payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
+            payload["options"]["mirostat_eta"] = model_info.params.get(
+                "mirostat_eta", None
+            )
+            payload["options"]["mirostat_tau"] = model_info.params.get(
+                "mirostat_tau", None
+            )
+            payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
+
+            payload["options"]["repeat_last_n"] = model_info.params.get(
+                "repeat_last_n", None
+            )
+            payload["options"]["repeat_penalty"] = model_info.params.get(
+                "frequency_penalty", None
+            )
+
+            payload["options"]["temperature"] = model_info.params.get(
+                "temperature", None
+            )
+            payload["options"]["seed"] = model_info.params.get("seed", None)
+
+            payload["options"]["stop"] = (
+                [
+                    bytes(stop, "utf-8").decode("unicode_escape")
+                    for stop in model_info.params["stop"]
+                ]
+                if model_info.params.get("stop", None)
+                else None
+            )
+
+            payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
+
+            payload["options"]["num_predict"] = model_info.params.get(
+                "max_tokens", None
+            )
+            payload["options"]["top_k"] = model_info.params.get("top_k", None)
+
+            payload["options"]["top_p"] = model_info.params.get("top_p", None)
+
+        if model_info.params.get("system", None):
+            # Check if the payload already has a system message
+            # If not, add a system message to the payload
+            if payload.get("messages"):
+                for message in payload["messages"]:
+                    if message.get("role") == "system":
+                        message["content"] = (
+                            model_info.params.get("system", None) + message["content"]
+                        )
+                        break
+                else:
+                    payload["messages"].insert(
+                        0,
+                        {
+                            "role": "system",
+                            "content": model_info.params.get("system", None),
+                        },
+                    )
+
+    if url_idx == None:
+        if ":" not in payload["model"]:
+            payload["model"] = f"{payload['model']}:latest"
+
+        if payload["model"] in app.state.MODELS:
+            url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
         else:
             raise HTTPException(
                 status_code=400,
@@ -892,16 +972,12 @@ async def generate_chat_completion(
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     log.info(f"url: {url}")
 
-    r = None
+    print(payload)
 
-    log.debug(
-        "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
-            form_data.model_dump_json(exclude_none=True).encode()
-        )
-    )
+    r = None
 
     def get_request():
-        nonlocal form_data
+        nonlocal payload
         nonlocal r
 
         request_id = str(uuid.uuid4())
@@ -910,7 +986,7 @@ async def generate_chat_completion(
 
             def stream_content():
                 try:
-                    if form_data.stream:
+                    if payload.get("stream", None):
                         yield json.dumps({"id": request_id, "done": False}) + "\n"
 
                     for chunk in r.iter_content(chunk_size=8192):
@@ -928,7 +1004,7 @@ async def generate_chat_completion(
             r = requests.request(
                 method="POST",
                 url=f"{url}/api/chat",
-                data=form_data.model_dump_json(exclude_none=True).encode(),
+                data=json.dumps(payload),
                 stream=True,
             )
 
@@ -984,14 +1060,62 @@ async def generate_openai_chat_completion(
     user=Depends(get_verified_user),
 ):
 
-    if url_idx == None:
-        model = form_data.model
+    payload = {
+        **form_data.model_dump(exclude_none=True),
+    }
 
-        if ":" not in model:
-            model = f"{model}:latest"
+    model_id = form_data.model
+    model_info = Models.get_model_by_id(model_id)
 
-        if model in app.state.MODELS:
-            url_idx = random.choice(app.state.MODELS[model]["urls"])
+    if model_info:
+        print(model_info)
+        if model_info.base_model_id:
+            payload["model"] = model_info.base_model_id
+
+        model_info.params = model_info.params.model_dump()
+
+        if model_info.params:
+            payload["temperature"] = model_info.params.get("temperature", None)
+            payload["top_p"] = model_info.params.get("top_p", None)
+            payload["max_tokens"] = model_info.params.get("max_tokens", None)
+            payload["frequency_penalty"] = model_info.params.get(
+                "frequency_penalty", None
+            )
+            payload["seed"] = model_info.params.get("seed", None)
+            payload["stop"] = (
+                [
+                    bytes(stop, "utf-8").decode("unicode_escape")
+                    for stop in model_info.params["stop"]
+                ]
+                if model_info.params.get("stop", None)
+                else None
+            )
+
+        if model_info.params.get("system", None):
+            # Check if the payload already has a system message
+            # If not, add a system message to the payload
+            if payload.get("messages"):
+                for message in payload["messages"]:
+                    if message.get("role") == "system":
+                        message["content"] = (
+                            model_info.params.get("system", None) + message["content"]
+                        )
+                        break
+                else:
+                    payload["messages"].insert(
+                        0,
+                        {
+                            "role": "system",
+                            "content": model_info.params.get("system", None),
+                        },
+                    )
+
+    if url_idx == None:
+        if ":" not in payload["model"]:
+            payload["model"] = f"{payload['model']}:latest"
+
+        if payload["model"] in app.state.MODELS:
+            url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
         else:
             raise HTTPException(
                 status_code=400,
@@ -1004,7 +1128,7 @@ async def generate_openai_chat_completion(
     r = None
 
     def get_request():
-        nonlocal form_data
+        nonlocal payload
         nonlocal r
 
         request_id = str(uuid.uuid4())
@@ -1013,7 +1137,7 @@ async def generate_openai_chat_completion(
 
             def stream_content():
                 try:
-                    if form_data.stream:
+                    if payload.get("stream"):
                         yield json.dumps(
                             {"request_id": request_id, "done": False}
                         ) + "\n"
@@ -1033,7 +1157,7 @@ async def generate_openai_chat_completion(
             r = requests.request(
                 method="POST",
                 url=f"{url}/v1/chat/completions",
-                data=form_data.model_dump_json(exclude_none=True).encode(),
+                data=json.dumps(payload),
                 stream=True,
             )
 

+ 89 - 30
backend/apps/openai/main.py

@@ -10,7 +10,7 @@ import logging
 
 from pydantic import BaseModel
 
-
+from apps.web.models.models import Models
 from apps.web.models.users import Users
 from constants import ERROR_MESSAGES
 from utils.utils import (
@@ -53,7 +53,6 @@ app.state.config = AppConfig()
 app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
 app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
-
 app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
 app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
 app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
@@ -206,7 +205,13 @@ def merge_models_lists(model_lists):
         if models is not None and "error" not in models:
             merged_list.extend(
                 [
-                    {**model, "urlIdx": idx}
+                    {
+                        **model,
+                        "name": model.get("name", model["id"]),
+                        "owned_by": "openai",
+                        "openai": model,
+                        "urlIdx": idx,
+                    }
                     for model in models
                     if "api.openai.com"
                     not in app.state.config.OPENAI_API_BASE_URLS[idx]
@@ -252,7 +257,7 @@ async def get_all_models():
         log.info(f"models: {models}")
         app.state.MODELS = {model["id"]: model for model in models["data"]}
 
-        return models
+    return models
 
 
 @app.get("/models")
@@ -310,39 +315,93 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
     body = await request.body()
     # TODO: Remove below after gpt-4-vision fix from Open AI
     # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
+
+    payload = None
+
     try:
-        body = body.decode("utf-8")
-        body = json.loads(body)
+        if "chat/completions" in path:
+            body = body.decode("utf-8")
+            body = json.loads(body)
 
-        model = app.state.MODELS[body.get("model")]
+            payload = {**body}
 
-        idx = model["urlIdx"]
+            model_id = body.get("model")
+            model_info = Models.get_model_by_id(model_id)
 
-        if "pipeline" in model and model.get("pipeline"):
-            body["user"] = {"name": user.name, "id": user.id}
-            body["title"] = (
-                True if body["stream"] == False and body["max_tokens"] == 50 else False
-            )
+            if model_info:
+                print(model_info)
+                if model_info.base_model_id:
+                    payload["model"] = model_info.base_model_id
+
+                model_info.params = model_info.params.model_dump()
+
+                if model_info.params:
+                    payload["temperature"] = model_info.params.get("temperature", None)
+                    payload["top_p"] = model_info.params.get("top_p", None)
+                    payload["max_tokens"] = model_info.params.get("max_tokens", None)
+                    payload["frequency_penalty"] = model_info.params.get(
+                        "frequency_penalty", None
+                    )
+                    payload["seed"] = model_info.params.get("seed", None)
+                    payload["stop"] = (
+                        [
+                            bytes(stop, "utf-8").decode("unicode_escape")
+                            for stop in model_info.params["stop"]
+                        ]
+                        if model_info.params.get("stop", None)
+                        else None
+                    )
+
+                if model_info.params.get("system", None):
+                    # Check if the payload already has a system message
+                    # If not, add a system message to the payload
+                    if payload.get("messages"):
+                        for message in payload["messages"]:
+                            if message.get("role") == "system":
+                                message["content"] = (
+                                    model_info.params.get("system", None)
+                                    + message["content"]
+                                )
+                                break
+                        else:
+                            payload["messages"].insert(
+                                0,
+                                {
+                                    "role": "system",
+                                    "content": model_info.params.get("system", None),
+                                },
+                            )
+            else:
+                pass
+
+            print(app.state.MODELS)
+            model = app.state.MODELS[payload.get("model")]
+
+            idx = model["urlIdx"]
+
+            if "pipeline" in model and model.get("pipeline"):
+                payload["user"] = {"name": user.name, "id": user.id}
+                payload["title"] = (
+                    True
+                    if payload["stream"] == False and payload["max_tokens"] == 50
+                    else False
+                )
+
+            # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
+            # This is a workaround until OpenAI fixes the issue with this model
+            if payload.get("model") == "gpt-4-vision-preview":
+                if "max_tokens" not in payload:
+                    payload["max_tokens"] = 4000
+                log.debug("Modified payload:", payload)
+
+            # Convert the modified body back to JSON
+            payload = json.dumps(payload)
 
-        # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
-        # This is a workaround until OpenAI fixes the issue with this model
-        if body.get("model") == "gpt-4-vision-preview":
-            if "max_tokens" not in body:
-                body["max_tokens"] = 4000
-            log.debug("Modified body_dict:", body)
-
-        # Fix for ChatGPT calls failing because the num_ctx key is in body
-        if "num_ctx" in body:
-            # If 'num_ctx' is in the dictionary, delete it
-            # Leaving it there generates an error with the
-            # OpenAI API (Feb 2024)
-            del body["num_ctx"]
-
-        # Convert the modified body back to JSON
-        body = json.dumps(body)
     except json.JSONDecodeError as e:
         log.error("Error loading request body into a dictionary:", e)
 
+    print(payload)
+
     url = app.state.config.OPENAI_API_BASE_URLS[idx]
     key = app.state.config.OPENAI_API_KEYS[idx]
 
@@ -361,7 +420,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
         r = requests.request(
             method=request.method,
             url=target_url,
-            data=body,
+            data=payload if payload else body,
             headers=headers,
             stream=True,
         )

+ 12 - 0
backend/apps/web/internal/db.py

@@ -1,3 +1,5 @@
+import json
+
 from peewee import *
 from peewee_migrate import Router
 from playhouse.db_url import connect
@@ -8,6 +10,16 @@ import logging
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["DB"])
 
+
+class JSONField(TextField):
+    def db_value(self, value):
+        return json.dumps(value)
+
+    def python_value(self, value):
+        if value is not None:
+            return json.loads(value)
+
+
 # Check if the file exists
 if os.path.exists(f"{DATA_DIR}/ollama.db"):
     # Rename the file

+ 61 - 0
backend/apps/web/internal/migrations/009_add_models.py

@@ -0,0 +1,61 @@
+"""Peewee migrations -- 009_add_models.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    @migrator.create_model
+    class Model(pw.Model):
+        id = pw.TextField(unique=True)
+        user_id = pw.TextField()
+        base_model_id = pw.TextField(null=True)
+
+        name = pw.TextField()
+
+        meta = pw.TextField()
+        params = pw.TextField()
+
+        created_at = pw.BigIntegerField(null=False)
+        updated_at = pw.BigIntegerField(null=False)
+
+        class Meta:
+            table_name = "model"
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    migrator.remove_model("model")

+ 130 - 0
backend/apps/web/internal/migrations/010_migrate_modelfiles_to_models.py

@@ -0,0 +1,130 @@
+"""Peewee migrations -- 009_add_models.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+import json
+
+from utils.misc import parse_ollama_modelfile
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    # Fetch data from 'modelfile' table and insert into 'model' table
+    migrate_modelfile_to_model(migrator, database)
+    # Drop the 'modelfile' table
+    migrator.remove_model("modelfile")
+
+
+def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
+    ModelFile = migrator.orm["modelfile"]
+    Model = migrator.orm["model"]
+
+    modelfiles = ModelFile.select()
+
+    for modelfile in modelfiles:
+        # Extract and transform data in Python
+
+        modelfile.modelfile = json.loads(modelfile.modelfile)
+        meta = json.dumps(
+            {
+                "description": modelfile.modelfile.get("desc"),
+                "profile_image_url": modelfile.modelfile.get("imageUrl"),
+                "ollama": {"modelfile": modelfile.modelfile.get("content")},
+                "suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
+                "categories": modelfile.modelfile.get("categories"),
+                "user": {**modelfile.modelfile.get("user", {}), "community": True},
+            }
+        )
+
+        info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
+
+        # Insert the processed data into the 'model' table
+        Model.create(
+            id=f"ollama-{modelfile.tag_name}",
+            user_id=modelfile.user_id,
+            base_model_id=info.get("base_model_id"),
+            name=modelfile.modelfile.get("title"),
+            meta=meta,
+            params=json.dumps(info.get("params", {})),
+            created_at=modelfile.timestamp,
+            updated_at=modelfile.timestamp,
+        )
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    recreate_modelfile_table(migrator, database)
+    move_data_back_to_modelfile(migrator, database)
+    migrator.remove_model("model")
+
+
+def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
+    query = """
+    CREATE TABLE IF NOT EXISTS modelfile (
+        user_id TEXT,
+        tag_name TEXT,
+        modelfile JSON,
+        timestamp BIGINT
+    )
+    """
+    migrator.sql(query)
+
+
+def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
+    Model = migrator.orm["model"]
+    Modelfile = migrator.orm["modelfile"]
+
+    models = Model.select()
+
+    for model in models:
+        # Extract and transform data in Python
+        meta = json.loads(model.meta)
+
+        modelfile_data = {
+            "title": model.name,
+            "desc": meta.get("description"),
+            "imageUrl": meta.get("profile_image_url"),
+            "content": meta.get("ollama", {}).get("modelfile"),
+            "suggestionPrompts": meta.get("suggestion_prompts"),
+            "categories": meta.get("categories"),
+            "user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
+        }
+
+        # Insert the processed data back into the 'modelfile' table
+        Modelfile.create(
+            user_id=model.user_id,
+            tag_name=model.id,
+            modelfile=modelfile_data,
+            timestamp=model.created_at,
+        )

+ 5 - 3
backend/apps/web/main.py

@@ -6,7 +6,7 @@ from apps.web.routers import (
     users,
     chats,
     documents,
-    modelfiles,
+    models,
     prompts,
     configs,
     memories,
@@ -40,6 +40,9 @@ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
 app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
 app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
+
+
+app.state.MODELS = {}
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 
 
@@ -56,11 +59,10 @@ app.include_router(users.router, prefix="/users", tags=["users"])
 app.include_router(chats.router, prefix="/chats", tags=["chats"])
 
 app.include_router(documents.router, prefix="/documents", tags=["documents"])
-app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
+app.include_router(models.router, prefix="/models", tags=["models"])
 app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
 app.include_router(memories.router, prefix="/memories", tags=["memories"])
 
-
 app.include_router(configs.router, prefix="/configs", tags=["configs"])
 app.include_router(utils.router, prefix="/utils", tags=["utils"])
 

+ 8 - 0
backend/apps/web/models/modelfiles.py

@@ -1,3 +1,11 @@
+################################################################################
+#                              DEPRECATION NOTICE                              #
+#                                                                              #
+# This file has been deprecated since version 0.2.0.                           #
+#                                                                              #
+################################################################################
+
+
 from pydantic import BaseModel
 from peewee import *
 from playhouse.shortcuts import model_to_dict

+ 179 - 0
backend/apps/web/models/models.py

@@ -0,0 +1,179 @@
+import json
+import logging
+from typing import Optional
+
+import peewee as pw
+from peewee import *
+
+from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict
+
+from apps.web.internal.db import DB, JSONField
+
+from typing import List, Union, Optional
+from config import SRC_LOG_LEVELS
+
+import time
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+
+####################
+# Models DB Schema
+####################
+
+
+# ModelParams is a model for the data stored in the params field of the Model table
+class ModelParams(BaseModel):
+    model_config = ConfigDict(extra="allow")
+    pass
+
+
+# ModelMeta is a model for the data stored in the meta field of the Model table
+class ModelMeta(BaseModel):
+    profile_image_url: Optional[str] = "/favicon.png"
+
+    description: Optional[str] = None
+    """
+        User-facing description of the model.
+    """
+
+    capabilities: Optional[dict] = None
+
+    model_config = ConfigDict(extra="allow")
+
+    pass
+
+
+class Model(pw.Model):
+    id = pw.TextField(unique=True)
+    """
+        The model's id as used in the API. If set to an existing model, it will override the model.
+    """
+    user_id = pw.TextField()
+
+    base_model_id = pw.TextField(null=True)
+    """
+        An optional pointer to the actual model that should be used when proxying requests.
+    """
+
+    name = pw.TextField()
+    """
+        The human-readable display name of the model.
+    """
+
+    params = JSONField()
+    """
+        Holds a JSON encoded blob of parameters, see `ModelParams`.
+    """
+
+    meta = JSONField()
+    """
+        Holds a JSON encoded blob of metadata, see `ModelMeta`.
+    """
+
+    updated_at = BigIntegerField()
+    created_at = BigIntegerField()
+
+    class Meta:
+        database = DB
+
+
+class ModelModel(BaseModel):
+    id: str
+    user_id: str
+    base_model_id: Optional[str] = None
+
+    name: str
+    params: ModelParams
+    meta: ModelMeta
+
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class ModelResponse(BaseModel):
+    id: str
+    name: str
+    meta: ModelMeta
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
+
+class ModelForm(BaseModel):
+    id: str
+    base_model_id: Optional[str] = None
+    name: str
+    meta: ModelMeta
+    params: ModelParams
+
+
+class ModelsTable:
+    def __init__(
+        self,
+        db: pw.SqliteDatabase | pw.PostgresqlDatabase,
+    ):
+        self.db = db
+        self.db.create_tables([Model])
+
+    def insert_new_model(
+        self, form_data: ModelForm, user_id: str
+    ) -> Optional[ModelModel]:
+        model = ModelModel(
+            **{
+                **form_data.model_dump(),
+                "user_id": user_id,
+                "created_at": int(time.time()),
+                "updated_at": int(time.time()),
+            }
+        )
+        try:
+            result = Model.create(**model.model_dump())
+
+            if result:
+                return model
+            else:
+                return None
+        except Exception as e:
+            print(e)
+            return None
+
+    def get_all_models(self) -> List[ModelModel]:
+        return [ModelModel(**model_to_dict(model)) for model in Model.select()]
+
+    def get_model_by_id(self, id: str) -> Optional[ModelModel]:
+        try:
+            model = Model.get(Model.id == id)
+            return ModelModel(**model_to_dict(model))
+        except:
+            return None
+
+    def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
+        try:
+            # update only the fields that are present in the model
+            query = Model.update(**model.model_dump()).where(Model.id == id)
+            query.execute()
+
+            model = Model.get(Model.id == id)
+            return ModelModel(**model_to_dict(model))
+        except Exception as e:
+            print(e)
+
+            return None
+
+    def delete_model_by_id(self, id: str) -> bool:
+        try:
+            query = Model.delete().where(Model.id == id)
+            query.execute()
+            return True
+        except:
+            return False
+
+
+Models = ModelsTable(DB)

+ 0 - 124
backend/apps/web/routers/modelfiles.py

@@ -1,124 +0,0 @@
-from fastapi import Depends, FastAPI, HTTPException, status
-from datetime import datetime, timedelta
-from typing import List, Union, Optional
-
-from fastapi import APIRouter
-from pydantic import BaseModel
-import json
-from apps.web.models.modelfiles import (
-    Modelfiles,
-    ModelfileForm,
-    ModelfileTagNameForm,
-    ModelfileUpdateForm,
-    ModelfileResponse,
-)
-
-from utils.utils import get_current_user, get_admin_user
-from constants import ERROR_MESSAGES
-
-router = APIRouter()
-
-############################
-# GetModelfiles
-############################
-
-
-@router.get("/", response_model=List[ModelfileResponse])
-async def get_modelfiles(
-    skip: int = 0, limit: int = 50, user=Depends(get_current_user)
-):
-    return Modelfiles.get_modelfiles(skip, limit)
-
-
-############################
-# CreateNewModelfile
-############################
-
-
-@router.post("/create", response_model=Optional[ModelfileResponse])
-async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
-    modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
-
-    if modelfile:
-        return ModelfileResponse(
-            **{
-                **modelfile.model_dump(),
-                "modelfile": json.loads(modelfile.modelfile),
-            }
-        )
-    else:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.DEFAULT(),
-        )
-
-
-############################
-# GetModelfileByTagName
-############################
-
-
-@router.post("/", response_model=Optional[ModelfileResponse])
-async def get_modelfile_by_tag_name(
-    form_data: ModelfileTagNameForm, user=Depends(get_current_user)
-):
-    modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
-
-    if modelfile:
-        return ModelfileResponse(
-            **{
-                **modelfile.model_dump(),
-                "modelfile": json.loads(modelfile.modelfile),
-            }
-        )
-    else:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.NOT_FOUND,
-        )
-
-
-############################
-# UpdateModelfileByTagName
-############################
-
-
-@router.post("/update", response_model=Optional[ModelfileResponse])
-async def update_modelfile_by_tag_name(
-    form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
-):
-    modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
-    if modelfile:
-        updated_modelfile = {
-            **json.loads(modelfile.modelfile),
-            **form_data.modelfile,
-        }
-
-        modelfile = Modelfiles.update_modelfile_by_tag_name(
-            form_data.tag_name, updated_modelfile
-        )
-
-        return ModelfileResponse(
-            **{
-                **modelfile.model_dump(),
-                "modelfile": json.loads(modelfile.modelfile),
-            }
-        )
-    else:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
-        )
-
-
-############################
-# DeleteModelfileByTagName
-############################
-
-
-@router.delete("/delete", response_model=bool)
-async def delete_modelfile_by_tag_name(
-    form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
-):
-    result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
-    return result

+ 108 - 0
backend/apps/web/routers/models.py

@@ -0,0 +1,108 @@
+from fastapi import Depends, FastAPI, HTTPException, status, Request
+from datetime import datetime, timedelta
+from typing import List, Union, Optional
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+import json
+from apps.web.models.models import Models, ModelModel, ModelForm, ModelResponse
+
+from utils.utils import get_verified_user, get_admin_user
+from constants import ERROR_MESSAGES
+
+router = APIRouter()
+
+###########################
+# getModels
+###########################
+
+
+@router.get("/", response_model=List[ModelResponse])
+async def get_models(user=Depends(get_verified_user)):
+    return Models.get_all_models()
+
+
+############################
+# AddNewModel
+############################
+
+
+@router.post("/add", response_model=Optional[ModelModel])
+async def add_new_model(
+    request: Request, form_data: ModelForm, user=Depends(get_admin_user)
+):
+    if form_data.id in request.app.state.MODELS:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
+        )
+    else:
+        model = Models.insert_new_model(form_data, user.id)
+
+        if model:
+            return model
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                detail=ERROR_MESSAGES.DEFAULT(),
+            )
+
+
+############################
+# GetModelById
+############################
+
+
+@router.get("/{id}", response_model=Optional[ModelModel])
+async def get_model_by_id(id: str, user=Depends(get_verified_user)):
+    model = Models.get_model_by_id(id)
+
+    if model:
+        return model
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.NOT_FOUND,
+        )
+
+
+############################
+# UpdateModelById
+############################
+
+
+@router.post("/{id}/update", response_model=Optional[ModelModel])
+async def update_model_by_id(
+    request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
+):
+    model = Models.get_model_by_id(id)
+    if model:
+        model = Models.update_model_by_id(id, form_data)
+        return model
+    else:
+        if form_data.id in request.app.state.MODELS:
+            model = Models.insert_new_model(form_data, user.id)
+            print(model)
+            if model:
+                return model
+            else:
+                raise HTTPException(
+                    status_code=status.HTTP_401_UNAUTHORIZED,
+                    detail=ERROR_MESSAGES.DEFAULT(),
+                )
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                detail=ERROR_MESSAGES.DEFAULT(),
+            )
+
+
+############################
+# DeleteModelById
+############################
+
+
+@router.delete("/{id}/delete", response_model=bool)
+async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
+    result = Models.delete_model_by_id(id)
+    return result

+ 2 - 0
backend/constants.py

@@ -32,6 +32,8 @@ class ERROR_MESSAGES(str, Enum):
     COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
     FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
 
+    MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
+
     NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
     INVALID_TOKEN = (
         "Your session has expired or the token is invalid. Please sign in again."

+ 104 - 6
backend/main.py

@@ -19,8 +19,8 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
 from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.responses import StreamingResponse, Response
 
-from apps.ollama.main import app as ollama_app
-from apps.openai.main import app as openai_app
+from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models
+from apps.openai.main import app as openai_app, get_all_models as get_openai_models
 
 from apps.litellm.main import (
     app as litellm_app,
@@ -36,10 +36,10 @@ from apps.web.main import app as webui_app
 
 import asyncio
 from pydantic import BaseModel
-from typing import List
+from typing import List, Optional
 
-
-from utils.utils import get_admin_user
+from apps.web.models.models import Models, ModelModel
+from utils.utils import get_admin_user, get_verified_user
 from apps.rag.utils import rag_messages
 
 from config import (
@@ -53,6 +53,8 @@ from config import (
     FRONTEND_BUILD_DIR,
     CACHE_DIR,
     STATIC_DIR,
+    ENABLE_OPENAI_API,
+    ENABLE_OLLAMA_API,
     ENABLE_LITELLM,
     ENABLE_MODEL_FILTER,
     MODEL_FILTER_LIST,
@@ -110,11 +112,19 @@ app = FastAPI(
 )
 
 app.state.config = AppConfig()
+
+app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
+app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
+
 app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
 app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
+
 app.state.config.WEBHOOK_URL = WEBHOOK_URL
 
+
+app.state.MODELS = {}
+
 origins = ["*"]
 
 
@@ -231,6 +241,11 @@ app.add_middleware(
 
 @app.middleware("http")
 async def check_url(request: Request, call_next):
+    if len(app.state.MODELS) == 0:
+        await get_all_models()
+    else:
+        pass
+
     start_time = int(time.time())
     response = await call_next(request)
     process_time = int(time.time()) - start_time
@@ -247,9 +262,11 @@ async def update_embedding_function(request: Request, call_next):
     return response
 
 
+# TODO: Deprecate LiteLLM
 app.mount("/litellm/api", litellm_app)
+
 app.mount("/ollama", ollama_app)
-app.mount("/openai/api", openai_app)
+app.mount("/openai", openai_app)
 
 app.mount("/images/api/v1", images_app)
 app.mount("/audio/api/v1", audio_app)
@@ -260,6 +277,87 @@ app.mount("/api/v1", webui_app)
 webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
 
 
+async def get_all_models():
+    openai_models = []
+    ollama_models = []
+
+    if app.state.config.ENABLE_OPENAI_API:
+        openai_models = await get_openai_models()
+
+        openai_models = openai_models["data"]
+
+    if app.state.config.ENABLE_OLLAMA_API:
+        ollama_models = await get_ollama_models()
+
+        ollama_models = [
+            {
+                "id": model["model"],
+                "name": model["name"],
+                "object": "model",
+                "created": int(time.time()),
+                "owned_by": "ollama",
+                "ollama": model,
+            }
+            for model in ollama_models["models"]
+        ]
+
+    models = openai_models + ollama_models
+    custom_models = Models.get_all_models()
+
+    for custom_model in custom_models:
+        if custom_model.base_model_id == None:
+            for model in models:
+                if (
+                    custom_model.id == model["id"]
+                    or custom_model.id == model["id"].split(":")[0]
+                ):
+                    model["name"] = custom_model.name
+                    model["info"] = custom_model.model_dump()
+        else:
+            owned_by = "openai"
+            for model in models:
+                if (
+                    custom_model.base_model_id == model["id"]
+                    or custom_model.base_model_id == model["id"].split(":")[0]
+                ):
+                    owned_by = model["owned_by"]
+                    break
+
+            models.append(
+                {
+                    "id": custom_model.id,
+                    "name": custom_model.name,
+                    "object": "model",
+                    "created": custom_model.created_at,
+                    "owned_by": owned_by,
+                    "info": custom_model.model_dump(),
+                    "preset": True,
+                }
+            )
+
+    app.state.MODELS = {model["id"]: model for model in models}
+
+    webui_app.state.MODELS = app.state.MODELS
+
+    return models
+
+
+@app.get("/api/models")
+async def get_models(user=Depends(get_verified_user)):
+    models = await get_all_models()
+    if app.state.config.ENABLE_MODEL_FILTER:
+        if user.role == "user":
+            models = list(
+                filter(
+                    lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
+                    models,
+                )
+            )
+            return {"data": models}
+
+    return {"data": models}
+
+
 @app.get("/api/config")
 async def get_app_config():
     # Checking and Handling the Absence of 'ui' in CONFIG_DATA

+ 74 - 0
backend/utils/misc.py

@@ -1,5 +1,6 @@
 from pathlib import Path
 import hashlib
+import json
 import re
 from datetime import timedelta
 from typing import Optional
@@ -110,3 +111,76 @@ def parse_duration(duration: str) -> Optional[timedelta]:
             total_duration += timedelta(weeks=number)
 
     return total_duration
+
+
+def parse_ollama_modelfile(model_text):
+    parameters_meta = {
+        "mirostat": int,
+        "mirostat_eta": float,
+        "mirostat_tau": float,
+        "num_ctx": int,
+        "repeat_last_n": int,
+        "repeat_penalty": float,
+        "temperature": float,
+        "seed": int,
+        "stop": str,
+        "tfs_z": float,
+        "num_predict": int,
+        "top_k": int,
+        "top_p": float,
+    }
+
+    data = {"base_model_id": None, "params": {}}
+
+    # Parse base model
+    base_model_match = re.search(
+        r"^FROM\s+(\w+)", model_text, re.MULTILINE | re.IGNORECASE
+    )
+    if base_model_match:
+        data["base_model_id"] = base_model_match.group(1)
+
+    # Parse template
+    template_match = re.search(
+        r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
+    )
+    if template_match:
+        data["params"] = {"template": template_match.group(1).strip()}
+
+    # Parse stops
+    stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE)
+    if stops:
+        data["params"]["stop"] = stops
+
+    # Parse other parameters from the provided list
+    for param, param_type in parameters_meta.items():
+        param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
+        if param_match:
+            value = param_match.group(1)
+            if param_type == int:
+                value = int(value)
+            elif param_type == float:
+                value = float(value)
+            data["params"][param] = value
+
+    # Parse adapter
+    adapter_match = re.search(r"ADAPTER (.+)", model_text, re.IGNORECASE)
+    if adapter_match:
+        data["params"]["adapter"] = adapter_match.group(1)
+
+    # Parse system description
+    system_desc_match = re.search(
+        r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
+    )
+    if system_desc_match:
+        data["params"]["system"] = system_desc_match.group(1).strip()
+
+    # Parse messages
+    messages = []
+    message_matches = re.findall(r"MESSAGE (\w+) (.+)", model_text, re.IGNORECASE)
+    for role, content in message_matches:
+        messages.append({"role": role, "content": content})
+
+    if messages:
+        data["params"]["messages"] = messages
+
+    return data

+ 10 - 0
backend/utils/models.py

@@ -0,0 +1,10 @@
+from apps.web.models.models import Models, ModelModel, ModelForm, ModelResponse
+
+
+def get_model_id_from_custom_model_id(id: str):
+    model = Models.get_model_by_id(id)
+
+    if model:
+        return model.id
+    else:
+        return id

+ 123 - 0
src/lib/apis/index.ts

@@ -1,5 +1,54 @@
 import { WEBUI_BASE_URL } from '$lib/constants';
 
+export const getModels = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/models`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	let models = res?.data ?? [];
+
+	models = models
+		.filter((models) => models)
+		.sort((a, b) => {
+			// Compare case-insensitively
+			const lowerA = a.name.toLowerCase();
+			const lowerB = b.name.toLowerCase();
+
+			if (lowerA < lowerB) return -1;
+			if (lowerA > lowerB) return 1;
+
+			// If same case-insensitively, sort by original strings,
+			// lowercase will come before uppercase due to ASCII values
+			if (a < b) return -1;
+			if (a > b) return 1;
+
+			return 0; // They are equal
+		});
+
+	console.log(models);
+	return models;
+};
+
 export const getBackendConfig = async () => {
 	let error = null;
 
@@ -196,3 +245,77 @@ export const updateWebhookUrl = async (token: string, url: string) => {
 
 	return res.url;
 };
+
+export const getModelConfig = async (token: string): Promise<GlobalModelConfig> => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, {
+		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;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res.models;
+};
+
+export interface ModelConfig {
+	id: string;
+	name: string;
+	meta: ModelMeta;
+	base_model_id?: string;
+	params: ModelParams;
+}
+
+export interface ModelMeta {
+	description?: string;
+	capabilities?: object;
+}
+
+export interface ModelParams {}
+
+export type GlobalModelConfig = ModelConfig[];
+
+export const updateModelConfig = async (token: string, config: GlobalModelConfig) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			models: config
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 2 - 1
src/lib/apis/litellm/index.ts

@@ -33,7 +33,8 @@ export const getLiteLLMModels = async (token: string = '') => {
 					id: model.id,
 					name: model.name ?? model.id,
 					external: true,
-					source: 'LiteLLM'
+					source: 'LiteLLM',
+					custom_info: model.custom_info
 				}))
 				.sort((a, b) => {
 					return a.name.localeCompare(b.name);

+ 17 - 32
src/lib/apis/modelfiles/index.ts → src/lib/apis/models/index.ts

@@ -1,18 +1,16 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
-export const createNewModelfile = async (token: string, modelfile: object) => {
+export const addNewModel = async (token: string, model: object) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/create`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/add`, {
 		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
 		},
-		body: JSON.stringify({
-			modelfile: modelfile
-		})
+		body: JSON.stringify(model)
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
@@ -31,10 +29,10 @@ export const createNewModelfile = async (token: string, modelfile: object) => {
 	return res;
 };
 
-export const getModelfiles = async (token: string = '') => {
+export const getModelInfos = async (token: string = '') => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/`, {
 		method: 'GET',
 		headers: {
 			Accept: 'application/json',
@@ -59,22 +57,19 @@ export const getModelfiles = async (token: string = '') => {
 		throw error;
 	}
 
-	return res.map((modelfile) => modelfile.modelfile);
+	return res;
 };
 
-export const getModelfileByTagName = async (token: string, tagName: string) => {
+export const getModelById = async (token: string, id: string) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
-		method: 'POST',
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}`, {
+		method: 'GET',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({
-			tag_name: tagName
-		})
+		}
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
@@ -94,27 +89,20 @@ export const getModelfileByTagName = async (token: string, tagName: string) => {
 		throw error;
 	}
 
-	return res.modelfile;
+	return res;
 };
 
-export const updateModelfileByTagName = async (
-	token: string,
-	tagName: string,
-	modelfile: object
-) => {
+export const updateModelById = async (token: string, id: string, model: object) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/update`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}/update`, {
 		method: 'POST',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
 		},
-		body: JSON.stringify({
-			tag_name: tagName,
-			modelfile: modelfile
-		})
+		body: JSON.stringify(model)
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
@@ -137,19 +125,16 @@ export const updateModelfileByTagName = async (
 	return res;
 };
 
-export const deleteModelfileByTagName = async (token: string, tagName: string) => {
+export const deleteModelById = async (token: string, id: string) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/delete`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/models/${id}/delete`, {
 		method: 'DELETE',
 		headers: {
 			Accept: 'application/json',
 			'Content-Type': 'application/json',
 			authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({
-			tag_name: tagName
-		})
+		}
 	})
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();

+ 6 - 1
src/lib/apis/openai/index.ts

@@ -230,7 +230,12 @@ export const getOpenAIModels = async (token: string = '') => {
 
 	return models
 		? models
-				.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
+				.map((model) => ({
+					id: model.id,
+					name: model.name ?? model.id,
+					external: true,
+					custom_info: model.custom_info
+				}))
 				.sort((a, b) => {
 					return a.name.localeCompare(b.name);
 				})

+ 104 - 106
src/lib/components/chat/Chat.svelte

@@ -10,7 +10,7 @@
 		chatId,
 		chats,
 		config,
-		modelfiles,
+		type Model,
 		models,
 		settings,
 		showSidebar,
@@ -60,25 +60,7 @@
 	let showModelSelector = true;
 
 	let selectedModels = [''];
-	let atSelectedModel = '';
-
-	let selectedModelfile = null;
-	$: selectedModelfile =
-		selectedModels.length === 1 &&
-		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
-			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
-			: null;
-
-	let selectedModelfiles = {};
-	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
-		const modelfile =
-			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
-
-		return {
-			...a,
-			...(modelfile && { [tagName]: modelfile })
-		};
-	}, {});
+	let atSelectedModel: Model | undefined;
 
 	let chat = null;
 	let tags = [];
@@ -164,6 +146,7 @@
 
 		if ($page.url.searchParams.get('q')) {
 			prompt = $page.url.searchParams.get('q') ?? '';
+
 			if (prompt) {
 				await tick();
 				submitPrompt(prompt);
@@ -211,7 +194,7 @@
 				await settings.set({
 					..._settings,
 					system: chatContent.system ?? _settings.system,
-					options: chatContent.options ?? _settings.options
+					params: chatContent.options ?? _settings.params
 				});
 				autoScroll = true;
 				await tick();
@@ -300,7 +283,7 @@
 						models: selectedModels,
 						system: $settings.system ?? undefined,
 						options: {
-							...($settings.options ?? {})
+							...($settings.params ?? {})
 						},
 						messages: messages,
 						history: history,
@@ -317,6 +300,7 @@
 
 			// Reset chat input textarea
 			prompt = '';
+			document.getElementById('chat-textarea').style.height = '';
 			files = [];
 
 			// Send prompt
@@ -328,75 +312,92 @@
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 
 		await Promise.all(
-			(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
-				async (modelId) => {
-					console.log('modelId', modelId);
-					const model = $models.filter((m) => m.id === modelId).at(0);
-
-					if (model) {
-						// Create response message
-						let responseMessageId = uuidv4();
-						let responseMessage = {
-							parentId: parentId,
-							id: responseMessageId,
-							childrenIds: [],
-							role: 'assistant',
-							content: '',
-							model: model.id,
-							userContext: null,
-							timestamp: Math.floor(Date.now() / 1000) // Unix epoch
-						};
-
-						// Add message to history and Set currentId to messageId
-						history.messages[responseMessageId] = responseMessage;
-						history.currentId = responseMessageId;
-
-						// Append messageId to childrenIds of parent message
-						if (parentId !== null) {
-							history.messages[parentId].childrenIds = [
-								...history.messages[parentId].childrenIds,
-								responseMessageId
-							];
-						}
+			(modelId
+				? [modelId]
+				: atSelectedModel !== undefined
+				? [atSelectedModel.id]
+				: selectedModels
+			).map(async (modelId) => {
+				console.log('modelId', modelId);
+				const model = $models.filter((m) => m.id === modelId).at(0);
+
+				if (model) {
+					// If there are image files, check if model is vision capable
+					const hasImages = messages.some((message) =>
+						message.files?.some((file) => file.type === 'image')
+					);
 
-						await tick();
+					if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
+						toast.error(
+							$i18n.t('Model {{modelName}} is not vision capable', {
+								modelName: model.name ?? model.id
+							})
+						);
+					}
 
-						let userContext = null;
-						if ($settings?.memory ?? false) {
-							if (userContext === null) {
-								const res = await queryMemory(localStorage.token, prompt).catch((error) => {
-									toast.error(error);
-									return null;
-								});
-
-								if (res) {
-									if (res.documents[0].length > 0) {
-										userContext = res.documents.reduce((acc, doc, index) => {
-											const createdAtTimestamp = res.metadatas[index][0].created_at;
-											const createdAtDate = new Date(createdAtTimestamp * 1000)
-												.toISOString()
-												.split('T')[0];
-											acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`);
-											return acc;
-										}, []);
-									}
+					// Create response message
+					let responseMessageId = uuidv4();
+					let responseMessage = {
+						parentId: parentId,
+						id: responseMessageId,
+						childrenIds: [],
+						role: 'assistant',
+						content: '',
+						model: model.id,
+						modelName: model.name ?? model.id,
+						userContext: null,
+						timestamp: Math.floor(Date.now() / 1000) // Unix epoch
+					};
+
+					// Add message to history and Set currentId to messageId
+					history.messages[responseMessageId] = responseMessage;
+					history.currentId = responseMessageId;
+
+					// Append messageId to childrenIds of parent message
+					if (parentId !== null) {
+						history.messages[parentId].childrenIds = [
+							...history.messages[parentId].childrenIds,
+							responseMessageId
+						];
+					}
 
-									console.log(userContext);
+					await tick();
+
+					let userContext = null;
+					if ($settings?.memory ?? false) {
+						if (userContext === null) {
+							const res = await queryMemory(localStorage.token, prompt).catch((error) => {
+								toast.error(error);
+								return null;
+							});
+
+							if (res) {
+								if (res.documents[0].length > 0) {
+									userContext = res.documents.reduce((acc, doc, index) => {
+										const createdAtTimestamp = res.metadatas[index][0].created_at;
+										const createdAtDate = new Date(createdAtTimestamp * 1000)
+											.toISOString()
+											.split('T')[0];
+										acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`);
+										return acc;
+									}, []);
 								}
+
+								console.log(userContext);
 							}
 						}
-						responseMessage.userContext = userContext;
+					}
+					responseMessage.userContext = userContext;
 
-						if (model?.external) {
-							await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
-						} else if (model) {
-							await sendPromptOllama(model, prompt, responseMessageId, _chatId);
-						}
-					} else {
-						toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
+					if (model?.owned_by === 'openai') {
+						await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
+					} else if (model) {
+						await sendPromptOllama(model, prompt, responseMessageId, _chatId);
 					}
+				} else {
+					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
 				}
-			)
+			})
 		);
 
 		await chats.set(await getChatList(localStorage.token));
@@ -430,7 +431,7 @@
 				// Prepare the base message object
 				const baseMessage = {
 					role: message.role,
-					content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content
+					content: message.content
 				};
 
 				// Extract and format image URLs if any exist
@@ -442,7 +443,6 @@
 				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
 					baseMessage.images = imageUrls;
 				}
-
 				return baseMessage;
 			});
 
@@ -473,13 +473,15 @@
 			model: model,
 			messages: messagesBody,
 			options: {
-				...($settings.options ?? {}),
+				...($settings.params ?? {}),
 				stop:
-					$settings?.options?.stop ?? undefined
-						? $settings.options.stop.map((str) =>
+					$settings?.params?.stop ?? undefined
+						? $settings.params.stop.map((str) =>
 								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 						  )
-						: undefined
+						: undefined,
+				num_predict: $settings?.params?.max_tokens ?? undefined,
+				repeat_penalty: $settings?.params?.frequency_penalty ?? undefined
 			},
 			format: $settings.requestFormat ?? undefined,
 			keep_alive: $settings.keepAlive ?? undefined,
@@ -605,7 +607,8 @@
 				if ($settings.saveChatHistory ?? true) {
 					chat = await updateChatById(localStorage.token, _chatId, {
 						messages: messages,
-						history: history
+						history: history,
+						models: selectedModels
 					});
 					await chats.set(await getChatList(localStorage.token));
 				}
@@ -716,18 +719,17 @@
 												: message?.raContent ?? message.content
 								  })
 						})),
-					seed: $settings?.options?.seed ?? undefined,
+					seed: $settings?.params?.seed ?? undefined,
 					stop:
-						$settings?.options?.stop ?? undefined
-							? $settings.options.stop.map((str) =>
+						$settings?.params?.stop ?? undefined
+							? $settings.params.stop.map((str) =>
 									decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 							  )
 							: undefined,
-					temperature: $settings?.options?.temperature ?? undefined,
-					top_p: $settings?.options?.top_p ?? undefined,
-					num_ctx: $settings?.options?.num_ctx ?? undefined,
-					frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
-					max_tokens: $settings?.options?.num_predict ?? undefined,
+					temperature: $settings?.params?.temperature ?? undefined,
+					top_p: $settings?.params?.top_p ?? undefined,
+					frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
+					max_tokens: $settings?.params?.max_tokens ?? undefined,
 					docs: docs.length > 0 ? docs : undefined,
 					citations: docs.length > 0
 				},
@@ -797,6 +799,7 @@
 				if ($chatId == _chatId) {
 					if ($settings.saveChatHistory ?? true) {
 						chat = await updateChatById(localStorage.token, _chatId, {
+							models: selectedModels,
 							messages: messages,
 							history: history
 						});
@@ -935,10 +938,8 @@
 					) + ' {{prompt}}',
 				titleModelId,
 				userPrompt,
-				titleModel?.external ?? false
-					? titleModel?.source?.toLowerCase() === 'litellm'
-						? `${LITELLM_API_BASE_URL}/v1`
-						: `${OPENAI_API_BASE_URL}`
+				titleModel?.owned_by === 'openai' ?? false
+					? `${OPENAI_API_BASE_URL}`
 					: `${OLLAMA_API_BASE_URL}/v1`
 			);
 
@@ -1025,16 +1026,12 @@
 					<Messages
 						chatId={$chatId}
 						{selectedModels}
-						{selectedModelfiles}
 						{processing}
 						bind:history
 						bind:messages
 						bind:autoScroll
 						bind:prompt
 						bottomPadding={files.length > 0}
-						suggestionPrompts={chatIdProp
-							? []
-							: selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions}
 						{sendPrompt}
 						{continueGeneration}
 						{regenerateResponse}
@@ -1048,7 +1045,8 @@
 		bind:files
 		bind:prompt
 		bind:autoScroll
-		bind:selectedModel={atSelectedModel}
+		bind:atSelectedModel
+		{selectedModels}
 		{messages}
 		{submitPrompt}
 		{stopResponse}

+ 60 - 13
src/lib/components/chat/MessageInput.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { onMount, tick, getContext } from 'svelte';
-	import { mobile, modelfiles, settings, showSidebar } from '$lib/stores';
+	import { type Model, mobile, settings, showSidebar, models } from '$lib/stores';
 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
 
 	import {
@@ -27,7 +27,9 @@
 	export let stopResponse: Function;
 
 	export let autoScroll = true;
-	export let selectedModel = '';
+
+	export let atSelectedModel: Model | undefined;
+	export let selectedModels: [''];
 
 	let chatTextAreaElement: HTMLTextAreaElement;
 	let filesInputElement;
@@ -52,6 +54,11 @@
 
 	let speechRecognition;
 
+	let visionCapableModels = [];
+	$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
+		(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
+	);
+
 	$: if (prompt) {
 		if (chatTextAreaElement) {
 			chatTextAreaElement.style.height = '';
@@ -358,6 +365,10 @@
 					inputFiles.forEach((file) => {
 						console.log(file, file.name.split('.').at(-1));
 						if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
+							if (visionCapableModels.length === 0) {
+								toast.error($i18n.t('Selected model(s) do not support image inputs'));
+								return;
+							}
 							let reader = new FileReader();
 							reader.onload = (event) => {
 								files = [
@@ -429,8 +440,8 @@
 
 <div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
 	<div class="w-full">
-		<div class="px-2.5 md:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
-			<div class="flex flex-col max-w-5xl w-full">
+		<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
+			<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
 				<div class="relative">
 					{#if autoScroll === false && messages.length > 0}
 						<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
@@ -494,12 +505,12 @@
 						bind:chatInputPlaceholder
 						{messages}
 						on:select={(e) => {
-							selectedModel = e.detail;
+							atSelectedModel = e.detail;
 							chatTextAreaElement?.focus();
 						}}
 					/>
 
-					{#if selectedModel !== ''}
+					{#if atSelectedModel !== undefined}
 						<div
 							class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
 						>
@@ -508,21 +519,21 @@
 									crossorigin="anonymous"
 									alt="model profile"
 									class="size-5 max-w-[28px] object-cover rounded-full"
-									src={$modelfiles.find((modelfile) => modelfile.tagName === selectedModel.id)
-										?.imageUrl ??
+									src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
+										?.profile_image_url ??
 										($i18n.language === 'dg-DG'
 											? `/doge.png`
 											: `${WEBUI_BASE_URL}/static/favicon.png`)}
 								/>
 								<div>
-									Talking to <span class=" font-medium">{selectedModel.name} </span>
+									Talking to <span class=" font-medium">{atSelectedModel.name}</span>
 								</div>
 							</div>
 							<div>
 								<button
 									class="flex items-center"
 									on:click={() => {
-										selectedModel = '';
+										atSelectedModel = undefined;
 									}}
 								>
 									<XMark />
@@ -535,7 +546,7 @@
 		</div>
 
 		<div class="bg-white dark:bg-gray-900">
-			<div class="max-w-6xl px-2.5 md:px-16 mx-auto inset-x-0">
+			<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
 				<div class=" pb-2">
 					<input
 						bind:this={filesInputElement}
@@ -550,6 +561,12 @@
 									if (
 										['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])
 									) {
+										if (visionCapableModels.length === 0) {
+											toast.error($i18n.t('Selected model(s) do not support image inputs'));
+											inputFiles = null;
+											filesInputElement.value = '';
+											return;
+										}
 										let reader = new FileReader();
 										reader.onload = (event) => {
 											files = [
@@ -589,6 +606,7 @@
 						dir={$settings?.chatDirection ?? 'LTR'}
 						class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
 						on:submit|preventDefault={() => {
+							// check if selectedModels support image input
 							submitPrompt(prompt, user);
 						}}
 					>
@@ -597,7 +615,36 @@
 								{#each files as file, fileIdx}
 									<div class=" relative group">
 										{#if file.type === 'image'}
-											<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
+											<div class="relative">
+												<img
+													src={file.url}
+													alt="input"
+													class=" h-16 w-16 rounded-xl object-cover"
+												/>
+												{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
+													<Tooltip
+														className=" absolute top-1 left-1"
+														content={$i18n.t('{{ models }}', {
+															models: [...(atSelectedModel ? [atSelectedModel] : selectedModels)]
+																.filter((id) => !visionCapableModels.includes(id))
+																.join(', ')
+														})}
+													>
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															viewBox="0 0 24 24"
+															fill="currentColor"
+															class="size-4 fill-yellow-300"
+														>
+															<path
+																fill-rule="evenodd"
+																d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
+																clip-rule="evenodd"
+															/>
+														</svg>
+													</Tooltip>
+												{/if}
+											</div>
 										{:else if file.type === 'doc'}
 											<div
 												class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
@@ -883,7 +930,7 @@
 
 									if (e.key === 'Escape') {
 										console.log('Escape');
-										selectedModel = '';
+										atSelectedModel = undefined;
 									}
 								}}
 								rows="1"

+ 2 - 8
src/lib/components/chat/Messages.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { v4 as uuidv4 } from 'uuid';
 
-	import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
+	import { chats, config, settings, user as _user, mobile } from '$lib/stores';
 	import { tick, getContext } from 'svelte';
 
 	import { toast } from 'svelte-sonner';
@@ -26,7 +26,6 @@
 
 	export let user = $_user;
 	export let prompt;
-	export let suggestionPrompts = [];
 	export let processing = '';
 	export let bottomPadding = false;
 	export let autoScroll;
@@ -34,7 +33,6 @@
 	export let messages = [];
 
 	export let selectedModels;
-	export let selectedModelfiles = [];
 
 	$: if (autoScroll && bottomPadding) {
 		(async () => {
@@ -247,9 +245,7 @@
 <div class="h-full flex mb-16">
 	{#if messages.length == 0}
 		<Placeholder
-			models={selectedModels}
-			modelfiles={selectedModelfiles}
-			{suggestionPrompts}
+			modelIds={selectedModels}
 			submitPrompt={async (p) => {
 				let text = p;
 
@@ -316,7 +312,6 @@
 								{#key message.id}
 									<ResponseMessage
 										{message}
-										modelfiles={selectedModelfiles}
 										siblings={history.messages[message.parentId]?.childrenIds ?? []}
 										isLastMessage={messageIdx + 1 === messages.length}
 										{readOnly}
@@ -348,7 +343,6 @@
 										{chatId}
 										parentMessage={history.messages[message.parentId]}
 										{messageIdx}
-										{selectedModelfiles}
 										{updateChatMessages}
 										{confirmEditResponseMessage}
 										{rateMessage}

+ 54 - 53
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -4,7 +4,7 @@
 	import hljs from 'highlight.js';
 	import 'highlight.js/styles/github-dark.min.css';
 	import { loadPyodide } from 'pyodide';
-	import { tick } from 'svelte';
+	import { onMount, tick } from 'svelte';
 	import PyodideWorker from '$lib/workers/pyodide.worker?worker';
 
 	export let id = '';
@@ -12,6 +12,7 @@
 	export let lang = '';
 	export let code = '';
 
+	let highlightedCode = null;
 	let executing = false;
 
 	let stdout = null;
@@ -202,60 +203,60 @@ __builtins__.input = input`);
 		};
 	};
 
-	$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : '';
+	$: if (code) {
+		highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
+	}
 </script>
 
-{#if code}
-	<div class="mb-4" dir="ltr">
-		<div
-			class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
-		>
-			<div class="p-1">{@html lang}</div>
-
-			<div class="flex items-center">
-				{#if lang === 'python' || (lang === '' && checkPythonCode(code))}
-					{#if executing}
-						<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
-					{:else}
-						<button
-							class="copy-code-button bg-none border-none p-1"
-							on:click={() => {
-								executePython(code);
-							}}>Run</button
-						>
-					{/if}
+<div class="mb-4" dir="ltr">
+	<div
+		class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
+	>
+		<div class="p-1">{@html lang}</div>
+
+		<div class="flex items-center">
+			{#if lang === 'python' || (lang === '' && checkPythonCode(code))}
+				{#if executing}
+					<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
+				{:else}
+					<button
+						class="copy-code-button bg-none border-none p-1"
+						on:click={() => {
+							executePython(code);
+						}}>Run</button
+					>
 				{/if}
-				<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
-					>{copied ? 'Copied' : 'Copy Code'}</button
-				>
-			</div>
+			{/if}
+			<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
+				>{copied ? 'Copied' : 'Copy Code'}</button
+			>
 		</div>
-
-		<pre
-			class=" hljs p-4 px-5 overflow-x-auto"
-			style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
-				stdout ||
-				stderr ||
-				result) &&
-				'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
-				class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
-			></pre>
-
-		<div
-			id="plt-canvas-{id}"
-			class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
-		/>
-
-		{#if executing}
-			<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
-				<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
-				<div class="text-sm">Running...</div>
-			</div>
-		{:else if stdout || stderr || result}
-			<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
-				<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
-				<div class="text-sm">{stdout || stderr || result}</div>
-			</div>
-		{/if}
 	</div>
-{/if}
+
+	<pre
+		class=" hljs p-4 px-5 overflow-x-auto"
+		style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
+			stdout ||
+			stderr ||
+			result) &&
+			'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
+			class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
+		></pre>
+
+	<div
+		id="plt-canvas-{id}"
+		class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
+	/>
+
+	{#if executing}
+		<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
+			<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+			<div class="text-sm">Running...</div>
+		</div>
+	{:else if stdout || stderr || result}
+		<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
+			<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
+			<div class="text-sm">{stdout || stderr || result}</div>
+		</div>
+	{/if}
+</div>

+ 0 - 3
src/lib/components/chat/Messages/CompareMessages.svelte

@@ -13,8 +13,6 @@
 
 	export let parentMessage;
 
-	export let selectedModelfiles;
-
 	export let updateChatMessages: Function;
 	export let confirmEditResponseMessage: Function;
 	export let rateMessage: Function;
@@ -130,7 +128,6 @@
 				>
 					<ResponseMessage
 						message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
-						modelfiles={selectedModelfiles}
 						siblings={groupedMessages[model].messages.map((m) => m.id)}
 						isLastMessage={true}
 						{updateChatMessages}

+ 36 - 37
src/lib/components/chat/Messages/Placeholder.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 	import { WEBUI_BASE_URL } from '$lib/constants';
-	import { user } from '$lib/stores';
+	import { config, user, models as _models } from '$lib/stores';
 	import { onMount, getContext } from 'svelte';
 
 	import { blur, fade } from 'svelte/transition';
@@ -9,23 +9,20 @@
 
 	const i18n = getContext('i18n');
 
+	export let modelIds = [];
 	export let models = [];
-	export let modelfiles = [];
 
 	export let submitPrompt;
-	export let suggestionPrompts;
 
 	let mounted = false;
-	let modelfile = null;
 	let selectedModelIdx = 0;
 
-	$: modelfile =
-		models[selectedModelIdx] in modelfiles ? modelfiles[models[selectedModelIdx]] : null;
-
-	$: if (models.length > 0) {
+	$: if (modelIds.length > 0) {
 		selectedModelIdx = models.length - 1;
 	}
 
+	$: models = modelIds.map((id) => $_models.find((m) => m.id === id));
+
 	onMount(() => {
 		mounted = true;
 	});
@@ -41,25 +38,14 @@
 							selectedModelIdx = modelIdx;
 						}}
 					>
-						{#if model in modelfiles}
-							<img
-								crossorigin="anonymous"
-								src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
-								alt="modelfile"
-								class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
-								draggable="false"
-							/>
-						{:else}
-							<img
-								crossorigin="anonymous"
-								src={$i18n.language === 'dg-DG'
-									? `/doge.png`
-									: `${WEBUI_BASE_URL}/static/favicon.png`}
-								class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
-								alt="logo"
-								draggable="false"
-							/>
-						{/if}
+						<img
+							crossorigin="anonymous"
+							src={model?.info?.meta?.profile_image_url ??
+								($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
+							class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
+							alt="logo"
+							draggable="false"
+						/>
 					</button>
 				{/each}
 			</div>
@@ -70,23 +56,32 @@
 		>
 			<div>
 				<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
-					{#if modelfile}
-						{modelfile.title}
+					{#if models[selectedModelIdx]?.info}
+						{models[selectedModelIdx]?.info?.name}
 					{:else}
 						{$i18n.t('Hello, {{name}}', { name: $user.name })}
 					{/if}
 				</div>
 
 				<div in:fade={{ duration: 200, delay: 200 }}>
-					{#if modelfile}
-						<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
-							{modelfile.desc}
+					{#if models[selectedModelIdx]?.info}
+						<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3">
+							{models[selectedModelIdx]?.info?.meta?.description}
 						</div>
-						{#if modelfile.user}
+						{#if models[selectedModelIdx]?.info?.meta?.user}
 							<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
-								By <a href="https://openwebui.com/m/{modelfile.user.username}"
-									>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
-								>
+								By
+								{#if models[selectedModelIdx]?.info?.meta?.user.community}
+									<a
+										href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
+											.username}"
+										>{models[selectedModelIdx]?.info?.meta?.user.name
+											? models[selectedModelIdx]?.info?.meta?.user.name
+											: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
+									>
+								{:else}
+									{models[selectedModelIdx]?.info?.meta?.user.name}
+								{/if}
 							</div>
 						{/if}
 					{:else}
@@ -99,7 +94,11 @@
 		</div>
 
 		<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
-			<Suggestions {suggestionPrompts} {submitPrompt} />
+			<Suggestions
+				suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
+					$config.default_prompt_suggestions}
+				{submitPrompt}
+			/>
 		</div>
 	</div>
 {/key}

+ 8 - 10
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -14,7 +14,7 @@
 
 	const dispatch = createEventDispatcher();
 
-	import { config, settings } from '$lib/stores';
+	import { config, models, settings } from '$lib/stores';
 	import { synthesizeOpenAISpeech } from '$lib/apis/audio';
 	import { imageGenerations } from '$lib/apis/images';
 	import {
@@ -34,7 +34,6 @@
 	import RateComment from './RateComment.svelte';
 	import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
 
-	export let modelfiles = [];
 	export let message;
 	export let siblings;
 
@@ -52,6 +51,9 @@
 	export let continueGeneration: Function;
 	export let regenerateResponse: Function;
 
+	let model = null;
+	$: model = $models.find((m) => m.id === message.model);
+
 	let edit = false;
 	let editedContent = '';
 	let editTextAreaElement: HTMLTextAreaElement;
@@ -338,17 +340,13 @@
 		dir={$settings.chatDirection}
 	>
 		<ProfileImage
-			src={modelfiles[message.model]?.imageUrl ??
+			src={model?.info?.meta?.profile_image_url ??
 				($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
 		/>
 
 		<div class="w-full overflow-hidden pl-1">
 			<Name>
-				{#if message.model in modelfiles}
-					{modelfiles[message.model]?.title}
-				{:else}
-					{message.model ? ` ${message.model}` : ''}
-				{/if}
+				{model?.name ?? message.model}
 
 				{#if message.timestamp}
 					<span
@@ -442,8 +440,8 @@
 									{#if token.type === 'code'}
 										<CodeBlock
 											id={`${message.id}-${tokenIdx}`}
-											lang={token.lang}
-											code={revertSanitizedResponseContent(token.text)}
+											lang={token?.lang ?? ''}
+											code={revertSanitizedResponseContent(token?.text ?? '')}
 										/>
 									{:else}
 										{@html marked.parse(token.raw, {

+ 4 - 9
src/lib/components/chat/Messages/UserMessage.svelte

@@ -4,7 +4,7 @@
 	import { tick, createEventDispatcher, getContext } from 'svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
-	import { modelfiles, settings } from '$lib/stores';
+	import { models, settings } from '$lib/stores';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	import { user as _user } from '$lib/stores';
@@ -60,8 +60,7 @@
 	{#if !($settings?.chatBubble ?? true)}
 		<ProfileImage
 			src={message.user
-				? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ??
-				  '/user.png'
+				? $models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png'
 				: user?.profile_image_url ?? '/user.png'}
 		/>
 	{/if}
@@ -70,12 +69,8 @@
 			<div>
 				<Name>
 					{#if message.user}
-						{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
-							{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
-						{:else}
-							{$i18n.t('You')}
-							<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
-						{/if}
+						{$i18n.t('You')}
+						<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
 					{:else if $settings.showUsername || $_user.name !== user.name}
 						{user.name}
 					{:else}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 2 - 0
src/lib/components/chat/Messages/test.json


+ 5 - 7
src/lib/components/chat/ModelSelector.svelte

@@ -45,13 +45,11 @@
 				<div class="mr-1 max-w-full">
 					<Selector
 						placeholder={$i18n.t('Select a model')}
-						items={$models
-							.filter((model) => model.name !== 'hr')
-							.map((model) => ({
-								value: model.id,
-								label: model.name,
-								info: model
-							}))}
+						items={$models.map((model) => ({
+							value: model.id,
+							label: model.name,
+							model: model
+						}))}
 						bind:value={selectedModel}
 					/>
 				</div>

+ 43 - 18
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -12,7 +12,9 @@
 
 	import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
 	import { toast } from 'svelte-sonner';
-	import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
+	import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
+	import { getModels } from '$lib/apis';
+
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	const i18n = getContext('i18n');
@@ -23,7 +25,12 @@
 	export let searchEnabled = true;
 	export let searchPlaceholder = $i18n.t('Search a model');
 
-	export let items = [{ value: 'mango', label: 'Mango' }];
+	export let items: {
+		label: string;
+		value: string;
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		[key: string]: any;
+	} = [];
 
 	export let className = 'w-[30rem]';
 
@@ -239,19 +246,37 @@
 						}}
 					>
 						<div class="flex items-center gap-2">
-							<div class="line-clamp-1">
-								{item.label}
-
-								<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
-									>{item.info?.details?.parameter_size ?? ''}</span
-								>
+							<div class="flex items-center">
+								<div class="line-clamp-1">
+									{item.label}
+								</div>
+								{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
+									<div class="flex ml-1 items-center">
+										<Tooltip
+											content={`${
+												item.model.ollama?.details?.quantization_level
+													? item.model.ollama?.details?.quantization_level + ' '
+													: ''
+											}${
+												item.model.ollama?.size
+													? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
+													: ''
+											}`}
+											className="self-end"
+										>
+											<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
+												>{item.model.ollama?.details?.parameter_size ?? ''}</span
+											>
+										</Tooltip>
+									</div>
+								{/if}
 							</div>
 
 							<!-- {JSON.stringify(item.info)} -->
 
-							{#if item.info.external}
-								<Tooltip content={item.info?.source ?? 'External'}>
-									<div class=" mr-2">
+							{#if item.model.owned_by === 'openai'}
+								<Tooltip content={`${'External'}`}>
+									<div class="">
 										<svg
 											xmlns="http://www.w3.org/2000/svg"
 											viewBox="0 0 16 16"
@@ -271,15 +296,15 @@
 										</svg>
 									</div>
 								</Tooltip>
-							{:else}
+							{/if}
+
+							{#if item.model?.info?.meta?.description}
 								<Tooltip
-									content={`${
-										item.info?.details?.quantization_level
-											? item.info?.details?.quantization_level + ' '
-											: ''
-									}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
+									content={`${sanitizeResponseContent(
+										item.model?.info?.meta?.description
+									).replaceAll('\n', '<br>')}`}
 								>
-									<div class=" mr-2">
+									<div class="">
 										<svg
 											xmlns="http://www.w3.org/2000/svg"
 											fill="none"

+ 0 - 155
src/lib/components/chat/Settings/Advanced.svelte

@@ -1,155 +0,0 @@
-<script lang="ts">
-	import { createEventDispatcher, onMount, getContext } from 'svelte';
-	import AdvancedParams from './Advanced/AdvancedParams.svelte';
-
-	const i18n = getContext('i18n');
-	const dispatch = createEventDispatcher();
-
-	export let saveSettings: Function;
-
-	// Advanced
-	let requestFormat = '';
-	let keepAlive = null;
-
-	let options = {
-		// Advanced
-		seed: 0,
-		temperature: '',
-		repeat_penalty: '',
-		repeat_last_n: '',
-		mirostat: '',
-		mirostat_eta: '',
-		mirostat_tau: '',
-		top_k: '',
-		top_p: '',
-		stop: '',
-		tfs_z: '',
-		num_ctx: '',
-		num_predict: ''
-	};
-
-	const toggleRequestFormat = async () => {
-		if (requestFormat === '') {
-			requestFormat = 'json';
-		} else {
-			requestFormat = '';
-		}
-
-		saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
-	};
-
-	onMount(() => {
-		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
-
-		requestFormat = settings.requestFormat ?? '';
-		keepAlive = settings.keepAlive ?? null;
-
-		options.seed = settings.seed ?? 0;
-		options.temperature = settings.temperature ?? '';
-		options.repeat_penalty = settings.repeat_penalty ?? '';
-		options.top_k = settings.top_k ?? '';
-		options.top_p = settings.top_p ?? '';
-		options.num_ctx = settings.num_ctx ?? '';
-		options = { ...options, ...settings.options };
-		options.stop = (settings?.options?.stop ?? []).join(',');
-	});
-</script>
-
-<div class="flex flex-col h-full justify-between text-sm">
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
-		<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
-
-		<AdvancedParams bind:options />
-		<hr class=" dark:border-gray-700" />
-
-		<div class=" py-1 w-full justify-between">
-			<div class="flex w-full justify-between">
-				<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					type="button"
-					on:click={() => {
-						keepAlive = keepAlive === null ? '5m' : null;
-					}}
-				>
-					{#if keepAlive === null}
-						<span class="ml-2 self-center">{$i18n.t('Default')}</span>
-					{:else}
-						<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
-					{/if}
-				</button>
-			</div>
-
-			{#if keepAlive !== null}
-				<div class="flex mt-1 space-x-2">
-					<input
-						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-						type="text"
-						placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
-						bind:value={keepAlive}
-					/>
-				</div>
-			{/if}
-		</div>
-
-		<div>
-			<div class=" py-1 flex w-full justify-between">
-				<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					on:click={() => {
-						toggleRequestFormat();
-					}}
-				>
-					{#if requestFormat === ''}
-						<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
-					{:else if requestFormat === 'json'}
-						<!-- <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            viewBox="0 0 20 20"
-                            fill="currentColor"
-                            class="w-4 h-4 self-center"
-                        >
-                            <path
-                                d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
-                            />
-                        </svg> -->
-						<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
-					{/if}
-				</button>
-			</div>
-		</div>
-	</div>
-
-	<div class="flex justify-end pt-3 text-sm font-medium">
-		<button
-			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
-			on:click={() => {
-				saveSettings({
-					options: {
-						seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
-						stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
-						temperature: options.temperature !== '' ? options.temperature : undefined,
-						repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
-						repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
-						mirostat: options.mirostat !== '' ? options.mirostat : undefined,
-						mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
-						mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
-						top_k: options.top_k !== '' ? options.top_k : undefined,
-						top_p: options.top_p !== '' ? options.top_p : undefined,
-						tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
-						num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
-						num_predict: options.num_predict !== '' ? options.num_predict : undefined
-					},
-					keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
-				});
-
-				dispatch('save');
-			}}
-		>
-			{$i18n.t('Save')}
-		</button>
-	</div>
-</div>

+ 177 - 97
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

@@ -1,14 +1,16 @@
 <script lang="ts">
-	import { getContext } from 'svelte';
+	import { getContext, createEventDispatcher } from 'svelte';
+
+	const dispatch = createEventDispatcher();
 
 	const i18n = getContext('i18n');
 
-	export let options = {
+	export let params = {
 		// Advanced
 		seed: 0,
-		stop: '',
+		stop: null,
 		temperature: '',
-		repeat_penalty: '',
+		frequency_penalty: '',
 		repeat_last_n: '',
 		mirostat: '',
 		mirostat_eta: '',
@@ -17,40 +19,86 @@
 		top_p: '',
 		tfs_z: '',
 		num_ctx: '',
-		num_predict: ''
+		max_tokens: '',
+		template: null
 	};
+
+	let customFieldName = '';
+	let customFieldValue = '';
+
+	$: if (params) {
+		dispatch('change', params);
+	}
 </script>
 
-<div class=" space-y-3 text-xs">
-	<div>
-		<div class=" py-0.5 flex w-full justify-between">
-			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
-			<div class=" flex-1 self-center">
-				<input
-					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-					type="number"
-					placeholder="Enter Seed"
-					bind:value={options.seed}
-					autocomplete="off"
-					min="0"
-				/>
-			</div>
+<div class=" space-y-1 text-xs">
+	<div class=" py-0.5 w-full justify-between">
+		<div class="flex w-full justify-between">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
+
+			<button
+				class="p-1 px-3 text-xs flex rounded transition"
+				type="button"
+				on:click={() => {
+					params.seed = (params?.seed ?? null) === null ? 0 : null;
+				}}
+			>
+				{#if (params?.seed ?? null) === null}
+					<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
+				{:else}
+					<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
+				{/if}
+			</button>
 		</div>
-	</div>
 
-	<div>
-		<div class=" py-0.5 flex w-full justify-between">
-			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
-			<div class=" flex-1 self-center">
-				<input
-					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-					type="text"
-					placeholder={$i18n.t('Enter stop sequence')}
-					bind:value={options.stop}
-					autocomplete="off"
-				/>
+		{#if (params?.seed ?? null) !== null}
+			<div class="flex mt-0.5 space-x-2">
+				<div class=" flex-1">
+					<input
+						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						type="number"
+						placeholder="Enter Seed"
+						bind:value={params.seed}
+						autocomplete="off"
+						min="0"
+					/>
+				</div>
 			</div>
+		{/if}
+	</div>
+
+	<div class=" py-0.5 w-full justify-between">
+		<div class="flex w-full justify-between">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div>
+
+			<button
+				class="p-1 px-3 text-xs flex rounded transition"
+				type="button"
+				on:click={() => {
+					params.stop = (params?.stop ?? null) === null ? '' : null;
+				}}
+			>
+				{#if (params?.stop ?? null) === null}
+					<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
+				{:else}
+					<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
+				{/if}
+			</button>
 		</div>
+
+		{#if (params?.stop ?? null) !== null}
+			<div class="flex mt-0.5 space-x-2">
+				<div class=" flex-1">
+					<input
+						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						type="text"
+						placeholder={$i18n.t('Enter stop sequence')}
+						bind:value={params.stop}
+						autocomplete="off"
+					/>
+				</div>
+			</div>
+		{/if}
 	</div>
 
 	<div class=" py-0.5 w-full justify-between">
@@ -61,10 +109,10 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.temperature = options.temperature === '' ? 0.8 : '';
+					params.temperature = (params?.temperature ?? '') === '' ? 0.8 : '';
 				}}
 			>
-				{#if options.temperature === ''}
+				{#if (params?.temperature ?? '') === ''}
 					<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
 				{:else}
 					<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
@@ -72,7 +120,7 @@
 			</button>
 		</div>
 
-		{#if options.temperature !== ''}
+		{#if (params?.temperature ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -81,13 +129,13 @@
 						min="0"
 						max="1"
 						step="0.05"
-						bind:value={options.temperature}
+						bind:value={params.temperature}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.temperature}
+						bind:value={params.temperature}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -107,18 +155,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.mirostat = options.mirostat === '' ? 0 : '';
+					params.mirostat = (params?.mirostat ?? '') === '' ? 0 : '';
 				}}
 			>
-				{#if options.mirostat === ''}
+				{#if (params?.mirostat ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.mirostat !== ''}
+		{#if (params?.mirostat ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -127,13 +175,13 @@
 						min="0"
 						max="2"
 						step="1"
-						bind:value={options.mirostat}
+						bind:value={params.mirostat}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.mirostat}
+						bind:value={params.mirostat}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -153,18 +201,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.mirostat_eta = options.mirostat_eta === '' ? 0.1 : '';
+					params.mirostat_eta = (params?.mirostat_eta ?? '') === '' ? 0.1 : '';
 				}}
 			>
-				{#if options.mirostat_eta === ''}
+				{#if (params?.mirostat_eta ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.mirostat_eta !== ''}
+		{#if (params?.mirostat_eta ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -173,13 +221,13 @@
 						min="0"
 						max="1"
 						step="0.05"
-						bind:value={options.mirostat_eta}
+						bind:value={params.mirostat_eta}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.mirostat_eta}
+						bind:value={params.mirostat_eta}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -199,10 +247,10 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.mirostat_tau = options.mirostat_tau === '' ? 5.0 : '';
+					params.mirostat_tau = (params?.mirostat_tau ?? '') === '' ? 5.0 : '';
 				}}
 			>
-				{#if options.mirostat_tau === ''}
+				{#if (params?.mirostat_tau ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
 					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
@@ -210,7 +258,7 @@
 			</button>
 		</div>
 
-		{#if options.mirostat_tau !== ''}
+		{#if (params?.mirostat_tau ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -219,13 +267,13 @@
 						min="0"
 						max="10"
 						step="0.5"
-						bind:value={options.mirostat_tau}
+						bind:value={params.mirostat_tau}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.mirostat_tau}
+						bind:value={params.mirostat_tau}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -245,18 +293,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.top_k = options.top_k === '' ? 40 : '';
+					params.top_k = (params?.top_k ?? '') === '' ? 40 : '';
 				}}
 			>
-				{#if options.top_k === ''}
+				{#if (params?.top_k ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.top_k !== ''}
+		{#if (params?.top_k ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -265,13 +313,13 @@
 						min="0"
 						max="100"
 						step="0.5"
-						bind:value={options.top_k}
+						bind:value={params.top_k}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.top_k}
+						bind:value={params.top_k}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -291,18 +339,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.top_p = options.top_p === '' ? 0.9 : '';
+					params.top_p = (params?.top_p ?? '') === '' ? 0.9 : '';
 				}}
 			>
-				{#if options.top_p === ''}
+				{#if (params?.top_p ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.top_p !== ''}
+		{#if (params?.top_p ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -311,13 +359,13 @@
 						min="0"
 						max="1"
 						step="0.05"
-						bind:value={options.top_p}
+						bind:value={params.top_p}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.top_p}
+						bind:value={params.top_p}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -331,24 +379,24 @@
 
 	<div class=" py-0.5 w-full justify-between">
 		<div class="flex w-full justify-between">
-			<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div>
+			<div class=" self-center text-xs font-medium">{$i18n.t('Frequencey Penalty')}</div>
 
 			<button
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.repeat_penalty = options.repeat_penalty === '' ? 1.1 : '';
+					params.frequency_penalty = (params?.frequency_penalty ?? '') === '' ? 1.1 : '';
 				}}
 			>
-				{#if options.repeat_penalty === ''}
+				{#if (params?.frequency_penalty ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.repeat_penalty !== ''}
+		{#if (params?.frequency_penalty ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -357,13 +405,13 @@
 						min="0"
 						max="2"
 						step="0.05"
-						bind:value={options.repeat_penalty}
+						bind:value={params.frequency_penalty}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.repeat_penalty}
+						bind:value={params.frequency_penalty}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -383,18 +431,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.repeat_last_n = options.repeat_last_n === '' ? 64 : '';
+					params.repeat_last_n = (params?.repeat_last_n ?? '') === '' ? 64 : '';
 				}}
 			>
-				{#if options.repeat_last_n === ''}
+				{#if (params?.repeat_last_n ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.repeat_last_n !== ''}
+		{#if (params?.repeat_last_n ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -403,13 +451,13 @@
 						min="-1"
 						max="128"
 						step="1"
-						bind:value={options.repeat_last_n}
+						bind:value={params.repeat_last_n}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.repeat_last_n}
+						bind:value={params.repeat_last_n}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="-1"
@@ -429,18 +477,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.tfs_z = options.tfs_z === '' ? 1 : '';
+					params.tfs_z = (params?.tfs_z ?? '') === '' ? 1 : '';
 				}}
 			>
-				{#if options.tfs_z === ''}
+				{#if (params?.tfs_z ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.tfs_z !== ''}
+		{#if (params?.tfs_z ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -449,13 +497,13 @@
 						min="0"
 						max="2"
 						step="0.05"
-						bind:value={options.tfs_z}
+						bind:value={params.tfs_z}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div>
 					<input
-						bind:value={options.tfs_z}
+						bind:value={params.tfs_z}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="0"
@@ -475,18 +523,18 @@
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.num_ctx = options.num_ctx === '' ? 2048 : '';
+					params.num_ctx = (params?.num_ctx ?? '') === '' ? 2048 : '';
 				}}
 			>
-				{#if options.num_ctx === ''}
+				{#if (params?.num_ctx ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.num_ctx !== ''}
+		{#if (params?.num_ctx ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -495,13 +543,13 @@
 						min="-1"
 						max="10240000"
 						step="1"
-						bind:value={options.num_ctx}
+						bind:value={params.num_ctx}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div class="">
 					<input
-						bind:value={options.num_ctx}
+						bind:value={params.num_ctx}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="-1"
@@ -513,24 +561,24 @@
 	</div>
 	<div class=" py-0.5 w-full justify-between">
 		<div class="flex w-full justify-between">
-			<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div>
+			<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
 
 			<button
 				class="p-1 px-3 text-xs flex rounded transition"
 				type="button"
 				on:click={() => {
-					options.num_predict = options.num_predict === '' ? 128 : '';
+					params.max_tokens = (params?.max_tokens ?? '') === '' ? 128 : '';
 				}}
 			>
-				{#if options.num_predict === ''}
+				{#if (params?.max_tokens ?? '') === ''}
 					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
 				{:else}
-					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
 				{/if}
 			</button>
 		</div>
 
-		{#if options.num_predict !== ''}
+		{#if (params?.max_tokens ?? '') !== ''}
 			<div class="flex mt-0.5 space-x-2">
 				<div class=" flex-1">
 					<input
@@ -539,13 +587,13 @@
 						min="-2"
 						max="16000"
 						step="1"
-						bind:value={options.num_predict}
+						bind:value={params.max_tokens}
 						class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
 					/>
 				</div>
 				<div class="">
 					<input
-						bind:value={options.num_predict}
+						bind:value={params.max_tokens}
 						type="number"
 						class=" bg-transparent text-center w-14"
 						min="-2"
@@ -556,4 +604,36 @@
 			</div>
 		{/if}
 	</div>
+	<div class=" py-0.5 w-full justify-between">
+		<div class="flex w-full justify-between">
+			<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
+
+			<button
+				class="p-1 px-3 text-xs flex rounded transition"
+				type="button"
+				on:click={() => {
+					params.template = (params?.template ?? null) === null ? '' : null;
+				}}
+			>
+				{#if (params?.template ?? null) === null}
+					<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+				{:else}
+					<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
+				{/if}
+			</button>
+		</div>
+
+		{#if (params?.template ?? null) !== null}
+			<div class="flex mt-0.5 space-x-2">
+				<div class=" flex-1">
+					<textarea
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
+						placeholder="Write your model template content here"
+						rows="4"
+						bind:value={params.template}
+					/>
+				</div>
+			</div>
+		{/if}
+	</div>
 </div>

+ 28 - 27
src/lib/components/chat/Settings/General.svelte

@@ -41,21 +41,21 @@
 	let requestFormat = '';
 	let keepAlive = null;
 
-	let options = {
+	let params = {
 		// Advanced
 		seed: 0,
 		temperature: '',
-		repeat_penalty: '',
+		frequency_penalty: '',
 		repeat_last_n: '',
 		mirostat: '',
 		mirostat_eta: '',
 		mirostat_tau: '',
 		top_k: '',
 		top_p: '',
-		stop: '',
+		stop: null,
 		tfs_z: '',
 		num_ctx: '',
-		num_predict: ''
+		max_tokens: ''
 	};
 
 	const toggleRequestFormat = async () => {
@@ -80,14 +80,14 @@
 		requestFormat = settings.requestFormat ?? '';
 		keepAlive = settings.keepAlive ?? null;
 
-		options.seed = settings.seed ?? 0;
-		options.temperature = settings.temperature ?? '';
-		options.repeat_penalty = settings.repeat_penalty ?? '';
-		options.top_k = settings.top_k ?? '';
-		options.top_p = settings.top_p ?? '';
-		options.num_ctx = settings.num_ctx ?? '';
-		options = { ...options, ...settings.options };
-		options.stop = (settings?.options?.stop ?? []).join(',');
+		params.seed = settings.seed ?? 0;
+		params.temperature = settings.temperature ?? '';
+		params.frequency_penalty = settings.frequency_penalty ?? '';
+		params.top_k = settings.top_k ?? '';
+		params.top_p = settings.top_p ?? '';
+		params.num_ctx = settings.num_ctx ?? '';
+		params = { ...params, ...settings.params };
+		params.stop = settings?.params?.stop ? (settings?.params?.stop ?? []).join(',') : null;
 	});
 
 	const applyTheme = (_theme: string) => {
@@ -228,7 +228,7 @@
 			</div>
 
 			{#if showAdvanced}
-				<AdvancedParams bind:options />
+				<AdvancedParams bind:params />
 				<hr class=" dark:border-gray-700" />
 
 				<div class=" py-1 w-full justify-between">
@@ -300,20 +300,21 @@
 			on:click={() => {
 				saveSettings({
 					system: system !== '' ? system : undefined,
-					options: {
-						seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
-						stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
-						temperature: options.temperature !== '' ? options.temperature : undefined,
-						repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
-						repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
-						mirostat: options.mirostat !== '' ? options.mirostat : undefined,
-						mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
-						mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
-						top_k: options.top_k !== '' ? options.top_k : undefined,
-						top_p: options.top_p !== '' ? options.top_p : undefined,
-						tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
-						num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
-						num_predict: options.num_predict !== '' ? options.num_predict : undefined
+					params: {
+						seed: (params.seed !== 0 ? params.seed : undefined) ?? undefined,
+						stop: params.stop !== null ? params.stop.split(',').filter((e) => e) : undefined,
+						temperature: params.temperature !== '' ? params.temperature : undefined,
+						frequency_penalty:
+							params.frequency_penalty !== '' ? params.frequency_penalty : undefined,
+						repeat_last_n: params.repeat_last_n !== '' ? params.repeat_last_n : undefined,
+						mirostat: params.mirostat !== '' ? params.mirostat : undefined,
+						mirostat_eta: params.mirostat_eta !== '' ? params.mirostat_eta : undefined,
+						mirostat_tau: params.mirostat_tau !== '' ? params.mirostat_tau : undefined,
+						top_k: params.top_k !== '' ? params.top_k : undefined,
+						top_p: params.top_p !== '' ? params.top_p : undefined,
+						tfs_z: params.tfs_z !== '' ? params.tfs_z : undefined,
+						num_ctx: params.num_ctx !== '' ? params.num_ctx : undefined,
+						max_tokens: params.max_tokens !== '' ? params.max_tokens : undefined
 					},
 					keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
 				});

+ 37 - 287
src/lib/components/chat/Settings/Models.svelte

@@ -1,5 +1,4 @@
 <script lang="ts">
-	import queue from 'async/queue';
 	import { toast } from 'svelte-sonner';
 
 	import {
@@ -12,32 +11,19 @@
 		cancelOllamaRequest,
 		uploadModel
 	} from '$lib/apis/ollama';
+
 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
-	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user } from '$lib/stores';
+	import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
 	import { splitStream } from '$lib/utils';
 	import { onMount, getContext } from 'svelte';
-	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
+
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let getModels: Function;
 
-	let showLiteLLM = false;
-	let showLiteLLMParams = false;
 	let modelUploadInputElement: HTMLInputElement;
-	let liteLLMModelInfo = [];
-
-	let liteLLMModel = '';
-	let liteLLMModelName = '';
-	let liteLLMAPIBase = '';
-	let liteLLMAPIKey = '';
-	let liteLLMRPM = '';
-	let liteLLMMaxTokens = '';
-
-	let deleteLiteLLMModelName = '';
-
-	$: liteLLMModelName = liteLLMModel;
 
 	// Models
 
@@ -439,71 +425,22 @@
 		}
 	};
 
-	const addLiteLLMModelHandler = async () => {
-		if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
-			const res = await addLiteLLMModel(localStorage.token, {
-				name: liteLLMModelName,
-				model: liteLLMModel,
-				api_base: liteLLMAPIBase,
-				api_key: liteLLMAPIKey,
-				rpm: liteLLMRPM,
-				max_tokens: liteLLMMaxTokens
-			}).catch((error) => {
-				toast.error(error);
-				return null;
-			});
-
-			if (res) {
-				if (res.message) {
-					toast.success(res.message);
-				}
-			}
-		} else {
-			toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
-		}
-
-		liteLLMModelName = '';
-		liteLLMModel = '';
-		liteLLMAPIBase = '';
-		liteLLMAPIKey = '';
-		liteLLMRPM = '';
-		liteLLMMaxTokens = '';
-
-		liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
-		models.set(await getModels());
-	};
-
-	const deleteLiteLLMModelHandler = async () => {
-		const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelName).catch(
-			(error) => {
-				toast.error(error);
-				return null;
-			}
-		);
-
-		if (res) {
-			if (res.message) {
-				toast.success(res.message);
-			}
-		}
-
-		deleteLiteLLMModelName = '';
-		liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
-		models.set(await getModels());
-	};
-
 	onMount(async () => {
-		OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
-			toast.error(error);
-			return [];
-		});
-
-		if (OLLAMA_URLS.length > 0) {
-			selectedOllamaUrlIdx = 0;
-		}
+		await Promise.all([
+			(async () => {
+				OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
+					toast.error(error);
+					return [];
+				});
 
-		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
-		liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
+				if (OLLAMA_URLS.length > 0) {
+					selectedOllamaUrlIdx = 0;
+				}
+			})(),
+			(async () => {
+				ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
+			})()
+		]);
 	});
 </script>
 
@@ -587,24 +524,28 @@
 											viewBox="0 0 24 24"
 											fill="currentColor"
 											xmlns="http://www.w3.org/2000/svg"
-											><style>
+										>
+											<style>
 												.spinner_ajPY {
 													transform-origin: center;
 													animation: spinner_AtaB 0.75s infinite linear;
 												}
+
 												@keyframes spinner_AtaB {
 													100% {
 														transform: rotate(360deg);
 													}
 												}
-											</style><path
+											</style>
+											<path
 												d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
 												opacity=".25"
-											/><path
+											/>
+											<path
 												d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
 												class="spinner_ajPY"
-											/></svg
-										>
+											/>
+										</svg>
 									</div>
 								{:else}
 									<svg
@@ -833,24 +774,28 @@
 													viewBox="0 0 24 24"
 													fill="currentColor"
 													xmlns="http://www.w3.org/2000/svg"
-													><style>
+												>
+													<style>
 														.spinner_ajPY {
 															transform-origin: center;
 															animation: spinner_AtaB 0.75s infinite linear;
 														}
+
 														@keyframes spinner_AtaB {
 															100% {
 																transform: rotate(360deg);
 															}
 														}
-													</style><path
+													</style>
+													<path
 														d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
 														opacity=".25"
-													/><path
+													/>
+													<path
 														d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
 														class="spinner_ajPY"
-													/></svg
-												>
+													/>
+												</svg>
 											</div>
 										{:else}
 											<svg
@@ -929,203 +874,8 @@
 					{/if}
 				</div>
 			</div>
-			<hr class=" dark:border-gray-700 my-2" />
+		{:else}
+			<div>Ollama Not Detected</div>
 		{/if}
-
-		<div class=" space-y-3">
-			<div class="mt-2 space-y-3 pr-1.5">
-				<div>
-					<div class="mb-2">
-						<div class="flex justify-between items-center text-xs">
-							<div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
-							<button
-								class=" text-xs font-medium text-gray-500"
-								type="button"
-								on:click={() => {
-									showLiteLLM = !showLiteLLM;
-								}}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
-							>
-						</div>
-					</div>
-
-					{#if showLiteLLM}
-						<div>
-							<div class="flex justify-between items-center text-xs">
-								<div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
-								<button
-									class=" text-xs font-medium text-gray-500"
-									type="button"
-									on:click={() => {
-										showLiteLLMParams = !showLiteLLMParams;
-									}}
-									>{showLiteLLMParams
-										? $i18n.t('Hide Additional Params')
-										: $i18n.t('Show Additional Params')}</button
-								>
-							</div>
-						</div>
-
-						<div class="my-2 space-y-2">
-							<div class="flex w-full mb-1.5">
-								<div class="flex-1 mr-2">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-										placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
-										bind:value={liteLLMModel}
-										autocomplete="off"
-									/>
-								</div>
-
-								<button
-									class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-									on:click={() => {
-										addLiteLLMModelHandler();
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
-										/>
-									</svg>
-								</button>
-							</div>
-
-							{#if showLiteLLMParams}
-								<div>
-									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
-									<div class="flex w-full">
-										<div class="flex-1">
-											<input
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-												placeholder="Enter Model Name (model_name)"
-												bind:value={liteLLMModelName}
-												autocomplete="off"
-											/>
-										</div>
-									</div>
-								</div>
-
-								<div>
-									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
-									<div class="flex w-full">
-										<div class="flex-1">
-											<input
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-												placeholder={$i18n.t(
-													'Enter LiteLLM API Base URL (litellm_params.api_base)'
-												)}
-												bind:value={liteLLMAPIBase}
-												autocomplete="off"
-											/>
-										</div>
-									</div>
-								</div>
-
-								<div>
-									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
-									<div class="flex w-full">
-										<div class="flex-1">
-											<input
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-												placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
-												bind:value={liteLLMAPIKey}
-												autocomplete="off"
-											/>
-										</div>
-									</div>
-								</div>
-
-								<div>
-									<div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
-									<div class="flex w-full">
-										<div class="flex-1">
-											<input
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-												placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
-												bind:value={liteLLMRPM}
-												autocomplete="off"
-											/>
-										</div>
-									</div>
-								</div>
-
-								<div>
-									<div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
-									<div class="flex w-full">
-										<div class="flex-1">
-											<input
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-												placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
-												bind:value={liteLLMMaxTokens}
-												type="number"
-												min="1"
-												autocomplete="off"
-											/>
-										</div>
-									</div>
-								</div>
-							{/if}
-						</div>
-
-						<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
-							{$i18n.t('Not sure what to add?')}
-							<a
-								class=" text-gray-300 font-medium underline"
-								href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
-								target="_blank"
-							>
-								{$i18n.t('Click here for help.')}
-							</a>
-						</div>
-
-						<div>
-							<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
-							<div class="flex w-full">
-								<div class="flex-1 mr-2">
-									<select
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
-										bind:value={deleteLiteLLMModelName}
-										placeholder={$i18n.t('Select a model')}
-									>
-										{#if !deleteLiteLLMModelName}
-											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
-										{/if}
-										{#each liteLLMModelInfo as model}
-											<option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
-												>{model.model_name}</option
-											>
-										{/each}
-									</select>
-								</div>
-								<button
-									class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
-									on:click={() => {
-										deleteLiteLLMModelHandler();
-									}}
-								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								</button>
-							</div>
-						</div>
-					{/if}
-				</div>
-			</div>
-		</div>
 	</div>
 </div>

+ 1 - 1
src/lib/components/chat/SettingsModal.svelte

@@ -3,7 +3,7 @@
 	import { toast } from 'svelte-sonner';
 	import { models, settings, user } from '$lib/stores';
 
-	import { getModels as _getModels } from '$lib/utils';
+	import { getModels as _getModels } from '$lib/apis';
 
 	import Modal from '../common/Modal.svelte';
 	import Account from './Settings/Account.svelte';

+ 2 - 4
src/lib/components/chat/ShareChatModal.svelte

@@ -1,9 +1,9 @@
 <script lang="ts">
 	import { getContext, onMount } from 'svelte';
+	import { models } from '$lib/stores';
 
 	import { toast } from 'svelte-sonner';
 	import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
-	import { modelfiles } from '$lib/stores';
 	import { copyToClipboard } from '$lib/utils';
 
 	import Modal from '../common/Modal.svelte';
@@ -43,9 +43,7 @@
 					tab.postMessage(
 						JSON.stringify({
 							chat: _chat,
-							modelfiles: $modelfiles.filter((modelfile) =>
-								_chat.models.includes(modelfile.tagName)
-							)
+							models: $models.filter((m) => _chat.models.includes(m.id))
 						}),
 						'*'
 					);

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

@@ -29,6 +29,7 @@
 			dispatch('change', _state);
 		}
 	}}
+	type="button"
 >
 	<div class="top-0 left-0 absolute w-full flex justify-center">
 		{#if _state === 'checked'}

+ 2 - 1
src/lib/components/common/Tooltip.svelte

@@ -5,6 +5,7 @@
 	export let placement = 'top';
 	export let content = `I'm a tooltip!`;
 	export let touch = true;
+	export let className = 'flex';
 
 	let tooltipElement;
 	let tooltipInstance;
@@ -29,6 +30,6 @@
 	});
 </script>
 
-<div bind:this={tooltipElement} aria-label={content} class="flex">
+<div bind:this={tooltipElement} aria-label={content} class={className}>
 	<slot />
 </div>

+ 0 - 1
src/lib/components/layout/Navbar.svelte

@@ -6,7 +6,6 @@
 		WEBUI_NAME,
 		chatId,
 		mobile,
-		modelfiles,
 		settings,
 		showArchivedChats,
 		showSettings,

+ 73 - 92
src/lib/components/workspace/Modelfiles.svelte → src/lib/components/workspace/Models.svelte

@@ -5,67 +5,82 @@
 
 	import { onMount, getContext } from 'svelte';
 
-	import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
-	import { createModel, deleteModel } from '$lib/apis/ollama';
-	import {
-		createNewModelfile,
-		deleteModelfileByTagName,
-		getModelfiles
-	} from '$lib/apis/modelfiles';
+	import { WEBUI_NAME, modelfiles, models, settings, user } from '$lib/stores';
+	import { addNewModel, deleteModelById, getModelInfos } from '$lib/apis/models';
+
+	import { deleteModel } from '$lib/apis/ollama';
 	import { goto } from '$app/navigation';
 
+	import { getModels } from '$lib/apis';
+
 	const i18n = getContext('i18n');
 
 	let localModelfiles = [];
-	let importFiles;
-	let modelfilesImportInputElement: HTMLInputElement;
-	const deleteModelHandler = async (tagName) => {
-		let success = null;
 
-		success = await deleteModel(localStorage.token, tagName).catch((err) => {
-			toast.error(err);
+	let importFiles;
+	let modelsImportInputElement: HTMLInputElement;
+
+	const deleteModelHandler = async (model) => {
+		console.log(model.info);
+		if (!model?.info) {
+			toast.error(
+				$i18n.t('{{ owner }}: You cannot delete a base model', {
+					owner: model.owned_by.toUpperCase()
+				})
+			);
 			return null;
-		});
+		}
 
-		if (success) {
-			toast.success($i18n.t(`Deleted {{tagName}}`, { tagName }));
+		const res = await deleteModelById(localStorage.token, model.id);
+
+		if (res) {
+			toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
 		}
 
-		return success;
+		await models.set(await getModels(localStorage.token));
 	};
 
-	const deleteModelfile = async (tagName) => {
-		await deleteModelHandler(tagName);
-		await deleteModelfileByTagName(localStorage.token, tagName);
-		await modelfiles.set(await getModelfiles(localStorage.token));
+	const cloneModelHandler = async (model) => {
+		if ((model?.info?.base_model_id ?? null) === null) {
+			toast.error($i18n.t('You cannot clone a base model'));
+			return;
+		} else {
+			sessionStorage.model = JSON.stringify({
+				...model,
+				id: `${model.id}-clone`,
+				name: `${model.name} (Clone)`
+			});
+			goto('/workspace/models/create');
+		}
 	};
 
-	const shareModelfile = async (modelfile) => {
+	const shareModelHandler = async (model) => {
 		toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
 
 		const url = 'https://openwebui.com';
 
-		const tab = await window.open(`${url}/modelfiles/create`, '_blank');
+		const tab = await window.open(`${url}/models/create`, '_blank');
 		window.addEventListener(
 			'message',
 			(event) => {
 				if (event.origin !== url) return;
 				if (event.data === 'loaded') {
-					tab.postMessage(JSON.stringify(modelfile), '*');
+					tab.postMessage(JSON.stringify(model), '*');
 				}
 			},
 			false
 		);
 	};
 
-	const saveModelfiles = async (modelfiles) => {
-		let blob = new Blob([JSON.stringify(modelfiles)], {
+	const downloadModels = async (models) => {
+		let blob = new Blob([JSON.stringify(models)], {
 			type: 'application/json'
 		});
-		saveAs(blob, `modelfiles-export-${Date.now()}.json`);
+		saveAs(blob, `models-export-${Date.now()}.json`);
 	};
 
 	onMount(() => {
+		// Legacy code to sync localModelfiles with models
 		localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
 
 		if (localModelfiles) {
@@ -76,13 +91,13 @@
 
 <svelte:head>
 	<title>
-		{$i18n.t('Modelfiles')} | {$WEBUI_NAME}
+		{$i18n.t('Models')} | {$WEBUI_NAME}
 	</title>
 </svelte:head>
 
-<div class=" text-lg font-semibold mb-3">{$i18n.t('Modelfiles')}</div>
+<div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div>
 
-<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/modelfiles/create">
+<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create">
 	<div class=" self-center w-10">
 		<div
 			class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
@@ -98,26 +113,26 @@
 	</div>
 
 	<div class=" self-center">
-		<div class=" font-bold">{$i18n.t('Create a modelfile')}</div>
-		<div class=" text-sm">{$i18n.t('Customize Ollama models for a specific purpose')}</div>
+		<div class=" font-bold">{$i18n.t('Create a model')}</div>
+		<div class=" text-sm">{$i18n.t('Customize models for a specific purpose')}</div>
 	</div>
 </a>
 
 <hr class=" dark:border-gray-850" />
 
 <div class=" my-2 mb-5">
-	{#each $modelfiles as modelfile}
+	{#each $models as model}
 		<div
 			class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
 		>
 			<a
 				class=" flex flex-1 space-x-4 cursor-pointer w-full"
-				href={`/?models=${encodeURIComponent(modelfile.tagName)}`}
+				href={`/?models=${encodeURIComponent(model.id)}`}
 			>
 				<div class=" self-center w-10">
 					<div class=" rounded-full bg-stone-700">
 						<img
-							src={modelfile.imageUrl ?? '/user.png'}
+							src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
 							alt="modelfile profile"
 							class=" rounded-full w-full h-auto object-cover"
 						/>
@@ -125,9 +140,9 @@
 				</div>
 
 				<div class=" flex-1 self-center">
-					<div class=" font-bold capitalize">{modelfile.title}</div>
+					<div class=" font-bold line-clamp-1">{model.name}</div>
 					<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
-						{modelfile.desc}
+						{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
 					</div>
 				</div>
 			</a>
@@ -135,7 +150,7 @@
 				<a
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
-					href={`/workspace/modelfiles/edit?tag=${encodeURIComponent(modelfile.tagName)}`}
+					href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
 				>
 					<svg
 						xmlns="http://www.w3.org/2000/svg"
@@ -157,9 +172,7 @@
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
 					on:click={() => {
-						// console.log(modelfile);
-						sessionStorage.modelfile = JSON.stringify(modelfile);
-						goto('/workspace/modelfiles/create');
+						cloneModelHandler(model);
 					}}
 				>
 					<svg
@@ -182,7 +195,7 @@
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
 					on:click={() => {
-						shareModelfile(modelfile);
+						shareModelHandler(model);
 					}}
 				>
 					<svg
@@ -205,7 +218,7 @@
 					class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 					type="button"
 					on:click={() => {
-						deleteModelfile(modelfile.tagName);
+						deleteModelHandler(model);
 					}}
 				>
 					<svg
@@ -231,8 +244,8 @@
 <div class=" flex justify-end w-full mb-3">
 	<div class="flex space-x-1">
 		<input
-			id="modelfiles-import-input"
-			bind:this={modelfilesImportInputElement}
+			id="models-import-input"
+			bind:this={modelsImportInputElement}
 			bind:files={importFiles}
 			type="file"
 			accept=".json"
@@ -242,16 +255,18 @@
 
 				let reader = new FileReader();
 				reader.onload = async (event) => {
-					let savedModelfiles = JSON.parse(event.target.result);
-					console.log(savedModelfiles);
+					let savedModels = JSON.parse(event.target.result);
+					console.log(savedModels);
 
-					for (const modelfile of savedModelfiles) {
-						await createNewModelfile(localStorage.token, modelfile).catch((error) => {
-							return null;
-						});
+					for (const model of savedModels) {
+						if (model?.info ?? false) {
+							await addNewModel(localStorage.token, model.info).catch((error) => {
+								return null;
+							});
+						}
 					}
 
-					await modelfiles.set(await getModelfiles(localStorage.token));
+					await models.set(await getModels(localStorage.token));
 				};
 
 				reader.readAsText(importFiles[0]);
@@ -261,10 +276,10 @@
 		<button
 			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
 			on:click={() => {
-				modelfilesImportInputElement.click();
+				modelsImportInputElement.click();
 			}}
 		>
-			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Import Models')}</div>
 
 			<div class=" self-center">
 				<svg
@@ -285,10 +300,10 @@
 		<button
 			class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
 			on:click={async () => {
-				saveModelfiles($modelfiles);
+				downloadModels($models);
 			}}
 		>
-			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Modelfiles')}</div>
+			<div class=" self-center mr-2 font-medium">{$i18n.t('Export Models')}</div>
 
 			<div class=" self-center">
 				<svg
@@ -314,47 +329,13 @@
 			</div>
 
 			<div class="flex space-x-1">
-				<button
-					class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
-					on:click={async () => {
-						for (const modelfile of localModelfiles) {
-							await createNewModelfile(localStorage.token, modelfile).catch((error) => {
-								return null;
-							});
-						}
-
-						saveModelfiles(localModelfiles);
-						localStorage.removeItem('modelfiles');
-						localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
-						await modelfiles.set(await getModelfiles(localStorage.token));
-					}}
-				>
-					<div class=" self-center mr-2 font-medium">{$i18n.t('Sync All')}</div>
-
-					<div class=" self-center">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 16 16"
-							fill="currentColor"
-							class="w-3.5 h-3.5"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					</div>
-				</button>
-
 				<button
 					class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
 					on:click={async () => {
-						saveModelfiles(localModelfiles);
+						downloadModels(localModelfiles);
 
 						localStorage.removeItem('modelfiles');
 						localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
-						await modelfiles.set(await getModelfiles(localStorage.token));
 					}}
 				>
 					<div class=" self-center">
@@ -402,7 +383,7 @@
 		</div>
 
 		<div class=" self-center">
-			<div class=" font-bold">{$i18n.t('Discover a modelfile')}</div>
+			<div class=" font-bold">{$i18n.t('Discover a model')}</div>
 			<div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
 		</div>
 	</a>

+ 5 - 7
src/lib/components/workspace/Playground.svelte

@@ -321,13 +321,11 @@
 							<div class="max-w-full">
 								<Selector
 									placeholder={$i18n.t('Select a model')}
-									items={$models
-										.filter((model) => model.name !== 'hr')
-										.map((model) => ({
-											value: model.id,
-											label: model.name,
-											info: model
-										}))}
+									items={$models.map((model) => ({
+										value: model.id,
+										label: model.name,
+										model: model
+									}))}
 									bind:value={selectedModelId}
 								/>
 							</div>

+ 1 - 1
src/lib/constants.ts

@@ -8,7 +8,7 @@ export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
 
 export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`;
 export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
-export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
+export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai`;
 export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
 export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
 export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;

+ 14 - 0
src/lib/i18n/locales/ar-BH/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} ...يفكر",
 	"{{user}}'s Chats": "دردشات {{user}}",
 	"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
+	"A selected model does not support image input": "",
 	"a user": "مستخدم",
 	"About": "عن",
 	"Account": "الحساب",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "التعليمات المتقدمة",
 	"all": "الكل",
 	"All Documents": "جميع الملفات",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "جميع المستخدمين",
 	"Allow": "يسمح",
 	"Allow Chat Deletion": "يستطيع حذف المحادثات",
@@ -115,6 +117,7 @@
 	"Created at": "أنشئت في",
 	"Created At": "أنشئت من",
 	"Current Model": "الموديل المختار",
+	"Current Models": "",
 	"Current Password": "كلمة السر الحالية",
 	"Custom": "مخصص",
 	"Customize Ollama models for a specific purpose": "تخصيص الموديل Ollama لغرض محدد",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "أدخل LiteLLM API RPM (litllm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "أدخل LiteLLM الموديل (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "أدخل أكثر Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
 	"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
 	"Enter Score": "أدخل النتيجة",
@@ -235,6 +239,7 @@
 	"Input commands": "إدخال الأوامر",
 	"Interface": "واجهه المستخدم",
 	"Invalid Tag": "تاق غير صالحة",
+	"Is Model Vision Capable": "",
 	"January": "يناير",
 	"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
 	"Make sure to enclose them with": "تأكد من إرفاقها",
 	"Manage LiteLLM Models": "LiteLLM إدارة نماذج ",
+	"Manage Model Information": "",
 	"Manage Models": "إدارة النماذج",
 	"Manage Ollama Models": "Ollama إدارة موديلات ",
 	"March": "مارس",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
 	"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
 	"Model {{modelName}} already exists.": "موجود {{modelName}} موديل بالفعل",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "أسم الموديل",
 	"Model not selected": "لم تختار موديل",
 	"Model Tag Name": "أسم التاق للموديل",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "قم بتسمية ملف النموذج الخاص بك",
 	"New Chat": "دردشة جديدة",
 	"New Password": "كلمة المرور الجديدة",
+	"No": "",
 	"No results found": "لا توجد نتايج",
 	"No source available": "لا يوجد مصدر متاح",
 	"Not factually correct": "ليس صحيحا من حيث الواقع",
@@ -385,6 +397,7 @@
 	"Select a model": "أختار الموديل",
 	"Select an Ollama instance": "أختار سيرفر ",
 	"Select model": " أختار موديل",
+	"Selected models do not support image inputs": "",
 	"Send": "تم",
 	"Send a Message": "يُرجى إدخال طلبك هنا",
 	"Send message": "يُرجى إدخال طلبك هنا.",
@@ -492,6 +505,7 @@
 	"Workspace": "مساحة العمل",
 	"Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
+	"Yes": "",
 	"Yesterday": "أمس",
 	"You": "انت",
 	"You have no archived conversations.": "لا تملك محادثات محفوظه",

+ 14 - 0
src/lib/i18n/locales/bg-BG/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} мисли ...",
 	"{{user}}'s Chats": "{{user}}'s чатове",
 	"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
+	"A selected model does not support image input": "",
 	"a user": "потребител",
 	"About": "Относно",
 	"Account": "Акаунт",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Разширени Параметри",
 	"all": "всички",
 	"All Documents": "Всички Документи",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Всички Потребители",
 	"Allow": "Позволи",
 	"Allow Chat Deletion": "Позволи Изтриване на Чат",
@@ -115,6 +117,7 @@
 	"Created at": "Създадено на",
 	"Created At": "Създадено на",
 	"Current Model": "Текущ модел",
+	"Current Models": "",
 	"Current Password": "Текуща Парола",
 	"Custom": "Персонализиран",
 	"Customize Ollama models for a specific purpose": "Персонализиране на Ollama моделите за конкретна цел",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Въведете LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Въведете LiteLLM Model (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Въведете Max Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
 	"Enter Score": "Въведете оценка",
@@ -235,6 +239,7 @@
 	"Input commands": "Въведете команди",
 	"Interface": "Интерфейс",
 	"Invalid Tag": "Невалиден тег",
+	"Is Model Vision Capable": "",
 	"January": "Януари",
 	"join our Discord for help.": "свържете се с нашия Discord за помощ.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
 	"Make sure to enclose them with": "Уверете се, че са заключени с",
 	"Manage LiteLLM Models": "Управление на LiteLLM Моделите",
+	"Manage Model Information": "",
 	"Manage Models": "Управление на Моделите",
 	"Manage Ollama Models": "Управление на Ollama Моделите",
 	"March": "Март",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
 	"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
 	"Model {{modelName}} already exists.": "Моделът {{modelName}} вече съществува.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Открит е път до файловата система на модела. За актуализацията се изисква съкратено име на модела, не може да продължи.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Име на модел",
 	"Model not selected": "Не е избран модел",
 	"Model Tag Name": "Име на таг на модел",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Име на модфайла",
 	"New Chat": "Нов чат",
 	"New Password": "Нова парола",
+	"No": "",
 	"No results found": "Няма намерени резултати",
 	"No source available": "Няма наличен източник",
 	"Not factually correct": "Не е фактологически правилно",
@@ -385,6 +397,7 @@
 	"Select a model": "Изберете модел",
 	"Select an Ollama instance": "Изберете Ollama инстанция",
 	"Select model": "Изберете модел",
+	"Selected models do not support image inputs": "",
 	"Send": "Изпрати",
 	"Send a Message": "Изпращане на Съобщение",
 	"Send message": "Изпращане на съобщение",
@@ -492,6 +505,7 @@
 	"Workspace": "Работно пространство",
 	"Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].",
+	"Yes": "",
 	"Yesterday": "вчера",
 	"You": "вие",
 	"You have no archived conversations.": "Нямате архивирани разговори.",

+ 14 - 0
src/lib/i18n/locales/bn-BD/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} চিন্তা করছে...",
 	"{{user}}'s Chats": "{{user}}র চ্যাটস",
 	"{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক",
+	"A selected model does not support image input": "",
 	"a user": "একজন ব্যাবহারকারী",
 	"About": "সম্পর্কে",
 	"Account": "একাউন্ট",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "এডভান্সড প্যারামিটার্স",
 	"all": "সব",
 	"All Documents": "সব ডকুমেন্ট",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "সব ইউজার",
 	"Allow": "অনুমোদন",
 	"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
@@ -115,6 +117,7 @@
 	"Created at": "নির্মানকাল",
 	"Created At": "নির্মানকাল",
 	"Current Model": "বর্তমান মডেল",
+	"Current Models": "",
 	"Current Password": "বর্তমান পাসওয়ার্ড",
 	"Custom": "কাস্টম",
 	"Customize Ollama models for a specific purpose": "নির্দিষ্ট উদ্দেশ্যে Ollama মডেল পরিবর্তন করুন",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM এপিআই RPM দিন (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM মডেল দিন (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "সর্বোচ্চ টোকেন সংখ্যা দিন (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
 	"Enter Score": "স্কোর দিন",
@@ -235,6 +239,7 @@
 	"Input commands": "ইনপুট কমান্ডস",
 	"Interface": "ইন্টারফেস",
 	"Invalid Tag": "অবৈধ ট্যাগ",
+	"Is Model Vision Capable": "",
 	"January": "জানুয়ারী",
 	"join our Discord for help.": "সাহায্যের জন্য আমাদের Discord-এ যুক্ত হোন",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
 	"Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না",
 	"Manage LiteLLM Models": "LiteLLM মডেল ব্যবস্থাপনা করুন",
+	"Manage Model Information": "",
 	"Manage Models": "মডেলসমূহ ব্যবস্থাপনা করুন",
 	"Manage Ollama Models": "Ollama মডেলসূহ ব্যবস্থাপনা করুন",
 	"March": "মার্চ",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "{{modelTag}} ডাউনলোডের জন্য আগে থেকেই অপেক্ষমান আছে।",
 	"Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি",
 	"Model {{modelName}} already exists.": "{{modelName}} মডেল আগে থেকেই আছে",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "মডেল ফাইলসিস্টেম পাথ পাওয়া গেছে। আপডেটের জন্য মডেলের শর্টনেম আবশ্যক, এগিয়ে যাওয়া যাচ্ছে না।",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "মডেলের নাম",
 	"Model not selected": "মডেল নির্বাচন করা হয়নি",
 	"Model Tag Name": "মডেলের ট্যাগ নাম",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "আপনার মডেলফাইলের নাম দিন",
 	"New Chat": "নতুন চ্যাট",
 	"New Password": "নতুন পাসওয়ার্ড",
+	"No": "",
 	"No results found": "কোন ফলাফল পাওয়া যায়নি",
 	"No source available": "কোন উৎস পাওয়া যায়নি",
 	"Not factually correct": "তথ্যগত দিক থেকে সঠিক নয়",
@@ -385,6 +397,7 @@
 	"Select a model": "একটি মডেল নির্বাচন করুন",
 	"Select an Ollama instance": "একটি Ollama ইন্সট্যান্স নির্বাচন করুন",
 	"Select model": "মডেল নির্বাচন করুন",
+	"Selected models do not support image inputs": "",
 	"Send": "পাঠান",
 	"Send a Message": "একটি মেসেজ পাঠান",
 	"Send message": "মেসেজ পাঠান",
@@ -492,6 +505,7 @@
 	"Workspace": "ওয়ার্কস্পেস",
 	"Write a prompt suggestion (e.g. Who are you?)": "একটি প্রম্পট সাজেশন লিখুন (যেমন Who are you?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "৫০ শব্দের মধ্যে [topic or keyword] এর একটি সারসংক্ষেপ লিখুন।",
+	"Yes": "",
 	"Yesterday": "আগামী",
 	"You": "আপনি",
 	"You have no archived conversations.": "আপনার কোনও আর্কাইভ করা কথোপকথন নেই।",

+ 14 - 0
src/lib/i18n/locales/ca-ES/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} està pensant...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "Es requereix Backend de {{webUIName}}",
+	"A selected model does not support image input": "",
 	"a user": "un usuari",
 	"About": "Sobre",
 	"Account": "Compte",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Paràmetres Avançats",
 	"all": "tots",
 	"All Documents": "Tots els Documents",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Tots els Usuaris",
 	"Allow": "Permet",
 	"Allow Chat Deletion": "Permet la Supressió del Xat",
@@ -115,6 +117,7 @@
 	"Created at": "Creat el",
 	"Created At": "Creat el",
 	"Current Model": "Model Actual",
+	"Current Models": "",
 	"Current Password": "Contrasenya Actual",
 	"Custom": "Personalitzat",
 	"Customize Ollama models for a specific purpose": "Personalitza els models Ollama per a un propòsit específic",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Introdueix RPM de LiteLLM API (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Introdueix el Model de LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Introdueix el Màxim de Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Introdueix el Nombre de Passos (p. ex. 50)",
 	"Enter Score": "Introdueix el Puntuació",
@@ -235,6 +239,7 @@
 	"Input commands": "Entra ordres",
 	"Interface": "Interfície",
 	"Invalid Tag": "Etiqueta Inválida",
+	"Is Model Vision Capable": "",
 	"January": "Gener",
 	"join our Discord for help.": "uneix-te al nostre Discord per ajuda.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
 	"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
 	"Manage LiteLLM Models": "Gestiona Models LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gestiona Models",
 	"Manage Ollama Models": "Gestiona Models Ollama",
 	"March": "Març",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
 	"Model {{modelId}} not found": "Model {{modelId}} no trobat",
 	"Model {{modelName}} already exists.": "El model {{modelName}} ja existeix.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "S'ha detectat el camí del sistema de fitxers del model. És necessari un nom curt del model per a actualitzar, no es pot continuar.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nom del Model",
 	"Model not selected": "Model no seleccionat",
 	"Model Tag Name": "Nom de l'Etiqueta del Model",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nomena el teu fitxer de model",
 	"New Chat": "Xat Nou",
 	"New Password": "Nova Contrasenya",
+	"No": "",
 	"No results found": "No s'han trobat resultats",
 	"No source available": "Sense font disponible",
 	"Not factually correct": "No està clarament correcte",
@@ -385,6 +397,7 @@
 	"Select a model": "Selecciona un model",
 	"Select an Ollama instance": "Selecciona una instància d'Ollama",
 	"Select model": "Selecciona un model",
+	"Selected models do not support image inputs": "",
 	"Send": "Envia",
 	"Send a Message": "Envia un Missatge",
 	"Send message": "Envia missatge",
@@ -492,6 +505,7 @@
 	"Workspace": "Treball",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència de prompt (p. ex. Qui ets tu?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
+	"Yes": "",
 	"Yesterday": "Ayer",
 	"You": "Tu",
 	"You have no archived conversations.": "No tens converses arxivades.",

+ 14 - 0
src/lib/i18n/locales/de-DE/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} denkt nach...",
 	"{{user}}'s Chats": "{{user}}s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
+	"A selected model does not support image input": "",
 	"a user": "ein Benutzer",
 	"About": "Über",
 	"Account": "Account",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Erweiterte Parameter",
 	"all": "Alle",
 	"All Documents": "Alle Dokumente",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Alle Benutzer",
 	"Allow": "Erlauben",
 	"Allow Chat Deletion": "Chat Löschung erlauben",
@@ -115,6 +117,7 @@
 	"Created at": "Erstellt am",
 	"Created At": "Erstellt am",
 	"Current Model": "Aktuelles Modell",
+	"Current Models": "",
 	"Current Password": "Aktuelles Passwort",
 	"Custom": "Benutzerdefiniert",
 	"Customize Ollama models for a specific purpose": "Ollama-Modelle für einen bestimmten Zweck anpassen",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Gib die LiteLLM API RPM ein (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Gib das LiteLLM Model ein (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Gib die maximalen Token ein (litellm_params.max_tokens) an",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Gib den Model-Tag ein",
 	"Enter Number of Steps (e.g. 50)": "Gib die Anzahl an Schritten ein (z.B. 50)",
 	"Enter Score": "Score eingeben",
@@ -235,6 +239,7 @@
 	"Input commands": "Eingabebefehle",
 	"Interface": "Benutzeroberfläche",
 	"Invalid Tag": "Ungültiger Tag",
+	"Is Model Vision Capable": "",
 	"January": "Januar",
 	"join our Discord for help.": "Trete unserem Discord bei, um Hilfe zu erhalten.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Von der OpenWebUI-Community",
 	"Make sure to enclose them with": "Formatiere deine Variablen mit:",
 	"Manage LiteLLM Models": "LiteLLM-Modelle verwalten",
+	"Manage Model Information": "",
 	"Manage Models": "Modelle verwalten",
 	"Manage Ollama Models": "Ollama-Modelle verwalten",
 	"March": "März",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.",
 	"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
 	"Model {{modelName}} already exists.": "Modell {{modelName}} existiert bereits.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modell-Dateisystempfad erkannt. Modellkurzname ist für das Update erforderlich, Fortsetzung nicht möglich.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Modellname",
 	"Model not selected": "Modell nicht ausgewählt",
 	"Model Tag Name": "Modell-Tag-Name",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Benenne dein modelfile",
 	"New Chat": "Neuer Chat",
 	"New Password": "Neues Passwort",
+	"No": "",
 	"No results found": "Keine Ergebnisse gefunden",
 	"No source available": "Keine Quelle verfügbar.",
 	"Not factually correct": "Nicht sachlich korrekt.",
@@ -385,6 +397,7 @@
 	"Select a model": "Ein Modell auswählen",
 	"Select an Ollama instance": "Eine Ollama Instanz auswählen",
 	"Select model": "Modell auswählen",
+	"Selected models do not support image inputs": "",
 	"Send": "Senden",
 	"Send a Message": "Eine Nachricht senden",
 	"Send message": "Nachricht senden",
@@ -492,6 +505,7 @@
 	"Workspace": "Arbeitsbereich",
 	"Write a prompt suggestion (e.g. Who are you?)": "Gebe einen Prompt-Vorschlag ein (z.B. Wer bist du?)",
 	"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.",
+	"Yes": "",
 	"Yesterday": "Gestern",
 	"You": "Du",
 	"You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.",

+ 14 - 0
src/lib/i18n/locales/dg-DG/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} is thinkin'...",
 	"{{user}}'s Chats": "",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Much Required",
+	"A selected model does not support image input": "",
 	"a user": "such user",
 	"About": "Much About",
 	"Account": "Account",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Advanced Parameters",
 	"all": "all",
 	"All Documents": "",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "All Users",
 	"Allow": "Allow",
 	"Allow Chat Deletion": "Allow Delete Chats",
@@ -115,6 +117,7 @@
 	"Created at": "Created at",
 	"Created At": "",
 	"Current Model": "Current Model",
+	"Current Models": "",
 	"Current Password": "Current Password",
 	"Custom": "Custom",
 	"Customize Ollama models for a specific purpose": "Customize Ollama models for purpose",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Enter RPM of LiteLLM API (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Enter Model of LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Enter Maximum Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)",
 	"Enter Score": "",
@@ -235,6 +239,7 @@
 	"Input commands": "Input commands",
 	"Interface": "Interface",
 	"Invalid Tag": "",
+	"Is Model Vision Capable": "",
 	"January": "",
 	"join our Discord for help.": "join our Discord for help.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Made by OpenWebUI Community",
 	"Make sure to enclose them with": "Make sure to enclose them with",
 	"Manage LiteLLM Models": "Manage LiteLLM Models",
+	"Manage Model Information": "",
 	"Manage Models": "Manage Wowdels",
 	"Manage Ollama Models": "Manage Ollama Wowdels",
 	"March": "",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.",
 	"Model {{modelId}} not found": "Model {{modelId}} not found",
 	"Model {{modelName}} already exists.": "Model {{modelName}} already exists.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem bark detected. Model shortname is required for update, cannot continue.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Wowdel Name",
 	"Model not selected": "Model not selected",
 	"Model Tag Name": "Wowdel Tag Name",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Name your modelfile",
 	"New Chat": "New Bark",
 	"New Password": "New Barkword",
+	"No": "",
 	"No results found": "",
 	"No source available": "No source available",
 	"Not factually correct": "",
@@ -385,6 +397,7 @@
 	"Select a model": "Select a model much choice",
 	"Select an Ollama instance": "Select an Ollama instance very choose",
 	"Select model": "Select model much choice",
+	"Selected models do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "Send a Message much message",
 	"Send message": "Send message very send",
@@ -492,6 +505,7 @@
 	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?) much suggest",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword]. Much summarize.",
+	"Yes": "",
 	"Yesterday": "",
 	"You": "",
 	"You have no archived conversations.": "",

+ 14 - 0
src/lib/i18n/locales/en-GB/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "",
 	"{{user}}'s Chats": "",
 	"{{webUIName}} Backend Required": "",
+	"A selected model does not support image input": "",
 	"a user": "",
 	"About": "",
 	"Account": "",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "",
 	"all": "",
 	"All Documents": "",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "",
 	"Allow": "",
 	"Allow Chat Deletion": "",
@@ -115,6 +117,7 @@
 	"Created at": "",
 	"Created At": "",
 	"Current Model": "",
+	"Current Models": "",
 	"Current Password": "",
 	"Custom": "",
 	"Customize Ollama models for a specific purpose": "",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "",
 	"Enter LiteLLM Model (litellm_params.model)": "",
 	"Enter Max Tokens (litellm_params.max_tokens)": "",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "",
 	"Enter Number of Steps (e.g. 50)": "",
 	"Enter Score": "",
@@ -235,6 +239,7 @@
 	"Input commands": "",
 	"Interface": "",
 	"Invalid Tag": "",
+	"Is Model Vision Capable": "",
 	"January": "",
 	"join our Discord for help.": "",
 	"JSON": "",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Manage LiteLLM Models": "",
+	"Manage Model Information": "",
 	"Manage Models": "",
 	"Manage Ollama Models": "",
 	"March": "",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "",
 	"Model {{modelId}} not found": "",
 	"Model {{modelName}} already exists.": "",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "",
 	"Model not selected": "",
 	"Model Tag Name": "",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "",
 	"New Chat": "",
 	"New Password": "",
+	"No": "",
 	"No results found": "",
 	"No source available": "",
 	"Not factually correct": "",
@@ -385,6 +397,7 @@
 	"Select a model": "",
 	"Select an Ollama instance": "",
 	"Select model": "",
+	"Selected models do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "",
 	"Send message": "",
@@ -492,6 +505,7 @@
 	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "",
+	"Yes": "",
 	"Yesterday": "",
 	"You": "",
 	"You have no archived conversations.": "",

+ 14 - 0
src/lib/i18n/locales/en-US/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "",
 	"{{user}}'s Chats": "",
 	"{{webUIName}} Backend Required": "",
+	"A selected model does not support image input": "",
 	"a user": "",
 	"About": "",
 	"Account": "",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "",
 	"all": "",
 	"All Documents": "",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "",
 	"Allow": "",
 	"Allow Chat Deletion": "",
@@ -115,6 +117,7 @@
 	"Created at": "",
 	"Created At": "",
 	"Current Model": "",
+	"Current Models": "",
 	"Current Password": "",
 	"Custom": "",
 	"Customize Ollama models for a specific purpose": "",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "",
 	"Enter LiteLLM Model (litellm_params.model)": "",
 	"Enter Max Tokens (litellm_params.max_tokens)": "",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "",
 	"Enter Number of Steps (e.g. 50)": "",
 	"Enter Score": "",
@@ -235,6 +239,7 @@
 	"Input commands": "",
 	"Interface": "",
 	"Invalid Tag": "",
+	"Is Model Vision Capable": "",
 	"January": "",
 	"join our Discord for help.": "",
 	"JSON": "",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "",
 	"Make sure to enclose them with": "",
 	"Manage LiteLLM Models": "",
+	"Manage Model Information": "",
 	"Manage Models": "",
 	"Manage Ollama Models": "",
 	"March": "",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "",
 	"Model {{modelId}} not found": "",
 	"Model {{modelName}} already exists.": "",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "",
 	"Model not selected": "",
 	"Model Tag Name": "",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "",
 	"New Chat": "",
 	"New Password": "",
+	"No": "",
 	"No results found": "",
 	"No source available": "",
 	"Not factually correct": "",
@@ -385,6 +397,7 @@
 	"Select a model": "",
 	"Select an Ollama instance": "",
 	"Select model": "",
+	"Selected models do not support image inputs": "",
 	"Send": "",
 	"Send a Message": "",
 	"Send message": "",
@@ -492,6 +505,7 @@
 	"Workspace": "",
 	"Write a prompt suggestion (e.g. Who are you?)": "",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "",
+	"Yes": "",
 	"Yesterday": "",
 	"You": "",
 	"You have no archived conversations.": "",

+ 14 - 0
src/lib/i18n/locales/es-ES/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} está pensando...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Servidor Requerido",
+	"A selected model does not support image input": "",
 	"a user": "un usuario",
 	"About": "Sobre nosotros",
 	"Account": "Cuenta",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Parámetros Avanzados",
 	"all": "todo",
 	"All Documents": "Todos los Documentos",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Todos los Usuarios",
 	"Allow": "Permitir",
 	"Allow Chat Deletion": "Permitir Borrar Chats",
@@ -115,6 +117,7 @@
 	"Created at": "Creado en",
 	"Created At": "Creado en",
 	"Current Model": "Modelo Actual",
+	"Current Models": "",
 	"Current Password": "Contraseña Actual",
 	"Custom": "Personalizado",
 	"Customize Ollama models for a specific purpose": "Personaliza los modelos de Ollama para un propósito específico",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Ingrese el RPM de la API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Ingrese el modelo LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Ingrese tokens máximos (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)",
 	"Enter Score": "Ingrese la puntuación",
@@ -235,6 +239,7 @@
 	"Input commands": "Ingresar comandos",
 	"Interface": "Interfaz",
 	"Invalid Tag": "Etiqueta Inválida",
+	"Is Model Vision Capable": "",
 	"January": "Enero",
 	"join our Discord for help.": "Únase a nuestro Discord para obtener ayuda.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Hecho por la comunidad de OpenWebUI",
 	"Make sure to enclose them with": "Asegúrese de adjuntarlos con",
 	"Manage LiteLLM Models": "Administrar Modelos LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Administrar Modelos",
 	"Manage Ollama Models": "Administrar Modelos Ollama",
 	"March": "Marzo",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "El modelo '{{modelTag}}' ya está en cola para descargar.",
 	"Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado",
 	"Model {{modelName}} already exists.": "El modelo {{modelName}} ya existe.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nombre del modelo",
 	"Model not selected": "Modelo no seleccionado",
 	"Model Tag Name": "Nombre de la etiqueta del modelo",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nombra tu modelfile",
 	"New Chat": "Nuevo Chat",
 	"New Password": "Nueva Contraseña",
+	"No": "",
 	"No results found": "No se han encontrado resultados",
 	"No source available": "No hay fuente disponible",
 	"Not factually correct": "No es correcto en todos los aspectos",
@@ -385,6 +397,7 @@
 	"Select a model": "Selecciona un modelo",
 	"Select an Ollama instance": "Seleccione una instancia de Ollama",
 	"Select model": "Selecciona un modelo",
+	"Selected models do not support image inputs": "",
 	"Send": "Enviar",
 	"Send a Message": "Enviar un Mensaje",
 	"Send message": "Enviar Mensaje",
@@ -492,6 +505,7 @@
 	"Workspace": "Espacio de trabajo",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escribe una sugerencia para un prompt (por ejemplo, ¿quién eres?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escribe un resumen en 50 palabras que resuma [tema o palabra clave].",
+	"Yes": "",
 	"Yesterday": "Ayer",
 	"You": "Usted",
 	"You have no archived conversations.": "No tiene conversaciones archivadas.",

+ 14 - 0
src/lib/i18n/locales/fa-IR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} در حال فکر کردن است...",
 	"{{user}}'s Chats": "{{user}} چت ها",
 	"{{webUIName}} Backend Required": "بکند {{webUIName}} نیاز است.",
+	"A selected model does not support image input": "",
 	"a user": "یک کاربر",
 	"About": "درباره",
 	"Account": "حساب کاربری",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "پارامترهای پیشرفته",
 	"all": "همه",
 	"All Documents": "تمام سند ها",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "همه کاربران",
 	"Allow": "اجازه دادن",
 	"Allow Chat Deletion": "اجازه حذف گپ",
@@ -115,6 +117,7 @@
 	"Created at": "ایجاد شده در",
 	"Created At": "ایجاد شده در",
 	"Current Model": "مدل فعلی",
+	"Current Models": "",
 	"Current Password": "رمز عبور فعلی",
 	"Custom": "دلخواه",
 	"Customize Ollama models for a specific purpose": "مدل های اولاما را برای یک هدف خاص سفارشی کنید",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "RPM API مربوط به LiteLLM را وارد کنید (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "مدل مربوط به LiteLLM را وارد کنید (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "حداکثر تعداد توکن را وارد کنید (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "تگ مدل را وارد کنید (مثلا {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "تعداد گام ها را وارد کنید (مثال: 50)",
 	"Enter Score": "امتیاز را وارد کنید",
@@ -235,6 +239,7 @@
 	"Input commands": "ورودی دستورات",
 	"Interface": "رابط",
 	"Invalid Tag": "تگ نامعتبر",
+	"Is Model Vision Capable": "",
 	"January": "ژانویه",
 	"join our Discord for help.": "برای کمک به دیسکورد ما بپیوندید.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "ساخته شده توسط OpenWebUI Community",
 	"Make sure to enclose them with": "مطمئن شوید که آنها را با این محصور کنید:",
 	"Manage LiteLLM Models": "Manage LiteLLM Models",
+	"Manage Model Information": "",
 	"Manage Models": "مدیریت مدل\u200cها",
 	"Manage Ollama Models": "مدیریت مدل\u200cهای اولاما",
 	"March": "مارچ",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "مدل '{{modelTag}}' در حال حاضر در صف برای دانلود است.",
 	"Model {{modelId}} not found": "مدل {{modelId}} یافت نشد",
 	"Model {{modelName}} already exists.": "مدل {{modelName}} در حال حاضر وجود دارد.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "مسیر فایل سیستم مدل یافت شد. برای بروزرسانی نیاز است نام کوتاه مدل وجود داشته باشد.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "نام مدل",
 	"Model not selected": "مدل انتخاب نشده",
 	"Model Tag Name": "نام تگ مدل",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "فایل مدل را نام\u200cگذاری کنید",
 	"New Chat": "گپ جدید",
 	"New Password": "رمز عبور جدید",
+	"No": "",
 	"No results found": "نتیجه\u200cای یافت نشد",
 	"No source available": "منبعی در دسترس نیست",
 	"Not factually correct": "اشتباهی فکری نیست",
@@ -385,6 +397,7 @@
 	"Select a model": "انتخاب یک مدل",
 	"Select an Ollama instance": "انتخاب یک نمونه از اولاما",
 	"Select model": "انتخاب یک مدل",
+	"Selected models do not support image inputs": "",
 	"Send": "ارسال",
 	"Send a Message": "ارسال یک پیام",
 	"Send message": "ارسال پیام",
@@ -492,6 +505,7 @@
 	"Workspace": "محیط کار",
 	"Write a prompt suggestion (e.g. Who are you?)": "یک پیشنهاد پرامپت بنویسید (مثلاً شما کی هستید؟)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "خلاصه ای در 50 کلمه بنویسید که [موضوع یا کلمه کلیدی] را خلاصه کند.",
+	"Yes": "",
 	"Yesterday": "دیروز",
 	"You": "شما",
 	"You have no archived conversations.": "شما هیچ گفتگوی ذخیره شده ندارید.",

+ 14 - 0
src/lib/i18n/locales/fi-FI/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} miettii...",
 	"{{user}}'s Chats": "{{user}}:n keskustelut",
 	"{{webUIName}} Backend Required": "{{webUIName}} backend vaaditaan",
+	"A selected model does not support image input": "",
 	"a user": "käyttäjä",
 	"About": "Tietoja",
 	"Account": "Tili",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Edistyneet parametrit",
 	"all": "kaikki",
 	"All Documents": "Kaikki asiakirjat",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Kaikki käyttäjät",
 	"Allow": "Salli",
 	"Allow Chat Deletion": "Salli keskustelujen poisto",
@@ -115,6 +117,7 @@
 	"Created at": "Luotu",
 	"Created At": "Luotu",
 	"Current Model": "Nykyinen malli",
+	"Current Models": "",
 	"Current Password": "Nykyinen salasana",
 	"Custom": "Mukautettu",
 	"Customize Ollama models for a specific purpose": "Mukauta Ollama-malleja tiettyyn tarkoitukseen",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Syötä LiteLLM-APIn RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Syötä LiteLLM-malli (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Syötä maksimitokenit (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Syötä mallitagi (esim. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Syötä askelien määrä (esim. 50)",
 	"Enter Score": "Syötä pisteet",
@@ -235,6 +239,7 @@
 	"Input commands": "Syötä komennot",
 	"Interface": "Käyttöliittymä",
 	"Invalid Tag": "Virheellinen tagi",
+	"Is Model Vision Capable": "",
 	"January": "tammikuu",
 	"join our Discord for help.": "liity Discordiimme saadaksesi apua.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Tehnyt OpenWebUI-yhteisö",
 	"Make sure to enclose them with": "Varmista, että suljet ne",
 	"Manage LiteLLM Models": "Hallitse LiteLLM-malleja",
+	"Manage Model Information": "",
 	"Manage Models": "Hallitse malleja",
 	"Manage Ollama Models": "Hallitse Ollama-malleja",
 	"March": "maaliskuu",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Malli '{{modelTag}}' on jo jonossa ladattavaksi.",
 	"Model {{modelId}} not found": "Mallia {{modelId}} ei löytynyt",
 	"Model {{modelName}} already exists.": "Malli {{modelName}} on jo olemassa.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Mallin tiedostojärjestelmäpolku havaittu. Mallin lyhytnimi vaaditaan päivitykseen, ei voi jatkaa.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Mallin nimi",
 	"Model not selected": "Mallia ei valittu",
 	"Model Tag Name": "Mallitagin nimi",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nimeä mallitiedostosi",
 	"New Chat": "Uusi keskustelu",
 	"New Password": "Uusi salasana",
+	"No": "",
 	"No results found": "Ei tuloksia",
 	"No source available": "Ei lähdettä saatavilla",
 	"Not factually correct": "Ei faktisesti oikein",
@@ -385,6 +397,7 @@
 	"Select a model": "Valitse malli",
 	"Select an Ollama instance": "Valitse Ollama-instanssi",
 	"Select model": "Valitse malli",
+	"Selected models do not support image inputs": "",
 	"Send": "Lähetä",
 	"Send a Message": "Lähetä viesti",
 	"Send message": "Lähetä viesti",
@@ -492,6 +505,7 @@
 	"Workspace": "Työtilat",
 	"Write a prompt suggestion (e.g. Who are you?)": "Kirjoita ehdotettu kehote (esim. Kuka olet?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Kirjoita 50 sanan yhteenveto, joka tiivistää [aihe tai avainsana].",
+	"Yes": "",
 	"Yesterday": "Eilen",
 	"You": "Sinä",
 	"You have no archived conversations.": "Sinulla ei ole arkistoituja keskusteluja.",

+ 14 - 0
src/lib/i18n/locales/fr-CA/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} réfléchit...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
+	"A selected model does not support image input": "",
 	"a user": "un utilisateur",
 	"About": "À propos",
 	"Account": "Compte",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Paramètres avancés",
 	"all": "tous",
 	"All Documents": "Tous les documents",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Tous les utilisateurs",
 	"Allow": "Autoriser",
 	"Allow Chat Deletion": "Autoriser la suppression des discussions",
@@ -115,6 +117,7 @@
 	"Created at": "Créé le",
 	"Created At": "Créé le",
 	"Current Model": "Modèle actuel",
+	"Current Models": "",
 	"Current Password": "Mot de passe actuel",
 	"Custom": "Personnalisé",
 	"Customize Ollama models for a specific purpose": "Personnaliser les modèles Ollama pour un objectif spécifique",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Entrez le RPM de l'API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Entrez le modèle LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Entrez le nombre max de tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Entrez le tag du modèle (p. ex. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Entrez le nombre d'étapes (p. ex. 50)",
 	"Enter Score": "Entrez le score",
@@ -235,6 +239,7 @@
 	"Input commands": "Entrez des commandes d'entrée",
 	"Interface": "Interface",
 	"Invalid Tag": "Tag invalide",
+	"Is Model Vision Capable": "",
 	"January": "Janvier",
 	"join our Discord for help.": "rejoignez notre Discord pour obtenir de l'aide.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les entourer avec",
 	"Manage LiteLLM Models": "Gérer les modèles LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gérer les modèles",
 	"Manage Ollama Models": "Gérer les modèles Ollama",
 	"March": "Mars",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Le modèle '{{modelTag}}' est déjà dans la file d'attente pour le téléchargement.",
 	"Model {{modelId}} not found": "Modèle {{modelId}} non trouvé",
 	"Model {{modelName}} already exists.": "Le modèle {{modelName}} existe déjà.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Le chemin du système de fichiers du modèle a été détecté. Le nom court du modèle est nécessaire pour la mise à jour, impossible de continuer.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nom du modèle",
 	"Model not selected": "Modèle non sélectionné",
 	"Model Tag Name": "Nom de tag du modèle",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nommez votre fichier de modèle",
 	"New Chat": "Nouvelle discussion",
 	"New Password": "Nouveau mot de passe",
+	"No": "",
 	"No results found": "Aucun résultat trouvé",
 	"No source available": "Aucune source disponible",
 	"Not factually correct": "Non, pas exactement correct",
@@ -385,6 +397,7 @@
 	"Select a model": "Sélectionnez un modèle",
 	"Select an Ollama instance": "Sélectionner une instance Ollama",
 	"Select model": "Sélectionnez un modèle",
+	"Selected models do not support image inputs": "",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
@@ -492,6 +505,7 @@
 	"Workspace": "Espace de travail",
 	"Write a prompt suggestion (e.g. Who are you?)": "Rédigez une suggestion de prompt (p. ex. Qui êtes-vous ?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Rédigez un résumé en 50 mots qui résume [sujet ou mot-clé].",
+	"Yes": "",
 	"Yesterday": "hier",
 	"You": "Vous",
 	"You have no archived conversations.": "Vous n'avez aucune conversation archivée.",

+ 14 - 0
src/lib/i18n/locales/fr-FR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} réfléchit...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
+	"A selected model does not support image input": "",
 	"a user": "un utilisateur",
 	"About": "À propos",
 	"Account": "Compte",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Paramètres avancés",
 	"all": "tous",
 	"All Documents": "Tous les documents",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Tous les utilisateurs",
 	"Allow": "Autoriser",
 	"Allow Chat Deletion": "Autoriser la suppression du chat",
@@ -115,6 +117,7 @@
 	"Created at": "Créé le",
 	"Created At": "Créé le",
 	"Current Model": "Modèle actuel",
+	"Current Models": "",
 	"Current Password": "Mot de passe actuel",
 	"Custom": "Personnalisé",
 	"Customize Ollama models for a specific purpose": "Personnaliser les modèles Ollama pour un objectif spécifique",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Entrez le RPM de l'API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Entrez le modèle LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Entrez le nombre max de tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Entrez le tag du modèle (p. ex. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Entrez le nombre d'étapes (p. ex. 50)",
 	"Enter Score": "Entrez le score",
@@ -235,6 +239,7 @@
 	"Input commands": "Entrez les commandes d'entrée",
 	"Interface": "Interface",
 	"Invalid Tag": "Tag invalide",
+	"Is Model Vision Capable": "",
 	"January": "Janvier",
 	"join our Discord for help.": "rejoignez notre Discord pour obtenir de l'aide.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI",
 	"Make sure to enclose them with": "Assurez-vous de les entourer avec",
 	"Manage LiteLLM Models": "Gérer les modèles LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gérer les modèles",
 	"Manage Ollama Models": "Gérer les modèles Ollama",
 	"March": "Mars",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Le modèle '{{modelTag}}' est déjà dans la file d'attente pour le téléchargement.",
 	"Model {{modelId}} not found": "Modèle {{modelId}} non trouvé",
 	"Model {{modelName}} already exists.": "Le modèle {{modelName}} existe déjà.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Le chemin du système de fichiers du modèle a été détecté. Le nom court du modèle est nécessaire pour la mise à jour, impossible de continuer.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nom du modèle",
 	"Model not selected": "Modèle non sélectionné",
 	"Model Tag Name": "Nom de tag du modèle",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nommez votre fichier de modèle",
 	"New Chat": "Nouveau chat",
 	"New Password": "Nouveau mot de passe",
+	"No": "",
 	"No results found": "Aucun résultat trouvé",
 	"No source available": "Aucune source disponible",
 	"Not factually correct": "Non, pas exactement correct",
@@ -385,6 +397,7 @@
 	"Select a model": "Sélectionner un modèle",
 	"Select an Ollama instance": "Sélectionner une instance Ollama",
 	"Select model": "Sélectionner un modèle",
+	"Selected models do not support image inputs": "",
 	"Send": "Envoyer",
 	"Send a Message": "Envoyer un message",
 	"Send message": "Envoyer un message",
@@ -492,6 +505,7 @@
 	"Workspace": "Espace de travail",
 	"Write a prompt suggestion (e.g. Who are you?)": "Écrivez un prompt (e.x. Qui est-tu ?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Ecrivez un résumé en 50 mots [sujet ou mot-clé]",
+	"Yes": "",
 	"Yesterday": "hier",
 	"You": "Vous",
 	"You have no archived conversations.": "Vous n'avez aucune conversation archivée.",

+ 14 - 0
src/lib/i18n/locales/he-IL/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} חושב...",
 	"{{user}}'s Chats": "צ'אטים של {{user}}",
 	"{{webUIName}} Backend Required": "נדרש Backend של {{webUIName}}",
+	"A selected model does not support image input": "",
 	"a user": "משתמש",
 	"About": "אודות",
 	"Account": "חשבון",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "פרמטרים מתקדמים",
 	"all": "הכל",
 	"All Documents": "כל המסמכים",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "כל המשתמשים",
 	"Allow": "אפשר",
 	"Allow Chat Deletion": "אפשר מחיקת צ'אט",
@@ -115,6 +117,7 @@
 	"Created at": "נוצר ב",
 	"Created At": "נוצר ב",
 	"Current Model": "המודל הנוכחי",
+	"Current Models": "",
 	"Current Password": "הסיסמה הנוכחית",
 	"Custom": "מותאם אישית",
 	"Customize Ollama models for a specific purpose": "התאמה אישית של מודלים של Ollama למטרה מסוימת",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "הזן RPM של API של LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "הזן מודל LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "הזן מספר מקסימלי של טוקנים (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "הזן תג מודל (למשל {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "הזן מספר שלבים (למשל 50)",
 	"Enter Score": "הזן ציון",
@@ -235,6 +239,7 @@
 	"Input commands": "פקודות קלט",
 	"Interface": "ממשק",
 	"Invalid Tag": "תג לא חוקי",
+	"Is Model Vision Capable": "",
 	"January": "ינואר",
 	"join our Discord for help.": "הצטרף ל-Discord שלנו לעזרה.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "נוצר על ידי קהילת OpenWebUI",
 	"Make sure to enclose them with": "ודא להקיף אותם עם",
 	"Manage LiteLLM Models": "נהל מודלים של LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "נהל מודלים",
 	"Manage Ollama Models": "נהל מודלים של Ollama",
 	"March": "מרץ",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "המודל '{{modelTag}}' כבר בתור להורדה.",
 	"Model {{modelId}} not found": "המודל {{modelId}} לא נמצא",
 	"Model {{modelName}} already exists.": "המודל {{modelName}} כבר קיים.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "נתיב מערכת הקבצים של המודל זוהה. נדרש שם קצר של המודל לעדכון, לא ניתן להמשיך.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "שם המודל",
 	"Model not selected": "לא נבחר מודל",
 	"Model Tag Name": "שם תג המודל",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "תן שם לקובץ המודל שלך",
 	"New Chat": "צ'אט חדש",
 	"New Password": "סיסמה חדשה",
+	"No": "",
 	"No results found": "לא נמצאו תוצאות",
 	"No source available": "אין מקור זמין",
 	"Not factually correct": "לא נכון מבחינה עובדתית",
@@ -385,6 +397,7 @@
 	"Select a model": "בחר מודל",
 	"Select an Ollama instance": "בחר מופע של Ollama",
 	"Select model": "בחר מודל",
+	"Selected models do not support image inputs": "",
 	"Send": "שלח",
 	"Send a Message": "שלח הודעה",
 	"Send message": "שלח הודעה",
@@ -492,6 +505,7 @@
 	"Workspace": "סביבה",
 	"Write a prompt suggestion (e.g. Who are you?)": "כתוב הצעה מהירה (למשל, מי אתה?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "כתוב סיכום ב-50 מילים שמסכם [נושא או מילת מפתח].",
+	"Yes": "",
 	"Yesterday": "אתמול",
 	"You": "אתה",
 	"You have no archived conversations.": "אין לך שיחות בארכיון.",

+ 14 - 0
src/lib/i18n/locales/hi-IN/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} सोच रहा है...",
 	"{{user}}'s Chats": "{{user}} की चैट",
 	"{{webUIName}} Backend Required": "{{webUIName}} बैकएंड आवश्यक",
+	"A selected model does not support image input": "",
 	"a user": "एक उपयोगकर्ता",
 	"About": "हमारे बारे में",
 	"Account": "खाता",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "उन्नत पैरामीटर",
 	"all": "सभी",
 	"All Documents": "सभी डॉक्यूमेंट्स",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "सभी उपयोगकर्ता",
 	"Allow": "अनुमति दें",
 	"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
@@ -115,6 +117,7 @@
 	"Created at": "किस समय बनाया गया",
 	"Created At": "किस समय बनाया गया",
 	"Current Model": "वर्तमान मॉडल",
+	"Current Models": "",
 	"Current Password": "वर्तमान पासवर्ड",
 	"Custom": "कस्टम संस्करण",
 	"Customize Ollama models for a specific purpose": "किसी विशिष्ट उद्देश्य के लिए ओलामा मॉडल को अनुकूलित करें",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM दर्ज करें (litellm_params.rpm) ",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM Model दर्ज करें (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Max Tokens दर्ज करें (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Model tag दर्ज करें (उदा. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "चरणों की संख्या दर्ज करें (उदा. 50)",
 	"Enter Score": "स्कोर दर्ज करें",
@@ -235,6 +239,7 @@
 	"Input commands": "इनपुट क命",
 	"Interface": "इंटरफेस",
 	"Invalid Tag": "अवैध टैग",
+	"Is Model Vision Capable": "",
 	"January": "जनवरी",
 	"join our Discord for help.": "मदद के लिए हमारे डिस्कोर्ड में शामिल हों।",
 	"JSON": "ज्ञान प्रकार",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI समुदाय द्वारा निर्मित",
 	"Make sure to enclose them with": "उन्हें संलग्न करना सुनिश्चित करें",
 	"Manage LiteLLM Models": "LiteLLM मॉडल प्रबंधित करें",
+	"Manage Model Information": "",
 	"Manage Models": "मॉडल प्रबंधित करें",
 	"Manage Ollama Models": "Ollama मॉडल प्रबंधित करें",
 	"March": "मार्च",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "मॉडल '{{modelTag}}' पहले से ही डाउनलोड करने के लिए कतार में है।",
 	"Model {{modelId}} not found": "मॉडल {{modelId}} नहीं मिला",
 	"Model {{modelName}} already exists.": "मॉडल {{modelName}} पहले से मौजूद है।",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "मॉडल फ़ाइल सिस्टम पथ का पता चला. अद्यतन के लिए मॉडल संक्षिप्त नाम आवश्यक है, जारी नहीं रखा जा सकता।",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "मॉडल नाम",
 	"Model not selected": "मॉडल चयनित नहीं है",
 	"Model Tag Name": "मॉडल टैग नाम",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "अपनी मॉडलफ़ाइल को नाम दें",
 	"New Chat": "नई चैट",
 	"New Password": "नया पासवर्ड",
+	"No": "",
 	"No results found": "कोई परिणाम नहीं मिला",
 	"No source available": "कोई स्रोत उपलब्ध नहीं है",
 	"Not factually correct": "तथ्यात्मक रूप से सही नहीं है",
@@ -385,6 +397,7 @@
 	"Select a model": "एक मॉडल चुनें",
 	"Select an Ollama instance": "एक Ollama Instance चुनें",
 	"Select model": "मॉडल चुनें",
+	"Selected models do not support image inputs": "",
 	"Send": "भेज",
 	"Send a Message": "एक संदेश भेजो",
 	"Send message": "मेसेज भेजें",
@@ -492,6 +505,7 @@
 	"Workspace": "वर्कस्पेस",
 	"Write a prompt suggestion (e.g. Who are you?)": "एक त्वरित सुझाव लिखें (जैसे कि आप कौन हैं?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "50 शब्दों में एक सारांश लिखें जो [विषय या कीवर्ड] का सारांश प्रस्तुत करता हो।",
+	"Yes": "",
 	"Yesterday": "कल",
 	"You": "आप",
 	"You have no archived conversations.": "आपको कोई अंकित चैट नहीं है।",

+ 14 - 0
src/lib/i18n/locales/hr-HR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} razmišlja...",
 	"{{user}}'s Chats": "Razgovori korisnika {{user}}",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend je potreban",
+	"A selected model does not support image input": "",
 	"a user": "korisnik",
 	"About": "O aplikaciji",
 	"Account": "Račun",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Napredni parametri",
 	"all": "sve",
 	"All Documents": "Svi dokumenti",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Svi korisnici",
 	"Allow": "Dopusti",
 	"Allow Chat Deletion": "Dopusti brisanje razgovora",
@@ -115,6 +117,7 @@
 	"Created at": "Stvoreno",
 	"Created At": "Stvoreno",
 	"Current Model": "Trenutni model",
+	"Current Models": "",
 	"Current Password": "Trenutna lozinka",
 	"Custom": "Prilagođeno",
 	"Customize Ollama models for a specific purpose": "Prilagodite Ollama modele za specifičnu svrhu",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Unesite LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Unesite LiteLLM model (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Unesite maksimalan broj tokena (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Unesite oznaku modela (npr. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Unesite broj koraka (npr. 50)",
 	"Enter Score": "Unesite ocjenu",
@@ -235,6 +239,7 @@
 	"Input commands": "Unos naredbi",
 	"Interface": "Sučelje",
 	"Invalid Tag": "Nevažeća oznaka",
+	"Is Model Vision Capable": "",
 	"January": "Siječanj",
 	"join our Discord for help.": "pridružite se našem Discordu za pomoć.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Izradio OpenWebUI Community",
 	"Make sure to enclose them with": "Provjerite da ih zatvorite s",
 	"Manage LiteLLM Models": "Upravljajte LiteLLM modelima",
+	"Manage Model Information": "",
 	"Manage Models": "Upravljanje modelima",
 	"Manage Ollama Models": "Upravljanje Ollama modelima",
 	"March": "Ožujak",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' je već u redu za preuzimanje.",
 	"Model {{modelId}} not found": "Model {{modelId}} nije pronađen",
 	"Model {{modelName}} already exists.": "Model {{modelName}} već postoji.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Otkriven put datotečnog sustava modela. Kratko ime modela je potrebno za ažuriranje, nije moguće nastaviti.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Naziv modela",
 	"Model not selected": "Model nije odabran",
 	"Model Tag Name": "Naziv oznake modela",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nazovite svoju datoteku modela",
 	"New Chat": "Novi razgovor",
 	"New Password": "Nova lozinka",
+	"No": "",
 	"No results found": "Nema rezultata",
 	"No source available": "Nema dostupnog izvora",
 	"Not factually correct": "Nije činjenično točno",
@@ -385,6 +397,7 @@
 	"Select a model": "Odaberite model",
 	"Select an Ollama instance": "Odaberite Ollama instancu",
 	"Select model": "Odaberite model",
+	"Selected models do not support image inputs": "",
 	"Send": "Pošalji",
 	"Send a Message": "Pošaljite poruku",
 	"Send message": "Pošalji poruku",
@@ -492,6 +505,7 @@
 	"Workspace": "Radna ploča",
 	"Write a prompt suggestion (e.g. Who are you?)": "Napišite prijedlog prompta (npr. Tko si ti?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Napišite sažetak u 50 riječi koji sažima [temu ili ključnu riječ].",
+	"Yes": "",
 	"Yesterday": "Jučer",
 	"You": "Vi",
 	"You have no archived conversations.": "Nemate arhiviranih razgovora.",

+ 14 - 0
src/lib/i18n/locales/it-IT/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} sta pensando...",
 	"{{user}}'s Chats": "{{user}} Chat",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend richiesto",
+	"A selected model does not support image input": "",
 	"a user": "un utente",
 	"About": "Informazioni",
 	"Account": "Account",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Parametri avanzati",
 	"all": "tutti",
 	"All Documents": "Tutti i documenti",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Tutti gli utenti",
 	"Allow": "Consenti",
 	"Allow Chat Deletion": "Consenti l'eliminazione della chat",
@@ -115,6 +117,7 @@
 	"Created at": "Creato il",
 	"Created At": "Creato il",
 	"Current Model": "Modello corrente",
+	"Current Models": "",
 	"Current Password": "Password corrente",
 	"Custom": "Personalizzato",
 	"Customize Ollama models for a specific purpose": "Personalizza i modelli Ollama per uno scopo specifico",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Inserisci LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Inserisci il modello LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Inserisci Max Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Inserisci il tag del modello (ad esempio {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Inserisci il numero di passaggi (ad esempio 50)",
 	"Enter Score": "Inserisci il punteggio",
@@ -235,6 +239,7 @@
 	"Input commands": "Comandi di input",
 	"Interface": "Interfaccia",
 	"Invalid Tag": "Tag non valido",
+	"Is Model Vision Capable": "",
 	"January": "Gennaio",
 	"join our Discord for help.": "unisciti al nostro Discord per ricevere aiuto.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Realizzato dalla comunità OpenWebUI",
 	"Make sure to enclose them with": "Assicurati di racchiuderli con",
 	"Manage LiteLLM Models": "Gestisci modelli LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gestisci modelli",
 	"Manage Ollama Models": "Gestisci modelli Ollama",
 	"March": "Marzo",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Il modello '{{modelTag}}' è già in coda per il download.",
 	"Model {{modelId}} not found": "Modello {{modelId}} non trovato",
 	"Model {{modelName}} already exists.": "Il modello {{modelName}} esiste già.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Percorso del filesystem del modello rilevato. Il nome breve del modello è richiesto per l'aggiornamento, impossibile continuare.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nome modello",
 	"Model not selected": "Modello non selezionato",
 	"Model Tag Name": "Nome tag del modello",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Assegna un nome al tuo file modello",
 	"New Chat": "Nuova chat",
 	"New Password": "Nuova password",
+	"No": "",
 	"No results found": "Nessun risultato trovato",
 	"No source available": "Nessuna fonte disponibile",
 	"Not factually correct": "Non corretto dal punto di vista fattuale",
@@ -385,6 +397,7 @@
 	"Select a model": "Seleziona un modello",
 	"Select an Ollama instance": "Seleziona un'istanza Ollama",
 	"Select model": "Seleziona modello",
+	"Selected models do not support image inputs": "",
 	"Send": "Invia",
 	"Send a Message": "Invia un messaggio",
 	"Send message": "Invia messaggio",
@@ -492,6 +505,7 @@
 	"Workspace": "Area di lavoro",
 	"Write a prompt suggestion (e.g. Who are you?)": "Scrivi un suggerimento per il prompt (ad esempio Chi sei?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Scrivi un riassunto in 50 parole che riassume [argomento o parola chiave].",
+	"Yes": "",
 	"Yesterday": "Ieri",
 	"You": "Tu",
 	"You have no archived conversations.": "Non hai conversazioni archiviate.",

+ 14 - 0
src/lib/i18n/locales/ja-JP/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} は思考中です...",
 	"{{user}}'s Chats": "{{user}} のチャット",
 	"{{webUIName}} Backend Required": "{{webUIName}} バックエンドが必要です",
+	"A selected model does not support image input": "",
 	"a user": "ユーザー",
 	"About": "概要",
 	"Account": "アカウント",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "詳細パラメーター",
 	"all": "すべて",
 	"All Documents": "全てのドキュメント",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "すべてのユーザー",
 	"Allow": "許可",
 	"Allow Chat Deletion": "チャットの削除を許可",
@@ -115,6 +117,7 @@
 	"Created at": "作成日時",
 	"Created At": "作成日時",
 	"Current Model": "現在のモデル",
+	"Current Models": "",
 	"Current Password": "現在のパスワード",
 	"Custom": "カスタム",
 	"Customize Ollama models for a specific purpose": "特定の目的に合わせて Ollama モデルをカスタマイズ",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM を入力してください (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM モデルを入力してください (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "最大トークン数を入力してください (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "モデルタグを入力してください (例: {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "ステップ数を入力してください (例: 50)",
 	"Enter Score": "スコアを入力してください",
@@ -235,6 +239,7 @@
 	"Input commands": "入力コマンド",
 	"Interface": "インターフェース",
 	"Invalid Tag": "無効なタグ",
+	"Is Model Vision Capable": "",
 	"January": "1月",
 	"join our Discord for help.": "ヘルプについては、Discord に参加してください。",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI コミュニティによって作成",
 	"Make sure to enclose them with": "必ず次で囲んでください",
 	"Manage LiteLLM Models": "LiteLLM モデルを管理",
+	"Manage Model Information": "",
 	"Manage Models": "モデルを管理",
 	"Manage Ollama Models": "Ollama モデルを管理",
 	"March": "3月",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "モデル '{{modelTag}}' はすでにダウンロード待ち行列に入っています。",
 	"Model {{modelId}} not found": "モデル {{modelId}} が見つかりません",
 	"Model {{modelName}} already exists.": "モデル {{modelName}} はすでに存在します。",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "モデルファイルシステムパスが検出されました。モデルの短縮名が必要です。更新できません。",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "モデル名",
 	"Model not selected": "モデルが選択されていません",
 	"Model Tag Name": "モデルタグ名",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "モデルファイルに名前を付ける",
 	"New Chat": "新しいチャット",
 	"New Password": "新しいパスワード",
+	"No": "",
 	"No results found": "結果が見つかりません",
 	"No source available": "使用可能なソースがありません",
 	"Not factually correct": "実事上正しくない",
@@ -385,6 +397,7 @@
 	"Select a model": "モデルを選択",
 	"Select an Ollama instance": "Ollama インスタンスを選択",
 	"Select model": "モデルを選択",
+	"Selected models do not support image inputs": "",
 	"Send": "送信",
 	"Send a Message": "メッセージを送信",
 	"Send message": "メッセージを送信",
@@ -492,6 +505,7 @@
 	"Workspace": "ワークスペース",
 	"Write a prompt suggestion (e.g. Who are you?)": "プロンプトの提案を書いてください (例: あなたは誰ですか?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "[トピックまたはキーワード] を要約する 50 語の概要を書いてください。",
+	"Yes": "",
 	"Yesterday": "昨日",
 	"You": "あなた",
 	"You have no archived conversations.": "これまでにアーカイブされた会話はありません。",

+ 14 - 0
src/lib/i18n/locales/ka-GE/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} ფიქრობს...",
 	"{{user}}'s Chats": "{{user}}-ის ჩათები",
 	"{{webUIName}} Backend Required": "{{webUIName}} საჭიროა ბექენდი",
+	"A selected model does not support image input": "",
 	"a user": "მომხმარებელი",
 	"About": "შესახებ",
 	"Account": "ანგარიში",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "დამატებითი პარამეტრები",
 	"all": "ყველა",
 	"All Documents": "ყველა დოკუმენტი",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "ყველა მომხმარებელი",
 	"Allow": "ნების დართვა",
 	"Allow Chat Deletion": "მიმოწერის წაშლის დაშვება",
@@ -115,6 +117,7 @@
 	"Created at": "შექმნილია",
 	"Created At": "შექმნილია",
 	"Current Model": "მიმდინარე მოდელი",
+	"Current Models": "",
 	"Current Password": "მიმდინარე პაროლი",
 	"Custom": "საკუთარი",
 	"Customize Ollama models for a specific purpose": "Ollama მოდელების დამუშავება სპეციფიური დანიშნულებისთვის",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "შეიყვანეთ LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "შეიყვანეთ LiteLLM მოდელი (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "შეიყვანეთ მაქსიმალური ტოკენები (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "შეიყვანეთ მოდელის ტეგი (მაგ. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "შეიყვანეთ ნაბიჯების რაოდენობა (მაგ. 50)",
 	"Enter Score": "შეიყვანეთ ქულა",
@@ -235,6 +239,7 @@
 	"Input commands": "შეყვანით ბრძანებებს",
 	"Interface": "ინტერფეისი",
 	"Invalid Tag": "არასწორი ტეგი",
+	"Is Model Vision Capable": "",
 	"January": "იანვარი",
 	"join our Discord for help.": "შეუერთდით ჩვენს Discord-ს დახმარებისთვის",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "დამზადებულია OpenWebUI საზოგადოების მიერ",
 	"Make sure to enclose them with": "დარწმუნდით, რომ დაურთეთ ისინი",
 	"Manage LiteLLM Models": "LiteLLM მოდელების მართვა",
+	"Manage Model Information": "",
 	"Manage Models": "მოდელების მართვა",
 	"Manage Ollama Models": "Ollama მოდელების მართვა",
 	"March": "მარტივი",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "მოდელი „{{modelTag}}“ უკვე ჩამოტვირთვის რიგშია.",
 	"Model {{modelId}} not found": "მოდელი {{modelId}} ვერ მოიძებნა",
 	"Model {{modelName}} already exists.": "მოდელი {{modelName}} უკვე არსებობს.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "აღმოჩენილია მოდელის ფაილური სისტემის გზა. განახლებისთვის საჭიროა მოდელის მოკლე სახელი, გაგრძელება შეუძლებელია.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "მოდელის სახელი",
 	"Model not selected": "მოდელი არ არის არჩეული",
 	"Model Tag Name": "მოდელის ტეგის სახელი",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "თქვენი მოდელური ფაილის სახელი",
 	"New Chat": "ახალი მიმოწერა",
 	"New Password": "ახალი პაროლი",
+	"No": "",
 	"No results found": "ჩვენ ვერ პოულობით ნაპოვნი ჩაწერები",
 	"No source available": "წყარო არ არის ხელმისაწვდომი",
 	"Not factually correct": "არ ვეთანხმები პირდაპირ ვერც ვეთანხმები",
@@ -385,6 +397,7 @@
 	"Select a model": "მოდელის არჩევა",
 	"Select an Ollama instance": "Ollama ინსტანსის არჩევა",
 	"Select model": "მოდელის არჩევა",
+	"Selected models do not support image inputs": "",
 	"Send": "გაგზავნა",
 	"Send a Message": "შეტყობინების გაგზავნა",
 	"Send message": "შეტყობინების გაგზავნა",
@@ -492,6 +505,7 @@
 	"Workspace": "ვულერი",
 	"Write a prompt suggestion (e.g. Who are you?)": "დაწერეთ მოკლე წინადადება (მაგ. ვინ ხარ?",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "დაწერეთ რეზიუმე 50 სიტყვით, რომელიც აჯამებს [თემას ან საკვანძო სიტყვას].",
+	"Yes": "",
 	"Yesterday": "აღდგენა",
 	"You": "ჩემი",
 	"You have no archived conversations.": "არ ხართ არქივირებული განხილვები.",

+ 14 - 0
src/lib/i18n/locales/ko-KR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} 이(가) 생각중입니다....",
 	"{{user}}'s Chats": "{{user}}의 채팅",
 	"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
+	"A selected model does not support image input": "",
 	"a user": "사용자",
 	"About": "소개",
 	"Account": "계정",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "고급 매개변수",
 	"all": "모두",
 	"All Documents": "모든 문서",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "모든 사용자",
 	"Allow": "허용",
 	"Allow Chat Deletion": "채팅 삭제 허용",
@@ -115,6 +117,7 @@
 	"Created at": "생성일",
 	"Created At": "생성일",
 	"Current Model": "현재 모델",
+	"Current Models": "",
 	"Current Password": "현재 비밀번호",
 	"Custom": "사용자 정의",
 	"Customize Ollama models for a specific purpose": "특정 목적으로 Ollama 모델 사용자 정의",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM 입력(litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM 모델 입력(litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "최대 토큰 수 입력(litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "단계 수 입력(예: 50)",
 	"Enter Score": "점수 입력",
@@ -235,6 +239,7 @@
 	"Input commands": "입력 명령",
 	"Interface": "인터페이스",
 	"Invalid Tag": "잘못된 태그",
+	"Is Model Vision Capable": "",
 	"January": "1월",
 	"join our Discord for help.": "도움말을 보려면 Discord에 가입하세요.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI 커뮤니티에서 제작",
 	"Make sure to enclose them with": "다음으로 묶는 것을 잊지 마세요:",
 	"Manage LiteLLM Models": "LiteLLM 모델 관리",
+	"Manage Model Information": "",
 	"Manage Models": "모델 관리",
 	"Manage Ollama Models": "Ollama 모델 관리",
 	"March": "3월",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "모델 '{{modelTag}}'이(가) 이미 다운로드 대기열에 있습니다.",
 	"Model {{modelId}} not found": "모델 {{modelId}}를 찾을 수 없습니다.",
 	"Model {{modelName}} already exists.": "모델 {{modelName}}이(가) 이미 존재합니다.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "모델 파일 시스템 경로가 감지되었습니다. 업데이트하려면 모델 단축 이름이 필요하며 계속할 수 없습니다.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "모델 이름",
 	"Model not selected": "모델이 선택되지 않았습니다.",
 	"Model Tag Name": "모델 태그 이름",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "모델파일 이름 지정",
 	"New Chat": "새 채팅",
 	"New Password": "새 비밀번호",
+	"No": "",
 	"No results found": "결과 없음",
 	"No source available": "사용 가능한 소스 없음",
 	"Not factually correct": "사실상 맞지 않음",
@@ -385,6 +397,7 @@
 	"Select a model": "모델 선택",
 	"Select an Ollama instance": "Ollama 인스턴스 선택",
 	"Select model": "모델 선택",
+	"Selected models do not support image inputs": "",
 	"Send": "보내기",
 	"Send a Message": "메시지 보내기",
 	"Send message": "메시지 보내기",
@@ -492,6 +505,7 @@
 	"Workspace": "워크스페이스",
 	"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
+	"Yes": "",
 	"Yesterday": "어제",
 	"You": "당신",
 	"You have no archived conversations.": "채팅을 아카이브한 적이 없습니다.",

+ 14 - 0
src/lib/i18n/locales/nl-NL/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} is aan het denken...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht",
+	"A selected model does not support image input": "",
 	"a user": "een gebruiker",
 	"About": "Over",
 	"Account": "Account",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Geavanceerde Parameters",
 	"all": "alle",
 	"All Documents": "Alle Documenten",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Alle Gebruikers",
 	"Allow": "Toestaan",
 	"Allow Chat Deletion": "Sta Chat Verwijdering toe",
@@ -115,6 +117,7 @@
 	"Created at": "Gemaakt op",
 	"Created At": "Gemaakt op",
 	"Current Model": "Huidig Model",
+	"Current Models": "",
 	"Current Password": "Huidig Wachtwoord",
 	"Custom": "Aangepast",
 	"Customize Ollama models for a specific purpose": "Pas Ollama modellen aan voor een specifiek doel",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)",
 	"Enter Score": "Voeg score toe",
@@ -235,6 +239,7 @@
 	"Input commands": "Voer commando's in",
 	"Interface": "Interface",
 	"Invalid Tag": "Ongeldige Tag",
+	"Is Model Vision Capable": "",
 	"January": "Januari",
 	"join our Discord for help.": "join onze Discord voor hulp.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Gemaakt door OpenWebUI Community",
 	"Make sure to enclose them with": "Zorg ervoor dat je ze omringt met",
 	"Manage LiteLLM Models": "Beheer LiteLLM Modellen",
+	"Manage Model Information": "",
 	"Manage Models": "Beheer Modellen",
 	"Manage Ollama Models": "Beheer Ollama Modellen",
 	"March": "Maart",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' staat al in de wachtrij voor downloaden.",
 	"Model {{modelId}} not found": "Model {{modelId}} niet gevonden",
 	"Model {{modelName}} already exists.": "Model {{modelName}} bestaat al.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem path gedetecteerd. Model shortname is vereist voor update, kan niet doorgaan.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Model Naam",
 	"Model not selected": "Model niet geselecteerd",
 	"Model Tag Name": "Model Tag Naam",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Benoem je modelfile",
 	"New Chat": "Nieuwe Chat",
 	"New Password": "Nieuw Wachtwoord",
+	"No": "",
 	"No results found": "Geen resultaten gevonden",
 	"No source available": "Geen bron beschikbaar",
 	"Not factually correct": "Feitelijk niet juist",
@@ -385,6 +397,7 @@
 	"Select a model": "Selecteer een model",
 	"Select an Ollama instance": "Selecteer een Ollama instantie",
 	"Select model": "Selecteer een model",
+	"Selected models do not support image inputs": "",
 	"Send": "Verzenden",
 	"Send a Message": "Stuur een Bericht",
 	"Send message": "Stuur bericht",
@@ -492,6 +505,7 @@
 	"Workspace": "Werkruimte",
 	"Write a prompt suggestion (e.g. Who are you?)": "Schrijf een prompt suggestie (bijv. Wie ben je?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schrijf een samenvatting in 50 woorden die [onderwerp of trefwoord] samenvat.",
+	"Yes": "",
 	"Yesterday": "gisteren",
 	"You": "U",
 	"You have no archived conversations.": "U heeft geen gearchiveerde gesprekken.",

+ 14 - 0
src/lib/i18n/locales/pa-IN/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} ਸੋਚ ਰਿਹਾ ਹੈ...",
 	"{{user}}'s Chats": "{{user}} ਦੀਆਂ ਗੱਲਾਂ",
 	"{{webUIName}} Backend Required": "{{webUIName}} ਬੈਕਐਂਡ ਲੋੜੀਂਦਾ ਹੈ",
+	"A selected model does not support image input": "",
 	"a user": "ਇੱਕ ਉਪਭੋਗਤਾ",
 	"About": "ਬਾਰੇ",
 	"Account": "ਖਾਤਾ",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "ਉੱਚ ਸਤਰ ਦੇ ਪੈਰਾਮੀਟਰ",
 	"all": "ਸਾਰੇ",
 	"All Documents": "ਸਾਰੇ ਡਾਕੂਮੈਂਟ",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "ਸਾਰੇ ਉਪਭੋਗਤਾ",
 	"Allow": "ਅਨੁਮਤੀ",
 	"Allow Chat Deletion": "ਗੱਲਬਾਤ ਮਿਟਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ",
@@ -115,6 +117,7 @@
 	"Created at": "ਤੇ ਬਣਾਇਆ ਗਿਆ",
 	"Created At": "ਤੇ ਬਣਾਇਆ ਗਿਆ",
 	"Current Model": "ਮੌਜੂਦਾ ਮਾਡਲ",
+	"Current Models": "",
 	"Current Password": "ਮੌਜੂਦਾ ਪਾਸਵਰਡ",
 	"Custom": "ਕਸਟਮ",
 	"Customize Ollama models for a specific purpose": "ਇੱਕ ਖਾਸ ਉਦੇਸ਼ ਲਈ ਓਲਾਮਾ ਮਾਡਲਾਂ ਨੂੰ ਕਸਟਮਾਈਜ਼ ਕਰੋ",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM (litellm_params.rpm) ਦਰਜ ਕਰੋ",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM ਮਾਡਲ (litellm_params.model) ਦਰਜ ਕਰੋ",
 	"Enter Max Tokens (litellm_params.max_tokens)": "ਅਧਿਕਤਮ ਟੋਕਨ ਦਰਜ ਕਰੋ (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "ਮਾਡਲ ਟੈਗ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "ਕਦਮਾਂ ਦੀ ਗਿਣਤੀ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ 50)",
 	"Enter Score": "ਸਕੋਰ ਦਰਜ ਕਰੋ",
@@ -235,6 +239,7 @@
 	"Input commands": "ਇਨਪੁਟ ਕਮਾਂਡਾਂ",
 	"Interface": "ਇੰਟਰਫੇਸ",
 	"Invalid Tag": "ਗਲਤ ਟੈਗ",
+	"Is Model Vision Capable": "",
 	"January": "ਜਨਵਰੀ",
 	"join our Discord for help.": "ਮਦਦ ਲਈ ਸਾਡੇ ਡਿਸਕੋਰਡ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ।",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "ਓਪਨਵੈਬਯੂਆਈ ਕਮਿਊਨਿਟੀ ਦੁਆਰਾ ਬਣਾਇਆ ਗਿਆ",
 	"Make sure to enclose them with": "ਸੁਨਿਸ਼ਚਿਤ ਕਰੋ ਕਿ ਉਨ੍ਹਾਂ ਨੂੰ ਘੇਰੋ",
 	"Manage LiteLLM Models": "LiteLLM ਮਾਡਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ",
+	"Manage Model Information": "",
 	"Manage Models": "ਮਾਡਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ",
 	"Manage Ollama Models": "ਓਲਾਮਾ ਮਾਡਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ",
 	"March": "ਮਾਰਚ",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "ਮਾਡਲ '{{modelTag}}' ਪਹਿਲਾਂ ਹੀ ਡਾਊਨਲੋਡ ਲਈ ਕਤਾਰ ਵਿੱਚ ਹੈ।",
 	"Model {{modelId}} not found": "ਮਾਡਲ {{modelId}} ਨਹੀਂ ਮਿਲਿਆ",
 	"Model {{modelName}} already exists.": "ਮਾਡਲ {{modelName}} ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ।",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "ਮਾਡਲ ਫਾਈਲਸਿਸਟਮ ਪੱਥ ਪਾਇਆ ਗਿਆ। ਅੱਪਡੇਟ ਲਈ ਮਾਡਲ ਸ਼ੌਰਟਨੇਮ ਦੀ ਲੋੜ ਹੈ, ਜਾਰੀ ਨਹੀਂ ਰੱਖ ਸਕਦੇ।",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "ਮਾਡਲ ਨਾਮ",
 	"Model not selected": "ਮਾਡਲ ਚੁਣਿਆ ਨਹੀਂ ਗਿਆ",
 	"Model Tag Name": "ਮਾਡਲ ਟੈਗ ਨਾਮ",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "ਆਪਣੀ ਮਾਡਲਫਾਈਲ ਦਾ ਨਾਮ ਰੱਖੋ",
 	"New Chat": "ਨਵੀਂ ਗੱਲਬਾਤ",
 	"New Password": "ਨਵਾਂ ਪਾਸਵਰਡ",
+	"No": "",
 	"No results found": "ਕੋਈ ਨਤੀਜੇ ਨਹੀਂ ਮਿਲੇ",
 	"No source available": "ਕੋਈ ਸਰੋਤ ਉਪਲਬਧ ਨਹੀਂ",
 	"Not factually correct": "ਤੱਥਕ ਰੂਪ ਵਿੱਚ ਸਹੀ ਨਹੀਂ",
@@ -385,6 +397,7 @@
 	"Select a model": "ਇੱਕ ਮਾਡਲ ਚੁਣੋ",
 	"Select an Ollama instance": "ਇੱਕ ਓਲਾਮਾ ਇੰਸਟੈਂਸ ਚੁਣੋ",
 	"Select model": "ਮਾਡਲ ਚੁਣੋ",
+	"Selected models do not support image inputs": "",
 	"Send": "ਭੇਜੋ",
 	"Send a Message": "ਇੱਕ ਸੁਨੇਹਾ ਭੇਜੋ",
 	"Send message": "ਸੁਨੇਹਾ ਭੇਜੋ",
@@ -492,6 +505,7 @@
 	"Workspace": "ਕਾਰਜਸਥਲ",
 	"Write a prompt suggestion (e.g. Who are you?)": "ਇੱਕ ਪ੍ਰੰਪਟ ਸੁਝਾਅ ਲਿਖੋ (ਉਦਾਹਰਣ ਲਈ ਤੁਸੀਂ ਕੌਣ ਹੋ?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "50 ਸ਼ਬਦਾਂ ਵਿੱਚ ਇੱਕ ਸੰਖੇਪ ਲਿਖੋ ਜੋ [ਵਿਸ਼ਾ ਜਾਂ ਕੁੰਜੀ ਸ਼ਬਦ] ਨੂੰ ਸੰਖੇਪ ਕਰਦਾ ਹੈ।",
+	"Yes": "",
 	"Yesterday": "ਕੱਲ੍ਹ",
 	"You": "ਤੁਸੀਂ",
 	"You have no archived conversations.": "ਤੁਹਾਡੇ ਕੋਲ ਕੋਈ ਆਰਕਾਈਵ ਕੀਤੀਆਂ ਗੱਲਾਂ ਨਹੀਂ ਹਨ।",

+ 14 - 0
src/lib/i18n/locales/pl-PL/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} myśli...",
 	"{{user}}'s Chats": "{{user}} - czaty",
 	"{{webUIName}} Backend Required": "Backend {{webUIName}} wymagane",
+	"A selected model does not support image input": "",
 	"a user": "użytkownik",
 	"About": "O nas",
 	"Account": "Konto",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Zaawansowane parametry",
 	"all": "wszyscy",
 	"All Documents": "Wszystkie dokumenty",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Wszyscy użytkownicy",
 	"Allow": "Pozwól",
 	"Allow Chat Deletion": "Pozwól na usuwanie czatu",
@@ -115,6 +117,7 @@
 	"Created at": "Utworzono o",
 	"Created At": "Utworzono o",
 	"Current Model": "Bieżący model",
+	"Current Models": "",
 	"Current Password": "Bieżące hasło",
 	"Custom": "Niestandardowy",
 	"Customize Ollama models for a specific purpose": "Dostosuj modele Ollama do określonego celu",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Wprowadź API LiteLLM RPM(litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Wprowadź model LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Wprowadź maksymalną liczbę tokenów (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Wprowadź tag modelu (np. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Wprowadź liczbę kroków (np. 50)",
 	"Enter Score": "Wprowadź wynik",
@@ -235,6 +239,7 @@
 	"Input commands": "Wprowadź komendy",
 	"Interface": "Interfejs",
 	"Invalid Tag": "Nieprawidłowy tag",
+	"Is Model Vision Capable": "",
 	"January": "Styczeń",
 	"join our Discord for help.": "Dołącz do naszego Discorda po pomoc.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Stworzone przez społeczność OpenWebUI",
 	"Make sure to enclose them with": "Upewnij się, że są one zamknięte w",
 	"Manage LiteLLM Models": "Zarządzaj modelami LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Zarządzaj modelami",
 	"Manage Ollama Models": "Zarządzaj modelami Ollama",
 	"March": "Marzec",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' jest już w kolejce do pobrania.",
 	"Model {{modelId}} not found": "Model {{modelId}} nie został znaleziony",
 	"Model {{modelName}} already exists.": "Model {{modelName}} już istnieje.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Wykryto ścieżkę systemu plików modelu. Wymagana jest krótka nazwa modelu do aktualizacji, nie można kontynuować.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nazwa modelu",
 	"Model not selected": "Model nie został wybrany",
 	"Model Tag Name": "Nazwa tagu modelu",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nadaj nazwę swojemu plikowi modelu",
 	"New Chat": "Nowy czat",
 	"New Password": "Nowe hasło",
+	"No": "",
 	"No results found": "Nie znaleziono rezultatów",
 	"No source available": "Źródło nie dostępne",
 	"Not factually correct": "Nie zgodne z faktami",
@@ -385,6 +397,7 @@
 	"Select a model": "Wybierz model",
 	"Select an Ollama instance": "Wybierz instancję Ollama",
 	"Select model": "Wybierz model",
+	"Selected models do not support image inputs": "",
 	"Send": "Wyślij",
 	"Send a Message": "Wyślij Wiadomość",
 	"Send message": "Wyślij wiadomość",
@@ -492,6 +505,7 @@
 	"Workspace": "Obszar roboczy",
 	"Write a prompt suggestion (e.g. Who are you?)": "Napisz sugestię do polecenia (np. Kim jesteś?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Napisz podsumowanie w 50 słowach, które podsumowuje [temat lub słowo kluczowe].",
+	"Yes": "",
 	"Yesterday": "Wczoraj",
 	"You": "Ty",
 	"You have no archived conversations.": "Nie masz zarchiwizowanych rozmów.",

+ 14 - 0
src/lib/i18n/locales/pt-BR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} está pensando...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário",
+	"A selected model does not support image input": "",
 	"a user": "um usuário",
 	"About": "Sobre",
 	"Account": "Conta",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Parâmetros Avançados",
 	"all": "todos",
 	"All Documents": "Todos os Documentos",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Todos os Usuários",
 	"Allow": "Permitir",
 	"Allow Chat Deletion": "Permitir Exclusão de Bate-papo",
@@ -115,6 +117,7 @@
 	"Created at": "Criado em",
 	"Created At": "Criado em",
 	"Current Model": "Modelo Atual",
+	"Current Models": "",
 	"Current Password": "Senha Atual",
 	"Custom": "Personalizado",
 	"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Digite o RPM da API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Digite o Modelo LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Digite o Máximo de Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Digite a tag do modelo (por exemplo, {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Digite o Número de Etapas (por exemplo, 50)",
 	"Enter Score": "Digite a Pontuação",
@@ -235,6 +239,7 @@
 	"Input commands": "Comandos de entrada",
 	"Interface": "Interface",
 	"Invalid Tag": "Etiqueta Inválida",
+	"Is Model Vision Capable": "",
 	"January": "Janeiro",
 	"join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI",
 	"Make sure to enclose them with": "Certifique-se de colocá-los entre",
 	"Manage LiteLLM Models": "Gerenciar Modelos LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gerenciar Modelos",
 	"Manage Ollama Models": "Gerenciar Modelos Ollama",
 	"March": "Março",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para download.",
 	"Model {{modelId}} not found": "Modelo {{modelId}} não encontrado",
 	"Model {{modelName}} already exists.": "O modelo {{modelName}} já existe.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Otkrivena putanja datoteke modela. Skraćeno ime modela je potrebno za ažuriranje, ne može se nastaviti.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nome do Modelo",
 	"Model not selected": "Modelo não selecionado",
 	"Model Tag Name": "Nome da Tag do Modelo",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nomeie seu arquivo de modelo",
 	"New Chat": "Novo Bate-papo",
 	"New Password": "Nova Senha",
+	"No": "",
 	"No results found": "Nenhum resultado encontrado",
 	"No source available": "Nenhuma fonte disponível",
 	"Not factually correct": "Não é correto em termos factuais",
@@ -385,6 +397,7 @@
 	"Select a model": "Selecione um modelo",
 	"Select an Ollama instance": "Selecione uma instância Ollama",
 	"Select model": "Selecione um modelo",
+	"Selected models do not support image inputs": "",
 	"Send": "Enviar",
 	"Send a Message": "Enviar uma Mensagem",
 	"Send message": "Enviar mensagem",
@@ -492,6 +505,7 @@
 	"Workspace": "Espaço de trabalho",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem é você?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].",
+	"Yes": "",
 	"Yesterday": "Ontem",
 	"You": "Você",
 	"You have no archived conversations.": "Você não tem conversas arquivadas.",

+ 14 - 0
src/lib/i18n/locales/pt-PT/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} está pensando...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário",
+	"A selected model does not support image input": "",
 	"a user": "um usuário",
 	"About": "Sobre",
 	"Account": "Conta",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Parâmetros Avançados",
 	"all": "todos",
 	"All Documents": "Todos os Documentos",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Todos os Usuários",
 	"Allow": "Permitir",
 	"Allow Chat Deletion": "Permitir Exclusão de Bate-papo",
@@ -115,6 +117,7 @@
 	"Created at": "Criado em",
 	"Created At": "Criado em",
 	"Current Model": "Modelo Atual",
+	"Current Models": "",
 	"Current Password": "Senha Atual",
 	"Custom": "Personalizado",
 	"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Digite o RPM da API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Digite o Modelo LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Digite o Máximo de Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Digite a tag do modelo (por exemplo, {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Digite o Número de Etapas (por exemplo, 50)",
 	"Enter Score": "Digite a Pontuação",
@@ -235,6 +239,7 @@
 	"Input commands": "Comandos de entrada",
 	"Interface": "Interface",
 	"Invalid Tag": "Etiqueta Inválida",
+	"Is Model Vision Capable": "",
 	"January": "Janeiro",
 	"join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI",
 	"Make sure to enclose them with": "Certifique-se de colocá-los entre",
 	"Manage LiteLLM Models": "Gerenciar Modelos LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Gerenciar Modelos",
 	"Manage Ollama Models": "Gerenciar Modelos Ollama",
 	"March": "Março",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para download.",
 	"Model {{modelId}} not found": "Modelo {{modelId}} não encontrado",
 	"Model {{modelName}} already exists.": "O modelo {{modelName}} já existe.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Caminho do sistema de arquivos do modelo detectado. É necessário o nome curto do modelo para atualização, não é possível continuar.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Nome do Modelo",
 	"Model not selected": "Modelo não selecionado",
 	"Model Tag Name": "Nome da Tag do Modelo",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Nomeie seu arquivo de modelo",
 	"New Chat": "Novo Bate-papo",
 	"New Password": "Nova Senha",
+	"No": "",
 	"No results found": "Nenhum resultado encontrado",
 	"No source available": "Nenhuma fonte disponível",
 	"Not factually correct": "Não é correto em termos factuais",
@@ -385,6 +397,7 @@
 	"Select a model": "Selecione um modelo",
 	"Select an Ollama instance": "Selecione uma instância Ollama",
 	"Select model": "Selecione um modelo",
+	"Selected models do not support image inputs": "",
 	"Send": "Enviar",
 	"Send a Message": "Enviar uma Mensagem",
 	"Send message": "Enviar mensagem",
@@ -492,6 +505,7 @@
 	"Workspace": "Espaço de Trabalho",
 	"Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem é você?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].",
+	"Yes": "",
 	"Yesterday": "Ontem",
 	"You": "Você",
 	"You have no archived conversations.": "Você não tem bate-papos arquivados.",

+ 14 - 0
src/lib/i18n/locales/ru-RU/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} думает...",
 	"{{user}}'s Chats": "{{user}} чаты",
 	"{{webUIName}} Backend Required": "{{webUIName}} бэкенд требуемый",
+	"A selected model does not support image input": "",
 	"a user": "пользователь",
 	"About": "Об",
 	"Account": "Аккаунт",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Расширенные Параметры",
 	"all": "всё",
 	"All Documents": "Все документы",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Все пользователи",
 	"Allow": "Разрешить",
 	"Allow Chat Deletion": "Дозволять удаление чат",
@@ -115,6 +117,7 @@
 	"Created at": "Создано в",
 	"Created At": "Создано в",
 	"Current Model": "Текущая модель",
+	"Current Models": "",
 	"Current Password": "Текущий пароль",
 	"Custom": "Пользовательский",
 	"Customize Ollama models for a specific purpose": "Настроить модели Ollama для конкретной цели",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Введите RPM API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Введите модель LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Введите максимальное количество токенов (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Введите тег модели (например, {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Введите количество шагов (например, 50)",
 	"Enter Score": "Введите оценку",
@@ -235,6 +239,7 @@
 	"Input commands": "Введите команды",
 	"Interface": "Интерфейс",
 	"Invalid Tag": "Недопустимый тег",
+	"Is Model Vision Capable": "",
 	"January": "Январь",
 	"join our Discord for help.": "присоединяйтесь к нашему Discord для помощи.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Сделано сообществом OpenWebUI",
 	"Make sure to enclose them with": "Убедитесь, что они заключены в",
 	"Manage LiteLLM Models": "Управление моделями LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Управление моделями",
 	"Manage Ollama Models": "Управление моделями Ollama",
 	"March": "Март",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Модель '{{modelTag}}' уже находится в очереди на загрузку.",
 	"Model {{modelId}} not found": "Модель {{modelId}} не найдена",
 	"Model {{modelName}} already exists.": "Модель {{modelName}} уже существует.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Модель файловой системы обнаружена. Требуется имя тега модели для обновления, не удается продолжить.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Имя модели",
 	"Model not selected": "Модель не выбрана",
 	"Model Tag Name": "Имя тега модели",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Назовите свой файл модели",
 	"New Chat": "Новый чат",
 	"New Password": "Новый пароль",
+	"No": "",
 	"No results found": "Результатов не найдено",
 	"No source available": "Нет доступных источников",
 	"Not factually correct": "Не фактически правильно",
@@ -385,6 +397,7 @@
 	"Select a model": "Выберите модель",
 	"Select an Ollama instance": "Выберите экземпляр Ollama",
 	"Select model": "Выберите модель",
+	"Selected models do not support image inputs": "",
 	"Send": "Отправить",
 	"Send a Message": "Отправить сообщение",
 	"Send message": "Отправить сообщение",
@@ -492,6 +505,7 @@
 	"Workspace": "Рабочая область",
 	"Write a prompt suggestion (e.g. Who are you?)": "Напишите предложение промпта (например, Кто вы?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Напишите резюме в 50 словах, которое кратко описывает [тему или ключевое слово].",
+	"Yes": "",
 	"Yesterday": "Вчера",
 	"You": "Вы",
 	"You have no archived conversations.": "У вас нет архивированных бесед.",

+ 14 - 0
src/lib/i18n/locales/sr-RS/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} размишља...",
 	"{{user}}'s Chats": "Ћаскања корисника {{user}}",
 	"{{webUIName}} Backend Required": "Захтева се {{webUIName}} позадинац",
+	"A selected model does not support image input": "",
 	"a user": "корисник",
 	"About": "О нама",
 	"Account": "Налог",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Напредни параметри",
 	"all": "сви",
 	"All Documents": "Сви документи",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Сви корисници",
 	"Allow": "Дозволи",
 	"Allow Chat Deletion": "Дозволи брисање ћаскања",
@@ -115,6 +117,7 @@
 	"Created at": "Направљено у",
 	"Created At": "Направљено у",
 	"Current Model": "Тренутни модел",
+	"Current Models": "",
 	"Current Password": "Тренутна лозинка",
 	"Custom": "Прилагођено",
 	"Customize Ollama models for a specific purpose": "Прилагоди Ollama моделе за специфичну намену",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Унесите LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Унесите LiteLLM модел (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Унесите највећи број жетона (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Унесите ознаку модела (нпр. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Унесите број корака (нпр. 50)",
 	"Enter Score": "Унесите резултат",
@@ -235,6 +239,7 @@
 	"Input commands": "Унеси наредбе",
 	"Interface": "Изглед",
 	"Invalid Tag": "Неисправна ознака",
+	"Is Model Vision Capable": "",
 	"January": "Јануар",
 	"join our Discord for help.": "придружите се нашем Дискорду за помоћ.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Израдила OpenWebUI заједница",
 	"Make sure to enclose them with": "Уверите се да их затворите са",
 	"Manage LiteLLM Models": "Управљај LiteLLM моделима",
+	"Manage Model Information": "",
 	"Manage Models": "Управљај моделима",
 	"Manage Ollama Models": "Управљај Ollama моделима",
 	"March": "Март",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Модел „{{modelTag}}“ је већ у реду за преузимање.",
 	"Model {{modelId}} not found": "Модел {{modelId}} није пронађен",
 	"Model {{modelName}} already exists.": "Модел {{modelName}} већ постоји.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Откривена путања система датотека модела. За ажурирање је потребан кратак назив модела, не може се наставити.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Назив модела",
 	"Model not selected": "Модел није изабран",
 	"Model Tag Name": "Назив ознаке модела",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Назовите вашу модел-датотеку",
 	"New Chat": "Ново ћаскање",
 	"New Password": "Нова лозинка",
+	"No": "",
 	"No results found": "Нема резултата",
 	"No source available": "Нема доступног извора",
 	"Not factually correct": "Није чињенично тачно",
@@ -385,6 +397,7 @@
 	"Select a model": "Изабери модел",
 	"Select an Ollama instance": "Изабери Ollama инстанцу",
 	"Select model": "Изабери модел",
+	"Selected models do not support image inputs": "",
 	"Send": "Пошаљи",
 	"Send a Message": "Пошаљи поруку",
 	"Send message": "Пошаљи поруку",
@@ -492,6 +505,7 @@
 	"Workspace": "Радни простор",
 	"Write a prompt suggestion (e.g. Who are you?)": "Напишите предлог упита (нпр. „ко си ти?“)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Напишите сажетак у 50 речи који резимира [тему или кључну реч].",
+	"Yes": "",
 	"Yesterday": "Јуче",
 	"You": "Ти",
 	"You have no archived conversations.": "Немате архивиране разговоре.",

+ 14 - 0
src/lib/i18n/locales/sv-SE/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} tänker...",
 	"{{user}}'s Chats": "{{user}}s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Backend krävs",
+	"A selected model does not support image input": "",
 	"a user": "en användare",
 	"About": "Om",
 	"Account": "Konto",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Avancerade parametrar",
 	"all": "alla",
 	"All Documents": "Alla dokument",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Alla användare",
 	"Allow": "Tillåt",
 	"Allow Chat Deletion": "Tillåt chattborttagning",
@@ -115,6 +117,7 @@
 	"Created at": "Skapad",
 	"Created At": "Skapad",
 	"Current Model": "Aktuell modell",
+	"Current Models": "",
 	"Current Password": "Nuvarande lösenord",
 	"Custom": "Anpassad",
 	"Customize Ollama models for a specific purpose": "Anpassa Ollama-modeller för ett specifikt ändamål",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Ange LiteLLM API RPM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Ange LiteLLM-modell (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Ange max antal tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Ange modelltagg (t.ex. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Ange antal steg (t.ex. 50)",
 	"Enter Score": "Ange poäng",
@@ -235,6 +239,7 @@
 	"Input commands": "Indatakommandon",
 	"Interface": "Gränssnitt",
 	"Invalid Tag": "Ogiltig tagg",
+	"Is Model Vision Capable": "",
 	"January": "januar",
 	"join our Discord for help.": "gå med i vår Discord för hjälp.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Skapad av OpenWebUI Community",
 	"Make sure to enclose them with": "Se till att bifoga dem med",
 	"Manage LiteLLM Models": "Hantera LiteLLM-modeller",
+	"Manage Model Information": "",
 	"Manage Models": "Hantera modeller",
 	"Manage Ollama Models": "Hantera Ollama-modeller",
 	"March": "mars",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Modellen '{{modelTag}}' är redan i kö för nedladdning.",
 	"Model {{modelId}} not found": "Modell {{modelId}} hittades inte",
 	"Model {{modelName}} already exists.": "Modellen {{modelName}} finns redan.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modellens filsystemväg upptäckt. Modellens kortnamn krävs för uppdatering, kan inte fortsätta.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Modellnamn",
 	"Model not selected": "Modell inte vald",
 	"Model Tag Name": "Modelltaggnamn",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Namnge din modelfil",
 	"New Chat": "Ny chatt",
 	"New Password": "Nytt lösenord",
+	"No": "",
 	"No results found": "Inga resultat hittades",
 	"No source available": "Ingen tilgjengelig kilde",
 	"Not factually correct": "Inte faktiskt korrekt",
@@ -385,6 +397,7 @@
 	"Select a model": "Välj en modell",
 	"Select an Ollama instance": "Välj en Ollama-instans",
 	"Select model": "Välj en modell",
+	"Selected models do not support image inputs": "",
 	"Send": "Skicka",
 	"Send a Message": "Skicka ett meddelande",
 	"Send message": "Skicka meddelande",
@@ -492,6 +505,7 @@
 	"Workspace": "arbetsyta",
 	"Write a prompt suggestion (e.g. Who are you?)": "Skriv ett förslag (t.ex. Vem är du?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Skriv en sammanfattning på 50 ord som sammanfattar [ämne eller nyckelord].",
+	"Yes": "",
 	"Yesterday": "Igenom",
 	"You": "du",
 	"You have no archived conversations.": "Du har inga arkiverade konversationer.",

+ 14 - 0
src/lib/i18n/locales/tr-TR/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} düşünüyor...",
 	"{{user}}'s Chats": "{{user}} Sohbetleri",
 	"{{webUIName}} Backend Required": "{{webUIName}} Arkayüz Gerekli",
+	"A selected model does not support image input": "",
 	"a user": "bir kullanıcı",
 	"About": "Hakkında",
 	"Account": "Hesap",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Gelişmiş Parametreler",
 	"all": "tümü",
 	"All Documents": "Tüm Belgeler",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Tüm Kullanıcılar",
 	"Allow": "İzin ver",
 	"Allow Chat Deletion": "Sohbet Silmeye İzin Ver",
@@ -115,6 +117,7 @@
 	"Created at": "Oluşturulma tarihi",
 	"Created At": "Şu Tarihte Oluşturuldu:",
 	"Current Model": "Mevcut Model",
+	"Current Models": "",
 	"Current Password": "Mevcut Parola",
 	"Custom": "Özel",
 	"Customize Ollama models for a specific purpose": "Ollama modellerini belirli bir amaç için özelleştirin",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM'ini Girin (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM Modelini Girin (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Maksimum Token Sayısını Girin (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Model etiketini girin (örn. {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Adım Sayısını Girin (örn. 50)",
 	"Enter Score": "Skoru Girin",
@@ -235,6 +239,7 @@
 	"Input commands": "Giriş komutları",
 	"Interface": "Arayüz",
 	"Invalid Tag": "Geçersiz etiket",
+	"Is Model Vision Capable": "",
 	"January": "Ocak",
 	"join our Discord for help.": "yardım için Discord'umuza katılın.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "OpenWebUI Topluluğu tarafından yapılmıştır",
 	"Make sure to enclose them with": "Değişkenlerinizi şu şekilde biçimlendirin:",
 	"Manage LiteLLM Models": "LiteLLM Modellerini Yönet",
+	"Manage Model Information": "",
 	"Manage Models": "Modelleri Yönet",
 	"Manage Ollama Models": "Ollama Modellerini Yönet",
 	"March": "Mart",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' zaten indirme sırasında.",
 	"Model {{modelId}} not found": "{{modelId}} bulunamadı",
 	"Model {{modelName}} already exists.": "{{modelName}} zaten mevcut.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model dosya sistemi yolu algılandı. Güncelleme için model kısa adı gerekli, devam edilemiyor.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Model Adı",
 	"Model not selected": "Model seçilmedi",
 	"Model Tag Name": "Model Etiket Adı",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Model dosyanıza ad verin",
 	"New Chat": "Yeni Sohbet",
 	"New Password": "Yeni Parola",
+	"No": "",
 	"No results found": "Sonuç bulunamadı",
 	"No source available": "Kaynak mevcut değil",
 	"Not factually correct": "Gerçeklere göre doğru değil",
@@ -385,6 +397,7 @@
 	"Select a model": "Bir model seç",
 	"Select an Ollama instance": "Bir Ollama örneği seçin",
 	"Select model": "Model seç",
+	"Selected models do not support image inputs": "",
 	"Send": "Gönder",
 	"Send a Message": "Bir Mesaj Gönder",
 	"Send message": "Mesaj gönder",
@@ -492,6 +505,7 @@
 	"Workspace": "Çalışma Alanı",
 	"Write a prompt suggestion (e.g. Who are you?)": "Bir prompt önerisi yazın (örn. Sen kimsin?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "[Konuyu veya anahtar kelimeyi] özetleyen 50 kelimelik bir özet yazın.",
+	"Yes": "",
 	"Yesterday": "Dün",
 	"You": "Sen",
 	"You have no archived conversations.": "Arşivlenmiş sohbetleriniz yok.",

+ 14 - 0
src/lib/i18n/locales/uk-UA/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} думає...",
 	"{{user}}'s Chats": "Чати {{user}}а",
 	"{{webUIName}} Backend Required": "Необхідно підключення бекенду {{webUIName}}",
+	"A selected model does not support image input": "",
 	"a user": "користувача",
 	"About": "Про програму",
 	"Account": "Обліковий запис",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Розширені параметри",
 	"all": "всі",
 	"All Documents": "Усі документи",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Всі користувачі",
 	"Allow": "Дозволити",
 	"Allow Chat Deletion": "Дозволити видалення чату",
@@ -115,6 +117,7 @@
 	"Created at": "Створено у",
 	"Created At": "Створено у",
 	"Current Model": "Поточна модель",
+	"Current Models": "",
 	"Current Password": "Поточний пароль",
 	"Custom": "Налаштувати",
 	"Customize Ollama models for a specific purpose": "Налаштувати моделі Ollama для конкретної мети",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Введіть RPM API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Введіть модель LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Введіть максимальну кількість токенів (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Введіть тег моделі (напр., {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Введіть кількість кроків (напр., 50)",
 	"Enter Score": "Введіть бал",
@@ -235,6 +239,7 @@
 	"Input commands": "Команди вводу",
 	"Interface": "Інтерфейс",
 	"Invalid Tag": "Недійсний тег",
+	"Is Model Vision Capable": "",
 	"January": "Січень",
 	"join our Discord for help.": "приєднуйтеся до нашого Discord для допомоги.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Зроблено спільнотою OpenWebUI",
 	"Make sure to enclose them with": "Переконайтеся, що вони закриті",
 	"Manage LiteLLM Models": "Керування моделями LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Керування моделями",
 	"Manage Ollama Models": "Керування моделями Ollama",
 	"March": "Березень",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Модель '{{modelTag}}' вже знаходиться в черзі на завантаження.",
 	"Model {{modelId}} not found": "Модель {{modelId}} не знайдено",
 	"Model {{modelName}} already exists.": "Модель {{modelName}} вже існує.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Виявлено шлях до файлової системи моделі. Для оновлення потрібно вказати коротке ім'я моделі, не вдасться продовжити.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Назва моделі",
 	"Model not selected": "Модель не вибрана",
 	"Model Tag Name": "Ім'я тегу моделі",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Назвіть свій файл моделі",
 	"New Chat": "Новий чат",
 	"New Password": "Новий пароль",
+	"No": "",
 	"No results found": "Не знайдено жодного результату",
 	"No source available": "Джерело не доступне",
 	"Not factually correct": "Не відповідає дійсності",
@@ -385,6 +397,7 @@
 	"Select a model": "Виберіть модель",
 	"Select an Ollama instance": "Виберіть екземпляр Ollama",
 	"Select model": "Вибрати модель",
+	"Selected models do not support image inputs": "",
 	"Send": "Надіслати",
 	"Send a Message": "Надіслати повідомлення",
 	"Send message": "Надіслати повідомлення",
@@ -492,6 +505,7 @@
 	"Workspace": "Робочий простір",
 	"Write a prompt suggestion (e.g. Who are you?)": "Напишіть промт (напр., Хто ти?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Напишіть стислий зміст у 50 слів, який узагальнює [тема або ключове слово].",
+	"Yes": "",
 	"Yesterday": "Вчора",
 	"You": "Ви",
 	"You have no archived conversations.": "У вас немає архівованих розмов.",

+ 14 - 0
src/lib/i18n/locales/vi-VN/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} đang suy nghĩ...",
 	"{{user}}'s Chats": "{{user}}'s Chats",
 	"{{webUIName}} Backend Required": "{{webUIName}} Yêu cầu Backend",
+	"A selected model does not support image input": "",
 	"a user": "người sử dụng",
 	"About": "Giới thiệu",
 	"Account": "Tài khoản",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "Các tham số Nâng cao",
 	"all": "tất cả",
 	"All Documents": "Tất cả tài liệu",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "Danh sách người sử dụng",
 	"Allow": "Cho phép",
 	"Allow Chat Deletion": "Cho phép Xóa nội dung chat",
@@ -115,6 +117,7 @@
 	"Created at": "Được tạo vào lúc",
 	"Created At": "Tạo lúc",
 	"Current Model": "Mô hình hiện tại",
+	"Current Models": "",
 	"Current Password": "Mật khẩu hiện tại",
 	"Custom": "Tùy chỉnh",
 	"Customize Ollama models for a specific purpose": "Tùy chỉnh các mô hình dựa trên Ollama cho một mục đích cụ thể",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Nhập RPM API LiteLLM (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "Nhập Mô hình LiteLLM (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "Nhập Số Token Tối đa (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "Nhập thẻ mô hình (vd: {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "Nhập số Steps (vd: 50)",
 	"Enter Score": "Nhập Score",
@@ -235,6 +239,7 @@
 	"Input commands": "Nhập các câu lệnh",
 	"Interface": "Giao diện",
 	"Invalid Tag": "Tag không hợp lệ",
+	"Is Model Vision Capable": "",
 	"January": "Tháng 1",
 	"join our Discord for help.": "tham gia Discord của chúng tôi để được trợ giúp.",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "Được tạo bởi Cộng đồng OpenWebUI",
 	"Make sure to enclose them with": "Hãy chắc chắn bao quanh chúng bằng",
 	"Manage LiteLLM Models": "Quản lý mô hình với LiteLLM",
+	"Manage Model Information": "",
 	"Manage Models": "Quản lý mô hình",
 	"Manage Ollama Models": "Quản lý mô hình với Ollama",
 	"March": "Tháng 3",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "Mô hình '{{modelTag}}' đã có trong hàng đợi để tải xuống.",
 	"Model {{modelId}} not found": "Không tìm thấy Mô hình {{modelId}}",
 	"Model {{modelName}} already exists.": "Mô hình {{modelName}} đã tồn tại.",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Đường dẫn hệ thống tệp mô hình được phát hiện. Tên viết tắt mô hình là bắt buộc để cập nhật, không thể tiếp tục.",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "Tên Mô hình",
 	"Model not selected": "Chưa chọn Mô hình",
 	"Model Tag Name": "Tên thẻ Mô hình",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "Đặt tên cho tệp mô hình của bạn",
 	"New Chat": "Tạo cuộc trò chuyện mới",
 	"New Password": "Mật khẩu mới",
+	"No": "",
 	"No results found": "Không tìm thấy kết quả",
 	"No source available": "Không có nguồn",
 	"Not factually correct": "Không chính xác so với thực tế",
@@ -385,6 +397,7 @@
 	"Select a model": "Chọn mô hình",
 	"Select an Ollama instance": "Chọn một thực thể Ollama",
 	"Select model": "Chọn model",
+	"Selected models do not support image inputs": "",
 	"Send": "Gửi",
 	"Send a Message": "Gửi yêu cầu",
 	"Send message": "Gửi yêu cầu",
@@ -492,6 +505,7 @@
 	"Workspace": "Workspace",
 	"Write a prompt suggestion (e.g. Who are you?)": "Hãy viết một prompt (vd: Bạn là ai?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Viết một tóm tắt trong vòng 50 từ cho [chủ đề hoặc từ khóa].",
+	"Yes": "",
 	"Yesterday": "Hôm qua",
 	"You": "Bạn",
 	"You have no archived conversations.": "Bạn chưa lưu trữ một nội dung chat nào",

+ 14 - 0
src/lib/i18n/locales/zh-CN/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} 正在思考...",
 	"{{user}}'s Chats": "{{user}} 的聊天记录",
 	"{{webUIName}} Backend Required": "需要 {{webUIName}} 后端",
+	"A selected model does not support image input": "",
 	"a user": "用户",
 	"About": "关于",
 	"Account": "账户",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "高级参数",
 	"all": "所有",
 	"All Documents": "所有文档",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "所有用户",
 	"Allow": "允许",
 	"Allow Chat Deletion": "允许删除聊天记录",
@@ -115,6 +117,7 @@
 	"Created at": "创建于",
 	"Created At": "创建于",
 	"Current Model": "当前模型",
+	"Current Models": "",
 	"Current Password": "当前密码",
 	"Custom": "自定义",
 	"Customize Ollama models for a specific purpose": "定制特定用途的 Ollama 模型",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "输入 LiteLLM API 速率限制 (litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "输入 LiteLLM 模型 (litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "输入模型的 Max Tokens (litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "输入模型标签 (例如{{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "输入步数 (例如 50)",
 	"Enter Score": "输入分",
@@ -235,6 +239,7 @@
 	"Input commands": "输入命令",
 	"Interface": "界面",
 	"Invalid Tag": "无效标签",
+	"Is Model Vision Capable": "",
 	"January": "一月",
 	"join our Discord for help.": "加入我们的 Discord 寻求帮助。",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "由 OpenWebUI 社区制作",
 	"Make sure to enclose them with": "确保将它们包含在内",
 	"Manage LiteLLM Models": "管理 LiteLLM 模型",
+	"Manage Model Information": "",
 	"Manage Models": "管理模型",
 	"Manage Ollama Models": "管理 Ollama 模型",
 	"March": "三月",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "模型'{{modelTag}}'已在下载队列中。",
 	"Model {{modelId}} not found": "未找到模型{{modelId}}",
 	"Model {{modelName}} already exists.": "模型{{modelName}}已存在。",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "检测到模型文件系统路径。模型简名是更新所必需的,无法继续。",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "模型名称",
 	"Model not selected": "未选择模型",
 	"Model Tag Name": "模型标签名称",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "命名你的模型文件",
 	"New Chat": "新聊天",
 	"New Password": "新密码",
+	"No": "",
 	"No results found": "未找到结果",
 	"No source available": "没有可用来源",
 	"Not factually correct": "与事实不符",
@@ -385,6 +397,7 @@
 	"Select a model": "选择一个模型",
 	"Select an Ollama instance": "选择一个 Ollama 实例",
 	"Select model": "选择模型",
+	"Selected models do not support image inputs": "",
 	"Send": "发送",
 	"Send a Message": "发送消息",
 	"Send message": "发送消息",
@@ -492,6 +505,7 @@
 	"Workspace": "工作空间",
 	"Write a prompt suggestion (e.g. Who are you?)": "写一个提示建议(例如:你是谁?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "用 50 个字写一个总结 [主题或关键词]。",
+	"Yes": "",
 	"Yesterday": "昨天",
 	"You": "你",
 	"You have no archived conversations.": "你没有存档的对话。",

+ 14 - 0
src/lib/i18n/locales/zh-TW/translation.json

@@ -6,6 +6,7 @@
 	"{{modelName}} is thinking...": "{{modelName}} 正在思考...",
 	"{{user}}'s Chats": "{{user}} 的聊天",
 	"{{webUIName}} Backend Required": "需要 {{webUIName}} 後台",
+	"A selected model does not support image input": "",
 	"a user": "使用者",
 	"About": "關於",
 	"Account": "帳號",
@@ -31,6 +32,7 @@
 	"Advanced Parameters": "進階參數",
 	"all": "所有",
 	"All Documents": "所有文件",
+	"All selected models do not support image input, removed images": "",
 	"All Users": "所有使用者",
 	"Allow": "允許",
 	"Allow Chat Deletion": "允許刪除聊天紀錄",
@@ -115,6 +117,7 @@
 	"Created at": "建立於",
 	"Created At": "建立於",
 	"Current Model": "目前模型",
+	"Current Models": "",
 	"Current Password": "目前密碼",
 	"Custom": "自訂",
 	"Customize Ollama models for a specific purpose": "定制特定用途的 Ollama 模型",
@@ -181,6 +184,7 @@
 	"Enter LiteLLM API RPM (litellm_params.rpm)": "輸入 LiteLLM API RPM(litellm_params.rpm)",
 	"Enter LiteLLM Model (litellm_params.model)": "輸入 LiteLLM 模型(litellm_params.model)",
 	"Enter Max Tokens (litellm_params.max_tokens)": "輸入最大 Token 數(litellm_params.max_tokens)",
+	"Enter Model Display Name": "",
 	"Enter model tag (e.g. {{modelTag}})": "輸入模型標籤(例如 {{modelTag}})",
 	"Enter Number of Steps (e.g. 50)": "輸入步數(例如 50)",
 	"Enter Score": "輸入分數",
@@ -235,6 +239,7 @@
 	"Input commands": "輸入命令",
 	"Interface": "介面",
 	"Invalid Tag": "無效標籤",
+	"Is Model Vision Capable": "",
 	"January": "1月",
 	"join our Discord for help.": "加入我們的 Discord 尋找幫助。",
 	"JSON": "JSON",
@@ -253,6 +258,7 @@
 	"Made by OpenWebUI Community": "由 OpenWebUI 社區製作",
 	"Make sure to enclose them with": "請確保變數有被以下符號框住:",
 	"Manage LiteLLM Models": "管理 LiteLLM 模型",
+	"Manage Model Information": "",
 	"Manage Models": "管理模組",
 	"Manage Ollama Models": "管理 Ollama 模型",
 	"March": "3月",
@@ -272,7 +278,12 @@
 	"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' 模型已經在下載佇列中。",
 	"Model {{modelId}} not found": "找不到 {{modelId}} 模型",
 	"Model {{modelName}} already exists.": "模型 {{modelName}} 已存在。",
+	"Model {{modelName}} is not vision capable": "",
+	"Model Description": "",
+	"Model Display Name": "",
 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "模型文件系統路徑已檢測。需要更新模型短名,無法繼續。",
+	"Model info for {{modelName}} added successfully": "",
+	"Model info for {{modelName}} deleted successfully": "",
 	"Model Name": "模型名稱",
 	"Model not selected": "未選擇模型",
 	"Model Tag Name": "模型標籤",
@@ -289,6 +300,7 @@
 	"Name your modelfile": "命名你的 Modelfile",
 	"New Chat": "新增聊天",
 	"New Password": "新密碼",
+	"No": "",
 	"No results found": "沒有找到結果",
 	"No source available": "沒有可用的來源",
 	"Not factually correct": "與真實資訊不相符",
@@ -385,6 +397,7 @@
 	"Select a model": "選擇一個模型",
 	"Select an Ollama instance": "選擇 Ollama 實例",
 	"Select model": "選擇模型",
+	"Selected models do not support image inputs": "",
 	"Send": "傳送",
 	"Send a Message": "傳送訊息",
 	"Send message": "傳送訊息",
@@ -492,6 +505,7 @@
 	"Workspace": "工作區",
 	"Write a prompt suggestion (e.g. Who are you?)": "寫一個提示詞建議(例如:你是誰?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "寫一個 50 字的摘要來概括 [主題或關鍵詞]。",
+	"Yes": "",
 	"Yesterday": "昨天",
 	"You": "你",
 	"You have no archived conversations.": "你沒有任何已封存的對話",

+ 11 - 9
src/lib/stores/index.ts

@@ -1,5 +1,6 @@
 import { APP_NAME } from '$lib/constants';
 import { type Writable, writable } from 'svelte/store';
+import type { GlobalModelConfig, ModelConfig } from '$lib/apis';
 
 // Backend
 export const WEBUI_NAME = writable(APP_NAME);
@@ -42,27 +43,27 @@ export const showSettings = writable(false);
 export const showArchivedChats = writable(false);
 export const showChangelog = writable(false);
 
-type Model = OpenAIModel | OllamaModel;
+export type Model = OpenAIModel | OllamaModel;
 
-type OpenAIModel = {
+type BaseModel = {
 	id: string;
 	name: string;
-	external: boolean;
-	source?: string;
+	info?: ModelConfig;
 };
 
-type OllamaModel = {
-	id: string;
-	name: string;
+export interface OpenAIModel extends BaseModel {
+	external: boolean;
+	source?: string;
+}
 
-	// Ollama specific fields
+export interface OllamaModel extends BaseModel {
 	details: OllamaModelDetails;
 	size: number;
 	description: string;
 	model: string;
 	modified_at: string;
 	digest: string;
-};
+}
 
 type OllamaModelDetails = {
 	parent_model: string;
@@ -133,6 +134,7 @@ type Config = {
 	default_models?: string[];
 	default_prompt_suggestions?: PromptSuggestion[];
 	trusted_header_auth?: boolean;
+	model_config?: GlobalModelConfig;
 };
 
 type PromptSuggestion = {

+ 0 - 24
src/lib/utils/index.ts

@@ -1,29 +1,5 @@
 import { v4 as uuidv4 } from 'uuid';
 import sha256 from 'js-sha256';
-import { getOllamaModels } from '$lib/apis/ollama';
-import { getOpenAIModels } from '$lib/apis/openai';
-import { getLiteLLMModels } from '$lib/apis/litellm';
-
-export const getModels = async (token: string) => {
-	let models = await Promise.all([
-		getOllamaModels(token).catch((error) => {
-			console.log(error);
-			return null;
-		}),
-		getOpenAIModels(token).catch((error) => {
-			console.log(error);
-			return null;
-		}),
-		getLiteLLMModels(token).catch((error) => {
-			console.log(error);
-			return null;
-		})
-	]);
-
-	models = models.filter((models) => models).reduce((a, e, i, arr) => a.concat(e), []);
-
-	return models;
-};
 
 //////////////////////////
 // Helper functions

+ 3 - 28
src/routes/(app)/+layout.svelte

@@ -7,9 +7,8 @@
 
 	import { goto } from '$app/navigation';
 
-	import { getModels as _getModels } from '$lib/utils';
+	import { getModels as _getModels } from '$lib/apis';
 	import { getOllamaVersion } from '$lib/apis/ollama';
-	import { getModelfiles } from '$lib/apis/modelfiles';
 	import { getPrompts } from '$lib/apis/prompts';
 
 	import { getDocs } from '$lib/apis/documents';
@@ -20,7 +19,6 @@
 		showSettings,
 		settings,
 		models,
-		modelfiles,
 		prompts,
 		documents,
 		tags,
@@ -50,21 +48,6 @@
 		return _getModels(localStorage.token);
 	};
 
-	const setOllamaVersion = async (version: string = '') => {
-		if (version === '') {
-			version = await getOllamaVersion(localStorage.token).catch((error) => {
-				return '';
-			});
-		}
-
-		ollamaVersion = version;
-
-		console.log(ollamaVersion);
-		if (compareVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
-			toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
-		}
-	};
-
 	onMount(async () => {
 		if ($user === undefined) {
 			await goto('/auth');
@@ -93,9 +76,6 @@
 				(async () => {
 					models.set(await getModels());
 				})(),
-				(async () => {
-					modelfiles.set(await getModelfiles(localStorage.token));
-				})(),
 				(async () => {
 					prompts.set(await getPrompts(localStorage.token));
 				})(),
@@ -107,11 +87,6 @@
 				})()
 			]);
 
-			modelfiles.subscribe(async () => {
-				// should fetch models
-				models.set(await getModels());
-			});
-
 			document.addEventListener('keydown', function (event) {
 				const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
 				// Check if the Shift key is pressed
@@ -188,12 +163,12 @@
 	});
 </script>
 
-<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
+<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-10">
 	<Tooltip content={$i18n.t('Help')} placement="left">
 		<button
 			id="show-shortcuts-button"
 			bind:this={showShortcutsButtonElement}
-			class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full"
+			class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full"
 			on:click={() => {
 				showShortcuts = !showShortcuts;
 			}}

+ 2 - 2
src/routes/(app)/workspace/+layout.svelte

@@ -39,10 +39,10 @@
 			class="flex scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-xl bg-transparent/10 p-1"
 		>
 			<a
-				class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/modelfiles')
+				class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/models')
 					? 'bg-gray-50 dark:bg-gray-850'
 					: ''} transition"
-				href="/workspace/modelfiles">{$i18n.t('Modelfiles')}</a
+				href="/workspace/models">{$i18n.t('Models')}</a
 			>
 
 			<a

+ 1 - 1
src/routes/(app)/workspace/+page.svelte

@@ -3,6 +3,6 @@
 	import { onMount } from 'svelte';
 
 	onMount(() => {
-		goto('/workspace/modelfiles');
+		goto('/workspace/models');
 	});
 </script>

+ 0 - 5
src/routes/(app)/workspace/modelfiles/+page.svelte

@@ -1,5 +0,0 @@
-<script>
-	import Modelfiles from '$lib/components/workspace/Modelfiles.svelte';
-</script>
-
-<Modelfiles />

+ 0 - 721
src/routes/(app)/workspace/modelfiles/create/+page.svelte

@@ -1,721 +0,0 @@
-<script>
-	import { v4 as uuidv4 } from 'uuid';
-	import { toast } from 'svelte-sonner';
-	import { goto } from '$app/navigation';
-	import { settings, user, config, modelfiles, models } from '$lib/stores';
-
-	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
-	import { splitStream } from '$lib/utils';
-	import { onMount, tick, getContext } from 'svelte';
-	import { createModel } from '$lib/apis/ollama';
-	import { createNewModelfile, getModelfileByTagName, getModelfiles } from '$lib/apis/modelfiles';
-
-	const i18n = getContext('i18n');
-
-	let loading = false;
-
-	let filesInputElement;
-	let inputFiles;
-	let imageUrl = null;
-	let digest = '';
-	let pullProgress = null;
-	let success = false;
-
-	// ///////////
-	// Modelfile
-	// ///////////
-
-	let title = '';
-	let tagName = '';
-	let desc = '';
-
-	let raw = true;
-	let advanced = false;
-
-	// Raw Mode
-	let content = '';
-
-	// Builder Mode
-	let model = '';
-	let system = '';
-	let template = '';
-	let options = {
-		// Advanced
-		seed: 0,
-		stop: '',
-		temperature: '',
-		repeat_penalty: '',
-		repeat_last_n: '',
-		mirostat: '',
-		mirostat_eta: '',
-		mirostat_tau: '',
-		top_k: '',
-		top_p: '',
-		tfs_z: '',
-		num_ctx: '',
-		num_predict: ''
-	};
-
-	let modelfileCreator = null;
-
-	$: tagName = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}:latest` : '';
-
-	$: if (!raw) {
-		content = `FROM ${model}
-${template !== '' ? `TEMPLATE """${template}"""` : ''}
-${options.seed !== 0 ? `PARAMETER seed ${options.seed}` : ''}
-${options.stop !== '' ? `PARAMETER stop ${options.stop}` : ''}
-${options.temperature !== '' ? `PARAMETER temperature ${options.temperature}` : ''}
-${options.repeat_penalty !== '' ? `PARAMETER repeat_penalty ${options.repeat_penalty}` : ''}
-${options.repeat_last_n !== '' ? `PARAMETER repeat_last_n ${options.repeat_last_n}` : ''}
-${options.mirostat !== '' ? `PARAMETER mirostat ${options.mirostat}` : ''}
-${options.mirostat_eta !== '' ? `PARAMETER mirostat_eta ${options.mirostat_eta}` : ''}
-${options.mirostat_tau !== '' ? `PARAMETER mirostat_tau ${options.mirostat_tau}` : ''}
-${options.top_k !== '' ? `PARAMETER top_k ${options.top_k}` : ''}
-${options.top_p !== '' ? `PARAMETER top_p ${options.top_p}` : ''}
-${options.tfs_z !== '' ? `PARAMETER tfs_z ${options.tfs_z}` : ''}
-${options.num_ctx !== '' ? `PARAMETER num_ctx ${options.num_ctx}` : ''}
-${options.num_predict !== '' ? `PARAMETER num_predict ${options.num_predict}` : ''}
-SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
-	}
-
-	let suggestions = [
-		{
-			content: ''
-		}
-	];
-
-	let categories = {
-		character: false,
-		assistant: false,
-		writing: false,
-		productivity: false,
-		programming: false,
-		'data analysis': false,
-		lifestyle: false,
-		education: false,
-		business: false
-	};
-
-	const saveModelfile = async (modelfile) => {
-		await createNewModelfile(localStorage.token, modelfile);
-		await modelfiles.set(await getModelfiles(localStorage.token));
-	};
-
-	const submitHandler = async () => {
-		loading = true;
-
-		if (Object.keys(categories).filter((category) => categories[category]).length == 0) {
-			toast.error(
-				'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.'
-			);
-			loading = false;
-			success = false;
-			return success;
-		}
-
-		if (
-			$models.map((model) => model.name).includes(tagName) ||
-			(await getModelfileByTagName(localStorage.token, tagName).catch(() => false))
-		) {
-			toast.error(
-				`Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name to complete your modelfile.`
-			);
-			loading = false;
-			success = false;
-			return success;
-		}
-
-		if (
-			title !== '' &&
-			desc !== '' &&
-			content !== '' &&
-			Object.keys(categories).filter((category) => categories[category]).length > 0 &&
-			!$models.includes(tagName)
-		) {
-			const res = await createModel(localStorage.token, tagName, content);
-
-			if (res) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					try {
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (line !== '') {
-								console.log(line);
-								let data = JSON.parse(line);
-								console.log(data);
-
-								if (data.error) {
-									throw data.error;
-								}
-								if (data.detail) {
-									throw data.detail;
-								}
-
-								if (data.status) {
-									if (
-										!data.digest &&
-										!data.status.includes('writing') &&
-										!data.status.includes('sha256')
-									) {
-										toast.success(data.status);
-
-										if (data.status === 'success') {
-											success = true;
-										}
-									} else {
-										if (data.digest) {
-											digest = data.digest;
-
-											if (data.completed) {
-												pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
-											} else {
-												pullProgress = 100;
-											}
-										}
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-						toast.error(error);
-					}
-				}
-			}
-
-			if (success) {
-				await saveModelfile({
-					tagName: tagName,
-					imageUrl: imageUrl,
-					title: title,
-					desc: desc,
-					content: content,
-					suggestionPrompts: suggestions.filter((prompt) => prompt.content !== ''),
-					categories: Object.keys(categories).filter((category) => categories[category]),
-					user: modelfileCreator !== null ? modelfileCreator : undefined
-				});
-				await goto('/workspace/modelfiles');
-			}
-		}
-		loading = false;
-		success = false;
-	};
-
-	onMount(async () => {
-		window.addEventListener('message', async (event) => {
-			if (
-				![
-					'https://ollamahub.com',
-					'https://www.ollamahub.com',
-					'https://openwebui.com',
-					'https://www.openwebui.com',
-					'http://localhost:5173'
-				].includes(event.origin)
-			)
-				return;
-			const modelfile = JSON.parse(event.data);
-			console.log(modelfile);
-
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			await tick();
-			tagName = `${modelfile.user.username === 'hub' ? '' : `hub/`}${modelfile.user.username}/${
-				modelfile.tagName
-			}`;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			modelfileCreator = {
-				username: modelfile.user.username,
-				name: modelfile.user.name
-			};
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
-		});
-
-		if (window.opener ?? false) {
-			window.opener.postMessage('loaded', '*');
-		}
-
-		if (sessionStorage.modelfile) {
-			const modelfile = JSON.parse(sessionStorage.modelfile);
-			console.log(modelfile);
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			await tick();
-			tagName = modelfile.tagName;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
-
-			sessionStorage.removeItem('modelfile');
-		}
-	});
-</script>
-
-<div class="w-full max-h-full">
-	<input
-		bind:this={filesInputElement}
-		bind:files={inputFiles}
-		type="file"
-		hidden
-		accept="image/*"
-		on:change={() => {
-			let reader = new FileReader();
-			reader.onload = (event) => {
-				let originalImageUrl = `${event.target.result}`;
-
-				const img = new Image();
-				img.src = originalImageUrl;
-
-				img.onload = function () {
-					const canvas = document.createElement('canvas');
-					const ctx = canvas.getContext('2d');
-
-					// Calculate the aspect ratio of the image
-					const aspectRatio = img.width / img.height;
-
-					// Calculate the new width and height to fit within 100x100
-					let newWidth, newHeight;
-					if (aspectRatio > 1) {
-						newWidth = 100 * aspectRatio;
-						newHeight = 100;
-					} else {
-						newWidth = 100;
-						newHeight = 100 / aspectRatio;
-					}
-
-					// Set the canvas size
-					canvas.width = 100;
-					canvas.height = 100;
-
-					// Calculate the position to center the image
-					const offsetX = (100 - newWidth) / 2;
-					const offsetY = (100 - newHeight) / 2;
-
-					// Draw the image on the canvas
-					ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
-
-					// Get the base64 representation of the compressed image
-					const compressedSrc = canvas.toDataURL('image/jpeg');
-
-					// Display the compressed image
-					imageUrl = compressedSrc;
-
-					inputFiles = null;
-				};
-			};
-
-			if (
-				inputFiles &&
-				inputFiles.length > 0 &&
-				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
-			) {
-				reader.readAsDataURL(inputFiles[0]);
-			} else {
-				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
-				inputFiles = null;
-			}
-		}}
-	/>
-
-	<button
-		class="flex space-x-1"
-		on:click={() => {
-			history.back();
-		}}
-	>
-		<div class=" self-center">
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				viewBox="0 0 20 20"
-				fill="currentColor"
-				class="w-4 h-4"
-			>
-				<path
-					fill-rule="evenodd"
-					d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
-					clip-rule="evenodd"
-				/>
-			</svg>
-		</div>
-		<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
-	</button>
-	<!-- <hr class="my-3 dark:border-gray-700" /> -->
-
-	<form
-		class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
-		on:submit|preventDefault={() => {
-			submitHandler();
-		}}
-	>
-		<div class="flex justify-center my-4">
-			<div class="self-center">
-				<button
-					class=" {imageUrl
-						? ''
-						: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
-					type="button"
-					on:click={() => {
-						filesInputElement.click();
-					}}
-				>
-					{#if imageUrl}
-						<img
-							src={imageUrl}
-							alt="modelfile profile"
-							class=" rounded-full w-20 h-20 object-cover"
-						/>
-					{:else}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							class="w-8"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					{/if}
-				</button>
-			</div>
-		</div>
-
-		<div class="my-2 flex space-x-2">
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
-
-				<div>
-					<input
-						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Name your modelfile')}
-						bind:value={title}
-						required
-					/>
-				</div>
-			</div>
-
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
-
-				<div>
-					<input
-						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Add a model tag name')}
-						bind:value={tagName}
-						required
-					/>
-				</div>
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
-
-			<div>
-				<input
-					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-					placeholder={$i18n.t('Add a short description about what this modelfile does')}
-					bind:value={desc}
-					required
-				/>
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class="flex w-full justify-between">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					type="button"
-					on:click={() => {
-						raw = !raw;
-					}}
-				>
-					{#if raw}
-						<span class="ml-2 self-center"> {$i18n.t('Raw Format')} </span>
-					{:else}
-						<span class="ml-2 self-center"> {$i18n.t('Builder Mode')} </span>
-					{/if}
-				</button>
-			</div>
-
-			<!-- <div class=" text-sm font-semibold mb-2"></div> -->
-
-			{#if raw}
-				<div class="mt-2">
-					<div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
-
-					<div>
-						<textarea
-							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-							placeholder={`FROM llama2\nPARAMETER temperature 1\nSYSTEM """\nYou are Mario from Super Mario Bros, acting as an assistant.\n"""`}
-							rows="6"
-							bind:value={content}
-							required
-						/>
-					</div>
-
-					<div class="text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t('Not sure what to write? Switch to')}
-						<button
-							class="text-gray-500 dark:text-gray-300 font-medium cursor-pointer"
-							type="button"
-							on:click={() => {
-								raw = !raw;
-							}}>{$i18n.t('Builder Mode')}</button
-						>
-						or
-						<a
-							class=" text-gray-500 dark:text-gray-300 font-medium"
-							href="https://openwebui.com"
-							target="_blank"
-						>
-							{$i18n.t('Click here to check other modelfiles.')}
-						</a>
-					</div>
-				</div>
-			{:else}
-				<div class="my-2">
-					<div class=" text-xs font-semibold mb-2">{$i18n.t('From (Base Model)')}*</div>
-
-					<div>
-						<input
-							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-							placeholder="Write a modelfile base model name (e.g. llama2, mistral)"
-							bind:value={model}
-							required
-						/>
-					</div>
-
-					<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t('To access the available model names for downloading,')}
-						<a
-							class=" text-gray-500 dark:text-gray-300 font-medium"
-							href="https://ollama.com/library"
-							target="_blank">{$i18n.t('click here.')}</a
-						>
-					</div>
-				</div>
-
-				<div class="my-1">
-					<div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
-
-					<div>
-						<textarea
-							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
-							placeholder={`Write your modelfile system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
-							rows="4"
-							bind:value={system}
-						/>
-					</div>
-				</div>
-
-				<div class="flex w-full justify-between">
-					<div class=" self-center text-sm font-semibold">
-						{$i18n.t('Modelfile Advanced Settings')}
-					</div>
-
-					<button
-						class="p-1 px-3 text-xs flex rounded transition"
-						type="button"
-						on:click={() => {
-							advanced = !advanced;
-						}}
-					>
-						{#if advanced}
-							<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
-						{:else}
-							<span class="ml-2 self-center">{$i18n.t('Default')}</span>
-						{/if}
-					</button>
-				</div>
-
-				{#if advanced}
-					<div class="my-2">
-						<div class=" text-xs font-semibold mb-2">{$i18n.t('Template')}</div>
-
-						<div>
-							<textarea
-								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
-								placeholder="Write your modelfile template content here"
-								rows="4"
-								bind:value={template}
-							/>
-						</div>
-					</div>
-
-					<div class="my-2">
-						<div class=" text-xs font-semibold mb-2">{$i18n.t('Parameters')}</div>
-
-						<div>
-							<AdvancedParams bind:options />
-						</div>
-					</div>
-				{/if}
-			{/if}
-		</div>
-
-		<div class="my-2">
-			<div class="flex w-full justify-between mb-2">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					type="button"
-					on:click={() => {
-						if (suggestions.length === 0 || suggestions.at(-1).content !== '') {
-							suggestions = [...suggestions, { content: '' }];
-						}
-					}}
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 20 20"
-						fill="currentColor"
-						class="w-4 h-4"
-					>
-						<path
-							d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
-						/>
-					</svg>
-				</button>
-			</div>
-			<div class="flex flex-col space-y-1">
-				{#each suggestions as prompt, promptIdx}
-					<div class=" flex border dark:border-gray-600 rounded-lg">
-						<input
-							class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
-							placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
-							bind:value={prompt.content}
-						/>
-
-						<button
-							class="px-2"
-							type="button"
-							on:click={() => {
-								suggestions.splice(promptIdx, 1);
-								suggestions = suggestions;
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 20 20"
-								fill="currentColor"
-								class="w-4 h-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"
-								/>
-							</svg>
-						</button>
-					</div>
-				{/each}
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
-
-			<div class="grid grid-cols-4">
-				{#each Object.keys(categories) as category}
-					<div class="flex space-x-2 text-sm">
-						<input type="checkbox" bind:checked={categories[category]} />
-						<div class="capitalize">{category}</div>
-					</div>
-				{/each}
-			</div>
-		</div>
-
-		{#if pullProgress !== null}
-			<div class="my-2">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
-				<div class="w-full rounded-full dark:bg-gray-800">
-					<div
-						class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
-						style="width: {Math.max(15, pullProgress ?? 0)}%"
-					>
-						{pullProgress ?? 0}%
-					</div>
-				</div>
-				<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-					{digest}
-				</div>
-			</div>
-		{/if}
-
-		<div class="my-2 flex justify-end">
-			<button
-				class=" text-sm px-3 py-2 transition rounded-xl {loading
-					? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
-					: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
-				type="submit"
-				disabled={loading}
-			>
-				<div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
-
-				{#if loading}
-					<div class="ml-1.5 self-center">
-						<svg
-							class=" w-4 h-4"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							xmlns="http://www.w3.org/2000/svg"
-							><style>
-								.spinner_ajPY {
-									transform-origin: center;
-									animation: spinner_AtaB 0.75s infinite linear;
-								}
-								@keyframes spinner_AtaB {
-									100% {
-										transform: rotate(360deg);
-									}
-								}
-							</style><path
-								d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-								opacity=".25"
-							/><path
-								d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-								class="spinner_ajPY"
-							/></svg
-						>
-					</div>
-				{/if}
-			</button>
-		</div>
-	</form>
-</div>

+ 0 - 507
src/routes/(app)/workspace/modelfiles/edit/+page.svelte

@@ -1,507 +0,0 @@
-<script>
-	import { v4 as uuidv4 } from 'uuid';
-	import { toast } from 'svelte-sonner';
-	import { goto } from '$app/navigation';
-
-	import { onMount, getContext } from 'svelte';
-	import { page } from '$app/stores';
-
-	import { settings, user, config, modelfiles } from '$lib/stores';
-	import { splitStream } from '$lib/utils';
-
-	import { createModel } from '$lib/apis/ollama';
-	import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles';
-
-	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
-
-	const i18n = getContext('i18n');
-
-	let loading = false;
-
-	let filesInputElement;
-	let inputFiles;
-	let imageUrl = null;
-	let digest = '';
-	let pullProgress = null;
-	let success = false;
-
-	let modelfile = null;
-	// ///////////
-	// Modelfile
-	// ///////////
-
-	let title = '';
-	let tagName = '';
-	let desc = '';
-
-	// Raw Mode
-	let content = '';
-
-	let suggestions = [
-		{
-			content: ''
-		}
-	];
-
-	let categories = {
-		character: false,
-		assistant: false,
-		writing: false,
-		productivity: false,
-		programming: false,
-		'data analysis': false,
-		lifestyle: false,
-		education: false,
-		business: false
-	};
-
-	onMount(() => {
-		tagName = $page.url.searchParams.get('tag');
-
-		if (tagName) {
-			modelfile = $modelfiles.filter((modelfile) => modelfile.tagName === tagName)[0];
-
-			console.log(modelfile);
-
-			imageUrl = modelfile.imageUrl;
-			title = modelfile.title;
-			desc = modelfile.desc;
-			content = modelfile.content;
-			suggestions =
-				modelfile.suggestionPrompts.length != 0
-					? modelfile.suggestionPrompts
-					: [
-							{
-								content: ''
-							}
-					  ];
-
-			for (const category of modelfile.categories) {
-				categories[category.toLowerCase()] = true;
-			}
-		} else {
-			goto('/workspace/modelfiles');
-		}
-	});
-
-	const updateModelfile = async (modelfile) => {
-		await updateModelfileByTagName(localStorage.token, modelfile.tagName, modelfile);
-		await modelfiles.set(await getModelfiles(localStorage.token));
-	};
-
-	const updateHandler = async () => {
-		loading = true;
-
-		if (Object.keys(categories).filter((category) => categories[category]).length == 0) {
-			toast.error(
-				'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.'
-			);
-		}
-
-		if (
-			title !== '' &&
-			desc !== '' &&
-			content !== '' &&
-			Object.keys(categories).filter((category) => categories[category]).length > 0
-		) {
-			const res = await createModel(localStorage.token, tagName, content);
-
-			if (res) {
-				const reader = res.body
-					.pipeThrough(new TextDecoderStream())
-					.pipeThrough(splitStream('\n'))
-					.getReader();
-
-				while (true) {
-					const { value, done } = await reader.read();
-					if (done) break;
-
-					try {
-						let lines = value.split('\n');
-
-						for (const line of lines) {
-							if (line !== '') {
-								console.log(line);
-								let data = JSON.parse(line);
-								console.log(data);
-
-								if (data.error) {
-									throw data.error;
-								}
-								if (data.detail) {
-									throw data.detail;
-								}
-
-								if (data.status) {
-									if (
-										!data.digest &&
-										!data.status.includes('writing') &&
-										!data.status.includes('sha256')
-									) {
-										toast.success(data.status);
-
-										if (data.status === 'success') {
-											success = true;
-										}
-									} else {
-										if (data.digest) {
-											digest = data.digest;
-
-											if (data.completed) {
-												pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
-											} else {
-												pullProgress = 100;
-											}
-										}
-									}
-								}
-							}
-						}
-					} catch (error) {
-						console.log(error);
-						toast.error(error);
-					}
-				}
-			}
-
-			if (success) {
-				await updateModelfile({
-					tagName: tagName,
-					imageUrl: imageUrl,
-					title: title,
-					desc: desc,
-					content: content,
-					suggestionPrompts: suggestions.filter((prompt) => prompt.content !== ''),
-					categories: Object.keys(categories).filter((category) => categories[category])
-				});
-				await goto('/workspace/modelfiles');
-			}
-		}
-		loading = false;
-		success = false;
-	};
-</script>
-
-<div class="w-full max-h-full">
-	<input
-		bind:this={filesInputElement}
-		bind:files={inputFiles}
-		type="file"
-		hidden
-		accept="image/*"
-		on:change={() => {
-			let reader = new FileReader();
-			reader.onload = (event) => {
-				let originalImageUrl = `${event.target.result}`;
-
-				const img = new Image();
-				img.src = originalImageUrl;
-
-				img.onload = function () {
-					const canvas = document.createElement('canvas');
-					const ctx = canvas.getContext('2d');
-
-					// Calculate the aspect ratio of the image
-					const aspectRatio = img.width / img.height;
-
-					// Calculate the new width and height to fit within 100x100
-					let newWidth, newHeight;
-					if (aspectRatio > 1) {
-						newWidth = 100 * aspectRatio;
-						newHeight = 100;
-					} else {
-						newWidth = 100;
-						newHeight = 100 / aspectRatio;
-					}
-
-					// Set the canvas size
-					canvas.width = 100;
-					canvas.height = 100;
-
-					// Calculate the position to center the image
-					const offsetX = (100 - newWidth) / 2;
-					const offsetY = (100 - newHeight) / 2;
-
-					// Draw the image on the canvas
-					ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
-
-					// Get the base64 representation of the compressed image
-					const compressedSrc = canvas.toDataURL('image/jpeg');
-
-					// Display the compressed image
-					imageUrl = compressedSrc;
-
-					inputFiles = null;
-				};
-			};
-
-			if (
-				inputFiles &&
-				inputFiles.length > 0 &&
-				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
-			) {
-				reader.readAsDataURL(inputFiles[0]);
-			} else {
-				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
-				inputFiles = null;
-			}
-		}}
-	/>
-
-	<button
-		class="flex space-x-1"
-		on:click={() => {
-			history.back();
-		}}
-	>
-		<div class=" self-center">
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				viewBox="0 0 20 20"
-				fill="currentColor"
-				class="w-4 h-4"
-			>
-				<path
-					fill-rule="evenodd"
-					d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
-					clip-rule="evenodd"
-				/>
-			</svg>
-		</div>
-		<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
-	</button>
-	<form
-		class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
-		on:submit|preventDefault={() => {
-			updateHandler();
-		}}
-	>
-		<div class="flex justify-center my-4">
-			<div class="self-center">
-				<button
-					class=" {imageUrl
-						? ''
-						: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
-					type="button"
-					on:click={() => {
-						filesInputElement.click();
-					}}
-				>
-					{#if imageUrl}
-						<img
-							src={imageUrl}
-							alt="modelfile profile"
-							class=" rounded-full w-20 h-20 object-cover"
-						/>
-					{:else}
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							class="w-8"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
-								clip-rule="evenodd"
-							/>
-						</svg>
-					{/if}
-				</button>
-			</div>
-		</div>
-
-		<div class="my-2 flex space-x-2">
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
-
-				<div>
-					<input
-						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Name your modelfile')}
-						bind:value={title}
-						required
-					/>
-				</div>
-			</div>
-
-			<div class="flex-1">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div>
-
-				<div>
-					<input
-						class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={$i18n.t('Add a model tag name')}
-						value={tagName}
-						disabled
-						required
-					/>
-				</div>
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
-
-			<div>
-				<input
-					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-					placeholder={$i18n.t('Add a short description about what this modelfile does')}
-					bind:value={desc}
-					required
-				/>
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class="flex w-full justify-between">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div>
-			</div>
-
-			<!-- <div class=" text-sm font-semibold mb-2"></div> -->
-
-			<div class="mt-2">
-				<div class=" text-xs font-semibold mb-2">{$i18n.t('Content')}*</div>
-
-				<div>
-					<textarea
-						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
-						placeholder={`FROM llama2\nPARAMETER temperature 1\nSYSTEM """\nYou are Mario from Super Mario Bros, acting as an assistant.\n"""`}
-						rows="6"
-						bind:value={content}
-						required
-					/>
-				</div>
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class="flex w-full justify-between mb-2">
-				<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
-
-				<button
-					class="p-1 px-3 text-xs flex rounded transition"
-					type="button"
-					on:click={() => {
-						if (suggestions.length === 0 || suggestions.at(-1).content !== '') {
-							suggestions = [...suggestions, { content: '' }];
-						}
-					}}
-				>
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 20 20"
-						fill="currentColor"
-						class="w-4 h-4"
-					>
-						<path
-							d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
-						/>
-					</svg>
-				</button>
-			</div>
-			<div class="flex flex-col space-y-1">
-				{#each suggestions as prompt, promptIdx}
-					<div class=" flex border dark:border-gray-600 rounded-lg">
-						<input
-							class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
-							placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
-							bind:value={prompt.content}
-						/>
-
-						<button
-							class="px-2"
-							type="button"
-							on:click={() => {
-								suggestions.splice(promptIdx, 1);
-								suggestions = suggestions;
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 20 20"
-								fill="currentColor"
-								class="w-4 h-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"
-								/>
-							</svg>
-						</button>
-					</div>
-				{/each}
-			</div>
-		</div>
-
-		<div class="my-2">
-			<div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div>
-
-			<div class="grid grid-cols-4">
-				{#each Object.keys(categories) as category}
-					<div class="flex space-x-2 text-sm">
-						<input type="checkbox" bind:checked={categories[category]} />
-
-						<div class=" capitalize">{category}</div>
-					</div>
-				{/each}
-			</div>
-		</div>
-
-		{#if pullProgress !== null}
-			<div class="my-2">
-				<div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div>
-				<div class="w-full rounded-full dark:bg-gray-800">
-					<div
-						class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
-						style="width: {Math.max(15, pullProgress ?? 0)}%"
-					>
-						{pullProgress ?? 0}%
-					</div>
-				</div>
-				<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
-					{digest}
-				</div>
-			</div>
-		{/if}
-
-		<div class="my-2 flex justify-end">
-			<button
-				class=" text-sm px-3 py-2 transition rounded-xl {loading
-					? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
-					: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
-				type="submit"
-				disabled={loading}
-			>
-				<div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
-
-				{#if loading}
-					<div class="ml-1.5 self-center">
-						<svg
-							class=" w-4 h-4"
-							viewBox="0 0 24 24"
-							fill="currentColor"
-							xmlns="http://www.w3.org/2000/svg"
-							><style>
-								.spinner_ajPY {
-									transform-origin: center;
-									animation: spinner_AtaB 0.75s infinite linear;
-								}
-								@keyframes spinner_AtaB {
-									100% {
-										transform: rotate(360deg);
-									}
-								}
-							</style><path
-								d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-								opacity=".25"
-							/><path
-								d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-								class="spinner_ajPY"
-							/></svg
-						>
-					</div>
-				{/if}
-			</button>
-		</div>
-	</form>
-</div>

+ 5 - 0
src/routes/(app)/workspace/models/+page.svelte

@@ -0,0 +1,5 @@
+<script>
+	import Models from '$lib/components/workspace/Models.svelte';
+</script>
+
+<Models />

+ 576 - 0
src/routes/(app)/workspace/models/create/+page.svelte

@@ -0,0 +1,576 @@
+<script>
+	import { v4 as uuidv4 } from 'uuid';
+	import { toast } from 'svelte-sonner';
+	import { goto } from '$app/navigation';
+	import { settings, user, config, models } from '$lib/stores';
+
+	import { onMount, tick, getContext } from 'svelte';
+	import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
+	import { getModels } from '$lib/apis';
+
+	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+
+	const i18n = getContext('i18n');
+
+	let filesInputElement;
+	let inputFiles;
+
+	let showAdvanced = false;
+	let showPreview = false;
+
+	let loading = false;
+	let success = false;
+
+	// ///////////
+	// Model
+	// ///////////
+
+	let id = '';
+	let name = '';
+
+	let params = {};
+	let capabilities = {
+		vision: true
+	};
+
+	let info = {
+		id: '',
+		base_model_id: null,
+		name: '',
+		meta: {
+			profile_image_url: null,
+			description: '',
+			suggestion_prompts: [
+				{
+					content: ''
+				}
+			]
+		},
+		params: {
+			system: ''
+		}
+	};
+
+	$: if (name) {
+		id = name.replace(/\s+/g, '-').toLowerCase();
+	}
+
+	const submitHandler = async () => {
+		loading = true;
+
+		info.id = id;
+		info.name = name;
+		info.meta.capabilities = capabilities;
+		info.params.stop = params.stop !== null ? params.stop.split(',').filter((s) => s.trim()) : null;
+
+		if ($models.find((m) => m.id === info.id)) {
+			toast.error(
+				`Error: A model with the ID '${info.id}' already exists. Please select a different ID to proceed.`
+			);
+			loading = false;
+			success = false;
+			return success;
+		}
+
+		if (info) {
+			const res = await addNewModel(localStorage.token, {
+				...info,
+				meta: {
+					...info.meta,
+					profile_image_url: info.meta.profile_image_url ?? '/favicon.png',
+					suggestion_prompts: info.meta.suggestion_prompts
+						? info.meta.suggestion_prompts.filter((prompt) => prompt.content !== '')
+						: null
+				},
+				params: { ...info.params, ...params }
+			});
+
+			if (res) {
+				toast.success('Model created successfully!');
+				await goto('/workspace/models');
+				await models.set(await getModels(localStorage.token));
+			}
+		}
+
+		loading = false;
+		success = false;
+	};
+
+	const initModel = async (model) => {
+		name = model.name;
+		await tick();
+
+		id = model.id;
+
+		params = { ...params, ...model?.info?.params };
+		params.stop = params?.stop ? (params?.stop ?? []).join(',') : null;
+
+		capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) };
+
+		info = {
+			...info,
+			...model.info
+		};
+	};
+
+	onMount(async () => {
+		window.addEventListener('message', async (event) => {
+			if (
+				![
+					'https://ollamahub.com',
+					'https://www.ollamahub.com',
+					'https://openwebui.com',
+					'https://www.openwebui.com',
+					'http://localhost:5173'
+				].includes(event.origin)
+			)
+				return;
+
+			const model = JSON.parse(event.data);
+			console.log(model);
+
+			initModel(model);
+		});
+
+		if (window.opener ?? false) {
+			window.opener.postMessage('loaded', '*');
+		}
+
+		if (sessionStorage.model) {
+			const model = JSON.parse(sessionStorage.model);
+			sessionStorage.removeItem('model');
+
+			console.log(model);
+			initModel(model);
+		}
+	});
+</script>
+
+<div class="w-full max-h-full">
+	<input
+		bind:this={filesInputElement}
+		bind:files={inputFiles}
+		type="file"
+		hidden
+		accept="image/*"
+		on:change={() => {
+			let reader = new FileReader();
+			reader.onload = (event) => {
+				let originalImageUrl = `${event.target.result}`;
+
+				const img = new Image();
+				img.src = originalImageUrl;
+
+				img.onload = function () {
+					const canvas = document.createElement('canvas');
+					const ctx = canvas.getContext('2d');
+
+					// Calculate the aspect ratio of the image
+					const aspectRatio = img.width / img.height;
+
+					// Calculate the new width and height to fit within 100x100
+					let newWidth, newHeight;
+					if (aspectRatio > 1) {
+						newWidth = 100 * aspectRatio;
+						newHeight = 100;
+					} else {
+						newWidth = 100;
+						newHeight = 100 / aspectRatio;
+					}
+
+					// Set the canvas size
+					canvas.width = 100;
+					canvas.height = 100;
+
+					// Calculate the position to center the image
+					const offsetX = (100 - newWidth) / 2;
+					const offsetY = (100 - newHeight) / 2;
+
+					// Draw the image on the canvas
+					ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
+
+					// Get the base64 representation of the compressed image
+					const compressedSrc = canvas.toDataURL('image/jpeg');
+
+					// Display the compressed image
+					info.meta.profile_image_url = compressedSrc;
+
+					inputFiles = null;
+				};
+			};
+
+			if (
+				inputFiles &&
+				inputFiles.length > 0 &&
+				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
+			) {
+				reader.readAsDataURL(inputFiles[0]);
+			} else {
+				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
+				inputFiles = null;
+			}
+		}}
+	/>
+
+	<button
+		class="flex space-x-1"
+		on:click={() => {
+			history.back();
+		}}
+	>
+		<div class=" self-center">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 20 20"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+		<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
+	</button>
+	<!-- <hr class="my-3 dark:border-gray-700" /> -->
+
+	<form
+		class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
+		on:submit|preventDefault={() => {
+			submitHandler();
+		}}
+	>
+		<div class="flex justify-center my-4">
+			<div class="self-center">
+				<button
+					class=" {info.meta.profile_image_url
+						? ''
+						: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
+					type="button"
+					on:click={() => {
+						filesInputElement.click();
+					}}
+				>
+					{#if info.meta.profile_image_url}
+						<img
+							src={info.meta.profile_image_url}
+							alt="modelfile profile"
+							class=" rounded-full w-20 h-20 object-cover"
+						/>
+					{:else}
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 24 24"
+							fill="currentColor"
+							class="size-8"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					{/if}
+				</button>
+			</div>
+		</div>
+
+		<div class="my-2 flex space-x-2">
+			<div class="flex-1">
+				<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
+
+				<div>
+					<input
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+						placeholder={$i18n.t('Name your model')}
+						bind:value={name}
+						required
+					/>
+				</div>
+			</div>
+
+			<div class="flex-1">
+				<div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div>
+
+				<div>
+					<input
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+						placeholder={$i18n.t('Add a model id')}
+						bind:value={id}
+						required
+					/>
+				</div>
+			</div>
+		</div>
+
+		<div class="my-2">
+			<div class=" text-sm font-semibold mb-2">{$i18n.t('Base Model (From)')}</div>
+
+			<div>
+				<select
+					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+					placeholder="Select a base model (e.g. llama3, gpt-4o)"
+					bind:value={info.base_model_id}
+					required
+				>
+					<option value={null} class=" placeholder:text-gray-500"
+						>{$i18n.t('Select a base model')}</option
+					>
+					{#each $models.filter((m) => !m?.preset) as model}
+						<option value={model.id}>{model.name}</option>
+					{/each}
+				</select>
+			</div>
+		</div>
+
+		<div class="my-2">
+			<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}</div>
+
+			<div>
+				<input
+					class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+					placeholder={$i18n.t('Add a short description about what this model does')}
+					bind:value={info.meta.description}
+				/>
+			</div>
+		</div>
+
+		<div class="my-2">
+			<div class="flex w-full justify-between">
+				<div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div>
+			</div>
+
+			<div class="mt-2">
+				<div class="my-1">
+					<div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
+					<div>
+						<textarea
+							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
+							placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
+							rows="4"
+							bind:value={info.params.system}
+						/>
+					</div>
+				</div>
+
+				<div class="flex w-full justify-between">
+					<div class=" self-center text-xs font-semibold">
+						{$i18n.t('Advanced Params')}
+					</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						type="button"
+						on:click={() => {
+							showAdvanced = !showAdvanced;
+						}}
+					>
+						{#if showAdvanced}
+							<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Show')}</span>
+						{/if}
+					</button>
+				</div>
+
+				{#if showAdvanced}
+					<div class="my-2">
+						<AdvancedParams
+							bind:params
+							on:change={(e) => {
+								info.params = { ...info.params, ...params };
+							}}
+						/>
+					</div>
+				{/if}
+			</div>
+		</div>
+
+		<div class="my-2">
+			<div class="flex w-full justify-between items-center">
+				<div class="flex w-full justify-between items-center">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
+
+					<button
+						class="p-1 text-xs flex rounded transition"
+						type="button"
+						on:click={() => {
+							if (info.meta.suggestion_prompts === null) {
+								info.meta.suggestion_prompts = [{ content: '' }];
+							} else {
+								info.meta.suggestion_prompts = null;
+							}
+						}}
+					>
+						{#if info.meta.suggestion_prompts === null}
+							<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
+						{/if}
+					</button>
+				</div>
+
+				{#if info.meta.suggestion_prompts !== null}
+					<button
+						class="p-1 px-2 text-xs flex rounded transition"
+						type="button"
+						on:click={() => {
+							if (
+								info.meta.suggestion_prompts.length === 0 ||
+								info.meta.suggestion_prompts.at(-1).content !== ''
+							) {
+								info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }];
+							}
+						}}
+					>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 20 20"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
+							/>
+						</svg>
+					</button>
+				{/if}
+			</div>
+
+			{#if info.meta.suggestion_prompts}
+				<div class="flex flex-col space-y-1 mt-2">
+					{#if info.meta.suggestion_prompts.length > 0}
+						{#each info.meta.suggestion_prompts as prompt, promptIdx}
+							<div class=" flex border dark:border-gray-600 rounded-lg">
+								<input
+									class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
+									placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
+									bind:value={prompt.content}
+								/>
+
+								<button
+									class="px-2"
+									type="button"
+									on:click={() => {
+										info.meta.suggestion_prompts.splice(promptIdx, 1);
+										info.meta.suggestion_prompts = info.meta.suggestion_prompts;
+									}}
+								>
+									<svg
+										xmlns="http://www.w3.org/2000/svg"
+										viewBox="0 0 20 20"
+										fill="currentColor"
+										class="w-4 h-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"
+										/>
+									</svg>
+								</button>
+							</div>
+						{/each}
+					{:else}
+						<div class="text-xs text-center">No suggestion prompts</div>
+					{/if}
+				</div>
+			{/if}
+		</div>
+
+		<div class="my-2">
+			<div class="flex w-full justify-between">
+				<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
+			</div>
+			<div class="flex flex-col">
+				{#each Object.keys(capabilities) as capability}
+					<div class=" flex items-center gap-2">
+						<Checkbox
+							state={capabilities[capability] ? 'checked' : 'unchecked'}
+							on:change={(e) => {
+								capabilities[capability] = e.detail === 'checked';
+							}}
+						/>
+
+						<div class=" py-1.5 text-sm w-full capitalize">
+							{$i18n.t(capability)}
+						</div>
+					</div>
+				{/each}
+			</div>
+		</div>
+
+		<div class="my-2 text-gray-500">
+			<div class="flex w-full justify-between mb-2">
+				<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
+
+				<button
+					class="p-1 px-3 text-xs flex rounded transition"
+					type="button"
+					on:click={() => {
+						showPreview = !showPreview;
+					}}
+				>
+					{#if showPreview}
+						<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
+					{:else}
+						<span class="ml-2 self-center">{$i18n.t('Show')}</span>
+					{/if}
+				</button>
+			</div>
+
+			{#if showPreview}
+				<div>
+					<textarea
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+						rows="10"
+						value={JSON.stringify(info, null, 2)}
+						disabled
+						readonly
+					/>
+				</div>
+			{/if}
+		</div>
+
+		<div class="my-2 flex justify-end mb-20">
+			<button
+				class=" text-sm px-3 py-2 transition rounded-xl {loading
+					? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
+					: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
+				type="submit"
+				disabled={loading}
+			>
+				<div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
+
+				{#if loading}
+					<div class="ml-1.5 self-center">
+						<svg
+							class=" w-4 h-4"
+							viewBox="0 0 24 24"
+							fill="currentColor"
+							xmlns="http://www.w3.org/2000/svg"
+							><style>
+								.spinner_ajPY {
+									transform-origin: center;
+									animation: spinner_AtaB 0.75s infinite linear;
+								}
+								@keyframes spinner_AtaB {
+									100% {
+										transform: rotate(360deg);
+									}
+								}
+							</style><path
+								d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+								opacity=".25"
+							/><path
+								d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+								class="spinner_ajPY"
+							/></svg
+						>
+					</div>
+				{/if}
+			</button>
+		</div>
+	</form>
+</div>

+ 555 - 0
src/routes/(app)/workspace/models/edit/+page.svelte

@@ -0,0 +1,555 @@
+<script>
+	import { v4 as uuidv4 } from 'uuid';
+	import { toast } from 'svelte-sonner';
+	import { goto } from '$app/navigation';
+
+	import { onMount, getContext } from 'svelte';
+	import { page } from '$app/stores';
+	import { settings, user, config, models } from '$lib/stores';
+	import { splitStream } from '$lib/utils';
+
+	import { getModelInfos, updateModelById } from '$lib/apis/models';
+
+	import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
+	import { getModels } from '$lib/apis';
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+
+	const i18n = getContext('i18n');
+
+	let loading = false;
+	let success = false;
+
+	let filesInputElement;
+	let inputFiles;
+
+	let digest = '';
+	let pullProgress = null;
+
+	let showAdvanced = false;
+	let showPreview = false;
+
+	// ///////////
+	// model
+	// ///////////
+
+	let model = null;
+
+	let id = '';
+	let name = '';
+
+	let info = {
+		id: '',
+		base_model_id: null,
+		name: '',
+		meta: {
+			profile_image_url: '/favicon.png',
+			description: '',
+			suggestion_prompts: null
+		},
+		params: {
+			system: ''
+		}
+	};
+
+	let params = {};
+
+	let capabilities = {
+		vision: true
+	};
+
+	const updateHandler = async () => {
+		loading = true;
+
+		info.id = id;
+		info.name = name;
+		info.meta.capabilities = capabilities;
+		info.params.stop = params.stop !== null ? params.stop.split(',').filter((s) => s.trim()) : null;
+
+		const res = await updateModelById(localStorage.token, info.id, info);
+
+		if (res) {
+			toast.success('Model updated successfully');
+			await goto('/workspace/models');
+			await models.set(await getModels(localStorage.token));
+		}
+
+		loading = false;
+		success = false;
+	};
+
+	onMount(() => {
+		const _id = $page.url.searchParams.get('id');
+
+		if (_id) {
+			model = $models.find((m) => m.id === _id);
+			if (model) {
+				id = model.id;
+				name = model.name;
+
+				info = {
+					...info,
+					...JSON.parse(
+						JSON.stringify(
+							model?.info
+								? model?.info
+								: {
+										id: model.id,
+										name: model.name
+								  }
+						)
+					)
+				};
+
+				if (model.preset && model.owned_by === 'ollama' && !info.base_model_id.includes(':')) {
+					info.base_model_id = `${info.base_model_id}:latest`;
+				}
+
+				params = { ...params, ...model?.info?.params };
+				params.stop = params?.stop ? (params?.stop ?? []).join(',') : null;
+
+				if (model?.info?.meta?.capabilities) {
+					capabilities = { ...capabilities, ...model?.info?.meta?.capabilities };
+				}
+				console.log(model);
+			} else {
+				goto('/workspace/models');
+			}
+		} else {
+			goto('/workspace/models');
+		}
+	});
+</script>
+
+<div class="w-full max-h-full">
+	<input
+		bind:this={filesInputElement}
+		bind:files={inputFiles}
+		type="file"
+		hidden
+		accept="image/*"
+		on:change={() => {
+			let reader = new FileReader();
+			reader.onload = (event) => {
+				let originalImageUrl = `${event.target.result}`;
+
+				const img = new Image();
+				img.src = originalImageUrl;
+
+				img.onload = function () {
+					const canvas = document.createElement('canvas');
+					const ctx = canvas.getContext('2d');
+
+					// Calculate the aspect ratio of the image
+					const aspectRatio = img.width / img.height;
+
+					// Calculate the new width and height to fit within 100x100
+					let newWidth, newHeight;
+					if (aspectRatio > 1) {
+						newWidth = 100 * aspectRatio;
+						newHeight = 100;
+					} else {
+						newWidth = 100;
+						newHeight = 100 / aspectRatio;
+					}
+
+					// Set the canvas size
+					canvas.width = 100;
+					canvas.height = 100;
+
+					// Calculate the position to center the image
+					const offsetX = (100 - newWidth) / 2;
+					const offsetY = (100 - newHeight) / 2;
+
+					// Draw the image on the canvas
+					ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
+
+					// Get the base64 representation of the compressed image
+					const compressedSrc = canvas.toDataURL('image/jpeg');
+
+					// Display the compressed image
+					info.meta.profile_image_url = compressedSrc;
+
+					inputFiles = null;
+				};
+			};
+
+			if (
+				inputFiles &&
+				inputFiles.length > 0 &&
+				['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
+			) {
+				reader.readAsDataURL(inputFiles[0]);
+			} else {
+				console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
+				inputFiles = null;
+			}
+		}}
+	/>
+
+	<button
+		class="flex space-x-1"
+		on:click={() => {
+			history.back();
+		}}
+	>
+		<div class=" self-center">
+			<svg
+				xmlns="http://www.w3.org/2000/svg"
+				viewBox="0 0 20 20"
+				fill="currentColor"
+				class="w-4 h-4"
+			>
+				<path
+					fill-rule="evenodd"
+					d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
+					clip-rule="evenodd"
+				/>
+			</svg>
+		</div>
+		<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
+	</button>
+
+	{#if model}
+		<form
+			class="flex flex-col max-w-2xl mx-auto mt-4 mb-10"
+			on:submit|preventDefault={() => {
+				updateHandler();
+			}}
+		>
+			<div class="flex justify-center my-4">
+				<div class="self-center">
+					<button
+						class=" {info?.meta?.profile_image_url
+							? ''
+							: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
+						type="button"
+						on:click={() => {
+							filesInputElement.click();
+						}}
+					>
+						{#if info?.meta?.profile_image_url}
+							<img
+								src={info?.meta?.profile_image_url}
+								alt="modelfile profile"
+								class=" rounded-full w-20 h-20 object-cover"
+							/>
+						{:else}
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								class="w-8"
+							>
+								<path
+									fill-rule="evenodd"
+									d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
+									clip-rule="evenodd"
+								/>
+							</svg>
+						{/if}
+					</button>
+				</div>
+			</div>
+
+			<div class="my-2 flex space-x-2">
+				<div class="flex-1">
+					<div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div>
+
+					<div>
+						<input
+							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+							placeholder={$i18n.t('Name your model')}
+							bind:value={name}
+							required
+						/>
+					</div>
+				</div>
+
+				<div class="flex-1">
+					<div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div>
+
+					<div>
+						<input
+							class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg"
+							placeholder={$i18n.t('Add a model id')}
+							value={id}
+							disabled
+							required
+						/>
+					</div>
+				</div>
+			</div>
+
+			{#if model.preset}
+				<div class="my-2">
+					<div class=" text-sm font-semibold mb-2">{$i18n.t('Base Model (From)')}</div>
+
+					<div>
+						<select
+							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+							placeholder="Select a base model (e.g. llama3, gpt-4o)"
+							bind:value={info.base_model_id}
+							required
+						>
+							<option value={null} class=" placeholder:text-gray-500"
+								>{$i18n.t('Select a base model')}</option
+							>
+							{#each $models.filter((m) => m.id !== model.id && !m?.preset) as model}
+								<option value={model.id}>{model.name}</option>
+							{/each}
+						</select>
+					</div>
+				</div>
+			{/if}
+
+			<div class="my-2">
+				<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}</div>
+
+				<div>
+					<input
+						class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+						placeholder={$i18n.t('Add a short description about what this model does')}
+						bind:value={info.meta.description}
+					/>
+				</div>
+			</div>
+
+			<div class="my-2">
+				<div class="flex w-full justify-between">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div>
+				</div>
+
+				<!-- <div class=" text-sm font-semibold mb-2"></div> -->
+
+				<div class="mt-2">
+					<div class="my-1">
+						<div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
+						<div>
+							<textarea
+								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
+								placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
+								rows="4"
+								bind:value={info.params.system}
+							/>
+						</div>
+					</div>
+
+					<div class="flex w-full justify-between">
+						<div class=" self-center text-xs font-semibold">
+							{$i18n.t('Advanced Params')}
+						</div>
+
+						<button
+							class="p-1 px-3 text-xs flex rounded transition"
+							type="button"
+							on:click={() => {
+								showAdvanced = !showAdvanced;
+							}}
+						>
+							{#if showAdvanced}
+								<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
+							{:else}
+								<span class="ml-2 self-center">{$i18n.t('Show')}</span>
+							{/if}
+						</button>
+					</div>
+
+					{#if showAdvanced}
+						<div class="my-2">
+							<AdvancedParams
+								bind:params
+								on:change={(e) => {
+									info.params = { ...info.params, ...params };
+								}}
+							/>
+						</div>
+					{/if}
+				</div>
+			</div>
+
+			<div class="my-2">
+				<div class="flex w-full justify-between items-center">
+					<div class="flex w-full justify-between items-center">
+						<div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div>
+
+						<button
+							class="p-1 text-xs flex rounded transition"
+							type="button"
+							on:click={() => {
+								if (info.meta.suggestion_prompts === null) {
+									info.meta.suggestion_prompts = [{ content: '' }];
+								} else {
+									info.meta.suggestion_prompts = null;
+								}
+							}}
+						>
+							{#if info.meta.suggestion_prompts === null}
+								<span class="ml-2 self-center">{$i18n.t('Default')}</span>
+							{:else}
+								<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
+							{/if}
+						</button>
+					</div>
+
+					{#if info.meta.suggestion_prompts !== null}
+						<button
+							class="p-1 px-2 text-xs flex rounded transition"
+							type="button"
+							on:click={() => {
+								if (
+									info.meta.suggestion_prompts.length === 0 ||
+									info.meta.suggestion_prompts.at(-1).content !== ''
+								) {
+									info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }];
+								}
+							}}
+						>
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 20 20"
+								fill="currentColor"
+								class="w-4 h-4"
+							>
+								<path
+									d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
+								/>
+							</svg>
+						</button>
+					{/if}
+				</div>
+
+				{#if info.meta.suggestion_prompts}
+					<div class="flex flex-col space-y-1 mt-2">
+						{#if info.meta.suggestion_prompts.length > 0}
+							{#each info.meta.suggestion_prompts as prompt, promptIdx}
+								<div class=" flex border dark:border-gray-600 rounded-lg">
+									<input
+										class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
+										placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
+										bind:value={prompt.content}
+									/>
+
+									<button
+										class="px-2"
+										type="button"
+										on:click={() => {
+											info.meta.suggestion_prompts.splice(promptIdx, 1);
+											info.meta.suggestion_prompts = info.meta.suggestion_prompts;
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="w-4 h-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"
+											/>
+										</svg>
+									</button>
+								</div>
+							{/each}
+						{:else}
+							<div class="text-xs text-center">No suggestion prompts</div>
+						{/if}
+					</div>
+				{/if}
+			</div>
+
+			<div class="my-2">
+				<div class="flex w-full justify-between">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
+				</div>
+				<div class="flex flex-col">
+					{#each Object.keys(capabilities) as capability}
+						<div class=" flex items-center gap-2">
+							<Checkbox
+								state={capabilities[capability] ? 'checked' : 'unchecked'}
+								on:change={(e) => {
+									capabilities[capability] = e.detail === 'checked';
+								}}
+							/>
+
+							<div class=" py-1.5 text-sm w-full capitalize">
+								{$i18n.t(capability)}
+							</div>
+						</div>
+					{/each}
+				</div>
+			</div>
+
+			<div class="my-2 text-gray-500">
+				<div class="flex w-full justify-between mb-2">
+					<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						type="button"
+						on:click={() => {
+							showPreview = !showPreview;
+						}}
+					>
+						{#if showPreview}
+							<span class="ml-2 self-center">{$i18n.t('Hide')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Show')}</span>
+						{/if}
+					</button>
+				</div>
+
+				{#if showPreview}
+					<div>
+						<textarea
+							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
+							rows="10"
+							value={JSON.stringify(info, null, 2)}
+							disabled
+							readonly
+						/>
+					</div>
+				{/if}
+			</div>
+
+			<div class="my-2 flex justify-end mb-20">
+				<button
+					class=" text-sm px-3 py-2 transition rounded-xl {loading
+						? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
+						: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex"
+					type="submit"
+					disabled={loading}
+				>
+					<div class=" self-center font-medium">{$i18n.t('Save & Update')}</div>
+
+					{#if loading}
+						<div class="ml-1.5 self-center">
+							<svg
+								class=" w-4 h-4"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								xmlns="http://www.w3.org/2000/svg"
+								><style>
+									.spinner_ajPY {
+										transform-origin: center;
+										animation: spinner_AtaB 0.75s infinite linear;
+									}
+									@keyframes spinner_AtaB {
+										100% {
+											transform: rotate(360deg);
+										}
+									}
+								</style><path
+									d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+									opacity=".25"
+								/><path
+									d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+									class="spinner_ajPY"
+								/></svg
+							>
+						</div>
+					{/if}
+				</button>
+			</div>
+		</form>
+	{/if}
+</div>

+ 0 - 27
src/routes/modelfiles/create/+page.svelte

@@ -1,27 +0,0 @@
-<script lang="ts">
-	import { goto } from '$app/navigation';
-	import { onMount } from 'svelte';
-
-	onMount(async () => {
-		window.addEventListener('message', async (event) => {
-			if (
-				![
-					'https://ollamahub.com',
-					'https://www.ollamahub.com',
-					'https://openwebui.com',
-					'https://www.openwebui.com',
-					'http://localhost:5173'
-				].includes(event.origin)
-			)
-				return;
-			const modelfile = JSON.parse(event.data);
-			sessionStorage.modelfile = JSON.stringify(modelfile);
-
-			goto('/workspace/modelfiles/create');
-		});
-
-		if (window.opener ?? false) {
-			window.opener.postMessage('loaded', '*');
-		}
-	});
-</script>

+ 3 - 17
src/routes/s/[id]/+page.svelte

@@ -5,7 +5,7 @@
 
 	import dayjs from 'dayjs';
 
-	import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores';
+	import { settings, chatId, WEBUI_NAME, models } from '$lib/stores';
 	import { convertMessagesToHistory } from '$lib/utils';
 
 	import { getChatByShareId } from '$lib/apis/chats';
@@ -14,6 +14,7 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import { getUserById } from '$lib/apis/users';
 	import { error } from '@sveltejs/kit';
+	import { getModels } from '$lib/apis';
 
 	const i18n = getContext('i18n');
 
@@ -27,17 +28,6 @@
 	let showModelSelector = false;
 	let selectedModels = [''];
 
-	let selectedModelfiles = {};
-	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
-		const modelfile =
-			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
-
-		return {
-			...a,
-			...(modelfile && { [tagName]: modelfile })
-		};
-	}, {});
-
 	let chat = null;
 	let user = null;
 
@@ -69,10 +59,6 @@
 			if (await loadSharedChat()) {
 				await tick();
 				loaded = true;
-
-				window.setTimeout(() => scrollToBottom(), 0);
-				const chatInput = document.getElementById('chat-textarea');
-				chatInput?.focus();
 			} else {
 				await goto('/');
 			}
@@ -84,6 +70,7 @@
 	//////////////////////////
 
 	const loadSharedChat = async () => {
+		await models.set(await getModels(localStorage.token));
 		await chatId.set($page.params.id);
 		chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => {
 			await goto('/');
@@ -168,7 +155,6 @@
 							chatId={$chatId}
 							readOnly={true}
 							{selectedModels}
-							{selectedModelfiles}
 							{processing}
 							bind:history
 							bind:messages

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác