perf: avoid plugin loader on provider fast paths

This commit is contained in:
Peter Steinberger
2026-03-22 21:14:51 +00:00
parent 171b24c5c5
commit 593e333c10
9 changed files with 193 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
import { logVerbose } from "../../globals.js";
import { listSpeechProviders, normalizeSpeechProviderId } from "../../tts/provider-registry.js";
import { getSpeechProvider, normalizeSpeechProviderId } from "../../tts/provider-registry.js";
import {
getLastTtsAttempt,
getTtsMaxLength,
@@ -178,8 +178,7 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
}
const requested = args.trim().toLowerCase();
const knownProviders = new Set(listSpeechProviders(params.cfg).map((provider) => provider.id));
if (requested !== "edge" && !knownProviders.has(requested)) {
if (requested !== "edge" && !getSpeechProvider(requested, params.cfg)) {
return { shouldContinue: false, reply: ttsUsage() };
}

View File

@@ -1,5 +1,9 @@
import { loadConfig } from "../../config/config.js";
import { listSpeechProviders, normalizeSpeechProviderId } from "../../tts/provider-registry.js";
import {
getSpeechProvider,
listSpeechProviders,
normalizeSpeechProviderId,
} from "../../tts/provider-registry.js";
import {
OPENAI_TTS_MODELS,
OPENAI_TTS_VOICES,
@@ -104,8 +108,7 @@ export const ttsHandlers: GatewayRequestHandlers = {
typeof params.provider === "string" ? params.provider.trim() : "",
);
const cfg = loadConfig();
const knownProviders = new Set(listSpeechProviders(cfg).map((entry) => entry.id));
if (!provider || !knownProviders.has(provider)) {
if (!provider || !getSpeechProvider(provider, cfg)) {
respond(
false,
undefined,

View File

@@ -0,0 +1,52 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
const { loadOpenClawPluginsMock } = vi.hoisted(() => ({
loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()),
}));
vi.mock("../plugins/loader.js", () => ({
loadOpenClawPlugins: loadOpenClawPluginsMock,
}));
import { getImageGenerationProvider, listImageGenerationProviders } from "./provider-registry.js";
describe("image-generation provider registry", () => {
afterEach(() => {
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
resetPluginRuntimeStateForTest();
});
it("does not load plugins when listing without config", () => {
expect(listImageGenerationProviders()).toEqual([]);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("uses active plugin providers without loading from disk", () => {
const registry = createEmptyPluginRegistry();
registry.imageGenerationProviders.push({
pluginId: "custom-image",
pluginName: "Custom Image",
source: "test",
provider: {
id: "custom-image",
label: "Custom Image",
capabilities: {
generate: {},
edit: { enabled: false },
},
generateImage: async () => ({
images: [{ buffer: Buffer.from("image"), mimeType: "image/png" }],
}),
},
});
setActivePluginRegistry(registry);
const provider = getImageGenerationProvider("custom-image");
expect(provider?.id).toBe("custom-image");
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -15,11 +15,13 @@ function resolvePluginImageGenerationProviders(
cfg?: OpenClawConfig,
): ImageGenerationProviderPlugin[] {
const active = getActivePluginRegistry();
const registry =
(active?.imageGenerationProviders?.length ?? 0) > 0 || !cfg
? active
: loadOpenClawPlugins({ config: cfg });
return registry?.imageGenerationProviders?.map((entry) => entry.provider) ?? [];
const activeEntries = active?.imageGenerationProviders?.map((entry) => entry.provider) ?? [];
if (activeEntries.length > 0 || !cfg) {
return activeEntries;
}
return loadOpenClawPlugins({ config: cfg }).imageGenerationProviders.map(
(entry) => entry.provider,
);
}
function buildProviderMaps(cfg?: OpenClawConfig): {

View File

@@ -1,11 +1,22 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../../plugins/registry.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
const { loadOpenClawPluginsMock } = vi.hoisted(() => ({
loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()),
}));
vi.mock("../../plugins/loader.js", () => ({
loadOpenClawPlugins: loadOpenClawPluginsMock,
}));
import { buildMediaUnderstandingRegistry, getMediaUnderstandingProvider } from "./index.js";
describe("media-understanding provider registry", () => {
afterEach(() => {
setActivePluginRegistry(createEmptyPluginRegistry());
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
resetPluginRuntimeStateForTest();
});
it("keeps core-owned fallback providers registered by default", () => {
@@ -60,4 +71,11 @@ describe("media-understanding provider registry", () => {
expect(provider?.id).toBe("google");
});
it("does not load plugins when config is absent and no runtime registry is active", () => {
const registry = buildMediaUnderstandingRegistry();
expect([...registry.keys()]).toEqual(["groq", "deepgram"]);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -41,13 +41,15 @@ export function buildMediaUnderstandingRegistry(
mergeProviderIntoRegistry(registry, provider);
}
const active = getActivePluginRegistry();
const pluginRegistry =
(active?.mediaUnderstandingProviders?.length ?? 0) > 0
? active
: loadOpenClawPlugins({ config: cfg });
for (const entry of pluginRegistry?.mediaUnderstandingProviders ?? []) {
const activeEntries = active?.mediaUnderstandingProviders ?? [];
for (const entry of activeEntries) {
mergeProviderIntoRegistry(registry, entry.provider);
}
if (activeEntries.length === 0 && cfg) {
for (const entry of loadOpenClawPlugins({ config: cfg }).mediaUnderstandingProviders) {
mergeProviderIntoRegistry(registry, entry.provider);
}
}
if (overrides) {
for (const [key, provider] of Object.entries(overrides)) {
const normalizedKey = normalizeMediaProviderId(key);

View File

@@ -494,7 +494,7 @@ export async function resolveAutoImageModel(params: {
agentDir?: string;
activeModel?: ActiveMediaModel;
}): Promise<ActiveMediaModel | null> {
const providerRegistry = buildProviderRegistry();
const providerRegistry = buildProviderRegistry(undefined, params.cfg);
const toActive = (entry: MediaUnderstandingModelConfig | null): ActiveMediaModel | null => {
if (!entry || entry.type === "cli") {
return null;

View File

@@ -0,0 +1,62 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
const { loadOpenClawPluginsMock } = vi.hoisted(() => ({
loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()),
}));
vi.mock("../plugins/loader.js", () => ({
loadOpenClawPlugins: loadOpenClawPluginsMock,
}));
import { getSpeechProvider, listSpeechProviders } from "./provider-registry.js";
describe("speech provider registry", () => {
afterEach(() => {
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
resetPluginRuntimeStateForTest();
});
it("does not load plugins for builtin provider lookup", () => {
const provider = getSpeechProvider("openai", {} as OpenClawConfig);
expect(provider?.id).toBe("openai");
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("does not load plugins when listing without config", () => {
const providers = listSpeechProviders();
expect(providers.map((provider) => provider.id)).toEqual(["openai", "elevenlabs", "microsoft"]);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("uses active plugin speech providers without loading from disk", () => {
const registry = createEmptyPluginRegistry();
registry.speechProviders.push({
pluginId: "custom-speech",
pluginName: "Custom Speech",
source: "test",
provider: {
id: "custom-speech",
label: "Custom Speech",
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.from("audio"),
outputFormat: "mp3",
fileExtension: ".mp3",
voiceCompatible: false,
}),
},
});
setActivePluginRegistry(registry);
const provider = getSpeechProvider("custom-speech");
expect(provider?.id).toBe("custom-speech");
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -30,11 +30,32 @@ export function normalizeSpeechProviderId(
function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProviderPlugin[] {
const active = getActivePluginRegistry();
const registry =
(active?.speechProviders?.length ?? 0) > 0 || !cfg
? active
: loadOpenClawPlugins({ config: cfg });
return registry?.speechProviders?.map((entry) => entry.provider) ?? [];
const activeEntries = active?.speechProviders?.map((entry) => entry.provider) ?? [];
if (activeEntries.length > 0 || !cfg) {
return activeEntries;
}
return loadOpenClawPlugins({ config: cfg }).speechProviders.map((entry) => entry.provider);
}
function registerSpeechProvider(
maps: {
canonical: Map<string, SpeechProviderPlugin>;
aliases: Map<string, SpeechProviderPlugin>;
},
provider: SpeechProviderPlugin,
): void {
const id = normalizeSpeechProviderId(provider.id);
if (!id) {
return;
}
maps.canonical.set(id, provider);
maps.aliases.set(id, provider);
for (const alias of provider.aliases ?? []) {
const normalizedAlias = normalizeSpeechProviderId(alias);
if (normalizedAlias) {
maps.aliases.set(normalizedAlias, provider);
}
}
}
function buildProviderMaps(cfg?: OpenClawConfig): {
@@ -43,29 +64,16 @@ function buildProviderMaps(cfg?: OpenClawConfig): {
} {
const canonical = new Map<string, SpeechProviderPlugin>();
const aliases = new Map<string, SpeechProviderPlugin>();
const register = (provider: SpeechProviderPlugin) => {
const id = normalizeSpeechProviderId(provider.id);
if (!id) {
return;
}
canonical.set(id, provider);
aliases.set(id, provider);
for (const alias of provider.aliases ?? []) {
const normalizedAlias = normalizeSpeechProviderId(alias);
if (normalizedAlias) {
aliases.set(normalizedAlias, provider);
}
}
};
const maps = { canonical, aliases };
for (const buildProvider of BUILTIN_SPEECH_PROVIDER_BUILDERS) {
register(buildProvider());
registerSpeechProvider(maps, buildProvider());
}
for (const provider of resolveSpeechProviderPluginEntries(cfg)) {
register(provider);
registerSpeechProvider(maps, provider);
}
return { canonical, aliases };
return maps;
}
export function listSpeechProviders(cfg?: OpenClawConfig): SpeechProviderPlugin[] {
@@ -80,5 +88,10 @@ export function getSpeechProvider(
if (!normalized) {
return undefined;
}
const local = buildProviderMaps().aliases.get(normalized);
if (local || !cfg) {
return local;
}
return buildProviderMaps(cfg).aliases.get(normalized);
}