feat(llm B.1 #27): Test LLM button with classified error diagnostics
The brief's pain point is opaque load failures: llama-cpp-2's errors
bubble up as raw C++ strings ("cudaMalloc failed: out of memory",
"invalid gguf magic"). A user seeing that has no path to recovery.
New backend command test_llm_model runs a staged diagnostic:
1. Model not downloaded → `not-downloaded` + download hint.
2. File size ≤90% of expected → `incomplete` (stalled download)
+ re-download hint. Matters because llama-cpp-2 can segfault
on truncated GGUF rather than returning cleanly.
3. Requested model already loaded → `ready`, no side effects.
4. Otherwise attempt a real load. On failure, classify_llm_load_error
maps the raw string to one of:
- load-failed-vram (OOM / cudaMalloc / allocation)
- load-failed-corrupt (GGUF magic / unsupported format)
- load-failed-permission (permission denied / access denied)
- load-failed-other (catch-all)
Each category has a prewritten actionable hint pointing at the
specific Settings surface (tier picker, re-download, file perms).
classify_llm_load_error is pure-string and unit-tested — 8 cases
covering the main categories plus edge cases (OOM alias, Windows
"Access is denied", unknown errors). Ordered narrow-to-broad so
overlap doesn't misclassify.
Settings UI gets a "Test" button in the AI section's action row,
visible whenever the model is downloaded (both downloaded-idle and
loaded states). Shows inline hint below the status line when the
test surfaces one. Refreshes both local and global LLM status after
the test since a successful test implicitly loads the model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -562,6 +562,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Brief item B.1 #27: diagnostic button that classifies LLM setup
|
||||
// failures into actionable categories. Result shape comes from the
|
||||
// backend — category / ok / message / hint. `llmTestHint` is held
|
||||
// separately so we can render it below the one-line status without
|
||||
// stomping the existing llmStatus string.
|
||||
let llmTestBusy = $state(false);
|
||||
let llmTestHint = $state("");
|
||||
|
||||
async function testSelectedLlmModel() {
|
||||
if (llmTestBusy) return;
|
||||
const modelId = selectedLlmModelId();
|
||||
llmTestBusy = true;
|
||||
llmStatus = "Testing...";
|
||||
llmTestHint = "";
|
||||
try {
|
||||
const result = await invoke("test_llm_model", { modelId });
|
||||
llmStatus = result?.message ?? "Test complete";
|
||||
llmTestHint = result?.hint ?? "";
|
||||
// A successful test also means the model was loaded (or was
|
||||
// already loaded) — refresh both Settings-local and global
|
||||
// status so the sidebar chip and download/load buttons react.
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
} catch (err) {
|
||||
llmStatus = typeof err === "string" ? err : "Test failed";
|
||||
llmTestHint = "";
|
||||
} finally {
|
||||
llmTestBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setAiTier(nextTier) {
|
||||
settings.aiTier = nextTier;
|
||||
if (nextTier === "off") {
|
||||
@@ -1412,6 +1443,9 @@
|
||||
{LLM_MODELS.find((model) => model.id === selectedLlmModelId())?.subtitle || "Local model"}
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary mt-1">{llmStatus}</p>
|
||||
{#if llmTestHint}
|
||||
<p class="text-[11px] text-accent mt-1">{llmTestHint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
@@ -1429,12 +1463,24 @@
|
||||
class="px-3 py-2 rounded-lg bg-accent text-bg text-[12px] hover:bg-accent-hover"
|
||||
onclick={loadSelectedLlmModel}
|
||||
>Load</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded-lg border border-border text-[12px] text-text-secondary hover:text-text disabled:opacity-50"
|
||||
onclick={testSelectedLlmModel}
|
||||
disabled={llmTestBusy}
|
||||
>{llmTestBusy ? "Testing…" : "Test"}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded-lg border border-border text-[12px] text-text-secondary hover:text-text"
|
||||
onclick={deleteSelectedLlmModel}
|
||||
>Delete</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded-lg border border-border text-[12px] text-text-secondary hover:text-text disabled:opacity-50"
|
||||
onclick={testSelectedLlmModel}
|
||||
disabled={llmTestBusy}
|
||||
>{llmTestBusy ? "Testing…" : "Test"}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 rounded-lg border border-border text-[12px] text-text-secondary hover:text-text"
|
||||
|
||||
Reference in New Issue
Block a user