From 1d74ecd71f0facc2cf5a106f23f8b142c2815522 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 30 Apr 2026 14:35:00 -0700 Subject: [PATCH] fix(plugins): restore disabled TTS provider fallback --- CHANGELOG.md | 1 + .../capability-provider-runtime.test.ts | 158 +++++++++++++++--- src/plugins/capability-provider-runtime.ts | 12 -- 3 files changed, 140 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89a11c984c..62f3eee50f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index ca1fb774ca6..1b7af5bacc3 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -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, + }); }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index c03b9f0fcc4..96a5cff82a6 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -101,10 +101,6 @@ function createCapabilityProviderFallbackLoadOptions(params: { return loadOptions; } -function arePluginsGloballyDisabled(cfg: OpenClawConfig | undefined): boolean { - return cfg?.plugins?.enabled === false; -} - function findProviderById( entries: PluginRegistry[K], providerId: string, @@ -225,10 +221,6 @@ export function resolvePluginCapabilityProvider | 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[] { - if (arePluginsGloballyDisabled(params.cfg)) { - return []; - } - const activeRegistry = resolveRuntimePluginRegistry(); const activeProviders = activeRegistry?.[params.key] ?? []; if (