스트리밍

SSE 청크 형식과 마지막 usage 청크

요청 본문에 "stream": true 를 넣으면 응답이 Server-Sent Events 로 옵니다. /chat/completions/completions 둘 다 지원합니다.

content-type: text/event-stream; charset=utf-8
cache-control: no-cache, no-transform
x-accel-buffering: no
X-MyIP-Currency: KRW
X-MyIP-Generation-Id: gen-…
X-MyIP-Request-Id: req-…
X-MyIP-Model: google/gemma-4-26b-a4b
X-MyIP-Provider: MyIP Local GPU

프레임 형식

각 이벤트는 data: 로 시작하고 빈 줄(\n\n)로 끝납니다. 마지막은 항상 data: [DONE] 입니다.

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452301,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452301,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","choices":[{"index":0,"delta":{"content":"안녕"},"finish_reason":null}]}

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452302,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","choices":[{"index":0,"delta":{"content":"하세요"},"finish_reason":null}]}

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452302,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","choices":[{"index":0,"delta":{},"finish_reason":"stop","native_finish_reason":"stop"}]}

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452302,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","choices":[],"usage":{"prompt_tokens":18,"completion_tokens":9,"total_tokens":27,"cost":0.001890,"cost_details":{"upstream_inference_cost":0.000383}}}

data: [DONE]

모든 청크에서 우리가 다시 쓰는 필드는 셋입니다.

  • id — 업스트림 id 를 우리 generation id 로 바꿉니다. 스트림 전체가 같은 값을 갖고, X-MyIP-Generation-Id 헤더와도 같습니다.
  • model — 실제로 응답한 후보의 모델 id.
  • provider — 그 후보의 provider 표시명.

finish_reason 이 있는데 native_finish_reason 이 없으면 같은 값을 복사해 채웁니다.

마지막 usage 청크

스트리밍 응답에는 X-MyIP-Cost-KRW 헤더가 없습니다. 헤더는 첫 바이트보다 먼저 나가는데, 그 시점에는 토큰 수도 비용도 알 수 없기 때문입니다. 그래서 비용은 마지막 usage 청크에 실어 보냅니다.

usage.prompt_tokensinteger

프롬프트 토큰 수.

usage.completion_tokensinteger

완성 토큰 수. 추론(reasoning) 토큰이 있으면 여기 포함되어 옵니다.

usage.costnumber

이번 요청의 청구액. 단위는 KRW(원) 이고 소수점 6자리에서 반올림된 값입니다. X-MyIP-Currency: KRW 헤더가 단위를 못박습니다.

usage.cost_details.upstream_inference_costnumber | null

우리 원가(원). 원가를 모르는 후보면 null 입니다.

이 값은 나중에 GET /generation이 돌려주는 total_cost 와 정확히 같은 숫자입니다.

읽는 법

curl -N https://openrouter.myip.co.kr/api/v1/chat/completions \
  -H "Authorization: Bearer $MYIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/gemma-4-26b-a4b",
    "messages": [{"role": "user", "content": "세 문장으로 바다를 묘사해줘"}],
    "stream": true
  }'

스트림 도중의 실패

첫 바이트를 보낸 뒤에는 다른 후보로 재시도하지 않습니다. 이미 클라이언트가 받은 토큰을 없던 일로 만들 수 없기 때문입니다. 대신 오류를 SSE 이벤트로 알리고 스트림을 정상 종료합니다.

data: {"id":"gen-7kq…","object":"chat.completion.chunk","created":1788452310,"model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","error":{"code":502,"message":"upstream disconnected","metadata":{"error_type":"provider_error"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

data: [DONE]

HTTP 상태는 이미 200 으로 나갔으므로, 청크 안의 error 필드를 반드시 확인하세요. 상태 코드만 보면 성공으로 보입니다.

후보를 바꿔 재시도하는 것은 첫 바이트 이전에만 일어납니다. 그 단계에서 체인이 전부 실패하면 스트림이 아니라 일반 JSON 오류 응답이 옵니다.

중간에 끊었을 때

클라이언트가 연결을 끊으면 업스트림 호출도 함께 끊습니다. 그리고 그 시점까지 생성된 토큰으로 정산합니다. 스트림을 끊는 것이 무료 사용이 되면 안 되기 때문입니다. 그 요청은 사용 기록에 cancelled 로 남고, GET /generationcancelledtrue 가 됩니다.

/completions 의 스트리밍

레거시 /completions도 스트리밍을 지원합니다. 청크의 objecttext_completion 이고, 텍스트는 delta.content 가 아니라 choices[].text 에 들어갑니다. 나머지(usage 청크, [DONE], 오류 이벤트)는 같습니다.

정산 시점

비스트리밍 요청은 응답을 만들기 전에 정산이 끝나므로, 응답을 받은 직후 /generation 을 불러도 기록이 있습니다. 스트리밍은 다릅니다 — 정산은 스트림이 닫힌 뒤에 응답 수명과 분리되어 돌아갑니다. [DONE] 직후에 /generation 을 부르면 아직 404 일 수 있으니 짧게 재시도하세요.

마지막 수정 2026. 9. 5.