الوصف
عندما تفشل واجهة برمجة تطبيقات Gemini من Google في منتصف رد مُبَثّ (streamed)، تعامل Discourse AI هذا الرد على أنه مكتمل. عند تمكين الترجمة بالذكاء الاصطناعي، يتم تخزين المقطع المستلم قبل حدوث الفشل كترجمة نهائية للموضوع أو عنوان الموضوع. لا يتم تسجيل أي خطأ، ولا يعيد backfill محاولة معالجة العنصر أبداً، لأن صف الترجمة أصبح موجوداً الآن.
تم رصد المشكلة على موقع مضاف ذاتياً يعمل بإصدار release/2026.7 (2026.7.3، الترتيب f1caa6321287f918644fba9ff8943579fdcf027a) مع إضافة discourse-ai المدمجة. النموذج اللغوي هو gemini-3.8-flash عبر مزود Google، مع ضبط فئة الخدمة على 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"}}
يسجل سجل التدقيق (audit log) response_status بقيمة 200 وعدد صغير من response_tokens. أمثلة على ما تم تخزينه: منشور بطول 917 حرفاً تم حفظه كـ 26 حرفاً فقط (سطر التحية فقط)، منشورات تحتوي على روابط فقط تم حفظها كأول 8 إلى 24 حرفاً من الرابط، وعناوين مواضيع مقطوعة في منتصف الكلمة.
تم القياس على هذا الموقع: 23 من أصل 394 استدعاء ترجمة لفئة Flex تمت الإجابة عليها (5.8%) انتهت بهذه الطريقة. لم يحدث ذلك في أي من 952 استدعاء لفئة القياسية (standard-tier) لنفس النموذج.
السبب الجذري
DiscourseAi::Completions::Endpoints::Gemini#decode_chunkيقرأcandidatesفقط من كل حدث بث مُفكَّك. الحدث الذي يكون مفتاحه العلويerrorلا يُنتج أي أجزاء ويتم تخطيه دون أي فحص.DiscourseAi::Completions::Endpoints::Baseيقرر بين النجاح وإعادة المحاولة والفشل بناءً على حالة HTTP فقط (response.code.to_i != 200). الحالة هنا 200، لذا لا يتم الوصول إلى منطق إعادة المحاولة الموجود مسبقاً للرمز 503.- ينتهي البث بعد ذلك بشكل طبيعي، ويستلم المستدعي النص المتراكم حتى الآن. تقوم
DiscourseAi::Translation::PostLocalizerوTopicLocalizerبحفظه.
يوجد نفس الكود في main حتى تاريخ 2026-10-03.
الإصلاح المقترح
اعتبار كائن error في المستوى العلوي لحدث Gemini المُبَثّ كإكمال فاشل: تخلص من المخرجات الجزئية وأطلق CompletionFailed، بحيث ينطبق معالج إعادة المحاولة الموجود مسبقاً على الرموز القابلة لإعادة المحاولة مثل 503، ولا يستلم المستدعون قطّة كمقطع كإجابة كاملة.
إعادة إنتاج المشكلة
تعتمد الفشل على حمل Google، لذا لا يمكن إجبارها. على موقع يستخدم نموذج Gemini في فئة Flex مع الترجمة بالذكاء الاصطناعي، يمكن العثور على الاستدعاءات المتأثرة لاحقاً في سجل التدقيق:
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 تحتوي على النص المقطوع.
حل مؤقت
نقل عوامل الترجمة (translation agents) إلى فئة الخدمة القياسية (standard service tier) أوقف حدوث حالات جديدة. كان لا بد من العثور على المقاطع المخزنة باستخدام الاستعلام أعلاه، وحذفها، وترجمتها مرة أخرى.