Connections.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <script lang="ts">
  2. import { models, user } from '$lib/stores';
  3. import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
  4. const dispatch = createEventDispatcher();
  5. import {
  6. getOllamaConfig,
  7. getOllamaUrls,
  8. getOllamaVersion,
  9. updateOllamaConfig,
  10. updateOllamaUrls
  11. } from '$lib/apis/ollama';
  12. import {
  13. getOpenAIConfig,
  14. getOpenAIKeys,
  15. getOpenAIModels,
  16. getOpenAIUrls,
  17. updateOpenAIConfig,
  18. updateOpenAIKeys,
  19. updateOpenAIUrls
  20. } from '$lib/apis/openai';
  21. import { toast } from 'svelte-sonner';
  22. import Switch from '$lib/components/common/Switch.svelte';
  23. import Spinner from '$lib/components/common/Spinner.svelte';
  24. import Tooltip from '$lib/components/common/Tooltip.svelte';
  25. import { getModels as _getModels } from '$lib/apis';
  26. const i18n = getContext('i18n');
  27. const getModels = async () => {
  28. const models = await _getModels(localStorage.token);
  29. return models;
  30. };
  31. // External
  32. let OLLAMA_BASE_URLS = [''];
  33. let OPENAI_API_KEYS = [''];
  34. let OPENAI_API_BASE_URLS = [''];
  35. let pipelineUrls = {};
  36. let ENABLE_OPENAI_API = null;
  37. let ENABLE_OLLAMA_API = null;
  38. const verifyOpenAIHandler = async (idx) => {
  39. OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
  40. OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
  41. const res = await getOpenAIModels(localStorage.token, idx).catch((error) => {
  42. toast.error(error);
  43. return null;
  44. });
  45. if (res) {
  46. toast.success($i18n.t('Server connection verified'));
  47. if (res.pipelines) {
  48. pipelineUrls[OPENAI_API_BASE_URLS[idx]] = true;
  49. }
  50. }
  51. await models.set(await getModels());
  52. };
  53. const verifyOllamaHandler = async (idx) => {
  54. OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
  55. const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
  56. toast.error(error);
  57. return null;
  58. });
  59. if (res) {
  60. toast.success($i18n.t('Server connection verified'));
  61. }
  62. await models.set(await getModels());
  63. };
  64. const updateOpenAIHandler = async () => {
  65. // Check if API KEYS length is same than API URLS length
  66. if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
  67. // if there are more keys than urls, remove the extra keys
  68. if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
  69. OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
  70. }
  71. // if there are more urls than keys, add empty keys
  72. if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
  73. const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
  74. for (let i = 0; i < diff; i++) {
  75. OPENAI_API_KEYS.push('');
  76. }
  77. }
  78. }
  79. OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
  80. OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
  81. await models.set(await getModels());
  82. };
  83. const updateOllamaUrlsHandler = async () => {
  84. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '');
  85. console.log(OLLAMA_BASE_URLS);
  86. if (OLLAMA_BASE_URLS.length === 0) {
  87. ENABLE_OLLAMA_API = false;
  88. await updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
  89. toast.info($i18n.t('Ollama API disabled'));
  90. } else {
  91. OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
  92. const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
  93. toast.error(error);
  94. return null;
  95. });
  96. if (ollamaVersion) {
  97. toast.success($i18n.t('Server connection verified'));
  98. await models.set(await getModels());
  99. }
  100. }
  101. };
  102. onMount(async () => {
  103. if ($user.role === 'admin') {
  104. await Promise.all([
  105. (async () => {
  106. OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
  107. })(),
  108. (async () => {
  109. OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
  110. })(),
  111. (async () => {
  112. OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
  113. })()
  114. ]);
  115. OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
  116. const res = await getOpenAIModels(localStorage.token, idx);
  117. if (res.pipelines) {
  118. pipelineUrls[url] = true;
  119. }
  120. });
  121. const ollamaConfig = await getOllamaConfig(localStorage.token);
  122. const openaiConfig = await getOpenAIConfig(localStorage.token);
  123. ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
  124. ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
  125. }
  126. });
  127. </script>
  128. <form
  129. class="flex flex-col h-full justify-between text-sm"
  130. on:submit|preventDefault={() => {
  131. updateOpenAIHandler();
  132. updateOllamaUrlsHandler();
  133. dispatch('save');
  134. }}
  135. >
  136. <div class="space-y-3 pr-1.5 overflow-y-scroll scrollbar-hidden h-full">
  137. {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
  138. <div class=" space-y-3">
  139. <div class="mt-2 space-y-2 pr-1.5">
  140. <div class="flex justify-between items-center text-sm">
  141. <div class=" font-medium">{$i18n.t('OpenAI API')}</div>
  142. <div class="mt-1">
  143. <Switch
  144. bind:state={ENABLE_OPENAI_API}
  145. on:change={async () => {
  146. updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
  147. }}
  148. />
  149. </div>
  150. </div>
  151. {#if ENABLE_OPENAI_API}
  152. <div class="flex flex-col gap-1">
  153. {#each OPENAI_API_BASE_URLS as url, idx}
  154. <div class="flex w-full gap-2">
  155. <div class="flex-1 relative">
  156. <input
  157. class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
  158. ? 'pr-8'
  159. : ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  160. placeholder={$i18n.t('API Base URL')}
  161. bind:value={url}
  162. autocomplete="off"
  163. />
  164. {#if pipelineUrls[url]}
  165. <div class=" absolute top-2.5 right-2.5">
  166. <Tooltip content="Pipelines">
  167. <svg
  168. xmlns="http://www.w3.org/2000/svg"
  169. viewBox="0 0 24 24"
  170. fill="currentColor"
  171. class="size-4"
  172. >
  173. <path
  174. 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"
  175. />
  176. <path
  177. 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"
  178. />
  179. <path
  180. 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"
  181. />
  182. </svg>
  183. </Tooltip>
  184. </div>
  185. {/if}
  186. </div>
  187. <div class="flex-1">
  188. <input
  189. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  190. placeholder={$i18n.t('API Key')}
  191. bind:value={OPENAI_API_KEYS[idx]}
  192. autocomplete="off"
  193. />
  194. </div>
  195. <div class="self-center flex items-center">
  196. {#if idx === 0}
  197. <button
  198. class="px-1"
  199. on:click={() => {
  200. OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
  201. OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
  202. }}
  203. type="button"
  204. >
  205. <svg
  206. xmlns="http://www.w3.org/2000/svg"
  207. viewBox="0 0 16 16"
  208. fill="currentColor"
  209. class="w-4 h-4"
  210. >
  211. <path
  212. 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"
  213. />
  214. </svg>
  215. </button>
  216. {:else}
  217. <button
  218. class="px-1"
  219. on:click={() => {
  220. OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
  221. (url, urlIdx) => idx !== urlIdx
  222. );
  223. OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
  224. }}
  225. type="button"
  226. >
  227. <svg
  228. xmlns="http://www.w3.org/2000/svg"
  229. viewBox="0 0 16 16"
  230. fill="currentColor"
  231. class="w-4 h-4"
  232. >
  233. <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" />
  234. </svg>
  235. </button>
  236. {/if}
  237. </div>
  238. <div class="flex">
  239. <Tooltip content="Verify connection" className="self-start mt-0.5">
  240. <button
  241. class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  242. on:click={() => {
  243. verifyOpenAIHandler(idx);
  244. }}
  245. type="button"
  246. >
  247. <svg
  248. xmlns="http://www.w3.org/2000/svg"
  249. viewBox="0 0 20 20"
  250. fill="currentColor"
  251. class="w-4 h-4"
  252. >
  253. <path
  254. fill-rule="evenodd"
  255. 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"
  256. clip-rule="evenodd"
  257. />
  258. </svg>
  259. </button>
  260. </Tooltip>
  261. </div>
  262. </div>
  263. <div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
  264. {$i18n.t('WebUI will make requests to')}
  265. <span class=" text-gray-200">'{url}/models'</span>
  266. </div>
  267. {/each}
  268. </div>
  269. {/if}
  270. </div>
  271. </div>
  272. <hr class=" dark:border-gray-850" />
  273. <div class="pr-1.5 space-y-2">
  274. <div class="flex justify-between items-center text-sm">
  275. <div class=" font-medium">{$i18n.t('Ollama API')}</div>
  276. <div class="mt-1">
  277. <Switch
  278. bind:state={ENABLE_OLLAMA_API}
  279. on:change={async () => {
  280. updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
  281. if (OLLAMA_BASE_URLS.length === 0) {
  282. OLLAMA_BASE_URLS = [''];
  283. }
  284. }}
  285. />
  286. </div>
  287. </div>
  288. {#if ENABLE_OLLAMA_API}
  289. <div class="flex w-full gap-1.5">
  290. <div class="flex-1 flex flex-col gap-2">
  291. {#each OLLAMA_BASE_URLS as url, idx}
  292. <div class="flex gap-1.5">
  293. <input
  294. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  295. placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
  296. bind:value={url}
  297. />
  298. <div class="self-center flex items-center">
  299. {#if idx === 0}
  300. <button
  301. class="px-1"
  302. on:click={() => {
  303. OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
  304. }}
  305. type="button"
  306. >
  307. <svg
  308. xmlns="http://www.w3.org/2000/svg"
  309. viewBox="0 0 16 16"
  310. fill="currentColor"
  311. class="w-4 h-4"
  312. >
  313. <path
  314. 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"
  315. />
  316. </svg>
  317. </button>
  318. {:else}
  319. <button
  320. class="px-1"
  321. on:click={() => {
  322. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
  323. (url, urlIdx) => idx !== urlIdx
  324. );
  325. }}
  326. type="button"
  327. >
  328. <svg
  329. xmlns="http://www.w3.org/2000/svg"
  330. viewBox="0 0 16 16"
  331. fill="currentColor"
  332. class="w-4 h-4"
  333. >
  334. <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" />
  335. </svg>
  336. </button>
  337. {/if}
  338. </div>
  339. <div class="flex">
  340. <Tooltip content="Verify connection" className="self-start mt-0.5">
  341. <button
  342. class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  343. on:click={() => {
  344. verifyOllamaHandler(idx);
  345. }}
  346. type="button"
  347. >
  348. <svg
  349. xmlns="http://www.w3.org/2000/svg"
  350. viewBox="0 0 20 20"
  351. fill="currentColor"
  352. class="w-4 h-4"
  353. >
  354. <path
  355. fill-rule="evenodd"
  356. 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"
  357. clip-rule="evenodd"
  358. />
  359. </svg>
  360. </button>
  361. </Tooltip>
  362. </div>
  363. </div>
  364. {/each}
  365. </div>
  366. </div>
  367. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  368. {$i18n.t('Trouble accessing Ollama?')}
  369. <a
  370. class=" text-gray-300 font-medium underline"
  371. href="https://github.com/open-webui/open-webui#troubleshooting"
  372. target="_blank"
  373. >
  374. {$i18n.t('Click here for help.')}
  375. </a>
  376. </div>
  377. {/if}
  378. </div>
  379. {:else}
  380. <div class="flex h-full justify-center">
  381. <div class="my-auto">
  382. <Spinner className="size-6" />
  383. </div>
  384. </div>
  385. {/if}
  386. </div>
  387. <div class="flex justify-end pt-3 text-sm font-medium">
  388. <button
  389. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  390. type="submit"
  391. >
  392. {$i18n.t('Save')}
  393. </button>
  394. </div>
  395. </form>