From f5b1ae424c3fd933591057cab941ee1bccea6ee5 Mon Sep 17 00:00:00 2001 From: Sergei Shitikov Date: Fri, 20 Jun 2025 17:55:53 +0200 Subject: [PATCH] Support DMR in the admin area --- src/lib/apis/dmr/index.ts | 455 ++++++++++++++++++ src/lib/components/AddConnectionModal.svelte | 22 +- .../admin/Settings/Connections.svelte | 98 +++- .../Settings/Connections/DMRConnection.svelte | 86 ++++ .../Connections/ManageDMRModal.svelte | 45 ++ .../Settings/Models/Manage/ManageDMR.svelte | 115 +++++ src/lib/constants.ts | 1 + 7 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 src/lib/apis/dmr/index.ts create mode 100644 src/lib/components/admin/Settings/Connections/DMRConnection.svelte create mode 100644 src/lib/components/admin/Settings/Connections/ManageDMRModal.svelte create mode 100644 src/lib/components/admin/Settings/Models/Manage/ManageDMR.svelte diff --git a/src/lib/apis/dmr/index.ts b/src/lib/apis/dmr/index.ts new file mode 100644 index 000000000..24dc81736 --- /dev/null +++ b/src/lib/apis/dmr/index.ts @@ -0,0 +1,455 @@ +import { DMR_API_BASE_URL } from '$lib/constants'; + +export const verifyDMRConnection = async (token: string = '', connection: object = {}) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/verify`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...connection + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `DMR: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDMRConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/config`, { + 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) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type DMRConfig = { + ENABLE_DMR_API: boolean; + DMR_BASE_URL: string; + DMR_API_CONFIG: object; +}; + +export const updateDMRConfig = async (token: string = '', config: DMRConfig) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDMRUrl = async (token: string = '') => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/url`, { + 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) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.DMR_BASE_URL; +}; + +export const updateDMRUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.DMR_BASE_URL; +}; + +export const getDMRVersion = async (token: string) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/version`, { + 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) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.version ?? ''; +}; + +export const getDMRModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/tags`, { + 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) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.models ?? []; +}; + +export const getDMRModelInfo = async (token: string, name: string) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/show`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTextCompletion = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model: model, + prompt: text, + stream: false + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateChatCompletion = async (token: string = '', body: object) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/chat`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateEmbeddings = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/api/embeddings`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model: model, + prompt: text + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +// OpenAI-compatible endpoints +export const generateOpenAIChatCompletion = async (token: string = '', body: object) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/v1/chat/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateOpenAICompletion = async (token: string = '', body: object) => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/v1/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${DMR_API_BASE_URL}/v1/models`, { + 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) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; \ No newline at end of file diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index 2104d8f93..095417783 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -6,6 +6,7 @@ import { models } from '$lib/stores'; import { verifyOpenAIConnection } from '$lib/apis/openai'; import { verifyOllamaConnection } from '$lib/apis/ollama'; + import { verifyDMRConnection } from '$lib/apis/dmr'; import Modal from '$lib/components/common/Modal.svelte'; import Plus from '$lib/components/icons/Plus.svelte'; @@ -23,6 +24,7 @@ export let edit = false; export let ollama = false; + export let dmr = false; export let direct = false; export let connection = null; @@ -64,6 +66,22 @@ } }; + const verifyDMRHandler = async () => { + // remove trailing slash from url + url = url.replace(/\/$/, ''); + + const res = await verifyDMRConnection(localStorage.token, { + url, + key + }).catch((error) => { + toast.error(`${error}`); + }); + + if (res) { + toast.success($i18n.t('Server connection verified')); + } + }; + const verifyOpenAIHandler = async () => { // remove trailing slash from url url = url.replace(/\/$/, ''); @@ -91,6 +109,8 @@ const verifyHandler = () => { if (ollama) { verifyOllamaHandler(); + } else if (dmr) { + verifyDMRHandler(); } else { verifyOpenAIHandler(); } @@ -172,7 +192,7 @@ prefixId = connection.config?.prefix_id ?? ''; modelIds = connection.config?.model_ids ?? []; - if (ollama) { + if (ollama || dmr) { connectionType = connection.config?.connection_type ?? 'local'; } else { connectionType = connection.config?.connection_type ?? 'external'; diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte index ac0566f22..cde6ed09f 100644 --- a/src/lib/components/admin/Settings/Connections.svelte +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -6,6 +6,7 @@ import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama'; import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai'; + import { getDMRConfig, updateDMRConfig } from '$lib/apis/dmr'; import { getModels as _getModels } from '$lib/apis'; import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs'; @@ -19,6 +20,7 @@ import OpenAIConnection from './Connections/OpenAIConnection.svelte'; import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; import OllamaConnection from './Connections/OllamaConnection.svelte'; + import DMRConnection from './Connections/DMRConnection.svelte'; const i18n = getContext('i18n'); @@ -38,14 +40,19 @@ let OPENAI_API_BASE_URLS = ['']; let OPENAI_API_CONFIGS = {}; + let DMR_BASE_URL = ''; + let DMR_API_CONFIG = {}; + let ENABLE_OPENAI_API: null | boolean = null; let ENABLE_OLLAMA_API: null | boolean = null; + let ENABLE_DMR_API: null | boolean = null; let directConnectionsConfig = null; let pipelineUrls = {}; let showAddOpenAIConnectionModal = false; let showAddOllamaConnectionModal = false; + let showAddDMRConnectionModal = false; const updateOpenAIHandler = async () => { if (ENABLE_OPENAI_API !== null) { @@ -104,6 +111,26 @@ } }; + const updateDMRHandler = async () => { + if (ENABLE_DMR_API !== null) { + // Remove trailing slash + DMR_BASE_URL = DMR_BASE_URL.replace(/\/$/, ''); + + const res = await updateDMRConfig(localStorage.token, { + ENABLE_DMR_API: ENABLE_DMR_API, + DMR_BASE_URL: DMR_BASE_URL, + DMR_API_CONFIG: DMR_API_CONFIG + }).catch((error) => { + toast.error(`${error}`); + }); + + if (res) { + toast.success($i18n.t('DMR API settings updated')); + await models.set(await getModels()); + } + } + }; + const updateDirectConnectionsHandler = async () => { const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch( (error) => { @@ -135,10 +162,21 @@ await updateOllamaHandler(); }; + const addDMRConnectionHandler = async (connection) => { + DMR_BASE_URL = connection.url; + DMR_API_CONFIG = { + ...connection.config, + key: connection.key + }; + + await updateDMRHandler(); + }; + onMount(async () => { if ($user?.role === 'admin') { let ollamaConfig = {}; let openaiConfig = {}; + let dmrConfig = {}; await Promise.all([ (async () => { @@ -147,6 +185,9 @@ (async () => { openaiConfig = await getOpenAIConfig(localStorage.token); })(), + (async () => { + dmrConfig = await getDMRConfig(localStorage.token); + })(), (async () => { directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token); })() @@ -154,6 +195,7 @@ ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API; ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API; + ENABLE_DMR_API = dmrConfig.ENABLE_DMR_API; OPENAI_API_BASE_URLS = openaiConfig.OPENAI_API_BASE_URLS; OPENAI_API_KEYS = openaiConfig.OPENAI_API_KEYS; @@ -162,6 +204,9 @@ OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS; OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS; + DMR_BASE_URL = dmrConfig.DMR_BASE_URL; + DMR_API_CONFIG = dmrConfig.DMR_API_CONFIG; + if (ENABLE_OPENAI_API) { // get url and idx for (const [idx, url] of OPENAI_API_BASE_URLS.entries()) { @@ -196,6 +241,7 @@ const submitHandler = async () => { updateOpenAIHandler(); updateOllamaHandler(); + updateDMRHandler(); updateDirectConnectionsHandler(); dispatch('save'); @@ -213,9 +259,15 @@ onSubmit={addOllamaConnectionHandler} /> + +
- {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null} + {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && ENABLE_DMR_API !== null && directConnectionsConfig !== null}
@@ -360,6 +412,50 @@
+
+
+
{$i18n.t('Docker Model Runner API')}
+ +
+ { + updateDMRHandler(); + }} + /> +
+
+ + {#if ENABLE_DMR_API} +
+ +
+
+
{$i18n.t('Docker Model Runner Connection')}
+
+ +
+
+ { + updateDMRHandler(); + }} + onDelete={() => { + DMR_BASE_URL = ''; + DMR_API_CONFIG = {}; + updateDMRHandler(); + }} + /> +
+
+
+ {/if} +
+ +
+
{$i18n.t('Direct Connections')}
diff --git a/src/lib/components/admin/Settings/Connections/DMRConnection.svelte b/src/lib/components/admin/Settings/Connections/DMRConnection.svelte new file mode 100644 index 000000000..722d5f2bf --- /dev/null +++ b/src/lib/components/admin/Settings/Connections/DMRConnection.svelte @@ -0,0 +1,86 @@ + + + { + showDeleteConfirmDialog = true; + }} + onSubmit={(connection) => { + url = connection.url; + config = { ...connection.config, key: connection.key }; + onSubmit(connection); + }} +/> + + { + onDelete(); + showConfigModal = false; + }} +/> + + + +
+ + {#if !(config?.enable ?? true)} +
+ {/if} + + +
+ +
+ + + +
+
\ No newline at end of file diff --git a/src/lib/components/admin/Settings/Connections/ManageDMRModal.svelte b/src/lib/components/admin/Settings/Connections/ManageDMRModal.svelte new file mode 100644 index 000000000..acf7979c3 --- /dev/null +++ b/src/lib/components/admin/Settings/Connections/ManageDMRModal.svelte @@ -0,0 +1,45 @@ + + + +
+
+
+
+ {$i18n.t('Manage DMR')} +
+
+ +
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/lib/components/admin/Settings/Models/Manage/ManageDMR.svelte b/src/lib/components/admin/Settings/Models/Manage/ManageDMR.svelte new file mode 100644 index 000000000..15a27e15a --- /dev/null +++ b/src/lib/components/admin/Settings/Models/Manage/ManageDMR.svelte @@ -0,0 +1,115 @@ + + +
+
+ {#if loading} +
+
+ +
+
+ {:else} + +
+
+
+ {$i18n.t('Version')} +
+
+
+ {dmrVersion || $i18n.t('Unknown')} +
+
+ + +
+
+
+ {$i18n.t('Models')} ({dmrModels.length}) +
+
+ + {#if dmrModels.length === 0} +
+ {$i18n.t('No models found')} +
+ {:else} +
+ {#each dmrModels as model} +
+
+
+ {model.name || model.id} +
+ {#if model.description} +
+ {model.description} +
+ {/if} +
+ {#if model.size} +
+ {model.size} +
+ {/if} +
+ {/each} +
+ {/if} +
+ + +
+
{$i18n.t('Note')}
+
+ {$i18n.t('DMR does not support model management operations. Models are managed by the Docker Model Runner service directly.')} +
+
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d92f33671..fe130b882 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -9,6 +9,7 @@ export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`; export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai`; +export const DMR_API_BASE_URL = `${WEBUI_BASE_URL}/dmr`; export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/audio`; export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/images`; export const RETRIEVAL_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1/retrieval`;