V14 / fad4e1a110e5
LMStudioSetup.cs
Lilith/Core/LMStudio/LMStudioSetup.cs
Unchanged from the preceding versionCS
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Text.Json;
namespace Lilith.Core.LMStudio;
public static class LMStudioSetup
{
public static string BaseUrl => (Environment.GetEnvironmentVariable("LILITH_LMSTUDIO_URL") ?? "http://127.0.0.1:1234").TrimEnd('/');
public static void Authorize(HttpRequestMessage request)
{
string? token = Environment.GetEnvironmentVariable("LMSTUDIO_API_TOKEN");
if (!string.IsNullOrWhiteSpace(token)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
public static string? GuiPath => new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "LM Studio", "LM Studio.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "LM Studio", "LM Studio.exe")
}.FirstOrDefault(File.Exists);
private static string? CliPath
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string pointer = Path.Combine(home, ".lmstudio-home-pointer");
string studioHome = File.Exists(pointer) ? File.ReadAllText(pointer).Trim() : Path.Combine(home, ".lmstudio");
return new[] { Path.Combine(studioHome, "bin", "lms.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "lm-studio", "bin", "lms.exe") }
.FirstOrDefault(File.Exists);
}
}
public static bool IsInstalled() => GuiPath is not null || CliPath is not null;
public static async Task InstallGuiAsync(Logger logger)
{
logger.WriteLine("LM Studio", "Downloading the official Windows GUI installer…", LogColors.Cyan);
string installer = Path.Combine(Path.GetTempPath(), "LM-Studio-" + Guid.NewGuid().ToString("N") + ".exe");
string verifier = installer + ".verify.ps1";
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(15) };
using var downloadDeadline = new CancellationTokenSource(TimeSpan.FromMinutes(15));
string arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64 ? "arm64" : "x64";
using var response = await http.GetAsync($"https://lmstudio.ai/download/latest/win32/{arch}", HttpCompletionOption.ResponseHeadersRead, downloadDeadline.Token);
response.EnsureSuccessStatusCode();
await using (var file = File.Create(installer))
await using (var source = await response.Content.ReadAsStreamAsync(downloadDeadline.Token))
{
long total = response.Content.Headers.ContentLength ?? 0, received = 0;
var buffer = new byte[81920];
int count;
while ((count = await source.ReadAsync(buffer, downloadDeadline.Token)) > 0)
{
await file.WriteAsync(buffer.AsMemory(0, count), downloadDeadline.Token);
received += count;
if (total > 0) logger.WriteProgress("LM Studio", received, total);
}
if (received == 0 || (total > 0 && received != total)) throw new InvalidDataException("LM Studio installer download is incomplete.");
}
await File.WriteAllTextAsync(verifier, "param([string]$Installer)\n$ErrorActionPreference = 'Stop'\nif ((Get-AuthenticodeSignature -LiteralPath $Installer).Status -ne 'Valid') { throw 'LM Studio installer signature is invalid.' }\n");
await RunProcessAsync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", verifier, "-Installer", installer], logger, TimeSpan.FromSeconds(30));
await RunProcessAsync(installer, ["/S"], logger, TimeSpan.FromMinutes(15));
if (GuiPath is null) throw new InvalidOperationException("LM Studio GUI was not found after installation. Install it from lmstudio.ai/download and retry.");
}
finally
{
if (File.Exists(installer)) File.Delete(installer);
if (File.Exists(verifier)) File.Delete(verifier);
}
}
public static async Task EnsureServerAsync(Logger logger, bool startIfMissing = true)
{
if (await ProbeAsync()) return;
if (!startIfMissing) throw new InvalidOperationException("LM Studio server is not ready. Enable Local Server in its Developer tab.");
if (GuiPath is { } gui)
{
var start = new ProcessStartInfo(gui) { UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden };
start.Environment.Remove("ELECTRON_RUN_AS_NODE");
Process.Start(start)?.Dispose();
}
// The GUI initializes its bundled CLI on first launch.
for (int i = 0; i < 20 && CliPath is null; i++) await Task.Delay(500);
if (CliPath is not { } cli) throw new InvalidOperationException("Open LM Studio once and enable Developer → Local Server, then retry. The lms CLI is not available yet.");
if (!Uri.TryCreate(BaseUrl, UriKind.Absolute, out var uri) || !uri.IsLoopback)
throw new InvalidOperationException("Automatic LM Studio startup only supports a local endpoint.");
try
{
await RunProcessAsync(cli, ["server", "start", "--port", uri.Port.ToString(), "--bind", "127.0.0.1"], logger, TimeSpan.FromSeconds(60));
}
catch (Exception ex) when (ex is InvalidOperationException or TimeoutException)
{
throw new InvalidOperationException("LM Studio could not start its local service. Open the GUI, complete first-launch setup, and enable Developer → Local Server, then retry.", ex);
}
for (int i = 0; i < 30; i++) { if (await ProbeAsync()) return; await Task.Delay(500); }
throw new TimeoutException("LM Studio API did not become ready. Check its Developer tab and API token settings.");
}
private static async Task<bool> ProbeAsync()
{
try { await ListModelsAsync(TimeSpan.FromSeconds(2)); return true; }
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { return false; }
}
public static async Task<IReadOnlyList<LMStudioModel>> ListModelsAsync(TimeSpan? timeout = null)
{
using var http = new HttpClient { Timeout = timeout ?? TimeSpan.FromSeconds(15) };
using var request = new HttpRequestMessage(HttpMethod.Get, BaseUrl + "/api/v1/models");
Authorize(request);
using var json = await http.SendJsonDocumentAsync(request);
return json.RootElement.GetProperty("models").EnumerateArray()
.Where(m => m.GetProperty("type").GetString() == "llm")
.Select(m =>
{
var instances = m.GetProperty("loaded_instances");
JsonElement? loaded = instances.GetArrayLength() > 0 ? instances[0] : null;
return new LMStudioModel(m.GetProperty("key").GetString()!, m.GetProperty("display_name").GetString()!,
loaded?.GetProperty("id").GetString(), loaded?.GetProperty("config").GetProperty("context_length").GetInt32(), instances.GetArrayLength());
}).ToList();
}
public static async Task DownloadModelAsync(string model, Logger logger)
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(40));
async Task<JsonDocument> GetStatusAsync(string? job = null)
{
using var request = job is null ? HttpExtensions.JsonRequest(BaseUrl + "/api/v1/models/download", new { model })
: new HttpRequestMessage(HttpMethod.Get, BaseUrl + "/api/v1/models/download/status/" + Uri.EscapeDataString(job));
Authorize(request);
return await http.SendJsonDocumentAsync(request, deadline.Token);
}
using var first = await GetStatusAsync();
string? job = first.RootElement.TryGetProperty("job_id", out var id) ? id.GetString() : null;
string? status = first.RootElement.GetProperty("status").GetString();
if (status is "completed" or "already_downloaded") return;
if (status != "downloading" || job is null) throw new InvalidOperationException($"LM Studio download returned {status}.");
while (true)
{
await Task.Delay(750, deadline.Token);
using var current = await GetStatusAsync(job);
status = current.RootElement.GetProperty("status").GetString();
if (status == "completed") { logger.WriteLine("LM Studio", "Model download completed.", LogColors.Green); return; }
if (status is "failed" or "paused") throw new InvalidOperationException($"LM Studio download is {status}. Resume it in LM Studio and retry setup.");
if (current.RootElement.TryGetProperty("downloaded_bytes", out var bytes) && current.RootElement.TryGetProperty("total_size_bytes", out var total))
logger.WriteProgress("LM Studio", bytes.GetInt64(), total.GetInt64());
}
}
private static async Task RunProcessAsync(string executable, string[] arguments, Logger logger, TimeSpan timeout)
{
var start = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true };
start.Environment.Remove("ELECTRON_RUN_AS_NODE");
foreach (string argument in arguments) start.ArgumentList.Add(argument);
using var process = Process.Start(start) ?? throw new InvalidOperationException("Could not start LM Studio setup.");
async Task DrainAsync(StreamReader reader) { while (await reader.ReadLineAsync() is { } line) logger.WriteLine("LM Studio", line, LogColors.Gray); }
Task output = DrainAsync(process.StandardOutput), error = DrainAsync(process.StandardError);
try { await process.WaitForExitOrKillAsync(timeout, "LM Studio setup process timed out."); }
finally { await Task.WhenAll(output, error); }
if (process.ExitCode != 0) throw new InvalidOperationException($"LM Studio setup exited with code {process.ExitCode}.");
}
}