feat: store model configs in the database

This commit is contained in:
Jun Siang Cheah
2024-05-19 18:46:24 +08:00
parent 1bacd5d93f
commit 4002ead6af
50 changed files with 434 additions and 194 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte';
import { type Model, mobile, modelfiles, settings, showSidebar } from '$lib/stores';
import { type Model, mobile, modelfiles, settings, showSidebar, models } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
@@ -27,7 +27,8 @@
export let stopResponse: Function;
export let autoScroll = true;
export let selectedModel: Model | undefined;
export let selectedAtModel: Model | undefined;
export let selectedModels: [''];
let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement;
@@ -52,6 +53,8 @@
let speechRecognition;
let visionCapableState = 'all';
$: if (prompt) {
if (chatTextAreaElement) {
chatTextAreaElement.style.height = '';
@@ -59,6 +62,20 @@
}
}
$: {
if (selectedAtModel || selectedModels) {
visionCapableState = checkModelsAreVisionCapable();
if (visionCapableState === 'none') {
// Remove all image files
const fileCount = files.length;
files = files.filter((file) => file.type != 'image');
if (files.length < fileCount) {
toast.warning($i18n.t('All selected models do not support image input, removed images'));
}
}
}
}
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
@@ -326,6 +343,35 @@
}
};
const checkModelsAreVisionCapable = () => {
let modelsToCheck = [];
if (selectedAtModel !== undefined) {
modelsToCheck = [selectedAtModel.id];
} else {
modelsToCheck = selectedModels;
}
if (modelsToCheck.length == 0 || modelsToCheck[0] == '') {
return 'all';
}
let visionCapableCount = 0;
for (const modelName of modelsToCheck) {
const model = $models.find((m) => m.id === modelName);
if (!model) {
continue;
}
if (model.custom_info?.params.vision_capable ?? true) {
visionCapableCount++;
}
}
if (visionCapableCount == modelsToCheck.length) {
return 'all';
} else if (visionCapableCount == 0) {
return 'none';
} else {
return 'some';
}
};
onMount(() => {
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
@@ -358,11 +404,9 @@
inputFiles.forEach((file) => {
console.log(file, file.name.split('.').at(-1));
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (selectedModel !== undefined) {
if (!(selectedModel.custom_info?.vision_capable ?? true)) {
toast.error($i18n.t('Selected model does not support image inputs.'));
return;
}
if (visionCapableState == 'none') {
toast.error($i18n.t('Selected models do not support image inputs'));
return;
}
let reader = new FileReader();
reader.onload = (event) => {
@@ -500,12 +544,12 @@
bind:chatInputPlaceholder
{messages}
on:select={(e) => {
selectedModel = e.detail;
selectedAtModel = e.detail;
chatTextAreaElement?.focus();
}}
/>
{#if selectedModel !== undefined}
{#if selectedAtModel !== 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"
>
@@ -514,7 +558,7 @@
crossorigin="anonymous"
alt="model profile"
class="size-5 max-w-[28px] object-cover rounded-full"
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedModel.id)
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedAtModel.id)
?.imageUrl ??
($i18n.language === 'dg-DG'
? `/doge.png`
@@ -522,7 +566,7 @@
/>
<div>
Talking to <span class=" font-medium"
>{selectedModel.custom_info?.name ?? selectedModel.name}
>{selectedAtModel.custom_info?.name ?? selectedAtModel.name}
</span>
</div>
</div>
@@ -530,7 +574,7 @@
<button
class="flex items-center"
on:click={() => {
selectedModel = undefined;
selectedAtModel = undefined;
}}
>
<XMark />
@@ -556,13 +600,11 @@
const _inputFiles = Array.from(inputFiles);
_inputFiles.forEach((file) => {
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (selectedModel !== undefined) {
if (!(selectedModel.custom_info?.vision_capable ?? true)) {
toast.error($i18n.t('Selected model does not support image inputs.'));
inputFiles = null;
filesInputElement.value = '';
return;
}
if (visionCapableState === 'none') {
toast.error($i18n.t('Selected models do not support image inputs'));
inputFiles = null;
filesInputElement.value = '';
return;
}
let reader = new FileReader();
reader.onload = (event) => {
@@ -897,7 +939,7 @@
if (e.key === 'Escape') {
console.log('Escape');
selectedModel = undefined;
selectedAtModel = undefined;
}
}}
rows="1"

View File

@@ -12,7 +12,12 @@
import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
import {
capitalizeFirstLetter,
getModels,
sanitizeResponseContent,
splitStream
} from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
@@ -23,7 +28,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]';
@@ -248,12 +258,8 @@
<!-- {JSON.stringify(item.info)} -->
{#if item.info.external}
<Tooltip
content={`${item.info?.source ?? 'External'}${
item.info.custom_info?.description ? '<br>' : ''
}${item.info.custom_info?.description?.replaceAll('\n', '<br>') ?? ''}`}
>
<div class=" mr-2">
<Tooltip content={`${item.info?.source ?? 'External'}`}>
<div class="">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@@ -279,11 +285,9 @@
item.info?.details?.quantization_level
? item.info?.details?.quantization_level + ' '
: ''
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}${
item.info.custom_info?.description ? '<br>' : ''
}${item.info.custom_info?.description?.replaceAll('\n', '<br>') ?? ''}`}
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
>
<div class=" mr-2">
<div class="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -301,8 +305,31 @@
</div>
</Tooltip>
{/if}
{#if item.info?.custom_info?.params.description}
<Tooltip
content={`${sanitizeResponseContent(
item.info.custom_info?.params.description
).replaceAll('\n', '<br>')}`}
>
<div class="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
/>
</svg>
</div>
</Tooltip>
{/if}
</div>
{#if value === item.value}
<div class="ml-auto">
<Check />

View File

@@ -80,8 +80,8 @@
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;
modelDescription = model.custom_info?.params.description ?? '';
modelIsVisionCapable = model.custom_info?.params.vision_capable ?? false;
}
};
@@ -521,13 +521,18 @@
const modelSource =
'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
// Remove any existing config
modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
modelConfig = modelConfig.filter(
(m) => !(m.id === selectedModelId && m.source === modelSource)
);
// Add new config
modelConfig[modelSource].push({
modelConfig.push({
id: selectedModelId,
name: modelName,
description: modelDescription,
vision_capable: modelIsVisionCapable
source: modelSource,
params: {
description: modelDescription,
vision_capable: modelIsVisionCapable
}
});
await updateModelConfig(localStorage.token, modelConfig);
toast.success(
@@ -546,7 +551,9 @@
}
const modelSource =
'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
modelConfig = modelConfig.filter(
(m) => !(m.id === selectedModelId && m.source === modelSource)
);
await updateModelConfig(localStorage.token, modelConfig);
toast.success(
$i18n.t('Model info for {{modelName}} deleted successfully', { modelName: selectedModelId })
@@ -559,18 +566,28 @@
};
onMount(async () => {
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
console.log('mounting');
await Promise.all([
(async () => {
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
if (OLLAMA_URLS.length > 0) {
selectedOllamaUrlIdx = 0;
}
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
modelConfig = await getModelConfig(localStorage.token);
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
if (OLLAMA_URLS.length > 0) {
selectedOllamaUrlIdx = 0;
}
})(),
(async () => {
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
})(),
(async () => {
modelConfig = await getModelConfig(localStorage.token);
})(),
(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})()
]);
});
</script>