SPB Git

Repository Public

fine-grained-tool-streaming.md 35.3 KB 945 lines markdown

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
Files main
Menu

Theme