Models.svelte 29 KB

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