Basic checks
What is broken?
When Anthropic returns multiple tool calls in a single streaming response, the JSON argument fragments from different tool calls are concatenated into one invalid string, causing JSON::ParserError in StreamAccumulator#tool_calls_from_stream (called during to_message).
The root cause is in Providers::Anthropic::Tools#extract_tool_calls — input_json_delta events are returned with id: nil:
if json_delta?(data)
{ nil => ToolCall.new(id: nil, name: nil, arguments: data.dig("delta", "partial_json")) }
Then StreamAccumulator#accumulate_tool_calls routes all id: nil fragments to @latest_tool_call_id:
else
existing = @tool_calls[@latest_tool_call_id]
existing.arguments << fragment
When tool call A and tool call B stream with interleaved deltas, B fragments get appended to A argument string, producing:
{"symbol":"MNQM26","from":"2026-03-31","interval":"minute"}{"query":"market news March 31 2026"}
JSON.parse then raises unexpected characters after the JSON document.
How to reproduce
The bug triggers whenever Anthropic emits multiple interleaved tool calls in a single streamed turn. One easy repro path:
- Use Anthropic Claude with streaming enabled
- Register two or more tools (e.g., a local tool + Anthropic server tool like
web_search, or just multiple local tools)
- Send a prompt that triggers parallel tool calls (e.g., "Look up my recent trades and search the web for market news about the price spike at 7:11 AM")
- The streaming response includes interleaved
content_block_start + input_json_delta events for both tool calls
tool_calls_from_stream raises JSON::ParserError
The bug is about multiple interleaved Anthropic tool calls in general — mixing local and server tools just makes it easy to trigger.
Expected behavior
Each tool call JSON fragments should be accumulated separately, producing two valid JSON argument objects.
Root cause and fix
Anthropic streaming API includes an index field on every content_block_delta event that identifies which content block the fragment belongs to:
{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"symbol\":"}}
{"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"query\":"}}
The fix: pass the index through extract_tool_calls and use it in accumulate_tool_calls to map each fragment to the correct tool call (via a block_index -> tool_call_id mapping built from content_block_start events).
Current workaround
We are using calls: :one (disable_parallel_tool_use: true) to prevent Anthropic from emitting parallel tool calls, which avoids the bug entirely at the cost of sequential tool execution.
Environment
- ruby_llm version: 1.13.2
- Ruby version: 3.4.7
- Provider: Anthropic (Claude Sonnet 4)
- Streaming: yes
- Tools: multiple
Basic checks
What is broken?
When Anthropic returns multiple tool calls in a single streaming response, the JSON argument fragments from different tool calls are concatenated into one invalid string, causing
JSON::ParserErrorinStreamAccumulator#tool_calls_from_stream(called duringto_message).The root cause is in
Providers::Anthropic::Tools#extract_tool_calls—input_json_deltaevents are returned withid: nil:Then
StreamAccumulator#accumulate_tool_callsroutes allid: nilfragments to@latest_tool_call_id:When tool call A and tool call B stream with interleaved deltas, B fragments get appended to A argument string, producing:
JSON.parsethen raisesunexpected characters after the JSON document.How to reproduce
The bug triggers whenever Anthropic emits multiple interleaved tool calls in a single streamed turn. One easy repro path:
web_search, or just multiple local tools)content_block_start+input_json_deltaevents for both tool callstool_calls_from_streamraisesJSON::ParserErrorThe bug is about multiple interleaved Anthropic tool calls in general — mixing local and server tools just makes it easy to trigger.
Expected behavior
Each tool call JSON fragments should be accumulated separately, producing two valid JSON argument objects.
Root cause and fix
Anthropic streaming API includes an
indexfield on everycontent_block_deltaevent that identifies which content block the fragment belongs to:{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"symbol\":"}} {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"query\":"}}The fix: pass the
indexthroughextract_tool_callsand use it inaccumulate_tool_callsto map each fragment to the correct tool call (via ablock_index -> tool_call_idmapping built fromcontent_block_startevents).Current workaround
We are using
calls: :one(disable_parallel_tool_use: true) to prevent Anthropic from emitting parallel tool calls, which avoids the bug entirely at the cost of sequential tool execution.Environment