Discourse AI: Gemini request blocked via promptFeedback.blockReason is treated as an empty reply and never translated

Description

When Google’s Gemini API refuses a request with promptFeedback.blockReason, Discourse AI handles the response as a successful but empty reply. In AI translation this has two effects: the topic or post never receives a detected language, so it is never translated, and the backfill sends the same blocked request again on later runs. Nothing indicates to an administrator that a block occurred.

Observed on a self-hosted site running release/2026.7 (2026.7.3, commit f1caa6321287f918644fba9ff8943579fdcf027a) with the bundled discourse-ai plugin. The request was the language detection of a topic title, using the built-in locale detector agent on gemini-3.5-flash-lite. The title was a short, harmless sentence in Swedish. The same title was translated by gemini-3.8-flash without a block.

Google answered with HTTP 200 and a single stream event with no candidates:

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

The request already carries BLOCK_NONE for the four safety categories that Discourse AI sets, so this block is not one those settings influence. The audit log records response_status 200.

Root cause

  1. DiscourseAi::Completions::Endpoints::Gemini#decode_chunk reads only candidates from each stream event. promptFeedback is never inspected, so a blocked request produces an empty result with no error.
  2. DiscourseAi::Translation::LanguageDetector#detect returns nil when the reply does not match its language-tag pattern, which is the case for an empty reply. The topic or post keeps locale NULL.
  3. Jobs::TopicsLocaleDetectionBackfill selects topics with locale NULL on every run and has no attempt limit, so by reading the code the blocked request is repeated every five minutes for as long as the topic is in the backfill window. Posts are limited to two detection attempts a day by the relocalize quota, and are retried each day.
  4. Translation candidates require a detected locale, so the item is never translated.

The same code is present on main as of 2026-10-03.

Suggested fix

  1. Detect promptFeedback.blockReason in the Gemini endpoint and raise or log a distinct, visible error that names the reason.
  2. Record the failure against the topic or post so the backfill stops repeating a request that will be blocked again, or apply an attempt limit to topic detection as already exists for posts.
  3. Surface blocked items on the translation admin page, so an administrator can set the language by hand.

Reproduce

The block depends on Google’s filter and cannot be forced with a known input. Affected calls can be found in the audit log:

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

Workaround

Setting the topic’s language by hand lets translation proceed. On this site the translation model did not block the same text.