# 스트리밍 > Server-sent events의 청크 형식, keepalive 주석, 마지막 청크의 usage와 cost, 연결이 끊겼을 때의 동작을 설명합니다. Source: https://kiln.bricksum.com/docs/ko/streaming chat/completions 요청에서 stream을 true로 설정하면 생성되는 토큰을 server-sent events(SSE)로 실시간 수신합니다. 게이트웨이는 모델 서버의 이벤트를 도착하는 대로 전달합니다. 여기에 keepalive 주석을 더하고, usage를 요청하면 마지막 usage 청크에 비용을 추가하며, 요청하지 않으면 그 청크를 제거합니다. ## 요청 stream은 JSON 불리언 true로 보내세요. 게이트웨이는 문자열 "true"와 숫자 1도 스트리밍으로 읽지만, SDK와 다른 OpenAI 형식 서버는 불리언을 기대합니다. 공식 OpenAI SDK는 이 값을 대신 설정하고 스트림도 파싱합니다. **curl** ```bash curl https://api.bricksum.com/v1/chat/completions \ -H "Authorization: Bearer sk-bk-..." \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-32b", "messages": [{"role": "user", "content": "Hello!"}], "stream": true }' ``` **Python** ```python from openai import OpenAI client = OpenAI( base_url="https://api.bricksum.com/v1", api_key="sk-bk-...", ) stream = client.chat.completions.create( model="qwen3-32b", messages=[{"role": "user", "content": "Hello!"}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="") ``` **JavaScript** ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.bricksum.com/v1", apiKey: process.env.KILN_API_KEY, }); const stream = await client.chat.completions.create({ model: "qwen3-32b", messages: [{ role: "user", content: "Hello!" }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0].delta.content ?? ""); } ``` ## 이벤트 형식 응답의 Content-Type은 text/event-stream입니다. 각 이벤트는 chat.completion.chunk를 담은 data: 줄 하나이며, choices[0].delta에 새 텍스트가 들어 있습니다. 마지막 delta에 finish_reason이 실리고, 스트림은 data: [DONE]으로 끝납니다. **text/event-stream** ```text data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1710000000,"model":"qwen3-32b","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1710000000,"model":"qwen3-32b","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` 참고 대부분의 OpenAI SDK는 stream=true를 넘기면 SSE를 대신 파싱해 delta 객체를 돌려줍니다. 빈 줄로 나누고 주석 줄을 건너뛰는 처리는 원시 HTTP 클라이언트에서만 필요합니다. ## keepalive 주석 모델이 아직 토큰을 내지 않은 동안에는 게이트웨이가 15초마다 SSE 주석 줄을 보내 프록시와 클라이언트의 타임아웃을 막습니다. 부하가 높아 요청이 모델 서버의 대기열에서 기다리는 동안도 여기에 포함됩니다. 콜론으로 시작하는 줄은 주석이며 데이터가 없으므로 무시하세요. 부하가 높을 때 첫 토큰까지 오래 걸리는 것은 정상이며 에러가 아닙니다. **comment line** ```text : keepalive ``` ## 토큰 사용량과 비용 기본적으로 스트림에는 usage가 실리지 않습니다. 요청에서 `stream_options.include_usage` 값을 true로 설정하면 스트림 끝에 청크가 하나 더 옵니다. 이 청크는 choices가 비어 있고 usage 객체에 토큰 수가 담겨 있습니다. **request** ```json { "model": "qwen3-32b", "messages": [{"role": "user", "content": "Hello!"}], "stream": true, "stream_options": { "include_usage": true } } ``` **final chunk** ```text data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1710000000,"model":"qwen3-32b","choices":[],"usage":{"prompt_tokens":9,"completion_tokens":12,"total_tokens":21,"cost":0.000001155}} data: [DONE] ``` 게이트웨이는 그 청크에 usage.cost를 추가합니다. 이 값은 이 요청으로 크레딧에서 차감된 USD 금액이며, usage의 토큰 수와 모델의 토큰 단가로 계산합니다. 비스트리밍 응답에도 같은 usage.cost 필드가 실립니다. usage를 요청하지 않으면 이 청크는 클라이언트에 도달하기 전에 제거됩니다. 이 예시의 토큰 수와 cost는 gpt-oss-120b에서 측정한 예시 값이며, qwen3-32b의 값이 아닙니다. 계측된 모든 응답에는 `X-Neocloud-Generation-Id` 헤더가 실립니다. 값은 이 요청의 사용량 이벤트 ID입니다. 로그와 함께 저장해 두면 차감 내역이나 에러를 해당 요청까지 추적할 수 있습니다. ## 연결 끊김과 에러 - 클라이언트가 스트림 도중 연결을 끊어도 생성이 즉시 취소되지는 않습니다. 게이트웨이는 사용량을 기록하기 위해 스트림을 끝까지 읽으며, 생성된 토큰은 과금됩니다. - 모델 서버가 스트림 도중 실패하면 에러가 그대로 전달되고 그 요청은 과금되지 않습니다. - 스트림이 finish_reason: "length"로 끝나면 max_tokens를 늘리세요. reasoning 토큰도 max_tokens에 포함됩니다. 알려진 제한은 [API 레퍼런스](https://kiln.bricksum.com/docs/ko/api-reference)를 참고하세요.