diff --git a/extensions/inference-cortex-extension/src/index.ts b/extensions/inference-cortex-extension/src/index.ts index 62d3ef680..85c760b09 100644 --- a/extensions/inference-cortex-extension/src/index.ts +++ b/extensions/inference-cortex-extension/src/index.ts @@ -195,16 +195,12 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine { this.abortControllers.set(model.id, controller) - const loadedModels = await this.apiInstance() - .then((e) => e.get('inferences/server/models')) - .then((e) => e.json()) - .then((e) => (e as LoadedModelResponse).data ?? []) - .catch(() => []) + const loadedModels = await this.activeModels() console.log('Loaded models:', loadedModels) // This is to avoid loading the same model multiple times - if (loadedModels.some((e) => e.id === model.id)) { + if (loadedModels.some((e: { id: string }) => e.id === model.id)) { console.log(`Model ${model.id} already loaded`) return } @@ -216,8 +212,8 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine { ...extractModelLoadParams(model.settings), model: model.id, engine: - model.engine === "nitro" // Legacy model cache - ? "llama-cpp" + model.engine === 'nitro' // Legacy model cache + ? 'llama-cpp' : model.engine, cont_batching: this.cont_batching, n_parallel: this.n_parallel, @@ -253,6 +249,14 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine { ) } + async activeModels(): Promise<(object & { id: string })[]> { + return await this.apiInstance() + .then((e) => e.get('inferences/server/models')) + .then((e) => e.json()) + .then((e) => (e as LoadedModelResponse).data ?? []) + .catch(() => []) + } + /** * Clean cortex processes * @returns diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index ba3716d25..68d914a6a 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -1,6 +1,6 @@ use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf, io}; +use std::{fs, io, path::PathBuf}; use tauri::{AppHandle, Manager, Runtime, State}; use tauri_plugin_updater::UpdaterExt; @@ -273,9 +273,15 @@ pub fn get_active_extensions(app: AppHandle) -> Vec { }) }) .collect(), - Err(_) => vec![], + Err(error) => { + log::error!("Failed to parse extensions.json: {}", error); + vec![] + } + }, + Err(error) => { + log::error!("Failed to read extensions.json: {}", error); + vec![] }, - Err(_) => vec![], }; return contents; } @@ -315,13 +321,13 @@ pub fn change_app_data_folder( // Get current data folder path let current_data_folder = get_jan_data_folder_path(app_handle.clone()); let new_data_folder_path = PathBuf::from(&new_data_folder); - + // Create the new data folder if it doesn't exist if !new_data_folder_path.exists() { fs::create_dir_all(&new_data_folder_path) .map_err(|e| format!("Failed to create new data folder: {}", e))?; } - + // Copy all files from the old folder to the new one if current_data_folder.exists() { log::info!( @@ -329,17 +335,17 @@ pub fn change_app_data_folder( current_data_folder, new_data_folder_path ); - + copy_dir_recursive(¤t_data_folder, &new_data_folder_path) .map_err(|e| format!("Failed to copy data to new folder: {}", e))?; } else { log::info!("Current data folder does not exist, nothing to copy"); } - + // Update the configuration to point to the new folder let mut configuration = get_app_configurations(app_handle.clone()); configuration.data_folder = new_data_folder; - + // Save the updated configuration update_app_configuration(app_handle, configuration) } diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index d8c5f10d1..0a94e8b8a 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -146,3 +146,29 @@ export function formatMegaBytes(mb: number) { export function isDev() { return window.location.host.startsWith('localhost:') } + +export function formatDuration(startTime: number, endTime?: number): string { + const end = endTime || Date.now(); + const durationMs = end - startTime; + + if (durationMs < 0) { + return "Invalid duration (start time is in the future)"; + } + + const seconds = Math.floor(durationMs / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`; + } else if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else if (seconds > 0) { + return `${seconds}s`; + } else { + return `${durationMs}ms`; + } +} diff --git a/web-app/src/routes/settings/https-proxy.tsx b/web-app/src/routes/settings/https-proxy.tsx index d8bc06349..2d8f1b3ba 100644 --- a/web-app/src/routes/settings/https-proxy.tsx +++ b/web-app/src/routes/settings/https-proxy.tsx @@ -60,7 +60,6 @@ function HTTPSProxy() { }, [ noProxy, - proxyEnabled, proxyIgnoreSSL, proxyPassword, proxyUrl, diff --git a/web-app/src/routes/system-monitor.tsx b/web-app/src/routes/system-monitor.tsx index a551f9aea..4ea82d035 100644 --- a/web-app/src/routes/system-monitor.tsx +++ b/web-app/src/routes/system-monitor.tsx @@ -1,12 +1,14 @@ import { createFileRoute } from '@tanstack/react-router' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useHardware } from '@/hooks/useHardware' import { getHardwareInfo } from '@/services/hardware' import { Progress } from '@/components/ui/progress' import type { HardwareData } from '@/hooks/useHardware' import { route } from '@/constants/routes' -import { formatMegaBytes } from '@/lib/utils' +import { formatDuration, formatMegaBytes } from '@/lib/utils' import { IconDeviceDesktopAnalytics } from '@tabler/icons-react' +import { getActiveModels } from '@/services/models' +import { ActiveModel } from '@/types/models' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.systemMonitor as any)({ @@ -16,24 +18,27 @@ export const Route = createFileRoute(route.systemMonitor as any)({ function SystemMonitor() { const { hardwareData, setHardwareData, updateCPUUsage, updateRAMAvailable } = useHardware() + const [activeModels, setActiveModels] = useState([]) useEffect(() => { // Initial data fetch getHardwareInfo().then((data) => { setHardwareData(data as unknown as HardwareData) }) + getActiveModels().then(setActiveModels) // Set up interval for real-time updates const intervalId = setInterval(() => { getHardwareInfo().then((data) => { setHardwareData(data as unknown as HardwareData) - updateCPUUsage(data.cpu.usage) - updateRAMAvailable(data.ram.available) + updateCPUUsage(data.cpu?.usage) + updateRAMAvailable(data.ram?.available) }) + getActiveModels().then(setActiveModels) }, 5000) return () => clearInterval(intervalId) - }, [setHardwareData, updateCPUUsage, updateRAMAvailable]) + }, [setHardwareData, setActiveModels, updateCPUUsage, updateRAMAvailable]) // Calculate RAM usage percentage const ramUsagePercentage = @@ -125,31 +130,46 @@ function SystemMonitor() { {/* Current Active Model Section */}

- Current Active Model + Running Models

-
-
- GPT-4o + {activeModels.length === 0 && ( +
+ No models are currently running
-
-
- Provider - OpenAI -
-
- Context Length - 128K tokens -
-
- Status - -
- Running + )} + {activeModels.length > 0 && ( +
+ {activeModels.map((model) => ( +
+
+ + {model.id} +
- -
+
+
+ Provider + llama.cpp +
+
+ Uptime + + {formatDuration(model.start_time)} + +
+
+ Status + +
+ Running +
+
+
+
+
+ ))}
-
+ )}
{/* Active GPUs Section */} diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 7496f3926..19550ede9 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -1,5 +1,5 @@ import { ExtensionManager } from '@/lib/extension' -import { ExtensionTypeEnum, ModelExtension } from '@janhq/core' +import { EngineManager, ExtensionTypeEnum, ModelExtension } from '@janhq/core' import { Model as CoreModel } from '@janhq/core' /** @@ -204,6 +204,28 @@ export const importModel = async ( } } +/** + * Gets the active models for a given provider. + * @param provider + * @returns + */ +export const getActiveModels = async (provider?: string) => { + const providerName = provider || 'cortex' // we will go down to llama.cpp extension later on + const extension = EngineManager.instance().get(providerName) + + if (!extension) throw new Error('Model extension not found') + + try { + return 'activeModels' in extension && + typeof extension.activeModels === 'function' + ? ((await extension.activeModels()) ?? []) + : [] + } catch (error) { + console.error('Failed to get active models:', error) + return [] + } +} + /** * Configures the proxy options for model downloads. * @param param0 diff --git a/web-app/src/types/models.ts b/web-app/src/types/models.ts index d48a40021..1ea1e865b 100644 --- a/web-app/src/types/models.ts +++ b/web-app/src/types/models.ts @@ -4,13 +4,23 @@ * @enum {string} */ export enum ModelCapabilities { - COMPLETION = 'completion', - TOOLS = 'tools', - EMBEDDINGS = 'embeddings', - IMAGE_GENERATION = 'image_generation', - AUDIO_GENERATION = 'audio_generation', - TEXT_TO_IMAGE = 'text_to_image', - IMAGE_TO_IMAGE = 'image_to_image', - TEXT_TO_AUDIO = 'text_to_audio', - AUDIO_TO_TEXT = 'audio_to_text', -} \ No newline at end of file + COMPLETION = 'completion', + TOOLS = 'tools', + EMBEDDINGS = 'embeddings', + IMAGE_GENERATION = 'image_generation', + AUDIO_GENERATION = 'audio_generation', + TEXT_TO_IMAGE = 'text_to_image', + IMAGE_TO_IMAGE = 'image_to_image', + TEXT_TO_AUDIO = 'text_to_audio', + AUDIO_TO_TEXT = 'audio_to_text', +} + +export type ActiveModel = { + engine: string + id: string + model_size: number + object: 'model' + ram: number + start_time: number + vram: number +}