Commit dac96342 authored by Timothy J. Baek's avatar Timothy J. Baek
Browse files

feat: create model

parent ca3108a5
...@@ -39,6 +39,8 @@ from utils.utils import ( ...@@ -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( ...@@ -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( ...@@ -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()
......
...@@ -166,7 +166,9 @@ class ModelsTable: ...@@ -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:
......
...@@ -28,16 +28,24 @@ async def get_models(user=Depends(get_verified_user)): ...@@ -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(),
)
############################ ############################
......
...@@ -32,6 +32,8 @@ class ERROR_MESSAGES(str, Enum): ...@@ -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."
......
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
...@@ -194,7 +194,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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}
......
...@@ -27,7 +27,8 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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"
......
...@@ -13,7 +13,6 @@ ...@@ -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;
......
<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 @@ ...@@ -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">
......
<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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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>
...@@ -139,7 +139,7 @@ ...@@ -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>
......
...@@ -4,209 +4,88 @@ ...@@ -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';
const i18n = getContext('i18n'); import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
let loading = false; const i18n = getContext('i18n');
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 info = {
let raw = true; id: '',
let advanced = false; base_model_id: null,
name: '',
// Raw Mode meta: {
let content = ''; profile_image_url: null,
description: '',
// Builder Mode suggestion_prompts: [
let model = ''; {
let system = ''; content: ''
let template = ''; }
let params = { ]
// Advanced },
seed: 0, params: {
stop: '', system: ''
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}"""` : ''}
${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;
toast.error( info.name = name;
'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.'
);
loading = false;
success = false;
return success;
}
if ( if ($models.find((m) => m.id === info.id)) {
$models.map((model) => model.name).includes(tagName) ||
(await getModelById(localStorage.token, tagName).catch(() => false))
) {
toast.error( toast.error(
`Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name 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) {
title !== '' && // TODO: if profile image url === null, set it to default image '/favicon.png'
desc !== '' && const res = await addNewModel(localStorage.token, {
content !== '' && ...info,
Object.keys(categories).filter((category) => categories[category]).length > 0 && meta: {
!$models.includes(tagName) ...info.meta,
) { profile_image_url: info.meta.profile_image_url ?? '/favicon.png',
const res = await createModel(localStorage.token, tagName, content); suggestion_prompts: info.meta.suggestion_prompts.filter((prompt) => prompt.content !== '')
},
params: { ...info.params, ...params }
});
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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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=" 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="my-2">
<div class=" text-xs font-semibold mb-2">{$i18n.t('Parameters')}</div> <AdvancedParams
bind:params
<div> on:change={(e) => {
<AdvancedParams bind:params /> info.params = { ...info.params, ...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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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, ''); ...@@ -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>
</div>
{#if pullProgress !== null} {#if showPreview}
<div class="my-2"> <div>
<div class=" text-sm font-semibold mb-2">{$i18n.t('Pull Progress')}</div> <textarea
<div class="w-full rounded-full dark:bg-gray-800"> class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg"
<div rows="10"
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" value={JSON.stringify(info, null, 2)}
style="width: {Math.max(15, pullProgress ?? 0)}%" disabled
> readonly
{pullProgress ?? 0}% />
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{digest}
</div> </div>
</div> {/if}
{/if} </div>
<div class="my-2 flex justify-end"> <div class="my-2 flex justify-end mb-20">
<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'
......
...@@ -32,6 +32,10 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment