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

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
resolveLlamaCppExtension,
resolveLlamaCppExtensionPaths,
} from "../llama-cpp-extension.js";
describe("resolveLlamaCppExtension", () => {
it("finds the bundled @fusion/pi-llama-cpp package", () => {
const result = resolveLlamaCppExtension();
expect(result.status).toBe("ok");
if (result.status === "ok") {
expect(result.path).toMatch(/pi-llama-cpp[\\/]index\.ts$/);
expect(result.packageVersion).toMatch(/^\d+\.\d+\.\d+$/);
}
});
});
describe("resolveLlamaCppExtensionPaths", () => {
it("returns empty when useLlamaCpp is off", () => {
const result = resolveLlamaCppExtensionPaths({});
expect(result.paths).toEqual([]);
expect(result.warning).toBeUndefined();
expect(result.resolution).toBeNull();
});
it("returns extension path when useLlamaCpp is on", () => {
const result = resolveLlamaCppExtensionPaths({ useLlamaCpp: true });
expect(result.paths).toHaveLength(1);
expect(result.paths[0]).toMatch(/pi-llama-cpp[\\/]index\.ts$/);
expect(result.resolution?.status).toBe("ok");
});
});
describe("cached resolution roundtrip", () => {
it("set/get preserves snapshot", async () => {
const { setCachedLlamaCppResolution, getCachedLlamaCppResolution } =
await import("../llama-cpp-extension.js");
setCachedLlamaCppResolution({ status: "not-installed" });
expect(getCachedLlamaCppResolution()).toEqual({ status: "not-installed" });
setCachedLlamaCppResolution(null);
expect(getCachedLlamaCppResolution()).toBeNull();
});
});

View File

