Timothy Jaeryang Baek 2 tháng trước cách đây
mục cha
commit
8daa549146

+ 4 - 4
backend/open_webui/config.py

@@ -685,13 +685,13 @@ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
 
 
 ####################################
-# DIRECT API
+# DIRECT CONNECTIONS
 ####################################
 
-ENABLE_DIRECT_API = PersistentConfig(
-    "ENABLE_DIRECT_API",
+ENABLE_DIRECT_CONNECTIONS = PersistentConfig(
+    "ENABLE_DIRECT_CONNECTIONS",
     "direct.enable",
-    os.environ.get("ENABLE_DIRECT_API", "True").lower() == "true",
+    os.environ.get("ENABLE_DIRECT_CONNECTIONS", "True").lower() == "true",
 )
 
 ####################################

+ 5 - 5
backend/open_webui/main.py

@@ -97,8 +97,8 @@ from open_webui.config import (
     OPENAI_API_BASE_URLS,
     OPENAI_API_KEYS,
     OPENAI_API_CONFIGS,
-    # Direct API
-    ENABLE_DIRECT_API,
+    # Direct Connections
+    ENABLE_DIRECT_CONNECTIONS,
     # Code Interpreter
     ENABLE_CODE_INTERPRETER,
     CODE_INTERPRETER_ENGINE,
@@ -407,11 +407,11 @@ app.state.OPENAI_MODELS = {}
 
 ########################################
 #
-# DIRECT API
+# DIRECT CONNECTIONS
 #
 ########################################
 
-app.state.config.ENABLE_DIRECT_API = ENABLE_DIRECT_API
+app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
 
 ########################################
 #
@@ -1056,7 +1056,7 @@ async def get_app_config(request: Request):
             "enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
             **(
                 {
-                    "enable_direct_api": app.state.config.ENABLE_DIRECT_API,
+                    "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
                     "enable_channels": app.state.config.ENABLE_CHANNELS,
                     "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
                     "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,

+ 11 - 9
backend/open_webui/routers/configs.py

@@ -37,28 +37,30 @@ async def export_config(user=Depends(get_admin_user)):
 
 
 ############################
-# Direct API Config
+# Direct Connections Config
 ############################
 
 
 class DirectAPIConfigForm(BaseModel):
-    ENABLE_DIRECT_API: bool
+    ENABLE_DIRECT_CONNECTIONS: bool
 
 
-@router.get("/direct_api", response_model=DirectAPIConfigForm)
-async def get_direct_api_config(request: Request, user=Depends(get_admin_user)):
+@router.get("/direct_connections", response_model=DirectAPIConfigForm)
+async def get_direct_connections_config(request: Request, user=Depends(get_admin_user)):
     return {
-        "ENABLE_DIRECT_API": request.app.state.config.ENABLE_DIRECT_API,
+        "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
     }
 
 
-@router.post("/direct_api", response_model=DirectAPIConfigForm)
-async def set_direct_api_config(
+@router.post("/direct_connections", response_model=DirectAPIConfigForm)
+async def set_direct_connections_config(
     request: Request, form_data: DirectAPIConfigForm, user=Depends(get_admin_user)
 ):
-    request.app.state.config.ENABLE_DIRECT_API = form_data.ENABLE_DIRECT_API
+    request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
+        form_data.ENABLE_DIRECT_CONNECTIONS
+    )
     return {
-        "ENABLE_DIRECT_API": request.app.state.config.ENABLE_DIRECT_API,
+        "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
     }
 
 

+ 4 - 4
src/lib/apis/configs/index.ts

@@ -58,10 +58,10 @@ export const exportConfig = async (token: string) => {
 	return res;
 };
 
-export const getDirectApiConfig = async (token: string) => {
+export const getDirectConnectionsConfig = async (token: string) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_api`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
 		method: 'GET',
 		headers: {
 			'Content-Type': 'application/json',
@@ -85,10 +85,10 @@ export const getDirectApiConfig = async (token: string) => {
 	return res;
 };
 
-export const setDirectApiConfig = async (token: string, config: object) => {
+export const setDirectConnectionsConfig = async (token: string, config: object) => {
 	let error = null;
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_api`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
 		method: 'POST',
 		headers: {
 			'Content-Type': 'application/json',

+ 0 - 0
src/lib/components/admin/Settings/Connections/AddConnectionModal.svelte → src/lib/components/AddConnectionModal.svelte


+ 19 - 15
src/lib/components/admin/Settings/Connections.svelte

@@ -7,7 +7,7 @@
 	import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
 	import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
 	import { getModels as _getModels } from '$lib/apis';
-	import { getDirectApiConfig, setDirectApiConfig } from '$lib/apis/configs';
+	import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
 
 	import { models, user } from '$lib/stores';
 
@@ -17,7 +17,7 @@
 	import Plus from '$lib/components/icons/Plus.svelte';
 
 	import OpenAIConnection from './Connections/OpenAIConnection.svelte';
-	import AddConnectionModal from './Connections/AddConnectionModal.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
 	import OllamaConnection from './Connections/OllamaConnection.svelte';
 
 	const i18n = getContext('i18n');
@@ -38,7 +38,7 @@
 	let ENABLE_OPENAI_API: null | boolean = null;
 	let ENABLE_OLLAMA_API: null | boolean = null;
 
-	let directApiConfig = null;
+	let directConnectionsConfig = null;
 
 	let pipelineUrls = {};
 	let showAddOpenAIConnectionModal = false;
@@ -101,13 +101,15 @@
 		}
 	};
 
-	const updateDirectAPIHandler = async () => {
-		const res = await setDirectApiConfig(localStorage.token, directApiConfig).catch((error) => {
-			toast.error(`${error}`);
-		});
+	const updateDirectConnectionsHandler = async () => {
+		const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
+			(error) => {
+				toast.error(`${error}`);
+			}
+		);
 
 		if (res) {
-			toast.success($i18n.t('Direct API settings updated'));
+			toast.success($i18n.t('Direct Connections settings updated'));
 			await models.set(await getModels());
 		}
 	};
@@ -143,7 +145,7 @@
 					openaiConfig = await getOpenAIConfig(localStorage.token);
 				})(),
 				(async () => {
-					directApiConfig = await getDirectApiConfig(localStorage.token);
+					directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
 				})()
 			]);
 
