Bonsai 27B + Hermes Agent: Arreglando el Bug del Parser JSON Schema de llama.cpp
DEBUG_LOG // BONSAI_27B // HERMES_AGENT // LLAMA_CPP // PROXY_FIX
Después de hacer benchmark de Bonsai 27B en una GPU de 8GB y lograr 27.35 tok/s con 160K de contexto, el siguiente paso lógico era integrarlo en mi flujo de trabajo de agente en producción: Hermes Agent, el framework que impulsa mis reportes diarios AM/PM de infraestructura, triage de correos, facturación electrónica SRI, y monitoreo de GitHub.
El modelo funcionó perfectamente para chat de texto simple vía hermes -p bonsai_27b. Pero en cuanto intenté ejecutarlo a través del scheduler de cron jobs de Hermes — que inyecta los tool schemas de los servidores MCP (hledger, email, facturador-sri, payphone) — todo colapsó con un opaco HTTP 400.
El Error
HTTP 400: Unable to generate parser for this template.
Automatic parser generation failed: JSON schema conversion failed:
Error resolving ref #/properties/begin/anyOf/0: anyOf not in
{"type":"string","pattern":"^\\d{4}(-\\d{2}(-\\d{2})?)?$","nullable":true}
El mensaje se repetía 16 veces — una por cada herramienta MCP en la petición. Hermes enviaba 119 herramientas con ~170KB de schemas JSON, y el parser interno de gramática de llama-server no podía procesarlos.
Diagnóstico: Cuatro Capas de Profundidad
Capa 1 — --jinja Fue una Pista Falsa
Mi primer instinto fue eliminar --jinja de los flags de llama-server. El motor de plantillas Jinja procesa los templates de chat, y asumí que estaba conflictuando con los tool schemas de Hermes.
Resultado: El error persistió de forma idéntica. --jinja controla el formateo de mensajes de chat, no la generación de gramática JSON. Son subsistemas completamente distintos.
Capa 2 — anyOf en los Schemas del Email
Construí un proxy de debug entre Hermes y llama-server para inspeccionar el payload real. El sanitizador detectó bloques anyOf en los parámetros de las herramientas de email (to, cc, bcc — campos que aceptan tanto array como string):
"to": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "string"}
]
}
Eliminé esos. Seguía fallando.
Capa 3 — El $ref Fantasma
Al inspeccionar el request crudo de 170KB apareció el verdadero culpable. El schema_sanitizer interno de Hermes ya había limpiado el campo begin de la herramienta hledger — reemplazando su anyOf con un type: string plano. Pero el campo end todavía tenía un $ref colgante apuntando al anyOf ahora eliminado:
"begin": {
"type": "string",
"pattern": "^\\d{4}(-\\d{2}(-\\d{2})?)?$",
"nullable": true
},
"end": {
"$ref": "#/properties/begin/anyOf/0",
"nullable": true
}
llama-server perseguía ese $ref, no encontraba ningún anyOf en el destino, y lanzaba el error. Referencias $ref huérfanas a rutas de esquema ya sanitizadas.
Capa 4 — Los MCP Servers se Cargan a Nivel de Perfil
Incluso con enabled_toolsets=[] en el cron job, los servidores MCP definidos en config.yaml (hledger, email, facturador-sri, payphone, contífico) inyectan sus tool schemas en el system prompt de todas formas. No existe filtrado MCP por job — son globales al perfil.
La Solución: Proxy Sanitizador
Un proxy ligero en Python se interpone entre Hermes y llama-server, sanitizando los tool schemas en cada petición. Elimina tanto los bloques anyOf (aplanándolos con su primer tipo no-nulo) como los punteros $ref (reemplazándolos con un type: string seguro).
Código del Proxy
#!/usr/bin/env python3
"""Proxy sanitizador para llama.cpp: elimina anyOf y $ref de los tool schemas."""
import json, http.server, urllib.request
TARGET = "http://<llama-server-host>:11433/v1/chat/completions"
PORT = 11434
def sanitize(obj):
if isinstance(obj, dict):
if "anyOf" in obj:
types = [t.get("type") for t in obj["anyOf"]
if isinstance(t, dict) and t.get("type") != "null"]
if types:
obj["type"] = types[0]
del obj["anyOf"]
if "$ref" in obj:
del obj["$ref"]
obj.setdefault("type", "string")
for v in obj.values():
sanitize(v)
elif isinstance(obj, list):
for item in obj:
sanitize(item)
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
data = json.loads(body)
sanitize(data)
cleaned = json.dumps(data).encode("utf-8")
except Exception:
cleaned = body
req = urllib.request.Request(
TARGET, data=cleaned,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=300) as resp:
self.send_response(resp.status)
for k, v in resp.headers.items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(502)
self.end_headers()
self.wfile.write(
json.dumps({"error": f"HTTP Error {e.code}: {e.reason}"}).encode()
)
def log_message(self, *args):
pass
http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
Servicio Systemd
[Unit]
Description=Bonsai 27B Proxy Sanitizer
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /root/.hermes/profiles/bonsai_27b/proxy_sanitizer.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Actualización de Config en Hermes
custom_providers:
- name: bonsai-27b
base_url: http://127.0.0.1:11434/v1 # ← proxy, no directo
api_key: llama.cpp
model: Bonsai-27B-Q1_0.gguf
Diagrama de Arquitectura
┌──────────┐ 170KB JSON + 119 tools ┌────────────┐ sanitizado ┌─────────────┐
│ Hermes │ ────────────────────────────→ │ Proxy │ ──────────────→ │ llama-server │
│ Agent │ ←──────────────────────────── │ :11434 │ ←────────────── │ :11433 │
└──────────┘ text/stream response └────────────┘ text/stream └─────────────┘
│
elimina anyOf + $ref de todos
los tool schemas en tiempo real
Hermes + Bonsai: Lo Que Funciona Ahora
| Caso de Uso | Estado | Notas |
|---|---|---|
Chat de texto simple (bonsai_27b chat -q) |
✅ Funciona | Sin herramientas = sin conflicto |
| Cron jobs sin MCP | ✅ Funciona | enabled_toolsets=[] + proxy |
| Cron jobs CON MCP (email, hledger, SRI) | ✅ Funciona | El proxy limpia los schemas problemáticos |
| Agente interactivo con herramientas completas | ⚠️ Parcial | Funciona si las herramientas no generan anyOf o $ref huérfanos |
Causa Raíz Upstream
Esto es en última instancia un problema de llama.cpp — el conversor json_schema_to_grammar en llama-server no maneja keywords anyOf ni resolución de $ref en los schemas de tool calling. El workaround documentado aquí es un proxy del lado del cliente hasta que el parser upstream tenga soporte para anyOf y resolución de $ref.
Issue candidato para seguimiento: ggml-org/llama.cpp — buscar “Unable to generate parser JSON schema conversion anyOf”.
Conclusión Clave
Si estás intentando emparejar un modelo de razonamiento (Bonsai, Qwen3, DeepSeek) servido vía llama-server con un framework de agente complejo (Hermes, OpenCode, CrewAI) que tiene herramientas MCP con schemas JSON ricos, te vas a topar con este muro. El patrón del proxy sanitizador es genérico — reemplaza los tool schemas en el cable antes de que lleguen a llama.cpp, y el modelo funciona sin problemas.
Código fuente completo y unidad systemd disponibles en mis configs de homelab.