fix: make node-llama-cpp optional

This commit is contained in:
Peter Steinberger
2026-01-15 18:37:02 +00:00
parent 316e8b2eb2
commit cb78fa46a1
12 changed files with 277 additions and 87 deletions

View File

@@ -14,6 +14,7 @@ const createFetchMock = () =>
describe("embedding provider remote overrides", () => {
afterEach(() => {
vi.resetAllMocks();
vi.resetModules();
vi.unstubAllGlobals();
});
@@ -107,3 +108,63 @@ describe("embedding provider remote overrides", () => {
expect(headers.Authorization).toBe("Bearer provider-key");
});
});
describe("embedding provider local fallback", () => {
afterEach(() => {
vi.resetAllMocks();
vi.resetModules();
vi.unstubAllGlobals();
vi.doUnmock("./node-llama.js");
});
it("falls back to openai when node-llama-cpp is missing", async () => {
vi.doMock("./node-llama.js", () => ({
importNodeLlamaCpp: async () => {
throw Object.assign(new Error("Cannot find package 'node-llama-cpp'"), {
code: "ERR_MODULE_NOT_FOUND",
});
},
}));
const fetchMock = createFetchMock();
vi.stubGlobal("fetch", fetchMock);
const { createEmbeddingProvider } = await import("./embeddings.js");
const authModule = await import("../agents/model-auth.js");
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
apiKey: "provider-key",
});
const result = await createEmbeddingProvider({
config: {} as never,
provider: "local",
model: "text-embedding-3-small",
fallback: "openai",
});
expect(result.provider.id).toBe("openai");
expect(result.fallbackFrom).toBe("local");
expect(result.fallbackReason).toContain("node-llama-cpp");
});
it("throws a helpful error when local is requested and fallback is none", async () => {
vi.doMock("./node-llama.js", () => ({
importNodeLlamaCpp: async () => {
throw Object.assign(new Error("Cannot find package 'node-llama-cpp'"), {
code: "ERR_MODULE_NOT_FOUND",
});
},
}));
const { createEmbeddingProvider } = await import("./embeddings.js");
await expect(
createEmbeddingProvider({
config: {} as never,
provider: "local",
model: "text-embedding-3-small",
fallback: "none",
}),
).rejects.toThrow(/optional dependency node-llama-cpp/i);
});
});

View File

@@ -1,6 +1,7 @@
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
import type { ClawdbotConfig } from "../config/config.js";
import { importNodeLlamaCpp } from "./node-llama.js";
export type EmbeddingProvider = {
id: string;
@@ -105,7 +106,7 @@ async function createLocalEmbeddingProvider(
const modelCacheDir = options.local?.modelCacheDir?.trim();
// Lazy-load node-llama-cpp to keep startup light unless local is enabled.
const { getLlama, resolveModelFile, LlamaLogLevel } = await import("node-llama-cpp");
const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp();
let llama: Llama | null = null;
let embeddingModel: LlamaModel | null = null;
@@ -181,15 +182,32 @@ function formatError(err: unknown): string {
return String(err);
}
function isNodeLlamaCppMissing(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const code = (err as Error & { code?: unknown }).code;
if (code === "ERR_MODULE_NOT_FOUND") {
return err.message.includes("node-llama-cpp");
}
return false;
}
function formatLocalSetupError(err: unknown): string {
const detail = formatError(err);
const missing = isNodeLlamaCppMissing(err);
return [
"Local embeddings unavailable.",
detail ? `Reason: ${detail}` : undefined,
missing
? "Reason: optional dependency node-llama-cpp is missing (or failed to install)."
: detail
? `Reason: ${detail}`
: undefined,
missing && detail ? `Detail: ${detail}` : null,
"To enable local embeddings:",
"1) pnpm approve-builds",
"2) select node-llama-cpp",
"3) pnpm rebuild node-llama-cpp",
"1) Use Node 22 LTS (recommended for installs/updates)",
missing
? "2) Reinstall Clawdbot (this should install node-llama-cpp): npm i -g clawdbot@latest"
: null,
"3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
'Or set agents.defaults.memorySearch.provider = "openai" (remote).',
]
.filter(Boolean)

3
src/memory/node-llama.ts Normal file
View File

@@ -0,0 +1,3 @@
export async function importNodeLlamaCpp() {
return import("node-llama-cpp");
}