mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(plugins): load memory embedding provider owners at startup
Gateway startup now includes plugin owners for explicit memorySearch.provider and memorySearch.fallback values, including custom models.providers API owners and generic embedding provider contracts. Sentinel and disabled paths keep existing startup behavior for auto, local, none, disabled memory search, and disabled memory slots. Adds post-runtime-load diagnostics for configured memory embedding providers that remain unregistered. Closes #89651 Co-authored-by: Joseph Krug <5925937+joeykrug@users.noreply.github.com>
This commit is contained in:
@@ -435,3 +435,293 @@ describe("prepareGatewayPluginBootstrap startup plugins", () => {
|
||||
expect(startupInput.suppressPluginInfoLogs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadGatewayStartupPluginRuntime memory provider diagnostics", () => {
|
||||
beforeEach(() => {
|
||||
loadGatewayStartupPlugins.mockClear().mockReturnValue({
|
||||
pluginRegistry: { diagnostics: [], gatewayHandlers: {}, plugins: [] },
|
||||
gatewayMethods: ["ping"],
|
||||
});
|
||||
});
|
||||
|
||||
it("warns after a full startup runtime load when configured memory embedding providers stay unregistered", async () => {
|
||||
const log = createLog();
|
||||
const { loadGatewayStartupPluginRuntime } = await import("./server-startup-plugins.js");
|
||||
|
||||
await loadGatewayStartupPluginRuntime({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "voyage",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/workspace",
|
||||
log,
|
||||
baseMethods: ["ping"],
|
||||
startupPluginIds: ["voyage"],
|
||||
});
|
||||
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('memorySearch.provider="voyage"'),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn during setup-runtime pre-bind loads", async () => {
|
||||
const log = createLog();
|
||||
const { loadGatewayStartupPluginRuntime } = await import("./server-startup-plugins.js");
|
||||
|
||||
await loadGatewayStartupPluginRuntime({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "voyage",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
workspaceDir: "/workspace",
|
||||
log,
|
||||
baseMethods: ["ping"],
|
||||
startupPluginIds: ["telegram"],
|
||||
preferSetupRuntimeForChannelPlugins: true,
|
||||
});
|
||||
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("warnUnregisteredConfiguredMemoryEmbeddingProviders", () => {
|
||||
function registry(providerIds: string[], options: { embeddingProviderIds?: string[] } = {}) {
|
||||
return {
|
||||
memoryEmbeddingProviders: providerIds.map((id) => ({ provider: { id } })),
|
||||
embeddingProviders: (options.embeddingProviderIds ?? []).map((id) => ({ provider: { id } })),
|
||||
} as never;
|
||||
}
|
||||
|
||||
it("warns when a configured memory embedding provider is not registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
expect(String(log.warn.mock.calls[0]?.[0])).toContain('memorySearch.provider="openai"');
|
||||
});
|
||||
|
||||
it("does not warn when the configured memory embedding provider is registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry(["openai"]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns when a configured memory embedding fallback is not registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai", fallback: "ollama" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry(["openai"]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
expect(String(log.warn.mock.calls[0]?.[0])).toContain('memorySearch.fallback="ollama"');
|
||||
});
|
||||
|
||||
it("does not warn when the configured memory embedding fallback is registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai", fallback: "ollama" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry(["openai", "ollama"]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn when a generic embedding provider can serve configured memory search", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "generic-embed" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([], { embeddingProviderIds: ["generic-embed"] }),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn for core generic memory embedding providers", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai-compatible" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn for custom providers backed by core generic embeddings", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "tenant-embeddings" } } },
|
||||
models: {
|
||||
providers: {
|
||||
"tenant-embeddings": {
|
||||
api: "openai-responses",
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn for memory embedding fallbacks when primary provider is fts-only", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "none", fallback: "openai" } } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn for memory embedding providers when the memory slot is disabled", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: { defaults: { memorySearch: { provider: "openai", fallback: "ollama" } } },
|
||||
plugins: { slots: { memory: "none" } },
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function customOllamaConfig(source: "provider" | "fallback" = "provider"): OpenClawConfig {
|
||||
const memorySearch =
|
||||
source === "provider"
|
||||
? { provider: "ollama-5080" }
|
||||
: { provider: "openai", fallback: "ollama-5080" };
|
||||
return {
|
||||
agents: { defaults: { memorySearch } },
|
||||
models: {
|
||||
providers: {
|
||||
"ollama-5080": {
|
||||
api: "ollama",
|
||||
baseUrl: "http://gpu-box.local:11435",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
it.each([
|
||||
["provider", "memorySearch.provider"] as const,
|
||||
["fallback", "memorySearch.fallback"] as const,
|
||||
])(
|
||||
"does not warn for custom %s entries whose api-owner plugin is registered",
|
||||
async (source, _path) => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: customOllamaConfig(source),
|
||||
pluginRegistry: registry(["openai", "ollama"]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("warns for custom providers whose api-owner plugin is not registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: customOllamaConfig(),
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
expect(String(log.warn.mock.calls[0]?.[0])).toContain('memorySearch.provider="ollama-5080"');
|
||||
});
|
||||
|
||||
it("warns for custom fallbacks whose api-owner plugin is not registered", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: customOllamaConfig("fallback"),
|
||||
pluginRegistry: registry(["openai"]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
expect(String(log.warn.mock.calls[0]?.[0])).toContain('memorySearch.fallback="ollama-5080"');
|
||||
});
|
||||
|
||||
it("does not warn for sentinel or disabled memory search providers", async () => {
|
||||
const { warnUnregisteredConfiguredMemoryEmbeddingProviders } =
|
||||
await import("./server-startup-plugins.js");
|
||||
const log = createLog();
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: { memorySearch: { provider: "local", fallback: "auto" } },
|
||||
list: [
|
||||
{
|
||||
id: "muted",
|
||||
memorySearch: { enabled: false, provider: "openai", fallback: "ollama" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
pluginRegistry: registry([]),
|
||||
log,
|
||||
});
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,11 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
|
||||
import { initSubagentRegistry } from "../agents/subagent-registry.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { collectUnregisteredConfiguredMemoryEmbeddingProviders } from "../plugins/channel-plugin-ids.js";
|
||||
import { listRegisteredEmbeddingProviders } from "../plugins/embedding-providers.js";
|
||||
import { loadPluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginRegistryParams } from "../plugins/registry-types.js";
|
||||
import type { PluginRegistry, PluginRegistryParams } from "../plugins/registry-types.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { listCoreGatewayMethodNames } from "./methods/core-descriptors.js";
|
||||
@@ -169,6 +171,9 @@ export async function prepareGatewayPluginBootstrap(params: {
|
||||
setActivePluginRegistry(pluginRegistry);
|
||||
}
|
||||
|
||||
const runtimePluginsLoaded =
|
||||
!params.minimalTestGateway && shouldLoadRuntimePlugins && !shouldLoadSetupRuntimePlugins;
|
||||
|
||||
return {
|
||||
gatewayPluginConfigAtStart: gatewayPluginConfig,
|
||||
defaultWorkspaceDir,
|
||||
@@ -178,11 +183,39 @@ export async function prepareGatewayPluginBootstrap(params: {
|
||||
baseMethods,
|
||||
pluginRegistry,
|
||||
baseGatewayMethods,
|
||||
runtimePluginsLoaded:
|
||||
!params.minimalTestGateway && shouldLoadRuntimePlugins && !shouldLoadSetupRuntimePlugins,
|
||||
runtimePluginsLoaded,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn when `agents.*.memorySearch.provider` selects a memory embedding provider
|
||||
* that no loaded plugin registered. Without the owning plugin, `active-memory`
|
||||
* cannot embed and silently falls back to keyword/FTS-only recall.
|
||||
*/
|
||||
export function warnUnregisteredConfiguredMemoryEmbeddingProviders(params: {
|
||||
config: OpenClawConfig;
|
||||
pluginRegistry: Partial<Pick<PluginRegistry, "embeddingProviders" | "memoryEmbeddingProviders">>;
|
||||
log: Pick<GatewayPluginBootstrapLog, "warn">;
|
||||
}): void {
|
||||
const registeredProviderIds = new Set(
|
||||
[
|
||||
...(params.pluginRegistry.memoryEmbeddingProviders ?? []),
|
||||
...(params.pluginRegistry.embeddingProviders ?? []),
|
||||
...listRegisteredEmbeddingProviders().map((entry) => ({ provider: entry.adapter })),
|
||||
].map((entry) => entry.provider.id),
|
||||
);
|
||||
const unregistered = collectUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: params.config,
|
||||
registeredProviderIds,
|
||||
});
|
||||
for (const provider of unregistered) {
|
||||
const path = `memorySearch.${provider.source}`;
|
||||
params.log.warn(
|
||||
`${path}="${provider.configuredId}" is configured, but no loaded plugin registered a memory embedding provider that can serve "${provider.configuredId}". Semantic memory recall will fall back to keyword/FTS-only search. Ensure the plugin that provides "${provider.configuredId}" is installed and enabled.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads startup plugin runtimes through the deferred bootstrap boundary. */
|
||||
export async function loadGatewayStartupPluginRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -201,7 +234,7 @@ export async function loadGatewayStartupPluginRuntime(params: {
|
||||
// Keep server-plugin-bootstrap behind one lazy boundary; startup config tests can exercise
|
||||
// planning without importing plugin package runtimes.
|
||||
const { loadGatewayStartupPlugins } = await import("./server-plugin-bootstrap.js");
|
||||
return loadGatewayStartupPlugins({
|
||||
const loaded = loadGatewayStartupPlugins({
|
||||
cfg: params.cfg,
|
||||
activationSourceConfig: params.activationSourceConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -217,4 +250,15 @@ export async function loadGatewayStartupPluginRuntime(params: {
|
||||
suppressPluginInfoLogs: params.suppressPluginInfoLogs,
|
||||
startupTrace: params.startupTrace,
|
||||
});
|
||||
if (params.preferSetupRuntimeForChannelPlugins !== true) {
|
||||
// Surface configured memory embedding providers after the full startup
|
||||
// runtime load; setup-runtime pre-bind loads intentionally register only
|
||||
// early channel hooks and would produce false missing-provider warnings.
|
||||
warnUnregisteredConfiguredMemoryEmbeddingProviders({
|
||||
config: params.cfg,
|
||||
pluginRegistry: loaded.pluginRegistry,
|
||||
log: params.log,
|
||||
});
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,29 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
|
||||
realtimeVoiceProviders: ["openai"],
|
||||
imageGenerationProviders: ["openai"],
|
||||
videoGenerationProviders: ["openai"],
|
||||
memoryEmbeddingProviders: ["openai"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ollama",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
providers: ["ollama"],
|
||||
cliBackends: [],
|
||||
contracts: {
|
||||
memoryEmbeddingProviders: ["ollama"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "generic-embedding",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
contracts: {
|
||||
embeddingProviders: ["generic-embed"],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -928,6 +951,261 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the owning plugin for a configured memory embedding provider at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the owning plugin for a configured memory embedding fallback at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "ollama", fallback: "openai" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "ollama", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the owning plugin for a per-agent memory embedding provider at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
list: [{ id: "researcher", memorySearch: { provider: "openai" } }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the api-owner plugin for a custom models.providers memory embedding provider at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
// Custom id resolves to its `api` owner ("ollama") for the embedding
|
||||
// adapter, so the owning plugin must load at startup.
|
||||
memorySearch: { provider: "ollama-5080" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"ollama-5080": {
|
||||
api: "ollama",
|
||||
baseUrl: "http://gpu-box.local:11435",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "ollama", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the api-owner plugin for a custom models.providers memory embedding fallback at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai", fallback: "ollama-5080" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"ollama-5080": {
|
||||
api: "ollama",
|
||||
baseUrl: "http://gpu-box.local:11435",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "ollama", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes generic embedding provider owners for configured memory search at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "generic-embed" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "generic-embedding", "memory-core"],
|
||||
],
|
||||
[
|
||||
"does not load plugin owners for core generic memory embedding providers",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai-compatible" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"does not load plugin owners for custom providers backed by core generic embeddings",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "tenant-embeddings" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"tenant-embeddings": {
|
||||
api: "openai-responses",
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"does not load memory embedding provider owners when the memory slot is disabled",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai", fallback: "ollama" },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
slots: { memory: "none" },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser"],
|
||||
],
|
||||
[
|
||||
"ignores memory embedding fallbacks when primary provider is fts-only",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "none", fallback: "openai" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"ignores sentinel memory embedding providers that no plugin owns",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "local", fallback: "auto" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"skips memory embedding providers from disabled memory search blocks",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { enabled: false, provider: "openai", fallback: "ollama" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"honors explicit plugin disablement for configured memory embedding providers",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai" },
|
||||
},
|
||||
},
|
||||
plugins: { entries: { openai: { enabled: false } } },
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"honors denied plugins for configured memory embedding providers",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai" },
|
||||
},
|
||||
},
|
||||
plugins: { deny: ["openai"] },
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"skips a per-agent memory embedding provider when memory search is disabled by inherited defaults",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { enabled: false },
|
||||
},
|
||||
list: [{ id: "researcher", memorySearch: { provider: "openai", fallback: "ollama" } }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes the inherited default provider when a per-agent override re-enables memory search",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { enabled: false, provider: "openai", fallback: "ollama" },
|
||||
},
|
||||
list: [{ id: "researcher", memorySearch: { enabled: true } }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "ollama", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes default memory embedding providers for unlisted agents even when listed agents override memory search",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai" },
|
||||
},
|
||||
list: [
|
||||
{ id: "muted", memorySearch: { enabled: false } },
|
||||
{ id: "researcher", memorySearch: { provider: "ollama" } },
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "ollama", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes default memory embedding providers for listed agents that inherit defaults",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai" },
|
||||
},
|
||||
list: [{ id: "researcher" }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes explicitly selected external web search providers at startup",
|
||||
{
|
||||
@@ -1544,6 +1822,32 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
).toEqual(["amazon-bedrock", "browser"]);
|
||||
});
|
||||
|
||||
it("keeps configured memory embedding providers in restrictive startup metadata scopes", () => {
|
||||
const registry = createManifestRegistryFixture();
|
||||
const index = createInstalledPluginIndexFixture(registry);
|
||||
|
||||
expect(
|
||||
resolveGatewayStartupMetadataPluginIds({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: { provider: "openai", fallback: "ollama" },
|
||||
},
|
||||
},
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["browser", "memory-core"],
|
||||
slots: {
|
||||
memory: "memory-core",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: createPluginPlanningTestEnv(),
|
||||
index,
|
||||
}),
|
||||
).toEqual(["browser", "memory-core", "ollama", "openai"]);
|
||||
});
|
||||
|
||||
it("uses installed-index model support for restrictive startup shorthand model scopes", () => {
|
||||
const registry = createManifestRegistryFixture();
|
||||
const index = createInstalledPluginIndexFixture(registry);
|
||||
|
||||
@@ -14,6 +14,9 @@ export {
|
||||
} from "./channel-presence-policy.js";
|
||||
|
||||
export {
|
||||
collectConfiguredMemoryEmbeddingProviderIds,
|
||||
collectConfiguredMemoryEmbeddingStartupProviderOwners,
|
||||
collectUnregisteredConfiguredMemoryEmbeddingProviders,
|
||||
resolveChannelPluginIds,
|
||||
resolveChannelPluginIdsFromRegistry,
|
||||
resolveConfiguredDeferredChannelPluginIds,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
type ConfiguredModelProvider = NonNullable<
|
||||
NonNullable<OpenClawConfig["models"]>["providers"]
|
||||
>[string];
|
||||
const OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID = "openai-compatible";
|
||||
const OPENAI_COMPATIBLE_MODEL_APIS = new Set(["openai-completions", "openai-responses"]);
|
||||
|
||||
function resolveConfiguredProviderConfig(
|
||||
providerId: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): ConfiguredModelProvider | undefined {
|
||||
const providers = cfg?.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
return (
|
||||
providers[providerId] ??
|
||||
Object.entries(providers).find(
|
||||
([candidateId]) => normalizeProviderId(candidateId) === normalized,
|
||||
)?.[1]
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads a configured provider's backing API id when runtime lookup should follow an alias. */
|
||||
export function readConfiguredProviderApiId(params: {
|
||||
providerId: string;
|
||||
cfg?: OpenClawConfig;
|
||||
resolveApiProviderId?: (normalizedApiId: string) => string | undefined;
|
||||
resolveMissingApiProviderId?: (providerConfig: ConfiguredModelProvider) => string | undefined;
|
||||
}): string | undefined {
|
||||
const providerConfig = resolveConfiguredProviderConfig(params.providerId, params.cfg);
|
||||
if (!providerConfig) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeProviderId(params.providerId);
|
||||
const api = providerConfig.api?.trim();
|
||||
const resolvedProviderId = api
|
||||
? (params.resolveApiProviderId?.(normalizeProviderId(api)) ?? normalizeProviderId(api))
|
||||
: params.resolveMissingApiProviderId?.(providerConfig);
|
||||
return resolvedProviderId && resolvedProviderId !== normalized ? resolvedProviderId : undefined;
|
||||
}
|
||||
|
||||
export function resolveConfiguredGenericEmbeddingProviderId(
|
||||
providerId: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): string | undefined {
|
||||
return readConfiguredProviderApiId({
|
||||
providerId,
|
||||
cfg,
|
||||
resolveApiProviderId: (normalizedApiId) =>
|
||||
OPENAI_COMPATIBLE_MODEL_APIS.has(normalizedApiId)
|
||||
? OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID
|
||||
: normalizedApiId,
|
||||
resolveMissingApiProviderId: (providerConfig) =>
|
||||
providerConfig.baseUrl?.trim() ? OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID : undefined,
|
||||
});
|
||||
}
|
||||
@@ -10,45 +10,6 @@ type EmbeddingProviderCapabilityKey = "embeddingProviders" | "memoryEmbeddingPro
|
||||
type RegisteredAdapterEntry<TAdapter> = {
|
||||
adapter: TAdapter;
|
||||
};
|
||||
type ConfiguredModelProvider = NonNullable<
|
||||
NonNullable<OpenClawConfig["models"]>["providers"]
|
||||
>[string];
|
||||
|
||||
function resolveConfiguredProviderConfig(
|
||||
providerId: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): ConfiguredModelProvider | undefined {
|
||||
const providers = cfg?.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
return (
|
||||
providers[providerId] ??
|
||||
Object.entries(providers).find(
|
||||
([candidateId]) => normalizeProviderId(candidateId) === normalized,
|
||||
)?.[1]
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads a configured provider's backing API id when runtime lookup should follow an alias. */
|
||||
export function readConfiguredProviderApiId(params: {
|
||||
providerId: string;
|
||||
cfg?: OpenClawConfig;
|
||||
resolveApiProviderId?: (normalizedApiId: string) => string | undefined;
|
||||
resolveMissingApiProviderId?: (providerConfig: ConfiguredModelProvider) => string | undefined;
|
||||
}): string | undefined {
|
||||
const providerConfig = resolveConfiguredProviderConfig(params.providerId, params.cfg);
|
||||
if (!providerConfig) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeProviderId(params.providerId);
|
||||
const api = providerConfig.api?.trim();
|
||||
const resolvedProviderId = api
|
||||
? (params.resolveApiProviderId?.(normalizeProviderId(api)) ?? normalizeProviderId(api))
|
||||
: params.resolveMissingApiProviderId?.(providerConfig);
|
||||
return resolvedProviderId && resolvedProviderId !== normalized ? resolvedProviderId : undefined;
|
||||
}
|
||||
|
||||
/** Builds lookup ids for embedding providers, including configured API aliases. */
|
||||
export function resolveRuntimeEmbeddingProviderLookupIds(params: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** Runtime resolver for plugin-contributed embedding providers. */
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveConfiguredGenericEmbeddingProviderId } from "./embedding-provider-config.js";
|
||||
import {
|
||||
getRuntimeEmbeddingProviderAdapter,
|
||||
listRuntimeEmbeddingProviderAdapters,
|
||||
readConfiguredProviderApiId,
|
||||
resolveRuntimeEmbeddingProviderLookupIds,
|
||||
} from "./embedding-provider-runtime-shared.js";
|
||||
import {
|
||||
@@ -12,9 +12,6 @@ import {
|
||||
type EmbeddingProviderAdapter,
|
||||
} from "./embedding-providers.js";
|
||||
|
||||
const OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID = "openai-compatible";
|
||||
const OPENAI_COMPATIBLE_MODEL_APIS = new Set(["openai-completions", "openai-responses"]);
|
||||
|
||||
export { listRegisteredEmbeddingProviders };
|
||||
|
||||
/** Lists embedding provider adapters registered directly with the process registry. */
|
||||
@@ -31,20 +28,11 @@ export function listEmbeddingProviders(cfg?: OpenClawConfig): EmbeddingProviderA
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConfiguredEmbeddingProviderId(
|
||||
export function resolveConfiguredEmbeddingProviderId(
|
||||
providerId: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): string | undefined {
|
||||
return readConfiguredProviderApiId({
|
||||
providerId,
|
||||
cfg,
|
||||
resolveApiProviderId: (normalizedApiId) =>
|
||||
OPENAI_COMPATIBLE_MODEL_APIS.has(normalizedApiId)
|
||||
? OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID
|
||||
: normalizedApiId,
|
||||
resolveMissingApiProviderId: (providerConfig) =>
|
||||
providerConfig.baseUrl?.trim() ? OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID : undefined,
|
||||
});
|
||||
return resolveConfiguredGenericEmbeddingProviderId(providerId, cfg);
|
||||
}
|
||||
|
||||
function resolveEmbeddingProviderLookupIds(id: string, cfg?: OpenClawConfig): string[] {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/** Resolves plugin ids that should load during Gateway startup. */
|
||||
import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import { buildModelCatalogMergeKey } from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import {
|
||||
findNormalizedProviderValue,
|
||||
normalizeProviderId,
|
||||
} from "@openclaw/model-catalog-core/provider-id";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
|
||||
@@ -23,6 +26,7 @@ import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
||||
import { normalizePluginsConfigWithResolver } from "./config-normalization-shared.js";
|
||||
import { resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js";
|
||||
import { resolveConfiguredGenericEmbeddingProviderId } from "./embedding-provider-config.js";
|
||||
import {
|
||||
collectConfiguredSpeechProviderIds,
|
||||
normalizeConfiguredSpeechProviderIdForStartup,
|
||||
@@ -480,6 +484,211 @@ function collectConfiguredVoiceProviderIds(config: OpenClawConfig): ConfiguredVo
|
||||
};
|
||||
}
|
||||
|
||||
// Explicit memory provider startup only pulls plugin-owned remote/custom
|
||||
// providers into Gateway boot. Missing/"auto" stays lazy, "local" is covered by
|
||||
// the selected memory slot, and "none" disables provider-backed embeddings.
|
||||
const MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS: ReadonlySet<string> = new Set([
|
||||
"auto",
|
||||
"local",
|
||||
"none",
|
||||
]);
|
||||
|
||||
function normalizeMemoryEmbeddingProviderIdValue(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeOptionalLowercaseString(value);
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function normalizeExplicitMemoryEmbeddingProviderId(value: unknown): string | undefined {
|
||||
const normalized = normalizeMemoryEmbeddingProviderIdValue(value);
|
||||
return normalized && !MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS.has(normalized)
|
||||
? normalized
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readMemorySearchEnabled(
|
||||
memorySearch: Record<string, unknown> | undefined,
|
||||
): boolean | undefined {
|
||||
const enabled = memorySearch?.enabled;
|
||||
return typeof enabled === "boolean" ? enabled : undefined;
|
||||
}
|
||||
|
||||
function isMemorySlotExplicitlyDisabled(config: OpenClawConfig): boolean {
|
||||
return normalizeOptionalLowercaseString(config.plugins?.slots?.memory) === "none";
|
||||
}
|
||||
|
||||
export type MemoryEmbeddingStartupProviderSource = "provider" | "fallback";
|
||||
|
||||
export type ConfiguredMemoryEmbeddingStartupProviderOwner = {
|
||||
/** Raw memory-search provider id as configured (normalized). */
|
||||
configuredId: string;
|
||||
/**
|
||||
* Adapter ids a plugin can own for this provider: the configured id plus its
|
||||
* `models.providers.<id>.api` owner when a custom provider maps to one.
|
||||
*/
|
||||
ownerIds: ReadonlySet<string>;
|
||||
source: MemoryEmbeddingStartupProviderSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a configured memory embedding provider id to the adapter id(s) a
|
||||
* plugin manifest contract or runtime registry can own. Mirrors runtime
|
||||
* `getConfiguredMemoryEmbeddingProvider`: the raw id maps to a direct adapter,
|
||||
* and a custom `models.providers.<id>` entry additionally maps to its `api`
|
||||
* owner adapter (`provider: "ollama-5080"` with `api: "ollama"` -> "ollama").
|
||||
* Both candidates are returned so matching covers the direct adapter and the
|
||||
* API owner without the runtime adapter registry.
|
||||
*/
|
||||
function resolveMemoryEmbeddingProviderOwnerIds(
|
||||
providerId: string,
|
||||
config: OpenClawConfig,
|
||||
): string[] {
|
||||
const ownerIds = [providerId];
|
||||
const genericOwnerId = normalizeOptionalLowercaseString(
|
||||
resolveConfiguredGenericEmbeddingProviderId(providerId, config),
|
||||
);
|
||||
if (genericOwnerId && genericOwnerId !== providerId) {
|
||||
ownerIds.push(genericOwnerId);
|
||||
}
|
||||
const ownerApi = normalizeOptionalLowercaseString(
|
||||
findNormalizedProviderValue(config.models?.providers, providerId)?.api,
|
||||
);
|
||||
if (ownerApi && ownerApi !== providerId) {
|
||||
ownerIds.push(ownerApi);
|
||||
}
|
||||
return ownerIds;
|
||||
}
|
||||
|
||||
function resolveEffectiveMemoryEmbeddingProviderEntries(
|
||||
defaults: Record<string, unknown> | undefined,
|
||||
override: Record<string, unknown> | undefined,
|
||||
): Array<{
|
||||
configuredId: string;
|
||||
source: MemoryEmbeddingStartupProviderSource;
|
||||
}> {
|
||||
const enabled = readMemorySearchEnabled(override) ?? readMemorySearchEnabled(defaults) ?? true;
|
||||
if (!enabled) {
|
||||
return [];
|
||||
}
|
||||
const rawProvider = normalizeMemoryEmbeddingProviderIdValue(
|
||||
override?.provider ?? defaults?.provider,
|
||||
);
|
||||
const effectiveProvider = rawProvider === "auto" || !rawProvider ? "openai" : rawProvider;
|
||||
if (effectiveProvider === "none") {
|
||||
return [];
|
||||
}
|
||||
const entries: Array<{
|
||||
configuredId: string;
|
||||
source: MemoryEmbeddingStartupProviderSource;
|
||||
}> = [];
|
||||
const provider =
|
||||
rawProvider && !MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS.has(rawProvider)
|
||||
? rawProvider
|
||||
: undefined;
|
||||
if (provider) {
|
||||
entries.push({ configuredId: provider, source: "provider" });
|
||||
}
|
||||
const fallback = normalizeExplicitMemoryEmbeddingProviderId(
|
||||
override?.fallback ?? defaults?.fallback ?? "none",
|
||||
);
|
||||
if (fallback && fallback !== effectiveProvider) {
|
||||
entries.push({ configuredId: fallback, source: "fallback" });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect explicit memory embedding provider owners required by startup. The
|
||||
* resolver mirrors runtime memory-search inheritance for enablement, primary
|
||||
* provider, and fallback provider, then maps custom `models.providers` ids to
|
||||
* their API-owner adapter ids.
|
||||
*/
|
||||
export function collectConfiguredMemoryEmbeddingStartupProviderOwners(
|
||||
config: OpenClawConfig,
|
||||
): ConfiguredMemoryEmbeddingStartupProviderOwner[] {
|
||||
if (isMemorySlotExplicitlyDisabled(config)) {
|
||||
return [];
|
||||
}
|
||||
const byConfiguredIdAndSource = new Map<string, ConfiguredMemoryEmbeddingStartupProviderOwner>();
|
||||
const defaultsBlock = config.agents?.defaults?.memorySearch;
|
||||
const defaults = isRecord(defaultsBlock) ? defaultsBlock : undefined;
|
||||
const addEffectiveProviders = (override: Record<string, unknown> | undefined) => {
|
||||
for (const { configuredId, source } of resolveEffectiveMemoryEmbeddingProviderEntries(
|
||||
defaults,
|
||||
override,
|
||||
)) {
|
||||
const key = `${source}\0${configuredId}`;
|
||||
if (byConfiguredIdAndSource.has(key)) {
|
||||
continue;
|
||||
}
|
||||
byConfiguredIdAndSource.set(key, {
|
||||
configuredId,
|
||||
ownerIds: new Set(resolveMemoryEmbeddingProviderOwnerIds(configuredId, config)),
|
||||
source,
|
||||
});
|
||||
}
|
||||
};
|
||||
addEffectiveProviders(undefined);
|
||||
const agents = config.agents?.list;
|
||||
const agentEntries = Array.isArray(agents) ? agents.filter(isRecord) : [];
|
||||
if (agentEntries.length === 0) {
|
||||
return [...byConfiguredIdAndSource.values()];
|
||||
}
|
||||
for (const agent of agentEntries) {
|
||||
addEffectiveProviders(isRecord(agent.memorySearch) ? agent.memorySearch : undefined);
|
||||
}
|
||||
return [...byConfiguredIdAndSource.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configured memory embedding provider ids that map to a plugin-owned
|
||||
* memory embedding provider contract, including the resolved `api` owner for
|
||||
* custom `models.providers` ids so the owning plugin loads at startup.
|
||||
*/
|
||||
export function collectConfiguredMemoryEmbeddingProviderIds(
|
||||
config: OpenClawConfig,
|
||||
): ReadonlySet<string> {
|
||||
const providerIds = new Set<string>();
|
||||
for (const provider of collectConfiguredMemoryEmbeddingStartupProviderOwners(config)) {
|
||||
for (const ownerId of provider.ownerIds) {
|
||||
providerIds.add(ownerId);
|
||||
}
|
||||
}
|
||||
return providerIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report configured memory embedding providers that no loaded plugin can serve.
|
||||
* A provider is unregistered only when none of its resolved adapter ids (the
|
||||
* configured id and its `models.providers.<id>.api` owner) was registered, so
|
||||
* custom providers warn when their API-owner plugin is missing but stay quiet
|
||||
* once that plugin loads.
|
||||
*/
|
||||
export function collectUnregisteredConfiguredMemoryEmbeddingProviders(params: {
|
||||
config: OpenClawConfig;
|
||||
registeredProviderIds: ReadonlySet<string>;
|
||||
}): Array<{ configuredId: string; source: MemoryEmbeddingStartupProviderSource }> {
|
||||
const configured = collectConfiguredMemoryEmbeddingStartupProviderOwners(params.config);
|
||||
if (configured.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registered = new Set(
|
||||
[...params.registeredProviderIds]
|
||||
.map((id) => normalizeOptionalLowercaseString(id))
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
);
|
||||
return configured
|
||||
.filter((provider) => ![...provider.ownerIds].some((ownerId) => registered.has(ownerId)))
|
||||
.map((provider) => ({ configuredId: provider.configuredId, source: provider.source }))
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
left.configuredId.localeCompare(right.configuredId) ||
|
||||
left.source.localeCompare(right.source),
|
||||
);
|
||||
}
|
||||
|
||||
function addPluginConfigEntryIds(
|
||||
target: Set<string>,
|
||||
plugins: ReturnType<typeof normalizePluginsConfigForInstalledIndex>,
|
||||
@@ -589,6 +798,7 @@ function collectConfiguredProviderIds(config: OpenClawConfig): string[] {
|
||||
...configuredVoiceProviderIds.speechProviders,
|
||||
...configuredVoiceProviderIds.realtimeTranscriptionProviders,
|
||||
...configuredVoiceProviderIds.realtimeVoiceProviders,
|
||||
...collectConfiguredMemoryEmbeddingProviderIds(config),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1015,6 +1225,23 @@ function manifestOwnsConfiguredVoiceProvider(params: {
|
||||
return false;
|
||||
}
|
||||
|
||||
function manifestOwnsConfiguredMemoryEmbeddingProvider(params: {
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
configuredMemoryEmbeddingProviderIds: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
if (params.configuredMemoryEmbeddingProviderIds.size === 0) {
|
||||
return false;
|
||||
}
|
||||
const embeddingProviderIds = [
|
||||
...(params.manifest?.contracts?.memoryEmbeddingProviders ?? []),
|
||||
...(params.manifest?.contracts?.embeddingProviders ?? []),
|
||||
];
|
||||
return embeddingProviderIds.some((providerId) => {
|
||||
const normalized = normalizeOptionalLowercaseString(providerId);
|
||||
return normalized ? params.configuredMemoryEmbeddingProviderIds.has(normalized) : false;
|
||||
});
|
||||
}
|
||||
|
||||
function canStartConfiguredGenerationProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
@@ -1113,6 +1340,55 @@ function canStartConfiguredVoiceProviderPlugin(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function canStartConfiguredMemoryEmbeddingProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
config: OpenClawConfig;
|
||||
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
activationSource: {
|
||||
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
rootConfig?: OpenClawConfig;
|
||||
};
|
||||
configuredMemoryEmbeddingProviderIds: ReadonlySet<string>;
|
||||
platform?: NodeJS.Platform;
|
||||
}): boolean {
|
||||
if (
|
||||
!manifestOwnsConfiguredMemoryEmbeddingProvider({
|
||||
manifest: params.manifest,
|
||||
configuredMemoryEmbeddingProviderIds: params.configuredMemoryEmbeddingProviderIds,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
||||
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
||||
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: params.plugin.pluginId,
|
||||
origin: params.plugin.origin,
|
||||
config: params.pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
||||
activationSource: params.activationSource,
|
||||
});
|
||||
return (
|
||||
activationState.enabled &&
|
||||
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
function canStartConfiguredModelProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
@@ -1597,6 +1873,8 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
const configuredGenerationProviderIds =
|
||||
collectConfiguredGenerationProviderIds(activationSourceConfig);
|
||||
const configuredVoiceProviderIds = collectConfiguredVoiceProviderIds(activationSourceConfig);
|
||||
const configuredMemoryEmbeddingProviderIds =
|
||||
collectConfiguredMemoryEmbeddingProviderIds(activationSourceConfig);
|
||||
const normalizePluginId = createPluginRegistryIdNormalizer(params.index, {
|
||||
manifestRegistry: params.manifestRegistry,
|
||||
});
|
||||
@@ -1731,6 +2009,20 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
pluginIds.push(plugin.pluginId);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
canStartConfiguredMemoryEmbeddingProviderPlugin({
|
||||
plugin,
|
||||
manifest,
|
||||
config: params.config,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
configuredMemoryEmbeddingProviderIds,
|
||||
platform: params.platform,
|
||||
})
|
||||
) {
|
||||
pluginIds.push(plugin.pluginId);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
canStartExplicitHookPlugin({
|
||||
plugin,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Runtime bridge for plugin-provided memory embedding providers.
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { readConfiguredProviderApiId } from "./embedding-provider-config.js";
|
||||
import {
|
||||
getRuntimeEmbeddingProviderAdapter,
|
||||
listRuntimeEmbeddingProviderAdapters,
|
||||
readConfiguredProviderApiId,
|
||||
resolveRuntimeEmbeddingProviderLookupIds,
|
||||
} from "./embedding-provider-runtime-shared.js";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user