Repository: awaescher/OllamaSharp
Pull request #200: Update to M.E.AI 9.3.0-preview.1.25161.3
Author: login: stephentoub
Created: 2025-03-12T00:13:53Z | Merged: 2025-03-13T18:18:15Z



Diff:
diff --git a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
index a49b111..9a32945 100644
--- a/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
+++ b/src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
@@ -30,11 +30,10 @@ internal static class AbstractionMapper
 		{
 			FinishReason = ToFinishReason(stream.DoneReason),
 			AdditionalProperties = ParseOllamaChatResponseProps(stream),
-			Choices = [chatMessage],
 			CreatedAt = stream.CreatedAt,
 			ModelId = usedModel ?? stream.Model,
 			RawRepresentation = stream,
-			ResponseId = stream.CreatedAtString,
+			ResponseId = stream.CreatedAtString ?? Guid.NewGuid().ToString("N"),
 			Usage = ParseOllamaChatResponseUsage(stream)
 		};
 	}
@@ -43,12 +42,12 @@ internal static class AbstractionMapper
 	/// Converts Microsoft.Extensions.AI <see cref="ChatMessage"/> objects and
 	/// an option <see cref="ChatOptions"/> instance to an OllamaSharp <see cref="ChatRequest"/>.
 	/// </summary>
-	/// <param name="chatMessages">A list of chat messages.</param>
+	/// <param name="messages">A list of chat messages.</param>
 	/// <param name="options">Optional chat options to configure the request.</param>
 	/// <param name="stream">Indicates if the request should be streamed.</param>
 	/// <param name="serializerOptions">Serializer options</param>
 	/// <returns>A <see cref="ChatRequest"/> object containing the converted data.</returns>
