{"record":{"id":"a4f888885593e77b","repo":"zeroclaw-labs/zeroclaw","slug":"ollama-returned-non-prompt-guided-tools-payload-wh","errorCode":null,"errorMessage":"Ollama returned non-prompt-guided tools payload while native tools are disabled","messagePattern":"Ollama returned non-prompt-guided tools payload while native tools are disabled","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/ollama.rs","lineNumber":604,"sourceCode":"                    images: None,\n                    tool_calls: None,\n                    tool_name: None,\n                }\n            })\n            .collect()\n    }\n\n    fn with_prompt_guided_tool_instructions(\n        &self,\n        messages: &[ChatMessage],\n        tools: Option<&[zeroclaw_api::tool::ToolSpec]>,\n    ) -> anyhow::Result<Vec<ChatMessage>> {\n        let Some(tools) = tools.filter(|items| !items.is_empty()) else {\n            return Ok(messages.to_vec());\n        };\n\n        let ToolsPayload::PromptGuided { instructions } = self.convert_tools(tools) else {\n            anyhow::bail!(\n                \"Ollama returned non-prompt-guided tools payload while native tools are disabled\"\n            );\n        };\n        let mut modified_messages = messages.to_vec();\n\n        if let Some(system_message) = modified_messages.iter_mut().find(|m| m.role == \"system\") {\n            if !system_message.content.is_empty() {\n                system_message.content.push_str(\"\\n\\n\");\n            }\n            system_message.content.push_str(&instructions);\n        } else {\n            modified_messages.insert(0, ChatMessage::system(instructions));\n        }\n\n        Ok(modified_messages)\n    }\n\n    fn response_to_chat_response(&self, response: ApiChatResponse, model: &str) -> ChatResponse {","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/ollama.rs#L586-L622","documentation":"The Ollama provider hard-codes supports_native_tools() = false, so chat() injects tool specifications into the system prompt as text via with_prompt_guided_tool_instructions, which requires convert_tools to return ToolsPayload::PromptGuided. This bail fires when a non-empty tools slice converts to a native payload variant (OpenAI/Anthropic/Gemini) instead. Stock builds cannot reach it — OllamaModelProvider never overrides the trait's default PromptGuided conversion — so encountering it means a wrapper or fork changed tool conversion without enabling native tools.","triggerScenarios":"chat() with a non-empty request.tools on a modified Ollama provider whose convert_tools override returns a native ToolsPayload variant; downstream forks of ollama.rs enabling native conversion without also flipping supports_native_tools and serializing tools in send_request.","commonSituations":"Custom ModelProvider wrappers or vision/model-pin style decorators that alter convert_tools; forks porting native tool-calling support from another provider halfway; upstream refactors changing the default trait implementation.","solutions":["If you maintain a wrapper: do not override convert_tools for Ollama, or keep returning ToolsPayload::PromptGuided while native tools are disabled","If native Ollama tools are intended, implement the full path — supports_native_tools() = true plus tools serialization in the request body — instead of half-enabling it","Otherwise report it upstream with the wrapper code, since the shipped provider cannot produce a native payload"],"exampleFix":"// before (fork/wrapper)\nfn convert_tools(&self, tools: &[ToolSpec]) -> ToolsPayload {\n    ToolsPayload::OpenAI { tools: to_openai(tools) }\n}\n\n// after: keep prompt-guided while native tools are disabled\nfn convert_tools(&self, tools: &[ToolSpec]) -> ToolsPayload {\n    ToolsPayload::PromptGuided { instructions: build_tool_instructions_text(tools) }\n}","handlingStrategy":"type-guard","validationCode":"fn ollama_tools_prompt_guided_safe(\n    provider: &dyn ModelProvider,\n    tools: &[zeroclaw_api::tool::ToolSpec],\n) -> bool {\n    if tools.is_empty() { return true; }\n    !provider.supports_native_tools()\n        && matches!(provider.convert_tools(tools), ToolsPayload::PromptGuided { .. })\n}","typeGuard":"fn tools_payload_is_prompt_guided(payload: &ToolsPayload) -> bool {\n    matches!(payload, ToolsPayload::PromptGuided { .. })\n}","tryCatchPattern":"let messages = if !tools.is_empty() {\n    match provider.convert_tools(tools) {\n        ToolsPayload::PromptGuided { .. } => inject_instructions(messages, tools),\n        _ => return Err(anyhow::anyhow!(\"wrapper overrides convert_tools with native tools disabled\")),\n    }\n} else { messages.to_vec() };","preventionTips":["Never override convert_tools on a provider whose supports_native_tools() is false","If you enable native tool payloads, flip supports_native_tools() and serialize tools in the same change","Wrap third-party providers with a guard that asserts the PromptGuided variant before chat()"],"tags":["ollama","tools","prompt-guided","invariant","internal"],"backgroundTag":"internal-invariant-violation","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}