رفتن به محتوا

راهنماها

Stream و رویدادها

متن را همزمان با نوشته شدنش نمایش دهید. در این صفحه می‌بینید stream در Responses چه ساختاری دارد، هر رویداد چه معنایی دارد، خروجی را چطور درست کنار هم بگذارید و وقتی اتصال قطع شد چه کنید تا دو بار هزینه ندهید.

چه وقت از stream استفاده کنیم

بدون stream، کلاینت منتظر می‌ماند تا کل پاسخ آماده شود و بعد یک شیء JSON دریافت می‌کند. با stream، مهوش پاسخ را تکه‌تکه و به محض آماده شدن می‌فرستد و کاربر اولین کلمه‌ها را خیلی زودتر می‌بیند.

  • برای رابط‌های گفت‌وگو، نوشتن زنده و هر جایی که کاربر منتظر دیدن پاسخ است، از stream استفاده کنید.
  • برای کارهای انبوه، دسته‌بندی‌های کوتاه یا ارتباط سرور با سرور که فقط نتیجهٔ نهایی اهمیت دارد، stream لازم نیست. تکرار request بدون stream ساده‌تر است و با idempotency key می‌توان پاسخ ذخیره‌شده را دوباره گرفت.
  • برای کارهای طولانی که کسی منتظرشان نمی‌ماند، به‌جای stream از اجرای پس‌زمینه استفاده کنید. توضیحش در شیوهٔ اجرای درخواست در مهوش آمده است.

شروع stream در Responses

در body درخواست POST /v1/responses مقدار stream: true را بفرستید. فرستادن header Accept: text/event-stream هم نیت شما را برای proxyها و کتابخانه‌های HTTP روشن می‌کند.

curl -N https://api.mahvashai.ir/v1/responses \
  -H "Authorization: Bearer $MAHVASH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"model": "auto", "input": "Write a haiku about Tehran rain.", "stream": true}'

پاسخ با قالب Server-Sent Events فرستاده می‌شود: هر رویداد یک خط data: است که یک شیء JSON دارد و پس از آن یک خط خالی می‌آید. هر شیء فیلدهای type و sequence_number را دارد. بعد از رویداد پایانی، stream با خط data: [DONE] بسته می‌شود.

data: {"type":"response.created","sequence_number":1,"response":{"id":"resp_<uuid>","object":"response","status":"in_progress","output":[]}}

data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_<id>","type":"message","role":"assistant","status":"in_progress","content":[]}}

data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_<id>","output_index":0,"content_index":0,"part":{"type":"output_text","text":""}}

data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_<id>","output_index":0,"content_index":0,"delta":"Hel"}

data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_<id>","output_index":0,"content_index":0,"delta":"lo!"}

data: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_<id>","output_index":0,"content_index":0,"text":"Hello!"}

data: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_<uuid>","status":"completed","output":[...],"usage":{...}}}

data: [DONE]

نکته:برای آزمایش stream در ترمینال از curl -N استفاده کنید تا خروجی بافر نشود. همچنین مطمئن شوید proxyهای بین سرور شما و مهوش پاسخ‌های text/event-stream را بافر نمی‌کنند.

ساختار یک stream

همهٔ streamهای Responses از یک الگوی ثابت پیروی می‌کنند:

  1. ۱response.created: request پذیرفته شده است. این رویداد شناسهٔ response را با وضعیت in_progress دارد. همین حالا شناسه را ذخیره کنید؛ اگر اتصال بعداً قطع شود، با همین شناسه می‌توانید نتیجه را پیدا کنید.
  2. ۲response.output_item.added: یک آیتم تازه در خروجی شروع می‌شود؛ مثل یک پیام، فراخوانی تابع، خلاصهٔ استدلال یا فراخوانی ابزار. جایگاه آن با output_index مشخص می‌شود.
  3. ۳response.content_part.added: یک بخش محتوا داخل آیتم پیام شروع می‌شود که با content_index مشخص است.
  4. ۴رویدادهای delta، مثل response.output_text.delta، تکه‌های کوچک محتوا را می‌آورند که باید به متن اضافه شوند.
  5. ۵رویدادهای .done، مثل response.output_text.done، response.content_part.done و response.output_item.done، مقدار نهایی یک بخش یا آیتم را می‌آورند.
  6. ۶در پایان یکی از رویدادهای response.completed، response.incomplete یا response.failed می‌آید که شیء نهایی response را دارد.

