fix(plugins): restore disabled TTS provider fallback

This commit is contained in:
Vincent Koc
2026-04-30 14:35:00 -07:00
parent aa9db998f7
commit 1d74ecd71f
3 changed files with 140 additions and 31 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- TTS/providers: keep bundled speech-provider compat fallback available when plugins are globally disabled, so cold gateway and CLI startup can still resolve fallback speech providers instead of leaving explicit TTS provider selection with no registered providers. Thanks @vincentkoc.
- Discord: collapse repeated native slash-command deploy rate-limit startup logs into one non-fatal warning while keeping per-request REST timing in verbose output. Thanks @discord.
- Providers/OpenAI Codex: preserve existing wrapped Codex streams during OpenAI attribution so PI OAuth bearer injection reaches ChatGPT/Codex Responses, and strip native Codex-only unsupported payload fields without touching custom compatible endpoints. (#75111) Thanks @keshavbotagent.
- Agents/tool-result guard: use the resolved runtime context token budget for non-context-engine tool-result overflow checks, so long tool-heavy sessions no longer compact early when `contextTokens` is larger than native `contextWindow`. Fixes #74917. Thanks @kAIborg24.

View File

@@ -657,10 +657,10 @@ describe("resolvePluginCapabilityProviders", () => {
});
});
it("does not load bundled capability providers when plugins are globally disabled", () => {
it("uses active capability providers when plugins are globally disabled", () => {
const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig;
const loaded = createEmptyPluginRegistry();
loaded.mediaUnderstandingProviders.push({
const active = createEmptyPluginRegistry();
active.mediaUnderstandingProviders.push({
pluginId: "openai",
pluginName: "openai",
source: "test",
@@ -669,20 +669,99 @@ describe("resolvePluginCapabilityProviders", () => {
capabilities: ["image"],
},
} as never);
mocks.resolveRuntimePluginRegistry.mockReturnValue(loaded);
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
expectNoResolvedCapabilityProviders(
resolvePluginCapabilityProviders({
key: "mediaUnderstandingProviders",
cfg,
}),
);
const providers = resolvePluginCapabilityProviders({
key: "mediaUnderstandingProviders",
cfg,
});
expectResolvedCapabilityProviderIds(providers, ["openai"]);
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
expect(mocks.withBundledPluginAllowlistCompat).not.toHaveBeenCalled();
expect(mocks.withBundledPluginEnablementCompat).not.toHaveBeenCalled();
expect(mocks.withBundledPluginVitestCompat).not.toHaveBeenCalled();
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
});
it("loads bundled speech providers through compat when plugins are globally disabled", () => {
const cfg = {
plugins: { enabled: false },
messages: { tts: { provider: "mistral" } },
} as OpenClawConfig;
const allowlistCompat = {
...cfg,
plugins: {
enabled: false,
allow: ["microsoft"],
},
} as OpenClawConfig;
const compatConfig = {
...cfg,
plugins: {
enabled: true,
allow: ["microsoft"],
entries: { microsoft: { enabled: true } },
},
} as OpenClawConfig;
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.withBundledPluginEnablementCompat.mockReturnValue(compatConfig);
mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig);
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
params === undefined ? undefined : loaded,
);
const providers = resolvePluginCapabilityProviders({
key: "speechProviders",
cfg,
});
expectResolvedCapabilityProviderIds(providers, ["microsoft"]);
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
config: cfg,
env: process.env,
});
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
config: cfg,
pluginIds: ["microsoft"],
});
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
config: allowlistCompat,
pluginIds: ["microsoft"],
});
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: compatConfig,
onlyPluginIds: ["microsoft"],
activate: false,
});
});
it.each([
@@ -845,8 +924,21 @@ describe("resolvePluginCapabilityProviders", () => {
});
});
it("does not load targeted bundled capability providers when plugins are globally disabled", () => {
it("loads targeted bundled capability providers through compat when plugins are globally disabled", () => {
const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig;
const allowlistCompat = {
plugins: {
enabled: false,
allow: ["custom-plugin", "google"],
},
} as OpenClawConfig;
const enablementCompat = {
plugins: {
enabled: true,
allow: ["custom-plugin", "google"],
entries: { google: { enabled: true } },
},
};
const loaded = createEmptyPluginRegistry();
loaded.memoryEmbeddingProviders.push({
pluginId: "google",
@@ -857,7 +949,26 @@ describe("resolvePluginCapabilityProviders", () => {
create: async () => ({ provider: null }),
},
} as never);
mocks.resolveRuntimePluginRegistry.mockReturnValue(loaded);
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",
@@ -865,11 +976,20 @@ describe("resolvePluginCapabilityProviders", () => {
cfg,
});
expect(provider).toBeUndefined();
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
expect(mocks.withBundledPluginAllowlistCompat).not.toHaveBeenCalled();
expect(mocks.withBundledPluginEnablementCompat).not.toHaveBeenCalled();
expect(mocks.withBundledPluginVitestCompat).not.toHaveBeenCalled();
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
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();
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: enablementCompat,
onlyPluginIds: ["google"],
activate: false,
});
});
});

View File

@@ -101,10 +101,6 @@ function createCapabilityProviderFallbackLoadOptions(params: {
return loadOptions;
}
function arePluginsGloballyDisabled(cfg: OpenClawConfig | undefined): boolean {
return cfg?.plugins?.enabled === false;
}
function findProviderById<K extends CapabilityProviderRegistryKey>(
entries: PluginRegistry[K],
providerId: string,
@@ -225,10 +221,6 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
cfg?: OpenClawConfig;
installBundledRuntimeDeps?: boolean;
}): CapabilityProviderForKey<K> | undefined {
if (arePluginsGloballyDisabled(params.cfg)) {
return undefined;
}
const activeRegistry = resolveRuntimePluginRegistry();
const activeProvider = findProviderById(activeRegistry?.[params.key] ?? [], params.providerId);
if (activeProvider) {
@@ -263,10 +255,6 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
cfg?: OpenClawConfig;
installBundledRuntimeDeps?: boolean;
}): CapabilityProviderForKey<K>[] {
if (arePluginsGloballyDisabled(params.cfg)) {
return [];
}
const activeRegistry = resolveRuntimePluginRegistry();
const activeProviders = activeRegistry?.[params.key] ?? [];
if (