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
- Inspect the wrapped usage: high completion_tokens with semantic-empty output confirms think-only truncation; raise max_tokens.
- Record the rejected usage via the cost ledger helpers (record_rejected_tool_loop_cost_usage) so reports reconcile.
- End the chain with a non-reasoning model to convert persistent semantic-empties into successes.
- 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
- Wire rejected-usage extraction into cost dashboards so failed attempts are visible as spend.
- High completion tokens plus semantic-empty means truncation: fix budgets, do not add retries.
- Keep the typed error in logs (downcast before formatting) so classification survives aggregation.
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
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
- provider completed without final text or tool calls
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6cc8b6fb34df355d.
Report an issue: GitHub.