diff --git a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
index d6fe530..7f50246 100644
--- a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
+++ b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
@@ -31,8 +31,10 @@ internal static class AbstractionMapper
if (stream is null)
return null;
+ var responseId = stream.CreatedAtString ?? Guid.NewGuid().ToString("N");
var chatMessage = ToChatMessage(stream.Message);
chatMessage.CreatedAt = stream.CreatedAt;
+ chatMessage.MessageId = responseId;
return new ChatResponse(chatMessage)
{
@@ -41,7 +43,7 @@ internal static class AbstractionMapper
CreatedAt = stream.CreatedAt,
ModelId = usedModel ?? stream.Model,
RawRepresentation = stream,
- ResponseId = stream.CreatedAtString ?? Guid.NewGuid().ToString("N"),
+ ResponseId = responseId,
Usage = ParseOllamaChatResponseUsage(stream)
};
}
@@ -348,6 +350,7 @@ public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? respon
AdditionalProperties = ParseOllamaChatResponseProps(done),
CreatedAt = done.CreatedAt,
FinishReason = done.DoneReason is null ? null : new ChatFinishReason(done.DoneReason),
+ MessageId = responseId,
RawRepresentation = response,
ResponseId = responseId,
ModelId = done.Model
@@ -359,6 +362,7 @@ public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? respon
// no need to set "Contents" as we set the text
CreatedAt = response?.CreatedAt,
FinishReason = response?.Done == true ? ChatFinishReason.Stop : null,
+ MessageId = responseId,
RawRepresentation = response,
ResponseId = responseId,
ModelId = response?.Model
diff --git a/test/AbstractionMapperTests.cs b/test/AbstractionMapperTests.cs
index 13dca23..02c6ae0 100644
--- a/test/AbstractionMapperTests.cs
+++ b/test/AbstractionMapperTests.cs
@@ -930,6 +930,7 @@ public void Maps_Known_Properties()
response.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
response.FinishReason.ShouldBe(ChatFinishReason.Stop);
response.Messages[0].AuthorName.ShouldBeNull();
+ response.Messages[0].MessageId.ShouldBe(ollamaCreatedStamp);
response.Messages[0].RawRepresentation.ShouldBe(stream.Message);
response.Messages[0].Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant);
response.Messages[0].Text.ShouldBe("Hi.");
@@ -974,6 +975,7 @@ public void Maps_Known_Properties()
((TextContent)streamingChatCompletion.Contents[0]).Text.ShouldBe("Hi.");
streamingChatCompletion.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
streamingChatCompletion.FinishReason.ShouldBe(ChatFinishReason.Stop);
+ streamingChatCompletion.MessageId.ShouldBe("12345");
streamingChatCompletion.RawRepresentation.ShouldBe(stream);
streamingChatCompletion.ResponseId.ShouldBe("12345");
streamingChatCompletion.Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant);
@@ -1004,6 +1006,7 @@ public void Maps_Thinking_Tokens()
((TextReasoningContent)streamingChatCompletion.Contents[0]).Text.ShouldBe("Beer.");
streamingChatCompletion.CreatedAt.ShouldBe(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
streamingChatCompletion.FinishReason.ShouldBe(ChatFinishReason.Stop);
+ streamingChatCompletion.MessageId.ShouldBe("12345");
streamingChatCompletion.RawRepresentation.ShouldBe(stream);
streamingChatCompletion.ResponseId.ShouldBe("12345");
streamingChatCompletion.Role.ShouldBe(Microsoft.Extensions.AI.ChatRole.Assistant);
@@ -1068,6 +1071,58 @@ public void Maps_ToolCalls()
chatMessage.Text.ShouldBe("It seems the sun will be out all day.");
}
+ ///
+ /// Verifies that MEAI keeps updates from separate provider calls in separate messages.
+ ///
+ [Test]
+ public void Separates_Updates_With_Different_MessageIds()
+ {
+ var updates = new[]
+ {
+ AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream
+ {
+ Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "First" }
+ }, "response-1"),
+ AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream
+ {
+ Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "Second" }
+ }, "response-2")
+ };
+
+ var response = updates.ToChatResponse();
+
+ response.Messages.Count.ShouldBe(2);
+ response.Messages[0].MessageId.ShouldBe("response-1");
+ response.Messages[0].Text.ShouldBe("First");
+ response.Messages[1].MessageId.ShouldBe("response-2");
+ response.Messages[1].Text.ShouldBe("Second");
+ }
+
+ ///
+ /// Verifies that MEAI combines chunks that belong to the same logical message.
+ ///
+ [Test]
+ public void Combines_Updates_With_The_Same_MessageId()
+ {
+ var updates = new[]
+ {
+ AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream
+ {
+ Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "Hel" }
+ }, "response-1"),
+ AbstractionMapper.ToChatResponseUpdate(new ChatResponseStream
+ {
+ Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "lo" }
+ }, "response-1")
+ };
+
+ var response = updates.ToChatResponse();
+
+ response.Messages.Count.ShouldBe(1);
+ response.Messages[0].MessageId.ShouldBe("response-1");
+ response.Messages[0].Text.ShouldBe("Hello");
+ }
+
///
/// Verifies that performance metrics from ChatDoneResponseStream (LoadDuration, TotalDuration, etc.)
/// are correctly mapped to the AdditionalProperties dictionary.
@@ -1107,6 +1162,7 @@ public void Maps_Performance_Metrics_To_AdditionalProperties()
// Assert - AdditionalProperties contains performance metrics
const double NANOSECONDS_PER_MILLISECOND = 1_000_000;
+ streamingChatCompletion.MessageId.ShouldBe(responseId);
streamingChatCompletion.AdditionalProperties.ShouldNotBeNull();
streamingChatCompletion.AdditionalProperties.ShouldContainKey(Application.LoadDuration);
streamingChatCompletion.AdditionalProperties[Application.LoadDuration].ShouldBe(TimeSpan.FromMilliseconds(123_456_789 / NANOSECONDS_PER_MILLISECOND));
@@ -1127,6 +1183,13 @@ public void Maps_Performance_Metrics_To_AdditionalProperties()
usageContent.Details.InputTokenCount.ShouldBe(42);
usageContent.Details.OutputTokenCount.ShouldBe(15);
usageContent.Details.TotalTokenCount.ShouldBe(42 + 15);
+
+ var combinedResponse = new[] { streamingChatCompletion }.ToChatResponse();
+ combinedResponse.Messages.Single().AdditionalProperties.ShouldNotBeNull();
+ combinedResponse.Messages.Single().AdditionalProperties.ShouldContainKey(Application.TotalDuration);
+ combinedResponse.Usage.ShouldNotBeNull();
+ combinedResponse.Usage.InputTokenCount.ShouldBe(42);
+ combinedResponse.Usage.OutputTokenCount.ShouldBe(15);
}
}
}
@@ -1273,4 +1336,4 @@ public void Maps_Response()
}
#pragma warning restore CS8602 // Dereference of a possibly null reference.
-#pragma warning restore CS8604 // Possible null reference argument.
\ No newline at end of file
+#pragma warning restore CS8604 // Possible null reference argument.