شناسهٔ آیتم‌های خروجی ثابت است: آیتمی که هنگام stream اعلام می‌شود، همان آیتمی است که در response نهایی و در فراخوانی بعدی GET /v1/responses/{id} می‌بینید.

فهرست رویدادهای Responses

جدول زیر رویدادهایی را فهرست می‌کند که مهوش با قالبی یکسان می‌فرستد. رویدادهای مربوط به ابزار، رسانه و استدلال فقط وقتی می‌آیند که request و مسیر انتخاب‌شده آن‌ها را تولید کنند.

رویدادتوضیحات
response.createdrequest پذیرفته شد؛ وضعیت response برابر in_progress است.
response.output_item.addedیک آیتم تازه در خروجی شروع شد.
response.content_part.addedبخش تازه‌ای از محتوای پیام شروع شد.
response.output_text.deltaاین تکه را به متن همان آیتم و همان content_index اضافه کنید.
response.refusal.deltaتکهٔ تازه‌ای از متن امتناع؛ آن را به متن قبلی اضافه کنید.
response.reasoning_summary_part.addedبخش تازه‌ای از خلاصهٔ استدلال شروع شد.
response.reasoning_summary_text.deltaتکهٔ تازه‌ای از متن یا خلاصهٔ استدلال، اگر ارسال شود.
response.reasoning_text.deltaتکهٔ تازه‌ای از متن یا خلاصهٔ استدلال، اگر ارسال شود.
response.function_call_arguments.deltaتکهٔ تازه‌ای از آرگومان‌های فراخوانی تابع؛ JSON را فقط بعد از رویداد done پردازش کنید.
response.function_call_arguments.doneآرگومان‌های فراخوانی تابع کامل شد.
response.audio.deltaبخشی از دادهٔ صوتی رسید.
response.audio.doneخروجی صوتی کامل شد.
response.image_generation_call.in_progressپیشرفت ساخت تصویر یا یک تصویر نیمه‌کاره.
response.image_generation_call.generatingپیشرفت ساخت تصویر یا یک تصویر نیمه‌کاره.
response.image_generation_call.partial_imageپیشرفت ساخت تصویر یا یک تصویر نیمه‌کاره.
response.image_generation_call.completedساخت تصویر تمام شد.
response.web_search_call.in_progressجست‌وجوی وب شروع شد، در حال انجام است یا تمام شد.
response.web_search_call.searchingجست‌وجوی وب شروع شد، در حال انجام است یا تمام شد.
response.web_search_call.completedجست‌وجوی وب شروع شد، در حال انجام است یا تمام شد.
response.tool_call.deltaابزار ارائه‌دهنده بخشی از پیشرفت کارش را فرستاد.
response.output_text.doneمتن نهایی این بخش از محتوا.
response.refusal.doneمتن نهایی امتناع در این بخش.
response.reasoning_summary_text.doneخلاصهٔ استدلال کامل شد.
response.reasoning_summary_part.doneخلاصهٔ استدلال کامل شد.
response.content_part.doneاین بخش از محتوا کامل شد.
response.output_item.doneاین آیتم خروجی کامل شد.
response.completedرویداد پایانی موفق، همراه با response نهایی.
response.incompleteرویداد پایانی برای کاری که لغو شده یا نیمه‌تمام مانده است.
response.failedرویداد پایانی برای کاری که ناموفق بوده است.

ممکن است به‌مرور رویدادهای تازه‌ای اضافه شوند. کلاینت شما باید رویدادهای ناشناخته را نادیده بگیرد و هرگز به خاطر آن‌ها stream را شکست‌خورده حساب نکند.

کنار هم گذاشتن درست خروجی

یک کلاینت قابل‌اعتماد این قواعد را رعایت می‌کند:

  • بر اساس `type` تصمیم بگیرید. معنای هیچ رویدادی را از جایگاهش در stream حدس نزنید.
  • رویدادها را به ترتیب `sequence_number` اعمال کنید.
  • محتوا را بر اساس جایگاهش نگه دارید. هر delta را به متن output_index و content_index خودش اضافه کنید. یک response ممکن است چند آیتم و چند بخش داشته باشد و deltaهای آیتم‌های مختلف درهم برسند.
  • پیش از خواندن مقدارهای ساختاریافته منتظر `.done` بمانید. آرگومان‌های function call به‌صورت تکه‌های ناقص JSON می‌رسند؛ فقط بعد از response.function_call_arguments.done آن‌ها را parse کنید.
  • رویداد پایانی ملاک است. response داخل response.completed وضعیت نهایی و معتبر است. آنچه نمایش داده‌اید را با آن تطبیق دهید و id و usage آن را ذخیره کنید.

