Streaming replies
The server-sent events of POST /chat/messages, and how to read them in your language.
A successful send answers 200 with Content-Type: text/event-stream and writes events as the reply is generated.
Each event is an event: line and a JSON data: line, followed by a blank line:
event: message.started
data: {"clientRequestId":"6f1d2c9e-...","conversationId":"9b2e4c1a-...","userMessageId":"...","assistantMessageId":"...","generationId":"..."}
event: message.delta
data: {"generationId":"...","sequence":1,"text":"فروشگاه ما"}
event: message.delta
data: {"generationId":"...","sequence":2,"text":" با بیش از ده سال تجربه"}
event: message.completed
data: {"generationId":"...","message":{"id":"...","role":"assistant","content":[{"type":"text","text":"فروشگاه ما با بیش از ده سال تجربه…"}],"status":"completed", "...": "..."}}
event: usage.updated
data: {"usage":{"remainingCredits":868,"usedCredits":132,"...":"..."}}Errors found before the stream starts (bad input, no credits, a bad token) are normal JSON errors
with a 4xx or 5xx status. Once the stream has started, the status is 200 and a failure arrives as message.failed.
Events
| Event | Data | Notes |
|---|---|---|
message.started | clientRequestId, conversationId, userMessageId, assistantMessageId, generationId | Always first. Save conversationId to continue the conversation. |
message.delta | generationId, sequence, text | A piece of the reply. Append text in sequence order. |
agent.step | generationId, tool, status, detail | Only when answering as an agent: a tool is running, then completed or failed. |
message.completed | generationId, message | The whole reply as stored. Prefer it over the joined deltas. |
message.aborted | generationId, message | The reply was stopped; message has what was written so far. |
message.failed | generationId, error (code, message) | The model could not finish. Nothing is charged for a failed reply. |
usage.updated | usage | Always last. The same object as GET /usage. |
memory.suggested | id, note | Something the chat offers to remember; can be ignored. |
Exactly one of message.completed, message.aborted and message.failed ends each reply. Ignore event types you do
not know: new ones may be added.
Read the stream
Works in Node.js 18+, Deno, Bun and browsers. EventSource cannot send a POST body or an Authorization header,
so read the response body yourself:
const id = crypto.randomUUID();
const response = await fetch('https://manova.ir/api/v1/external/chat/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MANOVA_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': id,
},
body: JSON.stringify({ clientRequestId: id, mode: 'auto', content: [{ type: 'text', text: 'سلام!' }] }),
});
if (!response.ok) throw new Error((await response.json()).code);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let end;
while ((end = buffer.indexOf('\n\n')) !== -1) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const event = /^event: (.*)$/m.exec(block)?.[1];
const data = JSON.parse(/^data: (.*)$/m.exec(block)?.[1] ?? '{}');
if (event === 'message.delta') process.stdout.write(data.text);
if (event === 'message.failed') throw new Error(data.error.code);
}
}Long replies and dropped connections
A reply can stream for several minutes. Turn off response buffering in proxies between you and Manova, and use a read timeout of a few minutes rather than seconds.
Closing the connection stops the reply: it is saved as aborted and charged for what was written. Retrying with
the same clientRequestId replays that result (message.aborted) without charging again. To get a full answer,
send the message again with a new clientRequestId.