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:
@@ -115,7 +115,7 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks
|
|||||||
|
|
||||||
### Read — `CmsApi::get()`
|
### Read — `CmsApi::get()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Todos los registros
|
// Todos los registros
|
||||||
@@ -167,7 +167,7 @@ $datos = CmsApi::get("productos", "", "", "", [
|
|||||||
|
|
||||||
### Insert — `CmsApi::insert()`
|
### Insert — `CmsApi::insert()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Un registro
|
// Un registro
|
||||||
@@ -200,7 +200,7 @@ CmsApi::insert('productos',
|
|||||||
|
|
||||||
### Update — `CmsApi::update()`
|
### Update — `CmsApi::update()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Con condición string
|
// Con condición string
|
||||||
@@ -227,7 +227,7 @@ CmsApi::update('productos', ["activo" => 0], "precio < 50");
|
|||||||
|
|
||||||
### Delete — `CmsApi::delete()`
|
### Delete — `CmsApi::delete()`
|
||||||
|
|
||||||
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
|
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schema/<nombre_de_tabla>.ini.php
|
||||||
|
|
||||||
```php
|
```php
|
||||||
CmsApi::delete('productos', "num=5");
|
CmsApi::delete('productos', "num=5");
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
temperature=settings.temperature,
|
temperature=settings.temperature,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Separate system message
|
# Separate system message and convert OpenAI format to Claude format
|
||||||
system_content = ""
|
system_content = ""
|
||||||
api_messages: list[dict[str, Any]] = []
|
api_messages: list[dict[str, Any]] = []
|
||||||
for m in messages:
|
for m in messages:
|
||||||
@@ -46,6 +46,7 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
system_content = m["content"]
|
system_content = m["content"]
|
||||||
else:
|
else:
|
||||||
api_messages.append(m)
|
api_messages.append(m)
|
||||||
|
api_messages = self._convert_messages(api_messages)
|
||||||
|
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
"model": config.model_id or settings.default_model_id,
|
"model": config.model_id or settings.default_model_id,
|
||||||
@@ -62,8 +63,14 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
current_tool_id = ""
|
current_tool_id = ""
|
||||||
current_tool_name = ""
|
current_tool_name = ""
|
||||||
accumulated_args = ""
|
accumulated_args = ""
|
||||||
|
input_tokens = 0
|
||||||
|
|
||||||
async for event in stream:
|
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":
|
if event.type == "content_block_start":
|
||||||
block = event.content_block
|
block = event.content_block
|
||||||
if block.type == "tool_use":
|
if block.type == "tool_use":
|
||||||
@@ -103,12 +110,12 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if event.type == "message_delta":
|
if event.type == "message_delta":
|
||||||
|
output_tokens = getattr(event.usage, "output_tokens", 0) if event.usage else 0
|
||||||
yield StreamChunk(
|
yield StreamChunk(
|
||||||
finish_reason=event.delta.stop_reason or "",
|
finish_reason=event.delta.stop_reason or "",
|
||||||
usage={
|
usage={
|
||||||
"output_tokens": getattr(
|
"input_tokens": input_tokens,
|
||||||
event.usage, "output_tokens", 0
|
"output_tokens": output_tokens,
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,6 +142,7 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
system_content = m["content"]
|
system_content = m["content"]
|
||||||
else:
|
else:
|
||||||
api_messages.append(m)
|
api_messages.append(m)
|
||||||
|
api_messages = self._convert_messages(api_messages)
|
||||||
|
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
"model": config.model_id or settings.default_model_id,
|
"model": config.model_id or settings.default_model_id,
|
||||||
@@ -186,6 +194,103 @@ class ClaudeAdapter(ModelAdapter):
|
|||||||
# Helpers
|
# 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
|
@staticmethod
|
||||||
def _format_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _format_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""Convert internal tool definitions to Anthropic tool format."""
|
"""Convert internal tool definitions to Anthropic tool format."""
|
||||||
|
|||||||
@@ -98,21 +98,6 @@ Rule of thumb:
|
|||||||
|
|
||||||
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
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-<project-name>-db mysql -u root -p<password> <database>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
|
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
|
||||||
|
|
||||||
## Acai Core (web-base)
|
## 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`
|
11. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||||
12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
|
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
|
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/<nombre_de_tabla>.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
|
## MCP Tools
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user