اگر از SDKهای رسمی استفاده می‌کنید، خواندن رویدادها با خود SDK است. بدون SDK هم یک تابع کوچک کافی است:

// A dependency-free SSE reader for fetch(). Frames are separated by a blank line.
export async function* readEvents(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) return;
    buffer += decoder.decode(value, { stream: true });
    const frames = buffer.split(/\r?\n\r?\n/);
    buffer = frames.pop() ?? "";
    for (const frame of frames) {
      const data = frame.split(/\r?\n/)
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice(5).trimStart())
        .join("\n");
      if (!data) continue;
      if (data === "[DONE]") return;
      yield JSON.parse(data);
    }
  }
}

// Build text per output item and content part, keyed by their indexes.
const parts = new Map();
let terminal = null;
for await (const event of readEvents(response)) {
  if (event.type === "response.output_text.delta") {
    const key = event.output_index + ":" + event.content_index;
    parts.set(key, (parts.get(key) ?? "") + event.delta);
  }
  if (["response.completed", "response.incomplete", "response.failed"].includes(event.type)) {
    terminal = event;
  }
}
if (!terminal) {
  // The connection closed without an outcome. Do not assume success or failure.
}

رویدادهای پایانی و قطع اتصال

هر stream سالم دقیقاً با یکی از این رویدادها تمام می‌شود:

  • response.completed: پاسخ کامل است.
  • response.incomplete: تولید پاسخ زودتر متوقف شده؛ مثلاً به سقف max_output_tokens رسیده است. خروجی ناقص معتبر است؛ incomplete_details را بررسی کنید و تصمیم بگیرید آن را نمایش دهید یا ادامه‌اش را بخواهید.
  • response.failed: اجرای پاسخ ناموفق بوده است. پیام خطا نشان دهید، X-Request-ID را نگه دارید و متن ناقص را به‌عنوان پاسخ کامل نمایش ندهید.

اگر اتصال پیش از رسیدن رویداد پایانی قطع شود، نتیجه نامعلوم است: ممکن است مدل کارش را تمام کرده باشد، شکست خورده باشد یا هنوز در حال کار باشد. آن را موفق فرض نکنید و همان prompt را هم بلافاصله دوباره نفرستید؛ چون ممکن است یک اجرای پولی دیگر شروع شود. به‌جای آن، با شناسه‌ای که از response.created ذخیره کرده‌اید، response را از GET /v1/responses/{id} بگیرید و بر اساس وضعیت واقعی‌اش ادامه دهید.

مهم:requestهای stream را نمی‌توان با idempotency key دوباره گرفت (409 idempotent_stream_not_replayable). بعد از قطع اتصال، تنها راه، دریافت response با شناسهٔ آن است.

stream در Chat Completions فرق دارد

POST /v1/chat/completions با stream: true از قالب قدیمی‌تری استفاده می‌کند. هر خط data: یک chat.completion.chunk است که تکهٔ بعدی پیام را در choices[].delta دارد و این stream هم با data: [DONE] تمام می‌شود.

data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

این دو قالب جای هم به کار نمی‌روند. برای /v1/responses از خوانندهٔ رویدادهای Responses و برای /v1/chat/completions از خوانندهٔ مخصوص chunkها استفاده کنید.

رساندن stream به مرورگر

مرورگر هرگز نباید با API key شما مستقیم به مهوش وصل شود. stream را در بک‌اند باز کنید و فقط چیزی را که رابط کاربری لازم دارد به مرورگر برسانید:

  • اول کاربر خودتان را احراز کنید و بعد stream مهوش را با API key پروژه باز کنید.
  • تکه‌های متن (یا رویدادهای ساده‌شدهٔ خودتان) را از طریق SSE یا WebSocket به مرورگر بفرستید.
  • شناسهٔ response را از response.created در سمت خودتان ذخیره کنید تا اگر کاربر صفحه را دوباره بارگذاری کرد، بتوانید نتیجهٔ نهایی را نشان دهید.
  • اگر اتصال مرورگر قطع شد، آگاهانه تصمیم بگیرید که stream مهوش را تا آخر بخوانید یا نه، تا گفت‌وگوی ذخیره‌شده ناقص نماند.

سناریوی چت تعاملی پشتیبانی یک پیاده‌سازی کامل از همین الگو است.