Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="avares://LocalLLMServerManager/Assets/app-icon.ico" ToolTipText="Local LLM Server Manager v4.0.0" IsVisible="True">
<TrayIcon Icon="avares://LocalLLMServerManager/Assets/app-icon.ico" ToolTipText="Local LLM Server Manager v4.1.0" IsVisible="True">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Open Native Dashboard" Click="OnOpenDashboardClick"/>
Expand Down
76 changes: 64 additions & 12 deletions Endpoints/EngineEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LocalLLMServerManager.Services;
using LocalLLMServerManager.Shared.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
Expand All @@ -9,15 +10,24 @@ public static class EngineEndpoints
{
public static void MapEngineEndpoints(this WebApplication app)
{
app.MapGet("/api/gpu/vram", (IGpuTelemetryProvider telemetryProvider) =>
app.MapGet("/api/gpu/vram", (IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService) =>
{
var (gpuName, vramBytes, usedVramBytes) = telemetryProvider.GetGpuInfo();
return Results.Ok(BuildMemoryTelemetryResponse(telemetryProvider, settingsService));
});

app.MapGet("/api/hardware/memory", (IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService) =>
{
return Results.Ok(BuildMemoryTelemetryResponse(telemetryProvider, settingsService));
});

app.MapPost("/api/memory/purge", async (VramOrchestrator orchestrator, IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService, CancellationToken ct) =>
{
var eviction = await orchestrator.PurgeAllCachesAsync(stopIdleEngines: true, ct);
return Results.Ok(new
{
GpuName = gpuName,
VramBytes = vramBytes,
UsedVramBytes = usedVramBytes,
VramGB = Math.Round((double)vramBytes / (1024 * 1024 * 1024), 2)
status = "purged",
eviction,
memory = BuildMemoryTelemetryResponse(telemetryProvider, settingsService)
});
});

Expand All @@ -43,6 +53,7 @@ public static void MapEngineEndpoints(this WebApplication app)
return Results.BadRequest(new { message = $"Invalid or missing ComfyUI executable path: {execPath}" });
}

orchestrator.RecordActivity("comfy");
await orchestrator.EnsureVramForComfyUiAsync();
var success = await engineManager.StartComfyUiAsync(execPath, logger);

Expand All @@ -60,7 +71,7 @@ public static void MapEngineEndpoints(this WebApplication app)
return Results.Ok(new { message = "ComfyUI Stopped" });
});

