{"record":{"id":"57c8f5bdd8050d2e","repo":"zeroclaw-labs/zeroclaw","slug":"provider-completed-without-final-text-or-tool-call-57c8f5","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/azure_openai.rs","lineNumber":71,"sourceCode":"\n#[derive(Debug, Deserialize)]\nstruct ResponseMessage {\n    #[serde(default)]\n    content: Option<String>,\n}\n\nimpl ResponseMessage {\n    fn effective_content(&self) -> String {\n        self.content.clone().unwrap_or_default()\n    }\n}\n\n/// String-only completions have no native tool-call escape hatch. An empty or\n/// reasoning-only result is therefore a typed terminal failure, not a valid\n/// string result for direct callers that do not use the structured chat API.\nfn require_terminal_text(content: String) -> anyhow::Result<String> {\n    if zeroclaw_api::model_provider::strip_think_tags(&content).is_empty() {\n        return Err(anyhow::Error::new(\n            zeroclaw_api::model_provider::SemanticEmptyTerminalCompletion,\n        ));\n    }\n    Ok(content)\n}\n\n#[derive(Debug, Serialize)]\nstruct NativeChatRequest {\n    messages: Vec<NativeMessage>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    temperature: Option<f64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    tools: Option<Vec<NativeToolSpec>>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    tool_choice: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    reasoning_effort: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/azure_openai.rs#L53-L89","documentation":"Azure OpenAI provider's require_terminal_text guard for string-only completions: it strips <think> tags from the content and, if nothing visible remains, raises the typed SemanticEmptyTerminalCompletion. A string-only API has no tool-call escape hatch, so an empty or reasoning-only result is a terminal failure, not a valid empty string.","triggerScenarios":"chat_with_system on Azure OpenAI returns content that is empty, whitespace, or entirely wrapped in think tags (reasoning models served via Azure, e.g. DeepSeek-R1 or o-series style endpoints), or finish_reason 'length' with only partial reasoning emitted.","commonSituations":"Deploying reasoning models through Azure OpenAI with default max_tokens (small), content filters blanking the response, deployment names resolving to a reasoning variant, prompt ends with a stop sequence before the answer.","solutions":["Increase max_tokens on the deployment/request so the model finishes reasoning and emits final text.","Switch the deployment (deployment id maps to a model version) to a non-reasoning chat model for string callers.","Use the structured chat API if tool-call-only answers are legitimate for your flow.","Check Azure content-filter results in the raw response; a filtered completion can arrive as empty content."],"exampleFix":"// before\nlet text = azure.chat_with_system(Some(sys), user, \"gpt-deploy\", None).await?;\n// deployment is a reasoning model, returns only <think>...</think>\n\n// after: request a bigger budget / non-reasoning deployment\nlet text = azure.chat_with_system_opts(Some(sys), user, \"gpt-4o-deploy\", None, Opts { max_tokens: Some(4096), ..Default::default() }).await?;","handlingStrategy":"fallback","validationCode":"fn completion_is_usable(content: &str) -> bool {\n    !zeroclaw_api::model_provider::strip_think_tags(content).trim().is_empty()\n}","typeGuard":null,"tryCatchPattern":"match azure.chat_with_system(system, message, deployment, temp).await {\n    Ok(text) => Ok(text),\n    Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => {\n        // think-only or empty: switch deployment rather than retrying the same one\n        azure.chat_with_system(system, message, plain_deployment, temp).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Set explicit max_tokens on every Azure deployment used for string completions.","Map deployment names to non-reasoning model versions for string-only integrations.","Check content-filter results when an empty completion appears with a success status."],"tags":["llm","azure-openai","completion","empty-response","reasoning-model"],"backgroundTag":"llm-empty-completion","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}