From c5ab4098ca491a16fcdc6de21f06f120cbf7866c Mon Sep 17 00:00:00 2001 From: wylab Date: Fri, 13 Feb 2026 15:29:55 +0100 Subject: [PATCH] Fix tool_use message format for Anthropic API The agent loop produces messages in OpenAI format (role:tool, tool_calls array) but the Anthropic API expects its own format (tool_use content blocks in assistant messages, tool_result blocks in user messages). This caused 400 errors whenever the bot tried to use tools like web_search, because the follow-up message with tool results was malformed. Co-Authored-By: Claude Opus 4.6 --- nanobot/providers/anthropic_oauth.py | 80 +++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/nanobot/providers/anthropic_oauth.py b/nanobot/providers/anthropic_oauth.py index ef9ce65..43483ad 100644 --- a/nanobot/providers/anthropic_oauth.py +++ b/nanobot/providers/anthropic_oauth.py @@ -68,21 +68,87 @@ class AnthropicOAuthProvider(LLMProvider): self, messages: list[dict[str, Any]] ) -> tuple[str | None, list[dict[str, Any]]]: - """Prepare messages, extracting system prompt and adding Claude Code identity. + """Prepare messages: extract system prompt and convert OpenAI format to Anthropic. - Returns (system_prompt, messages_without_system) + The agent loop produces messages in OpenAI format: + - assistant msgs with tool_calls [{type:"function", function:{name, arguments}}] + - tool role msgs with tool_call_id, name, content + + Anthropic API expects: + - assistant msgs with content blocks [{type:"tool_use", id, name, input}] + - user msgs with content blocks [{type:"tool_result", tool_use_id, content}] + + Returns (system_prompt, anthropic_messages) """ system_parts = [get_claude_code_system_prefix()] - filtered_messages = [] + converted: list[dict[str, Any]] = [] for msg in messages: - if msg.get("role") == "system": + role = msg.get("role") + + if role == "system": system_parts.append(msg.get("content", "")) - else: - filtered_messages.append(msg) + continue + + if role == "assistant" and msg.get("tool_calls"): + # Convert OpenAI tool_calls to Anthropic content blocks + content_blocks: list[dict[str, Any]] = [] + text = msg.get("content") + if text: + content_blocks.append({"type": "text", "text": text}) + for tc in msg["tool_calls"]: + func = tc.get("function", {}) + args = func.get("arguments", "{}") + if isinstance(args, str): + try: + args = json.loads(args) + except (json.JSONDecodeError, TypeError): + args = {} + content_blocks.append({ + "type": "tool_use", + "id": tc.get("id", ""), + "name": func.get("name", ""), + "input": args, + }) + converted.append({"role": "assistant", "content": content_blocks}) + continue + + if role == "tool": + # Convert tool result to Anthropic user message with tool_result block + tool_result_block = { + "type": "tool_result", + "tool_use_id": msg.get("tool_call_id", ""), + "content": msg.get("content", ""), + } + # Merge into previous user message if it already has tool_result blocks + if converted and converted[-1].get("role") == "user": + prev_content = converted[-1].get("content") + if isinstance(prev_content, list): + prev_content.append(tool_result_block) + continue + converted.append({"role": "user", "content": [tool_result_block]}) + continue + + if role == "user": + content = msg.get("content", "") + # Merge text into previous user message if it has tool_result blocks + # (handles the "Reflect on the results" interleaved message) + if converted and converted[-1].get("role") == "user": + prev_content = converted[-1].get("content") + if isinstance(prev_content, list): + if isinstance(content, str): + prev_content.append({"type": "text", "text": content}) + elif isinstance(content, list): + prev_content.extend(content) + continue + converted.append({"role": role, "content": content}) + continue + + # Pass through other messages (assistant without tool_calls, etc.) + converted.append(msg) system_prompt = "\n\n".join(system_parts) - return system_prompt, filtered_messages + return system_prompt, converted def _convert_tools_to_anthropic( self,