세밀한 도구 스트리밍
세밀한 도구 스트리밍 (Fine-grained tool streaming)
세밀한 도구 스트리밍은 서버 측 JSON 버퍼링 없이 Claude가 도구의 입력을 생성하는 대로 여러분의 클라이언트로 전달해요. 버퍼링 단계를 건너뛰면 문서나 코드 블록 같은 큰 파라미터의 첫 조각까지 걸리는 시간이 줄어들고, 조각들은 표준 도구 사용과 같은 Streaming messages 이벤트로 도착해요.
출처: 문서
본문
참고 (Note) 이 기능에 제로 데이터 보존(ZDR)이 어떻게 적용되는지는 API 및 데이터 보존을 참고하세요.
세밀한 도구 스트리밍은 서버 측 버퍼링이나 JSON 검증 없이, Claude가 도구 입력을 생성하는 대로 여러분의 클라이언트로 전달해요. 버퍼링 단계를 생략하면 문서나 코드 블록 같은 큰 파라미터의 첫 조각까지 시간이 줄어들고, 조각들은 표준 도구 사용과 같은 Streaming messages 이벤트로 도착해요.
경고 (Warning) API가 스트리밍하기 전에 도구 입력을 버퍼링하거나 검증하지 않으므로, 부분적이거나 잘못된 JSON을 받을 수 있어요. stop reason
max_tokens로 끝나는 응답은 파라미터를 중간에 잘라낼 수도 있어요. 조각을 축적하고, 파싱을 보호하고, 파싱할 수 없는 입력을 Claude에 반환하는 방법은 도구 응답의 잘못된 JSON 처리하기를 참고하세요.
세밀한 도구 스트리밍 사용 방법 (How to use fine-grained tool streaming)
모든 모델은 Claude API, Amazon Bedrock, AWS의 Claude Platform, Google Cloud, Microsoft Foundry에서 세밀한 도구 스트리밍을 지원해요. 사용하려면 세밀한 스트리밍을 원하는 사용자 정의 도구에 eager_input_streaming을 true로 설정하고 요청에서 스트리밍을 활성화하세요.
eager_input_streaming 필드는 선택 사항이에요. true로 설정하면 그 도구에 대해 세밀한 스트리밍이 켜지고, 생략하면 API가 각 파라미터 값을 스트리밍하기 전에 버퍼링·검증하는 표준 버퍼링 스트리밍이 됩니다. 예외는 여전히 레거시 fine-grained-tool-streaming-2025-05-14 베타 헤더를 보내는 요청인데, 이 경우 필드가 설정되지 않은 도구에서 세밀한 스트리밍이 켜져요. 도구별 필드가 그 헤더를 대체하며, 명시적 false는 요청이 여전히 그 헤더를 보내더라도 도구에 대해 버퍼링 스트리밍을 유지해요. 레거시 헤더는 computer use나 browser use 도구셋 항목과 결합할 수 없어요. API는 둘 다 보내는 요청을 거부하므로, 헤더를 제거하고 필요한 사용자 정의 도구에 eager_input_streaming을 설정하세요. 필드 정의는 도구 레퍼런스를 참고하세요.
다음 예시는 make_file 도구에 세밀한 스트리밍을 켜고 Claude에게 긴 시를 요청해서, 도구 입력이 충분히 커서 스트리밍되는 것을 볼 수 있게 해요:
ant messages create --stream --format jsonl <<'YAML' |
model: claude-opus-5-5
max_tokens: 65536
tools:
- name: make_file
description: Write text to a file
eager_input_streaming: true
input_schema:
type: object
properties:
filename:
type: string
description: The filename to write text to
lines_of_text:
type: array
description: An array of lines of text to write to the file
required:
- filename
- lines_of_text
messages:
- role: user
content: Can you write a long poem and make a file called poem.txt?
YAML
jq -rj 'select(.delta.type == "input_json_delta") | .delta.partial_json'
client = anthropic.Anthropic()
with client.messages.stream(
max_tokens=65536,
model="claude-opus-5-5",
tools=[
{
"name": "make_file",
"description": "Write text to a file",
"eager_input_streaming": True,
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "The filename to write text to",
},
"lines_of_text": {
"type": "array",
"description": "An array of lines of text to write to the file",
},
},
"required": ["filename", "lines_of_text"],
},
}
],
messages=[
{
"role": "user",
"content": "Can you write a long poem and make a file called poem.txt?",
}
],
) as stream:
for event in stream:
if event.type == "input_json":
print(event.partial_json, end="", flush=True)
final_message = stream.get_final_message()
print()
for block in final_message.content:
if block.type == "tool_use":
print(f"Complete tool input: {block.input}")
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 65536,
tools: [
{
name: "make_file",
description: "Write text to a file",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {
filename: {
type: "string",
description: "The filename to write text to"
},
lines_of_text: {
type: "array",
description: "An array of lines of text to write to the file"
}
},
required: ["filename", "lines_of_text"]
}
}
],
messages: [
{
role: "user",
content: "Can you write a long poem and make a file called poem.txt?"
}
]
});
stream.on("inputJson", (partialJson) => {
process.stdout.write(partialJson);
});
const message = await stream.finalMessage();
console.log();
for (const block of message.content) {
if (block.type === "tool_use") {
console.log("Complete tool input:", block.input);
}
}
AnthropicClient client = new();
MessageCreateParams parameters = new()
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 65536,
Tools =
[
new Tool
{
Name = "make_file",
Description = "Write text to a file",
EagerInputStreaming = true,
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["filename"] = JsonSerializer.SerializeToElement(
new { type = "string", description = "The filename to write text to" }
),
["lines_of_text"] = JsonSerializer.SerializeToElement(
new { type = "array", description = "An array of lines of text to write to the file" }
),
},
Required = ["filename", "lines_of_text"],
},
},
],
Messages =
[
new()
{
Role = Role.User,
Content = "Can you write a long poem and make a file called poem.txt?",
},
],
};
// The C# example assembles the input itself: content block index -> accumulated JSON
var toolInputs = new Dictionary<long, StringBuilder>();
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
if (
streamEvent.TryPickContentBlockStart(out var start)
&& start.ContentBlock.TryPickToolUse(out _)
)
{
toolInputs[start.Index] = new StringBuilder();
}
else if (
streamEvent.TryPickContentBlockDelta(out var delta)
&& delta.Delta.TryPickInputJson(out var inputJson)
)
{
Console.Write(inputJson.PartialJson);
toolInputs[delta.Index].Append(inputJson.PartialJson);
}
}
Console.WriteLine();
foreach (var accumulatedInput in toolInputs.Values)
{
Console.WriteLine($"Complete tool input: {accumulatedInput}");
}
client := anthropic.NewClient()
makeFileTool := anthropic.ToolParam{
Name: "make_file",
Description: anthropic.String("Write text to a file"),
EagerInputStreaming: anthropic.Bool(true),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"filename": map[string]any{
"type": "string",
"description": "The filename to write text to",
},
"lines_of_text": map[string]any{
"type": "array",
"description": "An array of lines of text to write to the file",
},
},
Required: []string{"filename", "lines_of_text"},
},
}
stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 65536,
Tools: []anthropic.ToolUnionParam{{OfTool: &makeFileTool}},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(
"Can you write a long poem and make a file called poem.txt?",
)),
},
})
message := anthropic.Message{}
for stream.Next() {
event := stream.Current()
if err := message.Accumulate(event); err != nil {
panic(err)
}
if delta, ok := event.AsAny().(anthropic.ContentBlockDeltaEvent); ok {
if inputJSON, ok := delta.Delta.AsAny().(anthropic.InputJSONDelta); ok {
fmt.Print(inputJSON.PartialJSON)
}
}
}
if err := stream.Err(); err != nil {
panic(err)
}
fmt.Println()
for _, block := range message.Content {
if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
fmt.Printf("Complete tool input: %s\n", toolUse.Input)
}
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool makeFileTool = Tool.builder()
.name("make_file")
.description("Write text to a file")
.eagerInputStreaming(true)
.inputSchema(Tool.InputSchema.builder()
.properties(Tool.InputSchema.Properties.builder()
.putAdditionalProperty("filename", JsonValue.from(Map.of(
"type", "string",
"description", "The filename to write text to")))
.putAdditionalProperty("lines_of_text", JsonValue.from(Map.of(
"type", "array",
"description", "An array of lines of text to write to the file")))
.build())
.addRequired("filename")
.addRequired("lines_of_text")
.build())
.build();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(65536L)
.addTool(makeFileTool)
.addUserMessage("Can you write a long poem and make a file called poem.txt?")
.build();
MessageAccumulator accumulator = MessageAccumulator.create();
try (StreamResponse<RawMessageStreamEvent> streamResponse =
client.messages().createStreaming(params)) {
streamResponse.stream().forEach(event -> {
accumulator.accumulate(event);
if (event.isContentBlockDelta()) {
var delta = event.asContentBlockDelta().delta();
if (delta.isInputJson()) {
IO.print(delta.asInputJson().partialJson());
}
}
});
}
IO.println("");
accumulator.message().content().forEach(block ->
block.toolUse().ifPresent(toolUse ->
IO.println("Complete tool input: " + toolUse._input())));
use Anthropic\Client;
use Anthropic\Messages\InputJSONDelta;
use Anthropic\Messages\Model;
use Anthropic\Messages\RawContentBlockDeltaEvent;
use Anthropic\Messages\RawContentBlockStartEvent;
use Anthropic\Messages\ToolUseBlock;
$client = new Client();
$stream = $client->messages->createStream(
maxTokens: 65536,
model: Model::CLAUDE_OPUS_5_5,
tools: [
[
'name' => 'make_file',
'description' => 'Write text to a file',
'eager_input_streaming' => true,
'input_schema' => [
'type' => 'object',
'properties' => [
'filename' => [
'type' => 'string',
'description' => 'The filename to write text to',
],
'lines_of_text' => [
'type' => 'array',
'description' => 'An array of lines of text to write to the file',
],
],
'required' => ['filename', 'lines_of_text'],
],
},
],
messages: [
[
'role' => 'user',
'content' => 'Can you write a long poem and make a file called poem.txt?',
],
],
);
// The PHP example assembles the input itself: index => accumulated JSON string
$toolInputs = [];
foreach ($stream as $event) {
switch (true) {
case $event instanceof RawContentBlockStartEvent:
if ($event->contentBlock instanceof ToolUseBlock) {
$toolInputs[$event->index] = '';
}
break;
case $event instanceof RawContentBlockDeltaEvent:
if ($event->delta instanceof InputJSONDelta) {
echo $event->delta->partialJSON;
$toolInputs[$event->index] .= $event->delta->partialJSON;
}
break;
}
}
echo "\n";
foreach ($toolInputs as $toolInput) {
echo "Complete tool input: {$toolInput}\n";
}
client = Anthropic::Client.new
stream = client.messages.stream(
model: Anthropic::Models::Model::CLAUDE_OPUS_5_5,
max_tokens: 65_536,
tools: [
{
name: "make_file",
description: "Write text to a file",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {
filename: {
type: "string",
description: "The filename to write text to"
},
lines_of_text: {
type: "array",
description: "An array of lines of text to write to the file"
}
},
required: ["filename", "lines_of_text"]
}
}
],
messages: [
{
role: "user",
content: "Can you write a long poem and make a file called poem.txt?"
}
]
)
stream.each do |event|
print event.partial_json if event.is_a?(Anthropic::Streaming::InputJsonEvent)
end
puts
stream.accumulated_message.content.each do |block|
puts "Complete tool input: #{block.input}" if block.type == :tool_use
end
모든 탭은 make_file 도구에 세밀한 스트리밍을 켜요. SDK 탭은 각 입력 조각이 도착하는 즉시 출력한 다음 스트림이 끝나면 완전히 축적된 입력을 출력해요. cURL 탭은 원시 이벤트 스트림을 보여주고, CLI 탭은 jq로 조각만 출력해요. 출력된 조각들이 완전한 도구 입력으로 합쳐지기 때문에, Claude가 시를 쓰는 대로 여러분의 터미널에 시가 채워져요:
{"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", "", "I.", "", "Beneath the vast and star-strewn sky,", "Where silver moonbeams softly lie,", ...
Complete tool input: {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", ...]}
eager_input_streaming이 없으면 API가 각 파라미터 값을 스트리밍하기 전에 버퍼링·검증하므로, 큰 파라미터에 대해서는 Claude가 생성을 마칠 때까지 아무것도 출력되지 않아요. 이 필드를 켜면 조각들이 Claude가 파라미터를 시작하는 즉시 도착하기 시작하며, 대개 더 길고 단어 중간에서 끊기는 경우가 더 적어요.
도구 입력 델타 축적하기 (Accumulating tool input deltas)
축적 계약은 표준 도구 사용 스트리밍과 같으므로, 이 섹션은 eager_input_streaming 유무와 관계없이 적용돼요. 이벤트 형식은 Streaming messages의 Input JSON delta를 참고하세요. 세밀한 도구 스트리밍은 결과에 대해 가정할 수 있는 것을 바꿔요. 서버가 검증 없이 조각을 스트리밍하므로 축적된 문자열이 유효한 JSON이 아닐 수 있어요.
tool_use 콘텐츠 블록이 스트리밍될 때 초기 content_block_start 이벤트에는 input: {}(빈 객체)가 포함돼요. 이것은 자리 표시자예요. 실제 입력은 각각 partial_json 문자열 조각을 담은 일련의 input_json_delta 이벤트로 도착해요. 완전한 입력을 조립하려면 이 조각들을 연결하고 블록이 닫힐 때 결과를 파싱하세요.
SDK가 축적 헬퍼를 제공한다면(이전 예시의 Python, TypeScript, Go, Java, Ruby 탭처럼) 자동으로 처리해줘요. 수동 패턴은 헬퍼가 없는 SDK나 입력이 어떻게 조립되는지 완전히 제어하고 싶을 때 사용해요.
축적 계약:
type: "tool_use"인content_block_start에서 빈 문자열을 초기화해요:input_json = ""- 각
type: "input_json_delta"인content_block_delta에서 추가해요:input_json += event.delta.partial_json content_block_stop에서 축적된 문자열을 파싱해요
다음 SDK 예시처럼 파싱을 보호하세요. 응답이 파라미터 중간에서 max_tokens로 멈출 수도 있어요. stop reason을 확인하고 더 높은 max_tokens로 재시도할지, 부분 입력을 복구할지 결정하세요.
초기 input: {}(객체)와 partial_json(문자열) 사이의 타입 불일치는 의도된 것이에요. 빈 객체는 콘텐츠 배열의 슬롯을 표시하고, 델타 문자열이 실제 값을 만듭니다.
# Accumulating per-block input deltas needs a programming language; the first
# example's CLI tab shows the raw fragments with jq. See the SDK tabs.
client = anthropic.Anthropic()
tool_inputs: dict[int, str] = {} # index -> accumulated JSON string
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=1024,
tools=[
{
"name": "get_weather",
"description": "Get current weather for a city",
"eager_input_streaming": True,
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
],
messages=[{"role": "user", "content": "Weather in Paris?"}],
) as stream:
for event in stream:
match event.type:
case "content_block_start" if event.content_block.type == "tool_use":
tool_inputs[event.index] = ""
case "content_block_delta" if event.delta.type == "input_json_delta":
tool_inputs[event.index] += event.delta.partial_json
case "content_block_stop" if event.index in tool_inputs:
raw_input = tool_inputs[event.index]
try:
parsed = json.loads(raw_input)
except json.JSONDecodeError:
# The accumulated string is not guaranteed to be valid JSON.
# See "Handling invalid JSON in tool responses" on this page.
print(f"Invalid tool input: {raw_input}")
else:
print(f"Tool input: {parsed}")
const client = new Anthropic();
const toolInputs = new Map<number, string>();
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 1024,
tools: [
{
name: "get_weather",
description: "Get current weather for a city",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}
],
messages: [{ role: "user", content: "Weather in Paris?" }]
});
for await (const event of stream) {
switch (event.type) {
case "content_block_start":
if (event.content_block.type === "tool_use") {
toolInputs.set(event.index, "");
}
break;
case "content_block_delta":
if (event.delta.type === "input_json_delta") {
toolInputs.set(
event.index,
(toolInputs.get(event.index) ?? "") + event.delta.partial_json
);
}
break;
case "content_block_stop":
if (toolInputs.has(event.index)) {
const rawInput = toolInputs.get(event.index)!;
try {
console.log("Tool input:", JSON.parse(rawInput));
} catch {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
console.log("Invalid tool input:", rawInput);
}
}
break;
}
}
AnthropicClient client = new();
MessageCreateParams parameters = new()
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools =
[
new Tool
{
Name = "get_weather",
Description = "Get current weather for a city",
EagerInputStreaming = true,
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["city"] = JsonSerializer.SerializeToElement(new { type = "string" }),
},
Required = ["city"],
},
},
],
Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }],
};
// Block index -> accumulated JSON fragments
// This example accumulates the deltas manually to show the raw stream;
// the SDK's MessageContentAggregator can also accumulate tool input automatically.
var toolInputs = new Dictionary<long, StringBuilder>();
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
if (
streamEvent.TryPickContentBlockStart(out var start)
&& start.ContentBlock.TryPickToolUse(out _)
)
{
toolInputs[start.Index] = new StringBuilder();
}
else if (
streamEvent.TryPickContentBlockDelta(out var delta)
&& delta.Delta.TryPickInputJson(out var inputJson)
)
{
toolInputs[delta.Index].Append(inputJson.PartialJson);
}
else if (
streamEvent.TryPickContentBlockStop(out var stop)
&& toolInputs.TryGetValue(stop.Index, out var accumulated)
)
{
try
{
using var parsed = JsonDocument.Parse(accumulated.ToString());
Console.WriteLine($"Tool input: {parsed.RootElement}");
}
catch (JsonException)
{
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
Console.WriteLine($"Invalid tool input: {accumulated}");
}
}
}
client := anthropic.NewClient()
toolInputs := map[int64]string{} // content block index -> accumulated JSON
stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: []anthropic.ToolUnionParam{{
OfTool: &anthropic.ToolParam{
Name: "get_weather",
Description: anthropic.String("Get current weather for a city"),
EagerInputStreaming: anthropic.Bool(true),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"city": map[string]any{"type": "string"},
},
Required: []string{"city"},
},
},
}},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Weather in Paris?")),
},
})
for stream.Next() {
switch event := stream.Current().AsAny().(type) {
case anthropic.ContentBlockStartEvent:
if _, ok := event.ContentBlock.AsAny().(anthropic.ToolUseBlock); ok {
toolInputs[event.Index] = ""
}
case anthropic.ContentBlockDeltaEvent:
if delta, ok := event.Delta.AsAny().(anthropic.InputJSONDelta); ok {
toolInputs[event.Index] += delta.PartialJSON
}
case anthropic.ContentBlockStopEvent:
if accumulated, ok := toolInputs[event.Index]; ok {
var parsed map[string]any
if err := json.Unmarshal([]byte(accumulated), &parsed); err != nil {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
fmt.Println("Invalid tool input:", accumulated)
} else {
fmt.Println("Tool input:", parsed)
}
}
}
}
if err := stream.Err(); err != nil {
panic(err)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();
Tool weatherTool = Tool.builder()
.name("get_weather")
.description("Get current weather for a city")
.eagerInputStreaming(true)
.inputSchema(Tool.InputSchema.builder()
.properties(Tool.InputSchema.Properties.builder()
.putAdditionalProperty("city", JsonValue.from(Map.of("type", "string")))
.build())
.addRequired("city")
.build())
.build();
MessageCreateParams createParams = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024)
.addTool(weatherTool)
.addUserMessage("Weather in Paris?")
.build();
// Content block index -> accumulated tool input JSON
Map<Long, StringBuilder> toolInputs = new HashMap<>();
try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(createParams)) {
var eventIterator = streamResponse.stream().iterator();
while (eventIterator.hasNext()) {
RawMessageStreamEvent event = eventIterator.next();
switch (event.type().value()) {
case CONTENT_BLOCK_START -> {
var blockStart = event.asContentBlockStart();
if (blockStart.contentBlock().isToolUse()) {
toolInputs.put(blockStart.index(), new StringBuilder());
}
}
case CONTENT_BLOCK_DELTA -> {
var blockDelta = event.asContentBlockDelta();
if (blockDelta.delta().isInputJson() && toolInputs.containsKey(blockDelta.index())) {
toolInputs.get(blockDelta.index()).append(blockDelta.delta().asInputJson().partialJson());
}
}
case CONTENT_BLOCK_STOP -> {
var blockStop = event.asContentBlockStop();
if (toolInputs.containsKey(blockStop.index())) {
String accumulated = toolInputs.get(blockStop.index()).toString();
try {
IO.println("Tool input: " + objectMapper.readTree(accumulated));
} catch (JsonProcessingException e) {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
IO.println("Invalid tool input: " + accumulated);
}
}
}
}
}
}
use Anthropic\Client;
use Anthropic\Messages\InputJSONDelta;
use Anthropic\Messages\Model;
use Anthropic\Messages\RawContentBlockDeltaEvent;
use Anthropic\Messages\RawContentBlockStartEvent;
use Anthropic\Messages\RawContentBlockStopEvent;
use Anthropic\Messages\ToolUseBlock;
$client = new Client();
// The PHP SDK does not provide a stream accumulator for tool input;
// the manual pattern shown here is the supported approach.
$toolInputs = []; // index => accumulated JSON string
$stream = $client->messages->createStream(
maxTokens: 1024,
model: Model::CLAUDE_OPUS_5_5,
tools: [
[
'name' => 'get_weather',
'description' => 'Get current weather for a city',
'eager_input_streaming' => true,
'input_schema' => [
'type' => 'object',
'properties' => ['city' => ['type' => 'string']],
'required' => ['city'],
],
],
],
messages: [['role' => 'user', 'content' => 'Weather in Paris?']],
);
foreach ($stream as $event) {
switch (true) {
case $event instanceof RawContentBlockStartEvent:
if ($event->contentBlock instanceof ToolUseBlock) {
$toolInputs[$event->index] = '';
}
break;
case $event instanceof RawContentBlockDeltaEvent:
if ($event->delta instanceof InputJSONDelta) {
$toolInputs[$event->index] .= $event->delta->partialJSON;
}
break;
case $event instanceof RawContentBlockStopEvent:
if (isset($toolInputs[$event->index])) {
$accumulated = $toolInputs[$event->index];
try {
$parsed = json_decode($accumulated, associative: true, flags: JSON_THROW_ON_ERROR);
echo "Tool input: " . json_encode($parsed) . "\n";
} catch (JsonException $e) {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
echo "Invalid tool input: {$accumulated}\n";
}
}
break;
}
}
client = Anthropic::Client.new
tool_inputs = {} # index -> accumulated JSON string
stream = client.messages.stream_raw(
model: Anthropic::Models::Model::CLAUDE_OPUS_5_5,
max_tokens: 1024,
tools: [
{
name: "get_weather",
description: "Get current weather for a city",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {city: {type: "string"}},
required: ["city"]
}
}
],
messages: [{role: "user", content: "Weather in Paris?"}]
)
stream.each do |event|
case event
when Anthropic::Models::RawContentBlockStartEvent
tool_inputs[event.index] = +"" if event.content_block.type == :tool_use
when Anthropic::Models::RawContentBlockDeltaEvent
if event.delta.is_a?(Anthropic::Models::InputJSONDelta)
tool_inputs[event.index] << event.delta.partial_json
end
when Anthropic::Models::RawContentBlockStopEvent
if tool_inputs.key?(event.index)
accumulated = tool_inputs[event.index]
begin
parsed = JSON.parse(accumulated)
puts "Tool input: #{parsed}"
rescue JSON::ParserError
# The accumulated string is not guaranteed to be valid JSON.
# See "Handling invalid JSON in tool responses" on this page.
puts "Invalid tool input: #{accumulated}"
end
end
end
end
팁 (Tip) 조각에 반응하는 것과 조립하는 것은 별개의 관심사예요. 첫 번째 예시는 조각이 도착하는 대로 반응하면서도 축적 헬퍼를 쓰는 탭에서는 조립을 SDK에 맡겨요. 축적 헬퍼를 쓰지 않거나 조립을 완전히 제어하고 싶을 때 수동 패턴을 사용하세요.
도구 응답의 잘못된 JSON 처리하기 (Handling invalid JSON in tool responses)
세밀한 도구 스트리밍에서는 도구 호출의 축적된 입력이 잘못되었거나 불완전한 JSON일 수 있어요. 그렇다면 도구를 실행할 수 없으므로, 대신 실패를 Claude에 보고하세요. 도구 결과의 content가 JSON일 필요는 없지만, 원시 문자열을 단일 키 아래 JSON 객체로 감싸면 잘못된 JSON을 받았다는 것을 Claude에게 명확히 하고 디버깅을 위해 원본 입력을 보존해줘요:
{
"INVALID_JSON": "<the unparseable input you received>"
}
래퍼를 문자열로 직렬화해서, is_error를 true로 설정한 tool result 콘텐츠 블록의 content로 반환하세요:
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"is_error": true,
"content": "{\"INVALID_JSON\": \"<the unparseable input you received>\"}"
}
참고 (Note) 문자열을 연결하는 대신 JSON 라이브러리로 래퍼를 만들면 잘못된 입력의 따옴표나 특수 문자가 올바르게 이스케이프돼요.
더 알아보기 (Learn more)
- 컨텍스트 창 (Context windows) — 컨텍스트 창이 어떻게 작동하는지, 확장 사고와 도구 사용이 어떻게 계산되는지, 대화가 커질 때 컨텍스트를 관리하는 방법
- Streaming messages — 서버 전송 이벤트로 텍스트, 도구 사용, 확장 사고 델타를 포함한 Messages API 응답을 점진적으로 스트리밍하기
- 도구 호출 처리하기 (Handle tool calls) — tool_use 블록 파싱, tool_result 응답 형식화, is_error로 오류 처리
- 도구 레퍼런스 (Tool reference) — Anthropic 제공 도구 디렉터리와 선택적 도구 정의 속성 레퍼런스