@@ -54,6 +54,11 @@ import {
resolveDroidCliExtensionPaths,
setCachedDroidCliResolution,
} from "./droid-cli-extension.js";
import {
getCachedLlamaCppResolution,
resolveLlamaCppExtensionPaths,
setCachedLlamaCppResolution,
} from "./llama-cpp-extension.js";
import { resolveSelfExtension } from "./self-extension.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
@@ -473,6 +478,24 @@ export async function runDaemon(opts: DaemonOptions = {}) {
}
})();
const llamaCppPaths = await (async () => {
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
const result = resolveLlamaCppExtensionPaths(globalSettings);
setCachedLlamaCppResolution(result.resolution);
if (result.warning) {
console.warn(`[extensions] llama-cpp: ${result.warning}`);
}
return result.paths;
} catch (err) {
console.warn(
`[extensions] Unable to evaluate useLlamaCpp setting: ${err instanceof Error ? err.message : String(err)}`,
);
setCachedLlamaCppResolution(null);
return [];
}
})();
// Always prefer Fusion's vendored `@fusion/pi-claude-cli` over any
// external `pi-claude-cli` install. Drops shadowing externals (e.g. a
// global `npm install -g pi-claude-cli`) so the upstream's once-and-lock
@@ -492,7 +515,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
);
const extensionsResult = await discoverAndLoadExtensions(
[...reconciledExtensionPaths, ...droidCliPaths],
[...reconciledExtensionPaths, ...droidCliPaths, ...llamaCppPaths],
cwd,
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
);
@@ -586,6 +609,17 @@ export async function runDaemon(opts: DaemonOptions = {}) {
}
return { status: r.status, reason: r.reason };
},
getLlamaCppExtensionStatus: () => {
const r = getCachedLlamaCppResolution();
if (!r) return null;
if (r.status === "ok") {
return { status: "ok", path: r.path, packageVersion: r.packageVersion };
}
if (r.status === "not-installed") {
return { status: "not-installed" };
}
return { status: r.status, reason: r.reason };
},
onUseClaudeCliToggled: (_prev, next) => {
if (!next) return;
void (async () => {

View File

@@ -53,6 +53,11 @@ import {
resolveDroidCliExtensionPaths,
setCachedDroidCliResolution,
} from "./droid-cli-extension.js";
import {
getCachedLlamaCppResolution,
resolveLlamaCppExtensionPaths,
setCachedLlamaCppResolution,
} from "./llama-cpp-extension.js";
import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js";
import { resolveSelfExtension } from "./self-extension.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
@@ -1272,6 +1277,24 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
})();
const llamaCppPaths = await (async () => {
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
const result = resolveLlamaCppExtensionPaths(globalSettings);
setCachedLlamaCppResolution(result.resolution);
if (result.warning) {
console.warn(`[extensions] llama-cpp: ${result.warning}`);
}
return result.paths;
} catch (err) {
console.warn(
`[extensions] Unable to evaluate useLlamaCpp setting: ${err instanceof Error ? err.message : String(err)}`,
);
setCachedLlamaCppResolution(null);
return [];
}
})();
// Always inject the cli's own extension (`@runfusion/fusion`) so its
// `fn_*` tools register globally even when the user hasn't run
// `pi install npm:@runfusion/fusion`. Without this, agent chat with
@@ -1294,6 +1317,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
...packageExtensionPaths,
...claudeCliPaths,
...droidCliPaths,
...llamaCppPaths,
],
cwd,
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
@@ -1580,6 +1604,17 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
return { status: r.status, reason: r.reason };
},
getLlamaCppExtensionStatus: () => {
const r = getCachedLlamaCppResolution();
if (!r) return null;
if (r.status === "ok") {
return { status: "ok", path: r.path, packageVersion: r.packageVersion };
}
if (r.status === "not-installed") {
return { status: "not-installed" };
}
return { status: r.status, reason: r.reason };
},
onUseClaudeCliToggled: (_prev, next) => {
if (!next) return;
void (async () => {
@@ -1829,6 +1864,17 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
return { status: r.status, reason: r.reason };
},
getLlamaCppExtensionStatus: () => {
const r = getCachedLlamaCppResolution();
if (!r) return null;
if (r.status === "ok") {
return { status: "ok", path: r.path, packageVersion: r.packageVersion };
}
if (r.status === "not-installed") {
return { status: "not-installed" };
}
return { status: r.status, reason: r.reason };
},
onUseClaudeCliToggled: (_prev, next) => {
if (!next) return;
void (async () => {

View File

@@ -0,0 +1,114 @@
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const require_ = createRequire(import.meta.url);
export type LlamaCppExtensionResolution =
| { status: "ok"; path: string; packageVersion: string }
| { status: "not-installed" }
| { status: "missing-entry"; reason: string }
| { status: "error"; reason: string };
export function resolveLlamaCppExtensionFromModuleUrl(
moduleUrl: string,
): LlamaCppExtensionResolution {
let pkgJsonPath: string | undefined;
const here = dirname(fileURLToPath(moduleUrl));
for (const rel of ["pi-llama-cpp", "../pi-llama-cpp", "../../pi-llama-cpp"]) {
const candidate = resolve(here, rel, "package.json");
if (existsSync(candidate)) {
pkgJsonPath = candidate;
break;
}
}
if (!pkgJsonPath) {
try {
pkgJsonPath = require_.resolve("@fusion/pi-llama-cpp/package.json");
} catch {
return { status: "not-installed" };
}
}
let pkgJson: { pi?: { extensions?: unknown }; version?: string };
try {
pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as typeof pkgJson;
} catch (err) {
return {
status: "error",
reason: `Failed to read @fusion/pi-llama-cpp package.json: ${err instanceof Error ? err.message : String(err)}`,
};
}
const extensions = pkgJson.pi?.extensions;
if (!Array.isArray(extensions) || extensions.length === 0) {
return {
status: "missing-entry",
reason: "@fusion/pi-llama-cpp package.json has no pi.extensions array",
};
}
const rawEntry = extensions[0];
if (typeof rawEntry !== "string" || rawEntry.length === 0) {
return {
status: "missing-entry",
reason: "@fusion/pi-llama-cpp pi.extensions[0] is not a valid path string",
};
}
const entryPath = resolve(dirname(pkgJsonPath), rawEntry);
if (!existsSync(entryPath)) {
return {
status: "missing-entry",
reason: `@fusion/pi-llama-cpp extension file not found at ${entryPath}`,
};
}
return { status: "ok", path: entryPath, packageVersion: pkgJson.version ?? "unknown" };
}
export function resolveLlamaCppExtension(): LlamaCppExtensionResolution {
return resolveLlamaCppExtensionFromModuleUrl(import.meta.url);
}
export function resolveLlamaCppExtensionPaths(globalSettings: {
useLlamaCpp?: unknown;
}): { paths: string[]; warning?: string; resolution: LlamaCppExtensionResolution | null } {
const enabled = globalSettings?.useLlamaCpp === true;
if (!enabled) return { paths: [], resolution: null };
const resolution = resolveLlamaCppExtension();
switch (resolution.status) {
case "ok":
return { paths: [resolution.path], resolution };
case "not-installed":
return {
paths: [],
resolution,
warning:
"useLlamaCpp is on but @fusion/pi-llama-cpp is not installed in node_modules. Run `pnpm install`.",
};
case "missing-entry":
case "error":
return { paths: [], resolution, warning: resolution.reason };
}
}
let cachedResolution: LlamaCppExtensionResolution | null = null;
export function setCachedLlamaCppResolution(
resolution: LlamaCppExtensionResolution | null,
): void {
cachedResolution = resolution;
}
export function getCachedLlamaCppResolution(): LlamaCppExtensionResolution | null {
return cachedResolution;
}
export const _testInternals = {
moduleUrl: (): string => fileURLToPath(import.meta.url),
};

View File

@@ -57,6 +57,11 @@ import {
resolveDroidCliExtensionPaths,
setCachedDroidCliResolution,
} from "./droid-cli-extension.js";
import {
getCachedLlamaCppResolution,
resolveLlamaCppExtensionPaths,
setCachedLlamaCppResolution,
} from "./llama-cpp-extension.js";
import { resolveSelfExtension } from "./self-extension.js";
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
@@ -539,6 +544,24 @@ export async function runServe(
}
})();
const llamaCppPaths = await (async () => {
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
const result = resolveLlamaCppExtensionPaths(globalSettings);
setCachedLlamaCppResolution(result.resolution);
if (result.warning) {
console.warn(`[extensions] llama-cpp: ${result.warning}`);
}
return result.paths;
} catch (err) {
console.warn(
`[extensions] Unable to evaluate useLlamaCpp setting: ${err instanceof Error ? err.message : String(err)}`,
);
setCachedLlamaCppResolution(null);
return [];
}
})();
// Inject the cli's own extension so fn_* tools register globally without
// requiring `pi install npm:@runfusion/fusion`.
const selfExtension = resolveSelfExtension();
@@ -555,6 +578,7 @@ export async function runServe(
...packageExtensionPaths,
...claudeCliPaths,
...droidCliPaths,
...llamaCppPaths,
],
cwd,
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
@@ -781,6 +805,17 @@ export async function runServe(
}
return { status: r.status, reason: r.reason };
},
getLlamaCppExtensionStatus: () => {
const r = getCachedLlamaCppResolution();
if (!r) return null;
if (r.status === "ok") {
return { status: "ok", path: r.path, packageVersion: r.packageVersion };
}
if (r.status === "not-installed") {
return { status: "not-installed" };
}
return { status: r.status, reason: r.reason };
},
onUseClaudeCliToggled: (_prev, next) => {
if (!next) return; // Toggle-off leaves existing skill symlinks alone.
void (async () => {