Skip to content

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. 1response.created — the request was admitted. It carries the response id with status in_progress. Store the ID now: it is how you recover if the connection drops later.
  2. 2response.output_item.added — a new output item begins: a message, a function call, a reasoning summary or a tool call. Its position is output_index.
  3. 3response.content_part.added — a content part begins inside a message item, identified by content_index.
  4. 4Deltas — for example response.output_text.delta — carry small pieces of content to append.
  5. 5.done events — for example response.output_text.done, response.content_part.done and response.output_item.done — carry the final value of a part or item.
  6. 6One terminal event: response.completed, response.incomplete or response.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.

EventDetails
response.createdResponse admitted; status is in_progress.
response.output_item.addedA new output item has started.
response.content_part.addedA message content part has started.
response.output_text.deltaAppend delta to the visible text for its item and content index.
response.refusal.deltaAppend a refusal-text delta.
response.reasoning_summary_part.addedA reasoning summary part has started.
response.reasoning_summary_text.deltaAppend a reasoning-text or summary delta when provided.
response.reasoning_text.deltaAppend a reasoning-text or summary delta when provided.
response.function_call_arguments.deltaAppend function-call argument bytes; parse JSON only after done.
response.function_call_arguments.doneFunction-call arguments are complete.
response.audio.deltaAudio bytes or chunk data arrived.
response.audio.doneAudio output is complete.
response.image_generation_call.in_progressImage generation progress or a partial image.
response.image_generation_call.generatingImage generation progress or a partial image.
response.image_generation_call.partial_imageImage generation progress or a partial image.
response.image_generation_call.completedImage generation completed.
response.web_search_call.in_progressWeb search started, is searching, or completed.
response.web_search_call.searchingWeb search started, is searching, or completed.
response.web_search_call.completedWeb search started, is searching, or completed.
response.tool_call.deltaA provider-hosted tool emitted a progress delta.
response.output_text.doneFinal text for this content part.
response.refusal.doneFinal refusal text for this content part.
response.reasoning_summary_text.doneReasoning summary content is complete.
response.reasoning_summary_part.doneReasoning summary content is complete.
response.content_part.doneA content part is complete.
response.output_item.doneAn output item is complete.
response.completedSuccessful terminal event with the final response.
response.incompleteTerminal event for cancelled or incomplete work.
response.failedTerminal 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_index and content_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.completed is the authoritative final state. Reconcile what you displayed with it and store its id and usage.

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 reached max_output_tokens. The partial output is valid; inspect incomplete_details and decide whether to show it or continue.
  • response.failed — the run failed. Show an error, keep the X-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 id from response.created on 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.