fix(memory): keep llama runtime optional (#71425)

* fix(memory): keep llama runtime optional

* fix(memory): harden optional llama runtime guard
This commit is contained in:
Vincent Koc
2026-04-25 00:09:12 -07:00
committed by GitHub
parent 4005a4f731
commit 9895ecead3
10 changed files with 69 additions and 746 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Browser: add viewport coordinate clicks for managed and existing-session automation, plus `openclaw browser click-coords` for CLI use. (#54452) Thanks @dluttz.
- Browser/config: support per-profile `browser.profiles.<name>.headless` overrides for locally launched browser profiles, so one profile can run headless without forcing all browser profiles headless. Thanks @nakamotoliu.
- Plugins/PDF: move local PDF extraction into a bundled `document-extract` plugin so core no longer owns `pdfjs-dist` or PDF image-rendering dependencies. Thanks @vincentkoc.
- Dependencies/memory: stop installing `node-llama-cpp` by default; local embeddings now load it only when operators install the optional runtime package. Thanks @vincentkoc.
- Matrix: require full cross-signing identity trust for self-device verification and add `openclaw matrix verify self` so operators can establish that trust from the CLI. (#70401) Thanks @gumadeiras.
- WebChat/sessions: keep runtime-only prompt context out of visible transcript history and scrub legacy wrappers from session history surfaces. Thanks @91wan.
- Gradium: add a bundled text-to-speech provider with voice-note and telephony output support. (#64958) Thanks @LaurentMazare.

View File

@@ -38,8 +38,9 @@ To set a provider explicitly:
Without an embedding provider, only keyword search is available.
To force the built-in local embedding provider, point `local.modelPath` at a
GGUF file:
To force the built-in local embedding provider, install the optional
`node-llama-cpp` runtime package next to OpenClaw, then point `local.modelPath`
at a GGUF file:
```json5
{
@@ -66,7 +67,7 @@ GGUF file:
| Voyage | `voyage` | Yes | |
| Mistral | `mistral` | Yes | |
| Ollama | `ollama` | No | Local, set explicitly |
| Local | `local` | Yes (first) | GGUF model, ~0.6 GB download |
| Local | `local` | Yes (first) | Optional `node-llama-cpp` runtime |
Auto-detection picks the first provider whose API key can be resolved, in the
order shown. Set `memorySearch.provider` to override.

View File

@@ -15,7 +15,8 @@ binary, and can index content beyond your workspace memory files.
- **Reranking and query expansion** for better recall.
- **Index extra directories** -- project docs, team notes, anything on disk.
- **Index session transcripts** -- recall earlier conversations.
- **Fully local** -- runs via Bun + node-llama-cpp, auto-downloads GGUF models.
- **Fully local** -- runs with the optional node-llama-cpp runtime package and
auto-downloads GGUF models.
- **Automatic fallback** -- if QMD is unavailable, OpenClaw falls back to the
builtin engine seamlessly.

View File

@@ -29,8 +29,8 @@ explicitly:
}
```
For local embeddings with no API key, use `provider: "local"` (requires
node-llama-cpp).
For local embeddings with no API key, install the optional `node-llama-cpp`
runtime package next to OpenClaw and use `provider: "local"`.
## Supported providers

View File

@@ -11,6 +11,10 @@ import { getProviderEnvVars } from "openclaw/plugin-sdk/provider-env-vars";
import { formatErrorMessage } from "../dreaming-shared.js";
import { filterUnregisteredMemoryEmbeddingProviderAdapters } from "./provider-adapter-registration.js";
const NODE_LLAMA_CPP_RUNTIME_PACKAGE = "node-llama-cpp";
const NODE_LLAMA_CPP_RUNTIME_VERSION = "3.18.1";
const NODE_LLAMA_CPP_INSTALL_SPEC = `${NODE_LLAMA_CPP_RUNTIME_PACKAGE}@${NODE_LLAMA_CPP_RUNTIME_VERSION}`;
export type BuiltinMemoryEmbeddingProviderDoctorMetadata = {
providerId: string;
authProviderId: string;
@@ -24,7 +28,7 @@ function isNodeLlamaCppMissing(err: unknown): boolean {
return false;
}
const code = (err as Error & { code?: unknown }).code;
return code === "ERR_MODULE_NOT_FOUND" && err.message.includes("node-llama-cpp");
return code === "ERR_MODULE_NOT_FOUND" && err.message.includes(NODE_LLAMA_CPP_RUNTIME_PACKAGE);
}
function listRemoteEmbeddingSetupHints(): string[] {
@@ -55,9 +59,9 @@ function formatLocalSetupError(err: unknown): string {
"To enable local embeddings:",
"1) Use Node 24 (recommended for installs/updates; Node 22 LTS, currently 22.14+, remains supported)",
missing
? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g openclaw@latest"
? `2) Install optional local embedding runtime next to OpenClaw: npm i -g ${NODE_LLAMA_CPP_INSTALL_SPEC}`
: null,
"3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
`3) If you use pnpm: pnpm approve-builds (select ${NODE_LLAMA_CPP_RUNTIME_PACKAGE}), then pnpm rebuild ${NODE_LLAMA_CPP_RUNTIME_PACKAGE}`,
...listRemoteEmbeddingSetupHints(),
]
.filter(Boolean)

View File

@@ -1658,14 +1658,6 @@
"typescript": "^6.0.3",
"vitest": "^4.1.5"
},
"peerDependencies": {
"node-llama-cpp": "3.18.1"
},
"peerDependenciesMeta": {
"node-llama-cpp": {
"optional": true
}
},
"overrides": {
"axios": "1.15.0",
"follow-redirects": "1.16.0",

714
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -137,11 +137,6 @@
"class": "core-runtime",
"risk": ["parser", "markdown"]
},
"node-llama-cpp": {
"owner": "capability:memory-local-embeddings",
"class": "optional-peer-runtime",
"risk": ["native", "local-model-runtime", "large-transitive-cone"]
},
"openai": {
"owner": "provider:openai",
"class": "default-runtime-initially",

View File

@@ -22,6 +22,8 @@ type PackageJson = {
license?: string;
repository?: { url?: string } | string;
bin?: Record<string, string>;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
};
@@ -58,6 +60,7 @@ export type NpmDistTagMirrorAuth = {
source: "node-auth-token" | "npm-token" | "none";
};
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
const OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE = "node-llama-cpp";
const MAX_CALVER_DISTANCE_DAYS = 2;
const REQUIRED_PACKED_PATHS = [
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
@@ -266,15 +269,25 @@ export function collectReleasePackageMetadataErrors(pkg: PackageJson): string[]
`package.json bin.openclaw must be "openclaw.mjs"; found "${pkg.bin?.openclaw ?? ""}".`,
);
}
if (pkg.peerDependencies?.["node-llama-cpp"] !== "3.18.1") {
if (pkg.dependencies?.[OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE]) {
errors.push(
`package.json peerDependencies["node-llama-cpp"] must be "3.18.1"; found "${
pkg.peerDependencies?.["node-llama-cpp"] ?? ""
}".`,
`package.json dependencies["${OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE}"] must be omitted; keep it optional.`,
);
}
if (pkg.peerDependenciesMeta?.["node-llama-cpp"]?.optional !== true) {
errors.push('package.json peerDependenciesMeta["node-llama-cpp"].optional must be true.');
if (pkg.optionalDependencies?.[OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE]) {
errors.push(
`package.json optionalDependencies["${OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE}"] must be omitted; keep it operator-installed.`,
);
}
if (pkg.peerDependencies?.[OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE]) {
errors.push(
`package.json peerDependencies["${OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE}"] must be omitted; keep it optional.`,
);
}
if (pkg.peerDependenciesMeta?.[OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE]) {
errors.push(
`package.json peerDependenciesMeta["${OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE}"] must be omitted; keep it optional.`,
);
}
return errors;

View File

@@ -519,13 +519,11 @@ describe("collectReleasePackageMetadataErrors", () => {
license: "MIT",
repository: { url: "git+https://github.com/openclaw/openclaw.git" },
bin: { openclaw: "openclaw.mjs" },
peerDependencies: { "node-llama-cpp": "3.18.1" },
peerDependenciesMeta: { "node-llama-cpp": { optional: true } },
}),
).toEqual([]);
});
it("requires node-llama-cpp to stay an optional peer", () => {
it("rejects node-llama-cpp as a peer dependency", () => {
expect(
collectReleasePackageMetadataErrors({
name: "openclaw",
@@ -534,7 +532,39 @@ describe("collectReleasePackageMetadataErrors", () => {
repository: { url: "git+https://github.com/openclaw/openclaw.git" },
bin: { openclaw: "openclaw.mjs" },
peerDependencies: { "node-llama-cpp": "3.18.1" },
peerDependenciesMeta: { "node-llama-cpp": { optional: true } },
}),
).toContain('package.json peerDependenciesMeta["node-llama-cpp"].optional must be true.');
).toEqual([
'package.json peerDependencies["node-llama-cpp"] must be omitted; keep it optional.',
'package.json peerDependenciesMeta["node-llama-cpp"] must be omitted; keep it optional.',
]);
});
it("rejects node-llama-cpp as a direct runtime dependency", () => {
expect(
collectReleasePackageMetadataErrors({
name: "openclaw",
description: "Multi-channel AI gateway with extensible messaging integrations",
license: "MIT",
repository: { url: "git+https://github.com/openclaw/openclaw.git" },
bin: { openclaw: "openclaw.mjs" },
dependencies: { "node-llama-cpp": "3.18.1" },
}),
).toContain('package.json dependencies["node-llama-cpp"] must be omitted; keep it optional.');
});
it("rejects node-llama-cpp as an optional dependency", () => {
expect(
collectReleasePackageMetadataErrors({
name: "openclaw",
description: "Multi-channel AI gateway with extensible messaging integrations",
license: "MIT",
repository: { url: "git+https://github.com/openclaw/openclaw.git" },
bin: { openclaw: "openclaw.mjs" },
optionalDependencies: { "node-llama-cpp": "3.18.1" },
}),
).toContain(
'package.json optionalDependencies["node-llama-cpp"] must be omitted; keep it operator-installed.',
);
});
});