Connections.svelte 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
  4. const dispatch = createEventDispatcher();
  5. import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
  6. import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
  7. import { getModels as _getModels } from '$lib/apis';
  8. import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
  9. import { config, models, settings, user } from '$lib/stores';
  10. import Switch from '$lib/components/common/Switch.svelte';
  11. import Spinner from '$lib/components/common/Spinner.svelte';
  12. import Tooltip from '$lib/components/common/Tooltip.svelte';
  13. import Plus from '$lib/components/icons/Plus.svelte';
  14. import OpenAIConnection from './Connections/OpenAIConnection.svelte';
  15. import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
  16. import OllamaConnection from './Connections/OllamaConnection.svelte';
  17. const i18n = getContext('i18n');
  18. const getModels = async () => {
  19. const models = await _getModels(
  20. localStorage.token,
  21. $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
  22. );
  23. return models;
  24. };
  25. // External
  26. let OLLAMA_BASE_URLS = [''];
  27. let OLLAMA_API_CONFIGS = {};
  28. let OPENAI_API_KEYS = [''];
  29. let OPENAI_API_BASE_URLS = [''];
  30. let OPENAI_API_CONFIGS = {};
  31. let ENABLE_OPENAI_API: null | boolean = null;
  32. let ENABLE_OLLAMA_API: null | boolean = null;
  33. let directConnectionsConfig = null;
  34. let pipelineUrls = {};
  35. let showAddOpenAIConnectionModal = false;
  36. let showAddOllamaConnectionModal = false;
  37. const updateOpenAIHandler = async () => {
  38. if (ENABLE_OPENAI_API !== null) {
  39. // Remove trailing slashes
  40. OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, ''));
  41. // Check if API KEYS length is same than API URLS length
  42. if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
  43. // if there are more keys than urls, remove the extra keys
  44. if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
  45. OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
  46. }
  47. // if there are more urls than keys, add empty keys
  48. if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
  49. const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
  50. for (let i = 0; i < diff; i++) {
  51. OPENAI_API_KEYS.push('');
  52. }
  53. }
  54. }
  55. const res = await updateOpenAIConfig(localStorage.token, {
  56. ENABLE_OPENAI_API: ENABLE_OPENAI_API,
  57. OPENAI_API_BASE_URLS: OPENAI_API_BASE_URLS,
  58. OPENAI_API_KEYS: OPENAI_API_KEYS,
  59. OPENAI_API_CONFIGS: OPENAI_API_CONFIGS
  60. }).catch((error) => {
  61. toast.error(`${error}`);
  62. });
  63. if (res) {
  64. toast.success($i18n.t('OpenAI API settings updated'));
  65. await models.set(await getModels());
  66. }
  67. }
  68. };
  69. const updateOllamaHandler = async () => {
  70. if (ENABLE_OLLAMA_API !== null) {
  71. // Remove trailing slashes
  72. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.map((url) => url.replace(/\/$/, ''));
  73. const res = await updateOllamaConfig(localStorage.token, {
  74. ENABLE_OLLAMA_API: ENABLE_OLLAMA_API,
  75. OLLAMA_BASE_URLS: OLLAMA_BASE_URLS,
  76. OLLAMA_API_CONFIGS: OLLAMA_API_CONFIGS
  77. }).catch((error) => {
  78. toast.error(`${error}`);
  79. });
  80. if (res) {
  81. toast.success($i18n.t('Ollama API settings updated'));
  82. await models.set(await getModels());
  83. }
  84. }
  85. };
  86. const updateDirectConnectionsHandler = async () => {
  87. const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
  88. (error) => {
  89. toast.error(`${error}`);
  90. }
  91. );
  92. if (res) {
  93. toast.success($i18n.t('Direct Connections settings updated'));
  94. await models.set(await getModels());
  95. }
  96. };
  97. const addOpenAIConnectionHandler = async (connection) => {
  98. OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
  99. OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
  100. OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
  101. await updateOpenAIHandler();
  102. };
  103. const addOllamaConnectionHandler = async (connection) => {
  104. OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
  105. OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
  106. ...connection.config,
  107. key: connection.key
  108. };
  109. await updateOllamaHandler();
  110. };
  111. onMount(async () => {
  112. if ($user.role === 'admin') {
  113. let ollamaConfig = {};
  114. let openaiConfig = {};
  115. await Promise.all([
  116. (async () => {
  117. ollamaConfig = await getOllamaConfig(localStorage.token);
  118. })(),
  119. (async () => {
  120. openaiConfig = await getOpenAIConfig(localStorage.token);
  121. })(),
  122. (async () => {
  123. directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
  124. })()
  125. ]);
  126. ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
  127. ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
  128. OPENAI_API_BASE_URLS = openaiConfig.OPENAI_API_BASE_URLS;
  129. OPENAI_API_KEYS = openaiConfig.OPENAI_API_KEYS;
  130. OPENAI_API_CONFIGS = openaiConfig.OPENAI_API_CONFIGS;
  131. OLLAMA_BASE_URLS = ollamaConfig.OLLAMA_BASE_URLS;
  132. OLLAMA_API_CONFIGS = ollamaConfig.OLLAMA_API_CONFIGS;
  133. if (ENABLE_OPENAI_API) {
  134. // get url and idx
  135. for (const [idx, url] of OPENAI_API_BASE_URLS.entries()) {
  136. if (!OPENAI_API_CONFIGS[idx]) {
  137. // Legacy support, url as key
  138. OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[url] || {};
  139. }
  140. }
  141. OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
  142. OPENAI_API_CONFIGS[idx] = OPENAI_API_CONFIGS[idx] || {};
  143. if (!(OPENAI_API_CONFIGS[idx]?.enable ?? true)) {
  144. return;
  145. }
  146. const res = await getOpenAIModels(localStorage.token, idx);
  147. if (res.pipelines) {
  148. pipelineUrls[url] = true;
  149. }
  150. });
  151. }
  152. if (ENABLE_OLLAMA_API) {
  153. for (const [idx, url] of OLLAMA_BASE_URLS.entries()) {
  154. if (!OLLAMA_API_CONFIGS[idx]) {
  155. OLLAMA_API_CONFIGS[idx] = OLLAMA_API_CONFIGS[url] || {};
  156. }
  157. }
  158. }
  159. }
  160. });
  161. const submitHandler = async () => {
  162. updateOpenAIHandler();
  163. updateOllamaHandler();
  164. updateDirectConnectionsHandler();
  165. dispatch('save');
  166. };
  167. </script>
  168. <AddConnectionModal
  169. bind:show={showAddOpenAIConnectionModal}
  170. onSubmit={addOpenAIConnectionHandler}
  171. />
  172. <AddConnectionModal
  173. ollama
  174. bind:show={showAddOllamaConnectionModal}
  175. onSubmit={addOllamaConnectionHandler}
  176. />
  177. <form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
  178. <div class=" overflow-y-scroll scrollbar-hidden h-full">
  179. {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
  180. <div class="my-2">
  181. <div class="mt-2 space-y-2 pr-1.5">
  182. <div class="flex justify-between items-center text-sm">
  183. <div class=" font-medium">{$i18n.t('OpenAI API')}</div>
  184. <div class="flex items-center">
  185. <div class="">
  186. <Switch
  187. bind:state={ENABLE_OPENAI_API}
  188. on:change={async () => {
  189. updateOpenAIHandler();
  190. }}
  191. />
  192. </div>
  193. </div>
  194. </div>
  195. {#if ENABLE_OPENAI_API}
  196. <hr class=" border-gray-100 dark:border-gray-850" />
  197. <div class="">
  198. <div class="flex justify-between items-center">
  199. <div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
  200. <Tooltip content={$i18n.t(`Add Connection`)}>
  201. <button
  202. class="px-1"
  203. on:click={() => {
  204. showAddOpenAIConnectionModal = true;
  205. }}
  206. type="button"
  207. >
  208. <Plus />
  209. </button>
  210. </Tooltip>
  211. </div>
  212. <div class="flex flex-col gap-1.5 mt-1.5">
  213. {#each OPENAI_API_BASE_URLS as url, idx}
  214. <OpenAIConnection
  215. pipeline={pipelineUrls[url] ? true : false}
  216. bind:url
  217. bind:key={OPENAI_API_KEYS[idx]}
  218. bind:config={OPENAI_API_CONFIGS[idx]}
  219. onSubmit={() => {
  220. updateOpenAIHandler();
  221. }}
  222. onDelete={() => {
  223. OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
  224. (url, urlIdx) => idx !== urlIdx
  225. );
  226. OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
  227. let newConfig = {};
  228. OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
  229. newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
  230. });
  231. OPENAI_API_CONFIGS = newConfig;
  232. updateOpenAIHandler();
  233. }}
  234. />
  235. {/each}
  236. </div>
  237. </div>
  238. {/if}
  239. </div>
  240. </div>
  241. <hr class=" border-gray-100 dark:border-gray-850" />
  242. <div class="pr-1.5 my-2">
  243. <div class="flex justify-between items-center text-sm mb-2">
  244. <div class=" font-medium">{$i18n.t('Ollama API')}</div>
  245. <div class="mt-1">
  246. <Switch
  247. bind:state={ENABLE_OLLAMA_API}
  248. on:change={async () => {
  249. updateOllamaHandler();
  250. }}
  251. />
  252. </div>
  253. </div>
  254. {#if ENABLE_OLLAMA_API}
  255. <hr class=" border-gray-100 dark:border-gray-850 my-2" />
  256. <div class="">
  257. <div class="flex justify-between items-center">
  258. <div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
  259. <Tooltip content={$i18n.t(`Add Connection`)}>
  260. <button
  261. class="px-1"
  262. on:click={() => {
  263. showAddOllamaConnectionModal = true;
  264. }}
  265. type="button"
  266. >
  267. <Plus />
  268. </button>
  269. </Tooltip>
  270. </div>
  271. <div class="flex w-full gap-1.5">
  272. <div class="flex-1 flex flex-col gap-1.5 mt-1.5">
  273. {#each OLLAMA_BASE_URLS as url, idx}
  274. <OllamaConnection
  275. bind:url
  276. bind:config={OLLAMA_API_CONFIGS[idx]}
  277. {idx}
  278. onSubmit={() => {
  279. updateOllamaHandler();
  280. }}
  281. onDelete={() => {
  282. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
  283. let newConfig = {};
  284. OLLAMA_BASE_URLS.forEach((url, newIdx) => {
  285. newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
  286. });
  287. OLLAMA_API_CONFIGS = newConfig;
  288. }}
  289. />
  290. {/each}
  291. </div>
  292. </div>
  293. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  294. {$i18n.t('Trouble accessing Ollama?')}
  295. <a
  296. class=" text-gray-300 font-medium underline"
  297. href="https://github.com/open-webui/open-webui#troubleshooting"
  298. target="_blank"
  299. >
  300. {$i18n.t('Click here for help.')}
  301. </a>
  302. </div>
  303. </div>
  304. {/if}
  305. </div>
  306. <hr class=" border-gray-100 dark:border-gray-850" />
  307. <div class="pr-1.5 my-2">
  308. <div class="flex justify-between items-center text-sm">
  309. <div class=" font-medium">{$i18n.t('Direct Connections')}</div>
  310. <div class="flex items-center">
  311. <div class="">
  312. <Switch
  313. bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
  314. on:change={async () => {
  315. updateDirectConnectionsHandler();
  316. }}
  317. />
  318. </div>
  319. </div>
  320. </div>
  321. <div class="mt-1.5">
  322. <div class="text-xs text-gray-500">
  323. {$i18n.t(
  324. 'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
  325. )}
  326. </div>
  327. </div>
  328. </div>
  329. {:else}
  330. <div class="flex h-full justify-center">
  331. <div class="my-auto">
  332. <Spinner className="size-6" />
  333. </div>
  334. </div>
  335. {/if}
  336. </div>
  337. <div class="flex justify-end pt-3 text-sm font-medium">
  338. <button
  339. 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"
  340. type="submit"
  341. >
  342. {$i18n.t('Save')}
  343. </button>
  344. </div>
  345. </form>