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:
@@ -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;
|
||||
|
||||
19
packages/dashboard/app/components/LlamaCppProviderCard.css
Normal file
19
packages/dashboard/app/components/LlamaCppProviderCard.css
Normal 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);
|
||||
}
|
||||
}
|
||||
98
packages/dashboard/app/components/LlamaCppProviderCard.tsx
Normal file
98
packages/dashboard/app/components/LlamaCppProviderCard.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user