@@ -191,7 +193,7 @@
 	const submitHandler = async () => {
 		updateOpenAIHandler();
 		updateOllamaHandler();
-		updateDirectAPIHandler();
+		updateDirectConnectionsHandler();
 
 		dispatch('save');
 	};
@@ -210,7 +212,7 @@
 
 <form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
-		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directApiConfig !== null}
+		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
 			<div class="my-2">
 				<div class="mt-2 space-y-2 pr-1.5">
 					<div class="flex justify-between items-center text-sm">
@@ -356,14 +358,14 @@
 
 			<div class="pr-1.5 my-2">
 				<div class="flex justify-between items-center text-sm">
-					<div class="  font-medium">{$i18n.t('Direct API')}</div>
+					<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
 
 					<div class="flex items-center">
 						<div class="">
 							<Switch
-								bind:state={directApiConfig.ENABLE_DIRECT_API}
+								bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
 								on:change={async () => {
-									updateDirectAPIHandler();
+									updateDirectConnectionsHandler();
 								}}
 							/>
 						</div>
@@ -372,7 +374,9 @@
 
 				<div class="mt-1.5">
 					<div class="text-xs text-gray-500">
-						{$i18n.t('Direct API allows users to use the models directly from their browser.')}
+						{$i18n.t(
+							'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
+						)}
 					</div>
 				</div>
 			</div>

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

@@ -4,7 +4,7 @@
 
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-	import AddConnectionModal from './AddConnectionModal.svelte';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
 
 	import Cog6 from '$lib/components/icons/Cog6.svelte';
 	import Wrench from '$lib/components/icons/Wrench.svelte';

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

@@ -5,7 +5,8 @@
 	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';
+	import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
+
 	import { connect } from 'socket.io-client';
 
 	export let onDelete = () => {};

+ 114 - 0
src/lib/components/chat/Settings/Connections.svelte

@@ -0,0 +1,114 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
+	import { getModels as _getModels } from '$lib/apis';
+
+	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
+
+	import { models, user } from '$lib/stores';
+
+	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 Plus from '$lib/components/icons/Plus.svelte';
+	import Connection from './Connections/Connection.svelte';
+
+	const getModels = async () => {
+		const models = await _getModels(localStorage.token);
+		return models;
+	};
+
+	let config = null;
+
+	let showConnectionModal = false;
+
+	onMount(async () => {});
+
+	const submitHandler = async () => {};
+	const updateHandler = async () => {};
+</script>
+
+<!-- <AddConnectionModal
+	bind:show={showConnectionModal}
+	onSubmit={addConnectionHandler}
+/> -->
+
+<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
+	<div class=" overflow-y-scroll scrollbar-hidden h-full">
+		<div class="my-2">
+			<div class="space-y-2 pr-1.5">
+				<div class="flex justify-between items-center text-sm">
+					<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
+				</div>
+
+				<div class="mt-1.5">
+					<div class="text-xs text-gray-500">
+						{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
+					</div>
+				</div>
+
+				{#if false}
+					<hr class=" border-gray-50 dark:border-gray-850" />
+
+					<div class="">
+						<div class="flex justify-between items-center">
+							<div class="font-medium">{$i18n.t('Manage Connections')}</div>
+
+							<Tooltip content={$i18n.t(`Add Connection`)}>
+								<button
+									class="px-1"
+									on:click={() => {
+										showConnectionModal = true;
+									}}
+									type="button"
+								>
+									<Plus />
+								</button>
+							</Tooltip>
+						</div>
+
+						<div class="flex flex-col gap-1.5 mt-1.5">
+							{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
+								<Connection
+									bind:url
+									bind:key={config.OPENAI_API_KEYS[idx]}
+									bind:config={config.OPENAI_API_CONFIGS[idx]}
+									onSubmit={() => {
+										updateHandler();
+									}}
+									onDelete={() => {
+										config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
+											(url, urlIdx) => idx !== urlIdx
+										);
+										config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
+											(key, keyIdx) => idx !== keyIdx
+										);
+
+										let newConfig = {};
+										config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
+											newConfig[newIdx] =
+												config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+										});
+										config.OPENAI_API_CONFIGS = newConfig;
+									}}
+								/>
+							{/each}
+						</div>
+					</div>
+				{/if}
+			</div>
+		</div>
+
+		<hr class=" border-gray-50 dark:border-gray-850" />
+	</div>
+
+	<div class="flex justify-end pt-3 text-sm font-medium">
+		<button
+			class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
+			type="submit"
+		>
+			{$i18n.t('Save')}
+		</button>
+	</div>
+</form>

