메시지 스트리밍하기

메시지 스트리밍하기 (Streaming messages)

스트리밍(streaming)을 쓰면 Message를 만들 때 "stream": true를 설정해서 응답을 server-sent events(SSE) 방식으로 조금씩 받아볼 수 있어요. 모델이 토큰을 만들어내는 대로 즉시 전달받으니, 사용자에게 더 자연스러운 경험을 줄 수 있답니다. SDK와 원시 HTTP 로우 레벨 API로 각각 어떻게 스트리밍하는지 살펴볼게요.

출처: 문서

본문

SDK로 스트리밍하기

Python SDKTypeScript SDK는 여러 가지 방식으로 스트리밍을 지원해요. PHP SDKcreateStream()을 통해 스트리밍을 제공해요. Python SDK는 동기(sync)와 비동기(async) 스트림을 모두 지원해요. 각 SDK의 문서에서 자세한 내용을 확인해 주세요.

```bash CLI ant messages create --stream --format jsonl \ --model claude-opus-5-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello"}' \ | jq -rj 'select(.delta.type? == "text_delta") | .delta.text' ```
client = anthropic.Anthropic()

with client.messages.stream(
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello"}],
    model="claude-opus-5-5",
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
const client = new Anthropic();

await client.messages
  .stream({
    messages: [{ role: "user", content: "Hello" }],
    model: "claude-opus-5-5",
    max_tokens: 1024
  })
  .on("text", (text) => {
    console.log(text);
  });
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Messages = [new() { Role = Role.User, Content = "Hello" }]
};

await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    Console.Write(msg);
}
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")),
	},
})

for stream.Next() {
	event := stream.Current()
	switch eventVariant := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch deltaVariant := eventVariant.Delta.AsAny().(type) {
		case anthropic.TextDelta:
			fmt.Print(deltaVariant.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024L)
    .addUserMessage("Hello")
    .build();

try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(event -> {
        event.contentBlockDelta().ifPresent(deltaEvent ->
            deltaEvent.delta().text().ifPresent(td ->
                System.out.print(td.text())
            )
        );
    });
}
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'Hello']
    ],
    model: 'claude-opus-5-5',
);

foreach ($stream as $message) {
    echo $message;
}
client = Anthropic::Client.new

stream = client.messages.stream(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello" }]
)

stream.text.each { |text| print(text) }

이벤트를 직접 다루지 않고 최종 메시지 얻기

텍스트가 도착하는 대로 처리할 필요가 없다면, SDK들이 제공하는 방식으로 스트리밍을 내부에서 쓰면서도 완전한 Message 객체(마치 .create()가 반환하는 것과 동일)를 돌려받을 수 있어요. 큰 max_tokens 값의 요청에서 특히 유용한데, 그런 경우 SDK들이 HTTP 타임아웃을 피하려고 스트리밍을 요구하거든요.

```bash CLI # ant CLI의 --stream 플래그는 이벤트를 한 줄씩 내보내고 최종 Message로 # 누적하지 않습니다. 긴 생성에서는 원시 이벤트를 스트리밍하세요: ant messages create --stream --format jsonl <<'YAML' model: claude-opus-5-5 max_tokens: 128000 messages: - role: user content: Write a detailed analysis... YAML ```
client = anthropic.Anthropic()

with client.messages.stream(
    max_tokens=128000,
    messages=[{"role": "user", "content": "Write a detailed analysis..."}],
    model="claude-opus-5-5",
) as stream:
    message = stream.get_final_message()

for block in message.content:
    if block.type == "text":
        print(block.text)
const client = new Anthropic();

const stream = client.messages.stream({
  max_tokens: 128000,
  messages: [{ role: "user", content: "Write a detailed analysis..." }],
  model: "claude-opus-5-5"
});

const message = await stream.finalMessage();
const textBlock = message.content.find((block) => block.type === "text");
if (textBlock && textBlock.type === "text") {
  console.log(textBlock.text);
}
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 128000,
    Messages = [new() { Role = Role.User, Content = "Write a detailed analysis..." }]
};

var fullText = "";
await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    fullText += msg;
}

Console.WriteLine(fullText);
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 128000,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Write a detailed analysis...")),
	},
})

message := anthropic.Message{}
for stream.Next() {
	event := stream.Current()
	if err := message.Accumulate(event); err != nil {
		log.Fatal(err)
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}

for _, block := range message.Content {
	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
		fmt.Println(textBlock.Text)
	}
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(128000L)
    .addUserMessage("Write a detailed analysis...")
    .build();

MessageAccumulator accumulator = MessageAccumulator.create();
try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(accumulator::accumulate);
}

Message message = accumulator.message();
message.content().stream()
    .flatMap(block -> block.text().stream())
    .forEach(textBlock -> System.out.println(textBlock.text()));
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 128000,
    messages: [
        ['role' => 'user', 'content' => 'Write a detailed analysis...']
    ],
    model: 'claude-opus-5-5',
);

$fullText = '';
foreach ($stream as $event) {
    if ($event->type === 'content_block_delta' && $event->delta->type === 'text_delta') {
        $fullText .= $event->delta->text;
    }
}

echo $fullText;
client = Anthropic::Client.new

message = client.messages.stream(
  model: "claude-opus-5-5",
  max_tokens: 128000,
  messages: [{ role: "user", content: "Write a detailed analysis..." }]
).accumulated_message

message.content.each do |block|
  puts block.text if block.type == :text
end

.stream() 호출은 SSE로 HTTP 연결을 유지하다가, .get_final_message()(Python)나 .finalMessage()(TypeScript)가 모든 이벤트를 모아 완전한 Message 객체를 돌려줘요. Go에서는 스트림 루프 안에서 message.Accumulate(event)를 호출해 같은 완전한 Message를 만들고, Java에서는 MessageAccumulator.create()로 만든 뒤 각 이벤트마다 accumulator.accumulate(event)를 호출해요. C#에서는 스트림의 .Aggregate() 확장 메서드를 await 해 완전한 Message를 얻거나, MessageContentAggregator.CollectAsync()에 넘겨 이벤트를 다루면서 누적할 수 있어요. Ruby에서는 스트림에 .accumulated_message를 호출하고, PHP SDK에서는 스트림 이벤트를 직접 순회하며 응답을 누적합니다.

이벤트 타입

각 SSE 이벤트는 명명된 이벤트 타입과 함께 관련 JSON 데이터를 포함해요. 각 이벤트는 SSE 이벤트 이름(예: event: message_stop)을 쓰고, 데이터 안에는 일치하는 type 이벤트를 담아요.

각 스트림은 다음의 이벤트 흐름을 사용해요:

  1. message_start: 비어 있는 content를 가진 Message 객체를 담아요. thinking-binding-controls-2026-08-01 베타 헤더 아래에서는 이 Message 객체가 input_transformations 배열도 지녀요. 중간에 서버 측 폴백이 일어난 뒤에는 마지막 message_delta 이벤트가 서빙 모델의 항목들과 함께 배열을 다시 전달해요.
  2. 일련의 콘텐츠 블록. 각각 content_block_start, 하나 이상의 content_block_delta 이벤트, 그리고 content_block_stop 이벤트를 가져요. 각 콘텐츠 블록은 최종 Message content 배열에서의 인덱스에 해당하는 index를 지녀요. 예외가 하나 있는데, 서버 측 폴백 응답 중에는 각 모델 경계에서 fallback 콘텐츠 블록이 content_block_startcontent_block_stop 쌍으로 도착하고 그 사이에 델타는 없어요.
  3. 하나 이상의 message_delta 이벤트. 최종 Message 객체에 대한 최상위 변경을 나타내요.
  4. 마지막 message_stop 이벤트.
`message_delta` 이벤트의 `usage` 필드에 보이는 토큰 수는 *누적(cumulative)* 값이에요.

Ping 이벤트

이벤트 스트림에는 얼마든지 많은 ping 이벤트가 포함될 수 있어요.

오류 이벤트

API는 스트림 안에서 오류를 이따금 보낼 수 있어요. 예를 들어 사용률이 높은 동안에는 overloaded_error를 받을 수 있는데, 이는 비스트리밍 상황의 HTTP 529에 해당해요:

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

기타 이벤트

버저닝 정책에 따라 새 이벤트 타입이 추가될 수 있으니, 모르는 이벤트 타입도 우아하게 처리하도록 코드를 작성하세요.

콘텐츠 블록 델타 타입

content_block_delta 이벤트는 주어진 indexcontent 블록을 갱신하는 delta 타입을 담아요.

텍스트 델타

text 콘텐츠 블록 델타는 다음과 같아요:

event: content_block_delta
data: {"type": "content_block_delta","index": 0,"delta": {"type": "text_delta", "text": "ello frien"}}

입력 JSON 델타

tool_use 콘텐츠 블록의 델타는 블록의 input 필드 갱신과 대응해요. 최대한 세밀한 단위를 지원하기 위해 델타는 *부분 JSON 문자열(partial JSON strings)*이고, 최종 tool_use.input은 항상 *객체(object)*예요.

content_block_stop 이벤트를 받으면 문자열 델타를 누적한 뒤 Pydantic 같은 라이브러리로 부분 JSON 파싱을 하거나, SDK가 제공하는 헬퍼(파싱된 증분 값에 접근)를 써서 JSON을 파싱할 수 있어요.

tool_use 콘텐츠 블록 델타는 다음과 같아요:

event: content_block_delta
data: {"type": "content_block_delta","index": 1,"delta": {"type": "input_json_delta","partial_json": "{\"location\": \"San Fra"}}

알아두세요: 현재 모델들은 input에서 한 번에 완전한 키-값 속성 하나만 내보내는 것을 지원해요. 그래서 도구를 쓸 때 모델이 작업하는 동안 스트리밍 이벤트 사이에 지연이 생길 수 있어요. input 키와 값이 누적되면 여러 content_block_delta 이벤트로 잘게 쪼갠 부분 JSON 형태로 내보내지는데, 이는 미래 모델에서 더 세밀한 단위를 자동으로 지원하기 위한 형식이에요.

사고(thinking) 델타

thinking을 스트리밍과 함께 쓸 때, thinking_delta 이벤트로 사고 콘텐츠를 받아요. 이 델타들은 thinking 콘텐츠 블록의 thinking 필드와 대응해요.

사고 콘텐츠의 경우, content_block_stop 이벤트 직전에 특별한 signature_delta 이벤트가 전송돼요. 이 서명은 thinking 블록의 무결성을 검증하는 데 쓰여요.

thinking 설정에 display: "omitted"를 설정하면 어떤 thinking 텍스트도 스트리밍되지 않아요. thinking 블록이 열리고, 빈 thinking 문자열을 가진 thinking_delta 하나와 signature_delta 하나를 받은 뒤 닫혀요. display: "updates"(베타)에서는 추론 블록이 같은 방식으로 스트리밍되고, 오직 일부 모델이 도구 호출 사이에 쓰는 진행 업데이트만 텍스트를 담은 thinking_delta 이벤트로 스트리밍돼요. thinking 표시 제어를 참고하세요.

전형적인 thinking 델타는 다음과 같아요:

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}}

서명 델타는 다음과 같아요:

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}}

전체 HTTP 스트림 응답

스트리밍 모드를 쓸 땐 클라이언트 SDK를 사용하세요. 하지만 직접 API 통합을 만들고 있다면 이 이벤트들을 직접 처리해야 해요.

스트림 응답은 다음으로 구성돼요:

  1. message_start 이벤트

  2. 여러 개일 수 있는 콘텐츠 블록. 각각은 다음을 포함해요:

    • content_block_start 이벤트
    • 여러 개일 수 있는 content_block_delta 이벤트
    • content_block_stop 이벤트
  3. 하나 이상의 message_delta 이벤트

  4. message_stop 이벤트

응답 전체에 ping 이벤트가 흩어져 있을 수 있어요. 형식에 대한 자세한 내용은 이벤트 타입을 참고하세요.

기본 스트리밍 요청

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -d '{ "model": "claude-opus-5-5", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 256, "stream": true }' ```
ant messages create --stream --format jsonl \
  --model claude-opus-5-5 \
  --max-tokens 256 \
  --message '{role: user, content: Hello}'
client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=256,
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
const client = new Anthropic();

const stream = client.messages.stream({
  model: "claude-opus-5-5",
  messages: [{ role: "user", content: "Hello" }],
  max_tokens: 256
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 256,
    Messages = [new() { Role = Role.User, Content = "Hello" }]
};

await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    Console.Write(msg);
}
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 256,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")),
	},
})

for stream.Next() {
	event := stream.Current()
	switch eventVariant := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch deltaVariant := eventVariant.Delta.AsAny().(type) {
		case anthropic.TextDelta:
			fmt.Print(deltaVariant.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(256L)
    .addUserMessage("Hello")
    .build();

try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(event -> {
        event.contentBlockDelta().ifPresent(deltaEvent ->
            deltaEvent.delta().text().ifPresent(td ->
                System.out.print(td.text())
            )
        );
    });
}
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 256,
    messages: [
        ['role' => 'user', 'content' => 'Hello']
    ],
    model: 'claude-opus-5-5',
);

foreach ($stream as $message) {
    echo $message;
}
client = Anthropic::Client.new

stream = client.messages.stream(
  model: "claude-opus-5-5",
  messages: [{ role: "user", content: "Hello" }],
  max_tokens: 256
)

stream.text.each { |text| print(text) }
event: message_start
data: {"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-5-5", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 25, "output_tokens": 1}}}

event: content_block_start
data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "!"}}

event: content_block_stop
data: {"type": "content_block_stop", "index": 0}

event: message_delta
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence":null}, "usage": {"output_tokens": 15}}

event: message_stop
data: {"type": "message_stop"}

도구 사용과 함께하는 스트리밍 요청

도구 사용은 파라미터 값의 [세밀한 스트리밍(fine-grained streaming)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming)을 지원해요. 도구별로 `eager_input_streaming`으로 활성화하세요.

이 요청은 Claude에게 도구로 날씨를 보고하라고 요청해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } } ], "tool_choice": {"type": "any"}, "messages": [ { "role": "user", "content": "What is the weather like in San Francisco?" } ], "stream": true }' ```
ant messages create --stream --format jsonl <<'YAML'
model: claude-opus-5
max_tokens: 1024
tools:
  - name: get_weather
    description: Get the current weather in a given location
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: The city and state, e.g. San Francisco, CA
      required:
        - location
tool_choice:
  type: any
messages:
  - role: user
    content: What is the weather like in San Francisco?
YAML
client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
        },
    }
]

with client.messages.stream(
    model="claude-opus-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "any"},
    messages=[
        {"role": "user", "content": "What is the weather like in San Francisco?"}
    ],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  }
];

const stream = client.messages.stream({
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: tools,
  tool_choice: { type: "any" },
  messages: [
    {
      role: "user",
      content: "What is the weather like in San Francisco?"
    }
  ]
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5,
    MaxTokens = 1024,
    Tools = [
        new ToolUnion(new Tool()
        {
            Name = "get_weather",
            Description = "Get the current weather in a given location",
            InputSchema = new InputSchema()
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }),
                },
                Required = ["location"],
            },
        }),
    ],
    ToolChoice = new ToolChoiceAny(),
    Messages = [
        new() { Role = Role.User, Content = "What is the weather like in San Francisco?" }
    ]
};

await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    Console.Write(msg);
}
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5,
	MaxTokens: 1024,
	Tools: []anthropic.ToolUnionParam{
		{OfTool: &anthropic.ToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather in a given location"),
			InputSchema: anthropic.ToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "The city and state, e.g. San Francisco, CA",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	ToolChoice: anthropic.ToolChoiceUnionParam{OfAny: &anthropic.ToolChoiceAnyParam{}},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather like in San Francisco?")),
	},
})

for stream.Next() {
	event := stream.Current()
	switch eventVariant := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch deltaVariant := eventVariant.Delta.AsAny().(type) {
		case anthropic.TextDelta:
			fmt.Print(deltaVariant.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5)
    .maxTokens(1024L)
    .addTool(Tool.builder()
        .name("get_weather")
        .description("Get the current weather in a given location")
        .inputSchema(Tool.InputSchema.builder()
            .properties(JsonValue.from(Map.of(
                "location", Map.of(
                    "type", "string",
                    "description", "The city and state, e.g. San Francisco, CA"
                )
            )))
            .putAdditionalProperty("required", JsonValue.from(List.of("location")))
            .build())
        .build())
    .toolChoice(ToolChoice.ofAny(ToolChoiceAny.builder().build()))
    .addUserMessage("What is the weather like in San Francisco?")
    .build();

try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(event -> {
        event.contentBlockDelta().ifPresent(deltaEvent ->
            deltaEvent.delta().text().ifPresent(td ->
                System.out.print(td.text())
            )
        );
    });
}
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'What is the weather like in San Francisco?']
    ],
    model: 'claude-opus-5',
    toolChoice: ['type' => 'any'],
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather in a given location',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'The city and state, e.g. San Francisco, CA'
                    ]
                ],
                'required' => ['location']
            ]
        ]
    ],
);