app.MapPost("/api/forge/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
app.MapPost("/api/forge/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
Expand All @@ -71,6 +82,7 @@ public static void MapEngineEndpoints(this WebApplication app)
return Results.BadRequest(new { message = $"Invalid or missing SD Forge executable path: {execPath}" });
}

orchestrator.RecordActivity("forge");
var success = await engineManager.StartForgeAsync(execPath, logger);
if (success)
{
Expand All @@ -88,11 +100,12 @@ public static void MapEngineEndpoints(this WebApplication app)

app.MapPost("/api/comfy/free", async (VramOrchestrator orchestrator) =>
{
orchestrator.RecordActivity("comfy");
await orchestrator.FreeComfyUiVramAsync();
return Results.Ok(new { message = "ComfyUI VRAM freed" });
});

app.MapPost("/api/audio/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
app.MapPost("/api/audio/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
Expand All @@ -106,6 +119,7 @@ public static void MapEngineEndpoints(this WebApplication app)
}
}

orchestrator.RecordActivity("audio");
var success = await engineManager.StartAudioEngineAsync(execPath, logger);
if (success)
{
Expand All @@ -123,12 +137,13 @@ public static void MapEngineEndpoints(this WebApplication app)

app.MapGet("/api/audio/voices", HandleGetVoicesAsync);

app.MapPost("/api/ollama/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
app.MapPost("/api/ollama/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
var execPath = AiEngineManager.ResolveOllamaExecutable(settings.OllamaExecutablePath);

orchestrator.RecordActivity("ollama");
var success = await engineManager.StartOllamaAsync(execPath, logger);
if (success)
{
Expand All @@ -144,8 +159,9 @@ public static void MapEngineEndpoints(this WebApplication app)
return Results.Ok(new { message = "Ollama Stopped" });
});

app.MapPost("/api/engine/start", async (EngineToggleRequest req, IAiEngineManager engineManager) =>
app.MapPost("/api/engine/start", async (EngineToggleRequest req, VramOrchestrator orchestrator, IAiEngineManager engineManager) =>
{
orchestrator.RecordActivity(req.Engine ?? "");
var result = await engineManager.StartEngineAsync(req.Engine ?? "");
if (result.Success)
{
Expand All @@ -165,6 +181,44 @@ public static void MapEngineEndpoints(this WebApplication app)
});
}

public static object BuildMemoryTelemetryResponse(IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService)
{
var (gpuName, vramBytes, usedVramBytes) = telemetryProvider.GetGpuInfo();
var sysMem = telemetryProvider.GetSystemMemoryInfo();
var settings = settingsService.LoadSettings();
double totalVramGb = Math.Round((double)vramBytes / (1024.0 * 1024.0 * 1024.0), 2);
double usedVramGb = Math.Round((double)usedVramBytes / (1024.0 * 1024.0 * 1024.0), 2);
double freeVramGb = Math.Max(0.0, Math.Round(totalVramGb - usedVramGb, 2));
int usedVramPct = vramBytes > 0 ? (int)Math.Clamp(Math.Round((double)usedVramBytes / vramBytes * 100.0), 0, 100) : 0;
double totalRamGb = Math.Round((double)sysMem.TotalRamBytes / (1024.0 * 1024.0 * 1024.0), 2);
double usedRamGb = Math.Round((double)sysMem.UsedRamBytes / (1024.0 * 1024.0 * 1024.0), 2);
double processWsMb = Math.Round((double)sysMem.ProcessWorkingSetBytes / (1024.0 * 1024.0), 1);
double nonPagedMb = Math.Round((double)sysMem.NonPagedPoolBytes / (1024.0 * 1024.0), 1);

return new
{
GpuName = gpuName,
VramBytes = vramBytes,
UsedVramBytes = usedVramBytes,
VramGB = totalVramGb,
TotalVramGb = totalVramGb,
UsedVramGb = usedVramGb,
FreeVramGb = freeVramGb,
UsedPercent = usedVramPct,
TotalRamBytes = sysMem.TotalRamBytes,
UsedRamBytes = sysMem.UsedRamBytes,
AvailableRamBytes = sysMem.AvailableRamBytes,
TotalRamGb = totalRamGb,
UsedRamGb = usedRamGb,
RamUsedPercent = sysMem.RamUsedPercent,
ProcessWorkingSetMb = processWsMb,
NonPagedPoolMb = nonPagedMb,
ModelCacheIdleTimeoutMinutes = settings.ModelCacheIdleTimeoutMinutes,
EngineHibernationTimeoutMinutes = settings.EngineHibernationTimeoutMinutes,
RamPressureThresholdPercent = settings.RamPressureThresholdPercent
};
}

public static async Task<IResult> HandleGetVoicesAsync(ISettingsService settingsService, System.Net.Http.IHttpClientFactory clientFactory)
{
var settings = settingsService.LoadSettings();
Expand Down Expand Up @@ -197,5 +251,3 @@ public static async Task<IResult> HandleGetVoicesAsync(ISettingsService settings
return Results.Ok(new { voices = defaultVoices, preferred = settings.PreferredAudioVoice });
}
}


2 changes: 1 addition & 1 deletion Endpoints/HealthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static void MapHealthEndpoints(this WebApplication app)
StableDiffusion = forgeHealthy ? "Online" : "Offline",
ComfyUI = comfyHealthy ? "Online" : "Offline",
PreferredImageEngine = settings.PreferredImageEngine,
Version = "4.0.0",
Version = "4.1.0",
});
});
}
Expand Down
10 changes: 9 additions & 1 deletion LocalLLMServerManager.Shared/Interfaces/ITelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ public record GpuTelemetryInfo(
double UsedVramGb,
double TotalVramGb,
double FreeVramGb,
int Percent
int Percent,
double UsedRamGb = 0.0,
double TotalRamGb = 32.0,
int RamPercent = 0,
double ProcessWorkingSetMb = 0.0,
double NonPagedPoolMb = 0.0,
int ModelCacheIdleTimeoutMinutes = 15,
int EngineHibernationTimeoutMinutes = 30,
int RamPressureThresholdPercent = 75
);

public interface ITelemetryService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>4.0.0</Version>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion>
<Version>4.1.0</Version>
<AssemblyVersion>4.1.0.0</AssemblyVersion>
<FileVersion>4.1.0.0</FileVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Expand Down
7 changes: 4 additions & 3 deletions LocalLLMServerManager.Shared/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public record AppSettings(
string AiAssistantApiKey = "",
string AiAssistantModel = "google/gemini-2.5-flash",
string AiAssistantCustomSystemPrompt = "",
string AiAssistantPromptsDirectory = ""
string AiAssistantPromptsDirectory = "",
int ModelCacheIdleTimeoutMinutes = 15,
int EngineHibernationTimeoutMinutes = 30,
int RamPressureThresholdPercent = 75
);


6 changes: 6 additions & 0 deletions LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ public class TelemetryData
public double TotalVramGb { get; set; } = 16.0;
public int VramPercent { get; set; }

public double UsedRamGb { get; set; } = 0.0;
public double TotalRamGb { get; set; } = 32.0;
public int RamPercent { get; set; } = 0;
public double ProcessWorkingSetMb { get; set; } = 0.0;
public double NonPagedPoolMb { get; set; } = 0.0;