+ 106 - 0
src/lib/components/chat/Settings/Connections/Connection.svelte

@@ -0,0 +1,106 @@
+<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 '$lib/components/AddConnectionModal.svelte';
+
+	export let onDelete = () => {};
+	export let onSubmit = () => {};
+
+	export let pipeline = false;
+
+	export let url = '';
+	export let key = '';
+	export let config = {};
+
+	let showConfigModal = false;
+</script>
+
+<AddConnectionModal
+	edit
+	bind:show={showConfigModal}
+	connection={{
+		url,
+		key,
+		config
+	}}
+	{onDelete}
+	onSubmit={(connection) => {
+		url = connection.url;
+		key = connection.key;
+		config = connection.config;
+		onSubmit(connection);
+	}}
+/>
+
+<div class="flex w-full gap-2 items-center">
+	<Tooltip
+		className="w-full relative"
+		content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
+			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}
+		<div class="flex w-full">
+			<div class="flex-1 relative">
+				<input
+					class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
+					placeholder={$i18n.t('API Base URL')}
+					bind:value={url}
+					autocomplete="off"
+				/>
+
+				{#if pipeline}
+					<div class=" absolute top-0.5 right-2.5">
+						<Tooltip content="Pipelines">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								viewBox="0 0 24 24"
+								fill="currentColor"
+								class="size-4"
+							>
+								<path
+									d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
+								/>
+								<path
+									d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
+								/>
+								<path
+									d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
+								/>
+							</svg>
+						</Tooltip>
+					</div>
+				{/if}
+			</div>
+
+			<SensitiveInput
+				inputClassName=" outline-none bg-transparent w-full"
+				placeholder={$i18n.t('API Key')}
+				bind:value={key}
+			/>
+		</div>
+	</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>

+ 40 - 1
src/lib/components/chat/SettingsModal.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { getContext, tick } from 'svelte';
 	import { toast } from 'svelte-sonner';
-	import { models, settings, user } from '$lib/stores';
+	import { config, models, settings, user } from '$lib/stores';
 	import { updateUserSettings } from '$lib/apis/users';
 	import { getModels as _getModels } from '$lib/apis';
 	import { goto } from '$app/navigation';
@@ -17,6 +17,7 @@
 	import Personalization from './Settings/Personalization.svelte';
 	import SearchInput from '../layout/Sidebar/SearchInput.svelte';
 	import Search from '../icons/Search.svelte';
+	import Connections from './Settings/Connections.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -122,6 +123,11 @@
 				'alwaysonwebsearch'
 			]
 		},
+		{
+			id: 'connections',
+			title: 'Connections',
+			keywords: []
+		},
 		{
 			id: 'personalization',
 			title: 'Personalization',
@@ -447,6 +453,32 @@
 								</div>
 								<div class=" self-center">{$i18n.t('Interface')}</div>
 							</button>
+						{:else if tabId === 'connections'}
+							{#if $user.role === 'admin' || ($user.role === 'user' && $config?.features?.enable_direct_connections)}
+								<button
+									class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+									'connections'
+										? ''
+										: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
+									on:click={() => {
+										selectedTab = 'connections';
+									}}
+								>
+									<div class=" self-center mr-2">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
+											/>
+										</svg>
+									</div>
+									<div class=" self-center">{$i18n.t('Connections')}</div>
+								</button>
+							{/if}
 						{:else if tabId === 'personalization'}
 							<button
 								class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
@@ -620,6 +652,13 @@
 							toast.success($i18n.t('Settings saved successfully!'));
 						}}
 					/>
+				{:else if selectedTab === 'connections'}
+					<Connections
+						{saveSettings}
+						on:save={() => {
+							toast.success($i18n.t('Settings saved successfully!'));
+						}}
+					/>
 				{:else if selectedTab === 'personalization'}
 					<Personalization
 						{saveSettings}