refac
This commit is contained in:
@@ -131,9 +131,185 @@ export const getOpenAIModels = async (token: string, urlIdx?: number) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getProviderModelCatalog = async (token: string, urlIdx: number) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/models/${urlIdx}/catalog`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadProviderModel = async (token: string, urlIdx: number, model: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/models/${urlIdx}/download`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ model })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getProviderModelDownloadStatus = async (token: string, urlIdx: number, jobId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${OPENAI_API_BASE_URL}/models/${urlIdx}/download/status/${encodeURIComponent(jobId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const loadProviderModel = async (token: string, urlIdx: number, model: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/models/${urlIdx}/load`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ model })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const unloadProviderModel = async (
|
||||
token: string,
|
||||
urlIdx: number,
|
||||
model: string,
|
||||
instanceId?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/models/${urlIdx}/unload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ model, ...(instanceId ? { instance_id: instanceId } : {}) })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteProviderModel = async (token: string, urlIdx: number, model: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${OPENAI_API_BASE_URL}/models/${urlIdx}?${new URLSearchParams({ model })}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err?.detail ?? err?.error?.message ?? 'Server connection failed';
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const verifyOpenAIConnection = async (
|
||||
token: string = '',
|
||||
connection: dict = {},
|
||||
connection: Record<string, any> = {},
|
||||
direct: boolean = false
|
||||
) => {
|
||||
const { url, key, config } = connection;
|
||||
|
||||
@@ -606,6 +606,7 @@
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="azure">{$i18n.t('Azure OpenAI')}</option>
|
||||
<option value="llama.cpp">{$i18n.t('llama.cpp')}</option>
|
||||
<option value="lmstudio">{$i18n.t('LM Studio')}</option>
|
||||
<option value="litellm">{$i18n.t('LiteLLM')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArrowPath from '$lib/components/icons/ArrowPath.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
|
||||
import OpenAIConnection from './Connections/OpenAIConnection.svelte';
|
||||
@@ -50,6 +51,7 @@
|
||||
let pipelineUrls: Record<string, boolean> = {};
|
||||
let showAddOpenAIConnectionModal = false;
|
||||
let showAddOllamaConnectionModal = false;
|
||||
let modelListRefreshing = false;
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
if (ENABLE_OPENAI_API !== null) {
|
||||
@@ -120,6 +122,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
const refreshModelListHandler = async () => {
|
||||
modelListRefreshing = true;
|
||||
|
||||
try {
|
||||
await models.set(await getModels());
|
||||
toast.success($i18n.t('Model list refreshed'));
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
} finally {
|
||||
modelListRefreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const addOpenAIConnectionHandler = async (connection: any) => {
|
||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
|
||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
|
||||
@@ -373,13 +388,33 @@
|
||||
)}
|
||||
let:labelId
|
||||
>
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
ariaLabelledbyId={labelId}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5">
|
||||
{#if connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||
<Tooltip content={$i18n.t('Refresh')}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
disabled={modelListRefreshing}
|
||||
on:click={refreshModelListHandler}
|
||||
aria-label={$i18n.t('Refresh')}
|
||||
>
|
||||
{#if modelListRefreshing}
|
||||
<Spinner className="size-3.5" />
|
||||
{:else}
|
||||
<ArrowPath className="size-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
ariaLabelledbyId={labelId}
|
||||
/>
|
||||
</div>
|
||||
</AdminSettingRow>
|
||||
</AdminSettingSection>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
|
||||
import ManageProviderModels from './ManageProviderModels.svelte';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
export let connections: { idx: number; url: string; provider?: string }[] = [];
|
||||
|
||||
let selectedUrlIdx = `${connections[0]?.idx ?? 0}`;
|
||||
|
||||
const getProviderLabel = (provider = '') => {
|
||||
if (provider === 'lmstudio') return $i18n.t('LM Studio');
|
||||
if (provider === 'llama.cpp') return $i18n.t('llama.cpp');
|
||||
return provider;
|
||||
};
|
||||
|
||||
const providerSupportsDelete = (provider = '') => provider === 'llama.cpp';
|
||||
|
||||
$: if (
|
||||
connections.length > 0 &&
|
||||
!connections.some((connection) => `${connection.idx}` === selectedUrlIdx)
|
||||
) {
|
||||
selectedUrlIdx = `${connections[0].idx}`;
|
||||
}
|
||||
$: selectedConnection =
|
||||
connections.find((connection) => `${connection.idx}` === selectedUrlIdx) ?? connections[0];
|
||||
</script>
|
||||
|
||||
{#if connections.length > 0}
|
||||
<div class="mb-2 text-sm font-normal">{$i18n.t('Model providers')}</div>
|
||||
|
||||
<div class="mb-2.5 flex-1">
|
||||
<SettingsSelect
|
||||
bind:value={selectedUrlIdx}
|
||||
className="w-full"
|
||||
placeholder={$i18n.t('Select an instance')}
|
||||
>
|
||||
{#each connections as connection}
|
||||
<option value={`${connection.idx}`}>
|
||||
{getProviderLabel(connection.provider)} - {connection.url}
|
||||
</option>
|
||||
{/each}
|
||||
</SettingsSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ManageProviderModels
|
||||
urlIdx={Number(selectedUrlIdx)}
|
||||
provider={selectedConnection?.provider ?? ''}
|
||||
providerLabel={getProviderLabel(selectedConnection?.provider)}
|
||||
supportsDelete={providerSupportsDelete(selectedConnection?.provider)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,295 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
import {
|
||||
deleteProviderModel,
|
||||
downloadProviderModel,
|
||||
getProviderModelCatalog,
|
||||
loadProviderModel,
|
||||
unloadProviderModel
|
||||
} from '$lib/apis/openai';
|
||||
import { getModels } from '$lib/apis';
|
||||
import { config, models, settings } from '$lib/stores';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Download from '$lib/components/icons/Download.svelte';
|
||||
import Play from '$lib/components/icons/Play.svelte';
|
||||
import Refresh from '$lib/components/icons/Refresh.svelte';
|
||||
import Trash from '$lib/components/icons/Trash.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
export let urlIdx: number;
|
||||
|
||||
type ProviderModel = {
|
||||
id?: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
model?: string;
|
||||
display_name?: string;
|
||||
status?: string | { value?: string };
|
||||
path?: string;
|
||||
size?: number;
|
||||
loaded_instances?: { id?: string }[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
let loading = true;
|
||||
let actionModel = '';
|
||||
let modelRef = '';
|
||||
let modelToDelete = '';
|
||||
let showDeleteConfirm = false;
|
||||
export let provider = '';
|
||||
export let providerLabel = '';
|
||||
export let supportsDelete = false;
|
||||
|
||||
let providerModels: ProviderModel[] = [];
|
||||
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2.5 text-left text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 disabled:opacity-50 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
const iconButtonClass =
|
||||
'inline-flex h-7 items-center justify-center rounded-lg border border-gray-100/50 bg-gray-50/40 px-2.5 text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:hover:bg-white/[0.06]';
|
||||
|
||||
const getModelId = (model: ProviderModel) => model.key ?? model.id ?? model.name ?? model.model ?? '';
|
||||
|
||||
const getDisplayName = (model: ProviderModel) => model.display_name ?? getModelId(model);
|
||||
|
||||
const getUnloadId = (model: ProviderModel) => model.loaded_instances?.[0]?.id ?? getModelId(model);
|
||||
|
||||
const getStatus = (model: ProviderModel) => {
|
||||
if (model.loaded_instances?.length) {
|
||||
return 'loaded';
|
||||
}
|
||||
|
||||
if (provider === 'lmstudio') {
|
||||
return 'unloaded';
|
||||
}
|
||||
|
||||
const status = model.status;
|
||||
if (typeof status === 'string') return status;
|
||||
return status?.value ?? 'available';
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
if (status === 'loaded' || status === 'sleeping') {
|
||||
return 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300';
|
||||
}
|
||||
if (status === 'loading' || status === 'downloading') {
|
||||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300';
|
||||
}
|
||||
return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300';
|
||||
};
|
||||
|
||||
const normalizeModels = (response: any): ProviderModel[] => {
|
||||
const entries = Array.isArray(response)
|
||||
? response
|
||||
: (response?.models ?? response?.data ?? response?.items ?? []);
|
||||
|
||||
return entries
|
||||
.map((model: ProviderModel | string) =>
|
||||
typeof model === 'string' ? { id: model, name: model } : model
|
||||
)
|
||||
.filter((model: ProviderModel) => getModelId(model) !== '')
|
||||
.sort((a: ProviderModel, b: ProviderModel) => getModelId(a).localeCompare(getModelId(b)));
|
||||
};
|
||||
|
||||
const refreshModels = async () => {
|
||||
loading = true;
|
||||
const res = await getProviderModelCatalog(localStorage.token, urlIdx).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
providerModels = normalizeModels(res);
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const refreshGlobalModels = async () => {
|
||||
await models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const runModelAction = async (
|
||||
modelId: string,
|
||||
action: (token: string, urlIdx: number, model: string) => Promise<unknown>,
|
||||
successMessage: string
|
||||
) => {
|
||||
actionModel = modelId;
|
||||
const res = await action(localStorage.token, urlIdx, modelId).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success(successMessage);
|
||||
await refreshModels();
|
||||
await refreshGlobalModels();
|
||||
}
|
||||
actionModel = '';
|
||||
};
|
||||
|
||||
const downloadModelHandler = async () => {
|
||||
const model = modelRef.trim();
|
||||
if (!model) return;
|
||||
|
||||
await runModelAction(
|
||||
model,
|
||||
downloadProviderModel,
|
||||
$i18n.t('Model download started')
|
||||
);
|
||||
modelRef = '';
|
||||
};
|
||||
|
||||
const loadModelHandler = async (model: string) => {
|
||||
await runModelAction(model, loadProviderModel, $i18n.t('Model loaded successfully'));
|
||||
};
|
||||
|
||||
const unloadModelHandler = async (model: ProviderModel) => {
|
||||
const modelId = getModelId(model);
|
||||
const instanceId = getUnloadId(model);
|
||||
|
||||
actionModel = modelId;
|
||||
const res = await unloadProviderModel(localStorage.token, urlIdx, modelId, instanceId).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Model unloaded successfully'));
|
||||
await refreshModels();
|
||||
await refreshGlobalModels();
|
||||
}
|
||||
actionModel = '';
|
||||
};
|
||||
|
||||
const deleteModelHandler = async () => {
|
||||
await runModelAction(modelToDelete, deleteProviderModel, $i18n.t('Model deleted successfully'));
|
||||
modelToDelete = '';
|
||||
};
|
||||
|
||||
$: if (urlIdx !== undefined) {
|
||||
refreshModels();
|
||||
}
|
||||
|
||||
onMount(refreshModels);
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete Model')}
|
||||
message={$i18n.t('This will delete the cached model and cannot be undone.')}
|
||||
onConfirm={deleteModelHandler}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-normal">{providerLabel || provider}</div>
|
||||
<Tooltip content={$i18n.t('Refresh')}>
|
||||
<button class={iconButtonClass} type="button" on:click={refreshModels} disabled={loading}>
|
||||
<Refresh className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<form class="flex gap-1.5" on:submit|preventDefault={downloadModelHandler}>
|
||||
<input
|
||||
class={inputClass}
|
||||
type="text"
|
||||
bind:value={modelRef}
|
||||
placeholder={$i18n.t('Type a model ref')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Tooltip content={$i18n.t('Download Model')}>
|
||||
<button class={iconButtonClass} type="submit" disabled={actionModel !== '' || modelRef.trim() === ''}>
|
||||
<Download className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</form>
|
||||
|
||||
{#if loading}
|
||||
<div class="py-5">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if providerModels.length === 0}
|
||||
<div class="py-5 text-center text-xs text-gray-400">
|
||||
{$i18n.t('No models found')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-h-96 overflow-y-auto rounded-lg border border-gray-100/50 dark:border-white/[0.04]">
|
||||
{#each providerModels as model}
|
||||
{@const modelId = getModelId(model)}
|
||||
{@const displayName = getDisplayName(model)}
|
||||
{@const status = getStatus(model)}
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-b border-gray-100/50 px-2 py-2 last:border-b-0 dark:border-white/[0.04]"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-xs font-medium text-gray-700 dark:text-gray-200">
|
||||
{displayName}
|
||||
</div>
|
||||
{#if displayName !== modelId}
|
||||
<div class="truncate text-[0.65rem] text-gray-400">{modelId}</div>
|
||||
{/if}
|
||||
<div class="mt-1 flex items-center gap-1.5">
|
||||
<span class="rounded-full px-1.5 py-0.5 text-[0.65rem] {getStatusClass(status)}">
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 gap-1">
|
||||
<Tooltip content={$i18n.t('Load Model')}>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
type="button"
|
||||
aria-label={$i18n.t('Load Model')}
|
||||
disabled={actionModel !== '' || status === 'loaded' || status === 'loading'}
|
||||
on:click={() => loadModelHandler(modelId)}
|
||||
>
|
||||
<Play className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Unload Model')}>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
type="button"
|
||||
aria-label={$i18n.t('Unload Model')}
|
||||
disabled={actionModel !== '' || status === 'unloaded'}
|
||||
on:click={() => unloadModelHandler(model)}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if supportsDelete}
|
||||
<Tooltip content={$i18n.t('Delete Model')}>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete Model')}
|
||||
disabled={actionModel !== ''}
|
||||
on:click={() => {
|
||||
modelToDelete = modelId;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
>
|
||||
<Trash className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,38 +1,72 @@
|
||||
<script>
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import ManageOllama from './Manage/ManageOllama.svelte';
|
||||
import { getOllamaConfig } from '$lib/apis/ollama';
|
||||
import { getOpenAIConfig } from '$lib/apis/openai';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
|
||||
import ManageMultipleOllama from './Manage/ManageMultipleOllama.svelte';
|
||||
import ManageMultipleProviderModels from './Manage/ManageMultipleProviderModels.svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let selected = null;
|
||||
let ollamaConfig = null;
|
||||
type ProviderConnection = { idx: number; url: string; provider: string; config: Record<string, any> };
|
||||
const MANAGEMENT_PROVIDERS = new Set(['llama.cpp', 'lmstudio']);
|
||||
|
||||
let selected: '' | 'ollama' | 'provider' | null = null;
|
||||
let ollamaConfig: any = null;
|
||||
let providerConnections: ProviderConnection[] = [];
|
||||
|
||||
$: hasOllamaManagement =
|
||||
ollamaConfig?.ENABLE_OLLAMA_API && (ollamaConfig?.OLLAMA_BASE_URLS ?? []).length > 0;
|
||||
$: hasProviderManagement = providerConnections.length > 0;
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.role === 'admin') {
|
||||
let openaiConfig: any = null;
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
if (ollamaConfig) {
|
||||
providerConnections =
|
||||
openaiConfig?.ENABLE_OPENAI_API
|
||||
? (openaiConfig.OPENAI_API_BASE_URLS ?? [])
|
||||
.map((url: string, idx: number) => ({
|
||||
idx,
|
||||
url,
|
||||
provider:
|
||||
(openaiConfig.OPENAI_API_CONFIGS?.[idx] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[String(idx)] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[url] ??
|
||||
{})?.provider ?? '',
|
||||
config:
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[idx] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[String(idx)] ??
|
||||
openaiConfig.OPENAI_API_CONFIGS?.[url] ??
|
||||
{}
|
||||
}))
|
||||
.filter((connection: ProviderConnection) => MANAGEMENT_PROVIDERS.has(connection.provider))
|
||||
: [];
|
||||
|
||||
const hasOllama = ollamaConfig?.ENABLE_OLLAMA_API && (ollamaConfig?.OLLAMA_BASE_URLS ?? []).length > 0;
|
||||
const hasProvider = providerConnections.length > 0;
|
||||
|
||||
if (hasOllama) {
|
||||
selected = 'ollama';
|
||||
return;
|
||||
}
|
||||
|
||||
selected = '';
|
||||
selected = hasProvider ? 'provider' : '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -64,8 +98,22 @@
|
||||
{:else if selected !== null}
|
||||
<div class=" flex w-full flex-col">
|
||||
<div class=" px-1.5 py-1">
|
||||
{#if hasOllamaManagement && hasProviderManagement}
|
||||
<div class="mb-2">
|
||||
<SettingsSelect
|
||||
bind:value={selected}
|
||||
className="w-full"
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="ollama">{$i18n.t('Ollama')}</option>
|
||||
<option value="provider">{$i18n.t('Model providers')}</option>
|
||||
</SettingsSelect>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selected === 'ollama'}
|
||||
<ManageMultipleOllama {ollamaConfig} />
|
||||
{:else if selected === 'provider'}
|
||||
<ManageMultipleProviderModels connections={providerConnections} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1716,6 +1716,7 @@
|
||||
"Model Parameters": "",
|
||||
"Model Params": "",
|
||||
"Model Permissions": "",
|
||||
"Model list refreshed": "",
|
||||
"Model removed from pinned models": "",
|
||||
"Model removed from selected models": "",
|
||||
"Model Response Mode": "",
|
||||
|
||||
@@ -1719,6 +1719,7 @@
|
||||
"Model Parameters": "",
|
||||
"Model Params": "",
|
||||
"Model Permissions": "",
|
||||
"Model list refreshed": "",
|
||||
"Model removed from pinned models": "",
|
||||
"Model removed from selected models": "",
|
||||
"Model Response Mode": "",
|
||||
|
||||
Reference in New Issue
Block a user