Stream an Agent Response Token-by-Token over SSE
A blockingPOST /agents/{id}/chat call makes your user stare at a spinner
until the whole answer exists. The streaming endpoints put the first words
on screen in under a second and keep them coming, which is what a
chat-style UI needs to feel alive. This guide walks the consumer side end
to end: picking a surface, parsing the wire format, cancelling mid-stream,
recovering from a dropped connection, and folding token and cost totals
into your ROI dashboards.
Endpoint paths below are relative; send them against
https://api.orbit.devotel.io/api/v1.
1. Why stream, and which surface to pick
Orbit ships two streaming surfaces for agent chat:POST /agents/{id}/chat/stream— native token streaming. The API proxies the agent runtime’s SSE connection directly to you, so tokens arrive as the model produces them. Frames are baredata:lines: partial tokens asdata: {"token":"…"}, and a terminal framedata: {"final":true,"conversation_id":"conv_…"}. Pick this whenever your runtime can hold an open HTTP response — it has the lowest time-to-first-token.POST /agents/{id}/stream— word-chunked streaming. The API runs the blocking chat path, then re-emits the finished response word-by-word at roughly 30 ms gaps as named SSE events. Use it when an SDK or framework expects atext/event-streamresponse but cannot drive the native stream, or when you want the terminaldoneevent to carry token and cost totals.
POST /agents/{id}/chat: a required
message, optional history (an array of {role, content} turns seeded
additively on top of persisted state), and optional conversationId.
If you only need the answer and not the UX, plain POST /agents/{id}/chat
remains the simplest call.
2. Auth, quota, and conversation carry-forward
- Auth. Send your secret API key in the
X-API-Keyheader (dv_live_sk_…). The key needs theagents:writescope; both streaming surfaces are write actions, same asPOST /chat. - Quota. All three chat surfaces —
/chat,/chat/stream, and/stream— share one tenant budget of 20 requests per minute. Twenty concurrent chat sessions on one tenant queue against the same bucket, so a bursty UI can hit429even when each individual user is idle. Handle429explicitly (see §6). - Conversation carry-forward. Pass
conversationIdfrom the previous turn’s terminal frame to continue a conversation; omit it to start a fresh one. The id is the thread your whole UI hangs on — persist it the moment you receive it, not at the end of your handler.
3. Wire format
Every frame is standard SSE: optionalevent: line, a data: line, blank
line. Keepalive comment frames (lines starting with :) may appear on
quiet stretches; a correct reader ignores any line beginning with :.
Native stream — POST /agents/{id}/chat/stream:
POST /agents/{id}/stream:
An empty agent answer produces
done with zero tokens and no token
events — treat that as a valid, empty reply.
4. Consumers
curl
-N disables buffering so frames print as they arrive:
Node (native stream)
Browser — React useEffect (word-chunked stream)
Browsers cannot send a body through EventSource, so use a fetch
reader. About thirty lines renders tokens as they land:
X-API-Key, exactly as sketched above.
Python (httpx)
Reconnecting after a dropped connection
The agent chat streams are request-scoped: one turn is one HTTP request, and tokens already emitted are not replayed. Orbit’s platform-event stream (GET /events/stream) is the surface with
Last-Event-ID replay semantics — reconnect with the last seen id and
buffered events newer than it are re-sent. The chat streams resume at the
conversation level instead: start a new request carrying the same
conversationId and the agent continues with its persisted history
intact. Persist the id from every terminal frame (§2) so a dropped
connection never loses the thread. For live dashboard data around the
agent (message DLRs, call lifecycle), subscribe to /events/stream with
Last-Event-ID and keep the chat stream dedicated to the reply itself.
5. Cancelling mid-stream
Callcontroller.abort() (browser), destroy the reader (Node), or close
the response (Python). The server watches the socket and stops emitting
the moment your connection closes — there is no separate cancel call. You
are billed for tokens the model produced up to that point; cancellation
stops further generation, it does not refund work already done.
6. Errors: error events versus HTTP status
Two failure classes, two handlers:
- HTTP 4xx/5xx before the stream opens. The response is the standard
JSON error envelope, not
text/event-stream. Checkres.okfirst and branch on status:401bad or missing key,403missingagents:writescope,404unknown agent id,422body failed validation,500retry with backoff. errorevent after the stream opened. A named event with{ code, message }; treat it as terminal for that request.429quota exhausted. The shared 20/min budget returns429with aRetry-Afterheader. Back off for that duration; do not retry immediately in a loop, and surface “the agent is busy” in the UI rather than failing the message silently. Spreading bursty traffic across the minute — or queueing turns client-side — beats parallel fan-out.
7. Cost telemetry from the done event
The word-chunked stream’s done event carries totalTokens and
costCents for the turn. Accumulate them per agent id into the same
ledger you feed your ROI dashboards — the
agent ROI attribution guide shows the
dashboard side of that picture. Two habits keep the numbers honest:
- Record the totals from
done, never by countingtokenevents yourself: word chunks are display units, not billing units. - Store
conversationIdwith each sample so cost rows join back to the conversation for debugging.