-	public static ChatRequest ToOllamaSharpChatRequest(IList<ChatMessage> chatMessages, ChatOptions? options, bool stream, JsonSerializerOptions serializerOptions)
+	public static ChatRequest ToOllamaSharpChatRequest(IEnumerable<ChatMessage> messages, ChatOptions? options, bool stream, JsonSerializerOptions serializerOptions)
 	{
 		object? format = null;
 
@@ -59,7 +58,7 @@ public static ChatRequest ToOllamaSharpChatRequest(IList<ChatMessage> chatMessag
 		{
 			Format = format,
 			KeepAlive = null,
-			Messages = ToOllamaSharpMessages(chatMessages, serializerOptions),
+			Messages = ToOllamaSharpMessages(messages, serializerOptions),
 			Model = options?.ModelId ?? string.Empty, // will be set OllamaApiClient.SelectedModel if not set
 			Options = new RequestOptions
 			{
@@ -198,18 +197,18 @@ private static string ToFunctionTypeString(JsonObject? schema)
 	/// <summary>
 	/// Converts a list of Microsoft.Extensions.AI.<see cref="ChatMessage"/> to a list of Ollama <see cref="Message"/>.
 	/// </summary>
-	/// <param name="chatMessages">The chat messages to convert.</param>
+	/// <param name="messages">The chat messages to convert.</param>
 	/// <param name="serializerOptions">Serializer options</param>
 	/// <returns>An enumeration of <see cref="Message"/> objects containing the converted data.</returns>
-	private static IEnumerable<Message> ToOllamaSharpMessages(IList<ChatMessage> chatMessages, JsonSerializerOptions serializerOptions)
+	private static IEnumerable<Message> ToOllamaSharpMessages(IEnumerable<ChatMessage> messages, JsonSerializerOptions serializerOptions)
 	{
-		foreach (var cm in chatMessages)
+		foreach (var cm in messages)
 		{
-			var images = cm.Contents.OfType<DataContent>().Where(dc => dc.MediaType is null || dc.MediaTypeStartsWith("image/")).Select(ToOllamaImage).Where(s => !string.IsNullOrEmpty(s)).ToArray();
+			var images = cm.Contents.OfType<DataContent>().Where(dc => dc.HasTopLevelMediaType("image")).Select(ToOllamaImage).ToArray();
 			var toolCalls = cm.Contents.OfType<FunctionCallContent>().Select(ToOllamaSharpToolCall).ToArray();
 
 			// Only generates a message if there is text/content, images or tool calls
-			if (cm.Text is not null || images.Length > 0 || toolCalls.Length > 0)
+			if (cm.Text is { Length: > 0 } || images.Length > 0 || toolCalls.Length > 0)
 			{
 				yield return new Message
 				{
@@ -243,15 +242,9 @@ private static IEnumerable<Message> ToOllamaSharpMessages(IList<ChatMessage> cha
 	/// </summary>
 	/// <param name="content">The data content to convert.</param>
 	/// <returns>A string containing the base64 image data.</returns>
-	private static string ToOllamaImage(DataContent? content)
+	private static string ToOllamaImage(DataContent content)
 	{
-		if (content is null)
-			return string.Empty;
-
-		if (content.Data.HasValue)
-			return Convert.ToBase64String(content.Data.Value.ToArray());
-
-		throw new NotSupportedException("Images have to be provided as content (byte-Array or base64-string) for Ollama to be used. Other image sources like links are not supported.");
+		return Convert.ToBase64String(content.Data.ToArray());
 	}
 
 	/// <summary>
@@ -312,20 +305,18 @@ private static Microsoft.Extensions.AI.ChatRole ToAbstractionRole(ChatRole? role
 	/// Converts a <see cref="ChatResponseStream"/> to a <see cref="ChatResponseUpdate"/>.
 	/// </summary>
 	/// <param name="response">The response stream to convert.</param>
+	/// <param name="responseId">The response ID to store onto the created update.</param>
 	/// <returns>A <see cref="ChatResponseUpdate"/> object containing the latest chat completion chunk.</returns>
-	public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? response)
+	public static ChatResponseUpdate ToChatResponseUpdate(ChatResponseStream? response, string responseId)
 	{
-		return new ChatResponseUpdate
+		// TODO: Check if "Message" can ever actually be null. If not, remove the null-coalescing operator
+		return new(ToAbstractionRole(response?.Message?.Role), response?.Message?.Content ?? string.Empty)
 		{
 			// no need to set "Contents" as we set the text
-			ChoiceIndex = 0, // should be left at 0 as Ollama does not support this
 			CreatedAt = response?.CreatedAt,
 			FinishReason = response?.Done == true ? ChatFinishReason.Stop : null,
 			RawRepresentation = response,
-			ResponseId = response?.CreatedAtString,
-			// TODO: Check if "Message" can ever actually be null. If not, remove the null-coalescing operator
-			Text = response?.Message?.Content ?? string.Empty,
-			Role = ToAbstractionRole(response?.Message?.Role),
+			ResponseId = responseId,
 			ModelId = response?.Model
 		};
 	}
diff --git a/src/OllamaSharp/MicrosoftAi/ChatResponseUpdateBuilder.cs b/src/OllamaSharp/MicrosoftAi/ChatResponseUpdateBuilder.cs
index 58dc536..1166618 100644
--- a/src/OllamaSharp/MicrosoftAi/ChatResponseUpdateBuilder.cs
+++ b/src/OllamaSharp/MicrosoftAi/ChatResponseUpdateBuilder.cs
@@ -1,4 +1,3 @@
-using System.Text;
 using Microsoft.Extensions.AI;
 
 namespace OllamaSharp.MicrosoftAi;
@@ -8,7 +7,6 @@ namespace OllamaSharp.MicrosoftAi;
 /// </summary>
 internal class ChatResponseUpdateBuilder
 {
-	private readonly StringBuilder _contentBuilder = new();
 	private ChatResponseUpdate? _first;
 
 	/// <summary>
@@ -20,19 +18,16 @@ public void Append(ChatResponseUpdate? update)
 		if (update is null)
 			return;
 
-		_contentBuilder.Append(update.Text ?? "");
-
 		_first ??= update;
 
 		_first.AdditionalProperties = update.AdditionalProperties;
 		_first.AuthorName = update.AuthorName;
-		_first.ChoiceIndex = update.ChoiceIndex;
 		_first.CreatedAt = update.CreatedAt;
 		_first.FinishReason = update.FinishReason;
 		_first.ResponseId = update.ResponseId;
 		_first.Role = update.Role;
 
-		//_first.Contents and .Text will be set in Complete() with values collected from each update
+		//_first.Contents will be set in Complete() with values collected from each update
 		//_first.RawRepresentation makes no sense 
 
 		// TODO: Check if this can ever be null. The docs imply not.
@@ -50,7 +45,6 @@ public void Append(ChatResponseUpdate? update)
 		if (_first is null)
 			return null;
 
-		_first.Text = _contentBuilder.ToString();
 		_first.Contents = Contents;
 
 		return _first;
diff --git a/src/OllamaSharp/OllamaApiClient.cs b/src/OllamaSharp/OllamaApiClient.cs
index c4e9754..ae10025 100644
--- a/src/OllamaSharp/OllamaApiClient.cs
+++ b/src/OllamaSharp/OllamaApiClient.cs
@@ -399,19 +399,21 @@ private async Task EnsureSuccessStatusCodeAsync(HttpResponseMessage response)
 	private EmbeddingGeneratorMetadata? _embeddingGeneratorMetadata;
 
 	/// <inheritdoc/>
-	async Task<ChatResponse> IChatClient.GetResponseAsync(IList<ChatMessage> chatMessages, ChatOptions? options, CancellationToken cancellationToken)
+	async Task<ChatResponse> IChatClient.GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options, CancellationToken cancellationToken)
 	{
-		var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: false, OutgoingJsonSerializerOptions);
+		var request = AbstractionMapper.ToOllamaSharpChatRequest(messages, options, stream: false, OutgoingJsonSerializerOptions);
 		var response = await ChatAsync(request, cancellationToken).StreamToEndAsync().ConfigureAwait(false);
 		return AbstractionMapper.ToChatResponse(response, response?.Model ?? request.Model ?? SelectedModel) ?? new ChatResponse([]);
 	}
 
 	/// <inheritdoc/>
-	async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(IList<ChatMessage> chatMessages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
+	async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
 	{
-		var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true, OutgoingJsonSerializerOptions);
+		var request = AbstractionMapper.ToOllamaSharpChatRequest(messages, options, stream: true, OutgoingJsonSerializerOptions);
+
+		string responseId = Guid.NewGuid().ToString("N");
 		await foreach (var response in ChatAsync(request, cancellationToken).ConfigureAwait(false))
-			yield return AbstractionMapper.ToChatResponseUpdate(response);
+			yield return AbstractionMapper.ToChatResponseUpdate(response, responseId);
 	}
 
 	/// <inheritdoc/>
@@ -430,7 +432,7 @@ async Task<GeneratedEmbeddings<Embedding<float>>> IEmbeddingGenerator<string, Em
 		null;
 
 	/// <inheritdoc />
-	object? IEmbeddingGenerator<string, Embedding<float>>.GetService(Type serviceKey, object? key) =>
+	object? IEmbeddingGenerator.GetService(Type serviceKey, object? key) =>
 		key is not null ? null :
 		serviceKey == typeof(EmbeddingGeneratorMetadata) ? (_embeddingGeneratorMetadata = new(Application.Ollama, Uri, SelectedModel)) :
 		serviceKey?.IsInstanceOfType(this) is true ? this :
diff --git a/src/OllamaSharp/OllamaSharp.csproj b/src/OllamaSharp/OllamaSharp.csproj
index 6fe9b08..5b2995e 100644
--- a/src/OllamaSharp/OllamaSharp.csproj
+++ b/src/OllamaSharp/OllamaSharp.csproj
@@ -43,7 +43,7 @@
 
 	<ItemGroup>
 		<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
-		<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
+		<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25161.3" />
 	</ItemGroup>
 
 	<ItemGroup>
diff --git a/test/AbstractionMapperTests.cs b/test/AbstractionMapperTests.cs
index d5adb20..4ee0282 100644
--- a/test/AbstractionMapperTests.cs
+++ b/test/AbstractionMapperTests.cs
@@ -22,8 +22,8 @@ public void Maps_Partial_Options_Class()
 		{
 			var messages = new List<ChatMessage>
 			{
-				new() { Role = Microsoft.Extensions.AI.ChatRole.Assistant, Text = "A" },
-				new() { Role = Microsoft.Extensions.AI.ChatRole.User, Text = "B" },
+				new(Microsoft.Extensions.AI.ChatRole.Assistant, "A"),
+				new(Microsoft.Extensions.AI.ChatRole.User, "B"),
 			};
 
 			var options = new ChatOptions { Temperature = 0.5f, /* other properties are left out */ };
@@ -68,32 +68,26 @@ public void Maps_Messages()
 		{
 			var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
 			{
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.Assistant, "Hi there.")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a1",
 					Contents = [new TextContent("Hi there.")],
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.Assistant,
-					Text = "Hi there."
 				},
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.User, "What is 3 + 4?")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a2",
 					Contents = [new TextContent("What is 3 + 4?")],
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.User,
-					Text = "What is 3 + 4?"
 				},
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.Assistant, "3 + 4 is 7")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a1",
 					Contents = [new TextContent("3 + 4 is 7")],
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.Assistant,
-					Text = "3 + 4 is 7"
 				},
 			};
 
@@ -176,7 +170,7 @@ public void Maps_Byte_Array_Images()
 					AuthorName = "a1",
 					Contents = [
 						new TextContent("Make me an image like this, but with beer."),
-						new DataContent(bytes)],
+						new DataContent(bytes, "image/png")],
 					RawRepresentation = null,
 					Role = Microsoft.Extensions.AI.ChatRole.User
 				}
@@ -191,7 +185,7 @@ public void Maps_Byte_Array_Images()
 		/// Links to images are not supported
 		/// </summary>
 		[Test]
-		public void Does_Not_Support_Image_Links()
+		public void Ignores_UriContent()
 		{
 			var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
 			{
@@ -201,7 +195,7 @@ public void Does_Not_Support_Image_Links()
 					AuthorName = "a1",
 					Contents = [
 						new TextContent("Make me an image like this, but with beer."),
-						new DataContent("https://unsplash.com/sunset.png")],
+						new UriContent("https://unsplash.com/sunset.png", "image/png")],
 					RawRepresentation = null,
 					Role = Microsoft.Extensions.AI.ChatRole.User
 				}
@@ -213,7 +207,7 @@ public void Does_Not_Support_Image_Links()
 				request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
 			};
 
-			act.Should().Throw<NotSupportedException>().Which.Message.Should().Contain("Images have to be provided as content");
+			act.Should().NotThrow();
 		}
 
 		[Test]
@@ -221,13 +215,11 @@ public void Maps_Messages_With_Tools()
 		{
 			var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
 			{
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.User, "What's the weather in Honululu?")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a1",
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.User,
-					Text = "What's the weather in Honululu?"
 				}
 			};
 
@@ -360,13 +352,11 @@ public void Maps_Messages_With_IEnumerable_StopSequences(object? enumerable)
 		{
 			var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
 			{
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.User, "What's the weather in Honululu?")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a1",
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.User,
-					Text = "What's the weather in Honululu?"
 				}
 			};
 
@@ -407,13 +397,11 @@ public void Maps_Messages_With_ToolResponse()
 		{
 			var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
 			{
-				new()
+				new(Microsoft.Extensions.AI.ChatRole.Tool, "The weather in Honolulu is 25°C.")
 				{
 					AdditionalProperties = [],
 					AuthorName = "a1",
 					RawRepresentation = null,
-					Role = Microsoft.Extensions.AI.ChatRole.Tool,
-					Text = "The weather in Honolulu is 25°C."
 				}
 			};
 
@@ -696,29 +684,27 @@ public void Maps_Known_Properties()
 				TotalDuration = 6666666666
 			};
 
-			var chatCompletion = AbstractionMapper.ToChatResponse(stream, usedModel: null);
-
-			chatCompletion.AdditionalProperties.Should().NotBeNull();
-			chatCompletion.AdditionalProperties[Application.EvalDuration].Should().Be(TimeSpan.FromSeconds(2.222222222));
-			chatCompletion.AdditionalProperties[Application.LoadDuration].Should().Be(TimeSpan.FromSeconds(3.333333333));
-			chatCompletion.AdditionalProperties[Application.TotalDuration].Should().Be(TimeSpan.FromSeconds(6.666666666));
-			chatCompletion.AdditionalProperties[Application.PromptEvalDuration].Should().Be(TimeSpan.FromSeconds(5.555555555));
-			chatCompletion.Choices.Should().HaveCount(1);
-			chatCompletion.Choices.Single().Text.Should().Be("Hi.");
-			chatCompletion.CreatedAt.Should().Be(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
-			chatCompletion.FinishReason.Should().Be(ChatFinishReason.Stop);
-			chatCompletion.Message.AuthorName.Should().BeNull();
-			chatCompletion.Message.RawRepresentation.Should().Be(stream.Message);
-			chatCompletion.Message.Role.Should().Be(Microsoft.Extensions.AI.ChatRole.Assistant);
-			chatCompletion.Message.Text.Should().Be("Hi.");
-			chatCompletion.Message.Contents.Should().BeEquivalentTo([new TextContent("Hi.")]);
-			chatCompletion.ModelId.Should().Be("llama3.1:8b");
-			chatCompletion.RawRepresentation.Should().Be(stream);
-			chatCompletion.ResponseId.Should().Be(ollamaCreatedStamp);
-			chatCompletion.Usage.Should().NotBeNull();
-			chatCompletion.Usage.InputTokenCount.Should().Be(411);
-			chatCompletion.Usage.OutputTokenCount.Should().Be(111);
-			chatCompletion.Usage.TotalTokenCount.Should().Be(111 + 411);
+			var response = AbstractionMapper.ToChatResponse(stream, usedModel: null);
+
+			response.AdditionalProperties.Should().NotBeNull();
+			response.AdditionalProperties[Application.EvalDuration].Should().Be(TimeSpan.FromSeconds(2.222222222));
+			response.AdditionalProperties[Application.LoadDuration].Should().Be(TimeSpan.FromSeconds(3.333333333));
+			response.AdditionalProperties[Application.TotalDuration].Should().Be(TimeSpan.FromSeconds(6.666666666));
+			response.AdditionalProperties[Application.PromptEvalDuration].Should().Be(TimeSpan.FromSeconds(5.555555555));
+			response.CreatedAt.Should().Be(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
+			response.FinishReason.Should().Be(ChatFinishReason.Stop);
+			response.Messages[0].AuthorName.Should().BeNull();
+			response.Messages[0].RawRepresentation.Should().Be(stream.Message);
+			response.Messages[0].Role.Should().Be(Microsoft.Extensions.AI.ChatRole.Assistant);
+			response.Messages[0].Text.Should().Be("Hi.");
+			response.Messages[0].Contents.Should().BeEquivalentTo([new TextContent("Hi.")]);
+			response.ModelId.Should().Be("llama3.1:8b");
+			response.RawRepresentation.Should().Be(stream);
+			response.ResponseId.Should().Be(ollamaCreatedStamp);
+			response.Usage.Should().NotBeNull();
+			response.Usage.InputTokenCount.Should().Be(411);
+			response.Usage.OutputTokenCount.Should().Be(111);
+			response.Usage.TotalTokenCount.Should().Be(111 + 411);
 		}
 	}
 
