openclaw - ✅(Solved) Fix TTS: microsoft Edge TTS provider never discovered when another speech provider (e.g. openai) registers at startup [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#66850Fetched 2026-04-15 06:24:06
View on GitHub
Comments
1
Participants
1
Timeline
3
Reactions
0
Participants
Timeline (top)
commented ×1cross-referenced ×1referenced ×1

When the openai plugin is enabled (e.g. for LLM), its speech provider registers itself in the runtime plugin registry as a side-effect of startup. resolvePluginCapabilityProviders in capability-provider-runtime.ts short-circuits with if (activeProviders.length > 0) return activeProviders — before running the compat path that enumerates allowlisted bundled providers. As a result, only openai TTS is ever available; bundled speech providers like microsoft Edge TTS are never discovered regardless of plugins.allow.

Error Message

  1. Send a message — TTS fails with microsoft: no provider registered; openai: OpenAI TTS API error (401)

Root Cause

src/plugins/capability-provider-runtime.tsresolvePluginCapabilityProviders:

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    if (activeProviders.length > 0) return activeProviders.map((entry) => entry.provider);
    // compat path (enumerates allowlisted bundled providers) — NEVER REACHED
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    return (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
}

The early return prevents the compat path from running whenever any provider has self-registered (even if that provider is not the one actually configured).

Fix Action

Fix

Skip the early return when a cfg is provided (i.e. when called for specific capability resolution). The compat path correctly enumerates all allowlisted bundled providers and should take precedence:

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    // Only short-circuit when no cfg is available (no allowlist to apply)
    if (activeProviders.length > 0 && !params.cfg) return activeProviders.map((entry) => entry.provider);
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    const compatProviders = (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
    return compatProviders.length > 0 ? compatProviders : activeProviders.map((entry) => entry.provider);
}

With this fix, listSpeechProviders(cfg) correctly returns ["elevenlabs","microsoft","minimax","openai","vydra"] and microsoft Edge TTS synthesizes successfully.

PR fix notes

PR #66855: fix: discover allowlisted bundled TTS providers when active registry has self-registered providers

Description (problem / solution / changelog)

Fixes #66850

Problem

When the openai plugin is enabled for LLM completions, it also registers its speech provider in the main runtime plugin registry as a side-effect of startup. resolvePluginCapabilityProviders short-circuits on the first call:

const activeProviders = activeRegistry?.[params.key] ?? [];
if (activeProviders.length > 0) {
  return activeProviders.map((entry) => entry.provider);  // ← compat path never reached
}

This prevents the compat path from running — the path that enumerates all bundled providers in plugins.allow. As a result, allowlisted bundled speech providers like microsoft Edge TTS are never discovered, causing:

TTS conversion failed: microsoft: no provider registered; openai: OpenAI TTS API error (401)

...even when "microsoft" is explicitly listed in plugins.allow and messages.tts.provider is set to "edge".

Fix

The compat path is now triggered when the active registry is non-empty and cfg carries an explicit plugins.allow list. That allow list is the signal that the caller wants a specific set of bundled providers to be available. Without an explicit allow list, withBundledPluginAllowlistCompat would add every bundled TTS provider to the set, injecting providers the user never configured (e.g. elevenlabs) into the fallback chain.

When the compat path does run, results are merged with the active registry rather than replacing it, so runtime-registered providers from workspace plugins that are not in the bundled manifest are preserved.

activeProviders non-empty + no plugins.allow  →  active registry as-is (original behaviour)
activeProviders non-empty + plugins.allow set →  compat path + merge with active-only providers
activeProviders empty                         →  compat path (original fallback, unchanged)

Tests

  • Regression test: openai self-registered in main registry + microsoft in plugins.allow → both returned via compat merge
  • Workspace-plugin test: runtime-registered custom-tts not in bundled manifest → preserved in merge
  • All existing tests pass unchanged, including the contract tests that use cfg without plugins.allow

Changed files

  • src/plugins/capability-provider-runtime.test.ts (modified, +122/-0)
  • src/plugins/capability-provider-runtime.ts (modified, +34/-3)

Code Example

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    if (activeProviders.length > 0) return activeProviders.map((entry) => entry.provider);
    // compat path (enumerates allowlisted bundled providers) — NEVER REACHED
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    return (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
}

---

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    // Only short-circuit when no cfg is available (no allowlist to apply)
    if (activeProviders.length > 0 && !params.cfg) return activeProviders.map((entry) => entry.provider);
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    const compatProviders = (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
    return compatProviders.length > 0 ? compatProviders : activeProviders.map((entry) => entry.provider);
}
RAW_BUFFERClick to expand / collapse

Summary

When the openai plugin is enabled (e.g. for LLM), its speech provider registers itself in the runtime plugin registry as a side-effect of startup. resolvePluginCapabilityProviders in capability-provider-runtime.ts short-circuits with if (activeProviders.length > 0) return activeProviders — before running the compat path that enumerates allowlisted bundled providers. As a result, only openai TTS is ever available; bundled speech providers like microsoft Edge TTS are never discovered regardless of plugins.allow.

Steps to reproduce

  1. Add "openai" and "microsoft" to plugins.allow in openclaw.json
  2. Set messages.tts.provider: "edge" (which normalizes to "microsoft")
  3. Send a message — TTS fails with microsoft: no provider registered; openai: OpenAI TTS API error (401)

Root cause

src/plugins/capability-provider-runtime.tsresolvePluginCapabilityProviders:

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    if (activeProviders.length > 0) return activeProviders.map((entry) => entry.provider);
    // compat path (enumerates allowlisted bundled providers) — NEVER REACHED
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    return (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
}

The early return prevents the compat path from running whenever any provider has self-registered (even if that provider is not the one actually configured).

Fix

Skip the early return when a cfg is provided (i.e. when called for specific capability resolution). The compat path correctly enumerates all allowlisted bundled providers and should take precedence:

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    // Only short-circuit when no cfg is available (no allowlist to apply)
    if (activeProviders.length > 0 && !params.cfg) return activeProviders.map((entry) => entry.provider);
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    const compatProviders = (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
    return compatProviders.length > 0 ? compatProviders : activeProviders.map((entry) => entry.provider);
}

With this fix, listSpeechProviders(cfg) correctly returns ["elevenlabs","microsoft","minimax","openai","vydra"] and microsoft Edge TTS synthesizes successfully.

Additional required change

"microsoft" must be in plugins.allow for the compat path to include it. This should probably be documented or defaulted.

Environment

  • OpenClaw v2026.4.14
  • plugins.allow includes "openai" and "microsoft"
  • messages.tts.provider: "edge"
  • Telegram channel

extent analysis

TL;DR

Modify the resolvePluginCapabilityProviders function to skip the early return when a configuration is provided, allowing the compat path to enumerate all allowlisted bundled providers.

Guidance

  • Update the resolvePluginCapabilityProviders function as shown in the proposed fix to ensure that the compat path is executed when a configuration is provided.
  • Verify that the plugins.allow list includes the desired providers, such as "microsoft", to ensure they are included in the compat path.
  • Test the speech providers using the listSpeechProviders(cfg) function to confirm that all expected providers are returned.
  • Consider documenting or defaulting the required providers in plugins.allow to avoid similar issues in the future.

Example

function resolvePluginCapabilityProviders(params) {
    const activeProviders = resolveRuntimePluginRegistry()?.[params.key] ?? [];
    if (activeProviders.length > 0 && !params.cfg) return activeProviders.map((entry) => entry.provider);
    const compatConfig = resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg });
    const compatProviders = (resolveRuntimePluginRegistry(compatConfig ? { config: compatConfig } : undefined)?.[params.key] ?? [])
        .map((entry) => entry.provider);
    return compatProviders.length > 0 ? compatProviders : activeProviders.map((entry) => entry.provider);
}

Notes

The proposed fix assumes that the resolveCapabilityProviderConfig function correctly resolves the configuration for the given key and parameters. Additionally, the plugins.allow list must include the desired providers for them to be included in the compat path.

Recommendation

Apply the proposed workaround by modifying the resolvePluginCapabilityProviders function to skip the early return when a configuration is provided. This will allow the compat path to enumerate all allowlisted bundled providers, ensuring that the desired speech providers are available.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix TTS: microsoft Edge TTS provider never discovered when another speech provider (e.g. openai) registers at startup [1 pull requests, 1 comments, 1 participants]