feat: create model

This commit is contained in:
Timothy J. Baek 2024-05-24 22:21:57 -07:00
parent ca3108a54d
commit dac9634242
13 changed files with 286 additions and 951 deletions

View File

@ -39,6 +39,8 @@ from utils.utils import (
get_admin_user, get_admin_user,
) )
from utils.models import get_model_id_from_custom_model_id
from config import ( from config import (
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
@ -873,10 +875,10 @@ async def generate_chat_completion(
url_idx: Optional[int] = None, url_idx: Optional[int] = None,
user=Depends(get_verified_user), user=Depends(get_verified_user),
): ):
model_id = get_model_id_from_custom_model_id(form_data.model)
model = model_id
if url_idx == None: if url_idx == None:
model = form_data.model
if ":" not in model: if ":" not in model:
model = f"{model}:latest" model = f"{model}:latest"
@ -893,6 +895,13 @@ async def generate_chat_completion(
r = None r = None
# payload = {
# **form_data.model_dump_json(exclude_none=True).encode(),
# "model": model,
# "messages": form_data.messages,
# }
log.debug( log.debug(
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format( "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
form_data.model_dump_json(exclude_none=True).encode() form_data.model_dump_json(exclude_none=True).encode()

View File

@ -166,7 +166,9 @@ class ModelsTable:
model = Model.get(Model.id == id) model = Model.get(Model.id == id)
return ModelModel(**model_to_dict(model)) return ModelModel(**model_to_dict(model))
except: except Exception as e:
print(e)
return None return None
def delete_model_by_id(self, id: str) -> bool: def delete_model_by_id(self, id: str) -> bool:

View File

@ -28,16 +28,24 @@ async def get_models(user=Depends(get_verified_user)):
@router.post("/add", response_model=Optional[ModelModel]) @router.post("/add", response_model=Optional[ModelModel])
async def add_new_model(form_data: ModelForm, user=Depends(get_admin_user)): async def add_new_model(
model = Models.insert_new_model(form_data, user.id) request: Request, form_data: ModelForm, user=Depends(get_admin_user)
):
if model: if form_data.id in request.app.state.MODELS:
return model
else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(), 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(),
)
############################ ############################

View File

@ -32,6 +32,8 @@ class ERROR_MESSAGES(str, Enum):
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." 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." 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." NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
INVALID_TOKEN = ( INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again." "Your session has expired or the token is invalid. Please sign in again."

10
backend/utils/models.py Normal file
View File

@ -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

View File

@ -194,7 +194,7 @@
await settings.set({ await settings.set({
..._settings, ..._settings,
system: chatContent.system ?? _settings.system, system: chatContent.system ?? _settings.system,
options: chatContent.options ?? _settings.options params: chatContent.options ?? _settings.params
}); });
autoScroll = true; autoScroll = true;
await tick(); await tick();
@ -283,7 +283,7 @@
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { options: {
...($settings.options ?? {}) ...($settings.params ?? {})
}, },
messages: messages, messages: messages,
history: history, history: history,
@ -431,7 +431,7 @@
// Prepare the base message object // Prepare the base message object
const baseMessage = { const baseMessage = {
role: message.role, 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 // Extract and format image URLs if any exist
@ -443,7 +443,6 @@
if (imageUrls && imageUrls.length > 0 && message.role === 'user') { if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
baseMessage.images = imageUrls; baseMessage.images = imageUrls;
} }
return baseMessage; return baseMessage;
}); });
@ -474,10 +473,10 @@
model: model, model: model,
messages: messagesBody, messages: messagesBody,
options: { options: {
...($settings.options ?? {}), ...($settings.params ?? {}),
stop: stop:
$settings?.options?.stop ?? undefined $settings?.params?.stop ?? undefined
? $settings.options.stop.map((str) => ? $settings.params.stop.map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined : undefined
@ -718,18 +717,18 @@
: message?.raContent ?? message.content : message?.raContent ?? message.content
}) })
})), })),
seed: $settings?.options?.seed ?? undefined, seed: $settings?.params?.seed ?? undefined,
stop: stop:
$settings?.options?.stop ?? undefined $settings?.params?.stop ?? undefined
? $settings.options.stop.map((str) => ? $settings.params.stop.map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined, : undefined,
temperature: $settings?.options?.temperature ?? undefined, temperature: $settings?.params?.temperature ?? undefined,
top_p: $settings?.options?.top_p ?? undefined, top_p: $settings?.params?.top_p ?? undefined,
num_ctx: $settings?.options?.num_ctx ?? undefined, num_ctx: $settings?.params?.num_ctx ?? undefined,
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, frequency_penalty: $settings?.params?.repeat_penalty ?? undefined,
max_tokens: $settings?.options?.num_predict ?? undefined, max_tokens: $settings?.params?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined, docs: docs.length > 0 ? docs : undefined,
citations: docs.length > 0 citations: docs.length > 0
}, },
@ -1045,7 +1044,7 @@
bind:files bind:files
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedModel={atSelectedModel} bind:atSelectedModel
{selectedModels} {selectedModels}
{messages} {messages}
{submitPrompt} {submitPrompt}

