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
- Set the join on the agent: agent.<alias>.tts_provider = "edge.main" (family.alias), matching an existing [providers.tts.edge.main] entry.
- Verify the agent alias the channel binds to actually exists under [agents.*] — a typo resolves to empty and lands here.
- Restart the runtime after the config change; TtsManager resolves the value once at construction.
- 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
- Always pair a [providers.tts.<family>.<alias>] entry with agent.<alias>.tts_provider = "<family>.<alias>"; the provider list alone enables nothing.
- Watch startup logs for "Skipping TTS provider" — a provider that failed validation never registers, so the dotted join would miss even when written.
- Include the tts_provider join in config template/lint checks so new agents cannot ship without it.
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
- Agent has no transcription_provider configured. Set `agent.<
- OpenAI TTS API error ({}): {}
- Edge TTS binary_path must be a bare command name without pat
- Edge TTS binary_path must be one of {:?}, got: {raw_path}
- agents.{agent_alias}.risk_profile is empty
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/0fa14c85dba3c608.
Report an issue: GitHub.