zeroclaw-labs/zeroclaw · error · SemanticEmptyTerminalCompletion

provider completed without final text or tool calls

Error message

provider completed without final text or tool calls

What it means

Inside reliable_terminal_error's semantic-empty branch: the optional rejected-attempt TokenUsage is wrapped into ReliableRejectedCompletionUsage::with_terminal_cause(usage, failures, SemanticEmptyTerminalCompletion). This keeps the 'provider completed without final text or tool calls' terminal cause attached to the usage record so rejected-turn cost accounting and SOP delivery classification both see the true cause.

Source

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

            .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)),
    }
}

fn reliable_terminal_error_with_cause(
    failures: FailureEvents,
    rejected_attempt_usage: Option<TokenUsage>,
    final_cause_is_semantic_empty: bool,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Inspect the wrapped usage: high completion_tokens with semantic-empty output confirms think-only truncation; raise max_tokens.
  2. Record the rejected usage via the cost ledger helpers (record_rejected_tool_loop_cost_usage) so reports reconcile.
  3. End the chain with a non-reasoning model to convert persistent semantic-empties into successes.
  4. Verify the terminal cause by downcasting to ReliableSemanticEmptyCompletion before deciding retry vs escalate.

Example fix

// before: treating the aggregate as opaque and retrying everything
if let Err(e) = reliable.chat(req, None).await { log::error!("{e}"); retry_all(); }

// after: classify by terminal cause and account rejected usage
match reliable.chat(req, None).await {
    Err(e) if e.downcast_ref::<ReliableSemanticEmptyCompletion>().is_some() => {
        cost::record_rejected_usage(&e); increase_budget_or_switch_model();
    }
    other => other?,
}
Defensive patterns

Strategy: fallback

Validate before calling

fn rejected_usage_of(e: &anyhow::Error) -> Option<&ReliableRejectedCompletionUsage> {
    e.downcast_ref::<ReliableSemanticEmptyCompletion>()?.rejected_usage.as_ref()
}

Type guard

fn is_semantic_empty_chain_failure(e: &anyhow::Error) -> bool {
    e.downcast_ref::<ReliableSemanticEmptyCompletion>().is_some()
}

Try / catch

match reliable.chat(request, None).await {
    Err(e) if is_semantic_empty_chain_failure(&e) => {
        cost_ledger::record_rejected(e.downcast_ref::<ReliableSemanticEmptyCompletion>().unwrap());
        retry_with_higher_budget().await
    }
    other => other,
}

Prevention

When it happens

Trigger: The reliable chain exhausted with a semantic-empty final cause AND the failed attempt reported token usage; the wrapper is constructed so downstream cost accounting attributes the spent tokens to a rejected attempt rather than an accepted turn.

Common situations: Reasoning models that burn tokens thinking then produce nothing billable-looking, budget dashboards showing unexplained spend, retries billed on failed attempts across the chain.

Related errors


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