foreach ($stream as $message) {
    echo $message;
}
client = Anthropic::Client.new

tools = [
  {
    name: "get_weather",
    description: "Get the current weather in a given location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "The city and state, e.g. San Francisco, CA"
        }
      },
      required: ["location"]
    }
  }
]

stream = client.messages.stream(
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: tools,
  tool_choice: { type: "any" },
  messages: [
    { role: "user", content: "What is the weather like in San Francisco?" }
  ]
)

stream.text.each { |text| print(text) }
event: message_start
data: {"type":"message_start","message":{"id":"msg_014p7gG3wDgGV9EUtLvnow3U","type":"message","role":"assistant","model":"claude-opus-5","stop_sequence":null,"usage":{"input_tokens":472,"output_tokens":2},"content":[],"stop_reason":null}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" let"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" check"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" weather"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" San"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Francisco"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" CA"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":":"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01T1x1fJ34qAmk2tNTrN7Up6","name":"get_weather","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"location\":"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"San"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Francisc"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"o,"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" CA\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":1}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":89}}

event: message_stop
data: {"type":"message_stop"}

thinking과 함께하는 스트리밍 요청

이 요청은 thinking과 스트리밍을 함께 켜요. display: "summarized" 설정은 Claude의 추론 전체가 아니라 요약된 사고 흐름을 스트리밍해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 20000, "stream": true, "thinking": { "type": "adaptive", "display": "summarized" }, "messages": [ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?" } ] }' ```
ant messages create --stream --format jsonl \
  --model claude-opus-5-5 \
  --max-tokens 20000 \
  --thinking '{type: adaptive, display: summarized}' \
  --message '{role: user, content: What is the greatest common divisor of 1071 and 462?}'
client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=20000,
    thinking={"type": "adaptive", "display": "summarized"},
    messages=[
        {
            "role": "user",
            "content": "What is the greatest common divisor of 1071 and 462?",
        }
    ],
) as stream:
    for event in stream:
        if event.type == "content_block_delta":
            delta = event.delta
            match delta.type:
                case "thinking_delta":
                    print(delta.thinking, end="", flush=True)
                case "text_delta":
                    print(delta.text, end="", flush=True)
const client = new Anthropic();

const stream = client.messages.stream({
  model: "claude-opus-5-5",
  max_tokens: 20000,
  thinking: { type: "adaptive", display: "summarized" },
  messages: [
    {
      role: "user",
      content: "What is the greatest common divisor of 1071 and 462?"
    }
  ]
});

for await (const event of stream) {
  if (event.type === "content_block_delta") {
    switch (event.delta.type) {
      case "thinking_delta":
        process.stdout.write(event.delta.thinking);
        break;
      case "text_delta":
        process.stdout.write(event.delta.text);
        break;
    }
  }
}
using Anthropic;
using Anthropic.Models.Messages;

AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 20000,
    Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized },
    Messages = [new() { Role = Role.User, Content = "What is the greatest common divisor of 1071 and 462?" }]
};

await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    Console.Write(msg);
}
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 20000,
	Thinking: anthropic.ThinkingConfigParamUnion{
		OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{
			Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized,
		},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What is the greatest common divisor of 1071 and 462?")),
	},
})

