Models.svelte 31 KB

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