Models.svelte 28 KB

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