설명
Google의 Gemini API가 스트리밍된 응답 도중 실패하면, Discourse AI는 해당 응답을 완료된 것으로 처리합니다. AI 번역이 활성화된 경우, 실패 전에 수신된 조각은 게시글 또는 주제 제목의 최종 번역으로 저장됩니다. 오류로 기록되지 않으며, 번역 행이 이미 존재하기 때문에 백필(backfill)이 해당 항목을 다시 시도하지 않습니다.
release/2026.7 (2026.7.3, 커밋 f1caa6321287f918644fba9ff8943579fdcf027a)을 실행 중인 자체 호스팅 사이트에서 번들된 discourse-ai 플러그인과 함께 관찰되었습니다. LLM은 Google 프로바이더를 통해 gemini-3.8-flash이며, 서비스 티어는 flex로 설정되어 있습니다. Flex는 Google이 부하를 분산하는 티어로, 이러한 스트리밍 중 실패가 정기적으로 발생합니다. 아래에서 설명하는 처리 방식은 티어에 의존하지 않습니다.
이러한 경우 Google은 HTTP 200으로 응답하며, 하나 이상의 정상 이벤트를 스트리밍한 후 동일한 스트림에서 오류 이벤트를 전송합니다:
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"}}
감사 로그에는 response_status 200과 작은 response_tokens 수가 기록됩니다. 저장된 예시로는: 917자 게시글이 26자로 저장된 경우(인사말 줄만), 링크만 있는 게시글이 URL의 첫 8~24자로 저장된 경우, 주제 제목이 단어 중간에서 잘린 경우 등이 있습니다.
해당 사이트에서 측정한 결과: Flex 티어 번역 호출 394건 중 23건(5.8%)이 이러한 방식으로 종료되었습니다. 동일한 모델에 대한 표준 티어 952건 중에서는 해당 사례가 없었습니다.
근본 원인
DiscourseAi::Completions::Endpoints::Gemini#decode_chunk는 각 파싱된 스트림 이벤트에서candidates만 읽습니다. 최상위 키가error인 이벤트는 어떤 부분도 생성하지 않으며, 어떤 검사 없이 건너뜁니다.DiscourseAi::Completions::Endpoints::Base는 HTTP 상태 코드만으로(response.code.to_i != 200) 성공, 재시도, 실패를 결정합니다. 여기서는 상태가 200이므로, 503에 대한 기존 재시도 로직에는 도달하지 않습니다.- 스트림은 정상적으로 종료되며, 호출자는 지금까지 누적된 텍스트를 수신합니다.
DiscourseAi::Translation::PostLocalizer와TopicLocalizer는 이를 저장합니다.
동일한 코드는 2026-10-03 기준 main 브랜치에도 존재합니다.
제안된 수정 사항
스트리밍된 Gemini 이벤트의 최상위 error 객체를 실패한 완료로 취급하십시오: 부분적인 출력을 폐기하고 CompletionFailed를 발생시켜, 503과 같은 재시도 가능한 코드에 기존 재시도 처리가 적용되고 호출자가 조각을 완전한 응답으로 수신하지 않도록 합니다.
재현 방법
실패는 Google의 부하에 의존하므로 강제할 수 없습니다. Flex 티어의 Gemini 모델을 사용하여 AI 번역을 사용하는 사이트에서, 영향을 받은 호출은 나중에 감사 로그에서 찾을 수 있습니다:
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의 해당 행에는 잘린 텍스트가 저장되어 있습니다.
우회 방법
번역 에이전트를 표준 서비스 티어로 이동하면 새로운 발생이 중단되었습니다. 저장된 조각은 위의 쿼리를 사용하여 찾아 삭제하고 다시 번역해야 했습니다.