Streaming responses
Sending a message streams the assistant's reply back token by token over Server-Sent Events (text/event-stream). Read the stream incrementally to show text as it is generated rather than waiting for the whole reply.
Sending a message
POST the message to a chat session. Include x-api-key and x-chatbot-id. Optional body fields include documentIds, imageIds, and an image object for image generation. Use curl -N to disable buffering while testing.
curl -N https://chat-api-dev.paicloud.ai//message/CHAT_ID \
-H "x-api-key: $PAI_CHAT_API_KEY" \
-H "x-chatbot-id: YOUR_CHATBOT_ID" \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}'Response stream
The response opens with SSE headers and emits data: lines, each a JSON object, separated by a blank line. Text arrives as { "content": "…" } chunks; concatenate them in order. The stream ends with a literal data: [DONE].
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no
data: {"content": "Hel"}
data: {"content": "lo, how can I help?"}
data: [DONE]Event shapes
Most frames carry text. Some carry a type for non-text events — attached files and email delivery. Ignore any type you do not handle. Always stop at [DONE].
# Text tokens — append content as it arrives
data: {"content": "partial text…"}
# Generated files attached to the reply
data: {"type": "files", "files": [ ... ]}
# Files sent to the user by email
data: {"type": "send_files_to_email", ...}
# Terminator — stop reading
data: [DONE]Parsing the stream
Read the body as a stream, buffer bytes, split on the blank-line frame separator, strip the data: prefix, and JSON.parse each frame unless it is [DONE].
const res = await fetch(`${BASE_URL}/message/${chatId}`, {
method: 'POST',
headers: {
'x-api-key': process.env.PAI_CHAT_API_KEY,
'x-chatbot-id': process.env.PAI_CHATBOT_ID,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: 'Hello' }),
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let answer = ''
while(true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// SSE frames are separated by a blank line
const frames = buffer.split('\n\n')
buffer = frames.pop() ?? ''
for (const frame of frames) {
const line = frame.replace(/^data: /, '').trim()
if (!line || line === '[DONE]') continue
const event = JSON.parse(line)
if (event.content) answer += event.content
// else handle event.type === 'files' | 'send_files_to_email'
}
}text/event-stream through untouched — the API sets X-Accel-Buffering: no and Cache-Control: no-transform to prevent buffering.See Authentication for headers and Error codes for non-stream failures (a 4xx/5xx is returned before the stream opens).