Models.svelte 34 KB

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