Repository Public
1---2title: Fine-grained tool streaming3url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming4description: Stream tool inputs without server-side JSON buffering for latency-sensitive applications.5---67<Note>8 To learn how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention).9</Note>1011Fine-grained tool streaming delivers a tool's input to your client as Claude generates it, without server-side buffering or JSON validation. Skipping the buffering step reduces the time to the first fragment of a large parameter, such as a document or a block of code, and the fragments arrive through the same [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming) events as standard tool use.1213<Warning>14 Because the API does not buffer or validate a tool's input before streaming it, you might receive partial or invalid JSON. A response that ends with the [stop reason](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) `max_tokens` can also cut a parameter off midway. Accumulate the fragments, guard the parse, and see [Handling invalid JSON in tool responses](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming#handling-invalid-json-in-tool-responses) for how to return unparseable input to Claude.15</Warning>1617## How to use fine-grained tool streaming1819All models support fine-grained tool streaming on the Claude API, [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). To use it, set `eager_input_streaming` to `true` on any user-defined tool where you want fine-grained streaming enabled, and enable streaming on your request.2021The `eager_input_streaming` field is optional. Setting it to `true` turns on fine-grained streaming for that tool, and omitting it gives you standard buffered streaming, in which the API buffers and validates each parameter value before streaming it back. The exception is a request that still sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header, which turns fine-grained streaming on for tools that leave the field unset. The per-tool field replaces that header, and an explicit `false` keeps buffered streaming for a tool even when a request still sends it. The legacy header cannot be combined with a [computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) or [browser use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/browser-use-tool) toolset entry: the API rejects a request that sends both, so remove the header and set `eager_input_streaming` on the user-defined tools that need it. See [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference) for the field definition.2223The following example turns on fine-grained streaming for a `make_file` tool and asks Claude for a long poem, so the tool input is large enough to watch it stream in:2425<CodeGroup>26 ```bash cURL27 curl https://api.anthropic.com/v1/messages \28 -H "content-type: application/json" \29 -H "x-api-key: $ANTHROPIC_API_KEY" \30 -H "anthropic-version: 2023-06-01" \31 -d '{32 "model": "claude-opus-5",33 "max_tokens": 65536,34 "tools": [35 {36 "name": "make_file",37 "description": "Write text to a file",38 "eager_input_streaming": true,39 "input_schema": {40 "type": "object",41 "properties": {42 "filename": {43 "type": "string",44 "description": "The filename to write text to"45 },46 "lines_of_text": {47 "type": "array",48 "description": "An array of lines of text to write to the file"49 }50 },51 "required": ["filename", "lines_of_text"]52 }53 }54 ],55 "messages": [56 {57 "role": "user",58 "content": "Can you write a long poem and make a file called poem.txt?"59 }60 ],61 "stream": true62 }'63 ```6465 ```bash CLI66 ant messages create --stream --format jsonl <<'YAML' |67 model: claude-opus-568 max_tokens: 6553669 tools:70 - name: make_file71 description: Write text to a file72 eager_input_streaming: true73 input_schema:74 type: object75 properties:76 filename:77 type: string78 description: The filename to write text to79 lines_of_text:80 type: array81 description: An array of lines of text to write to the file82 required:83 - filename84 - lines_of_text85 messages:86 - role: user87 content: Can you write a long poem and make a file called poem.txt?88 YAML89 jq -rj 'select(.delta.type == "input_json_delta") | .delta.partial_json'90 ```9192 ```python Python93 client = anthropic.Anthropic()9495 with client.messages.stream(96 max_tokens=65536,97 model="claude-opus-5",98 tools=[99 {100 "name": "make_file",101 "description": "Write text to a file",102 "eager_input_streaming": True,103 "input_schema": {104 "type": "object",105 "properties": {106 "filename": {107 "type": "string",108 "description": "The filename to write text to",109 },110 "lines_of_text": {111 "type": "array",112 "description": "An array of lines of text to write to the file",113 },114 },115 "required": ["filename", "lines_of_text"],116 },117 }118 ],119 messages=[120 {121 "role": "user",122 "content": "Can you write a long poem and make a file called poem.txt?",123 }124 ],125 ) as stream:126 for event in stream:127 if event.type == "input_json":128 print(event.partial_json, end="", flush=True)129 final_message = stream.get_final_message()130131 print()132 for block in final_message.content:133 if block.type == "tool_use":134 print(f"Complete tool input: {block.input}")135 ```136137 ```typescript TypeScript138 const client = new Anthropic();139140 const stream = client.messages.stream({141 model: "claude-opus-5",142 max_tokens: 65536,143 tools: [144 {145 name: "make_file",146 description: "Write text to a file",147 eager_input_streaming: true,148 input_schema: {149 type: "object",150 properties: {151 filename: {152 type: "string",153 description: "The filename to write text to"154 },155 lines_of_text: {156 type: "array",157 description: "An array of lines of text to write to the file"158 }159 },160 required: ["filename", "lines_of_text"]161 }162 }163 ],164 messages: [165 {166 role: "user",167 content: "Can you write a long poem and make a file called poem.txt?"168 }169 ]170 });171172 stream.on("inputJson", (partialJson) => {173 process.stdout.write(partialJson);174 });175176 const message = await stream.finalMessage();177 console.log();178 for (const block of message.content) {179 if (block.type === "tool_use") {180 console.log("Complete tool input:", block.input);181 }182 }183 ```184185 ```csharp C#186 AnthropicClient client = new();187188 MessageCreateParams parameters = new()189 {190 Model = Model.ClaudeOpus5,191 MaxTokens = 65536,192 Tools =193 [194 new Tool195 {196 Name = "make_file",197 Description = "Write text to a file",198 EagerInputStreaming = true,199 InputSchema = new InputSchema200 {201 Properties = new Dictionary<string, JsonElement>202 {203 ["filename"] = JsonSerializer.SerializeToElement(204 new { type = "string", description = "The filename to write text to" }205 ),206 ["lines_of_text"] = JsonSerializer.SerializeToElement(207 new { type = "array", description = "An array of lines of text to write to the file" }208 ),209 },210 Required = ["filename", "lines_of_text"],211 },212 },213 ],214 Messages =215 [216 new()217 {218 Role = Role.User,219 Content = "Can you write a long poem and make a file called poem.txt?",220 },221 ],222 };223224 // The C# example assembles the input itself: content block index -> accumulated JSON225 var toolInputs = new Dictionary<long, StringBuilder>();226227 await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))228 {229 if (230 streamEvent.TryPickContentBlockStart(out var start)231 && start.ContentBlock.TryPickToolUse(out _)232 )233 {234 toolInputs[start.Index] = new StringBuilder();235 }236 else if (237 streamEvent.TryPickContentBlockDelta(out var delta)238 && delta.Delta.TryPickInputJson(out var inputJson)239 )240 {241 Console.Write(inputJson.PartialJson);242 toolInputs[delta.Index].Append(inputJson.PartialJson);243 }244 }245246 Console.WriteLine();247 foreach (var accumulatedInput in toolInputs.Values)248 {249 Console.WriteLine($"Complete tool input: {accumulatedInput}");250 }251 ```252253 ```go Go254 client := anthropic.NewClient()255256 makeFileTool := anthropic.ToolParam{257 Name: "make_file",258 Description: anthropic.String("Write text to a file"),259 EagerInputStreaming: anthropic.Bool(true),260 InputSchema: anthropic.ToolInputSchemaParam{261 Properties: map[string]any{262 "filename": map[string]any{263 "type": "string",264 "description": "The filename to write text to",265 },266 "lines_of_text": map[string]any{267 "type": "array",268 "description": "An array of lines of text to write to the file",269 },270 },271 Required: []string{"filename", "lines_of_text"},272 },273 }274275 stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{276 Model: anthropic.ModelClaudeOpus5,277 MaxTokens: 65536,278 Tools: []anthropic.ToolUnionParam{{OfTool: &makeFileTool}},279 Messages: []anthropic.MessageParam{280 anthropic.NewUserMessage(anthropic.NewTextBlock(281 "Can you write a long poem and make a file called poem.txt?",282 )),283 },284 })285286 message := anthropic.Message{}287 for stream.Next() {288 event := stream.Current()289 if err := message.Accumulate(event); err != nil {290 panic(err)291 }292 if delta, ok := event.AsAny().(anthropic.ContentBlockDeltaEvent); ok {293 if inputJSON, ok := delta.Delta.AsAny().(anthropic.InputJSONDelta); ok {294 fmt.Print(inputJSON.PartialJSON)295 }296 }297 }298 if err := stream.Err(); err != nil {299 panic(err)300 }301302 fmt.Println()303 for _, block := range message.Content {304 if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {305 fmt.Printf("Complete tool input: %s\n", toolUse.Input)306 }307 }308 ```309310 ```java Java311 AnthropicClient client = AnthropicOkHttpClient.fromEnv();312313 Tool makeFileTool = Tool.builder()314 .name("make_file")315 .description("Write text to a file")316 .eagerInputStreaming(true)317 .inputSchema(Tool.InputSchema.builder()318 .properties(Tool.InputSchema.Properties.builder()319 .putAdditionalProperty("filename", JsonValue.from(Map.of(320 "type", "string",321 "description", "The filename to write text to")))322 .putAdditionalProperty("lines_of_text", JsonValue.from(Map.of(323 "type", "array",324 "description", "An array of lines of text to write to the file")))325 .build())326 .addRequired("filename")327 .addRequired("lines_of_text")328 .build())329 .build();330331 MessageCreateParams params = MessageCreateParams.builder()332 .model(Model.CLAUDE_OPUS_5)333 .maxTokens(65536L)334 .addTool(makeFileTool)335 .addUserMessage("Can you write a long poem and make a file called poem.txt?")336 .build();337338 MessageAccumulator accumulator = MessageAccumulator.create();339340 try (StreamResponse<RawMessageStreamEvent> streamResponse =341 client.messages().createStreaming(params)) {342 streamResponse.stream().forEach(event -> {343 accumulator.accumulate(event);344 if (event.isContentBlockDelta()) {345 var delta = event.asContentBlockDelta().delta();346 if (delta.isInputJson()) {347 IO.print(delta.asInputJson().partialJson());348 }349 }350 });351 }352353 IO.println("");354 accumulator.message().content().forEach(block ->355 block.toolUse().ifPresent(toolUse ->356 IO.println("Complete tool input: " + toolUse._input())));357 ```358359 ```php PHP360 use Anthropic\Client;361 use Anthropic\Messages\InputJSONDelta;362 use Anthropic\Messages\Model;363 use Anthropic\Messages\RawContentBlockDeltaEvent;364 use Anthropic\Messages\RawContentBlockStartEvent;365 use Anthropic\Messages\ToolUseBlock;366367 $client = new Client();368369 $stream = $client->messages->createStream(370 maxTokens: 65536,371 model: Model::CLAUDE_OPUS_5,372 tools: [373 [374 'name' => 'make_file',375 'description' => 'Write text to a file',376 'eager_input_streaming' => true,377 'input_schema' => [378 'type' => 'object',379 'properties' => [380 'filename' => [381 'type' => 'string',382 'description' => 'The filename to write text to',383 ],384 'lines_of_text' => [385 'type' => 'array',386 'description' => 'An array of lines of text to write to the file',387 ],388 ],389 'required' => ['filename', 'lines_of_text'],390 ],391 ],392 ],393 messages: [394 [395 'role' => 'user',396 'content' => 'Can you write a long poem and make a file called poem.txt?',397 ],398 ],399 );400401 // The PHP example assembles the input itself: index => accumulated JSON string402 $toolInputs = [];403404 foreach ($stream as $event) {405 switch (true) {406 case $event instanceof RawContentBlockStartEvent:407 if ($event->contentBlock instanceof ToolUseBlock) {408 $toolInputs[$event->index] = '';409 }410 break;411 case $event instanceof RawContentBlockDeltaEvent:412 if ($event->delta instanceof InputJSONDelta) {413 echo $event->delta->partialJSON;414 $toolInputs[$event->index] .= $event->delta->partialJSON;415 }416 break;417 }418 }419420 echo "\n";421 foreach ($toolInputs as $toolInput) {422 echo "Complete tool input: {$toolInput}\n";423 }424 ```425426 ```ruby Ruby427 client = Anthropic::Client.new428429 stream = client.messages.stream(430 model: Anthropic::Models::Model::CLAUDE_OPUS_5,431 max_tokens: 65_536,432 tools: [433 {434 name: "make_file",435 description: "Write text to a file",436 eager_input_streaming: true,437 input_schema: {438 type: "object",439 properties: {440 filename: {441 type: "string",442 description: "The filename to write text to"443 },444 lines_of_text: {445 type: "array",446 description: "An array of lines of text to write to the file"447 }448 },449 required: ["filename", "lines_of_text"]450 }451 }452 ],453 messages: [454 {455 role: "user",456 content: "Can you write a long poem and make a file called poem.txt?"457 }458 ]459 )460461 stream.each do |event|462 print event.partial_json if event.is_a?(Anthropic::Streaming::InputJsonEvent)463 end464465 puts466 stream.accumulated_message.content.each do |block|467 puts "Complete tool input: #{block.input}" if block.type == :tool_use468 end469 ```470</CodeGroup>471472Every tab turns on fine-grained streaming for the `make_file` tool. The SDK tabs print each input fragment the moment it arrives, then print the complete accumulated input once the stream ends. The cURL tab shows the raw event stream, and the CLI tab uses `jq` to print just the fragments. Because the printed fragments join into the full tool input, the poem fills your terminal as Claude writes it:473474```text wrap475{"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", "", "I.", "", "Beneath the vast and star-strewn sky,", "Where silver moonbeams softly lie,", ...476Complete tool input: {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", ...]}477```478479Without `eager_input_streaming`, the API buffers and validates each parameter value before streaming it back, so nothing prints for a large parameter until Claude has finished generating it. With it, fragments start arriving as soon as Claude begins the parameter, and they are typically longer, with fewer mid-word breaks.480481## Accumulating tool input deltas482483The accumulation contract is the same as for standard tool-use streaming, so this section applies with and without `eager_input_streaming`. See [Input JSON delta](https://platform.claude.com/docs/en/build-with-claude/streaming#input-json-delta) in Streaming messages for the event format. Fine-grained tool streaming changes what you can assume about the result: the server streams fragments without validating them, so the accumulated string might not be valid JSON.484485When a `tool_use` content block streams, the initial `content_block_start` event contains `input: {}` (an empty object). This is a placeholder. The actual input arrives as a series of `input_json_delta` events, each carrying a `partial_json` string fragment. To assemble the full input, concatenate these fragments and parse the result when the block closes.486487Where your SDK provides an accumulator helper (as the Python, TypeScript, Go, Java, and Ruby tabs in the previous example do), it handles this for you. The manual pattern is for SDKs without a helper, or when you want full control over how the input is assembled.488489The accumulation contract:4904911. On `content_block_start` with `type: "tool_use"`, initialize an empty string: `input_json = ""`4922. For each `content_block_delta` with `type: "input_json_delta"`, append: `input_json += event.delta.partial_json`4933. On `content_block_stop`, parse the accumulated string494495Guard the parse, as the following SDK examples do. A response can also stop at `max_tokens` midway through a parameter. Check the [stop reason](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) and decide whether to retry the request with a higher `max_tokens` or repair the partial input.496497The type mismatch between the initial `input: {}` (object) and `partial_json` (string) is by design. The empty object marks the slot in the content array. The delta strings build the real value.498499<CodeGroup>500 ```bash cURL501 # Accumulating per-block input deltas needs a programming language; the first502 # example's CLI tab shows the raw fragments with jq. See the SDK tabs.503 ```504505 ```bash CLI506 # Accumulating per-block input deltas needs a programming language; the first507 # example's CLI tab shows the raw fragments with jq. See the SDK tabs.508 ```509510 ```python Python511 client = anthropic.Anthropic()512513 tool_inputs: dict[int, str] = {} # index -> accumulated JSON string514515 with client.messages.stream(516 model="claude-opus-5",517 max_tokens=1024,518 tools=[519 {520 "name": "get_weather",521 "description": "Get current weather for a city",522 "eager_input_streaming": True,523 "input_schema": {524 "type": "object",525 "properties": {"city": {"type": "string"}},526 "required": ["city"],527 },528 }529 ],530 messages=[{"role": "user", "content": "Weather in Paris?"}],531 ) as stream:532 for event in stream:533 match event.type:534 case "content_block_start" if event.content_block.type == "tool_use":535 tool_inputs[event.index] = ""536 case "content_block_delta" if event.delta.type == "input_json_delta":537 tool_inputs[event.index] += event.delta.partial_json538 case "content_block_stop" if event.index in tool_inputs:539 raw_input = tool_inputs[event.index]540 try:541 parsed = json.loads(raw_input)542 except json.JSONDecodeError:543 # The accumulated string is not guaranteed to be valid JSON.544 # See "Handling invalid JSON in tool responses" on this page.545 print(f"Invalid tool input: {raw_input}")546 else:547 print(f"Tool input: {parsed}")548 ```549550 ```typescript TypeScript551 const client = new Anthropic();552553 const toolInputs = new Map<number, string>();554555 const stream = client.messages.stream({556 model: "claude-opus-5",557 max_tokens: 1024,558 tools: [559 {560 name: "get_weather",561 description: "Get current weather for a city",562 eager_input_streaming: true,563 input_schema: {564 type: "object",565 properties: { city: { type: "string" } },566 required: ["city"]567 }568 }569 ],570 messages: [{ role: "user", content: "Weather in Paris?" }]571 });572573 for await (const event of stream) {574 switch (event.type) {575 case "content_block_start":576 if (event.content_block.type === "tool_use") {577 toolInputs.set(event.index, "");578 }579 break;580 case "content_block_delta":581 if (event.delta.type === "input_json_delta") {582 toolInputs.set(583 event.index,584 (toolInputs.get(event.index) ?? "") + event.delta.partial_json585 );586 }587 break;588 case "content_block_stop":589 if (toolInputs.has(event.index)) {590 const rawInput = toolInputs.get(event.index)!;591 try {592 console.log("Tool input:", JSON.parse(rawInput));593 } catch {594 // The accumulated string is not guaranteed to be valid JSON.595 // See "Handling invalid JSON in tool responses" on this page.596 console.log("Invalid tool input:", rawInput);597 }598 }599 break;600 }601 }602 ```603604 ```csharp C#605 AnthropicClient client = new();606607 MessageCreateParams parameters = new()608 {609 Model = Model.ClaudeOpus5,610 MaxTokens = 1024,611 Tools =612 [613 new Tool614 {615 Name = "get_weather",616 Description = "Get current weather for a city",617 EagerInputStreaming = true,618 InputSchema = new InputSchema619 {620 Properties = new Dictionary<string, JsonElement>621 {622 ["city"] = JsonSerializer.SerializeToElement(new { type = "string" }),623 },624 Required = ["city"],625 },626 },627 ],628 Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }],629 };630631 // Block index -> accumulated JSON fragments632 // This example accumulates the deltas manually to show the raw stream;633 // the SDK's MessageContentAggregator can also accumulate tool input automatically.634 var toolInputs = new Dictionary<long, StringBuilder>();635636 await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))637 {638 if (639 streamEvent.TryPickContentBlockStart(out var start)640 && start.ContentBlock.TryPickToolUse(out _)641 )642 {643 toolInputs[start.Index] = new StringBuilder();644 }645 else if (646 streamEvent.TryPickContentBlockDelta(out var delta)647 && delta.Delta.TryPickInputJson(out var inputJson)648 )649 {650 toolInputs[delta.Index].Append(inputJson.PartialJson);651 }652 else if (653 streamEvent.TryPickContentBlockStop(out var stop)654 && toolInputs.TryGetValue(stop.Index, out var accumulated)655 )656 {657 try658 {659 using var parsed = JsonDocument.Parse(accumulated.ToString());660 Console.WriteLine($"Tool input: {parsed.RootElement}");661 }662 catch (JsonException)663 {664 // The accumulated string is not guaranteed to be valid JSON.665 // See "Handling invalid JSON in tool responses" on this page.666 Console.WriteLine($"Invalid tool input: {accumulated}");667 }668 }669 }670 ```671672 ```go Go673 client := anthropic.NewClient()674675 toolInputs := map[int64]string{} // content block index -> accumulated JSON676677 stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{678 Model: anthropic.ModelClaudeOpus5,679 MaxTokens: 1024,680 Tools: []anthropic.ToolUnionParam{{681 OfTool: &anthropic.ToolParam{682 Name: "get_weather",683 Description: anthropic.String("Get current weather for a city"),684 EagerInputStreaming: anthropic.Bool(true),685 InputSchema: anthropic.ToolInputSchemaParam{686 Properties: map[string]any{687 "city": map[string]any{"type": "string"},688 },689 Required: []string{"city"},690 },691 },692 }},693 Messages: []anthropic.MessageParam{694 anthropic.NewUserMessage(anthropic.NewTextBlock("Weather in Paris?")),695 },696 })697698 for stream.Next() {699 switch event := stream.Current().AsAny().(type) {700 case anthropic.ContentBlockStartEvent:701 if _, ok := event.ContentBlock.AsAny().(anthropic.ToolUseBlock); ok {702 toolInputs[event.Index] = ""703 }704 case anthropic.ContentBlockDeltaEvent:705 if delta, ok := event.Delta.AsAny().(anthropic.InputJSONDelta); ok {706 toolInputs[event.Index] += delta.PartialJSON707 }708 case anthropic.ContentBlockStopEvent:709 if accumulated, ok := toolInputs[event.Index]; ok {710 var parsed map[string]any711 if err := json.Unmarshal([]byte(accumulated), &parsed); err != nil {712 // The accumulated string is not guaranteed to be valid JSON.713 // See "Handling invalid JSON in tool responses" on this page.714 fmt.Println("Invalid tool input:", accumulated)715 } else {716 fmt.Println("Tool input:", parsed)717 }718 }719 }720 }721 if err := stream.Err(); err != nil {722 panic(err)723 }724 ```725726 ```java Java727 AnthropicClient client = AnthropicOkHttpClient.fromEnv();728 ObjectMapper objectMapper = new ObjectMapper();729730 Tool weatherTool = Tool.builder()731 .name("get_weather")732 .description("Get current weather for a city")733 .eagerInputStreaming(true)734 .inputSchema(Tool.InputSchema.builder()735 .properties(Tool.InputSchema.Properties.builder()736 .putAdditionalProperty("city", JsonValue.from(Map.of("type", "string")))737 .build())738 .addRequired("city")739 .build())740 .build();741742 MessageCreateParams createParams = MessageCreateParams.builder()743 .model(Model.CLAUDE_OPUS_5)744 .maxTokens(1024)745 .addTool(weatherTool)746 .addUserMessage("Weather in Paris?")747 .build();748749 // Content block index -> accumulated tool input JSON750 Map<Long, StringBuilder> toolInputs = new HashMap<>();751752 try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(createParams)) {753 var eventIterator = streamResponse.stream().iterator();754 while (eventIterator.hasNext()) {755 RawMessageStreamEvent event = eventIterator.next();756 switch (event.type().value()) {757 case CONTENT_BLOCK_START -> {758 var blockStart = event.asContentBlockStart();759 if (blockStart.contentBlock().isToolUse()) {760 toolInputs.put(blockStart.index(), new StringBuilder());761 }762 }763 case CONTENT_BLOCK_DELTA -> {764 var blockDelta = event.asContentBlockDelta();765 if (blockDelta.delta().isInputJson() && toolInputs.containsKey(blockDelta.index())) {766 toolInputs.get(blockDelta.index()).append(blockDelta.delta().asInputJson().partialJson());767 }768 }769 case CONTENT_BLOCK_STOP -> {770 var blockStop = event.asContentBlockStop();771 if (toolInputs.containsKey(blockStop.index())) {772 String accumulated = toolInputs.get(blockStop.index()).toString();773 try {774 IO.println("Tool input: " + objectMapper.readTree(accumulated));775 } catch (JsonProcessingException e) {776 // The accumulated string is not guaranteed to be valid JSON.777 // See "Handling invalid JSON in tool responses" on this page.778 IO.println("Invalid tool input: " + accumulated);779 }780 }781 }782 }783 }784 }785 ```786787 ```php PHP788 use Anthropic\Client;789 use Anthropic\Messages\InputJSONDelta;790 use Anthropic\Messages\Model;791 use Anthropic\Messages\RawContentBlockDeltaEvent;792 use Anthropic\Messages\RawContentBlockStartEvent;793 use Anthropic\Messages\RawContentBlockStopEvent;794 use Anthropic\Messages\ToolUseBlock;795796 $client = new Client();797798 // The PHP SDK does not provide a stream accumulator for tool input;799 // the manual pattern shown here is the supported approach.800 $toolInputs = []; // index => accumulated JSON string801802 $stream = $client->messages->createStream(803 maxTokens: 1024,804 model: Model::CLAUDE_OPUS_5,805 tools: [806 [807 'name' => 'get_weather',808 'description' => 'Get current weather for a city',809 'eager_input_streaming' => true,810 'input_schema' => [811 'type' => 'object',812 'properties' => ['city' => ['type' => 'string']],813 'required' => ['city'],814 ],815 ],816 ],817 messages: [['role' => 'user', 'content' => 'Weather in Paris?']],818 );819820 foreach ($stream as $event) {821 switch (true) {822 case $event instanceof RawContentBlockStartEvent:823 if ($event->contentBlock instanceof ToolUseBlock) {824 $toolInputs[$event->index] = '';825 }826 break;827 case $event instanceof RawContentBlockDeltaEvent:828 if ($event->delta instanceof InputJSONDelta) {829 $toolInputs[$event->index] .= $event->delta->partialJSON;830 }831 break;832 case $event instanceof RawContentBlockStopEvent:833 if (isset($toolInputs[$event->index])) {834 $accumulated = $toolInputs[$event->index];835 try {836 $parsed = json_decode($accumulated, associative: true, flags: JSON_THROW_ON_ERROR);837 echo "Tool input: " . json_encode($parsed) . "\n";838 } catch (JsonException $e) {839 // The accumulated string is not guaranteed to be valid JSON.840 // See "Handling invalid JSON in tool responses" on this page.841 echo "Invalid tool input: {$accumulated}\n";842 }843 }844 break;845 }846 }847 ```848849 ```ruby Ruby850 client = Anthropic::Client.new851852 tool_inputs = {} # index -> accumulated JSON string853854 stream = client.messages.stream_raw(855 model: Anthropic::Models::Model::CLAUDE_OPUS_5,856 max_tokens: 1024,857 tools: [858 {859 name: "get_weather",860 description: "Get current weather for a city",861 eager_input_streaming: true,862 input_schema: {863 type: "object",864 properties: {city: {type: "string"}},865 required: ["city"]866 }867 }868 ],869 messages: [{role: "user", content: "Weather in Paris?"}]870 )871872 stream.each do |event|873 case event874 when Anthropic::Models::RawContentBlockStartEvent875 tool_inputs[event.index] = +"" if event.content_block.type == :tool_use876 when Anthropic::Models::RawContentBlockDeltaEvent877 if event.delta.is_a?(Anthropic::Models::InputJSONDelta)878 tool_inputs[event.index] << event.delta.partial_json879 end880 when Anthropic::Models::RawContentBlockStopEvent881 if tool_inputs.key?(event.index)882 accumulated = tool_inputs[event.index]883 begin884 parsed = JSON.parse(accumulated)885 puts "Tool input: #{parsed}"886 rescue JSON::ParserError887 # The accumulated string is not guaranteed to be valid JSON.888 # See "Handling invalid JSON in tool responses" on this page.889 puts "Invalid tool input: #{accumulated}"890 end891 end892 end893 end894 ```895</CodeGroup>896897<Tip>898 Reacting to fragments and assembling them are separate concerns. The first example reacts to each fragment as it arrives and still hands assembly to the SDK in the tabs that use an accumulator helper. Use the manual pattern when you are not using an accumulator helper or when you want full control over assembly.899</Tip>900901## Handling invalid JSON in tool responses902903With fine-grained tool streaming, the accumulated input for a tool call might be invalid or incomplete JSON. When it is, you cannot run the tool, so report the failure back to Claude instead. The `content` of a tool result does not have to be JSON, but wrapping the raw string in a JSON object under a single key makes it unambiguous to Claude that you received invalid JSON, and preserves the original input for debugging:904905```json906{907 "INVALID_JSON": "<the unparseable input you received>"908}909```910911Return the wrapper, serialized to a string, as the `content` of a [tool result](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error) content block with `is_error` set to `true`:912913```json914{915 "type": "tool_result",916 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",917 "is_error": true,918 "content": "{\"INVALID_JSON\": \"<the unparseable input you received>\"}"919}920```921922<Note>923 Build the wrapper with your JSON library rather than by concatenating strings, so quotes and other special characters in the invalid input are escaped correctly.924</Note>925926## Next steps927928<CardGroup cols={2}>929 <Card title="Context windows" icon="stack" href="https://platform.claude.com/docs/en/build-with-claude/context-windows">930 Understand how the context window works, how extended thinking and tool use count toward it, and how to manage context as conversations grow.931 </Card>932933 <Card title="Streaming messages" icon="lightning" href="https://platform.claude.com/docs/en/build-with-claude/streaming">934 Stream Messages API responses incrementally with server-sent events, including text, tool use, and extended thinking deltas.935 </Card>936937 <Card title="Handle tool calls" icon="arrows-left-right" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls">938 Parse tool\_use blocks, format tool\_result responses, and handle errors with is\_error.939 </Card>940941 <Card title="Tool reference" icon="book" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference">942 Directory of Anthropic-provided tools and reference for optional tool definition properties.943 </Card>944</CardGroup>945