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
OpenAI-compatible provider's require_terminal_text guard: identical contract to the Azure variant. String-only completions strip think tags; an empty remainder is the typed SemanticEmptyTerminalCompletion, because callers of the string API cannot accept a tool-call-only turn and an empty Ok would silently corrupt agent loops.
Source
Thrown at crates/zeroclaw-providers/src/openai.rs:81
#[derive(Debug, Deserialize)]
struct ResponseMessage {
#[serde(default)]
content: Option<String>,
}
impl ResponseMessage {
fn effective_content(&self) -> String {
self.content.clone().unwrap_or_default()
}
}
/// String-only completions have no native tool-call escape hatch. An empty or
/// reasoning-only result is therefore a typed terminal failure, not a valid
/// string result for direct callers that do not use the structured chat API.
fn require_terminal_text(content: String) -> anyhow::Result<String> {
if zeroclaw_api::model_provider::strip_think_tags(&content).is_empty() {
return Err(anyhow::Error::new(
zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,
));
}
Ok(content)
}
#[derive(Debug, Serialize)]
struct NativeChatRequest {
model: String,
messages: Vec<NativeMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<NativeToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,View on GitHub (pinned to 88bb9c8533)
Solutions
- Raise max_tokens and retry; truncation inside reasoning is the most common cause.
- Serve/point at a non-reasoning chat model for string-only call sites.
- Move tool-using flows to the structured chat API where native tool_calls count as a valid terminal.
- Inspect the raw completion (finish_reason, content) once with logging to confirm which shape you are getting.
Example fix
// before
let text = openai.chat_with_system(Some(sys), user, "deepseek-r1", None).await?;
// whole answer inside <think> tags -> stripped to empty -> typed error
// after
let text = openai.chat_with_system_opts(Some(sys), user, "deepseek-chat", None, Opts { max_tokens: Some(4096) }).await?; Defensive patterns
Strategy: fallback
Validate before calling
fn completion_is_usable(content: &str) -> bool {
!zeroclaw_api::model_provider::strip_think_tags(content).trim().is_empty()
} Try / catch
match openai.chat_with_system(system, message, model, temp).await {
Ok(text) => Ok(text),
Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => openai.chat_with_system(system, message, fallback_model, temp).await,
Err(e) => Err(e),
} Prevention
- When adding OpenAI-compatible base_urls (vLLM, Ollama), smoke-test one completion and assert non-empty visible text.
- Default to generous max_tokens for reasoning models served through the string API.
- Log finish_reason alongside content length to distinguish truncation from genuine empty output.
When it happens
Trigger: chat_with_system on the OpenAI provider (or any OpenAI-compatible endpoint configured through it) yields empty content or reasoning-only content: o1-style/thinking models, deepseek-r1 via compatible base URLs, max_tokens truncation, or finish_reason 'length' with no visible tokens.
Common situations: Custom base_url pointing at vLLM/Ollama serving reasoning models, low max_tokens defaults, responses where the model wraps its whole answer in <think> and the harness strips it, degenerate sampling producing only whitespace.
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/534ef7bf9d1a0dcb.
Report an issue: GitHub.