diff --git a/extensions/google/generation-provider-metadata.ts b/extensions/google/generation-provider-metadata.ts new file mode 100644 index 00000000000..ea942815981 --- /dev/null +++ b/extensions/google/generation-provider-metadata.ts @@ -0,0 +1,121 @@ +import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation"; +import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth"; +import type { + VideoGenerationProvider, + VideoGenerationProviderConfiguredContext, +} from "openclaw/plugin-sdk/video-generation"; + +export const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview"; +export const GOOGLE_PRO_MUSIC_MODEL = "lyria-3-pro-preview"; +export const GOOGLE_MAX_INPUT_IMAGES = 10; + +export const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview"; +export const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const; +export const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0]; +export const GOOGLE_VIDEO_MAX_DURATION_SECONDS = + GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1]; + +function isGoogleProviderConfigured( + ctx: { agentDir?: string } | VideoGenerationProviderConfiguredContext, +): boolean { + return isProviderApiKeyConfigured({ + provider: "google", + agentDir: ctx.agentDir, + }); +} + +export function createGoogleMusicGenerationProviderMetadata(): Omit< + MusicGenerationProvider, + "generateMusic" +> { + return { + id: "google", + label: "Google", + defaultModel: DEFAULT_GOOGLE_MUSIC_MODEL, + models: [DEFAULT_GOOGLE_MUSIC_MODEL, GOOGLE_PRO_MUSIC_MODEL], + isConfigured: isGoogleProviderConfigured, + capabilities: { + generate: { + maxTracks: 1, + supportsLyrics: true, + supportsInstrumental: true, + supportsFormat: true, + supportedFormatsByModel: { + [DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"], + [GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"], + }, + }, + edit: { + enabled: true, + maxTracks: 1, + maxInputImages: GOOGLE_MAX_INPUT_IMAGES, + supportsLyrics: true, + supportsInstrumental: true, + supportsFormat: true, + supportedFormatsByModel: { + [DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"], + [GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"], + }, + }, + }, + }; +} + +export function createGoogleVideoGenerationProviderMetadata(): Omit< + VideoGenerationProvider, + "generateVideo" +> { + return { + id: "google", + label: "Google", + defaultModel: DEFAULT_GOOGLE_VIDEO_MODEL, + models: [ + DEFAULT_GOOGLE_VIDEO_MODEL, + "veo-3.1-generate-preview", + "veo-3.1-lite-generate-preview", + "veo-3.0-fast-generate-001", + "veo-3.0-generate-001", + "veo-2.0-generate-001", + ], + isConfigured: isGoogleProviderConfigured, + capabilities: { + generate: { + maxVideos: 1, + maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, + supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS], + aspectRatios: ["16:9", "9:16"], + resolutions: ["720P", "1080P"], + supportsAspectRatio: true, + supportsResolution: true, + supportsSize: true, + supportsAudio: true, + }, + imageToVideo: { + enabled: true, + maxVideos: 1, + maxInputImages: 1, + maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, + supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS], + aspectRatios: ["16:9", "9:16"], + resolutions: ["720P", "1080P"], + supportsAspectRatio: true, + supportsResolution: true, + supportsSize: true, + supportsAudio: true, + }, + videoToVideo: { + enabled: true, + maxVideos: 1, + maxInputVideos: 1, + maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, + supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS], + aspectRatios: ["16:9", "9:16"], + resolutions: ["720P", "1080P"], + supportsAspectRatio: true, + supportsResolution: true, + supportsSize: true, + supportsAudio: true, + }, + }, + }; +} diff --git a/extensions/google/index.ts b/extensions/google/index.ts index 195e79602a5..c022b655c70 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -1,17 +1,23 @@ import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation"; import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding"; +import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation"; import { buildGoogleGeminiCliBackend } from "./cli-backend.js"; import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js"; +import { + createGoogleMusicGenerationProviderMetadata, + createGoogleVideoGenerationProviderMetadata, +} from "./generation-provider-metadata.js"; import { geminiMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; -import { buildGoogleMusicGenerationProvider } from "./music-generation-provider.js"; import { registerGoogleProvider } from "./provider-registration.js"; import { buildGoogleSpeechProvider } from "./speech-provider.js"; import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js"; -import { buildGoogleVideoGenerationProvider } from "./video-generation-provider.js"; let googleImageGenerationProviderPromise: Promise | null = null; let googleMediaUnderstandingProviderPromise: Promise | null = null; +let googleMusicGenerationProviderPromise: Promise | null = null; +let googleVideoGenerationProviderPromise: Promise | null = null; type GoogleMediaUnderstandingProvider = Required< Pick< @@ -38,6 +44,24 @@ async function loadGoogleMediaUnderstandingProvider(): Promise { + if (!googleMusicGenerationProviderPromise) { + googleMusicGenerationProviderPromise = import("./music-generation-provider.js").then((mod) => + mod.buildGoogleMusicGenerationProvider(), + ); + } + return await googleMusicGenerationProviderPromise; +} + +async function loadGoogleVideoGenerationProvider(): Promise { + if (!googleVideoGenerationProviderPromise) { + googleVideoGenerationProviderPromise = import("./video-generation-provider.js").then((mod) => + mod.buildGoogleVideoGenerationProvider(), + ); + } + return await googleVideoGenerationProviderPromise; +} + async function loadGoogleRequiredMediaUnderstandingProvider(): Promise { const provider = await loadGoogleMediaUnderstandingProvider(); if ( @@ -104,6 +128,22 @@ function createLazyGoogleMediaUnderstandingProvider(): MediaUnderstandingProvide }; } +function createLazyGoogleMusicGenerationProvider(): MusicGenerationProvider { + return { + ...createGoogleMusicGenerationProviderMetadata(), + generateMusic: async (...args) => + await (await loadGoogleMusicGenerationProvider()).generateMusic(...args), + }; +} + +function createLazyGoogleVideoGenerationProvider(): VideoGenerationProvider { + return { + ...createGoogleVideoGenerationProviderMetadata(), + generateVideo: async (...args) => + await (await loadGoogleVideoGenerationProvider()).generateVideo(...args), + }; +} + export default definePluginEntry({ id: "google", name: "Google Plugin", @@ -115,9 +155,9 @@ export default definePluginEntry({ api.registerMemoryEmbeddingProvider(geminiMemoryEmbeddingProviderAdapter); api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider()); api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider()); - api.registerMusicGenerationProvider(buildGoogleMusicGenerationProvider()); + api.registerMusicGenerationProvider(createLazyGoogleMusicGenerationProvider()); api.registerSpeechProvider(buildGoogleSpeechProvider()); - api.registerVideoGenerationProvider(buildGoogleVideoGenerationProvider()); + api.registerVideoGenerationProvider(createLazyGoogleVideoGenerationProvider()); api.registerWebSearchProvider(createGeminiWebSearchProvider()); }, }); diff --git a/extensions/google/music-generation-provider.ts b/extensions/google/music-generation-provider.ts index eec050dc525..e455eec80fe 100644 --- a/extensions/google/music-generation-provider.ts +++ b/extensions/google/music-generation-provider.ts @@ -5,15 +5,17 @@ import type { MusicGenerationProvider, MusicGenerationRequest, } from "openclaw/plugin-sdk/music-generation"; -import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth"; import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { normalizeGoogleApiBaseUrl } from "./api.js"; +import { + createGoogleMusicGenerationProviderMetadata, + DEFAULT_GOOGLE_MUSIC_MODEL, + GOOGLE_MAX_INPUT_IMAGES, + GOOGLE_PRO_MUSIC_MODEL, +} from "./generation-provider-metadata.js"; -const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview"; -const GOOGLE_PRO_MUSIC_MODEL = "lyria-3-pro-preview"; const DEFAULT_TIMEOUT_MS = 180_000; -const GOOGLE_MAX_INPUT_IMAGES = 10; type GoogleInlineDataPart = { mimeType?: string; @@ -99,39 +101,7 @@ function extractTracks(params: { payload: GoogleGenerateMusicResponse; model: st export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider { return { - id: "google", - label: "Google", - defaultModel: DEFAULT_GOOGLE_MUSIC_MODEL, - models: [DEFAULT_GOOGLE_MUSIC_MODEL, GOOGLE_PRO_MUSIC_MODEL], - isConfigured: ({ agentDir }) => - isProviderApiKeyConfigured({ - provider: "google", - agentDir, - }), - capabilities: { - generate: { - maxTracks: 1, - supportsLyrics: true, - supportsInstrumental: true, - supportsFormat: true, - supportedFormatsByModel: { - [DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"], - [GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"], - }, - }, - edit: { - enabled: true, - maxTracks: 1, - maxInputImages: GOOGLE_MAX_INPUT_IMAGES, - supportsLyrics: true, - supportsInstrumental: true, - supportsFormat: true, - supportedFormatsByModel: { - [DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"], - [GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"], - }, - }, - }, + ...createGoogleMusicGenerationProviderMetadata(), async generateMusic(req) { if ((req.inputImages?.length ?? 0) > GOOGLE_MAX_INPUT_IMAGES) { throw new Error( diff --git a/extensions/google/video-generation-provider.ts b/extensions/google/video-generation-provider.ts index 627f8ebf51e..2de8fb6f63a 100644 --- a/extensions/google/video-generation-provider.ts +++ b/extensions/google/video-generation-provider.ts @@ -1,7 +1,6 @@ import { mkdtemp, readFile, rm } from "node:fs/promises"; import path from "node:path"; import { GoogleGenAI } from "@google/genai"; -import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth"; import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime"; import { createProviderOperationDeadline, @@ -16,15 +15,17 @@ import type { VideoGenerationRequest, } from "openclaw/plugin-sdk/video-generation"; import { normalizeGoogleApiBaseUrl } from "./api.js"; +import { + createGoogleVideoGenerationProviderMetadata, + DEFAULT_GOOGLE_VIDEO_MODEL, + GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS, + GOOGLE_VIDEO_MAX_DURATION_SECONDS, + GOOGLE_VIDEO_MIN_DURATION_SECONDS, +} from "./generation-provider-metadata.js"; -const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview"; const DEFAULT_TIMEOUT_MS = 180_000; const POLL_INTERVAL_MS = 10_000; const MAX_POLL_ATTEMPTS = 90; -const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const; -const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0]; -const GOOGLE_VIDEO_MAX_DURATION_SECONDS = - GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1]; function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined { const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl); @@ -151,61 +152,7 @@ async function downloadGeneratedVideo(params: { export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider { return { - id: "google", - label: "Google", - defaultModel: DEFAULT_GOOGLE_VIDEO_MODEL, - models: [ - DEFAULT_GOOGLE_VIDEO_MODEL, - "veo-3.1-generate-preview", - "veo-3.1-lite-generate-preview", - "veo-3.0-fast-generate-001", - "veo-3.0-generate-001", - "veo-2.0-generate-001", - ], - isConfigured: ({ agentDir }) => - isProviderApiKeyConfigured({ - provider: "google", - agentDir, - }), - capabilities: { - generate: { - maxVideos: 1, - maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, - supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS, - aspectRatios: ["16:9", "9:16"], - resolutions: ["720P", "1080P"], - supportsAspectRatio: true, - supportsResolution: true, - supportsSize: true, - supportsAudio: true, - }, - imageToVideo: { - enabled: true, - maxVideos: 1, - maxInputImages: 1, - maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, - supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS, - aspectRatios: ["16:9", "9:16"], - resolutions: ["720P", "1080P"], - supportsAspectRatio: true, - supportsResolution: true, - supportsSize: true, - supportsAudio: true, - }, - videoToVideo: { - enabled: true, - maxVideos: 1, - maxInputVideos: 1, - maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS, - supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS, - aspectRatios: ["16:9", "9:16"], - resolutions: ["720P", "1080P"], - supportsAspectRatio: true, - supportsResolution: true, - supportsSize: true, - supportsAudio: true, - }, - }, + ...createGoogleVideoGenerationProviderMetadata(), async generateVideo(req) { if ((req.inputImages?.length ?? 0) > 1) { throw new Error("Google video generation supports at most one input image."); diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index 420bc74a44f..021e1674e9e 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -8,7 +8,7 @@ import { normalizeMemoryMultimodalSettings, type MemoryMultimodalSettings, } from "../memory-host-sdk/multimodal.js"; -import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-provider-runtime.js"; +import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js"; import { clampInt, clampNumber, resolveUserPath } from "../utils.js"; import { resolveAgentConfig } from "./agent-scope.js"; @@ -388,9 +388,12 @@ export function resolveMemorySearchConfig( const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal); const multimodalProvider = resolved.provider === "auto" ? undefined : getMemoryEmbeddingProvider(resolved.provider); + // Config resolution is a startup/doctor hot path; only validate adapters + // already registered by the active runtime instead of cold-loading plugins. if ( multimodalActive && - !(multimodalProvider?.supportsMultimodalEmbeddings?.({ model: resolved.model }) ?? false) + multimodalProvider && + !(multimodalProvider.supportsMultimodalEmbeddings?.({ model: resolved.model }) ?? false) ) { throw new Error( "agents.*.memorySearch.multimodal requires a provider adapter that supports multimodal embeddings for the configured model.", diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index 2d75a346bf5..7cbd3018df8 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -47,6 +47,7 @@ vi.mock("./bundled-compat.js", () => ({ })); let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders; +let resolvePluginCapabilityProvider: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider; function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) { expect(providers.map((provider) => provider.id)).toEqual(expected); @@ -149,7 +150,8 @@ function expectCompatChainApplied(params: { describe("resolvePluginCapabilityProviders", () => { beforeAll(async () => { - ({ resolvePluginCapabilityProviders } = await import("./capability-provider-runtime.js")); + ({ resolvePluginCapabilityProvider, resolvePluginCapabilityProviders } = + await import("./capability-provider-runtime.js")); }); beforeEach(() => { @@ -310,4 +312,68 @@ describe("resolvePluginCapabilityProviders", () => { }); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: compatConfig }); }); + + it("loads only the bundled owner plugin for a targeted provider lookup", () => { + const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig; + const allowlistCompat = { + plugins: { + allow: ["custom-plugin", "google"], + }, + } as OpenClawConfig; + const enablementCompat = { + plugins: { + allow: ["custom-plugin", "google"], + entries: { google: { enabled: true } }, + }, + }; + const loaded = createEmptyPluginRegistry(); + loaded.memoryEmbeddingProviders.push({ + pluginId: "google", + pluginName: "google", + source: "test", + provider: { + id: "gemini", + create: async () => ({ provider: null }), + }, + } as never); + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "google", + origin: "bundled", + contracts: { memoryEmbeddingProviders: ["gemini"] }, + }, + { + id: "openai", + origin: "bundled", + contracts: { memoryEmbeddingProviders: ["openai"] }, + }, + ] as never, + diagnostics: [], + }); + mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat); + mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat); + mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => + params === undefined ? undefined : loaded, + ); + + const provider = resolvePluginCapabilityProvider({ + key: "memoryEmbeddingProviders", + providerId: "gemini", + cfg, + }); + + expect(provider?.id).toBe("gemini"); + expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({ + config: cfg, + pluginIds: ["google"], + }); + expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({ + config: allowlistCompat, + pluginIds: ["google"], + }); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ + config: enablementCompat, + }); + }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index c3a350b145d..d6362aa60e0 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -45,6 +45,7 @@ const CAPABILITY_CONTRACT_KEY: Record plugin.origin === "bundled" && (plugin.contracts?.[contractKey]?.length ?? 0) > 0, + (plugin) => + plugin.origin === "bundled" && + (plugin.contracts?.[contractKey]?.length ?? 0) > 0 && + (!params.providerId || (plugin.contracts?.[contractKey] ?? []).includes(params.providerId)), ) .map((plugin) => plugin.id) .toSorted((left, right) => left.localeCompare(right)); @@ -61,8 +65,9 @@ function resolveBundledCapabilityCompatPluginIds(params: { function resolveCapabilityProviderConfig(params: { key: CapabilityProviderRegistryKey; cfg?: OpenClawConfig; + pluginIds?: string[]; }) { - const pluginIds = resolveBundledCapabilityCompatPluginIds(params); + const pluginIds = params.pluginIds ?? resolveBundledCapabilityCompatPluginIds(params); const allowlistCompat = withBundledPluginAllowlistCompat({ config: params.cfg, pluginIds, @@ -78,6 +83,51 @@ function resolveCapabilityProviderConfig(params: { }); } +function findProviderById( + entries: PluginRegistry[K], + providerId: string, +): CapabilityProviderForKey | undefined { + const providerEntries = entries as unknown as Array<{ + provider: CapabilityProviderForKey & { id?: unknown }; + }>; + for (const entry of providerEntries) { + if (entry.provider.id === providerId) { + return entry.provider; + } + } + return undefined; +} + +export function resolvePluginCapabilityProvider(params: { + key: K; + providerId: string; + cfg?: OpenClawConfig; +}): CapabilityProviderForKey | undefined { + const activeRegistry = resolveRuntimePluginRegistry(); + const activeProvider = findProviderById(activeRegistry?.[params.key] ?? [], params.providerId); + if (activeProvider) { + return activeProvider; + } + + const pluginIds = resolveBundledCapabilityCompatPluginIds({ + key: params.key, + cfg: params.cfg, + providerId: params.providerId, + }); + if (pluginIds.length === 0) { + return undefined; + } + + const compatConfig = resolveCapabilityProviderConfig({ + key: params.key, + cfg: params.cfg, + pluginIds, + }); + const loadOptions = compatConfig === undefined ? undefined : { config: compatConfig }; + const registry = resolveRuntimePluginRegistry(loadOptions); + return findProviderById(registry?.[params.key] ?? [], params.providerId); +} + export function resolvePluginCapabilityProviders(params: { key: K; cfg?: OpenClawConfig; diff --git a/src/plugins/memory-embedding-provider-runtime.test.ts b/src/plugins/memory-embedding-provider-runtime.test.ts index ba30496f31b..3a337735716 100644 --- a/src/plugins/memory-embedding-provider-runtime.test.ts +++ b/src/plugins/memory-embedding-provider-runtime.test.ts @@ -9,9 +9,13 @@ const mocks = vi.hoisted(() => ({ resolvePluginCapabilityProviders: vi.fn< typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders >(() => []), + resolvePluginCapabilityProvider: vi.fn< + typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider + >(() => undefined), })); vi.mock("./capability-provider-runtime.js", () => ({ + resolvePluginCapabilityProvider: mocks.resolvePluginCapabilityProvider, resolvePluginCapabilityProviders: mocks.resolvePluginCapabilityProviders, })); @@ -28,6 +32,8 @@ beforeEach(async () => { clearMemoryEmbeddingProviders(); mocks.resolvePluginCapabilityProviders.mockReset(); mocks.resolvePluginCapabilityProviders.mockReturnValue([]); + mocks.resolvePluginCapabilityProvider.mockReset(); + mocks.resolvePluginCapabilityProvider.mockReturnValue(undefined); runtimeModule = await import("./memory-embedding-provider-runtime.js"); }); @@ -53,12 +59,18 @@ describe("memory embedding provider runtime resolution", () => { it("falls back to declared capability adapters when the registry is cold", () => { mocks.resolvePluginCapabilityProviders.mockReturnValue([createCapabilityAdapter("ollama")]); + mocks.resolvePluginCapabilityProvider.mockReturnValue(createCapabilityAdapter("ollama")); expect(runtimeModule.listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ "ollama", ]); expect(runtimeModule.getMemoryEmbeddingProvider("ollama")?.id).toBe("ollama"); - expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(2); + expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(1); + expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({ + key: "memoryEmbeddingProviders", + providerId: "ollama", + cfg: undefined, + }); }); it("prefers registered adapters over declared capability fallback adapters with the same id", () => { diff --git a/src/plugins/memory-embedding-provider-runtime.ts b/src/plugins/memory-embedding-provider-runtime.ts index 7a5e3ff64f0..5a5c34751e4 100644 --- a/src/plugins/memory-embedding-provider-runtime.ts +++ b/src/plugins/memory-embedding-provider-runtime.ts @@ -1,5 +1,8 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolvePluginCapabilityProviders } from "./capability-provider-runtime.js"; +import { + resolvePluginCapabilityProvider, + resolvePluginCapabilityProviders, +} from "./capability-provider-runtime.js"; import { getRegisteredMemoryEmbeddingProvider, listRegisteredMemoryEmbeddingProviders, @@ -35,5 +38,9 @@ export function getMemoryEmbeddingProvider( if (registered) { return registered.adapter; } - return listMemoryEmbeddingProviders(cfg).find((adapter) => adapter.id === id); + return resolvePluginCapabilityProvider({ + key: "memoryEmbeddingProviders", + providerId: id, + cfg, + }); }