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
29 changes: 22 additions & 7 deletions LocalLLMServerManager.Shared/Converters/StickerStudioConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Media.Imaging;
Expand All @@ -12,15 +13,19 @@ namespace LocalLLMServerManager.Shared.Views.Controls;
public class ImageSourceConverter : IValueConverter
{
public static readonly ImageSourceConverter Instance = new();
private static readonly ConditionalWeakTable<byte[], Bitmap> BitmapCache = new();

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
try
{
if (value is byte[] bytes && bytes.Length > 0)
{
using var ms = new MemoryStream(bytes);
return new Bitmap(ms);
return BitmapCache.GetValue(bytes, static b =>
{
using var ms = new MemoryStream(b);
return new Bitmap(ms);
});
}
if (value is string path && !string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
Expand Down Expand Up @@ -56,25 +61,35 @@ public class StageStatusBrushConverter : IValueConverter
{
public static readonly StageStatusBrushConverter Instance = new();

[ThreadStatic] private static SolidColorBrush? _completedBrush;
[ThreadStatic] private static SolidColorBrush? _runningBrush;
[ThreadStatic] private static SolidColorBrush? _errorBrush;
[ThreadStatic] private static SolidColorBrush? _idleBrush;

public static SolidColorBrush CompletedBrush => _completedBrush ??= new(Color.Parse("#238636"));
public static SolidColorBrush RunningBrush => _runningBrush ??= new(Color.Parse("#388bfd"));
public static SolidColorBrush ErrorBrush => _errorBrush ??= new(Color.Parse("#da3633"));
public static SolidColorBrush IdleBrush => _idleBrush ??= new(Color.Parse("#8b949e"));

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is StickerPipelineStage stage && parameter is string stepStr && int.TryParse(stepStr, out int step))
{
if (stage == StickerPipelineStage.Failed)
return new SolidColorBrush(Color.Parse("#da3633"));
return ErrorBrush;

int current = (int)stage;
if (stage != StickerPipelineStage.Idle)
{
if (stage == StickerPipelineStage.Ready)
return new SolidColorBrush(Color.Parse("#238636")); // Complete green
return CompletedBrush; // Complete green
if (current == step)
return new SolidColorBrush(Color.Parse("#388bfd")); // Active blue
return RunningBrush; // Active blue
if (current > step)
return new SolidColorBrush(Color.Parse("#238636")); // Complete green
return CompletedBrush; // Complete green
}
}
return new SolidColorBrush(Color.Parse("#8b949e")); // TextMutedBrush
return IdleBrush; // TextMutedBrush
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
Expand Down
6 changes: 5 additions & 1 deletion LocalLLMServerManager.Shared/Services/HttpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ namespace LocalLLMServerManager.Shared.Services;

public static class HttpHelper
{
private static readonly HttpMessageHandler SharedHandler = OperatingSystem.IsBrowser()
? new HttpClientHandler()
: new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) };

public static HttpClient CreateClient(string? apiBase = null)
{
var client = new HttpClient();
var client = new HttpClient(SharedHandler, disposeHandler: false);
if (!string.IsNullOrWhiteSpace(apiBase) && Uri.TryCreate(apiBase, UriKind.Absolute, out var baseUri))
{
client.BaseAddress = baseUri;
Expand Down
153 changes: 88 additions & 65 deletions LocalLLMServerManager.Shared/Services/OllamaModelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,55 @@ public async Task<List<OllamaModelItem>> LoadInstalledModelsAsync(string apiBase
var result = new List<OllamaModelItem>();
try
{
HttpResponseMessage response;
HttpResponseMessage? response = null;
try
{
response = await http.GetAsync($"{apiBase}/api/models");
if (!response.IsSuccessStatusCode && !OperatingSystem.IsBrowser())
try
{
response = await http.GetAsync($"{apiBase}/api/models");
if (!response.IsSuccessStatusCode && !OperatingSystem.IsBrowser())
{
response.Dispose();
response = await http.GetAsync("http://127.0.0.1:11434/api/tags");
}
}
catch
{
response?.Dispose();
if (OperatingSystem.IsBrowser()) return result;
response = await http.GetAsync("http://127.0.0.1:11434/api/tags");
}
}
catch
{
if (OperatingSystem.IsBrowser()) return result;
response = await http.GetAsync("http://127.0.0.1:11434/api/tags");
}

if (response.IsSuccessStatusCode)
{
var jsonStr = await response.Content.ReadAsStringAsync();
var doc = JsonNode.Parse(jsonStr);
var models = doc?["models"]?.AsArray();

if (models != null)
using var resp = response;
if (resp.IsSuccessStatusCode)
{
foreach (var m in models)
var jsonStr = await resp.Content.ReadAsStringAsync();
var doc = JsonNode.Parse(jsonStr);
var models = doc?["models"]?.AsArray();

if (models != null)
{
string name = m?["name"]?.ToString() ?? "Unknown";
long size = m?["size"]?.GetValue<long>() ?? 0L;
double sizeGb = Math.Round(size / (1024.0 * 1024.0 * 1024.0), 2);
string formatSize = sizeGb > 0 ? $"{sizeGb} GB" : "N/A";
foreach (var m in models)
{
string name = m?["name"]?.ToString() ?? "Unknown";
long size = m?["size"]?.GetValue<long>() ?? 0L;
double sizeGb = Math.Round(size / (1024.0 * 1024.0 * 1024.0), 2);
string formatSize = sizeGb > 0 ? $"{sizeGb} GB" : "N/A";

string cap = "💻 Coding & General";
string color = "#38BDF8";
if (name.Contains("math", StringComparison.OrdinalIgnoreCase)) { cap = "🧮 Mathematics"; color = "#C084FC"; }
else if (name.Contains("r1", StringComparison.OrdinalIgnoreCase) || name.Contains("deepseek", StringComparison.OrdinalIgnoreCase)) { cap = "🧠 Reasoning profile"; color = "#A855F7"; }
string cap = "💻 Coding & General";
string color = "#38BDF8";
if (name.Contains("math", StringComparison.OrdinalIgnoreCase)) { cap = "🧮 Mathematics"; color = "#C084FC"; }
else if (name.Contains("r1", StringComparison.OrdinalIgnoreCase) || name.Contains("deepseek", StringComparison.OrdinalIgnoreCase)) { cap = "🧠 Reasoning profile"; color = "#A855F7"; }

result.Add(new OllamaModelItem(name, formatSize, cap, color, false, null, size));
result.Add(new OllamaModelItem(name, formatSize, cap, color, false, null, size));
}
}
}
}
finally
{
response?.Dispose();
}
}
catch { }

Expand All @@ -65,39 +75,52 @@ public async Task<bool> UnloadAllVramAsync(string apiBase, HttpClient http)
{
try
{
HttpResponseMessage psResp;
HttpResponseMessage? psResp = null;
try
{
psResp = await http.GetAsync($"{apiBase}/api/ollama/ps");
if (!psResp.IsSuccessStatusCode) psResp = await http.GetAsync("http://127.0.0.1:11434/api/ps");
}
catch
{
psResp = await http.GetAsync("http://127.0.0.1:11434/api/ps");
}
try
{
psResp = await http.GetAsync($"{apiBase}/api/ollama/ps");
if (!psResp.IsSuccessStatusCode)
{
psResp.Dispose();
psResp = await http.GetAsync("http://127.0.0.1:11434/api/ps");
}
}
catch
{
psResp?.Dispose();
psResp = await http.GetAsync("http://127.0.0.1:11434/api/ps");
}

if (psResp.IsSuccessStatusCode)
{
var doc = JsonNode.Parse(await psResp.Content.ReadAsStringAsync());
var models = doc?["models"]?.AsArray();
if (models != null)
using var resp = psResp;
if (resp.IsSuccessStatusCode)
{
foreach (var m in models)
var doc = JsonNode.Parse(await resp.Content.ReadAsStringAsync());
var models = doc?["models"]?.AsArray();
if (models != null)
{
string name = m?["name"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(name))
foreach (var m in models)
{
var content = new StringContent(
JsonSerializer.Serialize(new { model = name, keep_alive = 0 }),
System.Text.Encoding.UTF8,
"application/json"
);
await http.PostAsync("http://127.0.0.1:11434/api/generate", content);
string name = m?["name"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(name))
{
var content = new StringContent(
JsonSerializer.Serialize(new { model = name, keep_alive = 0 }),
System.Text.Encoding.UTF8,
"application/json"
);
await http.PostAsync("http://127.0.0.1:11434/api/generate", content);
}
}
}
}
return true;
}
finally
{
psResp?.Dispose();
}
return true;
}
catch
{
Expand All @@ -109,12 +132,12 @@ public async Task<bool> PreloadModelAsync(string apiBase, string modelName, Http
{
try
{
var content = new StringContent(
using var content = new StringContent(
JsonSerializer.Serialize(new { model = modelName, keep_alive = -1 }),
System.Text.Encoding.UTF8,
"application/json"
);
var resp = await http.PostAsync("http://127.0.0.1:11434/api/generate", content);
using var resp = await http.PostAsync("http://127.0.0.1:11434/api/generate", content);
return resp.IsSuccessStatusCode;
}
catch
Expand All @@ -135,12 +158,12 @@ public async Task<bool> PullModelAsync(string modelName)
try
{
using var client = new HttpClient();
var content = new StringContent(
using var content = new StringContent(
JsonSerializer.Serialize(new { name = modelName, stream = false }),
System.Text.Encoding.UTF8,
"application/json"
);
var resp = await client.PostAsync("http://127.0.0.1:11434/api/pull", content);
using var resp = await client.PostAsync("http://127.0.0.1:11434/api/pull", content);
return resp.IsSuccessStatusCode;
}
catch
Expand All @@ -166,21 +189,21 @@ public async Task<bool> DeleteModelAsync(string apiBase, string modelName, HttpC
try
{
using var req = new HttpRequestMessage(HttpMethod.Delete, endpointUrl);
var resp = await http.SendAsync(req);
using var resp = await http.SendAsync(req);
if (resp.IsSuccessStatusCode) return true;
}
catch { }

// 2. Fallback to POST on proxy endpoint
try
{
var postContent = new StringContent(
using var postContent = new StringContent(
JsonSerializer.Serialize(new { model = modelName, name = modelName, type = "ollama" }),
System.Text.Encoding.UTF8,
"application/json"
);
using var postReq = new HttpRequestMessage(HttpMethod.Post, endpointUrl) { Content = postContent };
var postResp = await http.SendAsync(postReq);
using var postResp = await http.SendAsync(postReq);
if (postResp.IsSuccessStatusCode) return true;
}
catch { }
Expand All @@ -190,36 +213,36 @@ public async Task<bool> DeleteModelAsync(string apiBase, string modelName, HttpC
{
try
{
var unloadContent = new StringContent(
using var unloadContent = new StringContent(
JsonSerializer.Serialize(new { model = modelName, keep_alive = 0 }),
System.Text.Encoding.UTF8,
"application/json"
);
await http.PostAsync("http://127.0.0.1:11434/api/generate", unloadContent);
using var unloadResp = await http.PostAsync("http://127.0.0.1:11434/api/generate", unloadContent);
}
catch { }

try
{
var directContent = new StringContent(
using var directContent = new StringContent(
JsonSerializer.Serialize(new { model = modelName, name = modelName }),
System.Text.Encoding.UTF8,
"application/json"
);
using var directReq = new HttpRequestMessage(HttpMethod.Delete, "http://127.0.0.1:11434/api/delete") { Content = directContent };
var directResp = await http.SendAsync(directReq);
using var directResp = await http.SendAsync(directReq);
if (directResp.IsSuccessStatusCode) return true;

var altName = modelName.EndsWith(":latest", StringComparison.OrdinalIgnoreCase)
? modelName.Substring(0, modelName.Length - 7)
: $"{modelName}:latest";
var altContent = new StringContent(
using var altContent = new StringContent(
JsonSerializer.Serialize(new { model = altName, name = altName }),
System.Text.Encoding.UTF8,
"application/json"
);
using var altReq = new HttpRequestMessage(HttpMethod.Delete, "http://127.0.0.1:11434/api/delete") { Content = altContent };
var altResp = await http.SendAsync(altReq);
using var altResp = await http.SendAsync(altReq);
return altResp.IsSuccessStatusCode;
}
catch { }
Expand All @@ -246,14 +269,14 @@ public async Task<bool> DeleteLocalModelFileAsync(string apiBase, string filePat
? $"/api/models/delete?file_path={Uri.EscapeDataString(filePath)}&type=file"
: $"{baseAddress}/api/models/delete?file_path={Uri.EscapeDataString(filePath)}&type=file";

var content = new StringContent(
using var content = new StringContent(
JsonSerializer.Serialize(new { file_path = filePath, filePath, type = "file" }),
System.Text.Encoding.UTF8,
"application/json"
);

using var req = new HttpRequestMessage(HttpMethod.Delete, endpointUrl) { Content = content };
var resp = await http.SendAsync(req);
using var resp = await http.SendAsync(req);
if (resp.IsSuccessStatusCode) return true;

using var postContent = new StringContent(
Expand All @@ -262,7 +285,7 @@ public async Task<bool> DeleteLocalModelFileAsync(string apiBase, string filePat
"application/json"
);
using var postReq = new HttpRequestMessage(HttpMethod.Post, endpointUrl) { Content = postContent };
var postResp = await http.SendAsync(postReq);
using var postResp = await http.SendAsync(postReq);
return postResp.IsSuccessStatusCode;
}
catch
Expand Down
Loading
Loading