Skip to content

[BUG] Streaming parallel tool calls: JSON fragments concatenated across different tool calls #710

@alexandrigo

Description

@alexandrigo

Basic checks

  • I searched existing issues - this has not been reported
  • I can reproduce this consistently
  • This is a RubyLLM bug, not my application code

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_callsinput_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:

  1. Use Anthropic Claude with streaming enabled
  2. Register two or more tools (e.g., a local tool + Anthropic server tool like web_search, or just multiple local tools)
  3. 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")
  4. The streaming response includes interleaved content_block_start + input_json_delta events for both tool calls
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions