add LLM configuration and backend management components with automated setup scripts
This commit is contained in:
1 parent
822e9c9481
commit
20b81926b6
10 files changed
+575
-234
No files matched your search
@@ -5,7 +5,7 @@ import {
|
||||
Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers,
|
||||
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check, Palette
|
||||
} from "lucide-react";
|
||||
import { stopServer, formatBytes, getLlmStatus, startLlm, stopLlm } from "../services/api";
|
||||
import { stopServer, formatBytes, getLlmBackends, getLlmStats, getLlmStatus, benchmarkLlm, startLlm, stopLlm } from "../services/api";
|
||||
import { THEMES } from "../themes";
|
||||
|
||||
const ASPECT_RATIOS = [
|
||||
@@ -187,6 +187,9 @@ function Settings({
|
||||
setTheme,
|
||||
}) {
|
||||
const [llmStatus, setLlmStatus] = useState({ ready: false, settings: {} });
|
||||
const [llmBackends, setLlmBackends] = useState({ available: [], candidates: [] });
|
||||
const [llmStats, setLlmStats] = useState({ benchmarks: [] });
|
||||
const [benchmarkBusy, setBenchmarkBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -195,6 +198,13 @@ function Settings({
|
||||
const status = await getLlmStatus();
|
||||
if (!cancelled) setLlmStatus(status);
|
||||
} catch (_) {}
|
||||
try {
|
||||
const [backends, stats] = await Promise.all([getLlmBackends(), getLlmStats()]);
|
||||
if (!cancelled) {
|
||||
setLlmBackends(backends);
|
||||
setLlmStats(stats);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
fetchLlmStatus();
|
||||
const interval = setInterval(fetchLlmStatus, 3000);
|
||||
@@ -247,6 +257,37 @@ function Settings({
|
||||
performanceProfile: settings?.performanceProfile,
|
||||
});
|
||||
|
||||
const handleBenchmarkTextBackend = async () => {
|
||||
const model = llmStatus.settings?.model;
|
||||
if (!model) {
|
||||
showAlert({ title: "No Text Model Loaded", message: "Load a GGUF text model before benchmarking.", danger: true });
|
||||
return;
|
||||
}
|
||||
setBenchmarkBusy(true);
|
||||
try {
|
||||
const result = await benchmarkLlm(model, {
|
||||
contextSize: Math.min(2048, Number(textSettings?.contextSize) || 2048),
|
||||
gpuLayers: textSettings?.gpuLayers ?? -1,
|
||||
includeCpu: true,
|
||||
});
|
||||
const winner = result.winner;
|
||||
showAlert({
|
||||
title: winner ? "Benchmark Complete" : "Benchmark Finished",
|
||||
message: winner
|
||||
? `Fastest backend: ${winner.backendMode} at ${Number(winner.predicted_per_second || 0).toFixed(1)} tokens/sec.`
|
||||
: "Benchmark finished, but no backend returned usable timing data.",
|
||||
});
|
||||
const [status, backends, stats] = await Promise.all([getLlmStatus(), getLlmBackends(true), getLlmStats()]);
|
||||
setLlmStatus(status);
|
||||
setLlmBackends(backends);
|
||||
setLlmStats(stats);
|
||||
} catch (err) {
|
||||
showAlert({ title: "Benchmark Failed", message: err.message || String(err), danger: true });
|
||||
} finally {
|
||||
setBenchmarkBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleThinkingToggle = async (enabled) => {
|
||||
const nextSettings = { ...textSettings, enableThinking: enabled };
|
||||
|
||||
@@ -924,6 +965,26 @@ function Settings({
|
||||
GPU Layers: {llmStatus.settings?.gpuLayers === -1 ? "All" : llmStatus.settings?.gpuLayers}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: "10px", fontSize: "0.78rem", lineHeight: "1.5" }}>
|
||||
Available: {(llmBackends.available || []).map((backend) => backend.key.toUpperCase()).join(", ") || "None"}<br />
|
||||
Fastest saved: {(() => {
|
||||
const model = llmStatus.settings?.model;
|
||||
const winner = (llmStats.benchmarks || []).find((item) => item.ok && (!model || item.model === model));
|
||||
return winner
|
||||
? `${winner.backendMode} (${Number(winner.predicted_per_second || 0).toFixed(1)} tok/s)`
|
||||
: "Not benchmarked";
|
||||
})()}
|
||||
</div>
|
||||
<button
|
||||
className="m3-button secondary"
|
||||
type="button"
|
||||
onClick={handleBenchmarkTextBackend}
|
||||
disabled={benchmarkBusy || !llmStatus.ready}
|
||||
style={{ marginTop: "12px", width: "100%", justifyContent: "center" }}
|
||||
>
|
||||
<Gauge size={15} />
|
||||
{benchmarkBusy ? "Benchmarking..." : "Benchmark Text Backends"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -316,6 +316,32 @@ export async function getLlmStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLlmBackends(refresh = false) {
|
||||
const res = await fetch(`/api/llm/backends${refresh ? "?refresh=1" : ""}`);
|
||||
return await readJsonResponse(res, "The local server returned invalid text backend data.");
|
||||
}
|
||||
|
||||
export async function getLlmStats() {
|
||||
const res = await fetch("/api/llm/stats");
|
||||
return await readJsonResponse(res, "The local server returned invalid text runtime stats.");
|
||||
}
|
||||
|
||||
export async function benchmarkLlm(model, options = {}) {
|
||||
const res = await fetch("/api/llm/benchmark", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
backends: options.backends,
|
||||
includeCpu: options.includeCpu,
|
||||
prompt: options.prompt,
|
||||
contextSize: options.contextSize,
|
||||
gpuLayers: options.gpuLayers,
|
||||
}),
|
||||
});
|
||||
return await readJsonResponse(res, "The local server returned invalid benchmark data.");
|
||||
}
|
||||
|
||||
export async function listLlmModels() {
|
||||
const res = await fetch("/api/llm/models");
|
||||
const data = await readJsonResponse(res, "The local server returned invalid text model data.");
|
||||
@@ -358,6 +384,7 @@ export async function startLlm(model, options = {}) {
|
||||
batchSize: options.batchSize,
|
||||
ubatchSize: options.ubatchSize,
|
||||
performanceProfile: options.performanceProfile,
|
||||
preferredBackend: options.preferredBackend,
|
||||
}),
|
||||
});
|
||||
return await readJsonResponse(res, "The local server returned an invalid text backend response.");
|
||||
|
||||
Reference in new issue
Block a user