feat: allow model config via config.json

This commit is contained in:
Jun Siang Cheah 2024-05-09 20:25:30 +08:00
parent df3675aaf7
commit e76a444ed9
21 changed files with 364 additions and 119 deletions

View File

@ -18,7 +18,7 @@ from pydantic import BaseModel, ConfigDict
from typing import Optional, List from typing import Optional, List
from utils.utils import get_verified_user, get_current_user, get_admin_user from utils.utils import get_verified_user, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, ENV from config import SRC_LOG_LEVELS, ENV, MODEL_CONFIG
from constants import MESSAGES from constants import MESSAGES
import os import os
@ -67,6 +67,7 @@ with open(LITELLM_CONFIG_DIR, "r") as file:
app.state.ENABLE = ENABLE_LITELLM app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config app.state.CONFIG = litellm_config
app.state.MODEL_CONFIG = MODEL_CONFIG.get("litellm", [])
# Global variable to store the subprocess reference # Global variable to store the subprocess reference
background_process = None background_process = None
@ -238,6 +239,8 @@ async def get_models(user=Depends(get_current_user)):
) )
) )
for model in data["data"]:
add_custom_info_to_model(model)
return data return data
except Exception as e: except Exception as e:
@ -258,6 +261,14 @@ async def get_models(user=Depends(get_current_user)):
"object": "model", "object": "model",
"created": int(time.time()), "created": int(time.time()),
"owned_by": "openai", "owned_by": "openai",
"custom_info": next(
(
item
for item in app.state.MODEL_CONFIG
if item["name"] == model["model_name"]
),
{},
),
} }
for model in app.state.CONFIG["model_list"] for model in app.state.CONFIG["model_list"]
], ],
@ -270,6 +281,12 @@ async def get_models(user=Depends(get_current_user)):
} }
def add_custom_info_to_model(model: dict):
model["custom_info"] = next(
(item for item in app.state.MODEL_CONFIG if item["name"] == model["id"]), {}
)
@app.get("/model/info") @app.get("/model/info")
async def get_model_list(user=Depends(get_admin_user)): async def get_model_list(user=Depends(get_admin_user)):
return {"data": app.state.CONFIG["model_list"]} return {"data": app.state.CONFIG["model_list"]}

View File

@ -46,6 +46,7 @@ from config import (
ENABLE_MODEL_FILTER, ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST, MODEL_FILTER_LIST,
UPLOAD_DIR, UPLOAD_DIR,
MODEL_CONFIG,
) )
from utils.misc import calculate_sha256 from utils.misc import calculate_sha256
@ -64,6 +65,7 @@ app.add_middleware(
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.MODEL_CONFIG = MODEL_CONFIG.get("ollama", [])
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.MODELS = {} app.state.MODELS = {}
@ -158,15 +160,26 @@ async def get_all_models():
models = { models = {
"models": merge_models_lists( "models": merge_models_lists(
map(lambda response: response["models"] if response else None, responses) map(
lambda response: (response["models"] if response else None),
responses,
)
) )
} }
for model in models["models"]:
add_custom_info_to_model(model)
app.state.MODELS = {model["model"]: model for model in models["models"]} app.state.MODELS = {model["model"]: model for model in models["models"]}
return models return models
def add_custom_info_to_model(model: dict):
model["custom_info"] = next(
(item for item in app.state.MODEL_CONFIG if item["name"] == model["model"]), {}
)
@app.get("/api/tags") @app.get("/api/tags")
@app.get("/api/tags/{url_idx}") @app.get("/api/tags/{url_idx}")
async def get_ollama_tags( async def get_ollama_tags(

View File

@ -26,6 +26,7 @@ from config import (
CACHE_DIR, CACHE_DIR,
ENABLE_MODEL_FILTER, ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST, MODEL_FILTER_LIST,
MODEL_CONFIG,
) )
from typing import List, Optional from typing import List, Optional
@ -47,6 +48,7 @@ app.add_middleware(
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.MODEL_CONFIG = MODEL_CONFIG.get("openai", [])
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
@ -217,10 +219,19 @@ async def get_all_models():
) )
} }
for model in models["data"]:
add_custom_info_to_model(model)
log.info(f"models: {models}") log.info(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]} app.state.MODELS = {model["id"]: model for model in models["data"]}
return models return models
def add_custom_info_to_model(model: dict):
model["custom_info"] = next(
(item for item in app.state.MODEL_CONFIG if item["name"] == model["id"]), {}
)
@app.get("/models") @app.get("/models")

