Fix Claude adapter: convertir mensajes OpenAI→Claude format

- role=tool → role=user con tool_result blocks
- assistant con tool_calls → assistant con tool_use blocks
- Merge mensajes consecutivos del mismo role (Claude requiere alternancia)
- Capturar input_tokens del evento message_start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordan Diaz
2026-04-04 10:22:35 +00:00
parent 184486b62b
commit a9fbd01b5d
3 changed files with 115 additions and 23 deletions

View File

@@ -38,7 +38,7 @@ class ClaudeAdapter(ModelAdapter):
temperature=settings.temperature,
)
# Separate system message
# Separate system message and convert OpenAI format to Claude format
system_content = ""
api_messages: list[dict[str, Any]] = []
for m in messages:
@@ -46,6 +46,7 @@ class ClaudeAdapter(ModelAdapter):
system_content = m["content"]
else:
api_messages.append(m)
api_messages = self._convert_messages(api_messages)
kwargs: dict[str, Any] = {
"model": config.model_id or settings.default_model_id,
@@ -62,8 +63,14 @@ class ClaudeAdapter(ModelAdapter):
current_tool_id = ""
current_tool_name = ""
accumulated_args = ""
input_tokens = 0
async for event in stream:
if event.type == "message_start" and hasattr(event, "message"):
usage = getattr(event.message, "usage", None)
if usage:
input_tokens = getattr(usage, "input_tokens", 0)
if event.type == "content_block_start":
block = event.content_block
if block.type == "tool_use":
@@ -103,12 +110,12 @@ class ClaudeAdapter(ModelAdapter):
continue
if event.type == "message_delta":
output_tokens = getattr(event.usage, "output_tokens", 0) if event.usage else 0
yield StreamChunk(
finish_reason=event.delta.stop_reason or "",
usage={
"output_tokens": getattr(
event.usage, "output_tokens", 0
)
"input_tokens": input_tokens,
"output_tokens": output_tokens,
},
)
@@ -135,6 +142,7 @@ class ClaudeAdapter(ModelAdapter):
system_content = m["content"]
else:
api_messages.append(m)
api_messages = self._convert_messages(api_messages)
kwargs: dict[str, Any] = {
"model": config.model_id or settings.default_model_id,
@@ -186,6 +194,103 @@ class ClaudeAdapter(ModelAdapter):
# Helpers
# ------------------------------------------------------------------
@staticmethod
def _convert_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Convert OpenAI-format messages to Claude format.
- role=tool → role=user with tool_result content blocks
- assistant with tool_calls → assistant with tool_use content blocks
- Consecutive same-role messages get merged (Claude requires alternating)
"""
converted: list[dict[str, Any]] = []
for m in messages:
role = m.get("role", "")
if role == "tool":
# Convert to user message with tool_result block
block = {
"type": "tool_result",
"tool_use_id": m.get("tool_call_id", ""),
"content": m.get("content", ""),
}
if m.get("is_error"):
block["is_error"] = True
# Merge with previous user message if exists
if converted and converted[-1]["role"] == "user":
content = converted[-1]["content"]
if isinstance(content, str):
converted[-1]["content"] = [{"type": "text", "text": content}, block]
elif isinstance(content, list):
content.append(block)
else:
converted[-1]["content"] = [block]
else:
converted.append({"role": "user", "content": [block]})
elif role == "assistant" and "tool_calls" in m:
# Convert tool_calls to tool_use content blocks
blocks: list[dict[str, Any]] = []
text = m.get("content", "")
if text:
blocks.append({"type": "text", "text": text})
for tc in m["tool_calls"]:
func = tc.get("function", {})
args_str = func.get("arguments", "{}")
try:
args = json.loads(args_str) if isinstance(args_str, str) else args_str
except (json.JSONDecodeError, TypeError):
args = {}
blocks.append({
"type": "tool_use",
"id": tc.get("id", ""),
"name": func.get("name", ""),
"input": args,
})
# Merge with previous assistant if exists
if converted and converted[-1]["role"] == "assistant":
prev = converted[-1]["content"]
if isinstance(prev, str):
converted[-1]["content"] = [{"type": "text", "text": prev}] + blocks
elif isinstance(prev, list):
prev.extend(blocks)
else:
converted[-1]["content"] = blocks
else:
converted.append({"role": "assistant", "content": blocks})
elif role == "assistant":
content = m.get("content", "")
# Merge with previous assistant
if converted and converted[-1]["role"] == "assistant":
prev = converted[-1]["content"]
if isinstance(prev, str):
converted[-1]["content"] = prev + "\n" + content if content else prev
elif isinstance(prev, list) and content:
prev.append({"type": "text", "text": content})
else:
converted.append({"role": "assistant", "content": content})
elif role == "user":
content = m.get("content", "")
# Merge with previous user
if converted and converted[-1]["role"] == "user":
prev = converted[-1]["content"]
if isinstance(prev, str) and isinstance(content, str):
converted[-1]["content"] = prev + "\n" + content
elif isinstance(prev, list) and isinstance(content, str):
prev.append({"type": "text", "text": content})
elif isinstance(prev, str) and isinstance(content, list):
converted[-1]["content"] = [{"type": "text", "text": prev}] + content
elif isinstance(prev, list) and isinstance(content, list):
prev.extend(content)
else:
converted.append({"role": role, "content": content})
else:
converted.append(m)
return converted
@staticmethod
def _format_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Convert internal tool definitions to Anthropic tool format."""