البث المباشر (SSE)
يرسل البث المباشر الاستجابة أولًا بأول أثناء توليدها، عبر أحداث مرسلة من الخادم (Server-Sent Events). يرى المستخدم النص يظهر فورًا بدل انتظار النهاية : هذا ما يجعل واجهة الدردشة ممتعة.
التفعيل#
أضيفوا "stream": true. لتلقي عدّ الرموز أيضًا، أضيفوا
"stream_options": {"include_usage": true}.
from openai import OpenAI
client = OpenAI(api_key="zir_ai_...", base_url="https://api.cloudzir.com/v1")
flux = client.chat.completions.create(
model="mina-low",
messages=[{"role": "user", "content": "Explique le DNS en 5 phrases."}],
stream=True,
stream_options={"include_usage": True},
)
complet = []
for chunk in flux:
if chunk.choices and chunk.choices[0].delta.content:
morceau = chunk.choices[0].delta.content
complet.append(morceau)
print(morceau, end="", flush=True)
if chunk.usage: # dernier événement
print(f"\n\n{chunk.usage.total_tokens} jetons")const flux = await client.chat.completions.create({
model: "mina-low",
messages: [{ role: "user", content: "Explique le DNS en 5 phrases." }],
stream: true,
stream_options: { include_usage: true },
});
let complet = "";
for await (const chunk of flux) {
const morceau = chunk.choices?.[0]?.delta?.content;
if (morceau) {
complet += morceau;
process.stdout.write(morceau);
}
if (chunk.usage) console.log(`\n\n${chunk.usage.total_tokens} jetons`);
}curl -N https://api.cloudzir.com/v1/chat/completions \
-H "Authorization: Bearer $CLOUDZIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mina-low",
"messages": [{"role": "user", "content": "Explique le DNS en 5 phrases."}],
"stream": true
}'خيار -N في curl ضروري : بدونه، يُخزَّن الإخراج مؤقتًا ولن
تروا شيئًا قبل النهاية.
صيغة التدفق (واجهة OpenAI)#
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"mina-low",
"choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"mina-low",
"choices":[{"index":0,"delta":{"content":"Le "},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"mina-low",
"choices":[{"index":0,"delta":{"content":"DNS"},"finish_reason":null}]}
: keep-alive
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"mina-low",
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"mina-low",
"choices":[],"usage":{"prompt_tokens":18,"completion_tokens":96,"total_tokens":114}}
data: [DONE]
نقاط يجب تذكّرها :
- الأسطر التي تبدأ بـ
:هي تعليقات حفاظ على الاتصال (كل 15 ثانية أثناء انتظار محرك). يتجاهلها أي عميل SSE متوافق. - لا تظهر القطعة الحاملة لـ
usageإلا إن طلبتمinclude_usage؛ مصفوفةchoicesفيها فارغة. data: [DONE]تُنهي التدفق دائمًا.
استدعاء الأدوات في البث المباشر#
تصل المعاملات على شكل أجزاء يجب دمجها :
appels = {}
for chunk in flux:
for choix in chunk.choices or []:
for tc in choix.delta.tool_calls or []:
entree = appels.setdefault(tc.index, {"id": "", "name": "", "args": ""})
if tc.id:
entree["id"] = tc.id
if tc.function and tc.function.name:
entree["name"] = tc.function.name
if tc.function and tc.function.arguments:
entree["args"] += tc.function.arguments # à concaténer !
# À la fin seulement, les arguments forment un JSON valide
import json
for appel in appels.values():
arguments = json.loads(appel["args"])
الأخطاء أثناء التدفق#
يكون رمز HTTP بالفعل 200 عند بدء التدفق : يصل عطل يقع لاحقًا كأنه كائن
error داخل التدفق، متبوعًا بـ[DONE]. تحققوا إذًا من
وجود error في كل حدث :
data: {"error":{"message":"Erreur interne.","type":"api_error","param":null,"code":"api_error"}}
data: [DONE]
إعادة الإرسال إلى متصفح#
لا تعرّضوا مفتاحكم أبدًا في جانب العميل. مرروا التدفق عبر خادمكم :
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import httpx, json
app = FastAPI()
@app.post("/chat")
async def chat(payload: dict):
async def relais():
async with httpx.AsyncClient(timeout=None) as http:
async with http.stream(
"POST",
"https://api.cloudzir.com/v1/chat/completions",
headers={"Authorization": f"Bearer {CLE}"},
json={**payload, "stream": True},
) as amont:
async for ligne in amont.aiter_lines():
if ligne:
yield f"{ligne}\n\n"
return StreamingResponse(relais(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache",
"X-Accel-Buffering": "no"})app.post("/chat", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("X-Accel-Buffering", "no");
const amont = await fetch("https://api.cloudzir.com/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${CLE}` },
body: JSON.stringify({ ...req.body, stream: true }),
});
const reader = amont.body.getReader();
const decoder = new TextDecoder();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
res.write(decoder.decode(value));
}
res.end();
});قد يخزّن وسيط عكسي (nginx) أو شبكة توصيل محتوى (CDN) التدفق مؤقتًا ويلغي
كل فائدة البث المباشر. أضيفوا الترويسة X-Accel-Buffering: no
وعطّلوا proxy_buffering على المسار المعني.
الفوترة#
يُفوتَر الطلب المقطوع في الطريق عن الجزء المُولَّد. إن لم يُعِد المزوّد حقل
usage، تُقدّره CloudZIR وتُشير إليه كتقدير في
Usage.
صيغة Anthropic#
يستخدم البث المباشر من جهة /anthropic الأحداث المُسمّاة
(message_start، content_block_delta، ...) :
انظروا استخدام API الخاصة بـAnthropic.