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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
114
packages/cli/src/commands/llama-cpp-extension.ts
Normal file
114
packages/cli/src/commands/llama-cpp-extension.ts
Normal 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),
|
||||
};
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user