From a9fbd01b5d08b8f06d4e5e82d30c681ba2a54e3f Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Sat, 4 Apr 2026 10:22:35 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20Claude=20adapter:=20convertir=20mensajes?= =?UTF-8?q?=20OpenAI=E2=86=92Claude=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- docs/hooks-and-api.md | 8 +-- src/adapters/claude_adapter.py | 113 +++++++++++++++++++++++++++++-- src/orchestrator/agents/coder.py | 17 +---- 3 files changed, 115 insertions(+), 23 deletions(-) diff --git a/docs/hooks-and-api.md b/docs/hooks-and-api.md index 3e94aec..42a3132 100644 --- a/docs/hooks-and-api.md +++ b/docs/hooks-and-api.md @@ -115,7 +115,7 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks ### Read — `CmsApi::get()` -## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/.ini.php +## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/.ini.php ```php // Todos los registros @@ -167,7 +167,7 @@ $datos = CmsApi::get("productos", "", "", "", [ ### Insert — `CmsApi::insert()` -## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/.ini.php +## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/.ini.php ```php // Un registro @@ -200,7 +200,7 @@ CmsApi::insert('productos', ### Update — `CmsApi::update()` -## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/.ini.php +## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/.ini.php ```php // Con condición string @@ -227,7 +227,7 @@ CmsApi::update('productos', ["activo" => 0], "precio < 50"); ### Delete — `CmsApi::delete()` -## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/.ini.php +## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/.ini.php ```php CmsApi::delete('productos', "num=5"); diff --git a/src/adapters/claude_adapter.py b/src/adapters/claude_adapter.py index 5122880..f814878 100644 --- a/src/adapters/claude_adapter.py +++ b/src/adapters/claude_adapter.py @@ -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.""" diff --git a/src/orchestrator/agents/coder.py b/src/orchestrator/agents/coder.py index 29e986c..8bcc072 100644 --- a/src/orchestrator/agents/coder.py +++ b/src/orchestrator/agents/coder.py @@ -98,21 +98,6 @@ Rule of thumb: See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage. -## Database Access - -When the site is running in Docker, you can connect to the database: - -- **Host:** `127.0.0.1` -- **Port:** Check `.docker/docker-compose.yml` for the mapped port (usually 3307+) -- **Credentials:** Read from `.docker/.env`: - - `DB_USERNAME` - - `DB_PASSWORD` - - `DB_DATABASE` - -```bash -docker exec -it dw--db mysql -u root -p -``` - **Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`. ## Acai Core (web-base) @@ -138,6 +123,8 @@ Do NOT modify web-base files — they are shared across all projects. 11. Twig concatenation uses `~` operator: `'value=' ~ variable` 12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked 13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard +14. All CmsApi/Twig variables and field names should be extracted from the schemas in `cms/data/schema/.ini.php` before use. Do not guess variable names or field types. +15. NEVER make up a field or table name. Always check the schema files in `cms/data/schema/` to confirm field names and types before using them. ## MCP Tools