Search across all documentation pages
When Claude decides to call a tool while streaming, the tool's arguments do not arrive as one clean JSON object.
They arrive as a sequence of input_json_delta events, each one a small string fragment that only becomes valid JSON once every fragment has been concatenated in order.
Parsing too early - or parsing each fragment individually - is a common source of crashes in streaming tool-use code. This page shows the correct accumulate-then-parse pattern.
import json
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
json_buffer = ""
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=512,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Lisbon?"}],
) as stream:
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "input_json_delta":
json_buffer += event.delta.partial_json
elif event.type == "content_block_stop":
if json_buffer:
tool_input = json.loads(json_buffer)
print(tool_input) # {"city": "Lisbon"}
json_buffer = ""When to reach for this:
tools, since a tool call's arguments always stream as fragments, never as one event.get_weather..." before the arguments are fully known.A complete, runnable example that handles multiple tool calls in one response, keyed by block index so each tool's JSON accumulates independently.
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties"
What this demonstrates:
event.index, not a single global string, so concurrent tool_use blocks in the same response don't corrupt each other's JSON.name and id from content_block_start, since input_json_delta events carry only JSON fragments, not identifying metadata.json.loads(...) until content_block_stop, the only point at which the accumulated string is guaranteed to be complete and parseable."{}" when a tool call has no arguments at all (a tool with no required input still produces an empty JSON object).content_block_delta event's delta.partial_json field is a fragment of a JSON string - not a fragment of a parsed object - so fragments must be joined as strings before any parsing happens.content_block_start fires once per tool_use block and carries the tool's name and a unique id, both needed later to build the tool_result you send back to Claude.content_block_stop is the signal that a given block's JSON is complete; parsing before this event will fail on most fragments since they are, individually, invalid JSON.index - the same field used for text and thinking blocks - so any general-purpose stream handler that tracks per-block state should key by index, not by content type alone.partial_json at a Glance| Field | Type | Meaning |
|---|---|---|
event.delta.partial_json | str | The next fragment of the tool input's JSON string - append, never replace. |
event.content_block.name | str | The tool being called - available at content_block_start, not on each delta. |
event.content_block.id | str | The tool_use_id you must echo back in the matching tool_result block. |
event.index | int | Which content block this event belongs to - the accumulation key. |
# A small, reusable accumulator class instead of inline dicts - useful once you have
# more than a couple of call sites doing this pattern.
class ToolCallAccumulator:
def __init__(self) -> None:
self._parts: dict[int, list[str]] = {}
self._meta: dict[int, dict] = {}
def
json.loads() on every individual input_json_delta - almost every fragment is invalid JSON on its own, so this raises json.JSONDecodeError constantly. Fix: accumulate fragments as strings and parse exactly once, at content_block_stop.event.index, as shown above.content_block.name from a delta event instead of content_block_start - deltas don't carry the tool name, only content_block_start does. Fix: capture name and id when the block starts, store them alongside the buffer.input_json_delta events. Fix: default the buffer to "{}" before parsing if no deltas arrived.id back as tool_use_id in the tool_result - Claude cannot match your result to the right call without it. Fix: carry content_block.id through your accumulator into the result you send back.tool_name..." indicator instead of the raw partial JSON.| Alternative | Use When | Don't Use When |
|---|---|---|
Non-streaming tool use (client.messages.create) | The full tool call, not the process of arriving at it, is all your app needs | The user is watching the response live and tool selection latency matters |
| A tolerant "partial JSON" parser (e.g. attempting to close open braces) | You genuinely need to show live-updating partial arguments in a debug UI | Production code paths - a malformed partial parse can silently produce wrong tool input |
| Buffering the entire event list and processing after the stream closes | Simpler code, no need for a live UI | You want to act on a tool call the moment it's ready, before the rest of the response finishes |
A partial_json string field holding the next fragment of the tool's argument JSON - a few characters to a few dozen, with no guarantee it lines up with any JSON syntax boundary.
Only after content_block_stop fires for that block's index. Before that, the accumulated string can be (and usually is) incomplete JSON.
content_block_start includes content_block.name and content_block.id immediately, before any input_json_delta events arrive - capture those first.
Two separate tool_use content blocks stream, each with its own index. Keep a buffer per index so their JSON fragments don't get interleaved into one corrupted string.
Not usefully - a half-formed JSON string doesn't mean anything to a reader. Show a static "calling tool_name..." indicator instead and reveal the arguments once parsed.
That indicates a bug in your accumulation logic (e.g. missing a delta, wrong ordering, or mixing buffers across indexes) rather than a Claude API problem - the API guarantees the complete accumulated string is valid JSON at block close.
Yes - it must be echoed back exactly in the tool_result content block you send in the next turn, so Claude can match your result to the call it made.
The end result (a Python dict from json.loads) is identical. The difference is only in how you get there: non-streaming gives you the complete JSON string in one shot; streaming requires accumulating it first.
Its tool_use block may produce zero input_json_delta events. Default your buffer to an empty JSON object ("{}") before parsing so you don't call json.loads(""), which raises an error.
Once you have the parsed input, execution and the follow-up tool_result turn work exactly the same as in the non-streaming tool-use loop - see Combining Streaming with the Tool Use Loop for continuing the conversation.
input_json_delta fits in the overall event model.tools parameter used above is structured.asyncio code.Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.