@@ -728,27 +714,24 @@ public class ToChatResponseUpdateMethod : AbstractionMapperTests
 		public void Maps_Known_Properties()
 		{
 			var ollamaCreated = new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7));
-			var ollamaCreatedStamp = "2023-08-04T08:52:19.385406-07:00";
 
 			var stream = new ChatResponseStream
 			{
 				CreatedAt = ollamaCreated,
-				CreatedAtString = ollamaCreatedStamp,
 				Done = true,
 				Message = new Message { Role = OllamaSharp.Models.Chat.ChatRole.Assistant, Content = "Hi." },
 				Model = "llama3.1:8b"
 			};
 
-			var streamingChatCompletion = AbstractionMapper.ToChatResponseUpdate(stream);
+			var streamingChatCompletion = AbstractionMapper.ToChatResponseUpdate(stream, "12345");
 
 			streamingChatCompletion.AdditionalProperties.Should().BeNull();
 			streamingChatCompletion.AuthorName.Should().BeNull();
-			streamingChatCompletion.ChoiceIndex.Should().Be(0);
 			streamingChatCompletion.Contents.Should().BeEquivalentTo([new TextContent("Hi.")]);
 			streamingChatCompletion.CreatedAt.Should().Be(new DateTimeOffset(2023, 08, 04, 08, 52, 19, 385, 406, TimeSpan.FromHours(-7)));
 			streamingChatCompletion.FinishReason.Should().Be(ChatFinishReason.Stop);
 			streamingChatCompletion.RawRepresentation.Should().Be(stream);
