Discourse AI: un error de Gemini dentro de una respuesta en streaming se ignora y el texto parcial se guarda como una traducción completa

Descripción

Cuando la API de Gemini de Google falla a mitad de una respuesta transmitida en flujo (streamed), Discourse AI trata la respuesta como completa. Con la traducción por IA habilitada, el fragmento recibido antes de la falla se guarda como la traducción finalizada de la publicación o del título del tema. No se registra ningún error y el relleno (backfill) nunca reintenta el elemento, ya que ahora existe una fila de traducción.

Observado en un sitio autoalojado que ejecuta release/2026.7 (2026.7.3, commit f1caa6321287f918644fba9ff8943579fdcf027a) con el plugin discourse-ai incluido. El modelo de lenguaje (LLM) es gemini-3.8-flash a través del proveedor Google, con el nivel de servicio establecido en flex. Flex es el nivel en el que Google descarta carga, por lo que produce estas fallas a mitad del flujo con regularidad; el manejo descrito a continuación no depende del nivel.

En estos casos, Google responde con HTTP 200, transmite uno o más eventos normales y luego envía un evento de error en el mismo flujo:

data: {"candidates": [ ... ],"usageMetadata": { ... },"serviceTier": "flex","modelVersion": "gemini-3.8-flash","responseId": "..."}

data: {"error": {"code": 503,"message": "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.","status": "UNAVAILABLE"}}

El registro de auditoría registra response_status 200 y una pequeña cantidad de response_tokens. Ejemplos de lo que se guardó: una publicación de 917 caracteres guardada como 26 caracteres (solo su línea de saludo), publicaciones que solo contenían enlaces guardadas como los primeros 8 a 24 caracteres de la URL, y títulos de temas cortados a mitad de palabra.

Medido en este sitio: 23 de 394 llamadas de traducción de nivel Flex respondidas (5,8 %) terminaron de esta manera. Ninguna de las 952 llamadas de nivel estándar al mismo modelo lo hizo.

Causa raíz

  1. DiscourseAi::Completions::Endpoints::Gemini#decode_chunk solo lee candidates de cada evento de flujo analizado. Un evento cuya clave de nivel superior es error no genera partes y se omite sin ninguna comprobación.
  2. DiscourseAi::Completions::Endpoints::Base decide entre éxito, reintento y falla solo a partir del estado HTTP (response.code.to_i != 200). El estado es 200 aquí, por lo que la lógica de reintento existente para 503 nunca se alcanza.
  3. El flujo luego termina normalmente y el llamador recibe el texto acumulado hasta ese momento. DiscourseAi::Translation::PostLocalizer y TopicLocalizer lo guardan.

El mismo código está presente en main a fecha del 03-10-2026.

Solución sugerida

Tratar un objeto error de nivel superior en un evento de Gemini transmitido en flujo como una finalización fallida: descartar la salida parcial y lanzar CompletionFailed, para que el manejo de reintentos existente se aplique a códigos reintentables como 503 y los llamadores nunca reciban un fragmento como una respuesta completa.

Reproducir

La falla depende de la carga de Google, por lo que no se puede forzar. En un sitio que utiliza un modelo de Gemini en el nivel Flex con traducción por IA, las llamadas afectadas se pueden encontrar después en el registro de auditoría:

SELECT id, created_at, post_id, topic_id, response_status, response_tokens
FROM ai_api_audit_logs
WHERE feature_name = 'translation'
  AND raw_response_payload LIKE '%data: {"error"%'
  AND response_tokens > 0
ORDER BY created_at

Cada fila es una llamada que devolvió HTTP 200, produjo alguna salida y luego contenía un evento de error. Las filas correspondientes en post_localizations o topic_localizations contienen el texto truncado.

Solución temporal

Mover los agentes de traducción al nivel de servicio estándar detuvo nuevas ocurrencias. Los fragmentos almacenados tuvieron que encontrarse con la consulta anterior, eliminarse y traducirse de nuevo.

1 me gusta