# Discourse AI: ошибка Gemini в потоковом ответе игнорируется, а частичный текст сохраняется как полный перевод

**URL:** <https://meta.discourse.org/t/discourse-ai-a-gemini-error-event-inside-a-streamed-reply-is-ignored-and-the-partial-text-is-stored-as-a-complete-translation/413854>\
**Category:** Bug\
**Tags:** ai\
**Created:** [03.Октябрь.2026 16:15:59 UTC](https://meta.discourse.org/t/discourse-ai-a-gemini-error-event-inside-a-streamed-reply-is-ignored-and-the-partial-text-is-stored-as-a-complete-translation/413854 "2026-10-03T16:15:59Z")\
**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:** [03.Октябрь.2026 16:15:59 UTC](https://meta.discourse.org/t/discourse-ai-a-gemini-error-event-inside-a-streamed-reply-is-ignored-and-the-partial-text-is-stored-as-a-complete-translation/413854/1 "2026-10-03T16:15:59Z")

</div>

## Описание

Когда API Gemini от Google прерывается на полпути во время потоковой передачи ответа, Discourse AI считает ответ завершенным. При включенном переводе с помощью ИИ фрагмент, полученный до сбоя, сохраняется как завершённый перевод поста или названия темы. Ошибка не фиксируется в логах, и повторная обработка (backfill) никогда не повторяет запрос для этого элемента, так как строка перевода уже существует.

Проблема наблюдалась на самохостинговом сайте, работающем на `release/2026.7` (2026.7.3, коммит `f1caa6321287f918644fba9ff8943579fdcf027a`) с включенным плагином discourse-ai. В качестве LLM используется `gemini-3.8-flash` через провайдера Google, при этом уровень сервиса (service tier) установлен на `flex`. Уровень Flex — это тот, на котором Google сбрасывает нагрузку, поэтому такие сбои в середине потока происходят регулярно; однако описанная ниже обработка не зависит от уровня сервиса.

В таких случаях Google отвечает с HTTP-статусом 200, передает одно или несколько обычных событий, а затем отправляет событие об ошибке в том же потоке:

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

```

В журнале аудита (audit log) фиксируется `response_status` 200 и небольшое количество `response_tokens`. Примеры сохраненных данных: пост из 917 символов сохранен как 26 символов (только строка приветствия), посты, содержащие только ссылки, сохранены как первые 8–24 символа URL, а названия тем оборваны посреди слова.

Замеры на этом сайте: 23 из 394 запросов на перевод на уровне Flex (5,8%) завершились именно так. Ни один из 952 запросов на стандартном уровне к той же модели не завершился подобным образом.

## Корневая причина

1. `DiscourseAi::Completions::Endpoints::Gemini#decode_chunk` считывает только `candidates` из каждого разобранного события потока. Событие, у которого верхнеуровневый ключ — `error`, не возвращает частей (parts) и пропускается без какой-либо проверки.
2. `DiscourseAi::Completions::Endpoints::Base` определяет, является ли результат успехом, требует повторной попытки или является ошибкой, исключительно по HTTP-статусу (`response.code.to_i != 200`). Поскольку здесь статус равен 200, существующая логика повторных попыток для 503 никогда не срабатывает.
3. Затем поток завершается нормально, и вызывающая сторона получает накопленный до этого текст. `DiscourseAi::Translation::PostLocalizer` и `TopicLocalizer` сохраняют его.

Такой же код присутствует в ветке `main` на 03.10.2026.

## Предлагаемое исправление

Считать объект `error` верхнего уровня в потоковом событии Gemini ошибкой выполнения (failed completion): отбрасывать частичный вывод и вызывать `CompletionFailed`, чтобы существующая обработка повторных попыток применялась к кодам, допускающим повтор (таким как 503), и чтобы вызывающие стороны никогда не получали фрагмент как полный ответ.

## Воспроизведение

Сбой зависит от нагрузки на стороне Google, поэтому его невозможно вызвать принудительно. На сайте, использующем модель Gemini на уровне Flex с переводом с помощью ИИ, затронутые запросы можно найти постфактум в журнале аудита:

```sql
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

```

Каждая строка представляет собой запрос, который вернул HTTP 200, сгенерировал некоторый вывод, а затем содержал событие об ошибке. Соответствующие строки в `post_localizations` или `topic_localizations` содержат усеченный текст.

## Обходное решение

Перенос агентов перевода на стандартный уровень сервиса прекратил появление новых случаев. Сохраненные фрагменты пришлось найти с помощью запроса выше, удалить и перевести заново.