for stream.Next() {
	event := stream.Current()
	switch eventVariant := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch deltaVariant := eventVariant.Delta.AsAny().(type) {
		case anthropic.ThinkingDelta:
			fmt.Print(deltaVariant.Thinking)
		case anthropic.TextDelta:
			fmt.Print(deltaVariant.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(20000L)
    .thinking(ThinkingConfigAdaptive.builder()
        .display(ThinkingConfigAdaptive.Display.SUMMARIZED)
        .build())
    .addUserMessage("What is the greatest common divisor of 1071 and 462?")
    .build();

try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(event -> {
        event.contentBlockDelta().ifPresent(deltaEvent -> {
            deltaEvent.delta().thinking().ifPresent(td ->
                IO.print(td.thinking())
            );
            deltaEvent.delta().text().ifPresent(td ->
                IO.print(td.text())
            );
        });
    });
}
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 20000,
    messages: [
        ['role' => 'user', 'content' => 'What is the greatest common divisor of 1071 and 462?']
    ],
    model: 'claude-opus-5-5',
    thinking: ['type' => 'adaptive', 'display' => 'summarized'],
);

foreach ($stream as $message) {
    echo $message;
}
client = Anthropic::Client.new

stream = client.messages.stream(
  model: "claude-opus-5-5",
  max_tokens: 20000,
  thinking: { type: "adaptive", display: "summarized" },
  messages: [
    { role: "user", content: "What is the greatest common divisor of 1071 and 462?" }
  ]
)

stream.each do |event|
  if event.is_a?(Anthropic::Models::RawContentBlockDeltaEvent)
    delta = event.delta
    case delta
    when Anthropic::Models::ThinkingDelta
      print(delta.thinking)
    when Anthropic::Models::TextDelta
      print(delta.text)
    end
  end
end
event: message_start
data: {"type": "message_start", "message": {"id": "msg_01...", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-5-5", "stop_reason": null, "stop_sequence": null}}

event: content_block_start
data: {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n462 = 3 × 147 + 21"}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n147 = 7 × 21 + 0"}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\nThe remainder is 0, so GCD(1071, 462) = 21."}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}}

event: content_block_stop
data: {"type": "content_block_stop", "index": 0}

event: content_block_start
data: {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "The greatest common divisor of 1071 and 462 is **21**."}}

event: content_block_stop
data: {"type": "content_block_stop", "index": 1}

event: message_delta
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}}

event: message_stop
data: {"type": "message_stop"}

웹 검색 도구 사용과 함께하는 스트리밍 요청

이 요청은 Claude에게 현재 날씨 정보를 웹에서 검색하라고 요청해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "stream": true, "tools": [ { "type": "web_search_20250305", "name": "web_search", "max_uses": 5 } ], "messages": [ { "role": "user", "content": "What is the weather like in New York City today?" } ] }' ```
ant messages create --stream --format jsonl \
  --model claude-opus-5-5 \
  --max-tokens 1024 \
  --tool '{type: web_search_20250305, name: web_search, max_uses: 5}' \
  --message '{role: user, content: What is the weather like in New York City today?}'
client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}],
    messages=[
        {"role": "user", "content": "What is the weather like in New York City today?"}
    ],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
const client = new Anthropic();

const stream = client.messages.stream({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }],
  messages: [{ role: "user", content: "What is the weather like in New York City today?" }]
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}
using Anthropic;
using Anthropic.Models.Messages;

AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Tools = [new ToolUnion(new WebSearchTool20250305() { MaxUses = 5 })],
    Messages = [new() { Role = Role.User, Content = "What is the weather like in New York City today?" }]
};

await foreach (var msg in client.Messages.CreateStreaming(parameters))
{
    Console.Write(msg);
}
client := anthropic.NewClient()

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Tools: []anthropic.ToolUnionParam{
		{
			OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{
				MaxUses: anthropic.Int(5),
			},
		},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather like in New York City today?")),
	},
})

for stream.Next() {
	event := stream.Current()
	switch eventVariant := event.AsAny().(type) {
	case anthropic.ContentBlockDeltaEvent:
		switch deltaVariant := eventVariant.Delta.AsAny().(type) {
		case anthropic.TextDelta:
			fmt.Print(deltaVariant.Text)
		}
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

MessageCreateParams params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024L)
    .addTool(WebSearchTool20250305.builder()
        .maxUses(5L)
        .build())
    .addUserMessage("What is the weather like in New York City today?")
    .build();

try (var streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(event -> {
        event.contentBlockDelta().ifPresent(deltaEvent ->
            deltaEvent.delta().text().ifPresent(td ->
                System.out.print(td.text())
            )
        );
    });
}
$client = new Client();

$stream = $client->messages->createStream(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'What is the weather like in New York City today?']
    ],
    model: 'claude-opus-5-5',
    tools: [
        ['type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 5]
    ],
);

foreach ($stream as $message) {
    echo $message;
}
client = Anthropic::Client.new

