Skip to content

GetStreamingResponseAsync emits duplicate/incomplete FunctionCallContent when multiple tools are called in parallel #197

Description

@ziv690922

Bug Description

When the LLM calls multiple tools in parallel (e.g. ToolA + ToolB in the same response),
GetStreamingResponseAsync emits incorrect FunctionCallContent objects due to a logic
error in the RawContentBlockStopEvent handler.

Root Cause

In AnthropicClientExtensions.cs, the RawContentBlockStopEvent handler iterates over
all entries in streamingFunctions instead of only the entry at contentBlockStop.Index:

case RawContentBlockStopEvent contentBlockStop:
    if (streamingFunctions is not null)
    {
        foreach (var sf in streamingFunctions) // ← should only emit sf at contentBlockStop.Index
        {
            contents.Add(FunctionCallContent.CreateFromParsedArguments(...));
        }
        streamingFunctions.Clear();
    }
    break;

What Actually Happens

Given a response with ToolA (index 1) and ToolB (index 2):

Event streamingFunctions Emitted
ToolA RawContentBlockStopEvent {1: ToolA, 2: ToolB} FCC(ToolA) + FCC(ToolB) ← ToolB emitted early with incomplete Arguments
ToolB RawContentBlockStopEvent {} (already cleared) nothing ← ToolB's complete Arguments are lost

This results in:

  1. ToolB is emitted before its InputJsonDelta chunks are complete — Arguments may be incomplete
  2. FunctionInvokingChatClient receives duplicate/wrong FunctionCallContent, causing it to
    send a second API request with duplicate tool_use ids, which Anthropic API rejects with:
    {"type":"error","error":{"type":"invalid_request_error","message":"messages.X.content.Y: tool_use ids must be unique"}}

Expected Behavior

RawContentBlockStopEvent should only emit the FunctionCallContent for the tool at
contentBlockStop.Index:

case RawContentBlockStopEvent contentBlockStop:
    if (streamingFunctions is not null &&
        streamingFunctions.TryGetValue(contentBlockStop.Index, out var sf))
    {
        contents.Add(FunctionCallContent.CreateFromParsedArguments(
            sf.Arguments.ToString(),
            sf.CallId,
            sf.Name,
            json => (Dictionary<string, object?>?)JsonSerializer.Deserialize(
                json,
                AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(Dictionary<string, object?>))
            )
        ));
    }
    break;

Steps to Reproduce

  1. Use AnthropicClient.AsIChatClient() with UseFunctionInvocation() middleware
  2. Provide a prompt that causes the LLM to call two or more tools in parallel in the same response turn
  3. Observe AnthropicBadRequestException with tool_use ids must be unique

Environment

  • Package: Anthropic (official SDK)
  • Affected versions: v11.0.0, confirmed still present in main
  • Microsoft.Extensions.AI version: 10.4.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsdk

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions