diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs
index 43b3e321df9..9e36f548c00 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs
@@ -53,6 +53,7 @@ public ChatMessage Clone() =>
AdditionalProperties = AdditionalProperties,
_authorName = _authorName,
_contents = _contents,
+ CreatedAt = CreatedAt,
RawRepresentation = RawRepresentation,
Role = Role,
MessageId = MessageId,
@@ -65,6 +66,9 @@ public string? AuthorName
set => _authorName = string.IsNullOrWhiteSpace(value) ? null : value;
}
+ /// Gets or sets a timestamp for the chat message.
+ public DateTimeOffset? CreatedAt { get; set; }
+
/// Gets or sets the role of the author of the message.
public ChatRole Role { get; set; } = ChatRole.User;
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs
index a342ef1e69e..0889fed17d6 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs
@@ -130,19 +130,19 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
ChatMessage message = _messages![i];
updates[i] = new ChatResponseUpdate
{
- ConversationId = ConversationId,
-
AdditionalProperties = message.AdditionalProperties,
AuthorName = message.AuthorName,
Contents = message.Contents,
+ MessageId = message.MessageId,
RawRepresentation = message.RawRepresentation,
Role = message.Role,
- ResponseId = ResponseId,
- MessageId = message.MessageId,
- CreatedAt = CreatedAt,
+ ConversationId = ConversationId,
FinishReason = FinishReason,
- ModelId = ModelId
+ ModelId = ModelId,
+ ResponseId = ResponseId,
+
+ CreatedAt = message.CreatedAt ?? CreatedAt,
};
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
index 01ce878e79c..6691c665c54 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs
@@ -84,9 +84,10 @@ public static void AddMessages(this IList list, ChatResponseUpdate
var contentsList = filter is null ? update.Contents : update.Contents.Where(filter).ToList();
if (contentsList.Count > 0)
{
- list.Add(new ChatMessage(update.Role ?? ChatRole.Assistant, contentsList)
+ list.Add(new(update.Role ?? ChatRole.Assistant, contentsList)
{
AuthorName = update.AuthorName,
+ CreatedAt = update.CreatedAt,
RawRepresentation = update.RawRepresentation,
AdditionalProperties = update.AdditionalProperties,
});
@@ -268,7 +269,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
if (isNewMessage)
{
- message = new ChatMessage(ChatRole.Assistant, []);
+ message = new(ChatRole.Assistant, []);
response.Messages.Add(message);
}
else
@@ -280,11 +281,17 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
// Incorporate those into the latest message; in cases where the message
// stores a single value, prefer the latest update's value over anything
// stored in the message.
+
if (update.AuthorName is not null)
{
message.AuthorName = update.AuthorName;
}
+ if (update.CreatedAt is not null)
+ {
+ message.CreatedAt = update.CreatedAt;
+ }
+
if (update.Role is ChatRole role)
{
message.Role = role;
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
index 4d190fccda8..9562de0d93f 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
@@ -883,6 +883,10 @@
"Member": "System.Collections.Generic.IList Microsoft.Extensions.AI.ChatMessage.Contents { get; set; }",
"Stage": "Stable"
},
+ {
+ "Member": "System.DateTimeOffset? Microsoft.Extensions.AI.ChatMessage.CreatedAt { get; set; }",
+ "Stage": "Stable"
+ },
{
"Member": "string? Microsoft.Extensions.AI.ChatMessage.MessageId { get; set; }",
"Stage": "Stable"
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
index bb23e1de489..0fd8f4506db 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
@@ -95,6 +95,7 @@ public async Task GetResponseAsync(
// Create the return message.
ChatMessage message = new(ToChatRole(response.Role), response.Content)
{
+ CreatedAt = response.Created,
MessageId = response.Id, // There is no per-message ID, but there's only one message per response, so use the response ID
RawRepresentation = response,
};
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index 9fc2f3cbeef..a70fcec2a8f 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -438,6 +438,7 @@ internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompl
// Create the return message.
ChatMessage returnMessage = new()
{
+ CreatedAt = openAICompletion.CreatedAt,
MessageId = openAICompletion.Id, // There's no per-message ID, so we use the same value as the response ID
RawRepresentation = openAICompletion,
Role = FromOpenAIChatRole(openAICompletion.Role),
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index 60d0e10a159..dfe0c50d374 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -162,6 +162,11 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R
}
}
+ foreach (var message in response.Messages)
+ {
+ message.CreatedAt = openAIResponse.CreatedAt;
+ }
+
return response;
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs
index c449f064255..7fd9591a6ae 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs
@@ -18,6 +18,7 @@ public void Constructor_Parameterless_PropsDefaulted()
ChatMessage message = new();
Assert.Null(message.AuthorName);
Assert.Empty(message.Contents);
+ Assert.Null(message.CreatedAt);
Assert.Equal(ChatRole.User, message.Role);
Assert.Empty(message.Text);
Assert.NotNull(message.Contents);
@@ -50,6 +51,7 @@ public void Constructor_RoleString_PropsRoundtrip(string? text)
}
Assert.Null(message.AuthorName);
+ Assert.Null(message.CreatedAt);
Assert.Null(message.RawRepresentation);
Assert.Null(message.AdditionalProperties);
Assert.Equal(text ?? string.Empty, message.ToString());
@@ -113,6 +115,7 @@ public void Constructor_RoleList_PropsRoundtrip(int messageCount)
}
Assert.Null(message.AuthorName);
+ Assert.Null(message.CreatedAt);
Assert.Null(message.RawRepresentation);
Assert.Null(message.AdditionalProperties);
}
@@ -230,6 +233,20 @@ public void AdditionalProperties_Roundtrips()
Assert.Same(props, message.AdditionalProperties);
}
+ [Fact]
+ public void CreatedAt_Roundtrips()
+ {
+ ChatMessage message = new();
+ Assert.Null(message.CreatedAt);
+
+ DateTimeOffset now = DateTimeOffset.Now;
+ message.CreatedAt = now;
+ Assert.Equal(now, message.CreatedAt);
+
+ message.CreatedAt = null;
+ Assert.Null(message.CreatedAt);
+ }
+
[Fact]
public void ItCanBeSerializeAndDeserialized()
{
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs
index 802b414437d..de5809d3d97 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs
@@ -125,7 +125,7 @@ public void ToString_OutputsText()
}
[Fact]
- public void ToChatResponseUpdates()
+ public void ToChatResponseUpdates_SingleMessage()
{
ChatResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage" })
{
@@ -153,4 +153,55 @@ public void ToChatResponseUpdates()
Assert.Equal("value1", update1.AdditionalProperties?["key1"]);
Assert.Equal(42, update1.AdditionalProperties?["key2"]);
}
+
+ [Fact]
+ public void ToChatResponseUpdates_MultipleMessages()
+ {
+ ChatResponse response = new(
+ [
+ new ChatMessage(new ChatRole("customRole"), "Text")
+ {
+ CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
+ MessageId = "someMessage"
+ },
+ new ChatMessage(new ChatRole("secondRole"), "Another message")
+ {
+ CreatedAt = new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero),
+ MessageId = "anotherMessage"
+ }
+ ])
+ {
+ ResponseId = "12345",
+ ModelId = "someModel",
+ FinishReason = ChatFinishReason.ContentFilter,
+ CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
+ AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 },
+ };
+
+ ChatResponseUpdate[] updates = response.ToChatResponseUpdates();
+ Assert.NotNull(updates);
+ Assert.Equal(3, updates.Length);
+
+ ChatResponseUpdate update0 = updates[0];
+ Assert.Equal("12345", update0.ResponseId);
+ Assert.Equal("someMessage", update0.MessageId);
+ Assert.Equal("someModel", update0.ModelId);
+ Assert.Equal(ChatFinishReason.ContentFilter, update0.FinishReason);
+ Assert.Equal(new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt);
+ Assert.Equal("customRole", update0.Role?.Value);
+ Assert.Equal("Text", update0.Text);
+
+ ChatResponseUpdate update1 = updates[1];
+ Assert.Equal("12345", update1.ResponseId);
+ Assert.Equal("anotherMessage", update1.MessageId);
+ Assert.Equal("someModel", update1.ModelId);
+ Assert.Equal(ChatFinishReason.ContentFilter, update1.FinishReason);
+ Assert.Equal(new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero), update1.CreatedAt);
+ Assert.Equal("secondRole", update1.Role?.Value);
+ Assert.Equal("Another message", update1.Text);
+
+ ChatResponseUpdate update2 = updates[2];
+ Assert.Equal("value1", update2.AdditionalProperties?["key1"]);
+ Assert.Equal(42, update2.AdditionalProperties?["key2"]);
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs
index 8b13d640ae1..2eb8db9b477 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs
@@ -65,6 +65,65 @@ public async Task ToChatResponse_SuccessfullyCreatesResponse(bool useAsync)
Assert.Equal("Hello, world!", response.Text);
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ToChatResponse_UpdatesProduceMultipleResponseMessages(bool useAsync)
+ {
+ ChatResponseUpdate[] updates =
+ [
+
+ // First message - ID "msg1"
+ new(null, "Hi! ") { CreatedAt = new DateTimeOffset(2023, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" },
+ new(ChatRole.Assistant, "Hello") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" },
+ new(null, " from") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero) }, // Later CreatedAt should win
+ new(null, " AI") { MessageId = "msg1", AuthorName = "AI Assistant" }, // Later AuthorName should win
+
+ // Second message - ID "msg2"
+ new(ChatRole.User, "How") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 0, 0, TimeSpan.Zero), AuthorName = "User" },
+ new(null, " are") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero) },
+ new(null, " you?") { MessageId = "msg2", AuthorName = "Human User" }, // Later AuthorName should win
+
+ // Third message - ID "msg3"
+ new(ChatRole.Assistant, "I'm doing well,") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero) },
+ new(null, " thank you!") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero) }, // Later CreatedAt should win
+
+ // Updates without MessageId should continue the last message (msg3)
+ new(null, " How can I help?"),
+ ];
+
+ ChatResponse response = useAsync ?
+ await YieldAsync(updates).ToChatResponseAsync() :
+ updates.ToChatResponse();
+
+ Assert.NotNull(response);
+ Assert.Equal(3, response.Messages.Count);
+
+ // Verify first message
+ ChatMessage message1 = response.Messages[0];
+ Assert.Equal("msg1", message1.MessageId);
+ Assert.Equal(ChatRole.Assistant, message1.Role);
+ Assert.Equal("AI Assistant", message1.AuthorName); // Last value should win
+ Assert.Equal(new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero), message1.CreatedAt); // Last value should win
+ Assert.Equal("Hi! Hello from AI", message1.Text);
+
+ // Verify second message
+ ChatMessage message2 = response.Messages[1];
+ Assert.Equal("msg2", message2.MessageId);
+ Assert.Equal(ChatRole.User, message2.Role);
+ Assert.Equal("Human User", message2.AuthorName); // Last value should win
+ Assert.Equal(new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero), message2.CreatedAt); // Last value should win
+ Assert.Equal("How are you?", message2.Text);
+
+ // Verify third message
+ ChatMessage message3 = response.Messages[2];
+ Assert.Equal("msg3", message3.MessageId);
+ Assert.Equal(ChatRole.Assistant, message3.Role);
+ Assert.Null(message3.AuthorName); // No AuthorName set in later updates
+ Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero), message3.CreatedAt); // Last value should win
+ Assert.Equal("I'm doing well, thank you! How can I help?", message3.Text);
+ }
+
public static IEnumerable