-			streamingChatCompletion.ResponseId.Should().Be(ollamaCreatedStamp);
+			streamingChatCompletion.ResponseId.Should().Be("12345");
 			streamingChatCompletion.Role.Should().Be(Microsoft.Extensions.AI.ChatRole.Assistant);
 			streamingChatCompletion.Text.Should().Be("Hi.");
 		}
diff --git a/test/Tests.csproj b/test/Tests.csproj
index 2210de2..bae2fa2 100644
--- a/test/Tests.csproj
+++ b/test/Tests.csproj
@@ -26,7 +26,7 @@
 			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 		</PackageReference>
 		<PackageReference Include="System.Linq.Async" Version="6.0.1" />
-		<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
+		<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25161.3" />
 	</ItemGroup>
 
 	<ItemGroup>


Reviews:
user: login: awaescher
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-13T17:01:01Z
user: login: stephentoub
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:13:19Z
user: login: stephentoub
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:19:38Z
user: login: awaescher
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-13T17:43:04Z
user: login: awaescher
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-13T17:43:33Z
user: login: stephentoub
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:43:58Z
user: login: stephentoub
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:45:09Z

Review comments:
path: src/OllamaSharp/MicrosoftAi/ChatResponseUpdateAppender.cs
user: login: awaescher
body: Please don't remove `ChatResponseUpdateAppender`, `ChatResponseUpdateBuilder` and `IAsyncEnumerableExtensions`. These are not used in this solution but provide an extension method `StreamToEndAsync()` to `IAsyncEnumerable.`
created_at: 2025-03-12T06:35:50Z
author_association: OWNER
diff_hunk: @@ -213,21 +207,19 @@ public void Does_Not_Support_Image_Links()
 				request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
 			};
 
