Skip to content

[Microsoft.Extensions.AI] ImageGeneratingChatClient duplicates preceding contents and drops following contents #7620

Description

@JamesHuang1018

Description

ImageGeneratingChatClient.RequestState.ReplaceImageGenerationFunctionResults() can duplicate every AIContent item that appears before a matched image-generation function item and silently drop every item that appears after it.

The affected items are not limited to FunctionCallContent or FunctionResultContent. Any AIContent subtype can be duplicated or dropped based on its position, including UsageContent, TextContent, TextReasoningContent, DataContent, and UriContent.

This can cause duplicate tool-result events, inaccurate token usage, missing MCP results, and incomplete text or reasoning content. No exception is thrown.

Reproduction Steps

Use an ImageGeneratingChatClient wrapping a FunctionInvokingChatClient:

builder
    .UseImageGeneration(imageGenerator)
    .UseFunctionInvocation(loggerFactory);

Produce a response update whose Contents contains unrelated content on both sides of an image-generation function result:

[A, ImageGenerationFunctionResult, B]

For example:

[
  FunctionResultContent("mcp-call", mcpResult),
  FunctionResultContent("image-call", imageResult),
  UsageContent(...)
]

The relevant implementation behaves approximately as follows:

for (int index = contents.Count - 1; index >= 0; index--)
{
    AIContent content = contents[index];

    if (IsImageGenerationFunctionContent(content))
    {
        if (list is null)
        {
            list = CopyList(contents, index);
        }

        list.Add(ConvertImageGenerationContent(content));
    }
    else
    {
        list?.Add(content);
    }
}

Items after the image-generation item are visited while list is null and are therefore discarded. When the image-generation item is encountered, CopyList(contents, index) copies all preceding items. The reverse loop then visits those same preceding indexes and appends them a second time.

Expected behavior

Only the matched image-generation function call or result should be replaced. All unrelated content should retain its original order and cardinality:

[A, ImageGenerationToolResultContent, B]

Actual behavior

The preceding content is duplicated and the following content is dropped:

[A, ImageGenerationToolResultContent, A]

A concrete observed sequence was:

[
  FunctionResultContent(CallId = "mcp-call"),
  ImageGenerationToolResultContent,
  FunctionResultContent(CallId = "mcp-call")
]

If UsageContent or another AIContent item follows the image-generation result, it is silently absent from the returned list.

Regression?

Unknown.

Known Workarounds

Deduplicating FunctionResultContent by CallId downstream can suppress duplicate tool-result events, but it cannot restore content that has already been dropped. There is currently no complete downstream workaround.

Configuration

  • Microsoft.Extensions.AI: 10.7.0
  • Microsoft.Extensions.AI.Abstractions: 10.7.0
  • .NET 10
  • macOS
  • Blazor: not applicable

Other information

The issue appears to be in ImageGeneratingChatClient.RequestState.ReplaceImageGenerationFunctionResults().

The method scans the list in reverse, creates the replacement list by copying only the prefix before the first matched image-generation item, and then continues scanning that already-copied prefix. A forward-order replacement or cloning the full list and replacing matched indexes in place would preserve unrelated content.

Suggested regression coverage:

  • Image-generation content at the beginning
  • Image-generation content in the middle
  • Image-generation content at the end
  • Multiple unrelated AIContent subtypes before and after the matched item
  • Both FunctionCallContent and FunctionResultContent image-generation conversions

ImageGeneratingChatClient is currently marked with the experimental diagnostic MEAI001.

Metadata

Metadata

Assignees

Labels

area-aiMicrosoft.Extensions.AI librariesbugThis issue describes a behavior which is not expected - a bug.

Type

No type

Fields

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