يكون طلب البث (Streaming request) آمناً لإعادة التشغيل فقط عندما تتحقق ثلاثة أمور في آن واحد: لم يصل أي شيء إلى عميلك (Client)، ولم يتم قياس أي شيء قابل للملاحظة (Metered)، ولا يحمل الطلب أي حالة على جانب الخادم (Server-side state). بعد حدث المخرج الأول، الخطوة الصحيحة هي الإبلاغ عن الفشل بدلاً من إعادة تشغيل الطلب.
تطبق TokenLab هذه القاعدة على بوابتها (Gateway) لبث Responses API، عبر كل من HTTP وWebSocket. تم تغيير مسار WebSocket في 2026-09-28 ليتوافق مع HTTP.
لماذا يختلف البث عن الطلب العادي
يعيد استدعاء غير البث (Non-streaming call) نصاً (Body) أو خطأً. يمكنك إعادة محاولة الخطأ لأنك لم تحصل على أي شيء.
أما البث فيقدم لك مخرجات قبل انتهاء الطلب. حدث المخرج الأول هو نقطة اللاعودة. إذا انقطع الاتصال بعد ذلك، فستحتفظ بنص جزئي. إعادة تشغيل الطلب تعني توليد نفس الإجابة مرة أخرى والدفع مقابلها مرتين. قد تقوم أيضاً بتكرار استدعاء أداة (Tool call) قام وكيلك بتنفيذه بالفعل.
ينص دليل بث TokenLab على ذلك مباشرة:
بعد وصول الحدث الأول، يعتبر البث المتقطع غير مكتمل ولا يتم إعادة تشغيله تلقائياً.
لذا يحتاج عميلك إلى حالة محلية واحدة: saw_output. تتحول إلى true في اللحظة التي تصل فيها أي مخرجات إلى الكود الخاص بك. كل قرار بإعادة المحاولة يقرأ هذه الحالة أولاً.
البث الذي ينتهي بدون response.completed هو فشل. لا تفترض أن النص الذي لديك مكتمل. تعامل مع أحداث response.failed وresponse.incomplete وerror.
قرار إعادة التشغيل، نقطة بنقطة
تعيد TokenLab تشغيل الطلب مرة واحدة على مسار متاح آخر عندما تتحقق كل هذه الشروط: الطلب عديم الحالة (Stateless). لم يصل أي شيء إلى العميل. لم يتم ملاحظة أي نتيجة أو استخدام للمحاولة الفاشلة. والفشل إما حدث قابل لإعادة المحاولة قبل المخرجات أو خطأ قراءة في المنبع (Upstream) قبل الحدث الأول. تحدث إعادة تشغيل واحدة كحد أقصى لكل طلب. إذا فشل البديل قبل المخرجات أيضاً، فلا يتم إعادة تشغيل ذلك الفشل مرة أخرى.
المصدر: دليل بث TokenLab وسلوك البوابة، كما لوحظ في 2026-09-28.
| نقطة الفشل | هل تعيد TokenLab التشغيل؟ | السبب |
|---|---|---|
حدث قابل لإعادة المحاولة قبل المخرجات (response.failed، أو حدث error محدد كقابل لإعادة المحاولة، مثل خطأ في المنبع بسبب التحميل الزائد أو خطأ داخلي) |
نعم، مرة واحدة، إذا كان الطلب عديم الحالة | لم يصل شيء إلى العميل ولم يتم ملاحظة أي استخدام، لذا فإن التنفيذ الثاني غير مرئي. |
| انقطاع بث المنبع (خطأ قراءة) قبل الحدث الأول | نعم، مرة واحدة، إذا كان الطلب عديم الحالة | نفس النطاق. العميل لا يحتفظ بأي مخرجات ولا توجد تكلفة. |
| فشل ثانٍ قبل المخرجات، بعد إعادة تشغيل واحدة | لا | الميزانية هي إعادة تشغيل واحدة لكل طلب. |
| أي فشل بعد وصول المخرجات إلى العميل | لا | العميل يحتفظ بالفعل بنص جزئي. إعادة التشغيل ستؤدي لتكرار المخرجات والتكلفة. |
استجابة مخزنة (store)، أو استكمال (previous_response_id)، أو طلب مرتبط بالأصل |
لا | التنفيذ الثاني قد ينشئ استجابة مخزنة ثانية أو يغير حالة المحادثة. |
| انتهاء مهلة الحدث الأول | لا | قد لا يزال المنبع في مرحلة التوليد. إعادة التشغيل قد تنفذ نفس العمل مرتين بينما تستمر المحاولة الأولى. |
| تجاوز سعة المخزن المؤقت قبل المخرجات | لا | الحد محدود محلياً في البوابة. من المرجح جداً أن يصطدم نفس البادئة كبيرة الحجم به مرة أخرى في المسار التالي. |
| انقطاع اتصال العميل | لا | توقف العميل عن الاستماع. |
| فشل حتمي، على سبيل المثال طلب غير صالح | لا | إعادة المحاولة لا يمكنها تغيير النتيجة. يتم تسليمه كما هو. |
| فشل يتضمن بالفعل استخداماً (Usage) | لا | تم قياس المحاولة. يتم تسليمه كما هو. |
| لا يوجد مسار آخر متبقٍ | لا | لا يوجد مكان لإرساله. يحصل العميل على الفشل مع الكود الخاص به. |
عندما لا يتم إعادة تشغيل الفشل، أو لا يتبقى مسار آخر، تتلقاه مع كود الخطأ الخاص به. أمثلة عامة: stream_read_error عند انقطاع بث المنبع، وupstream_stream_buffer_limit عند تجاوز سعة المخزن المؤقت. إذا فشل اختيار المسار نفسه بعد قرار إعادة التشغيل، تنتهي دورة WebSocket بـ websocket_response_failed (الحالة 500) ويتم استرداد التكلفة المحجوزة.
تتبع الفوترة نفس الخط. أنت تدفع مقابل المحاولة المسلمة فقط. قد يكون الطلب الذي تمت إعادة تشغيله قد نُفذ في المنبع مرتين، وتلك التكلفة الإضافية في المنبع تتحملها TokenLab، لأنه لم يصلك شيء من المحاولة الأولى. الدورة الفاشلة التي لا تسلم شيئاً يتم استرداد تكلفتها.
تفصيلة توقيت واحدة تهم معالجة الأخطاء لديك. قبل بدء المخرجات، تحتفظ البوابة بـ response.created وresponse.in_progress حتى وصول حدث المخرج الأول أو حدوث فشل، لمدة أقصاها 10 ثوانٍ. تصل تلك الأحداث المحتجزة إليك مع المخرج الأول، أو مع الحدث النهائي. الترتيب والمحتوى لا يتغيران. أنت تراهم فقط متأخرين قليلاً. تلك الـ 10 ثوانٍ هي حد أقصى، وليست تأخيراً نموذجياً.
ما الذي تغير في WebSocket في 2026-09-28
تقدم TokenLab خدمة Responses API عبر بث HTTP ("stream": true، أحداث مرسلة من الخادم) وعبر WebSocket على wss://api.tokenlab.sh/v1/responses، حيث يرسل العميل أحداث response.create. استجابات WebSocket يتم بثها دائماً. وهي لا تدعم background أو response.cancel. يتعامل كل اتصال مع استجابة نشطة واحدة في كل مرة لمدة تصل إلى 60 دقيقة.
قبل التغيير، كان المساران غير متوافقين. كان HTTP يحتفظ بأحداث دورة الحياة ويعيد تشغيل حالات الفشل عديمة الحالة قبل المخرجات. بينما كان WebSocket يرسل response.created على الفور ويسلم حالات الفشل قبل المخرجات إلى العميل، مع استرداد تكلفتها. نفس الخلل في المنبع أنتج إجابة نظيفة على HTTP وخطأ على WebSocket.
يتبع مسار WebSocket الآن قاعدة HTTP، بما في ذلك إعادة تشغيل البث الذي ينقطع قبل وصول أي حدث. داخلياً، معظم حالات فشل المنبع التي تظهر في دورات WebSocket تحدث قبل أي مخرجات. هذا هو بالضبط النطاق الذي تكون فيه إعادة التشغيل آمنة.
تعمل البوابة على تحسين حالة الفشل قبل المخرجات. وهي لا تضمن اكتمال البث.
كيف تم شحن التغيير دون كسر السلوكيات الأخرى
اتبع العمل عملية مبنية لاكتشاف تغييرات السلوك الصامتة.
- قفل السلوك. قبل التغيير، تم تسجيل كل سيناريو لدورة WebSocket كـ fixture: الإطارات التي يتلقاها العميل، استدعاءات المنبع التي تم إجراؤها، ونتيجة الفوترة. نمت المجموعة إلى 63 سيناريو مسجلاً أثناء هذا العمل. يجب الإعلان عن تغيير السلوك مسبقاً. فقط الـ fixtures المذكورة في ذلك الإعلان قد تتغير. يجب أن تظل كل fixture أخرى متطابقة بايت ببايت.
- فحوصات الطفرة. تم اختبار كل قاعدة قرار جديدة عن طريق قلبها عمداً، مثل إعادة تشغيل انتهاء مهلة الحدث الأول أو عدم إعادة تشغيل فشل القارئ، والتأكد من فشل القفل.
- مراجعة الاكتشاف. الإصدار الأول جعل حالة تجاوز سعة المخزن المؤقت قابلة لإعادة التشغيل، بناءً على ادعاء التكافؤ مع HTTP. أظهرت المراجعة أن HTTP لا يعيد تشغيل تلك الحالة أبداً، للسبب المذكور في الجدول. استعاد تحديث لاحق السلوك القديم وأضاف سيناريوهات حدودية: فشل قارئ ثانٍ لا يتم إعادة تشغيله، لا يوجد مسار متبقٍ، فشل بعد
response.createdمحتجزة، وبث بديل ينقطع بعد ذلك.
سجلات الطلبات لدورة نجحت بعد إعادة التشغيل تسجل الآن أيضاً المحاولة الفاشلة السابقة، كما كان يفعل HTTP بالفعل.
كود العميل الذي يمتلك قرار إعادة المحاولة
اضبط إعادة المحاولات التلقائية لـ SDK على 0 لاستدعاءات البث. هذا يبقي القرار في الكود الخاص بك. احتفظ بقرار إعادة التشغيل في مكان واحد، وليس موزاً عبر المعالجات. بالنسبة لأخطاء HTTP، احترم retryable وretry_after كما هو موضح في دليل معالجة الأخطاء، واحتفظ بمعرفات الطلبات.
SSE عبر HTTP
import os
from openai import OpenAI
with OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0, # امتلك قرار إعادة المحاولة بدلاً من إعادة إرسال بث مقروء جزئياً
) as client:
completed, saw_output = False, False
with client.responses.create(
model="gpt-5.6-terra",
input="Reply with one short sentence about retries.",
stream=True,
) as stream:
for event in stream:
if event.type == "response.output_text.delta":
saw_output = True
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
completed = True
elif event.type in {"response.failed", "response.incomplete", "error"}:
raise RuntimeError(f"{event.type} after_output={saw_output}")
if not completed:
raise RuntimeError(f"stream closed before response.completed, after_output={saw_output}")
print()
يستخدم المثال OpenAI SDK 2.15.0 مقابل https://api.tokenlab.sh/v1 مع max_retries=0. هو يتتبع saw_output، ويرفع استثناء عند أحداث response.failed وresponse.incomplete وerror، وعند بث يغلق قبل response.completed. تم التحقق منه مقابل الإنتاج في 2026-09-28 مع gpt-5.6-terra.
إذا وصل الفشل مع saw_output == false وكان الطلب مؤهلاً (عديم الحالة، مع فشل قابل لإعادة التشغيل)، فقد قامت TokenLab بإعادة تشغيله مرة واحدة بالفعل؛ الاستجابات المخزنة، الاستكمالات، وانتهاء مهلة الحدث الأول لم يتم إعادة تشغيلها على الإطلاق. قرر على مستوى التطبيق ما إذا كان الطلب الجديد مقبولاً، لأن الطلب الجديد هو توليد جديد. إذا كان saw_output == true، أبلغ عن الفشل وأظهر ما لديك، أو تخلص من النص الجزئي عمداً.
WebSocket
import asyncio
import json
import os
import websockets
URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}
async def run_turn(prompt: str) -> str:
headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
await ws.send(json.dumps({
"type": "response.create",
"model": "gpt-5.6-terra",
"input": prompt,
"store": False,
}))
text, saw_output = [], False
async for raw in ws:
event = json.loads(raw)
kind = event.get("type")
if kind == "response.output_text.delta":
saw_output = True
text.append(event["delta"])
elif kind in TERMINAL:
if kind != "response.completed":
# بعد بدء المخرجات، يعتبر الفشل نهائياً لهذه الدورة.
# أعد الإرسال فقط إذا كان بإمكان تطبيقك التخلص من النص الجزئي.
raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
return "".join(text)
raise RuntimeError(f"socket closed before a terminal event, after_output={saw_output}")
print(asyncio.run(run_turn("Reply with one short sentence about retries.")))
يستخدم المثال websockets 16.0، ويتصل بـ wss://api.tokenlab.sh/v1/responses مع ترويسة Bearer، ويرسل response.create واحدة مع store: false، ويجمع response.output_text.delta. يرفع استثناءً مع after_output عند أي حدث نهائي غير مكتمل أو إغلاق مبكر. تم التحقق منه مقابل الإنتاج في 2026-09-28 مع gpt-5.6-terra.
علامة after_output هي نفس فكرة saw_output. تخبر الكود الخاص بك ما إذا كانت الدورة الجديدة ممكنة حتى بدون تكرار الآثار الجانبية.
قائمة مرجعية لمنطق إعادة المحاولة الخاص بك
- عامل البث الذي ينتهي بدون
response.completedكفشل، في كل مرة. - تتبع قيمة منطقية (boolean) واحدة حول ما إذا كانت المخرجات قد وصلت إلى الكود الخاص بك. قم بتبديلها عند حدث المخرج الأول، وليس عند حدث دورة الحياة الأول.
- الفشل قبل المخرجات في طلب مؤهل قد حصل بالفعل على إعادة تشغيل واحدة من البوابة؛ المحاولة الإضافية هي قرارك.
- بعد مخرجات جزئية، أعد الإرسال فقط إذا كان بإمكان تطبيقك التخلص من النص الجزئي وقبول الدفع مقابل توليدين.
- في حلقات الوكلاء (Agent loops)، تحقق مما إذا كان البث الجزئي يحتوي بالفعل على استدعاء أداة تصرف كودك بناءً عليه. لا تعد تشغيل دورة لا يمكنك التراجع عن آثارها الجانبية.
- بالنسبة للاستجابات المخزنة واستكمالات
previous_response_id، افحص الحالة الموجودة قبل إعادة إرسال أي شيء. - اضبط إعادة محاولات البث على 0 في SDK الخاص بك واحتفظ بقرار إعادة التشغيل في دالة واحدة.
- سجل معرفات الطلبات حتى تتمكن من مطابقة الإجابة المسلمة بالمحاولات التي تسبقها.
الأسئلة الشائعة
هل تعيد TokenLab تشغيل البث بعد مخرجات جزئية؟
لا. بمجرد وصول المخرجات إلى عميلك، يتم الإبلاغ عن الفشل ولا يتم إعادة تشغيله أبداً. أنت تحتفظ بنص جزئي، لذا فإن إعادة التشغيل ستكرر المخرجات والتكلفة. يقرر تطبيقك ما إذا كان سيعرض أو يقتطع أو يتخلص مما لديه.
هل سأُحاسب مرتين إذا أعادت البوابة تشغيل طلبي؟
لا. أنت تدفع مقابل المحاولة المسلمة فقط. قد يكون الطلب الذي تمت إعادة تشغيله قد نُفذ في المنبع مرتين، ولكن لم يصلك شيء من المحاولة الأولى، وتلك التكلفة الإضافية في المنبع تتحملها TokenLab. الدورة الفاشلة التي لا تسلم شيئاً يتم استرداد تكلفتها.
لماذا لا تتم إعادة محاولة انتهاء مهلة الحدث الأول؟
لأن المنبع قد لا يزال في مرحلة التوليد. إعادة التشغيل قد تنفذ نفس العمل مرتين بينما تستمر المحاولة الأولى. يتم التعامل مع انتهاء مهلة الحدث الأول بشكل مختلف عن خطأ القراءة الذي يكسر البث قبل الحدث الأول.
هل يمكنني إعادة محاولة استجابة مخزنة أو استكمال previous_response_id؟
ليس تلقائياً. لا تعيد TokenLab أبداً تشغيل الاستجابات المخزنة، أو الاستكمالات، أو الطلبات المرتبطة بالأصل، لأن التنفيذ الثاني قد ينشئ استجابة مخزنة ثانية أو يغير حالة المحادثة. افحص الحالة الموجودة قبل إعادة الإرسال، ولا تعد الإرسال إلا إذا كان بإمكان تطبيقك التوفيق بين تلك الحالة.
إذا كنت ترغب في مراقبة بث الأحداث الخام بنفسك، أنشئ مفتاح API وسجل كل نوع حدث يتلقاه عميلك. يغطي دليل البث ودليل معالجة الأخطاء مجموعة الأحداث الكاملة. للحصول على خلفية حول كيفية توجيه البوابة واستعادتها، راجع بنية موثوقية TokenLab AI API وResponses API مقابل Chat Completions للوكلاء.
المصادر
- https://docs.tokenlab.sh/guides/streamingتمت المراجعة في 2026-09-28
- https://docs.tokenlab.sh/guides/error-handlingتمت المراجعة في 2026-09-28