-			act.Should().Throw<NotSupportedException>().Which.Message.Should().Contain("Images have to be provided as content");
+			act.Should().NotThrow();
path: test/AbstractionMapperTests.cs
user: login: awaescher
body: I see the behavior was changed to not throw anymore but the test is still named `Does_Not_Support_Image_Links()` and the summary is also telling something different.

As far as I know, Ollama still needs images as base64, why should we remove the exception?
created_at: 2025-03-12T06:37:42Z
author_association: OWNER
path: src/OllamaSharp/MicrosoftAi/ChatResponseUpdateAppender.cs
user: login: stephentoub
body: I can certainly put it back if you still find value in it, but...

Is that still necessary even though there's a built-in ToChatResponseAsync extension that takes an `IAsyncEnumerable<ChatResponseUpdate>` and produces a `ChatResponse`:
https://github.com/dotnet/extensions/blob/e351c2506f2af13be2645fbd554c89fb9bd32abf/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs#L132-L133
as well as a built-in AddMessagesAsync extension that takes an `IAsyncEnumerable<ChatResponseUpdate>` and adds corresponding messages into an `IList<ChatMessage>`:
https://github.com/dotnet/extensions/blob/e351c2506f2af13be2645fbd554c89fb9bd32abf/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs#L81-L82
?
created_at: 2025-03-13T17:13:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,21 +207,19 @@ public void Does_Not_Support_Image_Links()
 				request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
 			};
 
