Models.svelte 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  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().replace(/^ollama\s+(run|pull)\s+/, '');
  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. } else {
  219. const error = await fileResponse?.json();
  220. toast.error(error?.detail ?? error);
  221. }
  222. if (uploaded) {
  223. const res = await createModel(
  224. localStorage.token,
  225. `${name}:latest`,
  226. `FROM @${modelFileDigest}\n${modelFileContent}`
  227. );
  228. if (res && res.ok) {
  229. const reader = res.body
  230. .pipeThrough(new TextDecoderStream())
  231. .pipeThrough(splitStream('\n'))
  232. .getReader();
  233. while (true) {
  234. const { value, done } = await reader.read();
  235. if (done) break;
  236. try {
  237. let lines = value.split('\n');
  238. for (const line of lines) {
  239. if (line !== '') {
  240. console.log(line);
  241. let data = JSON.parse(line);
  242. console.log(data);
  243. if (data.error) {
  244. throw data.error;
  245. }
  246. if (data.detail) {
  247. throw data.detail;
  248. }
  249. if (data.status) {
  250. if (
  251. !data.digest &&
  252. !data.status.includes('writing') &&
  253. !data.status.includes('sha256')
  254. ) {
  255. toast.success(data.status);
  256. } else {
  257. if (data.digest) {
  258. digest = data.digest;
  259. if (data.completed) {
  260. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  261. } else {
  262. pullProgress = 100;
  263. }
  264. }
  265. }
  266. }
  267. }
  268. }
  269. } catch (error) {
  270. console.log(error);
  271. toast.error(error);
  272. }
  273. }
  274. }
  275. }
  276. modelFileUrl = '';
  277. if (modelUploadInputElement) {
  278. modelUploadInputElement.value = '';
  279. }
  280. modelInputFile = null;
  281. modelTransferring = false;
  282. uploadProgress = null;
  283. models.set(await getModels());
  284. };
  285. const deleteModelHandler = async () => {
  286. const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
  287. (error) => {
  288. toast.error(error);
  289. }
  290. );
  291. if (res) {
  292. toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
  293. }
  294. deleteModelTag = '';
  295. models.set(await getModels());
  296. };
  297. const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
  298. const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
  299. (error) => {
  300. opts.callback({ success: false, error, modelName: opts.modelName });
  301. return null;
  302. }
  303. );
  304. if (res) {
  305. const reader = res.body
  306. .pipeThrough(new TextDecoderStream())
  307. .pipeThrough(splitStream('\n'))
  308. .getReader();
  309. while (true) {
  310. try {
  311. const { value, done } = await reader.read();
  312. if (done) break;
  313. let lines = value.split('\n');
  314. for (const line of lines) {
  315. if (line !== '') {
  316. let data = JSON.parse(line);
  317. console.log(data);
  318. if (data.error) {
  319. throw data.error;
  320. }
  321. if (data.detail) {
  322. throw data.detail;
  323. }
  324. if (data.id) {
  325. modelDownloadStatus[opts.modelName] = {
  326. ...modelDownloadStatus[opts.modelName],
  327. requestId: data.id,
  328. reader,
  329. done: false
  330. };
  331. console.log(data);
  332. }
  333. if (data.status) {
  334. if (data.digest) {
  335. let downloadProgress = 0;
  336. if (data.completed) {
  337. downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
  338. } else {
  339. downloadProgress = 100;
  340. }
  341. modelDownloadStatus[opts.modelName] = {
  342. ...modelDownloadStatus[opts.modelName],
  343. pullProgress: downloadProgress,
  344. digest: data.digest
  345. };
  346. } else {
  347. toast.success(data.status);
  348. modelDownloadStatus[opts.modelName] = {
  349. ...modelDownloadStatus[opts.modelName],
  350. done: data.status === 'success'
  351. };
  352. }
  353. }
  354. }
  355. }
  356. } catch (error) {
  357. console.log(error);
  358. if (typeof error !== 'string') {
  359. error = error.message;
  360. }
  361. opts.callback({ success: false, error, modelName: opts.modelName });
  362. }
  363. }
  364. console.log(modelDownloadStatus[opts.modelName]);
  365. if (modelDownloadStatus[opts.modelName].done) {
  366. opts.callback({ success: true, modelName: opts.modelName });
  367. } else {
  368. opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
  369. }
  370. }
  371. };
  372. const addLiteLLMModelHandler = async () => {
  373. if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
  374. const res = await addLiteLLMModel(localStorage.token, {
  375. name: liteLLMModelName,
  376. model: liteLLMModel,
  377. api_base: liteLLMAPIBase,
  378. api_key: liteLLMAPIKey,
  379. rpm: liteLLMRPM,
  380. max_tokens: liteLLMMaxTokens
  381. }).catch((error) => {
  382. toast.error(error);
  383. return null;
  384. });
  385. if (res) {
  386. if (res.message) {
  387. toast.success(res.message);
  388. }
  389. }
  390. } else {
  391. toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName }));
  392. }
  393. liteLLMModelName = '';
  394. liteLLMModel = '';
  395. liteLLMAPIBase = '';
  396. liteLLMAPIKey = '';
  397. liteLLMRPM = '';
  398. liteLLMMaxTokens = '';
  399. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  400. models.set(await getModels());
  401. };
  402. const deleteLiteLLMModelHandler = async () => {
  403. const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelId).catch(
  404. (error) => {
  405. toast.error(error);
  406. return null;
  407. }
  408. );
  409. if (res) {
  410. if (res.message) {
  411. toast.success(res.message);
  412. }
  413. }
  414. deleteLiteLLMModelId = '';
  415. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  416. models.set(await getModels());
  417. };
  418. onMount(async () => {
  419. OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
  420. toast.error(error);
  421. return [];
  422. });
  423. if (OLLAMA_URLS.length > 0) {
  424. selectedOllamaUrlIdx = 0;
  425. }
  426. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
  427. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  428. });
  429. const cancelModelPullHandler = async (model: string) => {
  430. const { reader, requestId } = modelDownloadStatus[model];
  431. if (reader) {
  432. await reader.cancel();
  433. await cancelOllamaRequest(localStorage.token, requestId);
  434. delete modelDownloadStatus[model];
  435. await deleteModel(localStorage.token, model);
  436. toast.success(`${model} download has been canceled`);
  437. }
  438. };
  439. </script>
  440. <div class="flex flex-col h-full justify-between text-sm">
  441. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
  442. {#if ollamaVersion}
  443. <div class="space-y-2 pr-1.5">
  444. <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
  445. {#if OLLAMA_URLS.length > 0}
  446. <div class="flex gap-2">
  447. <div class="flex-1 pb-1">
  448. <select
  449. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  450. bind:value={selectedOllamaUrlIdx}
  451. placeholder={$i18n.t('Select an Ollama instance')}
  452. >
  453. {#each OLLAMA_URLS as url, idx}
  454. <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
  455. {/each}
  456. </select>
  457. </div>
  458. <div>
  459. <div class="flex w-full justify-end">
  460. <Tooltip content="Update All Models" placement="top">
  461. <button
  462. 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"
  463. on:click={() => {
  464. updateModelsHandler();
  465. }}
  466. >
  467. <svg
  468. xmlns="http://www.w3.org/2000/svg"
  469. viewBox="0 0 16 16"
  470. fill="currentColor"
  471. class="w-4 h-4"
  472. >
  473. <path
  474. 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"
  475. />
  476. <path
  477. 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"
  478. />
  479. </svg>
  480. </button>
  481. </Tooltip>
  482. </div>
  483. </div>
  484. </div>
  485. {#if updateModelId}
  486. Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
  487. {/if}
  488. {/if}
  489. <div class="space-y-2">
  490. <div>
  491. <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
  492. <div class="flex w-full">
  493. <div class="flex-1 mr-2">
  494. <input
  495. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  496. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  497. modelTag: 'mistral:7b'
  498. })}
  499. bind:value={modelTag}
  500. />
  501. </div>
  502. <button
  503. 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"
  504. on:click={() => {
  505. pullModelHandler();
  506. }}
  507. disabled={modelTransferring}
  508. >
  509. {#if modelTransferring}
  510. <div class="self-center">
  511. <svg
  512. class=" w-4 h-4"
  513. viewBox="0 0 24 24"
  514. fill="currentColor"
  515. xmlns="http://www.w3.org/2000/svg"
  516. ><style>
  517. .spinner_ajPY {
  518. transform-origin: center;
  519. animation: spinner_AtaB 0.75s infinite linear;
  520. }
  521. @keyframes spinner_AtaB {
  522. 100% {
  523. transform: rotate(360deg);
  524. }
  525. }
  526. </style><path
  527. 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"
  528. opacity=".25"
  529. /><path
  530. 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"
  531. class="spinner_ajPY"
  532. /></svg
  533. >
  534. </div>
  535. {:else}
  536. <svg
  537. xmlns="http://www.w3.org/2000/svg"
  538. viewBox="0 0 16 16"
  539. fill="currentColor"
  540. class="w-4 h-4"
  541. >
  542. <path
  543. 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"
  544. />
  545. <path
  546. 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"
  547. />
  548. </svg>
  549. {/if}
  550. </button>
  551. </div>
  552. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  553. {$i18n.t('To access the available model names for downloading,')}
  554. <a
  555. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  556. href="https://ollama.com/library"
  557. target="_blank">{$i18n.t('click here.')}</a
  558. >
  559. </div>
  560. {#if Object.keys(modelDownloadStatus).length > 0}
  561. {#each Object.keys(modelDownloadStatus) as model}
  562. {#if 'pullProgress' in modelDownloadStatus[model]}
  563. <div class="flex flex-col">
  564. <div class="font-medium mb-1">{model}</div>
  565. <div class="">
  566. <div class="flex flex-row justify-between space-x-4 pr-2">
  567. <div class=" flex-1">
  568. <div
  569. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  570. style="width: {Math.max(
  571. 15,
  572. modelDownloadStatus[model].pullProgress ?? 0
  573. )}%"
  574. >
  575. {modelDownloadStatus[model].pullProgress ?? 0}%
  576. </div>
  577. </div>
  578. <Tooltip content="Cancel">
  579. <button
  580. class="text-gray-800 dark:text-gray-100"
  581. on:click={() => {
  582. cancelModelPullHandler(model);
  583. }}
  584. >
  585. <svg
  586. class="w-4 h-4 text-gray-800 dark:text-white"
  587. aria-hidden="true"
  588. xmlns="http://www.w3.org/2000/svg"
  589. width="24"
  590. height="24"
  591. fill="currentColor"
  592. viewBox="0 0 24 24"
  593. >
  594. <path
  595. stroke="currentColor"
  596. stroke-linecap="round"
  597. stroke-linejoin="round"
  598. stroke-width="2"
  599. d="M6 18 17.94 6M18 18 6.06 6"
  600. />
  601. </svg>
  602. </button>
  603. </Tooltip>
  604. </div>
  605. {#if 'digest' in modelDownloadStatus[model]}
  606. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  607. {modelDownloadStatus[model].digest}
  608. </div>
  609. {/if}
  610. </div>
  611. </div>
  612. {/if}
  613. {/each}
  614. {/if}
  615. </div>
  616. <div>
  617. <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  618. <div class="flex w-full">
  619. <div class="flex-1 mr-2">
  620. <select
  621. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  622. bind:value={deleteModelTag}
  623. placeholder={$i18n.t('Select a model')}
  624. >
  625. {#if !deleteModelTag}
  626. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  627. {/if}
  628. {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
  629. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  630. >{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
  631. >
  632. {/each}
  633. </select>
  634. </div>
  635. <button
  636. 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"
  637. on:click={() => {
  638. deleteModelHandler();
  639. }}
  640. >
  641. <svg
  642. xmlns="http://www.w3.org/2000/svg"
  643. viewBox="0 0 16 16"
  644. fill="currentColor"
  645. class="w-4 h-4"
  646. >
  647. <path
  648. fill-rule="evenodd"
  649. 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"
  650. clip-rule="evenodd"
  651. />
  652. </svg>
  653. </button>
  654. </div>
  655. </div>
  656. <div class="pt-1">
  657. <div class="flex justify-between items-center text-xs">
  658. <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
  659. <button
  660. class=" text-xs font-medium text-gray-500"
  661. type="button"
  662. on:click={() => {
  663. showExperimentalOllama = !showExperimentalOllama;
  664. }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
  665. >
  666. </div>
  667. </div>
  668. {#if showExperimentalOllama}
  669. <form
  670. on:submit|preventDefault={() => {
  671. uploadModelHandler();
  672. }}
  673. >
  674. <div class=" mb-2 flex w-full justify-between">
  675. <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
  676. <button
  677. class="p-1 px-3 text-xs flex rounded transition"
  678. on:click={() => {
  679. if (modelUploadMode === 'file') {
  680. modelUploadMode = 'url';
  681. } else {
  682. modelUploadMode = 'file';
  683. }
  684. }}
  685. type="button"
  686. >
  687. {#if modelUploadMode === 'file'}
  688. <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
  689. {:else}
  690. <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
  691. {/if}
  692. </button>
  693. </div>
  694. <div class="flex w-full mb-1.5">
  695. <div class="flex flex-col w-full">
  696. {#if modelUploadMode === 'file'}
  697. <div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
  698. <input
  699. id="model-upload-input"
  700. bind:this={modelUploadInputElement}
  701. type="file"
  702. bind:files={modelInputFile}
  703. on:change={() => {
  704. console.log(modelInputFile);
  705. }}
  706. accept=".gguf,.safetensors"
  707. required
  708. hidden
  709. />
  710. <button
  711. type="button"
  712. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
  713. on:click={() => {
  714. modelUploadInputElement.click();
  715. }}
  716. >
  717. {#if modelInputFile && modelInputFile.length > 0}
  718. {modelInputFile[0].name}
  719. {:else}
  720. {$i18n.t('Click here to select')}
  721. {/if}
  722. </button>
  723. </div>
  724. {:else}
  725. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  726. <input
  727. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
  728. ''
  729. ? 'mr-2'
  730. : ''}"
  731. type="url"
  732. required
  733. bind:value={modelFileUrl}
  734. placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
  735. />
  736. </div>
  737. {/if}
  738. </div>
  739. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  740. <button
  741. 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"
  742. type="submit"
  743. disabled={modelTransferring}
  744. >
  745. {#if modelTransferring}
  746. <div class="self-center">
  747. <svg
  748. class=" w-4 h-4"
  749. viewBox="0 0 24 24"
  750. fill="currentColor"
  751. xmlns="http://www.w3.org/2000/svg"
  752. ><style>
  753. .spinner_ajPY {
  754. transform-origin: center;
  755. animation: spinner_AtaB 0.75s infinite linear;
  756. }
  757. @keyframes spinner_AtaB {
  758. 100% {
  759. transform: rotate(360deg);
  760. }
  761. }
  762. </style><path
  763. 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"
  764. opacity=".25"
  765. /><path
  766. 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"
  767. class="spinner_ajPY"
  768. /></svg
  769. >
  770. </div>
  771. {:else}
  772. <svg
  773. xmlns="http://www.w3.org/2000/svg"
  774. viewBox="0 0 16 16"
  775. fill="currentColor"
  776. class="w-4 h-4"
  777. >
  778. <path
  779. 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"
  780. />
  781. <path
  782. 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"
  783. />
  784. </svg>
  785. {/if}
  786. </button>
  787. {/if}
  788. </div>
  789. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  790. <div>
  791. <div>
  792. <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
  793. <textarea
  794. bind:value={modelFileContent}
  795. 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"
  796. rows="6"
  797. />
  798. </div>
  799. </div>
  800. {/if}
  801. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  802. {$i18n.t('To access the GGUF models available for downloading,')}
  803. <a
  804. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  805. href="https://huggingface.co/models?search=gguf"
  806. target="_blank">{$i18n.t('click here.')}</a
  807. >
  808. </div>
  809. {#if uploadMessage}
  810. <div class="mt-2">
  811. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  812. <div class="w-full rounded-full dark:bg-gray-800">
  813. <div
  814. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  815. style="width: 100%"
  816. >
  817. {uploadMessage}
  818. </div>
  819. </div>
  820. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  821. {modelFileDigest}
  822. </div>
  823. </div>
  824. {:else if uploadProgress !== null}
  825. <div class="mt-2">
  826. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  827. <div class="w-full rounded-full dark:bg-gray-800">
  828. <div
  829. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  830. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  831. >
  832. {uploadProgress ?? 0}%
  833. </div>
  834. </div>
  835. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  836. {modelFileDigest}
  837. </div>
  838. </div>
  839. {/if}
  840. </form>
  841. {/if}
  842. </div>
  843. </div>
  844. <hr class=" dark:border-gray-700 my-2" />
  845. {/if}
  846. <div class=" space-y-3">
  847. <div class="mt-2 space-y-3 pr-1.5">
  848. <div>
  849. <div class="mb-2">
  850. <div class="flex justify-between items-center text-xs">
  851. <div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
  852. <button
  853. class=" text-xs font-medium text-gray-500"
  854. type="button"
  855. on:click={() => {
  856. showLiteLLM = !showLiteLLM;
  857. }}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
  858. >
  859. </div>
  860. </div>
  861. {#if showLiteLLM}
  862. <div>
  863. <div class="flex justify-between items-center text-xs">
  864. <div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
  865. <button
  866. class=" text-xs font-medium text-gray-500"
  867. type="button"
  868. on:click={() => {
  869. showLiteLLMParams = !showLiteLLMParams;
  870. }}
  871. >{showLiteLLMParams
  872. ? $i18n.t('Hide Additional Params')
  873. : $i18n.t('Show Additional Params')}</button
  874. >
  875. </div>
  876. </div>
  877. <div class="my-2 space-y-2">
  878. <div class="flex w-full mb-1.5">
  879. <div class="flex-1 mr-2">
  880. <input
  881. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  882. placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
  883. bind:value={liteLLMModel}
  884. autocomplete="off"
  885. />
  886. </div>
  887. <button
  888. 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"
  889. on:click={() => {
  890. addLiteLLMModelHandler();
  891. }}
  892. >
  893. <svg
  894. xmlns="http://www.w3.org/2000/svg"
  895. viewBox="0 0 16 16"
  896. fill="currentColor"
  897. class="w-4 h-4"
  898. >
  899. <path
  900. 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"
  901. />
  902. </svg>
  903. </button>
  904. </div>
  905. {#if showLiteLLMParams}
  906. <div>
  907. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
  908. <div class="flex w-full">
  909. <div class="flex-1">
  910. <input
  911. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  912. placeholder="Enter Model Name (model_name)"
  913. bind:value={liteLLMModelName}
  914. autocomplete="off"
  915. />
  916. </div>
  917. </div>
  918. </div>
  919. <div>
  920. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
  921. <div class="flex w-full">
  922. <div class="flex-1">
  923. <input
  924. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  925. placeholder={$i18n.t(
  926. 'Enter LiteLLM API Base URL (litellm_params.api_base)'
  927. )}
  928. bind:value={liteLLMAPIBase}
  929. autocomplete="off"
  930. />
  931. </div>
  932. </div>
  933. </div>
  934. <div>
  935. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
  936. <div class="flex w-full">
  937. <div class="flex-1">
  938. <input
  939. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  940. placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
  941. bind:value={liteLLMAPIKey}
  942. autocomplete="off"
  943. />
  944. </div>
  945. </div>
  946. </div>
  947. <div>
  948. <div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
  949. <div class="flex w-full">
  950. <div class="flex-1">
  951. <input
  952. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  953. placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
  954. bind:value={liteLLMRPM}
  955. autocomplete="off"
  956. />
  957. </div>
  958. </div>
  959. </div>
  960. <div>
  961. <div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
  962. <div class="flex w-full">
  963. <div class="flex-1">
  964. <input
  965. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  966. placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
  967. bind:value={liteLLMMaxTokens}
  968. type="number"
  969. min="1"
  970. autocomplete="off"
  971. />
  972. </div>
  973. </div>
  974. </div>
  975. {/if}
  976. </div>
  977. <div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
  978. {$i18n.t('Not sure what to add?')}
  979. <a
  980. class=" text-gray-300 font-medium underline"
  981. href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
  982. target="_blank"
  983. >
  984. {$i18n.t('Click here for help.')}
  985. </a>
  986. </div>
  987. <div>
  988. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  989. <div class="flex w-full">
  990. <div class="flex-1 mr-2">
  991. <select
  992. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  993. bind:value={deleteLiteLLMModelId}
  994. placeholder={$i18n.t('Select a model')}
  995. >
  996. {#if !deleteLiteLLMModelId}
  997. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  998. {/if}
  999. {#each liteLLMModelInfo as model}
  1000. <option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
  1001. >{model.model_name}</option
  1002. >
  1003. {/each}
  1004. </select>
  1005. </div>
  1006. <button
  1007. 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"
  1008. on:click={() => {
  1009. deleteLiteLLMModelHandler();
  1010. }}
  1011. >
  1012. <svg
  1013. xmlns="http://www.w3.org/2000/svg"
  1014. viewBox="0 0 16 16"
  1015. fill="currentColor"
  1016. class="w-4 h-4"
  1017. >
  1018. <path
  1019. fill-rule="evenodd"
  1020. 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"
  1021. clip-rule="evenodd"
  1022. />
  1023. </svg>
  1024. </button>
  1025. </div>
  1026. </div>
  1027. {/if}
  1028. </div>
  1029. </div>
  1030. </div>
  1031. </div>
  1032. </div>