View File

@ -27,7 +27,8 @@
export let stopResponse: Function; export let stopResponse: Function;
export let autoScroll = true; export let autoScroll = true;
export let selectedAtModel: Model | undefined;
export let atSelectedModel: Model | undefined;
export let selectedModels: ['']; export let selectedModels: [''];
let chatTextAreaElement: HTMLTextAreaElement; let chatTextAreaElement: HTMLTextAreaElement;
@ -52,7 +53,6 @@
export let messages = []; export let messages = [];
let speechRecognition; let speechRecognition;
let visionCapableState = 'all'; let visionCapableState = 'all';
$: if (prompt) { $: if (prompt) {
@ -62,19 +62,48 @@
} }
} }
$: { // $: {
if (selectedAtModel || selectedModels) { // if (atSelectedModel || selectedModels) {
visionCapableState = checkModelsAreVisionCapable(); // visionCapableState = checkModelsAreVisionCapable();
if (visionCapableState === 'none') { // if (visionCapableState === 'none') {
// Remove all image files // // Remove all image files
const fileCount = files.length; // const fileCount = files.length;
files = files.filter((file) => file.type != 'image'); // files = files.filter((file) => file.type != 'image');
if (files.length < fileCount) { // if (files.length < fileCount) {
toast.warning($i18n.t('All selected models do not support image input, removed images')); // toast.warning($i18n.t('All selected models do not support image input, removed images'));
} // }
// }
// }
// }
const checkModelsAreVisionCapable = () => {
let modelsToCheck = [];
if (atSelectedModel !== undefined) {
modelsToCheck = [atSelectedModel.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?.meta.vision_capable ?? true) {
visionCapableCount++;
} }
} }
} if (visionCapableCount == modelsToCheck.length) {
return 'all';
} else if (visionCapableCount == 0) {
return 'none';
} else {
return 'some';
}
};
let mediaRecorder; let mediaRecorder;
let audioChunks = []; let audioChunks = [];
@ -343,35 +372,6 @@
} }
}; };
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?.meta.vision_capable ?? true) {
visionCapableCount++;
}
}
if (visionCapableCount == modelsToCheck.length) {
return 'all';
} else if (visionCapableCount == 0) {
return 'none';
} else {
return 'some';
}
};
onMount(() => { onMount(() => {
window.setTimeout(() => chatTextAreaElement?.focus(), 0); window.setTimeout(() => chatTextAreaElement?.focus(), 0);
@ -479,8 +479,8 @@
<div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0"> <div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
<div class="w-full"> <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=" -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="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative"> <div class="relative">
{#if autoScroll === false && messages.length > 0} {#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30"> <div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
@ -544,12 +544,12 @@
bind:chatInputPlaceholder bind:chatInputPlaceholder
{messages} {messages}
on:select={(e) => { on:select={(e) => {
selectedAtModel = e.detail; atSelectedModel = e.detail;
chatTextAreaElement?.focus(); chatTextAreaElement?.focus();
}} }}
/> />
{#if selectedAtModel !== undefined} {#if atSelectedModel !== undefined}
<div <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" 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"
> >
@ -558,23 +558,21 @@
crossorigin="anonymous" crossorigin="anonymous"
alt="model profile" alt="model profile"
class="size-5 max-w-[28px] object-cover rounded-full" class="size-5 max-w-[28px] object-cover rounded-full"
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedAtModel.id) src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
?.imageUrl ?? ?.profile_image_url ??
($i18n.language === 'dg-DG' ($i18n.language === 'dg-DG'
? `/doge.png` ? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)} : `${WEBUI_BASE_URL}/static/favicon.png`)}
/> />
<div> <div>
Talking to <span class=" font-medium" Talking to <span class=" font-medium">{atSelectedModel.name}</span>
>{selectedAtModel.custom_info?.name ?? selectedAtModel.name}
</span>
</div> </div>
</div> </div>
<div> <div>
<button <button
class="flex items-center" class="flex items-center"
on:click={() => { on:click={() => {
selectedAtModel = undefined; atSelectedModel = undefined;
}} }}
> >
<XMark /> <XMark />
@ -966,7 +964,7 @@
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('Escape'); console.log('Escape');
selectedAtModel = undefined; atSelectedModel = undefined;
} }
}} }}
rows="1" rows="1"