-			act.Should().Throw<NotSupportedException>().Which.Message.Should().Contain("Images have to be provided as content");
+			act.Should().NotThrow();
path: test/AbstractionMapperTests.cs
user: login: stephentoub
body: DataContent is now exclusively about in-memory data, so the Uris it produces are always data URIs containing base64-encoded content.

UriContent is now used for possibly/likely-remote urls.
created_at: 2025-03-13T17:19:37Z
author_association: CONTRIBUTOR
path: src/OllamaSharp/MicrosoftAi/ChatResponseUpdateAppender.cs
user: login: awaescher
body: I was not aware of these method but yes, please put these classes back. I had the issue lately that someone was not able to use IAsyncEnumerable but the extension methods provided.
created_at: 2025-03-13T17:43:04Z
author_association: OWNER
diff_hunk: @@ -213,21 +207,19 @@ public void Does_Not_Support_Image_Links()
 				request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
 			};
 
-			act.Should().Throw<NotSupportedException>().Which.Message.Should().Contain("Images have to be provided as content");
+			act.Should().NotThrow();
path: test/AbstractionMapperTests.cs
user: login: awaescher
body: Ah okay, but I would suggest changing the test name and summary accordingly, what do you think?
created_at: 2025-03-13T17:43:33Z
author_association: OWNER
diff_hunk: @@ -213,21 +207,19 @@ public void Does_Not_Support_Image_Links()
 				request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
 			};
 
-			act.Should().Throw<NotSupportedException>().Which.Message.Should().Contain("Images have to be provided as content");
+			act.Should().NotThrow();
path: test/AbstractionMapperTests.cs
user: login: stephentoub
body: Yup
created_at: 2025-03-13T17:43:58Z
author_association: CONTRIBUTOR
path: src/OllamaSharp/MicrosoftAi/ChatResponseUpdateAppender.cs
user: login: stephentoub
body: I'll put them back. I would be interested in seeing that issue, though, to see if there's something the built-in methods don't handle that should be addressed in the abstractions library. In general I don't want folks having to write code like that in StreamToEndAsync that does their own manual aggregation.
created_at: 2025-03-13T17:45:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: stephentoub
created_at: 2025-03-13T12:43:00Z
body: @awaescher, any chance we could get this merged and a new build produced? The template discussed at https://devblogs.microsoft.com/dotnet/announcing-dotnet-ai-template-preview1/ uses OllamaSharp and I'd like for it to be able to update to the latest M.E.AI preview. Thanks!
author_association: CONTRIBUTOR
user: login: awaescher
created_at: 2025-03-13T15:42:03Z
body: I mean usually I'd wait for the contributor to work on the comments. Merging and building is a matter a few minutes.
author_association: OWNER
user: login: stephentoub
created_at: 2025-03-13T16:23:37Z
body: > I mean usually I'd wait for the contributor to work on the comments.

Sorry, what comments? On the PR? I don't see any. I'm happy to fix things if there are things to be fixed.
author_association: CONTRIBUTOR
user: login: awaescher
created_at: 2025-03-13T17:02:10Z
body: Oh wow my bad. I had to finish the review first. 🙈
author_association: OWNER
user: login: stephentoub
created_at: 2025-03-13T18:11:42Z
body: Thanks. I reverted the removals, fixed them up, and renamed the test.
author_association: CONTRIBUTOR
user: login: awaescher
created_at: 2025-03-13T18:18:11Z
body: Awesome, thanks for your help!
author_association: OWNER
user: login: stephentoub
created_at: 2025-03-13T18:21:27Z
body: Thank you :)
author_association: CONTRIBUTOR