View File

@ -35,6 +35,19 @@ class SetDefaultSuggestionsForm(BaseModel):
suggestions: List[PromptSuggestion] suggestions: List[PromptSuggestion]
class ModelConfig(BaseModel):
id: str
name: str
description: str
vision_capable: bool
class SetModelConfigForm(BaseModel):
ollama: List[ModelConfig]
litellm: List[ModelConfig]
openai: List[ModelConfig]
############################ ############################
# SetDefaultModels # SetDefaultModels
############################ ############################
@ -57,3 +70,14 @@ async def set_global_default_suggestions(
data = form_data.model_dump() data = form_data.model_dump()
request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.DEFAULT_PROMPT_SUGGESTIONS return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
@router.post("/models", response_model=SetModelConfigForm)
async def set_global_default_suggestions(
request: Request,
form_data: SetModelConfigForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.MODEL_CONFIG = data
return request.app.state.MODEL_CONFIG

View File

@ -424,6 +424,8 @@ WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
MODEL_CONFIG = CONFIG_DATA.get("models", {"ollama": [], "litellm": [], "openai": []})
#################################### ####################################
# WEBUI_SECRET_KEY # WEBUI_SECRET_KEY
#################################### ####################################

View File

@ -58,6 +58,7 @@ from config import (
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
WEBHOOK_URL, WEBHOOK_URL,
ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_EXPORT,
MODEL_CONFIG,
) )
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES

View File

@ -33,7 +33,8 @@ export const getLiteLLMModels = async (token: string = '') => {
id: model.id, id: model.id,
name: model.name ?? model.id, name: model.name ?? model.id,
external: true, external: true,
source: 'LiteLLM' source: 'LiteLLM',
custom_info: model.custom_info ?? {}
})) }))
.sort((a, b) => { .sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);

View File

@ -163,7 +163,12 @@ export const getOpenAIModels = async (token: string = '') => {
return models return models
? models ? models
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true })) .map((model) => ({
id: model.id,
name: model.name ?? model.id,
external: true,
custom_info: model.custom_info ?? {}
}))
.sort((a, b) => { .sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}) })

View File

