fix(tts): merge allowlisted speech providers

This commit is contained in:
Peter Steinberger
2026-04-25 04:19:57 +01:00
parent cf07f01d0d
commit 73a6a2a6ab
4 changed files with 106 additions and 20 deletions

View File

@@ -203,7 +203,7 @@ describe("resolvePluginCapabilityProviders", () => {
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
});
it("keeps active capability providers even when cfg is passed", () => {
it("keeps active capability providers when cfg compat has no extra providers", () => {
const active = createEmptyPluginRegistry();
active.speechProviders.push({
pluginId: "microsoft",
@@ -233,11 +233,80 @@ describe("resolvePluginCapabilityProviders", () => {
expectResolvedCapabilityProviderIds(providers, ["microsoft"]);
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalledWith({
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: expect.anything(),
});
});
it("merges active and allowlisted bundled capability providers when cfg is passed", () => {
const active = createEmptyPluginRegistry();
active.speechProviders.push({
pluginId: "openai",
pluginName: "openai",
source: "test",
provider: {
id: "openai",
label: "openai",
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.from("x"),
outputFormat: "mp3",
voiceCompatible: false,
fileExtension: ".mp3",
}),
},
} as never);
const loaded = createEmptyPluginRegistry();
loaded.speechProviders.push({
pluginId: "microsoft",
pluginName: "microsoft",
source: "test",
provider: {
id: "microsoft",
label: "microsoft",
aliases: ["edge"],
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.from("x"),
outputFormat: "mp3",
voiceCompatible: false,
fileExtension: ".mp3",
}),
},
} as never);
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "microsoft",
origin: "bundled",
contracts: { speechProviders: ["microsoft"] },
},
] as never,
diagnostics: [],
});
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
params === undefined ? active : loaded,
);
const providers = resolvePluginCapabilityProviders({
key: "speechProviders",
cfg: {
plugins: { allow: ["openai", "microsoft"] },
messages: { tts: { provider: "edge" } },
} as OpenClawConfig,
});
expectResolvedCapabilityProviderIds(providers, ["openai", "microsoft"]);
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["openai", "microsoft"],
}),
}),
});
});
it.each([
["memoryEmbeddingProviders", "memoryEmbeddingProviders"],
["speechProviders", "speechProviders"],

View File

@@ -98,6 +98,30 @@ function findProviderById<K extends CapabilityProviderRegistryKey>(
return undefined;
}
function mergeCapabilityProviders<K extends CapabilityProviderRegistryKey>(
left: PluginRegistry[K],
right: PluginRegistry[K],
): CapabilityProviderForKey<K>[] {
const merged = new Map<string, CapabilityProviderForKey<K>>();
const unnamed: CapabilityProviderForKey<K>[] = [];
const addEntries = (entries: PluginRegistry[K]) => {
for (const entry of entries) {
const provider = entry.provider as CapabilityProviderForKey<K> & { id?: string };
if (!provider.id) {
unnamed.push(provider);
continue;
}
if (!merged.has(provider.id)) {
merged.set(provider.id, provider);
}
}
};
addEntries(left);
addEntries(right);
return [...merged.values(), ...unnamed];
}
export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegistryKey>(params: {
key: K;
providerId: string;
@@ -134,29 +158,15 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
}): CapabilityProviderForKey<K>[] {
const activeRegistry = resolveRuntimePluginRegistry();
const activeProviders = activeRegistry?.[params.key] ?? [];
if (activeProviders.length > 0 && params.key !== "memoryEmbeddingProviders") {
if (activeProviders.length > 0 && params.key !== "memoryEmbeddingProviders" && !params.cfg) {
return activeProviders.map((entry) => entry.provider) as CapabilityProviderForKey<K>[];
}
const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
const loadOptions = compatConfig === undefined ? undefined : { config: compatConfig };
const registry = resolveRuntimePluginRegistry(loadOptions);
const loadedProviders = registry?.[params.key] ?? [];
if (params.key !== "memoryEmbeddingProviders") {
return (registry?.[params.key] ?? []).map(
(entry) => entry.provider,
) as CapabilityProviderForKey<K>[];
return mergeCapabilityProviders(activeProviders, loadedProviders);
}
const merged = new Map<string, CapabilityProviderForKey<K>>();
for (const entry of activeProviders) {
const provider = entry.provider as CapabilityProviderForKey<K> & { id?: string };
if (provider.id) {
merged.set(provider.id, provider);
}
}
for (const entry of registry?.[params.key] ?? []) {
const provider = entry.provider as CapabilityProviderForKey<K> & { id?: string };
if (provider.id && !merged.has(provider.id)) {
merged.set(provider.id, provider);
}
}
return [...merged.values()];
return mergeCapabilityProviders(activeProviders, loadedProviders);
}