SSE wire contract
AI chat endpoint는 Server-Sent Events(text/event-stream)를 stream합니다.
이 페이지는 POST /chat/control/stream에서 사용하는 control-stream contract를
설명합니다. source of truth는 @kaleidr/inference-ui의 parser + types
(packages/inference-ui/src/control/sse.ts 및 types.ts)입니다.
kaleidr.js를 사용한다면 chat bundle이 이를 파싱하므로 이 페이지는
custom client를 만들 때만 필요합니다.
/chat/summary/stream과 /chat/button/stream은 서로 다른 SSE
contract를 사용하며 여기서는 다루지 않습니다. 해당 endpoint에서 control-stream
parser를 재사용하지 마세요.
호환성 규칙
Client는 알 수 없는 event name을 무시해야 합니다. stable contract에는 version bump 없이 새 event가 추가될 수 있습니다. 아래 "diagnostic / experimental" 표의 downstream event는 contract가 아닙니다 — official client는 무시하며 사라질 수 있습니다.
Stable event
Payload는 control parser가 가정하는 형식입니다. start에는 phase field가 없습니다
(phase event에만 존재).
event: | Payload | Meaning |
|---|---|---|
| (unnamed) | { text } | 증분 prose token. [[Place]] marker를 포함할 수 있습니다. |
phase | { type: "phase", phase, session_id } | stream 이전 lifecycle. |
start | { type: "start", session_id, purpose? } | Stream 열림. |
place | { name, coordinates: { lat, lng }, address?, category?, source?, id? } | 확인된 place(merge-by-name → map pin). source: "vendor"는 org가 upload한 data에서 온 place를 나타냅니다. |
place_linked | place fields + is_update? | 이미 emit된 place를 enrich. |
early_actions | { type: "early_actions", actions: [ { type, payload } ], places_count?, is_partial? } | 전체 reply 이전의 map action(fitBounds / route / highlight). |
prompt_options | { type: "prompt_options", schema_version, kind, question, allow_free_text, options: [ { label, send_text } ] } | 명확화 chip — tappable option으로 표시하고 option의 send_text를 다음 user message로 전송합니다. |
route_clear | {} | 새 route 도착 전에 모든 overlay clear. Stable. |
route | { type: "route", schema_version, id?, profile, origin, destination, primary, alternatives[], comparison[], stops?, connectors?, color?, label? } | journey 형태 turn — engine-computed route. prose보다 먼저 emit되어 답변 stream 중 map이 그릴 수 있습니다. |
quota | { type: "quota", product, meter, tier, used, limit, reset_at, period } | stream 중 meter — remaining budget 표시. |
end | { type: "end", full_text, places[], actions[], question_type, response_time, total_tokens? } | Terminal envelope — 전체 결과. |
error | { type: "error", message, code?, partial_text? } | Terminal error. |
: keep-alive | comment line | Proxy keep-alive(~10s). 무시. |
제거됨: grounding (2026-09-05). Grounding은 계속 답변에 영향을 주지만
wire에 frame을 보내지 않습니다.
Diagnostic / experimental (non-contract)
이는 emit되는 implementation detail입니다. official client는 무시하므로 의존하지 마세요.
event: | Payload | Notes |
|---|---|---|
debug | { scope, … } | 내부 diagnostic. 변경되거나 사라질 수 있음. |
place_name_found | { name, is_early? } | Non-contract; official client는 무시. |
Token 및 structured data
Unnamed event는 plain prose를 stream합니다. place가 참조되는 곳에
inline [[Place]] marker가 포함될 수 있습니다(예: …cafes near [[Louvre]]…) —
clean display를 위해 제거하거나 prose를 완전히 무시하고 structured event를 기반으로
UI를 구동하세요.
Structured data는 event로 도착하며 prose 안에 들어가지 않습니다. Resolved
place는 geocode되면서 place(place_linked는 late metadata)로 stream됩니다.
map action은 early_actions, route는 route_clear 이후 route로 오며,
authoritative final payload는 end event입니다.
일반적인 client는 place event가 도착하는 즉시 pin을 render하고
end를 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
// IMPORTANT: ignore any unknown event name (forward-compat rule above).