{"record":{"id":"6cc8b6fb34df355d","repo":"zeroclaw-labs/zeroclaw","slug":"provider-completed-without-final-text-or-tool-call-6cc8b6","errorCode":null,"errorMessage":"provider completed without final text or tool calls","messagePattern":"provider completed without final text or tool calls","errorType":"exception","errorClass":"SemanticEmptyTerminalCompletion","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/reliable.rs","lineNumber":1293,"sourceCode":"            .or(Some(&self.terminal_cause))\n    }\n}\n\nfn reliable_terminal_error(\n    failures: FailureEvents,\n    rejected_attempt_usage: Option<TokenUsage>,\n    final_cause_is_semantic_empty: bool,\n) -> anyhow::Error {\n    let rejected_attempt_usage = rejected_attempt_usage.or_else(accounted_rejected_attempt_usage);\n    if final_cause_is_semantic_empty {\n        let terminal_cause = zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion;\n        return anyhow::Error::new(ReliableSemanticEmptyCompletion {\n            failures: failures.clone(),\n            rejected_usage: rejected_attempt_usage.map(|usage| {\n                ReliableRejectedCompletionUsage::with_terminal_cause(\n                    usage,\n                    failures,\n                    anyhow::Error::new(\n                        zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,\n                    ),\n                )\n            }),\n            terminal_cause,\n        });\n    }\n\n    match rejected_attempt_usage {\n        Some(usage) => anyhow::Error::new(ReliableRejectedCompletionUsage::new(usage, failures)),\n        None => anyhow::Error::msg(failure_aggregate(&failures)),\n    }\n}\n\nfn reliable_terminal_error_with_cause(\n    failures: FailureEvents,\n    rejected_attempt_usage: Option<TokenUsage>,\n    final_cause_is_semantic_empty: bool,","sourceCodeStart":1275,"sourceCodeEnd":1311,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/reliable.rs#L1275-L1311","documentation":"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.","triggerScenarios":"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.","commonSituations":"Reasoning models that burn tokens thinking then produce nothing billable-looking, budget dashboards showing unexplained spend, retries billed on failed attempts across the chain.","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."],"exampleFix":"// before: treating the aggregate as opaque and retrying everything\nif let Err(e) = reliable.chat(req, None).await { log::error!(\"{e}\"); retry_all(); }\n\n// after: classify by terminal cause and account rejected usage\nmatch reliable.chat(req, None).await {\n    Err(e) if e.downcast_ref::<ReliableSemanticEmptyCompletion>().is_some() => {\n        cost::record_rejected_usage(&e); increase_budget_or_switch_model();\n    }\n    other => other?,\n}","handlingStrategy":"fallback","validationCode":"fn rejected_usage_of(e: &anyhow::Error) -> Option<&ReliableRejectedCompletionUsage> {\n    e.downcast_ref::<ReliableSemanticEmptyCompletion>()?.rejected_usage.as_ref()\n}","typeGuard":"fn is_semantic_empty_chain_failure(e: &anyhow::Error) -> bool {\n    e.downcast_ref::<ReliableSemanticEmptyCompletion>().is_some()\n}","tryCatchPattern":"match reliable.chat(request, None).await {\n    Err(e) if is_semantic_empty_chain_failure(&e) => {\n        cost_ledger::record_rejected(e.downcast_ref::<ReliableSemanticEmptyCompletion>().unwrap());\n        retry_with_higher_budget().await\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["llm","provider-chain","cost-accounting","empty-response"],"backgroundTag":"llm-empty-completion","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}