Models.svelte 41 KB

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