La traducción se trunca silenciosamente cuando falla el análisis de la secuencia JSON (sin generar error)

Resumen
DiscourseAi::Translation::PostLocalizer (localización de contenido) ocasionalmente
produce traducciones truncadas sin registrar ningún error en ningún lugar (registro de Rails,
registro de Sidekiq, colas de trabajos muertos/reintentos de Sidekiq). El truncamiento ocurre en medio de una oración,
de forma silenciosa, y el resultado parcial se guarda como si fuera una traducción completa y exitosa.

Observado en: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash a través del proveedor de Google.

Evidencia de reproducción
Dos casos reales en nuestra instancia:

  • Una publicación en alemán de ~2090 caracteres traducida al español produjo solo 50 caracteres,
    cortándose inmediatamente antes de una palabra entre comillas (“aufwendiger”) en el texto
    original.
  • Una publicación en alemán de ~1830 caracteres traducida al inglés (83 caracteres) y al eslovaco
    (97 caracteres) — ambas se cortaron en el exacto mismo punto de la oración,
    inmediatamente antes de un código corto de emoji (:collision:) en el texto original.
    Todos los demás idiomas (fr, it, pt, ru, he) para la misma publicación se
    tradujeron completamente (1600–1950 caracteres).

Causa raíz (hasta donde puedo rastrearla)
En lib/completions/structured_output.rb, StructuredOutput#read_buffered_property
recurre a DiscourseAi::Utils::BestEffortJsonParser.extract_key siempre que
@partial_json_tracker.broken? sea verdadero. Este recurso devuelve lo que se
había analizado antes de que la transmisión JSON se rompiera — sin excepción, sin advertencia, sin
indicación alguna de que la salida está incompleta. En
lib/translation/base_translator.rb#get_translation, este valor se utiliza tal cual
como traducción final y se persiste mediante PostLocalization#save!.

Ambos puntos de truncamiento en nuestros datos caen inmediatamente antes de un carácter
“especial” en el texto original (una comilla recta, un código corto de emoji),
sugiriendo que el rastreador de transmisión JSON se confunde con ciertos
caracteres/secuencias en la salida transmitida del modelo y marca la transmisión
como rota? prematuramente.

Impacto
El contenido se publica con traducciones visiblemente incompletas y a veces sin sentido,
y no hay señal operativa (registro, trabajo muerto, notificación de administrador)
para detectarlo. Solo lo descubrimos porque un miembro de la comunidad
notó texto corrupto en medio de una oración en la interfaz de usuario.

Correcciones sugeridas

  • Mostrar una advertencia/error (Rails.logger + Discourse-error-log) siempre que
    StructuredOutput#broken? sea verdadero y se utilice el recurso de mejor esfuerzo, para que
    los administradores puedan detectar esto en lugar de enviar contenido parcial de forma silenciosa.
  • Considere validar la longitud traducida contra un umbral de coherencia relativo
    a la longitud del texto original antes de guardar una PostLocalization.

Estoy encantado de proporcionar pares completos de texto original/traducido de forma privada si es útil para
debuggear el rastreador JSON.

2 Me gusta

Añado un dato más a esto: estamos viendo lo que parece ser el mismo mecanismo subyacente, pero manifestándose de forma diferente.

Observado en: Discourse core + discourse-ai, LLM: GPT-5.1 vía OpenAI (conexión de API personalizada, no un modelo sembrado).

Evidencia de reproducción

Mismo mensaje original (en ruso, ~2000 caracteres, encabezados/negrita/listas con viñetas/enlaces),
traducido a varios idiomas mediante traducción AI nativa:

  • Polaco (pl): la estructura del markdown de la imagen incrustada se perdió — pasó de
    ![texto alternativo|690x460](upload://...) a un enlace mal formado que carece del prefijo !
    y del separador |, con el texto alternativo y las dimensiones unidos como texto plano. Se renderizó como un enlace clicable en lugar de una imagen incrustada.
  • Ucraniano (uk): las secuencias literales \n\n aparecen como texto plano en todo el mensaje, en lugar de los saltos de párrafo. El markdown de la imagen en esta misma traducción estaba intacto — por lo que la corrupción no está vinculada a un síntoma fijo, varía según la ejecución.

A diferencia del truncamiento descrito anteriormente, nuestro caso no muestra contenido faltante —
el texto completo está presente, pero con secuencias de escape / sintaxis de markdown corruptas en lugar de una salida acortada. Proveedor diferente (OpenAI vs Google aquí), síntoma diferente (corrupción vs truncamiento), misma raíz sospechada:
StructuredOutput#read_buffered_property cayendo en la función de respaldo
BestEffortJsonParser#extract_key, que no desescapa las secuencias de cadenas JSON (\n permanece literal) y parece manejar incorrectamente los caracteres especiales adyacentes a la sintaxis de markdown cuando se activa la función de respaldo durante el análisis.

Tampoco hay errores en los registros de Sidekiq o Rails en nuestro lado — el mismo comportamiento de fallo silencioso.