zeroclaw-labs/zeroclaw · error · ReliableSemanticEmptyCompletion

All model providers/models failed after {} failure event(s).

Error message

All model providers/models failed after {} failure event(s). Events:

What it means

reliable_terminal_error builds the final error after the reliable provider chain is exhausted. Its Display (via failure_aggregate) starts with 'All model providers/models failed after N failure event(s). Events:'. When the last failure is semantically empty, it returns the typed ReliableSemanticEmptyCompletion carrying the full failure events, optional rejected-attempt usage wrapped with the SemanticEmptyTerminalCompletion terminal cause, so cost accounting and delivery layers can classify the run without guessing.

Source

Thrown at crates/zeroclaw-providers/src/reliable.rs:1287

impl std::error::Error for ReliableSemanticEmptyCompletion {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.rejected_usage
            .as_ref()
            .map(|usage| usage as &(dyn std::error::Error + 'static))
            .or(Some(&self.terminal_cause))
    }
}

fn reliable_terminal_error(
    failures: FailureEvents,
    rejected_attempt_usage: Option<TokenUsage>,
    final_cause_is_semantic_empty: bool,
) -> anyhow::Error {
    let rejected_attempt_usage = rejected_attempt_usage.or_else(accounted_rejected_attempt_usage);
    if final_cause_is_semantic_empty {
        let terminal_cause = zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion;
        return anyhow::Error::new(ReliableSemanticEmptyCompletion {
            failures: failures.clone(),
            rejected_usage: rejected_attempt_usage.map(|usage| {
                ReliableRejectedCompletionUsage::with_terminal_cause(
                    usage,
                    failures,
                    anyhow::Error::new(
                        zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,
                    ),
                )
            }),
            terminal_cause,
        });
    }

    match rejected_attempt_usage {
        Some(usage) => anyhow::Error::new(ReliableRejectedCompletionUsage::new(usage, failures)),
        None => anyhow::Error::msg(failure_aggregate(&failures)),
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the Events list in the message: it enumerates per-provider/per-model failures; fix the dominant repeated cause first.
  2. If events show think-only outputs, raise max_tokens or swap in one non-reasoning model as the last chain entry.
  3. Add at least one provider on independent infrastructure so a single upstream outage cannot exhaust the chain.
  4. Use the rejected usage attached to ReliableRejectedCompletionUsage for cost reconciliation of failed attempts.

Example fix

# before: chain of reasoning models, all truncate inside thinking
providers = ["deepseek-r1", "qwen-thinking"]

# after: end the chain with a plain chat model and generous budgets
providers = ["deepseek-r1", "qwen-thinking", "gpt-4o-mini"]
[limits]
max_tokens = 4096
Defensive patterns

Strategy: fallback

Validate before calling

fn chain_is_cross_vendor(models: &[(&str, &str)]) -> bool {
    models.iter().map(|(p, _)| *p).collect::<std::collections::HashSet<_>>().len() > 1
}

Try / catch

match reliable.chat(request, None).await {
    Ok(resp) => Ok(resp),
    Err(e) => {
        if e.downcast_ref::<ReliableSemanticEmptyCompletion>().is_some() {
            log::warn!("chain exhausted with semantic-empty terminal: {e:#}");
        }
        escalate_to_operator(&e) // chain already retried everything; do not blind-retry
    }
}

Prevention

When it happens

Trigger: Every candidate provider/model in the reliable chain failed and the final candidate's failure was a semantic-empty completion (empty/think-only text with no tool calls); reliable_terminal_error is called with final_cause_is_semantic_empty = true and the aggregate plus typed wrapper is produced.

Common situations: A chain of reasoning models all hitting max_tokens inside thinking, one misconfigured base_url replicated across chain entries, rate limits knocking out the first providers and a reasoning model failing last, all chain entries pointing at the same broken upstream.

Related errors


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