# Discourse AI: o decodificador de streaming do Gemini perde o restante da resposta quando um fragmento de rede termina na linha em branco entre dois eventos

**URL:** <https://meta.discourse.org/t/discourse-ai-gemini-streaming-decoder-drops-the-rest-of-a-reply-when-a-network-chunk-ends-inside-the-blank-line-between-two-events/413982>\
**Category:** Bug\
**Tags:** ai\
**Created:** [5 Outubro , 2026 19:17 UTC](https://meta.discourse.org/t/discourse-ai-gemini-streaming-decoder-drops-the-rest-of-a-reply-when-a-network-chunk-ends-inside-the-blank-line-between-two-events/413982 "2026-10-05T19:17:03Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Sailor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sailor/32/579029_2.png) [@Sailor](https://meta.discourse.org/u/Sailor)\
**Post date:** [5 Outubro , 2026 19:17 UTC](https://meta.discourse.org/t/discourse-ai-gemini-streaming-decoder-drops-the-rest-of-a-reply-when-a-network-chunk-ends-inside-the-blank-line-between-two-events/413982/1 "2026-10-05T19:17:03Z")

</div>

## Descrição

Quando uma resposta do Gemini é transmitida em streaming, o Discourse AI ocasionalmente processa apenas a primeira parte dela. A resposta completa é recebida do Google e armazenada em `ai_api_audit_logs.raw_response_payload`, o stream termina com `finishReason` `STOP` e nenhum erro é registrado. O chamador recebe o texto até um certo evento e nada após ele. Com a tradução por IA, o texto truncado é armazenado como a tradução finalizada.

Observado em um site auto-hospedado executando `release/2026.7` (2026.7.3) com o plugin discourse-ai incluído e `gemini-3.8-flash` na camada de serviço padrão. Em 3 de aproximadamente 26.200 chamadas de tradução ao longo de três dias, `response_tokens` no log de auditoria foi menor que o `candidatesTokenCount` final relatado pelo Google na mesma resposta: 196 de 241, 84 de 328 e 131 de 179. Em cada caso, a contagem armazenada é igual à contagem cumulativa do Google em um evento intermediário, portanto a decodificação parou após esse evento.

Isso é separado dos eventos de erro no stream corrigidos por #44262: não há evento de erro aqui, e o decodificador não foi alterado por essa pull request.

## Causa raiz

`DiscourseAi::Completions::Endpoints::Gemini::GeminiStreamingDecoder#decode` divide seu buffer em `/\r?\n\r?\n/` e aceita um segmento apenas se ele começar com `data: {`.

Quando um chunk de rede termina dentro da linha em branco que separa dois eventos, por exemplo após o primeiro `\r\n` de `\r\n\r\n`, o evento anterior é analisado e o buffer é esvaziado. O próximo chunk então começa com o `\r\n` restante, de modo que o próximo segmento é `"\r\ndata: {...}"`. Ele não começa com `data: {`, portanto é mantido no buffer como uma linha incompleta. Todos os eventos posteriores são adicionados a esse buffer e também não são reconhecidos. O stream termina normalmente e os eventos restantes são descartados junto com o buffer.

O mesmo acontece com separadores apenas LF quando um chunk termina entre os dois caracteres de quebra de linha.

O decodificador é idêntico em `v2026.7.3` e no `main` em `b5548f76` (2026-10-05).

## Reproduzir

Em um console Rails:

```ruby
decoder = DiscourseAi::Completions::Endpoints::Gemini::GeminiStreamingDecoder.new
event = ->(text, tokens) { %(data: {"candidates": [{"content": {"parts": [{"text": "#{text}"}],"role": "model"}}],"usageMetadata": {"candidatesTokenCount": #{tokens}}}) }
chunks = [
  event.("A", 11) + "\r\n\r\n",
  event.("B", 38) + "\r\n",
  "\r\n" + event.("C", 65) + "\r\n\r\n",
  event.("D", 95) + "\r\n\r\n",
]
chunks.flat_map { |chunk| decoder.decode(chunk) }.map { |e| e.dig(:candidates, 0, :content, :parts, 0, :text) }

```

Resultado: `["A", "B"]`. Esperado: `["A", "B", "C", "D"]`. Mover o limite do chunk para qualquer ponto fora do separador, incluindo o meio de um evento, dá o resultado esperado.

## Correção sugerida

Ignorar quebras de linha iniciais em um segmento antes de testá-lo, por exemplo:

```ruby
line = line.sub(/\A[\r\n]+/, "")
if line.start_with?("data: {")

```

Com essa mudança, a reprodução acima retorna todos os quatro eventos, assim como limites após `\r\n`, após `\r\n\r`, dentro de um evento e com separadores apenas LF.

## Encontrar chamadas afetadas

Cada linha retornada por esta consulta é uma chamada que o Google completou normalmente e o Discourse decodificou apenas parcialmente:

```sql
SELECT l.id, l.created_at, l.post_id, l.topic_id, l.response_tokens AS stored_tokens, g.sent_tokens
FROM ai_api_audit_logs l
CROSS JOIN LATERAL (
  SELECT MAX((m)[1]::int) AS sent_tokens
  FROM regexp_matches(l.raw_response_payload, '"candidatesTokenCount":\s*(\d+)', 'g') AS m
) g
WHERE l.language_model LIKE 'gemini%'
  AND l.raw_response_payload ~ '"finishReason":\s*"STOP"'
  AND g.sent_tokens <> l.response_tokens
ORDER BY l.created_at

```

## Impacto

O defeito depende apenas de onde os limites dos chunks de rede caem, portanto não é específico para tradução ou para uma camada de serviço; qualquer conclusão do Gemini transmitida em streaming pode perder seu final dessa maneira. Para tradução, o resultado é uma publicação ou título encurtado silenciosamente que o backfill não reprocessa, porque uma linha de localização existe.
