Discourse AI: un evento di errore di Gemini all'interno di una risposta in streaming viene ignorato e il testo parziale viene salvato come traduzione completa

Descrizione

Quando l’API Gemini di Google fallisce a metà di una risposta in streaming, Discourse AI tratta la risposta come completa. Con la traduzione AI abilitata, il frammento ricevuto prima del fallimento viene salvato come traduzione finale del post o del titolo dell’argomento. Nessun errore viene registrato nei log e il backfill non riprova mai l’elemento, poiché ora esiste una riga di traduzione.

Osservato su un sito self-hosted in esecuzione su release/2026.7 (2026.7.3, commit f1caa6321287f918644fba9ff8943579fdcf027a) con il plugin discourse-ai incluso. Il modello LLM è gemini-3.8-flash tramite il provider Google, con il livello di servizio impostato su flex. Flex è il livello in cui Google riduce il carico, quindi produce regolarmente questi fallimenti a metà dello stream; la gestione descritta di seguito non dipende dal livello.

In questi casi, Google risponde con HTTP 200, trasmette uno o più eventi normali e quindi invia un evento di errore nello stesso stream:

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"}}

Il registro di audit registra response_status 200 e un piccolo conteggio di response_tokens. Esempi di ciò che è stato salvato: un post di 917 caratteri salvato come 26 caratteri (solo la sua riga di saluto), post contenenti solo link salvati come i primi 8-24 caratteri dell’URL e titoli degli argomenti tagliati a metà parola.

Misurato su questo sito: 23 su 394 chiamate di traduzione di livello Flex (5,8%) si sono concluse in questo modo. Nessuna delle 952 chiamate di livello standard allo stesso modello lo ha fatto.

Causa principale

  1. DiscourseAi::Completions::Endpoints::Gemini#decode_chunk legge solo candidates da ciascun evento di stream analizzato. Un evento la cui chiave di livello superiore è error non produce parti e viene ignorato senza alcun controllo.
  2. DiscourseAi::Completions::Endpoints::Base decide tra successo, retry e fallimento basandosi esclusivamente sullo stato HTTP (response.code.to_i != 200). Lo stato qui è 200, quindi la logica di retry esistente per 503 non viene mai raggiunta.
  3. Lo stream termina quindi normalmente e il chiamante riceve il testo accumulato finora. DiscourseAi::Translation::PostLocalizer e TopicLocalizer lo salvano.

Lo stesso codice è presente su main al 2026-10-03.

Correzione suggerita

Trattare un oggetto error di livello superiore in un evento Gemini in streaming come un completamento fallito: scartare l’output parziale e sollevare CompletionFailed, in modo che la gestione del retry esistente si applichi a codici riprovabili come 503 e i chiamanti non ricevano mai un frammento come risposta completa.

Riproduzione

Il fallimento dipende dal carico di Google, quindi non può essere forzato. Su un sito che utilizza un modello Gemini di livello Flex con la traduzione AI, le chiamate interessate possono essere trovate successivamente nel registro di audit:

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

Ogni riga è una chiamata che ha restituito HTTP 200, ha prodotto un certo output e ha quindi trasportato un evento di errore. Le righe corrispondenti in post_localizations o topic_localizations contengono il testo troncato.

Soluzione temporanea

Spostare gli agenti di traduzione sul livello di servizio standard ha impedito nuove occorrenze. I frammenti salvati hanno dovuto essere trovati con la query sopra, eliminati e tradotti di nuovo.

1 Mi Piace