SSE wire contract
The AI chat endpoints stream Server-Sent Events (text/event-stream). This
page is the event reference; the source of truth is the parser + types in
@kaleidr/inference-ui (packages/inference-ui/src/control/sse.ts and
types.ts). If you use kaleidr.js, the chat bundle parses this for you — you
only need this page to build a custom client.
Events
event: | Payload (shape) | When |
|---|---|---|
start / phase | { type, phase, session_id } | Lifecycle — thinking / waiting phases. |
token | unnamed event — data: { "text": "…" } | Prose, streamed incrementally. May contain [[Place]] markers. |
place | { name, coordinates: { lat, lng }, address?, category?, source?, … } | A resolved place (merge-by-name → a map pin). source: "vendor" marks a place from the organization's own uploaded data rather than one geocoded from the answer; style those pins differently if you wish, or ignore the field. |
place_linked | partial place fields | Metadata enrichment of an already-emitted place. |
early_actions | { actions: [ { type, payload } ] } | Map actions (fitBounds / route / highlight) before the full reply. |
quota | { used, limit, tier, reset_at } | Mid-stream meter — surface remaining budget. |
grounding | { sources: [ { title, url, tier, layer } ] } | Sources for a grounded answer (grounded turns only). layer is "org" for a source from your organization's own uploaded data and "baseline" for everything else — present on every source, so you can render one rule rather than branching on whether the field is there. Label them distinctly when an answer mixes both: where your data and a third-party source disagree, Kaleidr surfaces the conflict rather than silently picking one, and a reader can only act on that if they can see which is which. |
route | { type: "route", profile, origin, destination, primary: { geometry, distance_m, duration_s, … }, alternatives: [] } | A journey-phrased turn ("route from A to B") — the engine-computed route, emitted before the prose so the map can draw while the answer streams. |
prompt_options | { question, options: [ { label, send_text } ] } | The assistant needs a disambiguation ("which Springfield?") — render as tappable chips and submit an option's send_text as the next user message. |
end | { full_text, places: [], actions: [], … } | Terminal envelope — the complete result. |
error | { message } | Terminal error. |
A keep-alive comment line (: keep-alive) arrives every ~10s to hold the
connection open through proxies — ignore it.
Tokens and structured data
token events stream plain prose. They may carry inline [[Place]] markers
where a place is referenced (e.g. …cafes near [[Louvre]]…) — strip them for clean
display, or ignore the prose entirely and drive your UI from the structured events.
Structured data arrives as events, never inline in the prose. Resolved places
stream on place (and place_linked for late metadata) as they geocode; map actions
arrive on early_actions; routes arrive on route; the authoritative final payload is the end event
({ full_text, places, actions }). A typical client renders pins from place events
as they arrive and treats end as the source of truth.
Minimal client
const res = await fetch(`${API}/inference-api/b2b/v1/chat/control/stream`, {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({ messages: [{ role: 'user', content: 'cafes near the Louvre' }] }),
});
const reader = res.body.getReader();
// …decode chunks, split on \n\n, parse `event:` + `data:` lines…
See Endpoints for the request bodies, and Errors for failure handling.