Функции
Потоковая передача
Потоковая передача ответов в реальном времени
Потоковая передача (streaming) позволяет получать ответ модели частями в реальном времени, вместо ожидания полной генерации. Это кардинально улучшает пользовательский опыт в интерактивных приложениях.
Как работает потоковая передача
Без стриминга (обычный режим)
[Пользователь отправляет запрос]
↓
[Модель думает и генерирует весь текст]
↓
⏳ Ожидание...
↓
[Возвращается полный ответ в JSON]
Время отклика = время генерации текста
Пользователь ждёт 5-30 секунд до первого символа ответа.
Со стримингом
[Пользователь отправляет запрос]
↓
[Модель начинает генерацию]
↓
⚡ Сразу → "При"
⚡ Сразу → "вет"
⚡ Сразу → "! Как"
⚡ Сразу → " дела"
⚡ Сразу → "?"
↓
[DONE]
Первый символ приходит за ~0.5-2 секунды
Пользователь видит "живой" ответ, как в ChatGPT.
Включение потоковой передачи
Добавьте параметр stream: true
в запрос:
{
"model": "gpt-4o-mini",
"messages": [
{ "role": "user", "content": "Расскажи историю" }
],
"stream": true
}
Формат ответа (Server-Sent Events)
Ответ приходит в формате Server-Sent Events (SSE). Каждая порция данных начинается с data:
и содержит JSON с частичным ответом.
Пример потока
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"При"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"вет"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"! Как"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":" дела"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"?"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}
data: [DONE]
Ключевые моменты:
- Каждая строка начинается с
data:
- Между событиями — пустые строки
- В
delta.content
— очередной кусочек текста finish_reason: "stop"
— генерация завершена[DONE]
— поток закрыт
Примеры реализации
cURL (просмотр потока)
curl -N https://api.aijora.com/api/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AIJORA_API_KEY" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Расскажи короткую историю про кота"
}
],
"stream": true
}'
Флаг -N
отключает буферизацию для потоковых данных.
JavaScript (Fetch API)
async function streamChat(message) {
const response = await fetch('https://api.aijora.com/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.AIJORA_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Декодируем chunk
const chunk = decoder.decode(value);
// Разбиваем на строки
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6); // Убираем "data: "
if (data === '[DONE]') {
console.log('\n[Стрим завершён]');
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
process.stdout.write(content); // Выводим без переноса строки
}
} catch (e) {
// Игнорируем ошибки парсинга
}
}
}
}
}
// Использование
streamChat('Расскажи про JavaScript');
async function streamChatWithAccumulation(message, onChunk, onComplete) {
let fullText = '';
const response = await fetch('https://api.aijora.com/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.AIJORA_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// Последняя строка может быть неполной
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onComplete(fullText);
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
fullText += content;
onChunk(content, fullText); // Передаём chunk и полный текст
}
} catch (e) {
// Игнорируем
}
}
}
}
}
// Использование
streamChatWithAccumulation(
'Расскажи анекдот',
(chunk, fullText) => {
console.log('Новый chunk:', chunk);
// Обновляем UI с fullText
},
(fullText) => {
console.log('Готово! Полный текст:', fullText);
}
);
import { useState } from 'react';
function useStreamChat() {
const [content, setContent] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<Error | null>(null);
const sendMessage = async (message: string) => {
setContent('');
setIsStreaming(true);
setError(null);
try {
const response = await fetch('https://api.aijora.com/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_AIJORA_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
stream: true
})
});
const reader = response.body?.getReader();
if (!reader) throw new Error('No reader');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices[0]?.delta?.content;
if (delta) {
setContent(prev => prev + delta);
}
} catch (e) {
// Игнорируем
}
}
}
}
} catch (err) {
setError(err as Error);
} finally {
setIsStreaming(false);
}
};
return { content, isStreaming, error, sendMessage };
}
// Компонент
function ChatComponent() {
const { content, isStreaming, sendMessage } = useStreamChat();
return (
<div>
<div className="response">
{content}
{isStreaming && <span className="cursor">▊</span>}
</div>
<button onClick={() => sendMessage('Привет!')}>
Отправить
</button>
</div>
);
}
Python (Requests)
import os
import json
import requests
def stream_chat(message):
response = requests.post(
'https://api.aijora.com/api/v1/chat/completions',
headers={
'Authorization': f'Bearer {os.getenv("AIJORA_API_KEY")}',
'Content-Type': 'application/json'
},
json={
'model': 'gpt-4o-mini',
'messages': [{'role': 'user', 'content': message}],
'stream': True
},
stream=True # Важно!
)
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data = line[6:] # Убираем "data: "
if data == '[DONE]':
print('\n[Стрим завершён]')
break
try:
parsed = json.loads(data)
content = parsed['choices'][0]['delta'].get('content', '')
if content:
print(content, end='', flush=True)
except json.JSONDecodeError:
pass
# Использование
stream_chat('Расскажи про Python')
import os
import json
import requests
from typing import Callable
def stream_chat(
message: str,
on_chunk: Callable[[str], None],
on_complete: Callable[[str], None]
):
full_text = ''
response = requests.post(
'https://api.aijora.com/api/v1/chat/completions',
headers={
'Authorization': f'Bearer {os.getenv("AIJORA_API_KEY")}'
},
json={
'model': 'gpt-4o-mini',
'messages': [{'role': 'user', 'content': message}],
'stream': True
},
stream=True
)
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data = line[6:]
if data == '[DONE]':
on_complete(full_text)
break
try:
parsed = json.loads(data)
content = parsed['choices'][0]['delta'].get('content', '')
if content:
full_text += content
on_chunk(content)
except json.JSONDecodeError:
pass
# Использование
stream_chat(
'Расскажи историю',
on_chunk=lambda chunk: print(chunk, end='', flush=True),
on_complete=lambda text: print(f'\n\nВсего символов: {len(text)}')
)
import os
import json
import aiohttp
import asyncio
async def stream_chat_async(message: str):
async with aiohttp.ClientSession() as session:
async with session.post(
'https://api.aijora.com/api/v1/chat/completions',
headers={
'Authorization': f'Bearer {os.getenv("AIJORA_API_KEY")}',
'Content-Type': 'application/json'
},
json={
'model': 'gpt-4o-mini',
'messages': [{'role': 'user', 'content': message}],
'stream': True
}
) as response:
async for line in response.content:
line = line.decode('utf-8').strip()
if line.startswith('data: '):
data = line[6:]
if data == '[DONE]':
print('\n[Завершено]')
break
try:
parsed = json.loads(data)
content = parsed['choices'][0]['delta'].get('content', '')
if content:
print(content, end='', flush=True)
await asyncio.sleep(0) # Даём другим задачам выполниться
except json.JSONDecodeError:
pass
# Использование
asyncio.run(stream_chat_async('Привет!'))
OpenAI SDK
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.AIJORA_API_KEY,
baseURL: 'https://api.aijora.com/api/v1'
});
async function streamWithSDK() {
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Расскажи историю' }],
stream: true
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
process.stdout.write(content);
}
}
streamWithSDK();
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv('AIJORA_API_KEY'),
base_url='https://api.aijora.com/api/v1'
)
stream = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'Расскажи историю'}],
stream=True
)
for chunk in stream:
content = chunk.choices[0].delta.content or ''
print(content, end='', flush=True)
Особенности
- Стоимость не зависит от использования streaming — тарифицируются только токены
- Информация о токенах (
usage
) приходит в последнем chunk перед[DONE]
- Нельзя использовать
n > 1
(несколько вариантов) в режиме потоковой передачи