Creo que llama.cpp fuerza el esquema seleccionando únicamente los tokens que lo cumplen. También capturé una respuesta con tcpdump y verifiqué que la respuesta devuelta fuera JSON válido.
Sin embargo, contenía espacios y saltos de línea fuera de las cadenas de texto (es decir, era un JSON con formato legible), y Discourse escapaba esos saltos de línea, lo que lo hacía inválido (lo verifiqué en la consola de Rails).
Por cierto, estoy utilizando la función de detección de spam.