SSE 전송 프로토콜 명세
POST /chat/control/stream은 Server-Sent Events(text/event-stream)로 응답합니다. 연결을 유지하며 답변을 생성하는 대로 텍스트를 전송하고, 장소·지도 동작·경로에 대한 구조화된 이벤트도 함께 전달합니다.
이 페이지는 해당 스트림의 명세를 설명합니다. kaleidr.js를 사용하면 채팅 패널이 스트림을 직접 처리하므로 이 문서를 읽을 필요가 없습니다. 자체 클라이언트를 개발하는 경우 참고하세요. Node 및 Python 클라이언트 예제는 스트림 읽기, 요청 본문은 엔드포인트에서 확인할 수 있습니다.
/chat/summary/stream과 /chat/button/stream도 같은 프레임 형식을 사용하지만 이벤트 종류는 더 적습니다. 자세한 내용은 이 페이지의 끝부분에 있습니다.
스트림이 시작되기 전
거부된 요청은 스트림이 되지 않습니다. 인증, 권한 범위, 할당량, 속도 제한, 유효성 검증, 크기 오류는 JSON 본문과 4xx 상태 코드가 있는 일반 HTTP 응답으로 반환됩니다(오류). 먼저 상태를 확인하고, 200일 때만 본문을 SSE로 파싱하세요.
요청 크기 제한은 메시지 최대 400개, 메시지당 64,000자, 전체 메시지 합계 200,000자입니다. 하나라도 초과하면 conversation_too_long 같은 코드와 함께 422가 반환되며, 본문이 2 MiB를 넘으면 413이 반환됩니다. 전체 목록은 엔드포인트를 참조하세요.
성공한 응답에는 다음 헤더가 포함됩니다.
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
프레임 형식
프레임은 한두 줄과 그 뒤의 빈 줄로 구성됩니다. 각 줄은 \n으로 끝납니다.
event: place
data: {"name": "Louvre Museum", "coordinates": {"lat": 48.8606, "lng": 2.3376}}
data: {"text": "The Louvre sits on the right bank"}
: keep-alive
- 이름이 있는 프레임에는
event:줄과 그 뒤의data:줄이 있습니다. - 텍스트 프레임에는
data:줄만 있고event:줄은 없습니다. - 주석은 콜론으로 시작합니다. Kaleidr는 연결 유지 신호로
: keep-alive를 전송합니다. 데이터는 포함하지 않습니다. data:에는 항상 하나의 완전한 JSON 객체가 한 줄에 담깁니다. 텍스트는 UTF-8로 인코딩됩니다.
네트워크 청크 경계는 프레임 경계와 일치하지 않습니다. 한 번 읽을 때 프레임의 절반만 들어오거나 여러 프레임이 들어올 수 있습니다. 받은 데이터를 버퍼에 모아 줄바꿈으로 분리하고, 프레임을 끝내는 빈 줄이 나타난 뒤에만 처리하세요.
호환성 규칙
알 수 없는 이벤트 이름과 사용하지 않는 필드는 무시하세요. 버전을 변경하지 않고도 새로운 이벤트나 필드가 추가될 수 있습니다. 알 수 없는 이벤트를 오류로 처리하는 클라이언트는 동작하지 않을 수 있습니다.
안정적인 이벤트
event: | 페이로드 | 의미 |
|---|---|---|
| (이름 없음) | { text } | 답변 본문의 다음 부분입니다. 기존 텍스트에 이어 붙이세요. 장소 이름 주변에 [[Name]] 마커가 있을 수 있습니다. |
phase | { type: "phase", phase: "thinking", session_id } | 요청이 수락되어 처리가 시작되었습니다. phase는 항상 "thinking"입니다. |
start | { type: "start", session_id, purpose: "control" } | 곧 본문 전송이 시작됩니다. |
place | { name, coordinates?: { lat, lng }, address?, category?, icon?, source?, id?, unverified? } | 답변에 포함된 장소입니다. name으로 병합하세요. 이름에 대해 처음 받은 좌표가 최종 좌표입니다. source: "vendor"는 조직 자체 데이터의 장소를, source: "map"은 게시된 지도에 이미 표시된 장소를 의미합니다. |
place_linked | 장소 필드 및 is_update? | 이미 전송된 장소의 추가 정보입니다. 같은 name의 장소에 합치고, 아직 해당 장소를 받지 않았다 면 무시하세요. |
early_actions | { type: "early_actions", actions: [ { type, payload } ], places_count, is_partial: true } | 답변이 완료되기 전에 적용할 수 있는 지도 동작으로 좌표가 있는 장소가 두 개 이상일 때 전송됩니다. |
prompt_options | { type: "prompt_options", schema_version, kind, question, allow_free_text, options: [ { label, send_text } ] } | 요청이 모호할 때 전송되는 선택형 질문입니다. 선택지를 표시하고 선택한 send_text를 다음 사용자 메시지로 보내세요. |
route_clear | {} | 그려진 경로를 지웁니다. 새 경로가 뒤따릅니다. |
route | { type: "route", schema_version, id?, profile, origin, destination, primary, alternatives[], comparison[], stops?, color?, label?, transit?, attribution? } | 계산된 경로입니다. 길찾기 질문에서 본문보다 먼저 도착하므로 텍스트가 전송되는 동안 지도를 그릴 수 있습니다. |
end | { type: "end", full_text, places[], actions[], question_type, response_time, total_tokens?, dropped_places? } | 답변이 완료되었습니다. 이것이 최종적으로 신뢰해야 할 결과입니다. |
error | { type: "error", message, code?, partial_text } | 답변이 실패했습니다. end 대신 전송됩니다. |
quota | 서버 키인 경우 { type: "quota", product, meter, tier, used, limit, reset_at, period }, 세션 토큰인 경우 { type: "quota", meter, reset_at } | 호출이 과금된 뒤 조직의 AI 크레딧 사용량입니다. end 또는 error 뒤에 오는 스트림의 마지막 이벤트입니다. quota를 참조하세요. |
: keep-alive | 주석 줄 | 10초 동안 전송이 없을 때 보내는 연결 유지 신호입니다. 무시하되 활동으로 계산하세요. |
제거됨: grounding(2026-09-05). 더 이상 전송되지 않습니다.
실시간 데이터 카드와 레이어
실시간 정보나 관측 자료에 관한 질문에는 다음 이벤트가 추가될 수 있습니다. 카드는 본문보다 먼저 도착하고 레이어는 본문 전송 중 또는 후에 도착할 수 있습니다. 추가 정보이므로 지원하는 것만 표시하고 나머지는 무시하세요. 나열된 필드에 새 항목이 추가될 수도 있습니다.
event: | 페이로드 | 용도 |
|---|---|---|
weather_card | lat, lng, name, 기온(temp_c, temp_f), conditions, 선택적 forecast[]가 있는 객체 | 장소의 현재 날씨. |
weather_layer | layer, tile_path_template, tile_size, max_zoom, scope, attribution가 있는 객체 | 지역의 기온 오버레이. |
satellite_card | name, norad_id, lat, lng, altitude_km, velocity_kms, observed_at가 있는 객체 | 이름이 지정된 위성의 위치. |
constellation_layer | group, { name, norad_id, lat, lng } 요소의 features[], 선택적 track이 있는 객체 | 카드에 표시된 위성 주변의 다른 위성. |
building_card | lat, lng, 선택적 name, height_m, num_floors가 있는 객체 | 건물에 관한 정보. |
trees_card | lat, lng, count, radius_m가 있는 객체 | 지점 주변에서 조사된 나무. |
heritage_card | lat, lng, count, sites[]가 있는 객체 | 지점 주변의 문화유산 장소. |
rail_card | lat, lng, lines[], 선택적 nearest_station이 있는 객체 | 지점 주변의 철도 노선. |
arrivals_card | lat, lng, stop, departures[], alerts[], attribution가 있는 객체 | 정류장의 대중교통 출발 정보. |
vehicles_layer | operator, vehicles[], shapes[], attribution가 있는 객체 | 카드에 있는 정류장이나 노선의 대중교통 차량 위치. |
streetview_card | lat, lng, image_id, image_lat, image_lng가 있는 객체 | 장소 근처의 거리 수준 이미지. |
이 이벤트가 전송되면 end는 다섯 종류를 같은 이름으로 반복해 담습니다. 이벤트를 놓친 클라이언트도 확인할 수 있도록 하기 위함입니다: weather_card, satellite_card, constellation_layer, weather_layer, vehicles_layer(예: end.weather_card). 다른 카드는 end에 다시 포함되지 않습니다. 실시간 행사 목록은 플랫폼 API로 제공되지 않습니다. 데이터 라이선스를 참조하세요.
진단용 및 실험적 이벤트(명세 외)
이 이벤트가 전송 데이터에 나타날 수는 있지만 공식 명세에는 포함되지 않습니다. 이에 의존하지 마세요.
event: | 페이로드 | 참고 |
|---|---|---|
debug | { scope, … } | 내부 진단 정보입니다. 일반 요청에는 전송되지 않습니다. |
place_name_found | { name, is_early } | 장소 이름이 언급되었다는 초기 알림입니다. 이어서 오는 place 이벤트를 사용하세요. |
이벤트 순서
스트림은 다음 단계를 거칩니다. 선택 사항으로 표시된 단계는 모든 스트림에 나타나지 않습니다.
- 선택 사항.
source: "vendor"가 있는place이벤트. 키에vendor범위가 있으면 조직 자체 데이터에서 최대 세 장소를 먼저 보내 지도가 즉시 표시할 수 있게 합니다. phase. 답변 자체에서 보내는 첫 번째 이벤트입니다.- 선택 사항. 답변을 준비하는 동안
: keep-alive주석이 전송됩니다. - 선택 사항. 본문에 앞서 오는 이벤트:
prompt_options, 또는route_clear다음의route, 그리고 카드. start.- 텍스트 프레임과
place,place_linked,early_actions가 섞여 전송됩니다. 경우에 따라route_clear,route, 레이어 이벤트도 포함됩니다. 아무 데이터가 없을 때는 연결 유지 신호가 전송됩니다. - 선택 사항. 본문이 끝난 뒤에 도착하는
place,place_linked, 레이어 및 경로 이벤트. end또는error.- 선택 사항.
quota. - 서버가 연결을 닫습니다.
클라이언트에서는 다음 두 가지를 유의하세요.
end가 마지막 이벤트는 아닙니다. 응답 본문이 닫힐 때까지 계속 읽어야quota를 놓치지 않습니다.end나error없이 스트림이 닫힐 수도 있습니다. 예를 들어 네트워크 연결이 끊긴 경우입니다. 종료 이벤트 없이 닫힌 스트림은 실패로 처리하세요.
페이로드 세부 정보
본문 과 마커
텍스트 프레임에는 텍스트만 포함됩니다. 구조화된 데이터는 본문 안에 들어가지 않습니다. 텍스트에는 near [[Louvre Museum]]처럼 장소 이름을 이중 대괄호로 둘러싼 표시가 포함될 수 있으며, 채팅 패널은 이를 지도 핀 링크로 변환합니다. 일반 텍스트로 표시하려면 대괄호만 제거하고 이름은 유지하세요.
place
| 필드 | 유형 | 참고 |
|---|---|---|
| name | string | 병합할 때 사용하는 이름 키입니다. |
| coordinates | object { lat, lng } | 십진수 단위의 도입니다. 없을 수 있습니다. 확인된 위치가 없는 장소는 좌표 없이 unverified: true로 전송됩니다. 핀을 그리지 마세요. |
| address | string | 선택 사항. |
| category | string | 선택 사항. |
| icon | string | 선택 사항. 장소를 나타내는 이모지입니다. |
| source | string | 선택 사항. "vendor"는 조직 데이터에 있는 장소입니다. "map"은 방문자가 보는 게시된 지도에 이미 있는 장소이므로 채팅 패널이 별도 핀을 추가하지 않습니다. |
| id | string | 선택 사항. 자체 데이터에서 나온 장소에 포함됩니다. |
| unverified | boolean | 선택 사항. 장소 위치가 확인되지 않았다면 true입니다. |
장소에 다른 필드가 포함될 수도 있습니다. 사용하지 않는 필드는 무시하세요.
같은 이름이 여러 차례 전송될 수 있습니다. 처음에는 좌표, 이후에는 추가 정보가 옵니다. 이름마다 레코드 하나만 보관하고 좌표를 받은 뒤에는 핀을 이동하지 않으며 나중 이벤트로 다른 필드를 보완하세요.
동작
early_actions.actions와 end.actions는 { type, payload }의 목록입니다.
| 동작 유형 | 페이로드 | 처리 방법 |
|---|---|---|
| map.fitBounds | { coordinates: [[lng, lat], …], padding, maxZoom, duration } | 좌표가 모두 보이도록 지도 카메라를 조정합니다. |
| places.highlight | { places: [ { name, lat, lng } ], options } | 해당 장소를 강조 표시합니다. |
| map.showRoute | { id, label, profile, coordinates: [[lng, lat], …], distance_m, duration_s, turn_by_turn[], transit?, attribution? } | 선을 그리고 attribution을 표시합니다. |
| route.clear | { id? } | 해당 id의 경로를 제거하거나, id가 없다면 모든 경로를 제거합니다. |
다른 동작 유형이 나타날 수 있습니다. 처리하지 않는 유형은 건너뛰세요. 좌표 형식이 두 가지라는 점에 주의하세요. 장소는 coordinates: { lat, lng }를 사용하고 선은 [lng, lat] 쌍의 목록을 사용합니다.
route
primary에는 그릴 선이 담깁니다: { coordinates: [[lng, lat], …], distance_m, duration_s, turn_by_turn[] }. turn_by_turn의 각 단계에는 text가 있고 direction_type, distance_m, duration_s, lat, lng가 추가될 수 있습니다. origin과 destination에는 각각 label이 있습니다. alternatives[]는 route_id가 있는 다른 선을 담고, comparison[]에는 기본 경로 대비 각 대안의 delta_duration_s가 있습니다. stops[]가 있으면 다중 경유 경로의 { lat, lng, name } 지점을 순서대로 담습니다.
도로 경로에는 attribution: { text, url, license }가 포함됩니다. 경로를 그릴 때마다 url 링크와 함께 text를 표시하세요. 대중교통 경로의 운영기관 표시는 대신 transit 안에 있습니다.
각 route 이벤트에는 id가 있습니다. 이미 존재하는 id로 경로를 그리면 기존 경로를 교체합니다.
end
| 필드 | 유형 | 참고 |
|---|---|---|
| full_text | string | 완성된 답변입니다. 텍스트 프레임을 이어 붙인 결과보다 이를 우선하세요. |
| places | array | 순서가 있는 최종 장소 목록입니다. 수집한 장소와 이름을 기준으로 병합합니다. |
| actions | array | 최종 지도 동작입니다. 이를 적용하면 early_actions를 대체합니다. |
| dropped_places | array of strings | 선택 사항. 앞서 전송됐지만 최종 답변에 포함되지 않는 장소 이름입니다. 해당 핀을 제거하세요. |
| question_type | string | 질문 유형을 설명하는 참고용 레이블입니다. |
| response_time | number | 답변에 걸린 시간(초). |
| total_tokens | number | 선택 사항. 답변에서 사용한 토큰 수이며 과금 기준입니다. |
error
event: error
data: {"type": "error", "code": "upstream_stall", "message": "The AI service stopped responding. Please try again.", "partial_text": "The Louvre sits"}
message는 사용자에게 그대로 표시해도 됩니다. partial_text는 오류가 나기 전에 생성한 텍스트입니다. code는 다음 한 가지 경우에만 있습니다.
| 상황 | code | 처리 방법 |
|---|---|---|
| 답변 진행이 멈췄거나 스트림이 최대 길이에 도달함 | "upstream_stall" | 재시도를 안내하세요. |
| 답변 중 발생한 기타 실패 | 없음 | message를 표시하세요. 재시도하면 성공할 수 있습니다. |
error 다음에는 end가 오지 않습니다. quota 이벤트는 뒤따를 수 있습니다.
quota
필드는 스트림을 연 인증 정보에 따라 달라집니다.
| 필드 | 유형 | 서버 키 또는 이전 키 | 세션 토큰 |
|---|---|---|---|
| type | 문자열, "quota" | 예 | 예 |
| meter | 문자열 | 예 | 예 |
| reset_at | 문자열, 오프셋이 있는 ISO 8601 | 예 | 예 |
| product | 문자열 | 예 | 아니요 |
| period | 문자열 | 예 | 아니요 |
| tier | 문자열 | 예 | 아니요 |
| used | 숫자 | 예 | 아니요 |
| limit | 숫자 | 예 | 아니요 |
세션 토큰은 방문자가 웹페이지에서 사용하는 토큰으로 공개 가능한 키를 교환해 받거나 게시된 지도의 방문자 세션에서 발급됩니다. 세션 토큰의 quota 이벤트는 사용 기간이 언제 재설정되는지만 알려주고 조직의 요금제, 사용량, 한도는 공개하지 않습니다. 브라우저 클라이언트는 used 또는 limit가 있다고 가정하면 안 됩니다. 같은 규칙이 429 quota_exceeded 응답 본문에도 적용됩니다. 할당량 및 속도 제한을 참조하세요.
연결 유지와 시간 제한
| 동작 | 값 | 클라이언트 처리 방법 |
|---|---|---|
| 채팅 스트림의 연결 유지 신호 | 다른 출력 없이 10초가 지나면 전송되는 : keep-alive 주석 | 주석을 포함하여 바이트가 하나라도 들어오면 유휴 타이머를 재설정합니다. |
| 유휴 연결 | 약 1분간 조용한 연결은 전송 중 닫힐 수 있습니다 | 읽기 유휴 시간 제한을 30~60초로 설정합니다. 전체 시간 제한을 너무 짧게 설정하지 마세요. |
| 첫 본문까지 걸리는 시간 | 어려운 질문은 수십 초가 걸릴 수 있습니다 | phase 이후 진행 상태를 표시하고 대기 시간을 실패로 간주하지 마세요. |
| 최대 스트림 길이 | 약 10분 후 서버가 upstream_stall을 포함한 error를 전송합니다 | 보호 장치로 전체 제한 시간을 10분보다 조금 길게 설정합니다. |
스트림 중단 및 과금
스트림을 중단하려면 연결을 닫으세요. 서버는 약 10초 이내에 이를 감지하고 답변 생성을 중지합니다.
- 정상적으로 완료된 스트림은
end.total_tokens에 보고된 답변의 토큰 사용량으로 과금됩니다. - 완료 전에 중단되거나 실패한 스트림은 추정치로 과금됩니다. 최소한 요청으로 전송한 대화의 크기만큼 계산되며, 그 외에는 이미 스트리밍한 텍스트에 따라 산정됩니다. 따라서 요청 수락 시 예약한 금액보다 클 수 있습니다. 첫 프레임에서 연결을 닫는다고 긴 대화가 무료가 되지는 않습니다.
- 자체 데이터에서 온 장소(
source: "vendor")는 비용 계산에 포함하지 않습니다.
재시도는 새로운 요청이며 다시 과금됩니다. 할당량 및 속도 제한을 참조하세요.
요약 및 버튼 스트림
이 두 엔드포인트는 Kaleidr Chat의 장소 카드를 지원합니다. 동일한 프레임 형식, 이름 없는 텍스트 프레임, 마지막 quota 이벤트를 사용합니다. 채팅 스트림과는 세 가지가 다릅니다. phase 이벤트가 없고, 장소나 동작 이벤트가 없으며, 연결 유지 신호도 없습니다. 유휴 시간 제한은 60초로 설정하세요.
두 엔드포인트 모두 너무 긴 필드는 스트림이 시작되기 전에 422 field_too_long으로 거부하고, 명시되지 않은 필드는 무시합니다. 엔드포인트를 참조하세요. 이들 스트림이 중단되거나 실패하면 이미 전송된 텍스트에 대한 추정치로 과금되며 예약한 금액을 초과하지 않습니다.
POST /chat/summary/stream, 장소인 경 우:
- 선택 사항.
{ type: "address", address, source }페이로드의address { type: "start", poi_type }페이로드의start- 선택 사항이며 여러 번 올 수 있음.
{ type: "url", website, source }페이로드의url { text }페이로드의 텍스트 프레임{ type: "end", full_text, summary, poi_type, category, city, country, sources[], website?, total_tokens? }페이로드의end또는{ type: "error", message, partial_text }페이로드의error- 선택 사항.
quota
POST /chat/summary/stream, 건물인 경우(type: "building"):
{ type: "start", address }페이로드의start- 텍스트 프레임
{ type: "end", full_text, parsed, total_tokens? }페이로드의end또는error- 선택 사항.
quota
POST /chat/button/stream:
{ type: "start", query_type }페이로드의start- 텍스트 프레임
{ type: "end", full_text, reply, query_type, first_token_ms, total_ms, total_tokens? }페이로드의end- 선택 사항.
quota
버튼 스트림에는 error 이벤트가 없습니다. 시작한 뒤 실패하면 end 없이 연결이 닫힙니다. 시작 전에 실패하면 HTTP 500과 {"detail": "Button chat stream error"}가 반환됩니다.
**POST /spot/overview/stream**은 위에서 설명한 장소 요약 프레임을 보내며, 그 앞에 { type: "place", name }이 담긴 place 프레임을 보냅니다. name은 설명하는 장소나 지역의 전체 이름(예: "Springfield, Ohio, United States of America")으로, 라벨 표시나 복사에 사용합니다. 이 프레임은 다른 모든 프레임보다 먼저 오며 없을 수도 있습니다. 채팅 스트림의 place 이벤트와는 다르며 좌표를 포함하지 않습니다. 설명할 수 없는 ref에 대해서는 요약 대신 { type: "error", message, ref }가 담긴 단일 error 프레임을 보냅니다. 엔드포인트를 참조하세요.
관련 문서
- 스트림 읽기: 완전한 Node 및 Python 클라이언트.
- 엔드포인트: 각 스트림의 요청 본문.
- 오류: 스트림 시작 전과 처리 중 발생하는 오류.
- 할당량 및 속도 제한:
quota이벤트와 과금.