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

Default ModelProvider::chat implementation for providers without native tool calling: tool schemas are folded into the system prompt, a plain string completion is requested, and the wrapped ChatResponse must survive is_semantically_empty_terminal(). If the model returns nothing but whitespace or <think> reasoning, the typed SemanticEmptyTerminalCompletion error is raised so reliable/SOP delivery layers can distinguish 'model produced no usable output' from success.

Source

Thrown at crates/zeroclaw-api/src/model_provider.rs:681

                    system_message.content.push_str("\n\n");
                }
                system_message.content.push_str(&tool_instructions);
            } else {
                modified_messages.insert(0, ChatMessage::system(tool_instructions));
            }

            let text = self
                .chat_with_history(&modified_messages, model, temperature)
                .await?;
            let response = ChatResponse {
                text: Some(text),
                tool_calls: Vec::new(),
                usage: None,
                reasoning_content: None,
            };
            return (!response.is_semantically_empty_terminal())
                .then_some(response)
                .ok_or_else(|| anyhow::Error::new(SemanticEmptyTerminalCompletion));
        }

        let text = self
            .chat_with_history(request.messages, model, temperature)
            .await?;
        let response = ChatResponse {
            text: Some(text),
            tool_calls: Vec::new(),
            usage: None,
            reasoning_content: None,
        };
        (!response.is_semantically_empty_terminal())
            .then_some(response)
            .ok_or_else(|| anyhow::Error::new(SemanticEmptyTerminalCompletion))
    }

    /// Whether model_provider supports native tool calls over API.
    fn supports_native_tools(&self) -> bool {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Inspect the raw response of the failing model directly; if it is think-only, raise max_tokens or switch to a non-reasoning model for tool-using turns.
  2. Use a provider/endpoint that supports native tool calling so tool-call-only turns are valid instead of falling into the prompt-guided string path.
  3. Put the model behind the reliable provider chain so a semantic-empty on one model falls through to the next candidate.
  4. Check the provider capabilities configuration; a wrong capabilities flag can route a native-tools provider into this degraded path.

Example fix

// before: reasoning model on the prompt-guided string path emits only <think>...</think>
let resp = provider.chat(ChatRequest { messages, tools: Some(&tools), .. }, model, None).await?;

// after: chain a non-reasoning fallback via the reliable provider
let resp = reliable.chat(request, None).await // falls through on SemanticEmptyTerminalCompletion
    .or_else(|e| if e.is::<SemanticEmptyTerminalCompletion>() { retry_with_plain_model() } else { Err(e) })?;
Defensive patterns

Strategy: fallback

Validate before calling

fn is_semantically_empty(text: &str) -> bool {
    zeroclaw_api::model_provider::strip_think_tags(text).trim().is_empty()
}

Try / catch

match provider.chat(request, model, temperature).await {
    Ok(resp) => Ok(resp),
    Err(e) if e.is::<zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion>() => {
        fallback_provider().chat(request, fallback_model, temperature).await // typed terminal -> next candidate
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Calling chat() with a non-empty tools slice on a provider whose capabilities().native_tool_calling is false, where the string response strips to empty: reasoning models emitting only think tags, max_tokens exhausted before final text, or an upstream returning empty content with a success status.

Common situations: Pointing a prompt-guided-tools provider at a reasoning model (DeepSeek-R1 style), low max_tokens cutting output mid-reasoning, an OpenAI-compatible proxy stripping content blocks, model name typos that resolve to a reasoning-only variant.

Related errors


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