{"record":{"id":"d383a6db1b886b3e","repo":"zeroclaw-labs/zeroclaw","slug":"provider-completed-without-final-text-or-tool-call","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-api/src/model_provider.rs","lineNumber":681,"sourceCode":"                    system_message.content.push_str(\"\\n\\n\");\n                }\n                system_message.content.push_str(&tool_instructions);\n            } else {\n                modified_messages.insert(0, ChatMessage::system(tool_instructions));\n            }\n\n            let text = self\n                .chat_with_history(&modified_messages, model, temperature)\n                .await?;\n            let response = ChatResponse {\n                text: Some(text),\n                tool_calls: Vec::new(),\n                usage: None,\n                reasoning_content: None,\n            };\n            return (!response.is_semantically_empty_terminal())\n                .then_some(response)\n                .ok_or_else(|| anyhow::Error::new(SemanticEmptyTerminalCompletion));\n        }\n\n        let text = self\n            .chat_with_history(request.messages, model, temperature)\n            .await?;\n        let response = ChatResponse {\n            text: Some(text),\n            tool_calls: Vec::new(),\n            usage: None,\n            reasoning_content: None,\n        };\n        (!response.is_semantically_empty_terminal())\n            .then_some(response)\n            .ok_or_else(|| anyhow::Error::new(SemanticEmptyTerminalCompletion))\n    }\n\n    /// Whether model_provider supports native tool calls over API.\n    fn supports_native_tools(&self) -> bool {","sourceCodeStart":663,"sourceCodeEnd":699,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-api/src/model_provider.rs#L663-L699","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","Put the model behind the reliable provider chain so a semantic-empty on one model falls through to the next candidate.","Check the provider capabilities configuration; a wrong capabilities flag can route a native-tools provider into this degraded path."],"exampleFix":"// before: reasoning model on the prompt-guided string path emits only <think>...</think>\nlet resp = provider.chat(ChatRequest { messages, tools: Some(&tools), .. }, model, None).await?;\n\n// after: chain a non-reasoning fallback via the reliable provider\nlet resp = reliable.chat(request, None).await // falls through on SemanticEmptyTerminalCompletion\n    .or_else(|e| if e.is::<SemanticEmptyTerminalCompletion>() { retry_with_plain_model() } else { Err(e) })?;","handlingStrategy":"fallback","validationCode":"fn is_semantically_empty(text: &str) -> bool {\n    zeroclaw_api::model_provider::strip_think_tags(text).trim().is_empty()\n}","typeGuard":null,"tryCatchPattern":"match provider.chat(request, model, temperature).await {\n    Ok(resp) => Ok(resp),\n    Err(e) if e.is::<zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion>() => {\n        fallback_provider().chat(request, fallback_model, temperature).await // typed terminal -> next candidate\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Prefer providers with native tool calling for tool-enabled requests; the prompt-guided string path is where semantic-empties surface.","Size max_tokens above the model's observed reasoning budget before serving tool turns.","Keep one non-reasoning model last in every reliable chain to absorb think-only failures."],"tags":["llm","completion","tool-calling","reasoning-model","empty-response"],"backgroundTag":"llm-empty-completion","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}