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

Anthropic provider: when a native structured completion is projected into the legacy string-only API, require_terminal_text checks is_semantically_empty_terminal() first and raises the shared typed SemanticEmptyTerminalCompletion (so delivery boundaries keep the terminal cause). If the response has text = None at all, it also logs an ERROR before erroring. Typical cause: extended thinking consumed the budget, so no final answer block was produced.

Source

Thrown at crates/zeroclaw-providers/src/anthropic.rs:1855

                        "stop_reason": stop_reason,
                        "content_block_count": content_block_count,
                        "content_block_types": content_block_types,
                        "native_tool_call_count": parsed.tool_calls.len(),
                        "has_reasoning": parsed.reasoning_content.is_some(),
                    })),
                "Anthropic response completed without final text or tool calls"
            );
        }

        parsed
    }

    /// Project a native completion into the legacy string-only API without
    /// erasing the terminal semantic cause required by Reliable and SOP
    /// delivery boundaries.
    fn require_terminal_text(parsed: ProviderChatResponse) -> anyhow::Result<String> {
        if parsed.is_semantically_empty_terminal() {
            return Err(anyhow::Error::new(
                zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,
            ));
        }
        parsed.text.ok_or_else(|| {
            ::zeroclaw_log::record!(
                ERROR,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure),
                "anthropic: empty text in response"
            );
            anyhow::Error::msg("No response from Anthropic")
        })
    }

    /// Resolve thinking parameters for an API request. Returns the effective
    /// temperature (forced to 1.0 when thinking is active), the thinking
    /// config for the request body, and the effective max_tokens (raised to
    /// meet budget_tokens minimum when needed).

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Raise max_tokens so thinking completes and a final text block is produced.
  2. Call the structured chat API (chat/chat_with_tools) instead of the string projection when tool_use blocks are expected; native tool calls are a valid terminal there.
  3. Disable forced thinking for string-only callers.
  4. Chain a fallback model via the reliable provider to absorb semantic-empty terminals.

Example fix

// before
let text = anthropic.chat_with_system(Some(sys), prompt, "claude-3-7", None).await?;
// model thinks to the token limit -> SemanticEmptyTerminalCompletion

// after
let text = anthropic.chat_with_system_opts(Some(sys), prompt, model, None, ChatOptions { max_tokens: 8192, thinking: Thinking::Off, ..Default::default() }).await?;
Defensive patterns

Strategy: fallback

Validate before calling

fn has_final_text(resp: &ProviderChatResponse) -> bool {
    !resp.is_semantically_empty_terminal() && resp.text.as_deref().map(|t| !t.trim().is_empty()).unwrap_or(false)
}

Try / catch

match anthropic.chat_with_system(system, message, model, temp).await {
    Ok(text) => Ok(text),
    Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => non_reasoning_provider().chat_with_system(system, message, model2, temp).await,
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: chat_with_system (string API) on the Anthropic provider returns a completion whose content blocks are only thinking blocks, or empty, or stop_reason 'max_tokens' fires inside the thinking phase: the projection to String finds no final text and raises the typed error.

Common situations: Extended-thinking models with max_tokens set too low, thinking forced via API options while the caller expects plain text, beta header/feature mismatch returning empty content, Claude models answering entirely in tool_use blocks when the string API was called without tools.

Related errors


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