public TelemetryData()
{
}
Expand Down
64 changes: 57 additions & 7 deletions LocalLLMServerManager.Shared/Services/TelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,68 @@ public async Task<GpuTelemetryInfo> QueryGpuVramAsync(string apiBase, HttpClient
if (resp.IsSuccessStatusCode)
{
var doc = JsonNode.Parse(await resp.Content.ReadAsStringAsync());
string gpuName = doc?["gpuName"]?.ToString() ?? "GPU Telemetry Active";
double used = doc?["usedVramGb"]?.GetValue<double>() ?? 0.0;
double total = doc?["totalVramGb"]?.GetValue<double>() ?? 16.0;
double free = doc?["freeVramGb"]?.GetValue<double>() ?? (total - used);
int pct = doc?["usedPercent"]?.GetValue<int>() ?? (int)Math.Round((used / (total > 0 ? total : 1.0)) * 100.0);
string gpuName = doc?["gpuName"]?.ToString()
?? doc?["GpuName"]?.ToString()
?? "GPU Telemetry Active";

return new GpuTelemetryInfo(gpuName, used, total, free, pct);
double totalVram = TryGetDouble(doc, "totalVramGb", "TotalVramGb")
?? TryGetDouble(doc, "vramGB", "VramGB")
?? (TryGetDouble(doc, "vramBytes", "VramBytes") is double vb ? Math.Round(vb / (1024.0 * 1024.0 * 1024.0), 2) : 16.0);

double usedVram = TryGetDouble(doc, "usedVramGb", "UsedVramGb")
?? (TryGetDouble(doc, "usedVramBytes", "UsedVramBytes") is double uvb ? Math.Round(uvb / (1024.0 * 1024.0 * 1024.0), 2) : 0.0);

double freeVram = TryGetDouble(doc, "freeVramGb", "FreeVramGb")
?? Math.Max(0.0, Math.Round(totalVram - usedVram, 2));

int pct = TryGetInt(doc, "usedPercent", "UsedPercent")
?? (int)Math.Round((usedVram / (totalVram > 0 ? totalVram : 1.0)) * 100.0);

double totalRam = TryGetDouble(doc, "totalRamGb", "TotalRamGb") ?? 32.0;
double usedRam = TryGetDouble(doc, "usedRamGb", "UsedRamGb") ?? 0.0;
int ramPct = TryGetInt(doc, "ramUsedPercent", "RamUsedPercent")
?? (int)Math.Round((usedRam / (totalRam > 0 ? totalRam : 1.0)) * 100.0);

double processWs = TryGetDouble(doc, "processWorkingSetMb", "ProcessWorkingSetMb") ?? 0.0;
double nonPaged = TryGetDouble(doc, "nonPagedPoolMb", "NonPagedPoolMb") ?? 0.0;
int modelCacheTimeout = TryGetInt(doc, "modelCacheIdleTimeoutMinutes", "ModelCacheIdleTimeoutMinutes") ?? 15;
int hibernationTimeout = TryGetInt(doc, "engineHibernationTimeoutMinutes", "EngineHibernationTimeoutMinutes") ?? 30;
int ramPressureThreshold = TryGetInt(doc, "ramPressureThresholdPercent", "RamPressureThresholdPercent") ?? 75;

return new GpuTelemetryInfo(
gpuName,
usedVram,
totalVram,
freeVram,
pct,
usedRam,
totalRam,
ramPct,
processWs,
nonPaged,
modelCacheTimeout,
hibernationTimeout,
ramPressureThreshold
);
}
}
catch { }

return new GpuTelemetryInfo("GPU Telemetry Active", 0.0, 16.0, 16.0, 0);
return new GpuTelemetryInfo("GPU Telemetry Active", 0.0, 16.0, 16.0, 0, 0.0, 32.0, 0, 0.0, 0.0, 15, 30, 75);
}

private static double? TryGetDouble(JsonNode? node, string camel, string pascal)
{
var target = node?[camel] ?? node?[pascal];
if (target == null) return null;
try { return target.GetValue<double>(); } catch { return null; }
}

private static int? TryGetInt(JsonNode? node, string camel, string pascal)
{
var target = node?[camel] ?? node?[pascal];
if (target == null) return null;
try { return target.GetValue<int>(); } catch { return null; }
}

public async Task<(bool Ollama, bool Forge, bool ComfyUi)> CheckServiceHealthAsync(string apiBase, string comfyUrl, HttpClient http)
Expand Down
1 change: 1 addition & 0 deletions LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<StreamGeometry x:Key="IconSticker">M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z</StreamGeometry>
<StreamGeometry x:Key="IconGpu">M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm3-3c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z</StreamGeometry>
<StreamGeometry x:Key="IconRefresh">M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z</StreamGeometry>
<StreamGeometry x:Key="IconPurge">M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z</StreamGeometry>
<StreamGeometry x:Key="IconFolder">M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z</StreamGeometry>
<StreamGeometry x:Key="IconCopy">M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z</StreamGeometry>
<StreamGeometry x:Key="IconSave">M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z</StreamGeometry>
Expand Down
Loading
Loading