zeroclaw-labs/zeroclaw · error · anyhow::Error

Agent has no tts_provider configured. Set `agent.<alias>.tts

Error message

Agent has no tts_provider configured. Set `agent.<alias>.tts_provider = "<type>.<alias>"` referencing a [providers.tts.<type>.<alias>] entry.

What it means

TtsManager resolves the active agent's tts_provider (channel-owning agent, else the runtime-active agent) at construction; an empty resolved value means no agent binding or an explicit opt-out. synthesize fails loud with this message instead of silently guessing a provider, and the message spells out the exact config join: agent.<alias>.tts_provider = "<type>.<alias>" referencing a [providers.tts.<type>.<alias>] entry.

Source

Thrown at crates/zeroclaw-channels/src/tts.rs:1089

    pub async fn synthesize_opus(&self, text: &str) -> Result<Vec<u8>> {
        let audio = self.synthesize(text).await?;
        let provider_alias = self.agent_tts_provider.as_str();
        let format = self
            .tts_providers
            .get(provider_alias)
            .map(|p| p.output_format())
            .unwrap_or("unknown");
        if format == "opus" {
            return Ok(audio);
        }
        transcode_to_opus(audio).await
    }

    pub async fn synthesize(&self, text: &str) -> Result<Vec<u8>> {
        let provider_alias = self.agent_tts_provider.as_str();
        if provider_alias.is_empty() {
            bail!(
                "Agent has no tts_provider configured. Set \
                 `agent.<alias>.tts_provider = \"<type>.<alias>\"` referencing a \
                 [providers.tts.<type>.<alias>] entry."
            );
        }
        let voice = self
            .voice_by_alias
            .get(provider_alias)
            .map_or(self.default_voice.as_str(), String::as_str);
        self.synthesize_with_provider(text, provider_alias, voice)
            .await
    }

    /// Synthesize text using the runtime-active agent's resolved
    /// `tts_provider` reference and an explicit voice.
    pub async fn synthesize_with_voice(&self, text: &str, voice: &str) -> Result<Vec<u8>> {
        let provider_alias = self.agent_tts_provider.as_str();
        if provider_alias.is_empty() {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set the join on the agent: agent.<alias>.tts_provider = "edge.main" (family.alias), matching an existing [providers.tts.edge.main] entry.
  2. Verify the agent alias the channel binds to actually exists under [agents.*] — a typo resolves to empty and lands here.
  3. Restart the runtime after the config change; TtsManager resolves the value once at construction.
  4. Check the runtime logs for "Skipping TTS provider" warnings — a provider that failed validation (e.g. missing api_key) is not registered, so the dotted alias would also miss.

Example fix

# before — provider defined, agent join missing
[providers.tts.edge.main]
voice = "en-US-AriaNeural"

[agents.butler]
model_provider = "openai.main"

# after
[providers.tts.edge.main]
voice = "en-US-AriaNeural"

[agents.butler]
model_provider = "openai.main"
tts_provider = "edge.main"
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast at startup instead of at first synthesis.
fn assert_agent_tts_join(config: &Config, agent_alias: &str) -> anyhow::Result<()> {
    let agent = config
        .agents
        .get(agent_alias)
        .ok_or_else(|| anyhow::anyhow!("agent [{agent_alias}] not defined"))?;
    anyhow::ensure!(
        !agent.tts_provider.trim().is_empty(),
        "set [{agent_alias}] tts_provider = \"<family>.<alias>\""
    );
    Ok(())
}

Prevention

When it happens

Trigger: The agent block exists but tts_provider is unset/empty; the channel is built with an agent alias that does not exist in [agents.*] so the lookup falls through to empty; TtsManager::from_config was used with no runtime-active agent configured.

Common situations: A fresh install defines [providers.tts.*] but forgets the per-agent join, assuming the provider list alone enables TTS. Renaming an agent without updating the channel's owning-agent alias also yields an empty resolution. Note the dotted value must be family.alias (e.g. "edge.main"), not just the family.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/0fa14c85dba3c608. Report an issue: GitHub.