Llevas días probando modelos de razonamiento como DeepSeek-R1 o Qwen3-Thinking con llama.cpp. El servidor arranca, el modelo carga, envías una petición usando la API de Anthropic y… algo falla. Claude Code se queja de respuestas malformadas. El streaming no funciona bien. Pides “Hello World” y solo recibes “World”.
Lo primero que piensas es que es tu configuración. Otra variable de entorno mal puesta, otro parámetro que te falta. Pasas página y sigues con otra cosa.
Hasta que un día buscas el error en GitHub y encuentras un issue. Y otro. Y otro más. Gente con el mismo problema exacto, diferentes modelos, diferentes sistemas operativos, mismo resultado roto. Piensas “menos mal, no soy yo” —mal de muchos, consuelo de tontos— pero también piensas otra cosa: si nadie lo arregla, esto va a seguir roto para todos.
Así que decides ponerte a que funcione.
El problema: variables que se reinician donde no deben
El issue #18613 lo describía perfectamente. Un desarrollador pedía a llama-server que dijera “Hello World” y solo recibía “World”. Los logs del servidor mostraban que internamente generaba los tres tokens correctamente —“Hello”, ” World”, fin de secuencia— pero la respuesta JSON solo contenía el último.
El bug estaba en cómo llama.cpp manejaba el streaming de la API de Anthropic. Cada vez que se procesaba un chunk de tokens, las variables que controlaban si un bloque de contenido ya había empezado se declaraban como locales. Se reiniciaban a false en cada iteración. Resultado: en lugar de enviar un content_block_start al principio y luego deltas de contenido, el servidor enviaba un nuevo inicio de bloque por cada token. El cliente se volvía loco intentando parsear eso.
Pero había un segundo problema más específico de los modelos thinking. La API de Anthropic espera un campo signature en los bloques de pensamiento. En modo no-streaming tiene que estar presente aunque sea vacío. En streaming, tiene que haber un evento signature_delta antes de cerrar cada bloque. llama.cpp no enviaba ninguno de los dos.
La solución: estado persistente y firmas donde tocan
El fix requirió cambios en tools/server/server-task.h. Primero, mover el tracking de bloques a la estructura task_result_state para que persista entre llamadas:
// En task_result_state (persistente entre chunks)
bool anthropic_thinking_block_started = false;
bool anthropic_text_block_started = false;
Esto garantiza un único content_block_start por bloque, sin importar cuántos tokens se generen después.
Segundo, añadir los eventos de firma que faltaban:
// Antes de cerrar un bloque thinking en streaming
if (state.anthropic_thinking_block_started) {
// signature_delta vacío (requerido por la spec de Anthropic)
events.push_back({
{"type", "content_block_delta"},
{"delta", {{"type", "signature_delta"}, {"signature", ""}}}
});
}
En modo no-streaming, el campo signature se añade directamente al objeto del bloque thinking.
El proceso de revisión
El PR pasó por varias iteraciones. Uno de los maintainers señaló que usar un puntero raw al estado era un patrón problemático. Tenía razón. Refactoricé el código para copiar los valores directamente en la función update(), eliminando el riesgo de referencias inválidas.
Después de 27 tests de compatibilidad con la API de Anthropic y pruebas manuales con DeepSeek-R1-Distill-Qwen-7B y Qwen3-4B-Thinking, el PR fue aprobado. El desarrollador que había reportado el issue original confirmó que ahora recibía “Hello World” completo.
Por qué importa esto
Ollama anunció que su versión 0.14.0 es compatible con la API de Anthropic Messages (ver noticia aquí). Puedes usar Claude Code con modelos open source locales. Extended thinking, function calling, streaming, todo funciona.
Parte de esa compatibilidad depende de que el servidor implemente correctamente la especificación. Cuando alguien ejecuta Claude Code contra un modelo local que soporta razonamiento, los thinking blocks se serializan bien y el cliente no explota.
Es una pieza pequeña en un ecosistema grande. Pero cada vez que un desarrollador puede usar modelos de razonamiento open source con herramientas que antes solo funcionaban con APIs propietarias, algo mejora para todos.
El código abierto funciona así. Alguien encuentra un bug, otro lo arregla, y miles de personas que nunca sabrán que existió el problema se benefician sin darse cuenta. Trabajar para el bien común.