Models.svelte 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. <script lang="ts">
  2. import queue from 'async/queue';
  3. import { toast } from 'svelte-sonner';
  4. import {
  5. createModel,
  6. deleteModel,
  7. downloadModel,
  8. getOllamaUrls,
  9. getOllamaVersion,
  10. pullModel,
  11. cancelOllamaRequest,
  12. uploadModel
  13. } from '$lib/apis/ollama';
  14. import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
  15. import { WEBUI_NAME, models, user } from '$lib/stores';
  16. import { splitStream } from '$lib/utils';
  17. import { onMount, getContext } from 'svelte';
  18. import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
  19. import Tooltip from '$lib/components/common/Tooltip.svelte';
  20. const i18n = getContext('i18n');
  21. export let getModels: Function;
  22. let showLiteLLM = false;
  23. let showLiteLLMParams = false;
  24. let modelUploadInputElement: HTMLInputElement;
  25. let liteLLMModelInfo = [];
  26. let liteLLMModel = '';
  27. let liteLLMModelName = '';
  28. let liteLLMAPIBase = '';
  29. let liteLLMAPIKey = '';
  30. let liteLLMRPM = '';
  31. let liteLLMMaxTokens = '';
  32. let deleteLiteLLMModelId = '';
  33. $: liteLLMModelName = liteLLMModel;
  34. // Models
  35. let OLLAMA_URLS = [];
  36. let selectedOllamaUrlIdx: string | null = null;
  37. let updateModelId = null;
  38. let updateProgress = null;
  39. let showExperimentalOllama = false;
  40. let ollamaVersion = '';
  41. const MAX_PARALLEL_DOWNLOADS = 3;
  42. const modelDownloadQueue = queue(
  43. (task: { modelName: string }, cb) =>
  44. pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }),
  45. MAX_PARALLEL_DOWNLOADS
  46. );
  47. let modelDownloadStatus: Record<string, any> = {};
  48. let modelTransferring = false;
  49. let modelTag = '';
  50. let digest = '';
  51. let pullProgress = null;
  52. let modelUploadMode = 'file';
  53. let modelInputFile: File[] | null = null;
  54. let modelFileUrl = '';
  55. let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
  56. let modelFileDigest = '';
  57. let uploadProgress = null;
  58. let uploadMessage = '';
  59. let deleteModelTag = '';
  60. const updateModelsHandler = async () => {
  61. for (const model of $models.filter(
  62. (m) =>
  63. m.size != null &&
  64. (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))
  65. )) {
  66. console.log(model);
  67. updateModelId = model.id;
  68. const res = await pullModel(localStorage.token, model.id, selectedOllamaUrlIdx).catch(
  69. (error) => {
  70. toast.error(error);
  71. return null;
  72. }
  73. );
  74. if (res) {
  75. const reader = res.body
  76. .pipeThrough(new TextDecoderStream())
  77. .pipeThrough(splitStream('\n'))
  78. .getReader();
  79. while (true) {
  80. try {
  81. const { value, done } = await reader.read();
  82. if (done) break;
  83. let lines = value.split('\n');
  84. for (const line of lines) {
  85. if (line !== '') {
  86. let data = JSON.parse(line);
  87. console.log(data);
  88. if (data.error) {
  89. throw data.error;
  90. }
  91. if (data.detail) {
  92. throw data.detail;
  93. }
  94. if (data.status) {
  95. if (data.digest) {
  96. updateProgress = 0;
  97. if (data.completed) {
  98. updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
  99. } else {
  100. updateProgress = 100;
  101. }
  102. } else {
  103. toast.success(data.status);
  104. }
  105. }
  106. }
  107. }
  108. } catch (error) {
  109. console.log(error);
  110. }
  111. }
  112. }
  113. }
  114. updateModelId = null;
  115. updateProgress = null;
  116. };
  117. const pullModelHandler = async () => {
  118. const sanitizedModelTag = modelTag.trim();
  119. if (modelDownloadStatus[sanitizedModelTag]) {
  120. toast.error(
  121. $i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
  122. modelTag: sanitizedModelTag
  123. })
  124. );
  125. return;
  126. }
  127. if (Object.keys(modelDownloadStatus).length === 3) {
  128. toast.error(
  129. $i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
  130. );
  131. return;
  132. }
  133. modelTransferring = true;
  134. modelDownloadQueue.push(
  135. { modelName: sanitizedModelTag },
  136. async (data: { modelName: string; success: boolean; error?: Error }) => {
  137. const { modelName } = data;
  138. // Remove the downloaded model
  139. delete modelDownloadStatus[modelName];
  140. modelDownloadStatus = { ...modelDownloadStatus };
  141. if (!data.success) {
  142. toast.error(data.error);
  143. } else {
  144. toast.success(
  145. $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName })
  146. );
  147. const notification = new Notification($WEBUI_NAME, {
  148. body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }),
  149. icon: `${WEBUI_BASE_URL}/static/favicon.png`
  150. });
  151. models.set(await getModels());
  152. }
  153. }
  154. );
  155. modelTag = '';
  156. modelTransferring = false;
  157. };
  158. const uploadModelHandler = async () => {
  159. modelTransferring = true;
  160. let uploaded = false;
  161. let fileResponse = null;
  162. let name = '';
  163. if (modelUploadMode === 'file') {
  164. const file = modelInputFile ? modelInputFile[0] : null;
  165. if (file) {
  166. uploadMessage = 'Uploading...';
  167. fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
  168. (error) => {
  169. toast.error(error);
  170. return null;
  171. }
  172. );
  173. }
  174. } else {
  175. uploadProgress = 0;
  176. fileResponse = await downloadModel(
  177. localStorage.token,
  178. modelFileUrl,
  179. selectedOllamaUrlIdx
  180. ).catch((error) => {
  181. toast.error(error);
  182. return null;
  183. });
  184. }
  185. if (fileResponse && fileResponse.ok) {
  186. const reader = fileResponse.body
  187. .pipeThrough(new TextDecoderStream())
  188. .pipeThrough(splitStream('\n'))
  189. .getReader();
  190. while (true) {
  191. const { value, done } = await reader.read();
  192. if (done) break;
  193. try {
  194. let lines = value.split('\n');
  195. for (const line of lines) {
  196. if (line !== '') {
  197. let data = JSON.parse(line.replace(/^data: /, ''));
  198. if (data.progress) {
  199. if (uploadMessage) {
  200. uploadMessage = '';
  201. }
  202. uploadProgress = data.progress;
  203. }
  204. if (data.error) {
  205. throw data.error;
  206. }
  207. if (data.done) {
  208. modelFileDigest = data.blob;
  209. name = data.name;
  210. uploaded = true;
  211. }
  212. }
  213. }
  214. } catch (error) {
  215. console.log(error);
  216. }
  217. }
  218. }
  219. if (uploaded) {
  220. const res = await createModel(
  221. localStorage.token,
  222. `${name}:latest`,
  223. `FROM @${modelFileDigest}\n${modelFileContent}`
  224. );
  225. if (res && res.ok) {
  226. const reader = res.body
  227. .pipeThrough(new TextDecoderStream())
  228. .pipeThrough(splitStream('\n'))
  229. .getReader();
  230. while (true) {
  231. const { value, done } = await reader.read();
  232. if (done) break;
  233. try {
  234. let lines = value.split('\n');
  235. for (const line of lines) {
  236. if (line !== '') {
  237. console.log(line);
  238. let data = JSON.parse(line);
  239. console.log(data);
  240. if (data.error) {
  241. throw data.error;
  242. }
  243. if (data.detail) {
  244. throw data.detail;
  245. }
  246. if (data.status) {
  247. if (
  248. !data.digest &&
  249. !data.status.includes('writing') &&
  250. !data.status.includes('sha256')
  251. ) {
  252. toast.success(data.status);
  253. } else {
  254. if (data.digest) {
  255. digest = data.digest;
  256. if (data.completed) {
  257. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  258. } else {
  259. pullProgress = 100;
  260. }
  261. }
  262. }
  263. }
  264. }
  265. }
  266. } catch (error) {
  267. console.log(error);
  268. toast.error(error);
  269. }
  270. }
  271. }
  272. }
  273. modelFileUrl = '';
  274. if (modelUploadInputElement) {
  275. modelUploadInputElement.value = '';
  276. }
  277. modelInputFile = null;
  278. modelTransferring = false;
  279. uploadProgress = null;
  280. models.set(await getModels());
  281. };
  282. const deleteModelHandler = async () => {
  283. const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
  284. (error) => {
  285. toast.error(error);
  286. }
  287. );
  288. if (res) {
  289. toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
  290. }
  291. deleteModelTag = '';
  292. models.set(await getModels());
  293. };
  294. const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
  295. const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
  296. (error) => {
  297. opts.callback({ success: false, error, modelName: opts.modelName });
  298. return null;
  299. }
  300. );
  301. if (res) {
  302. const reader = res.body
  303. .pipeThrough(new TextDecoderStream())
  304. .pipeThrough(splitStream('\n'))
  305. .getReader();
  306. while (true) {
  307. try {
  308. const { value, done } = await reader.read();
  309. if (done) break;
  310. let lines = value.split('\n');
  311. for (const line of lines) {
  312. if (line !== '') {
  313. let data = JSON.parse(line);
  314. console.log(data);
  315. if (data.error) {
  316. throw data.error;
  317. }
  318. if (data.detail) {
  319. throw data.detail;
  320. }
  321. if (data.id) {
  322. modelDownloadStatus[opts.modelName] = {
  323. ...modelDownloadStatus[opts.modelName],
  324. requestId: data.id,
  325. reader,
  326. done: false
  327. };
  328. console.log(data);
  329. }
  330. if (data.status) {
  331. if (data.digest) {
  332. let downloadProgress = 0;
  333. if (data.completed) {
  334. downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
  335. } else {
  336. downloadProgress = 100;
  337. }
  338. modelDownloadStatus[opts.modelName] = {
  339. ...modelDownloadStatus[opts.modelName],
  340. pullProgress: downloadProgress,
  341. digest: data.digest
  342. };
  343. } else {
  344. toast.success(data.status);
  345. modelDownloadStatus[opts.modelName] = {
  346. ...modelDownloadStatus[opts.modelName],
  347. done: data.status === 'success'
  348. };
  349. }
  350. }
  351. }
  352. }
  353. } catch (error) {
  354. console.log(error);
  355. if (typeof error !== 'string') {
  356. error = error.message;
  357. }
  358. opts.callback({ success: false, error, modelName: opts.modelName });
  359. }
  360. }
  361. console.log(modelDownloadStatus[opts.modelName]);
  362. if (modelDownloadStatus[opts.modelName].done) {
  363. opts.callback({ success: true, modelName: opts.modelName });
  364. } else {
  365. opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
  366. }
  367. }
  368. };
  369. const addLiteLLMModelHandler = async () => {
  370. if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
  371. const res = await addLiteLLMModel(localStorage.token, {
  372. name: liteLLMModelName,
  373. model: liteLLMModel,
  374. api_base: liteLLMAPIBase,
  375. api_key: liteLLMAPIKey,
  376. rpm: liteLLMRPM,
  377. max_tokens: liteLLMMaxTokens
  378. }).catch((error) => {
  379. toast.error(error);
  380. return null;
  381. });
  382. if (res) {
  383. if (res.message) {
  384. toast.success(res.message);
  385. }
  386. }
  387. } else {
  388. toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
  389. }
  390. liteLLMModelName = '';
  391. liteLLMModel = '';
  392. liteLLMAPIBase = '';
  393. liteLLMAPIKey = '';
  394. liteLLMRPM = '';
  395. liteLLMMaxTokens = '';
  396. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  397. models.set(await getModels());
  398. };
  399. const deleteLiteLLMModelHandler = async () => {
  400. const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelId).catch(
  401. (error) => {
  402. toast.error(error);
  403. return null;
  404. }
  405. );
  406. if (res) {
  407. if (res.message) {
  408. toast.success(res.message);
  409. }
  410. }
  411. deleteLiteLLMModelId = '';
  412. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  413. models.set(await getModels());
  414. };
  415. onMount(async () => {
  416. OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
  417. toast.error(error);
  418. return [];
  419. });
  420. if (OLLAMA_URLS.length > 0) {
  421. selectedOllamaUrlIdx = 0;
  422. }
  423. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
  424. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  425. });
  426. const cancelModelPullHandler = async (model: string) => {
  427. const { reader, requestId } = modelDownloadStatus[model];
  428. if (reader) {
  429. await reader.cancel();
  430. await cancelOllamaRequest(localStorage.token, requestId);
  431. delete modelDownloadStatus[model];
  432. await deleteModel(localStorage.token, model);
  433. toast.success(`${model} download has been canceled`);
  434. }
  435. };
  436. </script>
  437. <div class="flex flex-col h-full justify-between text-sm">
  438. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
  439. {#if ollamaVersion}
  440. <div class="space-y-2 pr-1.5">
  441. <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
  442. {#if OLLAMA_URLS.length > 0}
  443. <div class="flex gap-2">
  444. <div class="flex-1 pb-1">
  445. <select
  446. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  447. bind:value={selectedOllamaUrlIdx}
  448. placeholder={$i18n.t('Select an Ollama instance')}
  449. >
  450. {#each OLLAMA_URLS as url, idx}
  451. <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
  452. {/each}
  453. </select>
  454. </div>
  455. <div>
  456. <div class="flex w-full justify-end">
  457. <Tooltip content="Update All Models" placement="top">
  458. <button
  459. class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  460. on:click={() => {
  461. updateModelsHandler();
  462. }}
  463. >
  464. <svg
  465. xmlns="http://www.w3.org/2000/svg"
  466. viewBox="0 0 16 16"
  467. fill="currentColor"
  468. class="w-4 h-4"
  469. >
  470. <path
  471. d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
  472. />
  473. <path
  474. d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
  475. />
  476. </svg>
  477. </button>
  478. </Tooltip>
  479. </div>
  480. </div>
  481. </div>
  482. {#if updateModelId}
  483. Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
  484. {/if}
  485. {/if}
  486. <div class="space-y-2">
  487. <div>
  488. <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
  489. <div class="flex w-full">
  490. <div class="flex-1 mr-2">
  491. <input
  492. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  493. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  494. modelTag: 'mistral:7b'
  495. })}
  496. bind:value={modelTag}
  497. />
  498. </div>
  499. <button
  500. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  501. on:click={() => {
  502. pullModelHandler();
  503. }}
  504. disabled={modelTransferring}
  505. >
  506. {#if modelTransferring}
  507. <div class="self-center">
  508. <svg
  509. class=" w-4 h-4"
  510. viewBox="0 0 24 24"
  511. fill="currentColor"
  512. xmlns="http://www.w3.org/2000/svg"
  513. ><style>
  514. .spinner_ajPY {
  515. transform-origin: center;
  516. animation: spinner_AtaB 0.75s infinite linear;
  517. }
  518. @keyframes spinner_AtaB {
  519. 100% {
  520. transform: rotate(360deg);
  521. }
  522. }
  523. </style><path
  524. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  525. opacity=".25"
  526. /><path
  527. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  528. class="spinner_ajPY"
  529. /></svg
  530. >
  531. </div>
  532. {:else}
  533. <svg
  534. xmlns="http://www.w3.org/2000/svg"
  535. viewBox="0 0 16 16"
  536. fill="currentColor"
  537. class="w-4 h-4"
  538. >
  539. <path
  540. d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
  541. />
  542. <path
  543. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  544. />
  545. </svg>
  546. {/if}
  547. </button>
  548. </div>
  549. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  550. {$i18n.t('To access the available model names for downloading,')}
  551. <a
  552. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  553. href="https://ollama.com/library"
  554. target="_blank">{$i18n.t('click here.')}</a
  555. >
  556. </div>
  557. {#if Object.keys(modelDownloadStatus).length > 0}
  558. {#each Object.keys(modelDownloadStatus) as model}
  559. {#if 'pullProgress' in modelDownloadStatus[model]}
  560. <div class="flex flex-col">
  561. <div class="font-medium mb-1">{model}</div>
  562. <div class="">
  563. <div class="flex flex-row justify-between space-x-4 pr-2">
  564. <div class=" flex-1">
  565. <div
  566. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  567. style="width: {Math.max(
  568. 15,
  569. modelDownloadStatus[model].pullProgress ?? 0
  570. )}%"
  571. >
  572. {modelDownloadStatus[model].pullProgress ?? 0}%
  573. </div>
  574. </div>
  575. <Tooltip content="Cancel">
  576. <button
  577. class="text-gray-800 dark:text-gray-100"
  578. on:click={() => {
  579. cancelModelPullHandler(model);
  580. }}
  581. >
  582. <svg
  583. class="w-4 h-4 text-gray-800 dark:text-white"
  584. aria-hidden="true"
  585. xmlns="http://www.w3.org/2000/svg"
  586. width="24"
  587. height="24"
  588. fill="currentColor"
  589. viewBox="0 0 24 24"
  590. >
  591. <path
  592. stroke="currentColor"
  593. stroke-linecap="round"
  594. stroke-linejoin="round"
  595. stroke-width="2"
  596. d="M6 18 17.94 6M18 18 6.06 6"
  597. />
  598. </svg>
  599. </button>
  600. </Tooltip>
  601. </div>
  602. {#if 'digest' in modelDownloadStatus[model]}
  603. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  604. {modelDownloadStatus[model].digest}
  605. </div>
  606. {/if}
  607. </div>
  608. </div>
  609. {/if}
  610. {/each}
  611. {/if}
  612. </div>
  613. <div>
  614. <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  615. <div class="flex w-full">
  616. <div class="flex-1 mr-2">
  617. <select
  618. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  619. bind:value={deleteModelTag}
  620. placeholder={$i18n.t('Select a model')}
  621. >
  622. {#if !deleteModelTag}
  623. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  624. {/if}
  625. {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
  626. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  627. >{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
  628. >
  629. {/each}
  630. </select>
  631. </div>
  632. <button
  633. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  634. on:click={() => {
  635. deleteModelHandler();
  636. }}
  637. >
  638. <svg
  639. xmlns="http://www.w3.org/2000/svg"
  640. viewBox="0 0 16 16"
  641. fill="currentColor"
  642. class="w-4 h-4"
  643. >
  644. <path
  645. fill-rule="evenodd"
  646. d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
  647. clip-rule="evenodd"
  648. />
  649. </svg>
  650. </button>
  651. </div>
  652. </div>
  653. <div class="pt-1">
  654. <div class="flex justify-between items-center text-xs">
  655. <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
  656. <button
  657. class=" text-xs font-medium text-gray-500"
  658. type="button"
  659. on:click={() => {
  660. showExperimentalOllama = !showExperimentalOllama;
  661. }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
  662. >
  663. </div>
  664. </div>
  665. {#if showExperimentalOllama}
  666. <form
  667. on:submit|preventDefault={() => {
  668. uploadModelHandler();
  669. }}
  670. >
  671. <div class=" mb-2 flex w-full justify-between">
  672. <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
  673. <button
  674. class="p-1 px-3 text-xs flex rounded transition"
  675. on:click={() => {
  676. if (modelUploadMode === 'file') {
  677. modelUploadMode = 'url';
  678. } else {
  679. modelUploadMode = 'file';
  680. }
  681. }}
  682. type="button"
  683. >
  684. {#if modelUploadMode === 'file'}
  685. <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
  686. {:else}
  687. <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
  688. {/if}
  689. </button>
  690. </div>
  691. <div class="flex w-full mb-1.5">
  692. <div class="flex flex-col w-full">
  693. {#if modelUploadMode === 'file'}
  694. <div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
  695. <input
  696. id="model-upload-input"
  697. bind:this={modelUploadInputElement}
  698. type="file"
  699. bind:files={modelInputFile}
  700. on:change={() => {
  701. console.log(modelInputFile);
  702. }}
  703. accept=".gguf,.safetensors"
  704. required
  705. hidden
  706. />
  707. <button
  708. type="button"
  709. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
  710. on:click={() => {
  711. modelUploadInputElement.click();
  712. }}
  713. >
  714. {#if modelInputFile && modelInputFile.length > 0}
  715. {modelInputFile[0].name}
  716. {:else}
  717. {$i18n.t('Click here to select')}
  718. {/if}
  719. </button>
  720. </div>
  721. {:else}
  722. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  723. <input
  724. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
  725. ''
  726. ? 'mr-2'
  727. : ''}"
  728. type="url"
  729. required
  730. bind:value={modelFileUrl}
  731. placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
  732. />
  733. </div>
  734. {/if}
  735. </div>
  736. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  737. <button
  738. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
  739. type="submit"
  740. disabled={modelTransferring}
  741. >
  742. {#if modelTransferring}
  743. <div class="self-center">
  744. <svg
  745. class=" w-4 h-4"
  746. viewBox="0 0 24 24"
  747. fill="currentColor"
  748. xmlns="http://www.w3.org/2000/svg"
  749. ><style>
  750. .spinner_ajPY {
  751. transform-origin: center;
  752. animation: spinner_AtaB 0.75s infinite linear;
  753. }
  754. @keyframes spinner_AtaB {
  755. 100% {
  756. transform: rotate(360deg);
  757. }
  758. }
  759. </style><path
  760. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  761. opacity=".25"
  762. /><path
  763. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  764. class="spinner_ajPY"
  765. /></svg
  766. >
  767. </div>
  768. {:else}
  769. <svg
  770. xmlns="http://www.w3.org/2000/svg"
  771. viewBox="0 0 16 16"
  772. fill="currentColor"
  773. class="w-4 h-4"
  774. >
  775. <path
  776. d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
  777. />
  778. <path
  779. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  780. />
  781. </svg>
  782. {/if}
  783. </button>
  784. {/if}
  785. </div>
  786. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  787. <div>
  788. <div>
  789. <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
  790. <textarea
  791. bind:value={modelFileContent}
  792. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
  793. rows="6"
  794. />
  795. </div>
  796. </div>
  797. {/if}
  798. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  799. {$i18n.t('To access the GGUF models available for downloading,')}
  800. <a
  801. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  802. href="https://huggingface.co/models?search=gguf"
  803. target="_blank">{$i18n.t('click here.')}</a
  804. >
  805. </div>
  806. {#if uploadMessage}
  807. <div class="mt-2">
  808. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  809. <div class="w-full rounded-full dark:bg-gray-800">
  810. <div
  811. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  812. style="width: 100%"
  813. >
  814. {uploadMessage}
  815. </div>
  816. </div>
  817. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  818. {modelFileDigest}
  819. </div>
  820. </div>
  821. {:else if uploadProgress !== null}
  822. <div class="mt-2">
  823. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  824. <div class="w-full rounded-full dark:bg-gray-800">
  825. <div
  826. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  827. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  828. >
  829. {uploadProgress ?? 0}%
  830. </div>
  831. </div>
  832. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  833. {modelFileDigest}
  834. </div>
  835. </div>
  836. {/if}
  837. </form>
  838. {/if}
  839. </div>
  840. </div>
  841. <hr class=" dark:border-gray-700 my-2" />
  842. {/if}
  843. <div class=" space-y-3">
  844. <div class="mt-2 space-y-3 pr-1.5">
  845. <div>
  846. <div class="mb-2">
  847. <div class="flex justify-between items-center text-xs">
  848. <div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
  849. <button
  850. class=" text-xs font-medium text-gray-500"
  851. type="button"
  852. on:click={() => {
  853. showLiteLLM = !showLiteLLM;
  854. }}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
  855. >
  856. </div>
  857. </div>
  858. {#if showLiteLLM}
  859. <div>
  860. <div class="flex justify-between items-center text-xs">
  861. <div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
  862. <button
  863. class=" text-xs font-medium text-gray-500"
  864. type="button"
  865. on:click={() => {
  866. showLiteLLMParams = !showLiteLLMParams;
  867. }}
  868. >{showLiteLLMParams
  869. ? $i18n.t('Hide Additional Params')
  870. : $i18n.t('Show Additional Params')}</button
  871. >
  872. </div>
  873. </div>
  874. <div class="my-2 space-y-2">
  875. <div class="flex w-full mb-1.5">
  876. <div class="flex-1 mr-2">
  877. <input
  878. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  879. placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
  880. bind:value={liteLLMModel}
  881. autocomplete="off"
  882. />
  883. </div>
  884. <button
  885. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  886. on:click={() => {
  887. addLiteLLMModelHandler();
  888. }}
  889. >
  890. <svg
  891. xmlns="http://www.w3.org/2000/svg"
  892. viewBox="0 0 16 16"
  893. fill="currentColor"
  894. class="w-4 h-4"
  895. >
  896. <path
  897. 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"
  898. />
  899. </svg>
  900. </button>
  901. </div>
  902. {#if showLiteLLMParams}
  903. <div>
  904. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
  905. <div class="flex w-full">
  906. <div class="flex-1">
  907. <input
  908. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  909. placeholder="Enter Model Name (model_name)"
  910. bind:value={liteLLMModelName}
  911. autocomplete="off"
  912. />
  913. </div>
  914. </div>
  915. </div>
  916. <div>
  917. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
  918. <div class="flex w-full">
  919. <div class="flex-1">
  920. <input
  921. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  922. placeholder={$i18n.t(
  923. 'Enter LiteLLM API Base URL (litellm_params.api_base)'
  924. )}
  925. bind:value={liteLLMAPIBase}
  926. autocomplete="off"
  927. />
  928. </div>
  929. </div>
  930. </div>
  931. <div>
  932. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
  933. <div class="flex w-full">
  934. <div class="flex-1">
  935. <input
  936. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  937. placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
  938. bind:value={liteLLMAPIKey}
  939. autocomplete="off"
  940. />
  941. </div>
  942. </div>
  943. </div>
  944. <div>
  945. <div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
  946. <div class="flex w-full">
  947. <div class="flex-1">
  948. <input
  949. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  950. placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
  951. bind:value={liteLLMRPM}
  952. autocomplete="off"
  953. />
  954. </div>
  955. </div>
  956. </div>
  957. <div>
  958. <div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
  959. <div class="flex w-full">
  960. <div class="flex-1">
  961. <input
  962. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  963. placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
  964. bind:value={liteLLMMaxTokens}
  965. type="number"
  966. min="1"
  967. autocomplete="off"
  968. />
  969. </div>
  970. </div>
  971. </div>
  972. {/if}
  973. </div>
  974. <div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
  975. {$i18n.t('Not sure what to add?')}
  976. <a
  977. class=" text-gray-300 font-medium underline"
  978. href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
  979. target="_blank"
  980. >
  981. {$i18n.t('Click here for help.')}
  982. </a>
  983. </div>
  984. <div>
  985. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  986. <div class="flex w-full">
  987. <div class="flex-1 mr-2">
  988. <select
  989. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  990. bind:value={deleteLiteLLMModelId}
  991. placeholder={$i18n.t('Select a model')}
  992. >
  993. {#if !deleteLiteLLMModelId}
  994. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  995. {/if}
  996. {#each liteLLMModelInfo as model}
  997. <option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
  998. >{model.model_name}</option
  999. >
  1000. {/each}
  1001. </select>
  1002. </div>
  1003. <button
  1004. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  1005. on:click={() => {
  1006. deleteLiteLLMModelHandler();
  1007. }}
  1008. >
  1009. <svg
  1010. xmlns="http://www.w3.org/2000/svg"
  1011. viewBox="0 0 16 16"
  1012. fill="currentColor"
  1013. class="w-4 h-4"
  1014. >
  1015. <path
  1016. fill-rule="evenodd"
  1017. d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
  1018. clip-rule="evenodd"
  1019. />
  1020. </svg>
  1021. </button>
  1022. </div>
  1023. </div>
  1024. {/if}
  1025. </div>
  1026. </div>
  1027. </div>
  1028. </div>
  1029. </div>