Models.svelte 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352
  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?.description ?? '';
  67. modelIsVisionCapable = model.custom_info?.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. const modelSource =
  444. 'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
  445. // Remove any existing config
  446. modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
  447. // Add new config
  448. modelConfig[modelSource].push({
  449. id: selectedModelId,
  450. name: modelName,
  451. description: modelDescription,
  452. vision_capable: modelIsVisionCapable
  453. });
  454. await updateModelConfig(localStorage.token, modelConfig);
  455. toast.success(
  456. $i18n.t('Model info for {{modelName}} added successfully', { modelName: selectedModelId })
  457. );
  458. models.set(await getModels());
  459. };
  460. const deleteModelInfoHandler = async () => {
  461. if (!selectedModelId) {
  462. return;
  463. }
  464. let model = $models.find((m) => m.id === selectedModelId);
  465. if (!model) {
  466. return;
  467. }
  468. const modelSource =
  469. 'details' in model ? 'ollama' : model.source === 'LiteLLM' ? 'litellm' : 'openai';
  470. modelConfig[modelSource] = modelConfig[modelSource].filter((m) => m.id !== selectedModelId);
  471. await updateModelConfig(localStorage.token, modelConfig);
  472. toast.success(
  473. $i18n.t('Model info for {{modelName}} deleted successfully', { modelName: selectedModelId })
  474. );
  475. models.set(await getModels());
  476. };
  477. const toggleIsVisionCapable = () => {
  478. modelIsVisionCapable = !modelIsVisionCapable;
  479. };
  480. onMount(async () => {
  481. OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
  482. toast.error(error);
  483. return [];
  484. });
  485. if (OLLAMA_URLS.length > 0) {
  486. selectedOllamaUrlIdx = 0;
  487. }
  488. liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
  489. modelConfig = await getModelConfig(localStorage.token);
  490. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
  491. });
  492. </script>
  493. <div class="flex flex-col h-full justify-between text-sm">
  494. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
  495. {#if ollamaVersion}
  496. <div class="space-y-2 pr-1.5">
  497. <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
  498. {#if OLLAMA_URLS.length > 0}
  499. <div class="flex gap-2">
  500. <div class="flex-1 pb-1">
  501. <select
  502. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  503. bind:value={selectedOllamaUrlIdx}
  504. placeholder={$i18n.t('Select an Ollama instance')}
  505. >
  506. {#each OLLAMA_URLS as url, idx}
  507. <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
  508. {/each}
  509. </select>
  510. </div>
  511. <div>
  512. <div class="flex w-full justify-end">
  513. <Tooltip content="Update All Models" placement="top">
  514. <button
  515. 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"
  516. on:click={() => {
  517. updateModelsHandler();
  518. }}
  519. >
  520. <svg
  521. xmlns="http://www.w3.org/2000/svg"
  522. viewBox="0 0 16 16"
  523. fill="currentColor"
  524. class="w-4 h-4"
  525. >
  526. <path
  527. 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"
  528. />
  529. <path
  530. 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"
  531. />
  532. </svg>
  533. </button>
  534. </Tooltip>
  535. </div>
  536. </div>
  537. </div>
  538. {#if updateModelId}
  539. Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
  540. {/if}
  541. {/if}
  542. <div class="space-y-2">
  543. <div>
  544. <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
  545. <div class="flex w-full">
  546. <div class="flex-1 mr-2">
  547. <input
  548. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  549. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  550. modelTag: 'mistral:7b'
  551. })}
  552. bind:value={modelTag}
  553. />
  554. </div>
  555. <button
  556. 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"
  557. on:click={() => {
  558. pullModelHandler();
  559. }}
  560. disabled={modelTransferring}
  561. >
  562. {#if modelTransferring}
  563. <div class="self-center">
  564. <svg
  565. class=" w-4 h-4"
  566. viewBox="0 0 24 24"
  567. fill="currentColor"
  568. xmlns="http://www.w3.org/2000/svg"
  569. >
  570. <style>
  571. .spinner_ajPY {
  572. transform-origin: center;
  573. animation: spinner_AtaB 0.75s infinite linear;
  574. }
  575. @keyframes spinner_AtaB {
  576. 100% {
  577. transform: rotate(360deg);
  578. }
  579. }
  580. </style>
  581. <path
  582. 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"
  583. opacity=".25"
  584. />
  585. <path
  586. 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"
  587. class="spinner_ajPY"
  588. />
  589. </svg>
  590. </div>
  591. {:else}
  592. <svg
  593. xmlns="http://www.w3.org/2000/svg"
  594. viewBox="0 0 16 16"
  595. fill="currentColor"
  596. class="w-4 h-4"
  597. >
  598. <path
  599. 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"
  600. />
  601. <path
  602. 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"
  603. />
  604. </svg>
  605. {/if}
  606. </button>
  607. </div>
  608. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  609. {$i18n.t('To access the available model names for downloading,')}
  610. <a
  611. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  612. href="https://ollama.com/library"
  613. target="_blank">{$i18n.t('click here.')}</a
  614. >
  615. </div>
  616. {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
  617. {#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
  618. {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
  619. <div class="flex flex-col">
  620. <div class="font-medium mb-1">{model}</div>
  621. <div class="">
  622. <div class="flex flex-row justify-between space-x-4 pr-2">
  623. <div class=" flex-1">
  624. <div
  625. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  626. style="width: {Math.max(
  627. 15,
  628. $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
  629. )}%"
  630. >
  631. {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
  632. </div>
  633. </div>
  634. <Tooltip content={$i18n.t('Cancel')}>
  635. <button
  636. class="text-gray-800 dark:text-gray-100"
  637. on:click={() => {
  638. cancelModelPullHandler(model);
  639. }}
  640. >
  641. <svg
  642. class="w-4 h-4 text-gray-800 dark:text-white"
  643. aria-hidden="true"
  644. xmlns="http://www.w3.org/2000/svg"
  645. width="24"
  646. height="24"
  647. fill="currentColor"
  648. viewBox="0 0 24 24"
  649. >
  650. <path
  651. stroke="currentColor"
  652. stroke-linecap="round"
  653. stroke-linejoin="round"
  654. stroke-width="2"
  655. d="M6 18 17.94 6M18 18 6.06 6"
  656. />
  657. </svg>
  658. </button>
  659. </Tooltip>
  660. </div>
  661. {#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
  662. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  663. {$MODEL_DOWNLOAD_POOL[model].digest}
  664. </div>
  665. {/if}
  666. </div>
  667. </div>
  668. {/if}
  669. {/each}
  670. {/if}
  671. </div>
  672. <div>
  673. <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  674. <div class="flex w-full">
  675. <div class="flex-1 mr-2">
  676. <select
  677. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  678. bind:value={deleteModelTag}
  679. placeholder={$i18n.t('Select a model')}
  680. >
  681. {#if !deleteModelTag}
  682. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  683. {/if}
  684. {#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
  685. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  686. >{(model.custom_info?.name ?? model.name) +
  687. ' (' +
  688. (model.size / 1024 ** 3).toFixed(1) +
  689. ' GB)'}</option
  690. >
  691. {/each}
  692. </select>
  693. </div>
  694. <button
  695. 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"
  696. on:click={() => {
  697. deleteModelHandler();
  698. }}
  699. >
  700. <svg
  701. xmlns="http://www.w3.org/2000/svg"
  702. viewBox="0 0 16 16"
  703. fill="currentColor"
  704. class="w-4 h-4"
  705. >
  706. <path
  707. fill-rule="evenodd"
  708. 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"
  709. clip-rule="evenodd"
  710. />
  711. </svg>
  712. </button>
  713. </div>
  714. </div>
  715. <div class="pt-1">
  716. <div class="flex justify-between items-center text-xs">
  717. <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
  718. <button
  719. class=" text-xs font-medium text-gray-500"
  720. type="button"
  721. on:click={() => {
  722. showExperimentalOllama = !showExperimentalOllama;
  723. }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
  724. >
  725. </div>
  726. </div>
  727. {#if showExperimentalOllama}
  728. <form
  729. on:submit|preventDefault={() => {
  730. uploadModelHandler();
  731. }}
  732. >
  733. <div class=" mb-2 flex w-full justify-between">
  734. <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
  735. <button
  736. class="p-1 px-3 text-xs flex rounded transition"
  737. on:click={() => {
  738. if (modelUploadMode === 'file') {
  739. modelUploadMode = 'url';
  740. } else {
  741. modelUploadMode = 'file';
  742. }
  743. }}
  744. type="button"
  745. >
  746. {#if modelUploadMode === 'file'}
  747. <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
  748. {:else}
  749. <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
  750. {/if}
  751. </button>
  752. </div>
  753. <div class="flex w-full mb-1.5">
  754. <div class="flex flex-col w-full">
  755. {#if modelUploadMode === 'file'}
  756. <div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
  757. <input
  758. id="model-upload-input"
  759. bind:this={modelUploadInputElement}
  760. type="file"
  761. bind:files={modelInputFile}
  762. on:change={() => {
  763. console.log(modelInputFile);
  764. }}
  765. accept=".gguf,.safetensors"
  766. required
  767. hidden
  768. />
  769. <button
  770. type="button"
  771. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
  772. on:click={() => {
  773. modelUploadInputElement.click();
  774. }}
  775. >
  776. {#if modelInputFile && modelInputFile.length > 0}
  777. {modelInputFile[0].name}
  778. {:else}
  779. {$i18n.t('Click here to select')}
  780. {/if}
  781. </button>
  782. </div>
  783. {:else}
  784. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  785. <input
  786. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
  787. ''
  788. ? 'mr-2'
  789. : ''}"
  790. type="url"
  791. required
  792. bind:value={modelFileUrl}
  793. placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
  794. />
  795. </div>
  796. {/if}
  797. </div>
  798. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  799. <button
  800. 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"
  801. type="submit"
  802. disabled={modelTransferring}
  803. >
  804. {#if modelTransferring}
  805. <div class="self-center">
  806. <svg
  807. class=" w-4 h-4"
  808. viewBox="0 0 24 24"
  809. fill="currentColor"
  810. xmlns="http://www.w3.org/2000/svg"
  811. >
  812. <style>
  813. .spinner_ajPY {
  814. transform-origin: center;
  815. animation: spinner_AtaB 0.75s infinite linear;
  816. }
  817. @keyframes spinner_AtaB {
  818. 100% {
  819. transform: rotate(360deg);
  820. }
  821. }
  822. </style>
  823. <path
  824. 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"
  825. opacity=".25"
  826. />
  827. <path
  828. 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"
  829. class="spinner_ajPY"
  830. />
  831. </svg>
  832. </div>
  833. {:else}
  834. <svg
  835. xmlns="http://www.w3.org/2000/svg"
  836. viewBox="0 0 16 16"
  837. fill="currentColor"
  838. class="w-4 h-4"
  839. >
  840. <path
  841. 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"
  842. />
  843. <path
  844. 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"
  845. />
  846. </svg>
  847. {/if}
  848. </button>
  849. {/if}
  850. </div>
  851. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  852. <div>
  853. <div>
  854. <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
  855. <textarea
  856. bind:value={modelFileContent}
  857. 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"
  858. rows="6"
  859. />
  860. </div>
  861. </div>
  862. {/if}
  863. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  864. {$i18n.t('To access the GGUF models available for downloading,')}
  865. <a
  866. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  867. href="https://huggingface.co/models?search=gguf"
  868. target="_blank">{$i18n.t('click here.')}</a
  869. >
  870. </div>
  871. {#if uploadMessage}
  872. <div class="mt-2">
  873. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  874. <div class="w-full rounded-full dark:bg-gray-800">
  875. <div
  876. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  877. style="width: 100%"
  878. >
  879. {uploadMessage}
  880. </div>
  881. </div>
  882. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  883. {modelFileDigest}
  884. </div>
  885. </div>
  886. {:else if uploadProgress !== null}
  887. <div class="mt-2">
  888. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  889. <div class="w-full rounded-full dark:bg-gray-800">
  890. <div
  891. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  892. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  893. >
  894. {uploadProgress ?? 0}%
  895. </div>
  896. </div>
  897. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  898. {modelFileDigest}
  899. </div>
  900. </div>
  901. {/if}
  902. </form>
  903. {/if}
  904. </div>
  905. </div>
  906. <hr class=" dark:border-gray-700 my-2" />
  907. {/if}
  908. <!--TODO: Hide LiteLLM options when ENABLE_LITELLM=false-->
  909. <div class=" space-y-3">
  910. <div class="mt-2 space-y-3 pr-1.5">
  911. <div>
  912. <div class="mb-2">
  913. <div class="flex justify-between items-center text-xs">
  914. <div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div>
  915. <button
  916. class=" text-xs font-medium text-gray-500"
  917. type="button"
  918. on:click={() => {
  919. showLiteLLM = !showLiteLLM;
  920. }}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button
  921. >
  922. </div>
  923. </div>
  924. {#if showLiteLLM}
  925. <div>
  926. <div class="flex justify-between items-center text-xs">
  927. <div class=" text-sm font-medium">{$i18n.t('Add a model')}</div>
  928. <button
  929. class=" text-xs font-medium text-gray-500"
  930. type="button"
  931. on:click={() => {
  932. showLiteLLMParams = !showLiteLLMParams;
  933. }}
  934. >{showLiteLLMParams
  935. ? $i18n.t('Hide Additional Params')
  936. : $i18n.t('Show Additional Params')}</button
  937. >
  938. </div>
  939. </div>
  940. <div class="my-2 space-y-2">
  941. <div class="flex w-full mb-1.5">
  942. <div class="flex-1 mr-2">
  943. <input
  944. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  945. placeholder={$i18n.t('Enter LiteLLM Model (litellm_params.model)')}
  946. bind:value={liteLLMModel}
  947. autocomplete="off"
  948. />
  949. </div>
  950. <button
  951. 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"
  952. on:click={() => {
  953. addLiteLLMModelHandler();
  954. }}
  955. >
  956. <svg
  957. xmlns="http://www.w3.org/2000/svg"
  958. viewBox="0 0 16 16"
  959. fill="currentColor"
  960. class="w-4 h-4"
  961. >
  962. <path
  963. 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"
  964. />
  965. </svg>
  966. </button>
  967. </div>
  968. {#if showLiteLLMParams}
  969. <div>
  970. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div>
  971. <div class="flex w-full">
  972. <div class="flex-1">
  973. <input
  974. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  975. placeholder="Enter Model Name (model_name)"
  976. bind:value={liteLLMModelName}
  977. autocomplete="off"
  978. />
  979. </div>
  980. </div>
  981. </div>
  982. <div>
  983. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
  984. <div class="flex w-full">
  985. <div class="flex-1">
  986. <input
  987. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  988. placeholder={$i18n.t(
  989. 'Enter LiteLLM API Base URL (litellm_params.api_base)'
  990. )}
  991. bind:value={liteLLMAPIBase}
  992. autocomplete="off"
  993. />
  994. </div>
  995. </div>
  996. </div>
  997. <div>
  998. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div>
  999. <div class="flex w-full">
  1000. <div class="flex-1">
  1001. <input
  1002. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1003. placeholder={$i18n.t('Enter LiteLLM API Key (litellm_params.api_key)')}
  1004. bind:value={liteLLMAPIKey}
  1005. autocomplete="off"
  1006. />
  1007. </div>
  1008. </div>
  1009. </div>
  1010. <div>
  1011. <div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div>
  1012. <div class="flex w-full">
  1013. <div class="flex-1">
  1014. <input
  1015. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1016. placeholder={$i18n.t('Enter LiteLLM API RPM (litellm_params.rpm)')}
  1017. bind:value={liteLLMRPM}
  1018. autocomplete="off"
  1019. />
  1020. </div>
  1021. </div>
  1022. </div>
  1023. <div>
  1024. <div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div>
  1025. <div class="flex w-full">
  1026. <div class="flex-1">
  1027. <input
  1028. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1029. placeholder={$i18n.t('Enter Max Tokens (litellm_params.max_tokens)')}
  1030. bind:value={liteLLMMaxTokens}
  1031. type="number"
  1032. min="1"
  1033. autocomplete="off"
  1034. />
  1035. </div>
  1036. </div>
  1037. </div>
  1038. {/if}
  1039. </div>
  1040. <div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
  1041. {$i18n.t('Not sure what to add?')}
  1042. <a
  1043. class=" text-gray-300 font-medium underline"
  1044. href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
  1045. target="_blank"
  1046. >
  1047. {$i18n.t('Click here for help.')}
  1048. </a>
  1049. </div>
  1050. <div>
  1051. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  1052. <div class="flex w-full">
  1053. <div class="flex-1 mr-2">
  1054. <select
  1055. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1056. bind:value={deleteLiteLLMModelName}
  1057. placeholder={$i18n.t('Select a model')}
  1058. >
  1059. {#if !deleteLiteLLMModelName}
  1060. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  1061. {/if}
  1062. {#each liteLLMModelInfo as model}
  1063. <option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
  1064. >{model.model_name}</option
  1065. >
  1066. {/each}
  1067. </select>
  1068. </div>
  1069. <button
  1070. 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"
  1071. on:click={() => {
  1072. deleteLiteLLMModelHandler();
  1073. }}
  1074. >
  1075. <svg
  1076. xmlns="http://www.w3.org/2000/svg"
  1077. viewBox="0 0 16 16"
  1078. fill="currentColor"
  1079. class="w-4 h-4"
  1080. >
  1081. <path
  1082. fill-rule="evenodd"
  1083. 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"
  1084. clip-rule="evenodd"
  1085. />
  1086. </svg>
  1087. </button>
  1088. </div>
  1089. </div>
  1090. {/if}
  1091. </div>
  1092. </div>
  1093. <hr class=" dark:border-gray-700 my-2" />
  1094. </div>
  1095. <div class=" space-y-3">
  1096. <div class="mt-2 space-y-3 pr-1.5">
  1097. <div>
  1098. <div class="mb-2">
  1099. <div class="flex justify-between items-center text-xs">
  1100. <div class=" text-sm font-medium">{$i18n.t('Manage Model Information')}</div>
  1101. <button
  1102. class=" text-xs font-medium text-gray-500"
  1103. type="button"
  1104. on:click={() => {
  1105. showModelInfo = !showModelInfo;
  1106. }}>{showModelInfo ? $i18n.t('Hide') : $i18n.t('Show')}</button
  1107. >
  1108. </div>
  1109. </div>
  1110. {#if showModelInfo}
  1111. <div>
  1112. <div class="flex justify-between items-center text-xs">
  1113. <div class=" text-sm font-medium">{$i18n.t('Current Models')}</div>
  1114. </div>
  1115. <div class="flex gap-2">
  1116. <div class="flex-1 pb-1">
  1117. <select
  1118. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1119. bind:value={selectedModelId}
  1120. on:change={onModelInfoIdChange}
  1121. >
  1122. {#if !selectedModelId}
  1123. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  1124. {/if}
  1125. {#each $models as model}
  1126. <option value={model.id} class="bg-gray-100 dark:bg-gray-700"
  1127. >{'details' in model
  1128. ? 'Ollama'
  1129. : model.source === 'LiteLLM'
  1130. ? 'LiteLLM'
  1131. : 'OpenAI'}: {model.name}{`${
  1132. model.custom_info?.name
  1133. ? ' - ' + model.custom_info?.name
  1134. : ''
  1135. }`}</option
  1136. >
  1137. {/each}
  1138. </select>
  1139. </div>
  1140. <button
  1141. 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"
  1142. on:click={() => {
  1143. deleteModelInfoHandler();
  1144. }}
  1145. >
  1146. <svg
  1147. xmlns="http://www.w3.org/2000/svg"
  1148. viewBox="0 0 16 16"
  1149. fill="currentColor"
  1150. class="w-4 h-4"
  1151. >
  1152. <path
  1153. fill-rule="evenodd"
  1154. 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"
  1155. clip-rule="evenodd"
  1156. />
  1157. </svg>
  1158. </button>
  1159. </div>
  1160. {#if selectedModelId}
  1161. <div>
  1162. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Display Name')}</div>
  1163. <div class="flex w-full mb-1.5">
  1164. <div class="flex-1 mr-2">
  1165. <input
  1166. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  1167. placeholder={$i18n.t('Enter Model Display Name')}
  1168. bind:value={modelName}
  1169. autocomplete="off"
  1170. />
  1171. </div>
  1172. <button
  1173. 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"
  1174. on:click={() => {
  1175. addModelInfoHandler();
  1176. }}
  1177. >
  1178. <svg
  1179. xmlns="http://www.w3.org/2000/svg"
  1180. viewBox="0 0 16 16"
  1181. fill="currentColor"
  1182. class="w-4 h-4"
  1183. >
  1184. <path
  1185. 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"
  1186. />
  1187. </svg>
  1188. </button>
  1189. </div>
  1190. </div>
  1191. <div>
  1192. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Description')}</div>
  1193. <div class="flex w-full">
  1194. <div class="flex-1">
  1195. <textarea
  1196. class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1"
  1197. rows="2"
  1198. bind:value={modelDescription}
  1199. />
  1200. </div>
  1201. </div>
  1202. </div>
  1203. <div class="py-0.5 flex w-full justify-between">
  1204. <div class=" self-center text-sm font-medium">
  1205. {$i18n.t('Is Model Vision Capable')}
  1206. </div>
  1207. <button
  1208. class="p-1 px-3sm flex rounded transition"
  1209. on:click={() => {
  1210. toggleIsVisionCapable();
  1211. }}
  1212. type="button"
  1213. >
  1214. {#if modelIsVisionCapable === true}
  1215. <span class="ml-2 self-center">{$i18n.t('Yes')}</span>
  1216. {:else}
  1217. <span class="ml-2 self-center">{$i18n.t('No')}</span>
  1218. {/if}
  1219. </button>
  1220. </div>
  1221. {/if}
  1222. </div>
  1223. {/if}
  1224. </div>
  1225. </div>
  1226. </div>
  1227. </div>
  1228. </div>