Browse Source

refac: ollama connections

Timothy Jaeryang Baek 8 months ago
parent
commit
607a8b2109

+ 115 - 9
backend/open_webui/apps/ollama/main.py

@@ -17,10 +17,14 @@ from open_webui.config import (
     ENABLE_OLLAMA_API,
     ENABLE_OLLAMA_API,
     MODEL_FILTER_LIST,
     MODEL_FILTER_LIST,
     OLLAMA_BASE_URLS,
     OLLAMA_BASE_URLS,
+    OLLAMA_API_CONFIGS,
     UPLOAD_DIR,
     UPLOAD_DIR,
     AppConfig,
     AppConfig,
 )
 )
-from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
+from open_webui.env import (
+    AIOHTTP_CLIENT_TIMEOUT,
+    AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
+)
 
 
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
@@ -67,6 +71,8 @@ app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
 
 
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
 app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
+app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
+
 app.state.MODELS = {}
 app.state.MODELS = {}
 
 
 
 
@@ -92,17 +98,64 @@ async def get_status():
     return {"status": True}
     return {"status": True}
 
 
 
 
+class ConnectionVerificationForm(BaseModel):
+    url: str
+    key: Optional[str] = None
+
+
+@app.post("/verify")
+async def verify_connection(
+    form_data: ConnectionVerificationForm, user=Depends(get_admin_user)
+):
+    url = form_data.url
+    key = form_data.key
+
+    headers = {}
+    if key:
+        headers["Authorization"] = f"Bearer {key}"
+
+    timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
+    async with aiohttp.ClientSession(timeout=timeout) as session:
+        try:
+            async with session.get(f"{url}/api/version", headers=headers) as r:
+                if r.status != 200:
+                    # Extract response error details if available
+                    error_detail = f"HTTP Error: {r.status}"
+                    res = await r.json()
+                    if "error" in res:
+                        error_detail = f"External Error: {res['error']}"
+                    raise Exception(error_detail)
+
+                response_data = await r.json()
+                return response_data
+
+        except aiohttp.ClientError as e:
+            # ClientError covers all aiohttp requests issues
+            log.exception(f"Client error: {str(e)}")
+            # Handle aiohttp-specific connection issues, timeout etc.
+            raise HTTPException(
+                status_code=500, detail="Open WebUI: Server Connection Error"
+            )
+        except Exception as e:
+            log.exception(f"Unexpected error: {e}")
+            # Generic error handler in case parsing JSON or other steps fail
+            error_detail = f"Unexpected error: {str(e)}"
+            raise HTTPException(status_code=500, detail=error_detail)
+
+
 @app.get("/config")
 @app.get("/config")
 async def get_config(user=Depends(get_admin_user)):
 async def get_config(user=Depends(get_admin_user)):
     return {
     return {
         "ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API,
         "ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API,
         "OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS,
         "OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS,
+        "OLLAMA_API_CONFIGS": app.state.config.OLLAMA_API_CONFIGS,
     }
     }
 
 
 
 
 class OllamaConfigForm(BaseModel):
 class OllamaConfigForm(BaseModel):
     ENABLE_OLLAMA_API: Optional[bool] = None
     ENABLE_OLLAMA_API: Optional[bool] = None
     OLLAMA_BASE_URLS: list[str]
     OLLAMA_BASE_URLS: list[str]
+    OLLAMA_API_CONFIGS: dict
 
 
 
 
 @app.post("/config/update")
 @app.post("/config/update")
