OpenAI Responses API streaming responses are missing MessageID on function calls.
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 894
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 23
Description
### 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.
```c#
// ============================================================
// 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("--client", [])
{
Description = "Client to use: 'completions' or 'responses'",
DefaultValueFactory = _ => "completions",
};
clientOption.AcceptOnlyFromAmong("completions", "responses");
var messageArg = new Argument("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
{
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();
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.
```bash
$ 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.
```bash
$ 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):
```c#
private static async IAsyncEnumerable ResponsesStreamingMiddleware(
IEnumerable 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_
Contributor guide
Assessment
This issue has not been assessed yet.