Discourse AI: запрос Gemini, заблокированный через promptFeedback.blockReason, обрабатывается как пустой ответ и не переводится

Описание

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

Проблема наблюдалась на self-hosted сайте, работающем на версии release/2026.7 (2026.7.3, коммит f1caa6321287f918644fba9ff8943579fdcf027a) с встроенным плагином discourse-ai. Запросом было определение языка заголовка темы с использованием встроенного агента детектора локали на базе gemini-3.5-flash-lite. Заголовок представлял собой короткое безобидное предложение на шведском языке. Тот же заголовок был успешно переведен моделью gemini-3.8-flash без блокировки.

Google ответил с HTTP 200 и одним потоковым событием без кандидатов:

data: {"promptFeedback": {"blockReason": "PROHIBITED_CONTENT"},"usageMetadata": {"promptTokenCount": 417,"totalTokenCount": 417,"promptTokensDetails": [{"modality": "TEXT","tokenCount": 417}],"serviceTier": "standard"},"modelVersion": "gemini-3.5-flash-lite","responseId": "..."}

Запрос уже содержит значение BLOCK_NONE для четырех категорий безопасности, которые устанавливает Discourse AI, поэтому эта блокировка не является следствием этих настроек. В журнале аудита записан response_status 200.

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

  1. Метод DiscourseAi::Completions::Endpoints::Gemini#decode_chunk читает только поле candidates из каждого потокового события. Поле promptFeedback никогда не проверяется, поэтому заблокированный запрос приводит к пустому результату без ошибки.
  2. Метод DiscourseAi::Translation::LanguageDetector#detect возвращает nil, если ответ не соответствует шаблону языкового тега, что и происходит в случае пустого ответа. У темы или сообщения поле locale остается NULL.
  3. Задача Jobs::TopicsLocaleDetectionBackfill выбирает темы с locale NULL при каждом запуске и не имеет ограничения на количество попыток, поэтому, судя по коду, заблокированный запрос повторяется каждые пять минут, пока тема находится в окне повторной обработки. Для сообщений действует ограничение в две попытки определения в день через квоту relocalize, и они повторяются каждый день.
  4. Кандидаты на перевод требуют обнаруженной локали, поэтому элемент никогда не переводится.

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

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

  1. Обнаруживать promptFeedback.blockReason в эндпоинте Gemini и вызывать или записывать в журнал отдельную, видимую ошибку, указывающую на причину.
  2. Записывать сбой для темы или сообщения, чтобы повторная обработка прекратила отправлять запрос, который будет заблокирован снова, либо применить ограничение на количество попыток для определения языка тем, как это уже сделано для сообщений.
  3. Отображать заблокированные элементы на административной странице перевода, чтобы администратор мог установить язык вручную.

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

Блокировка зависит от фильтра Google и не может быть принудительно вызвана известным входным значением. Затронутые вызовы можно найти в журнале аудита:

SELECT id, created_at, language_model, post_id, topic_id, response_status
FROM ai_api_audit_logs
WHERE feature_name = 'translation'
  AND raw_response_payload LIKE '%blockReason%'
ORDER BY created_at

Обходной путь

Ручная установка языка темы позволяет продолжить перевод. На этом сайте модель перевода не блокировала тот же текст.

1 лайк

спасибо за отчёт @Sailor :+1:

будет исправлено в FIX: Handle Gemini errors and blocked prompts reported in successful responses - Pull Request #44262 - discourse/discourse - GitHub

1 лайк

Спасибо за быстрый фикс. После #44262 одна часть этого отчёта остаётся открытой, если судить по диффу.

В результате изменения блокировка детектирования логируется, а для тем ограничена двумя попытками в день. Сам элемент при этом остаётся без локали, поэтому он никогда не переводится и бесконечно учитывается как ожидающий детектирования. В административном интерфейсе нет никакой информации о том, какие именно элементы относятся к этой категории.

За один бэкфилл примерно 6 700 сообщений и их тем на этом сайте возникли три таких случая, все на gemini-3.5-flash-lite с ошибкой PROHIBITED_CONTENT, и все они были безобидны: название темы, односложное сообщение и сообщение, состоящее из одной ссылки.

Два возможных решения:

  1. Запасная локаль, если детектирование заблокировано, например, локаль интерфейса автора или, для ответа, локаль его темы. В том единственном случае, когда локаль была установлена вручную, а элемент оставлен на бэкфилл, модель перевода перевела его без возражений.
  2. Если это невозможно, то список или счётчик элементов, для которых детектирование было заблокировано, на странице администрирования переводов, чтобы локаль можно было установить вручную.