|
@@ -13,10 +13,11 @@
|
|
uploadModel
|
|
uploadModel
|
|
} from '$lib/apis/ollama';
|
|
} from '$lib/apis/ollama';
|
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
|
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 { splitStream } from '$lib/utils';
|
|
import { onMount, getContext } from 'svelte';
|
|
import { onMount, getContext } from 'svelte';
|
|
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
|
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
|
|
|
+ import { getModelConfig, type GlobalModelConfig, updateModelConfig } from '$lib/apis';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
|
|
|
|
const i18n = getContext('i18n');
|
|
const i18n = getContext('i18n');
|
|
@@ -67,6 +68,23 @@
|
|
|
|
|
|
let deleteModelTag = '';
|
|
let deleteModelTag = '';
|
|
|
|
|
|
|
|
+ // Model configuration
|
|
|
|
+ let modelConfig: GlobalModelConfig;
|
|
|
|
+ let showModelInfo = false;
|
|
|
|
+ let selectedModelId = '';
|
|
|
|
+ let modelName = '';
|
|
|
|
+ let modelDescription = '';
|
|
|
|
+ let modelIsVisionCapable = false;
|
|
|
|
+
|
|
|
|
+ const onModelInfoIdChange = () => {
|
|
|
|
+ const model = $models.find((m) => m.id === selectedModelId);
|
|
|
|
+ if (model) {
|
|
|
|
+ modelName = model.custom_info?.name ?? model.name;
|
|
|
|
+ modelDescription = model.custom_info?.description ?? '';
|
|
|
|
+ modelIsVisionCapable = model.custom_info?.vision_capable ?? false;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
const updateModelsHandler = async () => {
|
|
const updateModelsHandler = async () => {
|
|
for (const model of $models.filter(
|
|
for (const model of $models.filter(
|
|
(m) =>
|
|
(m) =>
|
|
@@ -492,6 +510,53 @@
|
|
models.set(await getModels());
|
|
models.set(await getModels());
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ const addModelInfoHandler = async () => {
|
|
|
|
+ if (!selectedModelId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ let model = $models.find((m) => m.id === selectedModelId);
|
|
|
|
+ if (!model) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ const modelSource =
|
|
|
|
+ 'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
|
|
|
|
+ // Remove any existing config
|
|
|
|
+ modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
|
|
|
|
+ // Add new config
|
|
|
|
+ modelConfig[modelSource].push({
|
|
|
|
+ id: selectedModelId,
|
|
|
|
+ name: modelName,
|
|
|
|
+ description: modelDescription,
|
|
|
|
+ vision_capable: modelIsVisionCapable
|
|
|
|
+ });
|
|
|
|
+ await updateModelConfig(localStorage.token, modelConfig);
|
|
|
|
+ toast.success(
|
|
|
|
+ $i18n.t('Model info for {{modelName}} added successfully', { modelName: selectedModelId })
|
|
|
|
+ );
|
|
|
|
+ models.set(await getModels());
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const deleteModelInfoHandler = async () => {
|
|
|
|
+ if (!selectedModelId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ let model = $models.find((m) => m.id === selectedModelId);
|
|
|
|
+ if (!model) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ const modelSource =
|
|
|
|
+ 'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
|
|
|
|
+ modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
|
|
|
|
+ await updateModelConfig(localStorage.token, modelConfig);
|
|
|
|
+ toast.success(
|
|
|
|
+ $i18n.t('Model info for {{modelName}} deleted successfully', { modelName: selectedModelId })
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const toggleIsVisionCapable = () => {
|
|
|
|
+ modelIsVisionCapable = !modelIsVisionCapable;
|
|
|
|
+ };
|
|
|
|
+
|
|
onMount(async () => {
|
|
onMount(async () => {
|
|
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
|
|
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
|
|
toast.error(error);
|
|
toast.error(error);
|
|
@@ -502,8 +567,9 @@
|
|
selectedOllamaUrlIdx = 0;
|
|
selectedOllamaUrlIdx = 0;
|
|
}
|
|
}
|
|
|
|
|
|
- ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
|
|
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
|
|
|
+ modelConfig = await getModelConfig(localStorage.token);
|
|
|
|
+ ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
|
});
|
|
});
|
|
</script>
|
|
</script>
|
|
|
|
|
|
@@ -587,24 +653,28 @@
|
|
viewBox="0 0 24 24"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
fill="currentColor"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
- ><style>
|
|
|
|
|
|
+ >
|
|
|
|
+ <style>
|
|
.spinner_ajPY {
|
|
.spinner_ajPY {
|
|
transform-origin: center;
|
|
transform-origin: center;
|
|
animation: spinner_AtaB 0.75s infinite linear;
|
|
animation: spinner_AtaB 0.75s infinite linear;
|
|
}
|
|
}
|
|
|
|
+
|
|
@keyframes spinner_AtaB {
|
|
@keyframes spinner_AtaB {
|
|
100% {
|
|
100% {
|
|
transform: rotate(360deg);
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
class="spinner_ajPY"
|
|
- /></svg
|
|
|
|
- >
|
|
|
|
|
|
+ />
|
|
|
|
+ </svg>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{:else}
|
|
<svg
|
|
<svg
|
|
@@ -705,7 +775,7 @@
|
|
{/if}
|
|
{/if}
|
|
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
|
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
|
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
|
- >{(model.custom_info?.displayName ?? model.name) +
|
|
|
|
|
|
+ >{(model.custom_info?.name ?? model.name) +
|
|
' (' +
|
|
' (' +
|
|
(model.size / 1024 ** 3).toFixed(1) +
|
|
(model.size / 1024 ** 3).toFixed(1) +
|
|
' GB)'}</option
|
|
' GB)'}</option
|
|
@@ -836,24 +906,28 @@
|
|
viewBox="0 0 24 24"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
fill="currentColor"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
- ><style>
|
|
|
|
|
|
+ >
|
|
|
|
+ <style>
|
|
.spinner_ajPY {
|
|
.spinner_ajPY {
|
|
transform-origin: center;
|
|
transform-origin: center;
|
|
animation: spinner_AtaB 0.75s infinite linear;
|
|
animation: spinner_AtaB 0.75s infinite linear;
|
|
}
|
|
}
|
|
|
|
+
|
|
@keyframes spinner_AtaB {
|
|
@keyframes spinner_AtaB {
|
|
100% {
|
|
100% {
|
|
transform: rotate(360deg);
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
class="spinner_ajPY"
|
|
- /></svg
|
|
|
|
- >
|
|
|
|
|
|
+ />
|
|
|
|
+ </svg>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{:else}
|
|
<svg
|
|
<svg
|
|
@@ -935,6 +1009,7 @@
|
|
<hr class=" dark:border-gray-700 my-2" />
|
|
<hr class=" dark:border-gray-700 my-2" />
|
|
{/if}
|
|
{/if}
|
|
|
|
|
|
|
|
+ <!--TODO: Hide LiteLLM options when ENABLE_LITELLM=false-->
|
|
<div class=" space-y-3">
|
|
<div class=" space-y-3">
|
|
<div class="mt-2 space-y-3 pr-1.5">
|
|
<div class="mt-2 space-y-3 pr-1.5">
|
|
<div>
|
|
<div>
|
|
@@ -1129,6 +1204,148 @@
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
+ <hr class=" dark:border-gray-700 my-2" />
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <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 Model Information')}</div>
|
|
|
|
+ <button
|
|
|
|
+ class=" text-xs font-medium text-gray-500"
|
|
|
|
+ type="button"
|
|
|
|
+ on:click={() => {
|
|
|
|
+ showModelInfo = !showModelInfo;
|
|
|
|
+ }}>{showModelInfo ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
|
|
|
+ >
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ {#if showModelInfo}
|
|
|
|
+ <div>
|
|
|
|
+ <div class="flex justify-between items-center text-xs">
|
|
|
|
+ <div class=" text-sm font-medium">{$i18n.t('Current Models')}</div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="flex gap-2">
|
|
|
|
+ <div class="flex-1 pb-1">
|
|
|
|
+ <select
|
|
|
|
+ class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
|
|
|
+ bind:value={selectedModelId}
|
|
|
|
+ on:change={onModelInfoIdChange}
|
|
|
|
+ >
|
|
|
|
+ {#if !selectedModelId}
|
|
|
|
+ <option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
|
|
|
+ {/if}
|
|
|
|
+ {#each $models as model}
|
|
|
|
+ <option value={model.id} class="bg-gray-100 dark:bg-gray-700"
|
|
|
|
+ >{'details' in model
|
|
|
|
+ ? 'Ollama'
|
|
|
|
+ : model.source === 'LiteLLM'
|
|
|
|
+ ? 'LiteLLM'
|
|
|
|
+ : 'OpenAI'}: {model.name}{`${
|
|
|
|
+ model.custom_info?.name
|
|
|
|
+ ? ' - ' + model.custom_info?.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={() => {
|
|
|
|
+ deleteModelInfoHandler();
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <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>
|
|
|
|
+
|
|
|
|
+ {#if selectedModelId}
|
|
|
|
+ <div>
|
|
|
|
+ <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Display Name')}</div>
|
|
|
|
+ <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 Model Display Name')}
|
|
|
|
+ bind:value={modelName}
|
|
|
|
+ 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={() => {
|
|
|
|
+ addModelInfoHandler();
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <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>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div>
|
|
|
|
+ <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Description')}</div>
|
|
|
|
+
|
|
|
|
+ <div class="flex w-full">
|
|
|
|
+ <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"
|
|
|
|
+ rows="2"
|
|
|
|
+ bind:value={modelDescription}
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="py-0.5 flex w-full justify-between">
|
|
|
|
+ <div class=" self-center text-sm font-medium">
|
|
|
|
+ {$i18n.t('Is Model Vision Capable')}
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <button
|
|
|
|
+ class="p-1 px-3sm flex rounded transition"
|
|
|
|
+ on:click={() => {
|
|
|
|
+ toggleIsVisionCapable();
|
|
|
|
+ }}
|
|
|
|
+ type="button"
|
|
|
|
+ >
|
|
|
|
+ {#if modelIsVisionCapable === true}
|
|
|
|
+ <span class="ml-2 self-center">{$i18n.t('Yes')}</span>
|
|
|
|
+ {:else}
|
|
|
|
+ <span class="ml-2 self-center">{$i18n.t('No')}</span>
|
|
|
|
+ {/if}
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ {/if}
|
|
|
|
+ </div>
|
|
|
|
+ {/if}
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|