Fix tool_use message format for Anthropic API
All checks were successful
Build Nanobot OAuth / build (push) Successful in 1m59s

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 <noreply@anthropic.com>
This commit is contained in:
wylab
2026-02-13 15:29:55 +01:00
parent 5a8f3f772c
commit c5ab4098ca

View File

@@ -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,