feat(FN-3195): document llama.cpp provider setup and onboarding

Docs(FN-3195): adds llama.cpp provider setup and onboarding documentation to the main and dashboard READMEs.

Fusion-Task-Id: FN-3195
This commit is contained in:
Fusion
2026-05-04 10:12:43 -07:00
committed by gsxdsm
parent e9f2a4cfb5
commit b7ac46ecef
37 changed files with 1440 additions and 38 deletions

View File

@@ -1317,6 +1317,23 @@ export interface DroidCliStatus {
ready: boolean;
}
export interface LlamaCppStatus {
enabled: boolean;
extension: {
status: "ok" | "not-installed" | "missing-entry" | "error";
path?: string;
packageVersion?: string;
reason?: string;
} | null;
ready: boolean;
server: {
available: boolean;
url: string;
hasApiKey: boolean;
reason?: string;
};
}
/** Probe the local Claude CLI binary + setting + extension state. */
export function fetchClaudeCliStatus(): Promise<ClaudeCliStatus> {
return api<ClaudeCliStatus>("/providers/claude-cli/status");
@@ -1368,6 +1385,11 @@ export function fetchDroidCliStatus(): Promise<DroidCliStatus> {
return api<DroidCliStatus>("/providers/droid-cli/status");
}
/** Probe llama.cpp server + setting + extension state. */
export function fetchLlamaCppStatus(): Promise<LlamaCppStatus> {
return api<LlamaCppStatus>("/providers/llama-cpp/status");
}
// --- Runtime Provider Status Types ---
export interface RuntimeBinaryStatus {
@@ -1627,6 +1649,16 @@ export function setDroidCliEnabled(
});
}
/** Enable or disable the llama.cpp provider. */
export function setLlamaCppEnabled(
enabled: boolean,
): Promise<{ enabled: boolean; restartRequired: boolean }> {
return api<{ enabled: boolean; restartRequired: boolean }>("/auth/llama-cpp", {
method: "POST",
body: JSON.stringify({ enabled }),
});
}
export interface CustomProvider {
id: string;
name: string;

View File

@@ -0,0 +1,19 @@
.llama-cpp-provider-card {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.llama-cpp-status {
color: var(--text-muted);
}
.llama-cpp-status--ok {
color: var(--color-success);
}
@media (max-width: 768px) {
.llama-cpp-provider-card {
gap: var(--space-xs);
}
}

View File

@@ -0,0 +1,98 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Loader2 } from "lucide-react";
import { fetchLlamaCppStatus, setLlamaCppEnabled, type LlamaCppStatus } from "../api";
import { ProviderIcon } from "./ProviderIcon";
import "./LlamaCppProviderCard.css";
interface LlamaCppProviderCardProps {
authenticated: boolean;
onToggled?: (nextEnabled: boolean) => void;
compact?: boolean;
}
export function LlamaCppProviderCard({ authenticated, onToggled, compact = false }: LlamaCppProviderCardProps) {
const [status, setStatus] = useState<LlamaCppStatus | null>(null);
const [busy, setBusy] = useState<"enabling" | "disabling" | "testing" | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const refresh = useCallback(async () => {
const next = await fetchLlamaCppStatus();
if (mountedRef.current) setStatus(next);
return next;
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
const handleToggle = useCallback(async (next: boolean) => {
setBusy(next ? "enabling" : "disabling");
try {
const result = await setLlamaCppEnabled(next);
onToggled?.(result.enabled);
await refresh();
} finally {
if (mountedRef.current) setBusy(null);
}
}, [onToggled, refresh]);
const handleTest = useCallback(async () => {
setBusy("testing");
try {
await refresh();
} finally {
if (mountedRef.current) setBusy(null);
}
}, [refresh]);
const enabled = status?.enabled ?? authenticated;
const serverAvailable = status?.server.available ?? false;
const content = (
<>
<div className="auth-provider-info">
<ProviderIcon provider="llama-cpp" size={compact ? "sm" : "md"} />
<strong>llama.cpp — via HTTP server</strong>
</div>
<small className={`llama-cpp-status${status?.ready ? " llama-cpp-status--ok" : ""}`}>
{!status
? "Probing llama.cpp server…"
: status.server.available
? `Server reachable at ${status.server.url}`
: `Server unavailable: ${status.server.reason ?? "not reachable"}`}
</small>
<div className="auth-provider-cli-actions">
<button type="button" className="btn btn-sm" onClick={() => void handleTest()} disabled={busy !== null}>
{busy === "testing" ? <><Loader2 size={12} className="animate-spin" />Testing…</> : "Test"}
</button>
{enabled ? (
<button type="button" className="btn btn-sm" onClick={() => void handleToggle(false)} disabled={busy !== null}>
{busy === "disabling" ? "Disabling…" : "Disable"}
</button>
) : (
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => void handleToggle(true)}
disabled={busy !== null || !serverAvailable}
>
{busy === "enabling" ? "Enabling…" : "Enable"}
</button>
)}
</div>
</>
);
if (compact) {
return <div className="auth-provider-card auth-provider-card--cli llama-cpp-provider-card" data-testid="llama-cpp-provider-card">{content}</div>;
}
return <div className="onboarding-provider-card llama-cpp-provider-card" data-testid="llama-cpp-provider-card">{content}</div>;
}

View File

@@ -24,6 +24,7 @@ import { CustomModelDropdown } from "./CustomModelDropdown";
import { ProviderIcon } from "./ProviderIcon";
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
import { DroidCliProviderCard } from "./DroidCliProviderCard";
import { LlamaCppProviderCard } from "./LlamaCppProviderCard";
import { LoginInstructions } from "./LoginInstructions";
import { OAuthManualCodeForm } from "./OAuthManualCodeForm";
import { OnboardingDisclosure } from "./OnboardingDisclosure";
@@ -199,6 +200,7 @@ const ONBOARDING_CURATED_PROVIDER_FAMILY_ORDER = [
"anthropic",
"claude-cli",
"droid-cli",
"llama-cpp",
"openai-codex",
"gemini",
"minimax",
@@ -210,6 +212,7 @@ const ONBOARDING_PROVIDER_FAMILY_ALIASES: Record<string, (typeof ONBOARDING_CURA
anthropic: "anthropic",
"claude-cli": "claude-cli",
"droid-cli": "droid-cli",
"llama-cpp": "llama-cpp",
"openai-codex": "openai-codex",
google: "gemini",
gemini: "gemini",
@@ -1769,6 +1772,18 @@ export function ModelOnboardingModal({
);
}
if (provider.id === "llama-cpp" && provider.type === "cli") {
return (
<LlamaCppProviderCard
key={provider.id}
authenticated={provider.authenticated}
onToggled={() => {
void loadAuthStatus();
}}
/>
);
}
if (provider.id === "claude-cli" && provider.type === "cli") {
return (
<ClaudeCliProviderCard

View File

@@ -1,5 +1,9 @@
import { Cpu } from "lucide-react";
function LlamaCppIcon({ size, color, label = "llama.cpp" }: { size: number; color: string; label?: string }) {
return <Cpu size={size} color={color} aria-label={label} data-testid="llama-cpp-icon" />;
}
export interface ProviderIconProps {
provider: string;
size?: "sm" | "md" | "lg";
@@ -653,6 +657,8 @@ const providerConfig: Record<
"claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
"pi-claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
"droid-cli": { component: DroidCliIcon, color: "var(--provider-openai)", label: "Factory AI — via Droid CLI" },
"llama-cpp": { component: LlamaCppIcon, color: "var(--provider-ollama)", label: "llama.cpp" },
"llama-server": { component: LlamaCppIcon, color: "var(--provider-ollama)", label: "llama.cpp" },
openai: { component: OpenAIIcon, color: "var(--provider-openai)" },
"openai-codex": { component: OpenAIIcon, color: "var(--provider-openai)", label: "OpenAI Codex" }, // OpenAI alias

View File

@@ -28,6 +28,7 @@ const PiExtensionsManager = lazy(() => import("./PiExtensionsManager").then((m)
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
import { CliBinaryPanel } from "./CliBinaryPanel";
import { DroidCliProviderCard } from "./DroidCliProviderCard";
import { LlamaCppProviderCard } from "./LlamaCppProviderCard";
import { HermesRuntimeCard } from "./HermesRuntimeCard";
import { OpenClawRuntimeCard } from "./OpenClawRuntimeCard";
import { PaperclipRuntimeCard } from "./PaperclipRuntimeCard";
@@ -5033,6 +5034,7 @@ export function SettingsModal({
// auth state (Authenticated when signed in, Available otherwise).
const claudeCliProvider = cliAuthProviders.find((p) => p.id === "claude-cli");
const droidCliProvider = cliAuthProviders.find((p) => p.id === "droid-cli");
const llamaCppProvider = cliAuthProviders.find((p) => p.id === "llama-cpp");
const hasDroidPluginSlot = getSlotsForId("settings-provider-card").some(
(entry) => entry.pluginId === "fusion-plugin-droid-runtime",
);
@@ -5054,14 +5056,25 @@ export function SettingsModal({
}}
/>
) : null;
const llamaCppCard = llamaCppProvider ? (
<LlamaCppProviderCard
compact
authenticated={llamaCppProvider.authenticated}
onToggled={() => {
void loadAuthStatus();
}}
/>
) : null;
const showAuthenticatedGroup =
authenticatedProviders.length > 0
|| (claudeCliProvider?.authenticated ?? false)
|| ((droidCliProvider?.authenticated ?? false) && !hasDroidPluginSlot);
|| ((droidCliProvider?.authenticated ?? false) && !hasDroidPluginSlot)
|| (llamaCppProvider?.authenticated ?? false);
const showAvailableGroup =
unauthenticatedProviders.length > 0
|| (claudeCliProvider && !claudeCliProvider.authenticated)
|| (droidCliProvider && !droidCliProvider.authenticated && !hasDroidPluginSlot);
|| (droidCliProvider && !droidCliProvider.authenticated && !hasDroidPluginSlot)
|| (llamaCppProvider && !llamaCppProvider.authenticated);
return (
<>
<h4 className="settings-section-heading">Authentication</h4>
@@ -5085,6 +5098,7 @@ export function SettingsModal({
<div className="auth-group-label">Authenticated</div>
{claudeCliProvider?.authenticated && claudeCliCard}
{droidCliProvider?.authenticated && droidCliCard}
{llamaCppProvider?.authenticated && llamaCppCard}
{authenticatedProviders.map((provider) => (
<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
<div className="auth-provider-header">
@@ -5179,6 +5193,7 @@ export function SettingsModal({
<div className="auth-group-label">Available</div>
{claudeCliProvider && !claudeCliProvider.authenticated && claudeCliCard}
{droidCliProvider && !droidCliProvider.authenticated && droidCliCard}
{llamaCppProvider && !llamaCppProvider.authenticated && llamaCppCard}
{unauthenticatedProviders.map((provider) => (
<div key={provider.id} className="auth-provider-card">
<div className="auth-provider-header">

View File

@@ -0,0 +1,30 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { LlamaCppProviderCard } from "../LlamaCppProviderCard";
const fetchLlamaCppStatus = vi.fn();
const setLlamaCppEnabled = vi.fn();
vi.mock("../../api", () => ({
fetchLlamaCppStatus: (...args: unknown[]) => fetchLlamaCppStatus(...args),
setLlamaCppEnabled: (...args: unknown[]) => setLlamaCppEnabled(...args),
}));
describe("LlamaCppProviderCard", () => {
beforeEach(() => {
fetchLlamaCppStatus.mockResolvedValue({
enabled: false,
ready: false,
extension: { status: "ok" },
server: { available: true, url: "http://127.0.0.1:8080", hasApiKey: false },
});
setLlamaCppEnabled.mockResolvedValue({ enabled: true, restartRequired: false });
});
it("renders and enables provider", async () => {
render(<LlamaCppProviderCard authenticated={false} />);
await waitFor(() => expect(fetchLlamaCppStatus).toHaveBeenCalled());
fireEvent.click(screen.getByRole("button", { name: "Enable" }));
await waitFor(() => expect(setLlamaCppEnabled).toHaveBeenCalledWith(true));
});
});

View File

@@ -95,6 +95,13 @@ describe("ProviderIcon", () => {
expect(screen.getByLabelText("Ollama")).toBeInTheDocument();
});
it("renders llama.cpp icon aliases", () => {
const { rerender } = render(<ProviderIcon provider="llama-cpp" />);
expect(screen.getByTestId("llama-cpp-icon")).toBeInTheDocument();
rerender(<ProviderIcon provider="llama-server" />);
expect(screen.getByTestId("llama-cpp-icon")).toBeInTheDocument();
});
it("renders Cpu icon as fallback for unknown providers", () => {
render(<ProviderIcon provider="unknown" />);
// Cpu icon from lucide-react renders as an svg without our custom data-testid

View File

@@ -258,6 +258,7 @@ html {
--provider-groq: #f55036;
--provider-vercel: var(--text);
--provider-droid-cli: var(--text);
--provider-ollama: #d4a27f;
/* Runtime-plugin marks. */
--provider-hermes: #d4961c;
--provider-openclaw: #ff4f40;