@@ -110,17 +163,27 @@ async def update_config(form_data: OllamaConfigForm, user=Depends(get_admin_user
     app.state.config.ENABLE_OLLAMA_API = form_data.ENABLE_OLLAMA_API
     app.state.config.ENABLE_OLLAMA_API = form_data.ENABLE_OLLAMA_API
     app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS
     app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS
 
 
+    app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS
+
+    # Remove any extra configs
+    config_urls = app.state.config.OLLAMA_API_CONFIGS.keys()
+    for url in list(app.state.config.OLLAMA_BASE_URLS):
+        if url not in config_urls:
+            app.state.config.OLLAMA_API_CONFIGS.pop(url, None)
+
     return {
     return {
         "ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API,
         "ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API,
         "OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS,
         "OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS,
+        "OLLAMA_API_CONFIGS": app.state.config.OLLAMA_API_CONFIGS,
     }
     }
 
 
 
 
-async def fetch_url(url):
-    timeout = aiohttp.ClientTimeout(total=3)
+async def aiohttp_get(url, key=None):
+    timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
     try:
     try:
+        headers = {"Authorization": f"Bearer {key}"} if key else {}
         async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
         async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
-            async with session.get(url) as response:
+            async with session.get(url, headers=headers) as response:
                 return await response.json()
                 return await response.json()
     except Exception as e:
     except Exception as e:
         # Handle connection error here
         # Handle connection error here
@@ -204,13 +267,42 @@ def merge_models_lists(model_lists):
 
 
 async def get_all_models():
 async def get_all_models():
     log.info("get_all_models()")
     log.info("get_all_models()")
-
     if app.state.config.ENABLE_OLLAMA_API:
     if app.state.config.ENABLE_OLLAMA_API:
-        tasks = [
-            fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
-        ]
+        tasks = []
+        for idx, url in enumerate(app.state.config.OLLAMA_BASE_URLS):
+            if url not in app.state.config.OLLAMA_API_CONFIGS:
+                tasks.append(aiohttp_get(f"{url}/api/tags"))
+            else:
+                api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
+                enable = api_config.get("enable", True)
+
+                if enable:
+                    tasks.append(aiohttp_get(f"{url}/api/tags"))
+                else:
+                    tasks.append(None)
+
         responses = await asyncio.gather(*tasks)
         responses = await asyncio.gather(*tasks)
 
 
+        for idx, response in enumerate(responses):
+            if response:
+                url = app.state.config.OLLAMA_BASE_URLS[idx]
+                api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
+
+                prefix_id = api_config.get("prefix_id", None)
+                model_ids = api_config.get("model_ids", [])
+
+                if len(model_ids) != 0:
+                    response["models"] = list(
+                        filter(
+                            lambda model: model["model"] in model_ids,
+                            response["models"],
+                        )
+                    )
+
+                if prefix_id:
+                    for model in response["models"]:
+                        model["model"] = f"{prefix_id}.{model['model']}"
+
         models = {
         models = {
             "models": merge_models_lists(
             "models": merge_models_lists(
                 map(
                 map(
@@ -279,7 +371,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
         if url_idx is None:
         if url_idx is None:
             # returns lowest version
             # returns lowest version
             tasks = [
             tasks = [
-                fetch_url(f"{url}/api/version")
+                aiohttp_get(f"{url}/api/version")
                 for url in app.state.config.OLLAMA_BASE_URLS
                 for url in app.state.config.OLLAMA_BASE_URLS
             ]
             ]
             responses = await asyncio.gather(*tasks)
             responses = await asyncio.gather(*tasks)
@@ -718,6 +810,10 @@ async def generate_completion(
             )
             )
 
 
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
     url = app.state.config.OLLAMA_BASE_URLS[url_idx]
+    api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
+    prefix_id = api_config.get("prefix_id", None)
+    if prefix_id:
+        form_data.model = form_data.model.replace(f"{prefix_id}.", "")
     log.info(f"url: {url}")
     log.info(f"url: {url}")
 
 
     return await post_streaming_url(
     return await post_streaming_url(
@@ -799,6 +895,11 @@ async def generate_chat_completion(
     log.info(f"url: {url}")
     log.info(f"url: {url}")
     log.debug(f"generate_chat_completion() - 2.payload = {payload}")
     log.debug(f"generate_chat_completion() - 2.payload = {payload}")
 
 
+    api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
+    prefix_id = api_config.get("prefix_id", None)
+    if prefix_id:
+        payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
+
     return await post_streaming_url(
     return await post_streaming_url(
         f"{url}/api/chat",
         f"{url}/api/chat",
         json.dumps(payload),
         json.dumps(payload),
@@ -874,6 +975,11 @@ async def generate_openai_chat_completion(
     url = get_ollama_url(url_idx, payload["model"])
     url = get_ollama_url(url_idx, payload["model"])
     log.info(f"url: {url}")
     log.info(f"url: {url}")
 
 
+    api_config = app.state.config.OLLAMA_API_CONFIGS.get(url, {})
+    prefix_id = api_config.get("prefix_id", None)
+    if prefix_id:
+        payload["model"] = payload["model"].replace(f"{prefix_id}.", "")
+
     return await post_streaming_url(
     return await post_streaming_url(
         f"{url}/v1/chat/completions",
         f"{url}/v1/chat/completions",
         json.dumps(payload),
         json.dumps(payload),

+ 2 - 2
backend/open_webui/apps/openai/main.py

@@ -206,10 +206,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
 
 
 
 
-async def aiohttp_get(url, key):
+async def aiohttp_get(url, key=None):
     timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
     timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
     try:
     try:
-        headers = {"Authorization": f"Bearer {key}"}
+        headers = {"Authorization": f"Bearer {key}"} if key else {}
         async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
         async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
             async with session.get(url, headers=headers) as response:
             async with session.get(url, headers=headers) as response:
                 return await response.json()
                 return await response.json()

+ 39 - 0
src/lib/apis/ollama/index.ts

@@ -1,5 +1,44 @@
 import { OLLAMA_API_BASE_URL } from '$lib/constants';
 import { OLLAMA_API_BASE_URL } from '$lib/constants';
 
 
+
+export const verifyOllamaConnection = async (
+	token: string = '',
+	url: string = '',
+	key: string = ''
+) => {
+	let error = null;
+
+	const res = await fetch(
+		`${OLLAMA_API_BASE_URL}/verify`,
+		{
+			method: 'POST',
+			headers: {
+				Accept: 'application/json',
+				Authorization: `Bearer ${token}`,
+				'Content-Type': 'application/json',
+			},
+			body: JSON.stringify({
+				url,
+				key
+			})
+		}
+	)
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			error = `Ollama: ${err?.error?.message ?? 'Network Problem'}`;
+			return [];
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const getOllamaConfig = async (token: string = '') => {
 export const getOllamaConfig = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 

+ 50 - 85
src/lib/components/admin/Settings/Connections.svelte

@@ -13,10 +13,11 @@
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Plus from '$lib/components/icons/Plus.svelte';
 
 
 	import OpenAIConnection from './Connections/OpenAIConnection.svelte';
 	import OpenAIConnection from './Connections/OpenAIConnection.svelte';
-	import OpenAIConnectionModal from './Connections/OpenAIConnectionModal.svelte';
-	import Plus from '$lib/components/icons/Plus.svelte';
+	import AddConnectionModal from './Connections/AddConnectionModal.svelte';
+	import OllamaConnection from './Connections/OllamaConnection.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -38,10 +39,14 @@
 
 
 	let pipelineUrls = {};
 	let pipelineUrls = {};
 	let showAddOpenAIConnectionModal = false;
 	let showAddOpenAIConnectionModal = false;
+	let showAddOllamaConnectionModal = false;
 
 
 	const updateOpenAIHandler = async () => {
 	const updateOpenAIHandler = async () => {
 		if (ENABLE_OPENAI_API !== null) {
 		if (ENABLE_OPENAI_API !== null) {
-			OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
+			OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
+				(url, urlIdx) => OPENAI_API_BASE_URLS.indexOf(url) === urlIdx && url !== ''
+			).map((url) => url.replace(/\/$/, ''));
+
 			// Check if API KEYS length is same than API URLS length
 			// Check if API KEYS length is same than API URLS length
 			if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
 			if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
 				// if there are more keys than urls, remove the extra keys
 				// if there are more keys than urls, remove the extra keys
@@ -76,9 +81,10 @@
 
 
 	const updateOllamaHandler = async () => {
 	const updateOllamaHandler = async () => {
 		if (ENABLE_OLLAMA_API !== null) {
 		if (ENABLE_OLLAMA_API !== null) {
-			OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) =>
-				url.replace(/\/$/, '')
-			);
+			// Remove duplicate URLs
+			OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
+				(url, urlIdx) => OLLAMA_BASE_URLS.indexOf(url) === urlIdx && url !== ''
+			).map((url) => url.replace(/\/$/, ''));
 
 
 			console.log(OLLAMA_BASE_URLS);
 			console.log(OLLAMA_BASE_URLS);
 
 
@@ -110,6 +116,13 @@
 		await updateOpenAIHandler();
 		await updateOpenAIHandler();
 	};
 	};
 
 
+	const addOllamaConnectionHandler = async (connection) => {
+		OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
+		OLLAMA_API_CONFIGS[connection.url] = connection.config;
+
+		await updateOllamaHandler();
+	};
+
 	onMount(async () => {
 	onMount(async () => {
 		if ($user.role === 'admin') {
 		if ($user.role === 'admin') {
 			let ollamaConfig = {};
 			let ollamaConfig = {};
@@ -160,11 +173,17 @@
 	});
 	});
 </script>
 </script>
 
 
-<OpenAIConnectionModal
+<AddConnectionModal
 	bind:show={showAddOpenAIConnectionModal}
 	bind:show={showAddOpenAIConnectionModal}
 	onSubmit={addOpenAIConnectionHandler}
 	onSubmit={addOpenAIConnectionHandler}
 />
 />
 
 
+<AddConnectionModal
+	ollama
+	bind:show={showAddOllamaConnectionModal}
+	onSubmit={addOllamaConnectionHandler}
+/>
+
 <form
 <form
 	class="flex flex-col h-full justify-between text-sm"
 	class="flex flex-col h-full justify-between text-sm"
 	on:submit|preventDefault={() => {
 	on:submit|preventDefault={() => {
@@ -219,7 +238,7 @@
 										pipeline={pipelineUrls[url] ? true : false}
 										pipeline={pipelineUrls[url] ? true : false}
 										bind:url
 										bind:url
 										bind:key={OPENAI_API_KEYS[idx]}
 										bind:key={OPENAI_API_KEYS[idx]}
-										bind:config={OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS[idx]]}
+										bind:config={OPENAI_API_CONFIGS[url]}
 										onSubmit={() => {
 										onSubmit={() => {
 											updateOpenAIHandler();
 											updateOpenAIHandler();
 										}}
 										}}
@@ -247,11 +266,7 @@
 						<Switch
 						<Switch
 							bind:state={ENABLE_OLLAMA_API}
 							bind:state={ENABLE_OLLAMA_API}
 							on:change={async () => {
 							on:change={async () => {
-								updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
-
-								if (OLLAMA_BASE_URLS.length === 0) {
-									OLLAMA_BASE_URLS = [''];
-								}
+								updateOllamaHandler();
 							}}
 							}}
 						/>
 						/>
 					</div>
 					</div>
@@ -261,85 +276,35 @@
 					<hr class=" border-gray-50 dark:border-gray-850 my-2" />
 					<hr class=" border-gray-50 dark:border-gray-850 my-2" />
 
 
 					<div class="">
 					<div class="">
-						<div class="flex justify-between items-center mb-1.5">
+						<div class="flex justify-between items-center">
 							<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
 							<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
 
 
-							<button
-								class="px-1"
-								on:click={() => {
-									OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
-								}}
-								type="button"
-							>
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									viewBox="0 0 16 16"
-									fill="currentColor"
-									class="w-4 h-4"
+							<Tooltip content={$i18n.t(`Add Connection`)}>
+								<button
+									class="px-1"
+									on:click={() => {
+										showAddOllamaConnectionModal = true;
+									}}
+									type="button"
 								>
 								>
-									<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>
+									<Plus />
+								</button>
+							</Tooltip>
 						</div>
 						</div>
 
 
 						<div class="flex w-full gap-1.5">
 						<div class="flex w-full gap-1.5">
-							<div class="flex-1 flex flex-col gap-2">
+							<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
 								{#each OLLAMA_BASE_URLS as url, idx}
 								{#each OLLAMA_BASE_URLS as url, idx}
-									<div class="flex gap-1.5">
-										<input
-											class="w-full text-sm bg-transparent outline-none"
-											placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
-											bind:value={url}
-										/>
-
-										<div class="flex gap-1">
-											<Tooltip content="Verify" className="self-start">
-												<button
-													class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
-													on:click={() => {
-														verifyOllamaHandler(idx);
-													}}
-													type="button"
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														viewBox="0 0 20 20"
-														fill="currentColor"
-														class="w-4 h-4"
-													>
-														<path
-															fill-rule="evenodd"
-															d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
-															clip-rule="evenodd"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-
-											<Tooltip content={$i18n.t('Remove')} className="self-start">
-												<button
-													class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
-													on:click={() => {
-														OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
-															(url, urlIdx) => idx !== urlIdx
-														);
-													}}
-													type="button"
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														viewBox="0 0 16 16"
-														fill="currentColor"
-														class="w-4 h-4"
-													>
-														<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
-													</svg>
-												</button>
-											</Tooltip>
-										</div>
-									</div>
+									<OllamaConnection
+										bind:url
+										bind:config={OLLAMA_API_CONFIGS[url]}
+										onSubmit={() => {
+											updateOllamaHandler();
+										}}
+										onDelete={() => {
+											OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
+										}}
+									/>
 								{/each}
 								{/each}
 							</div>
 							</div>
 						</div>
 						</div>

+ 32 - 6
src/lib/components/admin/Settings/Connections/OpenAIConnectionModal.svelte → src/lib/components/admin/Settings/Connections/AddConnectionModal.svelte

@@ -5,6 +5,7 @@
 
 
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
 	import { verifyOpenAIConnection } from '$lib/apis/openai';
 	import { verifyOpenAIConnection } from '$lib/apis/openai';
+	import { verifyOllamaConnection } from '$lib/apis/ollama';
 
 
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
@@ -19,6 +20,7 @@
 
 
 	export let show = false;
 	export let show = false;
 	export let edit = false;
 	export let edit = false;
+	export let ollama = false;
 
 
 	export let connection = null;
 	export let connection = null;
 
 
@@ -33,6 +35,16 @@
 
 
 	let loading = false;
 	let loading = false;
 
 
+	const verifyOllamaHandler = async () => {
+		const res = await verifyOllamaConnection(localStorage.token, url, key).catch((error) => {
+			toast.error(error);
+		});
+
+		if (res) {
+			toast.success($i18n.t('Server connection verified'));
+		}
+	};
+
 	const verifyOpenAIHandler = async () => {
 	const verifyOpenAIHandler = async () => {
 		const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
 		const res = await verifyOpenAIConnection(localStorage.token, url, key).catch((error) => {
 			toast.error(error);
 			toast.error(error);
@@ -43,6 +55,14 @@
 		}
 		}
 	};
 	};
 
 
+	const verifyHandler = () => {
+		if (ollama) {
+			verifyOllamaHandler();
+		} else {
+			verifyOpenAIHandler();
+		}
+	};
+
 	const addModelHandler = () => {
 	const addModelHandler = () => {
 		if (modelId) {
 		if (modelId) {
 			modelIds = [...modelIds, modelId];
 			modelIds = [...modelIds, modelId];
@@ -53,7 +73,7 @@
 	const submitHandler = async () => {
 	const submitHandler = async () => {
 		loading = true;
 		loading = true;
 
 
-		if (!url || !key) {
+		if (!ollama && (!url || !key)) {
 			loading = false;
 			loading = false;
 			toast.error('URL and Key are required');
 			toast.error('URL and Key are required');
 			return;
 			return;
@@ -159,7 +179,7 @@
 								<button
 								<button
 									class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
 									class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
 									on:click={() => {
 									on:click={() => {
-										verifyOpenAIHandler();
+										verifyHandler();
 									}}
 									}}
 									type="button"
 									type="button"
 								>
 								>
@@ -194,7 +214,7 @@
 										className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
 										className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
 										bind:value={key}
 										bind:value={key}
 										placeholder={$i18n.t('API Key')}
 										placeholder={$i18n.t('API Key')}
-										required
+										required={!ollama}
 									/>
 									/>
 								</div>
 								</div>
 							</div>
 							</div>
@@ -249,9 +269,15 @@
 								</div>
 								</div>
 							{:else}
 							{:else}
 								<div class="text-gray-500 text-xs text-center py-2 px-10">
 								<div class="text-gray-500 text-xs text-center py-2 px-10">
-									{$i18n.t('Leave empty to include all models from "{{URL}}/models" endpoint', {
-										URL: url
-									})}
+									{#if ollama}
+										{$i18n.t('Leave empty to include all models from "{{URL}}/api/tags" endpoint', {
+											URL: url
+										})}
+									{:else}
+										{$i18n.t('Leave empty to include all models from "{{URL}}/models" endpoint', {
+											URL: url
+										})}
+									{/if}
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</div>
 						</div>

+ 70 - 0
src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+	import { getContext, tick } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+	import Cog6 from '$lib/components/icons/Cog6.svelte';
+	import AddConnectionModal from './AddConnectionModal.svelte';
+
+	export let onDelete = () => {};
+	export let onSubmit = () => {};
+
+	export let url = '';
+	export let config = {};
+
+	let showConfigModal = false;
+</script>
+
+<AddConnectionModal
+	ollama
+	edit
+	bind:show={showConfigModal}
+	connection={{
+		url,
+		key: config?.key ?? '',
+		config: config
+	}}
+	{onDelete}
+	onSubmit={(connection) => {
+		url = connection.url;
+		config = { ...connection.config, key: connection.key };
+		onSubmit(connection);
+	}}
+/>
+
+<div class="flex gap-1.5">
+	<Tooltip
+		className="w-full relative"
+		content={$i18n.t(`WebUI will make requests to "{{url}}/api/chat"`, {
+			url
+		})}
+		placement="top-start"
+	>
+		{#if !(config?.enable ?? true)}
+			<div
+				class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
+			></div>
+		{/if}
+
+		<input
+			class="w-full text-sm bg-transparent outline-none"
+			placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
+			bind:value={url}
+		/>
+	</Tooltip>
+
+	<div class="flex gap-1">
+		<Tooltip content={$i18n.t('Configure')} className="self-start">
+			<button
+				class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
+				on:click={() => {
+					showConfigModal = true;
+				}}
+				type="button"
+			>
+				<Cog6 />
+			</button>
+		</Tooltip>
+	</div>
+</div>

+ 2 - 2
src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte

@@ -5,7 +5,7 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Cog6 from '$lib/components/icons/Cog6.svelte';
 	import Cog6 from '$lib/components/icons/Cog6.svelte';
-	import OpenAIConnectionModal from './OpenAIConnectionModal.svelte';
+	import AddConnectionModal from './AddConnectionModal.svelte';
 	import { connect } from 'socket.io-client';
 	import { connect } from 'socket.io-client';
 
 
 	export let onDelete = () => {};
 	export let onDelete = () => {};
@@ -20,7 +20,7 @@
 	let showConfigModal = false;
 	let showConfigModal = false;
 </script>
 </script>
 
 
-<OpenAIConnectionModal
+<AddConnectionModal
 	edit
 	edit
 	bind:show={showConfigModal}
 	bind:show={showConfigModal}
 	connection={{
 	connection={{

+ 9 - 17
src/lib/components/admin/Settings/Models.svelte

@@ -36,7 +36,7 @@
 
 
 	let ollamaEnabled = null;
 	let ollamaEnabled = null;
 
 
-	let OLLAMA_URLS = [];
+	let OLLAMA_BASE_URLS = [];
 	let selectedOllamaUrlIdx: number | null = null;
 	let selectedOllamaUrlIdx: number | null = null;
 
 
 	let updateModelId = null;
 	let updateModelId = null;
@@ -532,21 +532,13 @@
 		if (ollamaConfig.ENABLE_OLLAMA_API) {
 		if (ollamaConfig.ENABLE_OLLAMA_API) {
 			ollamaEnabled = true;
 			ollamaEnabled = true;
 
 
-			await Promise.all([
-				(async () => {
-					OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
-						toast.error(error);
-						return [];
-					});
+			OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
 
 
-					if (OLLAMA_URLS.length > 0) {
-						selectedOllamaUrlIdx = 0;
-					}
-				})(),
-				(async () => {
-					ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
-				})()
-			]);
+			if (OLLAMA_BASE_URLS.length > 0) {
+				selectedOllamaUrlIdx = 0;
+			}
+
+			ollamaVersion = true;
 		} else {
 		} else {
 			ollamaEnabled = false;
 			ollamaEnabled = false;
 			toast.error($i18n.t('Ollama API is disabled'));
 			toast.error($i18n.t('Ollama API is disabled'));
@@ -568,7 +560,7 @@
 				<div class="space-y-2 pr-1.5">
 				<div class="space-y-2 pr-1.5">
 					<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
 					<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
 
 
-					{#if OLLAMA_URLS.length > 0}
+					{#if OLLAMA_BASE_URLS.length > 0}
 						<div class="flex gap-2">
 						<div class="flex gap-2">
 							<div class="flex-1 pb-1">
 							<div class="flex-1 pb-1">
 								<select
 								<select
@@ -576,7 +568,7 @@
 									bind:value={selectedOllamaUrlIdx}
 									bind:value={selectedOllamaUrlIdx}
 									placeholder={$i18n.t('Select an Ollama instance')}
 									placeholder={$i18n.t('Select an Ollama instance')}
 								>
 								>
-									{#each OLLAMA_URLS as url, idx}
+									{#each OLLAMA_BASE_URLS as url, idx}
 										<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
 										<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
 									{/each}
 									{/each}
 								</select>
 								</select>