@ -125,7 +125,7 @@
<option value="" disabled selected>{$i18n.t('Select a model')}</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model} {#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700" <option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{model.name}</option >{model.custom_info?.displayName ?? model.name}</option
> >
{/each} {/each}
</select> </select>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { modelfiles, settings, showSidebar } from '$lib/stores'; import { type Model, modelfiles, settings, showSidebar } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { import {
@ -27,7 +27,7 @@
export let stopResponse: Function; export let stopResponse: Function;
export let autoScroll = true; export let autoScroll = true;
export let selectedModel = ''; export let selectedModel: Model | undefined;
let chatTextAreaElement: HTMLTextAreaElement; let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement; let filesInputElement;
@ -359,6 +359,12 @@
inputFiles.forEach((file) => { inputFiles.forEach((file) => {
console.log(file, file.name.split('.').at(-1)); console.log(file, file.name.split('.').at(-1));
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (selectedModel !== undefined) {
if (!(selectedModel.custom_info?.vision_capable ?? true)) {
toast.error($i18n.t('Selected model does not support image inputs.'));
return;
}
}
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
files = [ files = [
@ -500,7 +506,7 @@
}} }}
/> />
{#if selectedModel !== ''} {#if selectedModel !== 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"
> >
@ -515,14 +521,16 @@
: `${WEBUI_BASE_URL}/static/favicon.png`)} : `${WEBUI_BASE_URL}/static/favicon.png`)}
/> />
<div> <div>
Talking to <span class=" font-medium">{selectedModel.name} </span> Talking to <span class=" font-medium"
>{selectedModel.custom_info?.displayName ?? selectedModel.name}
</span>
</div> </div>
</div> </div>
<div> <div>
<button <button
class="flex items-center" class="flex items-center"
on:click={() => { on:click={() => {
selectedModel = ''; selectedModel = undefined;
}} }}
> >
<XMark /> <XMark />
@ -548,6 +556,12 @@
const _inputFiles = Array.from(inputFiles); const _inputFiles = Array.from(inputFiles);
_inputFiles.forEach((file) => { _inputFiles.forEach((file) => {
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (selectedModel !== undefined) {
if (!(selectedModel.custom_info?.vision_capable ?? true)) {
toast.error($i18n.t('Selected model does not support image inputs.'));
return;
}
}
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
files = [ files = [
@ -880,7 +894,7 @@
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('Escape'); console.log('Escape');
selectedModel = ''; selectedModel = undefined;
} }
}} }}
rows="1" rows="1"

View File

@ -21,8 +21,12 @@
let filteredModels = []; let filteredModels = [];
$: filteredModels = $models $: filteredModels = $models
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')) .filter((p) =>
.sort((a, b) => a.name.localeCompare(b.name)); (p.custom_info?.displayName ?? p.name).includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
)
.sort((a, b) =>
(a.custom_info?.displayName ?? a.name).localeCompare(b.custom_info?.displayName ?? b.name)
);
$: if (prompt) { $: if (prompt) {
selectedIdx = 0; selectedIdx = 0;
@ -156,7 +160,7 @@
on:focus={() => {}} on:focus={() => {}}
> >
<div class=" font-medium text-black line-clamp-1"> <div class=" font-medium text-black line-clamp-1">
{model.name} {model.custom_info?.displayName ?? model.name}
</div> </div>
<!-- <div class=" text-xs text-gray-600 line-clamp-1"> <!-- <div class=" text-xs text-gray-600 line-clamp-1">

View File

@ -343,7 +343,7 @@
{#if message.model in modelfiles} {#if message.model in modelfiles}
{modelfiles[message.model]?.title} {modelfiles[message.model]?.title}
{:else} {:else}
{message.model ? ` ${message.model}` : ''} {message.modelName ? ` ${message.modelName}` : message.model ? ` ${message.model}` : ''}
{/if} {/if}
{#if message.timestamp} {#if message.timestamp}

View File

@ -49,7 +49,7 @@
.filter((model) => model.name !== 'hr') .filter((model) => model.name !== 'hr')
.map((model) => ({ .map((model) => ({
value: model.id, value: model.id,
label: model.name, label: model.custom_info?.displayName ?? model.name,
info: model info: model
}))} }))}
bind:value={selectedModel} bind:value={selectedModel}

View File

@ -247,7 +247,11 @@
<!-- {JSON.stringify(item.info)} --> <!-- {JSON.stringify(item.info)} -->
{#if item.info.external} {#if item.info.external}
<Tooltip content={item.info?.source ?? 'External'}> <Tooltip
content={`${item.info?.source ?? 'External'}${
item.info.custom_info?.description ? '<br>' : ''
}${item.info.custom_info?.description?.replaceAll('\n', '<br>') ?? ''}`}
>
<div class=" mr-2"> <div class=" mr-2">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -274,7 +278,9 @@
item.info?.details?.quantization_level item.info?.details?.quantization_level
? item.info?.details?.quantization_level + ' ' ? item.info?.details?.quantization_level + ' '
: '' : ''
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`} }${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}${
item.info.custom_info?.description ? '<br>' : ''
}${item.info.custom_info?.description?.replaceAll('\n', '<br>') ?? ''}`}
> >
<div class=" mr-2"> <div class=" mr-2">
<svg <svg

View File

@ -244,7 +244,10 @@
{#each $models as model} {#each $models as model}
{#if model.size != null} {#if model.size != null}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> <option value={model.name} class="bg-gray-100 dark:bg-gray-700">
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'} {(model.custom_info?.displayName ?? model.name) +
' (' +
(model.size / 1024 ** 3).toFixed(1) +
' GB)'}
</option> </option>
{/if} {/if}
{/each} {/each}
@ -262,7 +265,7 @@
{#each $models as model} {#each $models as model}
{#if model.name !== 'hr'} {#if model.name !== 'hr'}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> <option value={model.name} class="bg-gray-100 dark:bg-gray-700">
{model.name} {model.custom_info?.displayName ?? model.name}
</option> </option>
{/if} {/if}
{/each} {/each}

View File

@ -13,7 +13,7 @@
uploadModel uploadModel
} from '$lib/apis/ollama'; } from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user } from '$lib/stores'; import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm'; import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
@ -67,6 +67,8 @@
let deleteModelTag = ''; let deleteModelTag = '';
let showModelInfo = false;
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of $models.filter( for (const model of $models.filter(
(m) => (m) =>
@ -587,24 +589,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style><path </style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" opacity=".25"
/><path />
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
@ -705,7 +711,10 @@
{/if} {/if}
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option >{(model.custom_info?.displayName ?? model.name) +
' (' +
(model.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
> >
{/each} {/each}
</select> </select>
@ -833,24 +842,28 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> >
<style>
.spinner_ajPY { .spinner_ajPY {
transform-origin: center; transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear; animation: spinner_AtaB 0.75s infinite linear;
} }
@keyframes spinner_AtaB { @keyframes spinner_AtaB {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style><path </style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" opacity=".25"
/><path />
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY" class="spinner_ajPY"
/></svg />
> </svg>
</div> </div>
{:else} {:else}
<svg <svg
@ -932,6 +945,7 @@
<hr class=" dark:border-gray-700 my-2" /> <hr class=" dark:border-gray-700 my-2" />
{/if} {/if}
<!--TODO: Hide LiteLLM options when ENABLE_LITELLM=false-->
<div class=" space-y-3"> <div class=" space-y-3">
<div class="mt-2 space-y-3 pr-1.5"> <div class="mt-2 space-y-3 pr-1.5">
<div> <div>
@ -1126,6 +1140,79 @@
{/if} {/if}
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-2" />
</div> </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={selectedOllamaUrlIdx}-->
<!-- placeholder={$i18n.t('Select an existing model')}-->
<!-- >-->
<!-- {#each $config. as url, idx}-->
<!-- <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>-->
<!-- {/each}-->
<!-- </select>-->
<!-- </div>-->
<!-- <div>-->
<!-- <div class="flex w-full justify-end">-->
<!-- <Tooltip content="Update All Models" placement="top">-->
<!-- <button-->
<!-- class="p-2.5 flex gap-2 items-center 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={() => {-->
<!-- updateModelsHandler();-->
<!-- }}-->
<!-- >-->
<!-- <svg-->
<!-- xmlns="http://www.w3.org/2000/svg"-->
<!-- viewBox="0 0 16 16"-->
<!-- fill="currentColor"-->
<!-- class="w-4 h-4"-->
<!-- >-->
<!-- <path-->
<!-- d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"-->
<!-- />-->
<!-- <path-->
<!-- d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"-->
<!-- />-->
<!-- </svg>-->
<!-- </button>-->
<!-- </Tooltip>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- {#if updateModelId}-->
<!-- Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}-->
<!-- {/if}-->
<!-- </div>-->
<!-- {/if}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div> </div>
</div> </div>

View File

@ -321,7 +321,10 @@
{/if} {/if}
{#each $models.filter((m) => m.id && !m.external) as model} {#each $models.filter((m) => m.id && !m.external) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option >{(model.custom_info?.displayName ?? model.name) +
' (' +
(model.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
> >
{/each} {/each}
</select> </select>

View File

@ -39,27 +39,34 @@ export const showSidebar = writable(false);
export const showSettings = writable(false); export const showSettings = writable(false);
export const showChangelog = writable(false); export const showChangelog = writable(false);
type Model = OpenAIModel | OllamaModel; export type Model = OpenAIModel | OllamaModel;
type OpenAIModel = { type ModelCustomInfo = {
id: string; name?: string;
name: string; displayName?: string;
external: boolean; description?: string;
source?: string; vision_capable?: boolean;
}; };
type OllamaModel = { type BaseModel = {
id: string; id: string;
name: string; name: string;
custom_info?: ModelCustomInfo;
};
// Ollama specific fields interface OpenAIModel extends BaseModel {
external: boolean;
source?: string;
}
interface OllamaModel extends BaseModel {
details: OllamaModelDetails; details: OllamaModelDetails;
size: number; size: number;
description: string; description: string;
model: string; model: string;
modified_at: string; modified_at: string;
digest: string; digest: string;
}; }
type OllamaModelDetails = { type OllamaModelDetails = {
parent_model: string; parent_model: string;
@ -129,6 +136,20 @@ type Config = {
default_models?: string[]; default_models?: string[];
default_prompt_suggestions?: PromptSuggestion[]; default_prompt_suggestions?: PromptSuggestion[];
trusted_header_auth?: boolean; trusted_header_auth?: boolean;
model_config?: GlobalModelConfig;
};
type GlobalModelConfig = {
ollama?: ModelConfig[];
litellm?: ModelConfig[];
openai?: ModelConfig[];
};
type ModelConfig = {
id?: string;
name?: string;
description?: string;
vision_capable?: boolean;
}; };
type PromptSuggestion = { type PromptSuggestion = {

View File

@ -16,7 +16,8 @@
config, config,
WEBUI_NAME, WEBUI_NAME,
tags as _tags, tags as _tags,
showSidebar showSidebar,
type Model
} from '$lib/stores'; } from '$lib/stores';
import { copyToClipboard, splitStream } from '$lib/utils'; import { copyToClipboard, splitStream } from '$lib/utils';
@ -53,7 +54,7 @@
let showModelSelector = true; let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let atSelectedModel = ''; let atSelectedModel: Model | undefined;
let selectedModelfile = null; let selectedModelfile = null;
$: selectedModelfile = $: selectedModelfile =
@ -254,50 +255,66 @@
const _chatId = JSON.parse(JSON.stringify($chatId)); const _chatId = JSON.parse(JSON.stringify($chatId));
await Promise.all( await Promise.all(
(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => { (atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels).map(
console.log('modelId', modelId); async (modelId) => {
const model = $models.filter((m) => m.id === modelId).at(0); console.log('modelId', modelId);
const model = $models.filter((m) => m.id === modelId).at(0);
if (model) { if (model) {
// Create response message // If there are image files, check if model is vision capable
let responseMessageId = uuidv4(); const hasImages = messages.some((message) =>
let responseMessage = { message.files?.some((file) => file.type === 'image')
parentId: parentId, );
id: responseMessageId, if (hasImages && !(model.custom_info?.vision_capable ?? true)) {
childrenIds: [], toast.error(
role: 'assistant', $i18n.t('Model {{modelName}} is not vision capable', {
content: '', modelName: model.custom_info?.displayName ?? model.name ?? model.id
model: model.id, })
timestamp: Math.floor(Date.now() / 1000) // Unix epoch );
}; }
// Add message to history and Set currentId to messageId // Create response message
history.messages[responseMessageId] = responseMessage; let responseMessageId = uuidv4();
history.currentId = responseMessageId; let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant',
content: '',
model: model.id,
modelName: model.custom_info?.displayName ?? model.name ?? model.id,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
// Append messageId to childrenIds of parent message // Add message to history and Set currentId to messageId
if (parentId !== null) { history.messages[responseMessageId] = responseMessage;
history.messages[parentId].childrenIds = [ history.currentId = responseMessageId;
...history.messages[parentId].childrenIds,
responseMessageId // Append messageId to childrenIds of parent message
]; if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}) )
); );
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
}; };
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
const modelName = model.custom_info?.displayName ?? model.name ?? model.id;
model = model.id; model = model.id;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
@ -702,17 +719,17 @@
} else { } else {
toast.error( toast.error(
$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}) })
); );
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}); });
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}); });
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;

View File

@ -15,7 +15,8 @@
config, config,
WEBUI_NAME, WEBUI_NAME,
tags as _tags, tags as _tags,
showSidebar showSidebar,
type Model
} from '$lib/stores'; } from '$lib/stores';
import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils'; import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
@ -57,7 +58,7 @@
// let chatId = $page.params.id; // let chatId = $page.params.id;
let showModelSelector = true; let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let atSelectedModel = ''; let atSelectedModel: Model | undefined;
let selectedModelfile = null; let selectedModelfile = null;
@ -259,43 +260,58 @@
const _chatId = JSON.parse(JSON.stringify($chatId)); const _chatId = JSON.parse(JSON.stringify($chatId));
await Promise.all( await Promise.all(
(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => { (atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels).map(
const model = $models.filter((m) => m.id === modelId).at(0); async (modelId) => {
const model = $models.filter((m) => m.id === modelId).at(0);
if (model) { if (model) {
// Create response message // If there are image files, check if model is vision capable
let responseMessageId = uuidv4(); const hasImages = messages.some((message) =>
let responseMessage = { message.files?.some((file) => file.type === 'image')
parentId: parentId, );
id: responseMessageId, if (hasImages && !(model.custom_info?.vision_capable ?? true)) {
childrenIds: [], toast.error(
role: 'assistant', $i18n.t('Model {{modelName}} is not vision capable', {
content: '', modelName: model.custom_info?.displayName ?? model.name ?? model.id
model: model.id, })
timestamp: Math.floor(Date.now() / 1000) // Unix epoch );
}; }
// Add message to history and Set currentId to messageId // Create response message
history.messages[responseMessageId] = responseMessage; let responseMessageId = uuidv4();
history.currentId = responseMessageId; let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant',
content: '',
model: model.id,
modelName: model.custom_info?.displayName ?? model.name ?? model.id,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
// Append messageId to childrenIds of parent message // Add message to history and Set currentId to messageId
if (parentId !== null) { history.messages[responseMessageId] = responseMessage;
history.messages[parentId].childrenIds = [ history.currentId = responseMessageId;
...history.messages[parentId].childrenIds,
responseMessageId // Append messageId to childrenIds of parent message
]; if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else {
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
} }
}) )
); );
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
@ -706,17 +722,17 @@
} else { } else {
toast.error( toast.error(
$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}) })
); );
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}); });
} }
responseMessage.error = true; responseMessage.error = true;
responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
provider: model.name ?? model.id provider: model.custom_info?.displayName ?? model.name ?? model.id
}); });
responseMessage.done = true; responseMessage.done = true;
messages = messages; messages = messages;

View File

@ -326,7 +326,7 @@
.filter((model) => model.name !== 'hr') .filter((model) => model.name !== 'hr')
.map((model) => ({ .map((model) => ({
value: model.id, value: model.id,
label: model.name, label: model.custom_info?.displayName ?? model.name,
info: model info: model
}))} }))}
bind:value={selectedModelId} bind:value={selectedModelId}