راهنماها
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 از یک الگوی ثابت پیروی میکنند:
- ۱
response.created: request پذیرفته شده است. این رویداد شناسهٔ response را با وضعیتin_progressدارد. همین حالا شناسه را ذخیره کنید؛ اگر اتصال بعداً قطع شود، با همین شناسه میتوانید نتیجه را پیدا کنید. - ۲
response.output_item.added: یک آیتم تازه در خروجی شروع میشود؛ مثل یک پیام، فراخوانی تابع، خلاصهٔ استدلال یا فراخوانی ابزار. جایگاه آن باoutput_indexمشخص میشود. - ۳
response.content_part.added: یک بخش محتوا داخل آیتم پیام شروع میشود که باcontent_indexمشخص است. - ۴رویدادهای delta، مثل
response.output_text.delta، تکههای کوچک محتوا را میآورند که باید به متن اضافه شوند. - ۵رویدادهای
.done، مثلresponse.output_text.done،response.content_part.doneوresponse.output_item.done، مقدار نهایی یک بخش یا آیتم را میآورند. - ۶در پایان یکی از رویدادهای
response.completed،response.incompleteیاresponse.failedمیآید که شیء نهایی response را دارد.
شناسهٔ آیتمهای خروجی ثابت است: آیتمی که هنگام stream اعلام میشود، همان آیتمی است که در response نهایی و در فراخوانی بعدی GET /v1/responses/{id} میبینید.
فهرست رویدادهای Responses
جدول زیر رویدادهایی را فهرست میکند که مهوش با قالبی یکسان میفرستد. رویدادهای مربوط به ابزار، رسانه و استدلال فقط وقتی میآیند که request و مسیر انتخابشده آنها را تولید کنند.
| رویداد | توضیحات |
|---|---|
response.created | request پذیرفته شد؛ وضعیت 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 مهوش را تا آخر بخوانید یا نه، تا گفتوگوی ذخیرهشده ناقص نماند.
سناریوی چت تعاملی پشتیبانی یک پیادهسازی کامل از همین الگو است.