{"record":{"id":"534ef7bf9d1a0dcb","repo":"zeroclaw-labs/zeroclaw","slug":"provider-completed-without-final-text-or-tool-call-534ef7","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/openai.rs","lineNumber":81,"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    model: String,\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    max_tokens: Option<u32>,","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/openai.rs#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet text = openai.chat_with_system(Some(sys), user, \"deepseek-r1\", None).await?;\n// whole answer inside <think> tags -> stripped to empty -> typed error\n\n// after\nlet text = openai.chat_with_system_opts(Some(sys), user, \"deepseek-chat\", None, Opts { max_tokens: Some(4096) }).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 openai.chat_with_system(system, message, model, temp).await {\n    Ok(text) => Ok(text),\n    Err(e) if e.is::<SemanticEmptyTerminalCompletion>() => openai.chat_with_system(system, message, fallback_model, temp).await,\n    Err(e) => Err(e),\n}","preventionTips":["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."],"tags":["llm","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"}