View File

@ -13,7 +13,6 @@
export let models = []; export let models = [];
export let submitPrompt; export let submitPrompt;
export let suggestionPrompts;
let mounted = false; let mounted = false;
let selectedModelIdx = 0; let selectedModelIdx = 0;

View File

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -23,6 +25,10 @@
let customFieldName = ''; let customFieldName = '';
let customFieldValue = ''; let customFieldValue = '';
$: if (params) {
dispatch('change', params);
}
</script> </script>
<div class=" space-y-3 text-xs"> <div class=" space-y-3 text-xs">

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import queue from 'async/queue';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { import {
@ -12,33 +11,19 @@
cancelOllamaRequest, cancelOllamaRequest,
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, config } 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 { 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');
export let getModels: Function; export let getModels: Function;
let showLiteLLM = false;
let showLiteLLMParams = false;
let modelUploadInputElement: HTMLInputElement; let modelUploadInputElement: HTMLInputElement;
let liteLLMModelInfo = [];
let liteLLMModel = '';
let liteLLMModelName = '';
let liteLLMAPIBase = '';
let liteLLMAPIKey = '';
let liteLLMRPM = '';
let liteLLMMaxTokens = '';
let deleteLiteLLMModelName = '';
$: liteLLMModelName = liteLLMModel;
// Models // Models
@ -68,23 +53,6 @@
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?.meta.description ?? '';
modelIsVisionCapable = model.custom_info?.meta.vision_capable ?? false;
}
};
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of $models.filter( for (const model of $models.filter(
(m) => (m) =>
@ -457,106 +425,6 @@
} }
}; };
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());
};
const addModelInfoHandler = async () => {
if (!selectedModelId) {
return;
}
let model = $models.find((m) => m.id === selectedModelId);
if (!model) {
return;
}
// Remove any existing config
modelConfig = modelConfig.filter((m) => !(m.id === selectedModelId));
// Add new config
modelConfig.push({
id: selectedModelId,
name: modelName,
params: {},
meta: {
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;
}
modelConfig = modelConfig.filter((m) => !(m.id === selectedModelId));
await updateModelConfig(localStorage.token, modelConfig);
toast.success(
$i18n.t('Model info for {{modelName}} deleted successfully', { modelName: selectedModelId })
);
models.set(await getModels());
};
const toggleIsVisionCapable = () => {
modelIsVisionCapable = !modelIsVisionCapable;
};
onMount(async () => { onMount(async () => {
await Promise.all([ await Promise.all([
(async () => { (async () => {
@ -569,12 +437,6 @@
selectedOllamaUrlIdx = 0; selectedOllamaUrlIdx = 0;
} }
})(), })(),
(async () => {
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
})(),
(async () => {
modelConfig = await getModelConfig(localStorage.token);
})(),
(async () => { (async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
})() })()
@ -1015,344 +877,8 @@
{/if} {/if}
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" /> {:else}
<div>Ollama Not Detected</div>
{/if} {/if}
<!--TODO: Hide LiteLLM options when ENABLE_LITELLM=false-->
<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>
<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>

View File

@ -139,7 +139,7 @@
</div> </div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<div class=" font-bold capitalize line-clamp-1">{model.name}</div> <div class=" font-bold line-clamp-1">{model.name}</div>
<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
{model?.info?.meta?.description ?? model.id} {model?.info?.meta?.description ?? model.id}
</div> </div>

View File

@ -4,209 +4,88 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { settings, user, config, modelfiles, models } from '$lib/stores'; 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 { onMount, tick, getContext } from 'svelte';
import { createModel } from '$lib/apis/ollama';
import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models'; import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models';
import { getModels } from '$lib/apis';
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let loading = false;
let filesInputElement; let filesInputElement;
let inputFiles; let inputFiles;
let imageUrl = null;
let digest = ''; let showAdvanced = false;
let pullProgress = null; let showPreview = false;
let loading = false;
let success = false; let success = false;
// /////////// // ///////////
// Modelfile // Model
// /////////// // ///////////
let title = ''; let id = '';
let tagName = ''; let name = '';
let desc = '';
let raw = true; let info = {
let advanced = false; id: '',
base_model_id: null,
// Raw Mode name: '',
let content = ''; meta: {
profile_image_url: null,
// Builder Mode description: '',
let model = ''; suggestion_prompts: [
let system = ''; {
let template = ''; content: ''
let params = { }
// Advanced ]
seed: 0, },
stop: '', params: {
temperature: '', system: ''
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}"""` : ''}
${params.seed !== 0 ? `PARAMETER seed ${params.seed}` : ''}
${params.stop !== '' ? `PARAMETER stop ${params.stop}` : ''}
${params.temperature !== '' ? `PARAMETER temperature ${params.temperature}` : ''}
${params.repeat_penalty !== '' ? `PARAMETER repeat_penalty ${params.repeat_penalty}` : ''}
${params.repeat_last_n !== '' ? `PARAMETER repeat_last_n ${params.repeat_last_n}` : ''}
${params.mirostat !== '' ? `PARAMETER mirostat ${params.mirostat}` : ''}
${params.mirostat_eta !== '' ? `PARAMETER mirostat_eta ${params.mirostat_eta}` : ''}
${params.mirostat_tau !== '' ? `PARAMETER mirostat_tau ${params.mirostat_tau}` : ''}
${params.top_k !== '' ? `PARAMETER top_k ${params.top_k}` : ''}
${params.top_p !== '' ? `PARAMETER top_p ${params.top_p}` : ''}
${params.tfs_z !== '' ? `PARAMETER tfs_z ${params.tfs_z}` : ''}
${params.num_ctx !== '' ? `PARAMETER num_ctx ${params.num_ctx}` : ''}
${params.num_predict !== '' ? `PARAMETER num_predict ${params.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) => { let params = {};
await addNewModel(localStorage.token, modelfile);
await modelfiles.set(await getModelInfos(localStorage.token)); $: if (name) {
}; id = name.replace(/\s+/g, '-').toLowerCase();
}
const submitHandler = async () => { const submitHandler = async () => {
loading = true; loading = true;
if (Object.keys(categories).filter((category) => categories[category]).length == 0) { info.id = id;
info.name = name;
if ($models.find((m) => m.id === info.id)) {
toast.error( toast.error(
'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.' `Error: A model with the ID '${info.id}' already exists. Please select a different ID to proceed.`
); );
loading = false; loading = false;
success = false; success = false;
return success; return success;
} }
if ( if (info) {
$models.map((model) => model.name).includes(tagName) || // TODO: if profile image url === null, set it to default image '/favicon.png'
(await getModelById(localStorage.token, tagName).catch(() => false)) const res = await addNewModel(localStorage.token, {
) { ...info,
toast.error( meta: {
`Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name to complete your modelfile.` ...info.meta,
); profile_image_url: info.meta.profile_image_url ?? '/favicon.png',
loading = false; suggestion_prompts: info.meta.suggestion_prompts.filter((prompt) => prompt.content !== '')
success = false; },
return success; params: { ...info.params, ...params }
} });
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) { if (res) {
const reader = res.body toast.success('Model created successfully!');
.pipeThrough(new TextDecoderStream()) await goto('/workspace/models');
.pipeThrough(splitStream('\n')) await models.set(await getModels(localStorage.token));
.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; loading = false;
success = false; success = false;
}; };
@ -223,62 +102,18 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
].includes(event.origin) ].includes(event.origin)
) )
return; return;
const modelfile = JSON.parse(event.data); const model = JSON.parse(event.data);
console.log(modelfile); console.log(model);
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) { if (window.opener ?? false) {
window.opener.postMessage('loaded', '*'); window.opener.postMessage('loaded', '*');
} }
if (sessionStorage.modelfile) { if (sessionStorage.model) {
const modelfile = JSON.parse(sessionStorage.modelfile); const model = JSON.parse(sessionStorage.model);
console.log(modelfile); console.log(model);
imageUrl = modelfile.imageUrl; sessionStorage.removeItem('model');
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> </script>
@ -330,7 +165,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
const compressedSrc = canvas.toDataURL('image/jpeg'); const compressedSrc = canvas.toDataURL('image/jpeg');
// Display the compressed image // Display the compressed image
imageUrl = compressedSrc; info.meta.profile_image_url = compressedSrc;
inputFiles = null; inputFiles = null;
}; };
@ -382,7 +217,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class="flex justify-center my-4"> <div class="flex justify-center my-4">
<div class="self-center"> <div class="self-center">
<button <button
class=" {imageUrl class=" {info.meta.profile_image_url
? '' ? ''
: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200" : 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200"
type="button" type="button"
@ -390,9 +225,9 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
filesInputElement.click(); filesInputElement.click();
}} }}
> >
{#if imageUrl} {#if info.meta.profile_image_url}
<img <img
src={imageUrl} src={info.meta.profile_image_url}
alt="modelfile profile" alt="modelfile profile"
class=" rounded-full w-20 h-20 object-cover" class=" rounded-full w-20 h-20 object-cover"
/> />
@ -401,7 +236,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-8" class="size-8"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@ -421,35 +256,55 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" 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')} placeholder={$i18n.t('Name your model')}
bind:value={title} bind:value={name}
required required
/> />
</div> </div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class=" text-sm font-semibold mb-2">{$i18n.t('Model Tag Name')}*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" 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')} placeholder={$i18n.t('Add a model id')}
bind:value={tagName} bind:value={id}
required required
/> />
</div> </div>
</div> </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 as model}
<option value={model.id}>{model.name}</option>
{/each}
</select>
</div>
</div>
<div class="my-2"> <div class="my-2">
<div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div> <div class=" text-sm font-semibold mb-2">{$i18n.t('Description')}*</div>
<div> <div>
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" 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')} placeholder={$i18n.t('Add a short description about what this model does')}
bind:value={desc} bind:value={info.meta.description}
required required
/> />
</div> </div>
@ -457,137 +312,53 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
<div class="my-2"> <div class="my-2">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold">{$i18n.t('Modelfile')}</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</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>
<!-- <div class=" text-sm font-semibold mb-2"></div> --> <div class="mt-2">
{#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="my-1">
<div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div> <div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
<div> <div>
<textarea <textarea
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1" 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.`} placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
rows="4" rows="4"
bind:value={system} bind:value={info.params.system}
/> />
</div> </div>
</div> </div>
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-sm font-semibold"> <div class=" self-center text-sm font-semibold">
{$i18n.t('Modelfile Advanced Settings')} {$i18n.t('Advanced Params')}
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
advanced = !advanced; showAdvanced = !showAdvanced;
}} }}
> >
{#if advanced} {#if showAdvanced}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span> <span class="ml-2 self-center">{$i18n.t('Hide')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span> <span class="ml-2 self-center">{$i18n.t('Show')}</span>
{/if} {/if}
</button> </button>
</div> </div>
{#if advanced} {#if showAdvanced}
<div class="my-2"> <div class="my-2">
<div class=" text-xs font-semibold mb-2">{$i18n.t('Template')}</div> <AdvancedParams
bind:params
<div> on:change={(e) => {
<textarea info.params = { ...info.params, ...params };
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:params />
</div>
</div> </div>
{/if} {/if}
{/if} </div>
</div> </div>
<div class="my-2"> <div class="my-2">
@ -598,8 +369,11 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="button" type="button"
on:click={() => { on:click={() => {
if (suggestions.length === 0 || suggestions.at(-1).content !== '') { if (
suggestions = [...suggestions, { content: '' }]; info.meta.suggestion_prompts.length === 0 ||
info.meta.suggestion_prompts.at(-1).content !== ''
) {
info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }];
} }
}} }}
> >
@ -616,7 +390,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</button> </button>
</div> </div>
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
{#each suggestions as prompt, promptIdx} {#each info.meta.suggestion_prompts as prompt, promptIdx}
<div class=" flex border dark:border-gray-600 rounded-lg"> <div class=" flex border dark:border-gray-600 rounded-lg">
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600"
@ -628,8 +402,8 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
class="px-2" class="px-2"
type="button" type="button"
on:click={() => { on:click={() => {
suggestions.splice(promptIdx, 1); info.meta.suggestion_prompts.splice(promptIdx, 1);
suggestions = suggestions; info.meta.suggestion_prompts = info.meta.suggestion_prompts;
}} }}
> >
<svg <svg
@ -648,37 +422,39 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
</div> </div>
</div> </div>
<div class="my-2"> <div class="my-2 text-gray-500">
<div class=" text-sm font-semibold mb-2">{$i18n.t('Categories')}</div> <div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
<div class="grid grid-cols-4"> <button
{#each Object.keys(categories) as category} class="p-1 px-3 text-xs flex rounded transition"
<div class="flex space-x-2 text-sm"> type="button"
<input type="checkbox" bind:checked={categories[category]} /> on:click={() => {
<div class="capitalize">{category}</div> showPreview = !showPreview;
</div> }}
{/each} >
{#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> </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>
{#if pullProgress !== null} <div class="my-2 flex justify-end mb-20">
<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 <button
class=" text-sm px-3 py-2 transition rounded-xl {loading class=" text-sm px-3 py-2 transition rounded-xl {loading
? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'

View File

@ -32,6 +32,10 @@
// /////////// // ///////////
let model = null; let model = null;
let id = '';
let name = '';
let info = { let info = {
id: '', id: '',
base_model_id: null, base_model_id: null,
@ -51,9 +55,14 @@
const updateHandler = async () => { const updateHandler = async () => {
loading = true; loading = true;
info.id = id;
info.name = name;
const res = await updateModelById(localStorage.token, info.id, info); const res = await updateModelById(localStorage.token, info.id, info);
if (res) { if (res) {
toast.success('Model updated successfully');
await goto('/workspace/models'); await goto('/workspace/models');
await models.set(await getModels(localStorage.token)); await models.set(await getModels(localStorage.token));
} }
@ -63,11 +72,14 @@
}; };
onMount(() => { onMount(() => {
const id = $page.url.searchParams.get('id'); const _id = $page.url.searchParams.get('id');
if (id) { if (_id) {
model = $models.find((m) => m.id === id); model = $models.find((m) => m.id === _id);
if (model) { if (model) {
id = model.id;
name = model.name;
info = { info = {
...info, ...info,
...JSON.parse( ...JSON.parse(
@ -235,7 +247,7 @@
<input <input
class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" 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')} placeholder={$i18n.t('Name your model')}
bind:value={info.name} bind:value={name}
required required
/> />
</div> </div>
@ -248,7 +260,7 @@
<input <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" 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')} placeholder={$i18n.t('Add a model id')}
value={info.id} value={id}
disabled disabled
required required
/> />
@ -333,7 +345,12 @@
{#if showAdvanced} {#if showAdvanced}
<div class="my-2"> <div class="my-2">
<AdvancedParams bind:params /> <AdvancedParams
bind:params
on:change={(e) => {
info.params = { ...info.params, ...params };
}}
/>
</div> </div>
{/if} {/if}
</div> </div>
@ -432,24 +449,7 @@
{/if} {/if}
</div> </div>
{#if pullProgress !== null} <div class="my-2 flex justify-end mb-20">
<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 <button
class=" text-sm px-3 py-2 transition rounded-xl {loading class=" text-sm px-3 py-2 transition rounded-xl {loading
? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'