stream = client.messages.stream(
  model: :"claude-opus-5-5",
  max_tokens: 1024,
  tools: [
    {
      type: "web_search_20250305",
      name: "web_search",
      max_uses: 5
    }
  ],
  messages: [
    {
      role: "user",
      content: "What is the weather like in New York City today?"
    }
  ]
)

stream.text.each { |text| print(text) }
event: message_start
data: {"type":"message_start","message":{"id":"msg_01G...","type":"message","role":"assistant","model":"claude-opus-5-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2679,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":3}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll check"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the current weather in New York City for you"}}

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"."}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"server_tool_use","id":"srvtoolu_014hJH82Qum7Td6UV8gDXThB","name":"web_search","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"query"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\":\""}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"weather"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" NY"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"C to"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"day\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":1 }

event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"web_search_tool_result","tool_use_id":"srvtoolu_014hJH82Qum7Td6UV8gDXThB","content":[{"type":"web_search_result","title":"Weather in New York City in May 2025 (New York) - detailed Weather Forecast for a month","url":"https://world-weather.info/forecast/usa/new_york/may-2025/","encrypted_content":"Ev0DCioIAxgCIiQ3NmU4ZmI4OC1k...","page_age":null},...]}}

event: content_block_stop
data: {"type":"content_block_stop","index":2}

event: content_block_start
data: {"type":"content_block_start","index":3,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"Here's the current weather information for New York"}}

event: content_block_delta
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" City:\n\n# Weather"}}

event: content_block_delta
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" in New York City"}}

event: content_block_delta
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"\n\n"}}

...

event: content_block_stop
data: {"type":"content_block_stop","index":17}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10682,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":510,"server_tool_use":{"web_search_requests":1}}}

event: message_stop
data: {"type":"message_stop"}

오류 복구

Claude 4.5 이하

Claude 4.5 모델 이하에서는 네트워크 문제, 타임아웃, 기타 오류로 중단된 스트리밍 요청을 중단된 지점부터 재개해서 복구할 수 있어요. 이 방식은 전체 응답을 다시 처리하는 수고를 덜어줘요.

기본적인 복구 전략은 다음과 같아요:

  1. 부분 응답 캡처하기: 오류가 발생하기 전에 성공적으로 받은 모든 콘텐츠를 저장하세요.
  2. 이어서 보낼 요청 구성하기: 부분 어시스턴트 응답을 새 어시스턴트 메시지의 시작으로 포함하는 새 API 요청을 만드세요.
  3. 스트리밍 재개하기: 중단된 지점부터 나머지 응답을 계속 받으세요.

Claude 4.6 이상

Claude 4.6 이상 모델에서도 같은 캡처-후-재개 전략을 쓰지만 2단계가 달라져요: 부분 응답을 어시스턴트 메시지에 넣는 대신, 모델에게 중단된 지점부터 계속하라고 지시하는 사용자 메시지를 추가해요.

  1. 부분 응답 캡처하기: 오류가 발생하기 전에 성공적으로 받은 모든 콘텐츠를 저장하세요.
  2. 이어서 보낼 요청 구성하기: 부분 응답과 계속하라는 지시를 담은 사용자 메시지로 새 API 요청을 만드세요. 예를 들면:
    Your previous response was interrupted and ended with [previous_response]. Continue from where you left off.
    
  3. 스트리밍 재개하기: 중단된 지점부터 나머지 응답을 계속 받으세요.

오류 복구 모범 사례

  1. SDK 기능 사용하기: SDK에 내장된 메시지 누적과 오류 처리 기능을 활용하세요.
  2. 콘텐츠 타입 처리하기: 메시지에 여러 콘텐츠 블록(text, tool_use, thinking)이 들어갈 수 있다는 점을 알고 있어야 해요. 도구 사용과 확장 thinking 블록은 부분적으로 복구할 수 없어요. 스트리밍은 가장 최근 텍스트 블록부터 재개할 수 있습니다.

다음 단계

Handle each `stop_reason` value once a stream completes. Stream tool input JSON without server-side buffering for lower latency. Stream thinking output with `thinking_delta` and `signature_delta` events. Use the official SDKs, which handle streaming, accumulation, and reconnection for you. Process large volumes of requests asynchronously when you don't need real-time responses.

더 알아보기 (Learn more)