Description
When using OpenAI ResponsesClient based IChatClient, the ChatMessage with FunctionCallContent is missing MessageId. With similar configuration, the completions based IChatClient returns the MessageId as expected.
Reproduction Steps
Example with AzureOpenAIClient.
Running these 3 examples shows difference between implementations. (Make sure to backfill the Endpoint and API key).
dotnet demo.cs --client responses "What is the weather in London right now? Use the available tool." uses responses API and the result has no MessageID`
dotnet demo.cs --client chat "What is the weather in London right now? Use the available tool." uses completions API and the result has MessageID (as expected)
dotnet demo.cs --client responses "Hello" uses the responses API without function call, and the result has MessageID as expected.
// ============================================================
// To run:
// dotnet demo.cs --client responses "What is the weather in London right now? Use the available tool."
// dotnet demo.cs --client chat "What is the weather in London right now? Use the available tool."
// dotnet demo.cs --client responses "Hello"
//
// ============================================================
// ── Package Installs ──────────────────────────────────────────────────────────
#:package Azure.AI.OpenAI@2.9.0-beta.1
#:package Microsoft.Extensions.AI@10.5.0
#:package Microsoft.Extensions.AI.OpenAI@10.5.0
#:package System.CommandLine@2.0.7
#pragma warning disable OPENAI001
using System.CommandLine;
using System.ComponentModel;
using System.ClientModel;
using System.Runtime.InteropServices;
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
// ── Configuration ─────────────────────────────────────────────────────────────
const string Endpoint = "";
const string APIKey = "";
const string Deployment = "gpt-5-mini"; // must support reasoning + function calling
// ── Argument parsing ──────────────────────────────────────────────────────────
var clientOption = new Option<string>("--client", [])
{
Description = "Client to use: 'completions' or 'responses'",
DefaultValueFactory = _ => "completions",
};
clientOption.AcceptOnlyFromAmong("completions", "responses");
var messageArg = new Argument<string[]>("message")
{
Description = "Message to send to the model",
Arity = ArgumentArity.OneOrMore,
};
var rootCommand = new RootCommand("Azure OpenAI responses bug repro") { clientOption, messageArg };
rootCommand.SetAction(async (parseResult, cancellationToken) =>
{
var clientType = parseResult.GetValue(clientOption) ?? "completions";
var messageTokens = parseResult.GetValue(messageArg)!;
// ── Client setup ─────────────────────────────────────────────────────────
AzureOpenAIClient azureClient = new(
new Uri(Endpoint),
new ApiKeyCredential(APIKey));
IChatClient chatClient = clientType == "responses"
? azureClient.GetResponsesClient().AsIChatClient(Deployment)
.AsBuilder()
.ConfigureOptions(x => x.RawRepresentationFactory = _ =>
new CreateResponseOptions() { StoredOutputEnabled = false })
.Build()
: azureClient.GetChatClient(Deployment).AsIChatClient();
// ── Tool definition ───────────────────────────────────────────────────────
// A simple weather tool. The prompt below is written to force the model to
// both reason AND call this tool in the same response, which triggers the bug.
var getWeatherTool = AIFunctionFactory.Create(
([Description("The city to get weather for")] string city)
=> $"It is currently 18°C and cloudy in {city}.",
"get_weather",
"Returns current weather conditions for a city.");
// No UseFunctionInvocation() – we want to inspect the raw function call before
// anything is auto-invoked.
var options = new ChatOptions
{
Tools = [getWeatherTool],
};
// ── Debug: System info ────────────────────────────────────────────────────
Console.WriteLine("═══ Environment ══════════════════════════════════════════════════");
Console.WriteLine($" Runtime : {RuntimeInformation.FrameworkDescription}");
Console.WriteLine($" OS : {RuntimeInformation.OSDescription}");
Console.WriteLine($" OS Arch : {RuntimeInformation.OSArchitecture}");
Console.WriteLine($" Proc Arch : {RuntimeInformation.ProcessArchitecture}");
Console.WriteLine($" Deployment: {Deployment}");
Console.WriteLine($" Client : {clientType}");
Console.WriteLine();
var messages = new List<ChatMessage>
{
new(ChatRole.User, string.Join(" ", messageTokens))
};
// ── Step 1: Collect the raw ChatResponseUpdate stream ─────────────────────
// Each update is printed as it arrives.
// Both the reasoning deltas and the function-call delta are unlabelled.
Console.WriteLine("═══ Raw ChatResponseUpdate stream ═══════════════════════════════");
Console.WriteLine($" {"MessageId",-30} Contents");
Console.WriteLine($" {"─────────",-30} ────────");
var updates = new List<ChatResponseUpdate>();
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options))
{
updates.Add(update);
if (update.Contents.Count == 0)
{
Console.WriteLine($" {(update.MessageId ?? "(null)"),-30} [No content]");
continue;
}
var contentSummary = string.Join(" + ", update.Contents.Select(DescribeContent));
Console.WriteLine($" {(update.MessageId ?? "(null)"),-30} {contentSummary}");
}
// ── Step 2: Reconstruct via ToChatResponse() ──────────────────────────────
Console.WriteLine();
Console.WriteLine("═══ Reconstructed ChatResponse via ToChatResponse() ═════════════");
ChatResponse response = updates.ToChatResponse();
Console.WriteLine($"Total messages : {response.Messages.Count}");
for (int i = 0; i < response.Messages.Count; i++)
{
ChatMessage msg = response.Messages[i];
Console.WriteLine();
Console.WriteLine($" ┌─ Message[{i}]");
Console.WriteLine($" │ MessageId : {msg.MessageId ?? "(null) ← ⚠ BUG?"}");
Console.WriteLine($" │ Role : {msg.Role}");
foreach (var content in msg.Contents)
{
switch (content)
{
case TextReasoningContent r:
Console.WriteLine($" │ [TextReasoningContent] \"{Truncate(r.Text, 72)}\"");
break;
case FunctionCallContent f:
Console.WriteLine($" │ [FunctionCallContent]");
Console.WriteLine($" │ Name : {f.Name}");
Console.WriteLine($" │ CallId : {f.CallId}");
break;
case TextContent t:
Console.WriteLine($" │ [TextContent] \"{Truncate(t.Text, 72)}\"");
break;
default:
Console.WriteLine($" │ [{content.GetType().Name}]");
break;
}
}
Console.WriteLine(" └────────────────────────────────────────────────────────────");
}
return 0;
});
#pragma warning restore OPENAI001
return await rootCommand.Parse(args).InvokeAsync();
// ── Helpers ───────────────────────────────────────────────────────────────────
static string DescribeContent(AIContent c) => c switch
{
TextReasoningContent r => $"Reasoning(\"{Truncate(r.Text, 45)}\")",
FunctionCallContent f => $"FunctionCall(name={f.Name}, callId={f.CallId})",
TextContent t => $"Text(\"{Truncate(t.Text, 45)}\")",
_ => c.GetType().Name,
};
static string Truncate(string? s, int max) =>
s is null ? "" : s.Length <= max ? s : s[..max] + "…";
Expected behavior
Expecting the result to have MessageId on the ChatMessage just like I would with completions API.
$ dotnet demo.cs --client completions "What is the weather in London right now? Use the available tool."
═══ Environment ══════════════════════════════════════════════════
Runtime : .NET 10.0.0-rc.2.25502.107
OS : Ubuntu 24.04.3 LTS
OS Arch : Arm64
Proc Arch : Arm64
Deployment: gpt-5-mini
Client : completions
═══ Raw ChatResponseUpdate stream ═══════════════════════════════
MessageId Contents
───────── ────────
[No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH [No content]
chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH UsageContent
FunctionCall(name=get_weather, callId=call_8V2HGOVzRkFhVcrV1piGhQdQ)
═══ Reconstructed ChatResponse via ToChatResponse() ═════════════
Total messages : 1
┌─ Message[0]
│ MessageId : chatcmpl-DXayfSKsyyICsLlL3NGZW5CFsmJMH
│ Role : assistant
│ [FunctionCallContent]
│ Name : get_weather
│ CallId : call_8V2HGOVzRkFhVcrV1piGhQdQ
└────────────────────────────────────────────────────────────
Actual behavior
No MessageId in the result.
$ dotnet demo.cs --client responses "What is the weather in London right now? Use the available tool."
═══ Environment ══════════════════════════════════════════════════
Runtime : .NET 10.0.0-rc.2.25502.107
OS : Ubuntu 24.04.3 LTS
OS Arch : Arm64
Proc Arch : Arm64
Deployment: gpt-5-mini
Client : responses
═══ Raw ChatResponseUpdate stream ═══════════════════════════════
MessageId Contents
───────── ────────
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) [No content]
(null) FunctionCall(name=get_weather, callId=call_V7VpNFBxbUisZjPlPoInnpVT)
(null) UsageContent
═══ Reconstructed ChatResponse via ToChatResponse() ═════════════
Total messages : 1
┌─ Message[0]
│ MessageId : (null) ← ⚠ BUG?
│ Role : assistant
│ [FunctionCallContent]
│ Name : get_weather
│ CallId : call_V7VpNFBxbUisZjPlPoInnpVT
└────────────────────────────────────────────────────────────
Regression?
Not sure.
Known Workarounds
Temporarily experimenting with middleware based workaround (for the streaming portion):
private static async IAsyncEnumerable<ChatResponseUpdate> ResponsesStreamingMiddleware(
IEnumerable<ChatMessage> msgs,
Microsoft.Extensions.AI.ChatOptions? options,
IChatClient innerClient,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var update in innerClient.GetStreamingResponseAsync(msgs, options, ct).WithCancellation(ct).ConfigureAwait(false))
{
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
yield return update switch
{
{ RawRepresentation: StreamingResponseReasoningSummaryTextDeltaUpdate { ItemId: string itemId } } => WithMessageId(update, itemId), // Text delta
{ RawRepresentation: StreamingResponseOutputItemDoneUpdate { Item: ReasoningResponseItem item } } => WithMessageId(update, item.Id), // Reasoning item done
{ RawRepresentation: StreamingResponseOutputItemAddedUpdate { Item: FunctionCallResponseItem item } } => WithMessageId(update, item.Id),
{ RawRepresentation: StreamingResponseOutputItemDoneUpdate { Item: FunctionCallResponseItem item } } => WithMessageId(update, item.Id),
_ => update
};
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
}
private static ChatResponseUpdate WithMessageId(ChatResponseUpdate update, string messageId)
{
if (string.IsNullOrEmpty(update.MessageId))
{
update = update.Clone();
update.MessageId = messageId;
}
return update;
}
Configuration
Runtime : .NET 10.0.0-rc.2.25502.107
OS : Ubuntu 24.04.3 LTS
OS Arch : Arm64
Proc Arch : Arm64
Deployment: gpt-5-mini
Microsoft.Extensions.AI@10.5.0
Microsoft.Extensions.AI.OpenAI@10.5.0
Other information
No response
Description
When using OpenAI ResponsesClient based
IChatClient, theChatMessagewithFunctionCallContentis missingMessageId. With similar configuration, the completions based IChatClient returns theMessageIdas expected.Reproduction Steps
Example with
AzureOpenAIClient.Running these 3 examples shows difference between implementations. (Make sure to backfill the Endpoint and API key).
dotnet demo.cs --client responses "What is the weather in London right now? Use the available tool."uses responses API and the result has no MessageID`dotnet demo.cs --client chat "What is the weather in London right now? Use the available tool."uses completions API and the result has MessageID (as expected)dotnet demo.cs --client responses "Hello"uses the responses API without function call, and the result has MessageID as expected.Expected behavior
Expecting the result to have MessageId on the
ChatMessagejust like I would with completions API.Actual behavior
No MessageId in the result.
Regression?
Not sure.
Known Workarounds
Temporarily experimenting with middleware based workaround (for the streaming portion):
Configuration
Runtime : .NET 10.0.0-rc.2.25502.107
OS : Ubuntu 24.04.3 LTS
OS Arch : Arm64
Proc Arch : Arm64
Deployment: gpt-5-mini
Microsoft.Extensions.AI@10.5.0
Microsoft.Extensions.AI.OpenAI@10.5.0
Other information
No response