Tradução truncada silenciosamente quando a análise do fluxo JSON falha (sem erro gerado)

Resumo
DiscourseAi::Translation::PostLocalizer (localização de conteúdo) ocasionalmente
produz traduções truncadas sem registrar nenhum erro em nenhum lugar (log do Rails,
log do Sidekiq, filas de dead/retry do Sidekiq). O truncamento ocorre no meio de uma frase,
de forma silenciosa, e o resultado parcial é salvo como se fosse uma tradução
completa e bem-sucedida.

Observado em: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash via provedor Google.

Evidências de reprodução
Dois casos reais na nossa instância:

  • Uma postagem alemã de ~2090 caracteres traduzida para o espanhol produziu apenas 50 caracteres,
    cortando imediatamente antes de uma palavra entre aspas (“aufwendiger”) no texto
    de origem.
  • Uma postagem alemã de ~1830 caracteres traduzida para o inglês (83 caracteres) e eslovaco
    (97 caracteres) — ambas cortadas no exato mesmo ponto da frase,
    imediatamente antes de um shortcode de emoji (:collision:) no texto de origem.
    Todos os outros idiomas (fr, it, pt, ru, he) para a mesma postagem foram traduzidos
    completamente (1600–1950 caracteres).

Causa raiz (até onde pude rastrear)
Em lib/completions/structured_output.rb, StructuredOutput#read_buffered_property
recorre a DiscourseAi::Utils::BestEffortJsonParser.extract_key sempre que
@partial_json_tracker.broken? é verdadeiro. Esse recurso retorna tudo o que foi
analisado antes do fluxo JSON falhar — sem exceção, sem aviso, sem
indicação em nenhum lugar de que a saída está incompleta. Em
lib/translation/base_translator.rb#get_translation, esse valor é usado como está
como a tradução final e persistido via PostLocalization#save!.

Ambos os pontos de truncamento em nossos dados ocorrem imediatamente antes de um “caractere especial”
no texto de origem (uma aspa reta, um shortcode de emoji),
sugerindo que o rastreador de fluxo JSON fica confuso com certos
caracteres/sequências na saída em fluxo do modelo e marca o fluxo
como broken? prematuramente.

Impacto
O conteúdo é publicado com traduções visivelmente incompletas, às vezes sem sentido,
e não há sinal operacional (log, trabalho morto, notificação de administrador)
para detectá-lo. Só descobrimos porque um membro da comunidade
percebeu lixo no meio da frase na interface.

Correções sugeridas

  • Exibir um aviso/erro (Rails.logger + Discourse-error-log) sempre que
    StructuredOutput#broken? for verdadeiro e o recurso best-effort for usado, para que
    administradores possam detectar isso em vez de enviar silenciosamente conteúdo parcial.
  • Considere validar o comprimento traduzido em relação a um limite de sanidade relativo
    ao comprimento da origem antes de salvar um PostLocalization.

Fico feliz em fornecer pares completos de texto bruto/traduzido privativamente, se for útil para
debugar o rastreador JSON.

2 curtidas

Adicionando um ponto de dados a isso — estamos vendo o que parece ser o mesmo mecanismo subjacente, mas se manifestando de forma diferente.

Observado em: Discourse core + discourse-ai, LLM: GPT-5.1 via OpenAI (conexão de API personalizada, não um modelo pré-configurado).

Evidências de reprodução

Mesma publicação original (em russo, ~2000 caracteres, cabeçalhos/negrito/listas com marcadores/links), traduzida para vários idiomas via tradução de IA nativa:

  • Polonês (pl): a estrutura do markdown da imagem incorporada foi perdida — passou de ![texto alternativo|690x460](upload://...) para um link malformado, sem o prefixo ! e o separador |, com o texto alternativo e as dimensões unidos como texto puro. Renderizado como um link clicável em vez de uma imagem incorporada.
  • Ucraniano (uk): sequências literais \n\n aparecem como texto puro em toda a publicação, no lugar de quebras de parágrafo. O markdown da imagem nesta mesma tradução estava intacto — então a corrupção não está vinculada a um sintoma fixo, ela varia conforme a execução.

Diferentemente do truncamento descrito acima, nosso caso não mostra conteúdo faltando — o texto completo está presente, mas com sequências de escape / sintaxe de markdown corrompidas, em vez de uma saída encurtada. Provedor diferente (OpenAI vs Google aqui), sintoma diferente (corrupção vs truncamento), mesma causa raiz suspeita: StructuredOutput#read_buffered_property caindo de volta para BestEffortJsonParser#extract_key, que não desescapeia sequências de strings JSON (\n permanece literal) e parece também lidar incorretamente com caracteres especiais adjacentes à sintaxe de markdown quando a queda de volta ocorre durante a análise.

Nenhum erro nos logs do Sidekiq ou Rails do nosso lado também — o mesmo comportamento de falha silenciosa.