Models.svelte 28 KB

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