پرش به محتوا

پاسخ جریانی (Streaming)

پاسخ را تکه‌تکه و همان لحظه که ساخته می‌شود بگیرید: stream: true، قالب رویدادهای SSE، مصرف پایانی، قطع اتصال از سمت شما و خطایی که وسط پاسخ جریانی می‌رسد.

بازبینی: ۸ مهر ۱۴۰۵ ۷ دقیقه مطالعه

در حالت عادی، پاسخ یک‌جا و بعد از تمام شدن تولید متن می‌رسد. با "stream": true متن همان لحظه که ساخته می‌شود، تکه‌تکه به برنامهٔ شما می‌رسد؛ کاربر شروع پاسخ را زود می‌بیند و برای پاسخ‌های بلند تا آخرش منتظر نمی‌ماند. قالب همان Server-Sent Events (SSE) در OpenAI است و SDKها آن را خودشان می‌خوانند.

نمونه#

# -N turns off curl's own buffering so you see each event as it arrives
curl -N https://gisoo.pro/api/v1/chat/completions \
  -H "Authorization: Bearer $GISOO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "stream": true,
    "messages": [{"role": "user", "content": "Write a four-line poem about autumn in Persian."}]
  }'

قالب رویدادها#

پاسخ با Content-Type: text/event-stream می‌آید. هر رویداد یک خط data: با یک JSON است و رویدادها با یک خط خالی از هم جدا می‌شوند:

SSE text/event-stream
data: {"id":"req_c81f…","object":"chat.completion.chunk","model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"req_c81f…","object":"chat.completion.chunk","model":"gpt-4o-mini","choices":[{"index":0,"delta":{"content":"برگ"},"finish_reason":null}]}

: keepalive

data: {"id":"req_c81f…","object":"chat.completion.chunk","model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"req_c81f…","object":"chat.completion.chunk","model":"gpt-4o-mini","choices":[],"usage":{"prompt_tokens":19,"completion_tokens":58,"total_tokens":77}}

data: [DONE]
  • متن در choices[0].delta.content می‌آید؛ تکه‌ها را پشت هم بچسبانید.
  • رویداد finish_reason دلیل پایان را می‌گوید (همان مقدارهای پاسخ عادی).
  • رویداد یکی‌مانده به آخر choices خالی و usage دارد: تعداد توکن‌های همین درخواست. گیسو این رویداد را خودش از سرویس‌دهنده می‌خواهد؛ لازم نیست stream_options بفرستید.
  • خط‌هایی که با : شروع می‌شوند (مثل : keepalive) فقط اتصال را باز نگه می‌دارند؛ نادیده‌شان بگیرید.
  • جریان همیشه با data: [DONE] تمام می‌شود.

در فراخوانی ابزار، به‌جای متن، تکه‌های delta.tool_calls می‌آیند و آرگومان‌ها باید پشت هم چسبانده شوند؛ SDKها این کار را خودشان می‌کنند.

خطا در میانهٔ جریان#

اگر خطا پیش از شروع جریان پیش بیاید (کلید، اعتبار، مدل)، پاسخ یک JSON معمولی با وضعیت خطاست. ولی اگر اتصال سرویس‌دهنده وسط تولید متن قطع شود، وضعیت ۲۰۰ قبلاً فرستاده شده؛ پس خطا به شکل یک رویداد error در همان جریان می‌آید و بعدش [DONE]:

SSE رویداد خطا
data: {"error":{"message":"ارتباط با ارائه‌دهنده قطع شد.","type":"upstream_error","code":"stream_interrupted"}}

data: [DONE]

SDK رسمی OpenAI این رویداد را به استثنا تبدیل می‌کند. اگر خودتان جریان را می‌خوانید، پیش از choices دنبال کلید error بگردید. اگر اتصال سرویس‌دهنده پیش از تولید هیچ متنی قطع شود، هزینه‌ای کم نمی‌شود؛ اگر بخشی از متن را ساخته باشد، هزینهٔ همان بخش حساب می‌شود.

قطع کردن از سمت شما#

اگر کاربر دکمهٔ «توقف» را زد یا صفحه را بست، اتصال را ببندید (در پایتون stream.close()، در Node.js یک AbortController). گیسو اتصال به سرویس‌دهنده را هم می‌بندد تا تولید متن ادامه پیدا نکند. هزینهٔ متنی که تا آن لحظه ساخته شده حساب می‌شود؛ اگر سرویس‌دهنده مصرف را گزارش نکرده باشد، از روی طول متن فرستاده‌شده برآورد می‌شود.

رساندن جریان به مرورگر#

کلید باید روی سرور شما بماند، پس معمولاً سرورتان جریان گیسو را می‌گیرد و همان را به مرورگر می‌فرستد. در مرورگر، جریان را با fetch بخوانید:

Node.js خواندن جریان در مرورگر
// Browser → YOUR server (which holds the key) → Gisoo. Read the stream with fetch.
const res = await fetch("/chat/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Hi!" }),
});

const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const events = buffer.split("\n\n");
  buffer = events.pop();                       // keep an unfinished event for the next read
  for (const event of events) {
    const line = event.split("\n").find((l) => l.startsWith("data: "));
    if (!line || line === "data: [DONE]") continue;
    const json = JSON.parse(line.slice(6));
    if (json.error) throw new Error(json.error.message);
    output.textContent += json.choices[0]?.delta?.content ?? "";
  }
}

اگر جلوی برنامه‌تان nginx یا پراکسی دیگری دارید، بافر کردن پاسخ را برای همین مسیر خاموش کنید؛ وگرنه رویدادها جمع می‌شوند و یک‌جا می‌رسند:

متن nginx
# Your own reverse proxy in front of an app that re-streams Gisoo's answer
location /chat/stream {
    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;          # send each event on at once
    proxy_read_timeout 120s;
}
زمان

هر درخواست، جریانی یا عادی، حداکثر حدود ۹۰ ثانیه برای پاسخ سرویس‌دهنده صبر می‌کند. پاسخ خیلی بلند را به چند درخواست کوتاه‌تر بشکنید.

پاسخ پرسشتان را پیدا نکردید؟

شناسهٔ درخواست (هدر X-Request-Id) را با پرسشتان در تیکت بفرستید تا دقیق بررسی کنیم.

تیکت پشتیبانی