Guides
Streaming & events
Show text while the model is still writing. Learn how Responses streams are framed, what each event means, how to assemble output correctly, and how to handle disconnects without paying twice.
When to stream
Without streaming, your client waits for the complete answer and receives one JSON object. With streaming, Mahvash sends small events as soon as they are available, so a user sees the first words in a fraction of the time.
- Stream for chat interfaces, live drafting, and anything a person watches.
- Do not stream for batch jobs, short classifications, or server-to-server calls where only the final result matters. A non-streaming call is simpler to retry and can be replayed with an idempotency key.
- For long work that nobody watches, use background mode instead — see How Mahvash orchestrates a request.
Open a Responses stream
Set stream: true in the body of POST /v1/responses. Sending Accept: text/event-stream makes the intent explicit for proxies and HTTP libraries.
curl -N https://api.mahvashai.ir/v1/responses \
-H "Authorization: Bearer $MAHVASH_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"model": "auto", "input": "Write a haiku about Tehran rain.", "stream": true}'The body is delivered as Server-Sent Events: each event is one data: line containing a JSON object, followed by a blank line. Every object has a type and a sequence_number. After the terminal event the stream ends with a literal data: [DONE] line.
data: {"type":"response.created","sequence_number":1,"response":{"id":"resp_<uuid>","object":"response","status":"in_progress","output":[]}}
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_<id>","type":"message","role":"assistant","status":"in_progress","content":[]}}
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_<id>","output_index":0,"content_index":0,"part":{"type":"output_text","text":""}}
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_<id>","output_index":0,"content_index":0,"delta":"Hel"}
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_<id>","output_index":0,"content_index":0,"delta":"lo!"}
data: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_<id>","output_index":0,"content_index":0,"text":"Hello!"}
data: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_<uuid>","status":"completed","output":[...],"usage":{...}}}
data: [DONE]Note:Use curl -N (no buffering) when testing streams from a terminal, and make sure proxies between your server and Mahvash do not buffer text/event-stream responses.
The shape of a stream
Every Responses stream follows the same outline:
- 1
response.created— the request was admitted. It carries the responseidwith statusin_progress. Store the ID now: it is how you recover if the connection drops later. - 2
response.output_item.added— a new output item begins: a message, a function call, a reasoning summary or a tool call. Its position isoutput_index. - 3
response.content_part.added— a content part begins inside a message item, identified bycontent_index. - 4Deltas — for example
response.output_text.delta— carry small pieces of content to append. - 5
.doneevents — for exampleresponse.output_text.done,response.content_part.doneandresponse.output_item.done— carry the final value of a part or item. - 6One terminal event:
response.completed,response.incompleteorresponse.failed, carrying the final response object.
Output item IDs are stable: the item announced while streaming is the same item you will see in the final response and in a later GET /v1/responses/{id}.
Responses event catalog
The table lists the normalized events Mahvash emits. Tool, media and reasoning events appear only when the request and the selected route produce them.
| Event | Details |
|---|---|
response.created | Response admitted; status is in_progress. |
response.output_item.added | A new output item has started. |
response.content_part.added | A message content part has started. |
response.output_text.delta | Append delta to the visible text for its item and content index. |
response.refusal.delta | Append a refusal-text delta. |
response.reasoning_summary_part.added | A reasoning summary part has started. |
response.reasoning_summary_text.delta | Append a reasoning-text or summary delta when provided. |
response.reasoning_text.delta | Append a reasoning-text or summary delta when provided. |
response.function_call_arguments.delta | Append function-call argument bytes; parse JSON only after done. |
response.function_call_arguments.done | Function-call arguments are complete. |
response.audio.delta | Audio bytes or chunk data arrived. |
response.audio.done | Audio output is complete. |
response.image_generation_call.in_progress | Image generation progress or a partial image. |
response.image_generation_call.generating | Image generation progress or a partial image. |
response.image_generation_call.partial_image | Image generation progress or a partial image. |
response.image_generation_call.completed | Image generation completed. |
response.web_search_call.in_progress | Web search started, is searching, or completed. |
response.web_search_call.searching | Web search started, is searching, or completed. |
response.web_search_call.completed | Web search started, is searching, or completed. |
response.tool_call.delta | A provider-hosted tool emitted a progress delta. |
response.output_text.done | Final text for this content part. |
response.refusal.done | Final refusal text for this content part. |
response.reasoning_summary_text.done | Reasoning summary content is complete. |
response.reasoning_summary_part.done | Reasoning summary content is complete. |
response.content_part.done | A content part is complete. |
response.output_item.done | An output item is complete. |
response.completed | Successful terminal event with the final response. |
response.incomplete | Terminal event for cancelled or incomplete work. |
response.failed | Terminal event for failed work. |
New event types may be added over time. A client must ignore types it does not recognize — never fail the stream because of one.
Assembling output correctly
A robust client follows a few rules:
- Dispatch on `type`. Never infer the meaning of an event from its position.
- Apply events in `sequence_number` order.
- Key content by position. Append each delta to the text for its
output_indexandcontent_index. A response can contain several items and parts, and deltas of different items can interleave. - Wait for `.done` before parsing structured values. Function-call arguments arrive as partial JSON fragments; parse them only after
response.function_call_arguments.done. - Trust the terminal event. The response inside
response.completedis the authoritative final state. Reconcile what you displayed with it and store itsidandusage.
If you use the official SDKs, they parse the frames for you. Without an SDK, a small reader is enough:
// A dependency-free SSE reader for fetch(). Frames are separated by a blank line.
export async function* readEvents(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) return;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() ?? "";
for (const frame of frames) {
const data = frame.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n");
if (!data) continue;
if (data === "[DONE]") return;
yield JSON.parse(data);
}
}
}
// Build text per output item and content part, keyed by their indexes.
const parts = new Map();
let terminal = null;
for await (const event of readEvents(response)) {
if (event.type === "response.output_text.delta") {
const key = event.output_index + ":" + event.content_index;
parts.set(key, (parts.get(key) ?? "") + event.delta);
}
if (["response.completed", "response.incomplete", "response.failed"].includes(event.type)) {
terminal = event;
}
}
if (!terminal) {
// The connection closed without an outcome. Do not assume success or failure.
}Terminal events and disconnects
Exactly one terminal event ends a healthy stream:
response.completed— the answer is complete.response.incomplete— generation stopped early, for example because it reachedmax_output_tokens. The partial output is valid; inspectincomplete_detailsand decide whether to show it or continue.response.failed— the run failed. Show an error, keep theX-Request-ID, and do not present partial text as a complete answer.
If the connection closes before any terminal event, the outcome is unknown: the model may have finished, failed, or still be working. Do not treat that as success, and do not immediately send the same prompt again — that can start a second paid run. Instead, retrieve the response by the ID you saved from response.created with GET /v1/responses/{id} and continue from its real status.
Important:Streaming requests cannot be replayed with an idempotency key (409 idempotent_stream_not_replayable). Recovery after a disconnect always goes through retrieval by ID.
Chat Completions streams are different
POST /v1/chat/completions with stream: true uses the older chunk format. Each data: line is a chat.completion.chunk whose choices[].delta carries the next piece of the message, and the stream also ends with data: [DONE].
data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-<id>","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]The two formats are not interchangeable. Use a Responses event parser for /v1/responses and a chunk parser for /v1/chat/completions.
Relaying a stream to a browser
Browsers must never call Mahvash with your API key. Open the stream from your backend and forward only what the interface needs:
- Authenticate your own user first, then open the Mahvash stream with the project key.
- Forward text deltas (or your own simplified events) to the browser over SSE or WebSockets.
- Save the response
idfromresponse.createdon your side, so a reloaded page can fetch the final result. - When the browser disconnects, decide deliberately whether to keep reading the upstream stream to completion so the stored conversation stays consistent.
The Interactive support chat scenario shows a complete implementation.