Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
dac96342
Commit
dac96342
authored
May 24, 2024
by
Timothy J. Baek
Browse files
feat: create model
parent
ca3108a5
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
285 additions
and
950 deletions
+285
-950
backend/apps/ollama/main.py
backend/apps/ollama/main.py
+11
-2
backend/apps/web/models/models.py
backend/apps/web/models/models.py
+3
-1
backend/apps/web/routers/models.py
backend/apps/web/routers/models.py
+15
-7
backend/constants.py
backend/constants.py
+2
-0
backend/utils/models.py
backend/utils/models.py
+10
-0
src/lib/components/chat/Chat.svelte
src/lib/components/chat/Chat.svelte
+15
-16
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+51
-53
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+0
-1
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
...b/components/chat/Settings/Advanced/AdvancedParams.svelte
+7
-1
src/lib/components/chat/Settings/Models.svelte
src/lib/components/chat/Settings/Models.svelte
+4
-478
src/lib/components/workspace/Models.svelte
src/lib/components/workspace/Models.svelte
+1
-1
src/routes/(app)/workspace/models/create/+page.svelte
src/routes/(app)/workspace/models/create/+page.svelte
+142
-366
src/routes/(app)/workspace/models/edit/+page.svelte
src/routes/(app)/workspace/models/edit/+page.svelte
+24
-24
No files found.
backend/apps/ollama/main.py
View file @
dac96342
...
@@ -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
()
...
...
backend/apps/web/models/models.py
View file @
dac96342
...
@@ -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
:
...
...
backend/apps/web/routers/models.py
View file @
dac96342
...
@@ -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
(),
)
############################
############################
...
...
backend/constants.py
View file @
dac96342
...
@@ -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."
...
...
backend/utils/models.py
0 → 100644
View file @
dac96342
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
src/lib/components/chat/Chat.svelte
View file @
dac96342
...
@@ -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
,
option
s
:
chatContent
.
options
??
_settings
.
option
s
param
s
:
chatContent
.
options
??
_settings
.
param
s
});
});
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
.
option
s
??
{})
...($
settings
.
param
s
??
{})
},
},
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
.
option
s
??
{}),
...($
settings
.
param
s
??
{}),
stop
:
stop
:
$
settings
?.
option
s
?.
stop
??
undefined
$
settings
?.
param
s
?.
stop
??
undefined
?
$
settings
.
option
s
.
stop
.
map
((
str
)
=>
?
$
settings
.
param
s
.
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?.
option
s?.seed ?? undefined,
seed: $settings?.
param
s?.seed ?? undefined,
stop:
stop:
$settings?.
option
s?.stop ?? undefined
$settings?.
param
s?.stop ?? undefined
? $settings.
option
s.stop.map((str) =>
? $settings.
param
s.stop.map((str) =>
decodeURIComponent(JSON.parse('"
' + str.replace(/\"/g, '
\\
"') + '"
'))
decodeURIComponent(JSON.parse('"
' + str.replace(/\"/g, '
\\
"') + '"
'))
)
)
: undefined,
: undefined,
temperature: $settings?.
option
s?.temperature ?? undefined,
temperature: $settings?.
param
s?.temperature ?? undefined,
top_p: $settings?.
option
s?.top_p ?? undefined,
top_p: $settings?.
param
s?.top_p ?? undefined,
num_ctx: $settings?.
option
s?.num_ctx ?? undefined,
num_ctx: $settings?.
param
s?.num_ctx ?? undefined,
frequency_penalty: $settings?.
option
s?.repeat_penalty ?? undefined,
frequency_penalty: $settings?.
param
s?.repeat_penalty ?? undefined,
max_tokens: $settings?.
option
s?.num_predict ?? undefined,
max_tokens: $settings?.
param
s?.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}
...
...
src/lib/components/chat/MessageInput.svelte
View file @
dac96342
...
@@ -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) => {
s
elected
At
Model = e.detail;
atS
electedModel = e.detail;
chatTextAreaElement?.focus();
chatTextAreaElement?.focus();
}}
}}
/>
/>
{#if
s
elected
At
Model !== undefined}
{#if
atS
electedModel !== 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={$model
file
s.find((model
file
) => model
file.tagName
===
s
elected
At
Model.id)
src={$models.find((model) => model
.id
===
atS
electedModel.id)
?.info?.meta
?.image
U
rl ??
?.
profile_
image
_u
rl ??
($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={() => {
s
elected
At
Model = undefined;
atS
electedModel = 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');
s
elected
At
Model = undefined;
atS
electedModel = undefined;
}
}
}}
}}
rows="1"
rows="1"
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
dac96342
...
@@ -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;
...
...
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
View file @
dac96342
<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">
...
...
src/lib/components/chat/Settings/Models.svelte
View file @
dac96342
<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>
src/lib/components/workspace/Models.svelte
View file @
dac96342
...
@@ -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>
...
...
src/routes/(app)/workspace/models/create/+page.svelte
View file @
dac96342
...
@@ -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;
// ///////////
// ///////////
// Model
file
// 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
image
U
rl = compressedSrc;
info.meta.profile_
image
_u
rl = 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=" {image
U
rl
class=" {
info.meta.profile_
image
_u
rl
? ''
? ''
: '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 image
U
rl}
{#if
info.meta.profile_
image
_u
rl}
<img
<img
src={image
U
rl}
src={
info.meta.profile_
image
_u
rl}
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 model
file
')}
placeholder={$i18n.t('Name your model')}
bind:value={
titl
e}
bind:value={
nam
e}
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 model
file
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 model
file
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
Setting
s')}
{$i18n.t('Advanced
Param
s')}
</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={() => {
a
dvanced = !
a
dvanced;
showA
dvanced = !
showA
dvanced;
}}
}}
>
>
{#if
a
dvanced}
{#if
showA
dvanced}
<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
_prompt
s 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
_prompt
s.splice(promptIdx, 1);
suggestions =
suggestions;
info.meta.suggestion_prompts = info.meta.
suggestion
_prompt
s;
}}
}}
>
>
<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'
...
...
src/routes/(app)/workspace/models/edit/+page.svelte
View file @
dac96342
...
@@ -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'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment