본문으로 건너뛰기
스트리밍

스트리밍

stream을 true로 설정하면 모델이 작성하는 동안 응답이 server-sent events로 도착합니다. 추론이 먼저 오고, 그다음 답변, 마지막으로 종료 사유와 사용량이 있는 청크가 옵니다. 사람이 텍스트를 기다릴 때와 긴 응답에 사용하세요.

POST https://api.shannon-ai.com/v1/chat/completions

OpenAI SDK는 청크를 하나씩 제공합니다. 각 청크는 담고 있는 내용에 따라 읽으세요: 추론 한 조각, 답변 한 조각, 도구 호출, 또는 응답의 끝.

from openai import OpenAI, APIError

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://api.shannon-ai.com/v1",
)

stream = client.chat.completions.create(
    model="shannon-3",
    messages=[{"role": "user", "content": "Explain server-sent events in three sentences."}],
    stream=True,
)

answer, reasoning, tool_calls = [], [], []
finish_reason = usage = None

try:
    for chunk in stream:
        if chunk.usage:
            usage = chunk.usage
        if not chunk.choices:
            continue
        choice = chunk.choices[0]
        delta = choice.delta

        thought = getattr(delta, "reasoning_content", None)
        if thought:
            reasoning.append(thought)
        if delta.content:
            answer.append(delta.content)
            print(delta.content, end="", flush=True)
        for call in delta.tool_calls or []:
            tool_calls.append((call.id, call.function.name, call.function.arguments))
        if choice.finish_reason:
            finish_reason = choice.finish_reason
except APIError as error:
    # A failure after the stream started arrives as an error chunk.
    print(f"\nstream failed: {error}")

print(f"\n{finish_reason} {usage}")

스트림의 프레이밍

  • 응답의 상태는 200이고 헤더는 content-type: text/event-stream과 cache-control: no-cache입니다.
  • 각 이벤트는 data: 로 시작하는 한 줄이며 그 뒤에 빈 줄이 옵니다. data: 뒤의 텍스트는 JSON 청크 하나입니다. event: 줄이나 id: 줄은 없습니다.
  • : ping처럼 콜론으로 시작하는 줄은 연결을 유지하는 주석입니다. 건너뛰세요.
  • 스트림은 data: [DONE]으로 끝납니다. 이것 없이 멈춘 스트림은 불완전합니다.

순서대로의 청크

모든 청크는 chat.completion.chunk 객체입니다.

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "Server-sent"
      },
      "finish_reason": null
    }
  ]
}

한 스트림의 모든 청크는 같은 id, created, model을 가집니다. choices에는 항상 항목이 하나 있습니다. delta에는 새로 추가된 내용이 들어 있고, finish_reason은 마지막 청크까지 null입니다.

delta의 키 유형 설명 전송하는 곳
role string 항상 assistant입니다. 스트림의 시작에 한 번 도착합니다. 호스팅 오픈 웨이트 모델, shannon-2-lite, shannon-2-pro
reasoning_content string 모델의 추론 한 조각입니다. 조각을 순서대로 이어 붙이세요. 호스팅 오픈 웨이트 모델, Shannon 3 계열, shannon-1.6-pro, shannon-coder-1
content string 답변의 한 조각입니다. 조각을 순서대로 이어 붙이세요. 모든 모델
tool_calls array 완전한 도구 호출 하나입니다. 도구 호출 델타를 참고하세요. 모든 모델

추론이 먼저 오고, 그다음 답변, 그다음 도구 호출이 옵니다. 청크는 스트림에서의 위치가 아니라 delta의 키로 읽으세요. delta에는 첫 텍스트 조각과 함께 role이 들어 있을 수 있고, 마지막 것은 비어 있습니다.

전송되는 그대로의 전체 스트림입니다. 줄을 짧게 하기 위해 여기서는 각 청크의 id, object, created, model을 생략했습니다.

200 text/event-stream
data: {"choices":[{"index":0,"delta":{"reasoning_content":"Three sentences:"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"reasoning_content":" what it is, how it is framed, why it is used."},"finish_reason":null}]}

: ping

data: {"choices":[{"index":0,"delta":{"content":"Server-sent"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"content":" events let a server push text to a client over one HTTP reply."},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":84,"total_tokens":1274}}

data: [DONE]

추론 델타

추론하는 모델은 답변 전에 추론을 delta.reasoning_content로 보냅니다. 두 텍스트를 분리해 두세요. 추론은 접을 수 있는 별도 영역으로 표시하거나 생략합니다.

호스팅 오픈 웨이트 모델은 reasoning_effort가 off일 때와 요청에 response_format이 있을 때는 추론을 보내지 않습니다.

shannon-2-lite와 shannon-2-pro도 답변 전에 추론합니다. 이 엔드포인트에서는 추론하는 동안 : thinking 주석 줄을 보내고, 그다음 답변을 보냅니다.

reasoning_content는 OpenAI SDK의 타입 모델에 없습니다. Python에서는 getattr(delta, "reasoning_content", None)으로 읽고, JavaScript에서는 delta.reasoning_content가 그대로 작동합니다.

도구 호출 델타

도구 호출은 통째로 도착합니다. 호출마다 청크 하나이며, 그 delta에 완전한 arguments 문자열이 있는 호출이 들어 있습니다. index는 응답의 호출에 0부터 번호를 매깁니다. arguments를 조각별로 이어 붙이는 코드는 조각이 하나뿐이므로 그대로 작동합니다.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

호출 뒤의 마지막 청크는 finish_reason이 tool_calls입니다.

요청에 tools가 있으면 shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1은 답변 텍스트를 응답 끝에 델타 하나로 보냅니다.

마지막 청크

data: [DONE] 직전의 마지막 청크에는 비어 있는 delta, finish_reason, 요청의 usage가 모두 같은 청크에 들어 있습니다. 모든 스트림에 있으며 stream_options는 필요하지 않습니다.

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1190,
    "completion_tokens": 84,
    "total_tokens": 1274
  }
}

호스팅 오픈 웨이트 모델에서는 usage에 prompt_tokens_details.cached_tokens와 completion_tokens_details.reasoning_tokens도 있습니다.

web_search: true로 보낸 요청에서 검색이 결과를 찾았다면 마지막 청크에 sources도 들어 있고, 그 앞의 청크 하나에 인용 표시가 delta.annotations로 들어 있습니다. 내장 웹 검색

finish_reason 설명 전송하는 곳
stop 모델이 답변을 끝냈거나 stop 문자열이 나타났습니다. 모든 모델
tool_calls 응답에 도구 호출이 하나 이상 있습니다. 모든 모델
length 응답이 출력 한도에서 잘렸습니다. Shannon 3 계열, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

usage의 필드는 엔드포인트와 함께 설명되어 있습니다. Chat Completions

keep-alive 줄

모델이 작업 중이지만 아직 보낼 내용이 없는 동안 스트림은 주석 줄을 보냅니다. 여기에는 데이터가 없습니다. SSE 파서는 스스로 건너뛰며, 원시 줄을 읽는 코드는 콜론으로 시작하는 모든 줄을 건너뛰어야 합니다.

줄 전송하는 곳 시점
: ping Shannon 3 계열, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 15초마다.
: keepalive 호스팅 오픈 웨이트 모델 모델이 작업하는 동안 15초마다.
: thinking shannon-2-lite, shannon-2-pro 모델이 추론하는 동안.

스트림 안의 오류

스트림이 시작되면 상태는 200이므로, 실패는 스트림 안에서 도착합니다. choices가 있을 자리에 error 멤버가 있는 청크입니다.

200 text/event-stream
data: {"error":{"message":"The model backend failed to answer. Please retry.","type":"api_error","code":null,"param":null}}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":12,"total_tokens":1202}}

data: [DONE]

오류 청크 뒤에도 finish_reason과 usage가 있는 마지막 청크와 data: [DONE]이 이어집니다. 모든 청크에서 error를 확인하세요. 확인하지 않으면 중간에 실패한 응답이 완료된 것처럼 보입니다.

유형 코드 메시지 시점
api_error The model backend failed to answer. Please retry. 모델이 실패했거나 아무것도 작성하지 않았습니다. 요청을 다시 보내세요.
rate_limit_error Shannon routes are temporarily busy. Please retry. 모델이 지금 바쁩니다. 몇 초 기다렸다가 요청을 다시 보내세요.
invalid_request_error context_length_exceeded 대화가 모델의 컨텍스트 윈도우보다 깁니다. 줄인 뒤 다시 보내세요. 메시지 텍스트는 다양합니다.

호스팅 오픈 웨이트 모델은 type과 message만 있는 error 멤버를 보냅니다.

OpenAI Python 및 JavaScript SDK는 반복하는 동안 오류 청크를 API 오류로 발생시키므로, 루프를 일반적인 오류 처리로 감싸세요.

상태 코드, 오류 본문, 재시도할 항목은 별도 페이지에 있습니다. 오류 처리

연결 닫기

연결을 닫으면 전달이 멈출 뿐 요청이 멈추지는 않습니다. 모델은 응답을 끝까지 작성하며, 요청은 응답을 모두 읽은 것처럼 청구됩니다. 비용을 줄이려면 적게 요청하세요. 적용하는 모델에서는 max_tokens를 낮게 설정하세요.

스트림은 전달되는 도중에도 일찍 끝날 수 있습니다. 예를 들어 네트워크가 끊기거나 서버를 업데이트하는 중입니다. 그러면 마지막 청크와 data: [DONE] 없이 멈춥니다. 그런 응답은 불완전한 것으로 처리하고 요청을 다시 보내세요.

SDK 없이 스트림 읽기

올바른 리더는 다섯 가지를 합니다. 스트림 이전에 도착한 오류를 오류 처리로 보내고, 본문을 줄 단위로 나누고, 주석 줄을 건너뛰고, data: [DONE]에서 멈추고, 각 청크에서 error를 확인합니다.

import json
import requests

response = requests.post(
    "https://api.shannon-ai.com/v1/chat/completions",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "model": "shannon-3",
        "messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
        "stream": True,
    },
    stream=True,
)

