در حالت عادی، پاسخ یکجا و بعد از تمام شدن تولید متن میرسد. با "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."}]
}'
stream = client.chat.completions.create(
model="gpt-4o-mini",
stream=True,
messages=[{"role": "user", "content": "Write a four-line poem about autumn in Persian."}],
)
usage = None
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage: # the last chunk: choices is empty, usage is set
usage = chunk.usage
print("\n", usage)
const stream = await client.chat.completions.create({
model: "gpt-4o-mini",
stream: true,
messages: [{ role: "user", content: "Write a four-line poem about autumn in Persian." }],
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) process.stdout.write(text);
if (chunk.usage) console.log("\n", chunk.usage);
}
<?php
$stream = $client->chat()->createStreamed([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'Write a four-line poem about autumn in Persian.']],
]);
foreach ($stream as $response) {
echo $response->choices[0]->delta->content ?? '';
flush();
}
stream := client.Chat.Completions.NewStreaming(context.TODO(), openai.ChatCompletionNewParams{
Model: "gpt-4o-mini",
Messages: []openai.ChatCompletionMessageParamUnion{openai.UserMessage("Write a four-line poem about autumn in Persian.")},
})
for stream.Next() {
chunk := stream.Current()
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
if err := stream.Err(); err != nil {
panic(err)
}
await foreach (StreamingChatCompletionUpdate update in client.CompleteChatStreamingAsync(
[new UserChatMessage("Write a four-line poem about autumn in Persian.")]))
{
foreach (ChatMessageContentPart part in update.ContentUpdate)
{
Console.Write(part.Text);
}
}
قالب رویدادها#
پاسخ با Content-Type: text/event-stream میآید. هر رویداد یک خط data: با یک JSON است و رویدادها با یک خط خالی از هم جدا میشوند:
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]:
data: {"error":{"message":"ارتباط با ارائهدهنده قطع شد.","type":"upstream_error","code":"stream_interrupted"}}
data: [DONE]
SDK رسمی OpenAI این رویداد را به استثنا تبدیل میکند. اگر خودتان جریان را میخوانید، پیش از choices دنبال کلید error بگردید.
اگر اتصال سرویسدهنده پیش از تولید هیچ متنی قطع شود، هزینهای کم نمیشود؛ اگر بخشی از متن را ساخته باشد، هزینهٔ همان بخش حساب میشود.
قطع کردن از سمت شما#
اگر کاربر دکمهٔ «توقف» را زد یا صفحه را بست، اتصال را ببندید (در پایتون stream.close()، در Node.js یک AbortController). گیسو اتصال به سرویسدهنده را هم میبندد تا تولید متن ادامه پیدا نکند.
هزینهٔ متنی که تا آن لحظه ساخته شده حساب میشود؛ اگر سرویسدهنده مصرف را گزارش نکرده باشد، از روی طول متن فرستادهشده برآورد میشود.
رساندن جریان به مرورگر#
کلید باید روی سرور شما بماند، پس معمولاً سرورتان جریان گیسو را میگیرد و همان را به مرورگر میفرستد. در مرورگر، جریان را با fetch بخوانید:
// 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 یا پراکسی دیگری دارید، بافر کردن پاسخ را برای همین مسیر خاموش کنید؛ وگرنه رویدادها جمع میشوند و یکجا میرسند:
# 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) را با پرسشتان در تیکت بفرستید تا دقیق بررسی کنیم.