mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:40:43 +00:00
Tests: avoid memory-search cold plugin loads
This commit is contained in:
121
extensions/google/generation-provider-metadata.ts
Normal file
121
extensions/google/generation-provider-metadata.ts
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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<ImageGenerationProvider> | null = null;
|
||||
let googleMediaUnderstandingProviderPromise: Promise<MediaUnderstandingProvider> | null = null;
|
||||
let googleMusicGenerationProviderPromise: Promise<MusicGenerationProvider> | null = null;
|
||||
let googleVideoGenerationProviderPromise: Promise<VideoGenerationProvider> | null = null;
|
||||
|
||||
type GoogleMediaUnderstandingProvider = Required<
|
||||
Pick<
|
||||
@@ -38,6 +44,24 @@ async function loadGoogleMediaUnderstandingProvider(): Promise<MediaUnderstandin
|
||||
return await googleMediaUnderstandingProviderPromise;
|
||||
}
|
||||
|
||||
async function loadGoogleMusicGenerationProvider(): Promise<MusicGenerationProvider> {
|
||||
if (!googleMusicGenerationProviderPromise) {
|
||||
googleMusicGenerationProviderPromise = import("./music-generation-provider.js").then((mod) =>
|
||||
mod.buildGoogleMusicGenerationProvider(),
|
||||
);
|
||||
}
|
||||
return await googleMusicGenerationProviderPromise;
|
||||
}
|
||||
|
||||
async function loadGoogleVideoGenerationProvider(): Promise<VideoGenerationProvider> {
|
||||
if (!googleVideoGenerationProviderPromise) {
|
||||
googleVideoGenerationProviderPromise = import("./video-generation-provider.js").then((mod) =>
|
||||
mod.buildGoogleVideoGenerationProvider(),
|
||||
);
|
||||
}
|
||||
return await googleVideoGenerationProviderPromise;
|
||||
}
|
||||
|
||||
async function loadGoogleRequiredMediaUnderstandingProvider(): Promise<GoogleMediaUnderstandingProvider> {
|
||||
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());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityC
|
||||
function resolveBundledCapabilityCompatPluginIds(params: {
|
||||
key: CapabilityProviderRegistryKey;
|
||||
cfg?: OpenClawConfig;
|
||||
providerId?: string;
|
||||
}): string[] {
|
||||
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
||||
return loadPluginManifestRegistry({
|
||||
@@ -52,7 +53,10 @@ function resolveBundledCapabilityCompatPluginIds(params: {
|
||||
env: process.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => 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<K extends CapabilityProviderRegistryKey>(
|
||||
entries: PluginRegistry[K],
|
||||
providerId: string,
|
||||
): CapabilityProviderForKey<K> | undefined {
|
||||
const providerEntries = entries as unknown as Array<{
|
||||
provider: CapabilityProviderForKey<K> & { id?: unknown };
|
||||
}>;
|
||||
for (const entry of providerEntries) {
|
||||
if (entry.provider.id === providerId) {
|
||||
return entry.provider;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegistryKey>(params: {
|
||||
key: K;
|
||||
providerId: string;
|
||||
cfg?: OpenClawConfig;
|
||||
}): CapabilityProviderForKey<K> | 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<K extends CapabilityProviderRegistryKey>(params: {
|
||||
key: K;
|
||||
cfg?: OpenClawConfig;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user