# 1. Errors before the stream are plain JSON with their own status.
if response.status_code != 200:
    raise RuntimeError(f"{response.status_code}: {response.text}")

answer, done = [], False
for raw in response.iter_lines():          # 2. one line at a time
    line = raw.decode("utf-8")
    if not line.startswith("data: "):      # 3. empty lines and ": ping" comments
        continue
    data = line[len("data: "):]
    if data == "[DONE]":                   # 4. the end of the stream
        done = True
        break
    chunk = json.loads(data)
    if "error" in chunk:                   # 5. a failure inside the stream
        raise RuntimeError(chunk["error"]["message"])
    delta = chunk["choices"][0]["delta"]
    if delta.get("content"):
        answer.append(delta["content"])
    if chunk.get("usage"):
        print(chunk["choices"][0]["finish_reason"], chunk["usage"])

if not done:
    raise RuntimeError("the stream ended before [DONE]")
print("".join(answer))

Python 샘플은 requests 패키지를 사용합니다. JavaScript 샘플은 Node.js 18 이상에서 실행됩니다.

다른 형식의 스트림

Responses와 Messages 엔드포인트도 스트리밍을 지원합니다. 프레임에는 이벤트 이름이 있는 event: 줄과 JSON이 있는 data: 줄이 있습니다. 각 페이지에 이벤트가 순서대로 나열되어 있습니다.

스트림에서 Chat CompletionsResponsesMessages
프레임 data: 줄event: 줄과 data: 줄event: 줄과 data: 줄
추론이 도착하는 곳 delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
답변 텍스트가 도착하는 곳 delta.contentresponse.output_text.deltatext_delta
사용량이 도착하는 곳 마지막 청크response.completedmessage_delta
스트림의 끝 마지막 청크, 그다음 data: [DONE]response.completedmessage_stop