diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props
index dee583f7e39..23b183e6484 100644
--- a/eng/MSBuild/Shared.props
+++ b/eng/MSBuild/Shared.props
@@ -14,7 +14,7 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
index 29fd405b947..6895d4c1e42 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
@@ -7,11 +7,9 @@ namespace Microsoft.Extensions.AI;
/// Provides a base class for all content used with AI services.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
-[JsonDerivedType(typeof(AudioContent), typeDiscriminator: "audio")]
[JsonDerivedType(typeof(DataContent), typeDiscriminator: "data")]
[JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: "functionCall")]
[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")]
-[JsonDerivedType(typeof(ImageContent), typeDiscriminator: "image")]
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
[JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")]
public class AIContent
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AudioContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AudioContent.cs
deleted file mode 100644
index 356cce78413..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AudioContent.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents audio content.
-///
-public class AudioContent : DataContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The URI of the content. This can be a data URI.
- /// The media type (also known as MIME type) represented by the content.
- public AudioContent(Uri uri, string? mediaType = null)
- : base(uri, mediaType)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The URI of the content. This can be a data URI.
- /// The media type (also known as MIME type) represented by the content.
- [JsonConstructor]
- public AudioContent([StringSyntax(StringSyntaxAttribute.Uri)] string uri, string? mediaType = null)
- : base(uri, mediaType)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The byte contents.
- /// The media type (also known as MIME type) represented by the content.
- public AudioContent(ReadOnlyMemory data, string? mediaType = null)
- : base(data, mediaType)
- {
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
index 39d610a6dcb..1e158a8801b 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
@@ -105,6 +105,14 @@ public DataContent(ReadOnlyMemory data, string? mediaType = null)
_data = data;
}
+ ///
+ /// Determines whether the has the specified prefix.
+ ///
+ /// The media type prefix.
+ /// if the has the specified prefix, otherwise .
+ public bool MediaTypeStartsWith(string prefix)
+ => MediaType?.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) is true;
+
/// Sets to null if it's empty or composed entirely of whitespace.
private static void ValidateMediaType(ref string? mediaType)
{
@@ -172,6 +180,7 @@ public string Uri
/// as the instance actually contains all of the data it represents. If, however, the instance was constructed from another form of URI, one
/// that simply references where the data can be found but doesn't actually contain the data, this property returns .
///
+ [MemberNotNullWhen(true, nameof(Data))]
[JsonIgnore]
public bool ContainsData => _dataUri is not null || _data is not null;
@@ -180,7 +189,6 @@ public string Uri
/// If is , this property returns the represented data.
/// If is , this property returns .
///
- [MemberNotNullWhen(true, nameof(ContainsData))]
[JsonIgnore]
public ReadOnlyMemory? Data
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageContent.cs
deleted file mode 100644
index df559152412..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageContent.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents image content.
-///
-public class ImageContent : DataContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The URI of the content. This can be a data URI.
- /// The media type (also known as MIME type) represented by the content.
- public ImageContent(Uri uri, string? mediaType = null)
- : base(uri, mediaType)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The URI of the content. This can be a data URI.
- /// The media type (also known as MIME type) represented by the content.
- [JsonConstructor]
- public ImageContent([StringSyntax(StringSyntaxAttribute.Uri)] string uri, string? mediaType = null)
- : base(uri, mediaType)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The byte contents.
- /// The media type (also known as MIME type) represented by the content.
- public ImageContent(ReadOnlyMemory data, string? mediaType = null)
- : base(data, mediaType)
- {
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
index 1ec1225a9dc..85903beb701 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
@@ -484,12 +484,16 @@ private static List GetContentParts(IList con
parts.Add(new ChatMessageTextContentItem(textContent.Text));
break;
- case ImageContent imageContent when imageContent.Data is { IsEmpty: false } data:
- parts.Add(new ChatMessageImageContentItem(BinaryData.FromBytes(data), imageContent.MediaType));
- break;
+ case DataContent dataContent when dataContent.MediaTypeStartsWith("image/"):
+ if (dataContent.ContainsData)
+ {
+ parts.Add(new ChatMessageImageContentItem(BinaryData.FromBytes(dataContent.Data.Value), dataContent.MediaType));
+ }
+ else if (dataContent.Uri is string uri)
+ {
+ parts.Add(new ChatMessageImageContentItem(new Uri(uri)));
+ }
- case ImageContent imageContent when imageContent.Uri is string uri:
- parts.Add(new ChatMessageImageContentItem(new Uri(uri)));
break;
}
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj
index 919fa9b751f..b3f00b85912 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj
@@ -26,7 +26,7 @@
true
true
-
+
@@ -37,5 +37,5 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj
index f80630ceeb5..bd7e8d27201 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/Microsoft.Extensions.AI.Ollama.csproj
@@ -40,5 +40,5 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs
index 6bdd924b87a..46cbb7d7d11 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs
@@ -386,74 +386,76 @@ private IEnumerable ToOllamaChatRequestMessages(ChatMe
OllamaChatRequestMessage? currentTextMessage = null;
foreach (var item in content.Contents)
{
- if (currentTextMessage is not null && item is not ImageContent)
+ if (item is DataContent { ContainsData: true } dataContent && dataContent.MediaTypeStartsWith("image/"))
{
- yield return currentTextMessage;
- currentTextMessage = null;
- }
-
- switch (item)
- {
- case TextContent textContent:
- currentTextMessage = new OllamaChatRequestMessage
- {
- Role = content.Role.Value,
- Content = textContent.Text ?? string.Empty,
- };
- break;
-
- case ImageContent imageContent when imageContent.Data is not null:
- IList images = currentTextMessage?.Images ?? [];
- images.Add(Convert.ToBase64String(imageContent.Data.Value
+ IList images = currentTextMessage?.Images ?? [];
+ images.Add(Convert.ToBase64String(dataContent.Data.Value
#if NET
- .Span));
+ .Span));
#else
- .ToArray()));
+ .ToArray()));
#endif
- if (currentTextMessage is not null)
- {
- currentTextMessage.Images = images;
- }
- else
+ if (currentTextMessage is not null)
+ {
+ currentTextMessage.Images = images;
+ }
+ else
+ {
+ yield return new OllamaChatRequestMessage
{
- yield return new OllamaChatRequestMessage
+ Role = content.Role.Value,
+ Images = images,
+ };
+ }
+ }
+ else
+ {
+ if (currentTextMessage is not null)
+ {
+ yield return currentTextMessage;
+ currentTextMessage = null;
+ }
+
+ switch (item)
+ {
+ case TextContent textContent:
+ currentTextMessage = new OllamaChatRequestMessage
{
Role = content.Role.Value,
- Images = images,
+ Content = textContent.Text ?? string.Empty,
};
- }
+ break;
- break;
-
- case FunctionCallContent fcc:
- {
- yield return new OllamaChatRequestMessage
+ case FunctionCallContent fcc:
{
- Role = "assistant",
- Content = JsonSerializer.Serialize(new OllamaFunctionCallContent
+ yield return new OllamaChatRequestMessage
{
- CallId = fcc.CallId,
- Name = fcc.Name,
- Arguments = JsonSerializer.SerializeToElement(fcc.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary))),
- }, JsonContext.Default.OllamaFunctionCallContent)
- };
- break;
- }
+ Role = "assistant",
+ Content = JsonSerializer.Serialize(new OllamaFunctionCallContent
+ {
+ CallId = fcc.CallId,
+ Name = fcc.Name,
+ Arguments = JsonSerializer.SerializeToElement(fcc.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary))),
+ }, JsonContext.Default.OllamaFunctionCallContent)
+ };
+ break;
+ }
- case FunctionResultContent frc:
- {
- JsonElement jsonResult = JsonSerializer.SerializeToElement(frc.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
- yield return new OllamaChatRequestMessage
+ case FunctionResultContent frc:
{
- Role = "tool",
- Content = JsonSerializer.Serialize(new OllamaFunctionResultContent
+ JsonElement jsonResult = JsonSerializer.SerializeToElement(frc.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
+ yield return new OllamaChatRequestMessage
{
- CallId = frc.CallId,
- Result = jsonResult,
- }, JsonContext.Default.OllamaFunctionResultContent)
- };
- break;
+ Role = "tool",
+ Content = JsonSerializer.Serialize(new OllamaFunctionResultContent
+ {
+ CallId = frc.CallId,
+ Result = jsonResult,
+ }, JsonContext.Default.OllamaFunctionResultContent)
+ };
+ break;
+ }
}
}
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
index a6d3b013c0d..34541c55783 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
@@ -38,5 +38,5 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs
index 239e2add074..0cc75c5f34f 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs
@@ -536,10 +536,10 @@ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) =>
}
else if (contentPart.Kind == ChatMessageContentPartKind.Image)
{
- ImageContent? imageContent;
+ DataContent? imageContent;
aiContent = imageContent =
- contentPart.ImageUri is not null ? new ImageContent(contentPart.ImageUri, contentPart.ImageBytesMediaType) :
- contentPart.ImageBytes is not null ? new ImageContent(contentPart.ImageBytes.ToMemory(), contentPart.ImageBytesMediaType) :
+ contentPart.ImageUri is not null ? new DataContent(contentPart.ImageUri, contentPart.ImageBytesMediaType) :
+ contentPart.ImageBytes is not null ? new DataContent(contentPart.ImageBytes.ToMemory(), contentPart.ImageBytesMediaType) :
null;
if (imageContent is not null && contentPart.ImageDetailLevel?.ToString() is string detail)
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs
index e8193df24d5..96a71b83650 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatMessage.cs
@@ -185,7 +185,7 @@ public static IEnumerable FromOpenAIChatMessages(IEnumerable FromOpenAIChatContent(IList openAiMessageContentParts)
{
- List contents = new();
+ List contents = [];
foreach (var openAiContentPart in openAiMessageContentParts)
{
switch (openAiContentPart.Kind)
@@ -194,14 +194,13 @@ private static List FromOpenAIChatContent(IList ToOpenAIChatContent(IList
parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text));
break;
- case ImageContent imageContent when imageContent.Data is { IsEmpty: false } data:
- parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(data), imageContent.MediaType));
- break;
+ case DataContent dataContent when dataContent.MediaTypeStartsWith("image/"):
+ if (dataContent.ContainsData)
+ {
+ parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data.Value), dataContent.MediaType));
+ }
+ else if (dataContent.Uri is string uri)
+ {
+ parts.Add(ChatMessageContentPart.CreateImagePart(new Uri(uri)));
+ }
- case ImageContent imageContent when imageContent.Uri is string uri:
- parts.Add(ChatMessageContentPart.CreateImagePart(new Uri(uri)));
break;
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatCompletionTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatCompletionTests.cs
index 15134782bd7..7ff4d781c98 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatCompletionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatCompletionTests.cs
@@ -239,7 +239,7 @@ public void ToStreamingChatCompletionUpdates_MultiChoice()
new ChatMessage(ChatRole.Assistant,
[
new TextContent("Hello, "),
- new ImageContent("http://localhost/image.png"),
+ new DataContent("http://localhost/image.png", mediaType: "image/png"),
new TextContent("world!"),
])
{
@@ -275,7 +275,7 @@ public void ToStreamingChatCompletionUpdates_MultiChoice()
Assert.Equal(new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt);
Assert.Equal("assistant", update0.Role?.Value);
Assert.Equal("Hello, ", Assert.IsType(update0.Contents[0]).Text);
- Assert.IsType(update0.Contents[1]);
+ Assert.Equal("image/png", Assert.IsType(update0.Contents[1]).MediaType);
Assert.Equal("world!", Assert.IsType(update0.Contents[2]).Text);
Assert.Equal("choice1Value", update0.AdditionalProperties?["choice1Key"]);
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 d1325b89bb7..17ccb373e75 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatMessageTests.cs
@@ -124,8 +124,8 @@ public void Text_GetSet_UsesFirstTextContent()
{
ChatMessage message = new(ChatRole.User,
[
- new AudioContent("http://localhost/audio"),
- new ImageContent("http://localhost/image"),
+ new DataContent("http://localhost/audio"),
+ new DataContent("http://localhost/image"),
new FunctionCallContent("callId1", "fc1"),
new TextContent("text-1"),
new TextContent("text-2"),
@@ -163,8 +163,8 @@ public void Text_Set_AddsTextMessageToListWithNoText()
{
ChatMessage message = new(ChatRole.User,
[
- new AudioContent("http://localhost/audio"),
- new ImageContent("http://localhost/image"),
+ new DataContent("http://localhost/audio"),
+ new DataContent("http://localhost/image"),
new FunctionCallContent("callId1", "fc1"),
]);
Assert.Equal(3, message.Contents.Count);
@@ -265,7 +265,7 @@ public void ItCanBeSerializeAndDeserialized()
{
AdditionalProperties = new() { ["metadata-key-1"] = "metadata-value-1" }
},
- new ImageContent(new Uri("https://fake-random-test-host:123"), "mime-type/2")
+ new DataContent(new Uri("https://fake-random-test-host:123"), "mime-type/2")
{
AdditionalProperties = new() { ["metadata-key-2"] = "metadata-value-2" }
},
@@ -273,18 +273,10 @@ public void ItCanBeSerializeAndDeserialized()
{
AdditionalProperties = new() { ["metadata-key-3"] = "metadata-value-3" }
},
- new AudioContent(new BinaryData(new[] { 3, 2, 1 }, options: TestJsonSerializerContext.Default.Options), "mime-type/4")
+ new TextContent("content-4")
{
AdditionalProperties = new() { ["metadata-key-4"] = "metadata-value-4" }
},
- new ImageContent(new BinaryData(new[] { 2, 1, 3 }, options: TestJsonSerializerContext.Default.Options), "mime-type/5")
- {
- AdditionalProperties = new() { ["metadata-key-5"] = "metadata-value-5" }
- },
- new TextContent("content-6")
- {
- AdditionalProperties = new() { ["metadata-key-6"] = "metadata-value-6" }
- },
new FunctionCallContent("function-id", "plugin-name-function-name", new Dictionary { ["parameter"] = "argument" }),
new FunctionResultContent("function-id", "plugin-name-function-name", "function-result"),
];
@@ -292,7 +284,7 @@ public void ItCanBeSerializeAndDeserialized()
// Act
var chatMessageJson = JsonSerializer.Serialize(new ChatMessage(ChatRole.User, contents: items)
{
- Text = "content-1-override", // Override the content of the first text content item that has the "content-1" content
+ Text = "content-1-override", // Override the content of the first text content item that has the "content-1" content
AuthorName = "Fred",
AdditionalProperties = new() { ["message-metadata-key-1"] = "message-metadata-value-1" },
}, TestJsonSerializerContext.Default.Options);
@@ -316,15 +308,15 @@ public void ItCanBeSerializeAndDeserialized()
Assert.Single(textContent.AdditionalProperties);
Assert.Equal("metadata-value-1", textContent.AdditionalProperties["metadata-key-1"]?.ToString());
- var imageContent = deserializedMessage.Contents[1] as ImageContent;
- Assert.NotNull(imageContent);
- Assert.Equal("https://fake-random-test-host:123/", imageContent.Uri);
- Assert.Equal("mime-type/2", imageContent.MediaType);
- Assert.NotNull(imageContent.AdditionalProperties);
- Assert.Single(imageContent.AdditionalProperties);
- Assert.Equal("metadata-value-2", imageContent.AdditionalProperties["metadata-key-2"]?.ToString());
+ var dataContent = deserializedMessage.Contents[1] as DataContent;
+ Assert.NotNull(dataContent);
+ Assert.Equal("https://fake-random-test-host:123/", dataContent.Uri);
+ Assert.Equal("mime-type/2", dataContent.MediaType);
+ Assert.NotNull(dataContent.AdditionalProperties);
+ Assert.Single(dataContent.AdditionalProperties);
+ Assert.Equal("metadata-value-2", dataContent.AdditionalProperties["metadata-key-2"]?.ToString());
- var dataContent = deserializedMessage.Contents[2] as DataContent;
+ dataContent = deserializedMessage.Contents[2] as DataContent;
Assert.NotNull(dataContent);
Assert.True(dataContent.Data!.Value.Span.SequenceEqual(new BinaryData(new[] { 1, 2, 3 }, TestJsonSerializerContext.Default.Options)));
Assert.Equal("mime-type/3", dataContent.MediaType);
@@ -332,30 +324,14 @@ public void ItCanBeSerializeAndDeserialized()
Assert.Single(dataContent.AdditionalProperties);
Assert.Equal("metadata-value-3", dataContent.AdditionalProperties["metadata-key-3"]?.ToString());
- var audioContent = deserializedMessage.Contents[3] as AudioContent;
- Assert.NotNull(audioContent);
- Assert.True(audioContent.Data!.Value.Span.SequenceEqual(new BinaryData(new[] { 3, 2, 1 }, TestJsonSerializerContext.Default.Options)));
- Assert.Equal("mime-type/4", audioContent.MediaType);
- Assert.NotNull(audioContent.AdditionalProperties);
- Assert.Single(audioContent.AdditionalProperties);
- Assert.Equal("metadata-value-4", audioContent.AdditionalProperties["metadata-key-4"]?.ToString());
-
- imageContent = deserializedMessage.Contents[4] as ImageContent;
- Assert.NotNull(imageContent);
- Assert.True(imageContent.Data?.Span.SequenceEqual(new BinaryData(new[] { 2, 1, 3 }, TestJsonSerializerContext.Default.Options)));
- Assert.Equal("mime-type/5", imageContent.MediaType);
- Assert.NotNull(imageContent.AdditionalProperties);
- Assert.Single(imageContent.AdditionalProperties);
- Assert.Equal("metadata-value-5", imageContent.AdditionalProperties["metadata-key-5"]?.ToString());
-
- textContent = deserializedMessage.Contents[5] as TextContent;
+ textContent = deserializedMessage.Contents[3] as TextContent;
Assert.NotNull(textContent);
- Assert.Equal("content-6", textContent.Text);
+ Assert.Equal("content-4", textContent.Text);
Assert.NotNull(textContent.AdditionalProperties);
Assert.Single(textContent.AdditionalProperties);
- Assert.Equal("metadata-value-6", textContent.AdditionalProperties["metadata-key-6"]?.ToString());
+ Assert.Equal("metadata-value-4", textContent.AdditionalProperties["metadata-key-4"]?.ToString());
- var functionCallContent = deserializedMessage.Contents[6] as FunctionCallContent;
+ var functionCallContent = deserializedMessage.Contents[4] as FunctionCallContent;
Assert.NotNull(functionCallContent);
Assert.Equal("plugin-name-function-name", functionCallContent.Name);
Assert.Equal("function-id", functionCallContent.CallId);
@@ -363,7 +339,7 @@ public void ItCanBeSerializeAndDeserialized()
Assert.Single(functionCallContent.Arguments);
Assert.Equal("argument", functionCallContent.Arguments["parameter"]?.ToString());
- var functionResultContent = deserializedMessage.Contents[7] as FunctionResultContent;
+ var functionResultContent = deserializedMessage.Contents[5] as FunctionResultContent;
Assert.NotNull(functionResultContent);
Assert.Equal("function-result", functionResultContent.Result?.ToString());
Assert.Equal("function-id", functionResultContent.CallId);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateExtensionsTests.cs
index 33eca7dcaae..9af25dbd16a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateExtensionsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateExtensionsTests.cs
@@ -171,7 +171,7 @@ void AddGap()
{
for (int i = 0; i < gapLength; i++)
{
- updates.Add(new() { Contents = [new ImageContent("https://uri")] });
+ updates.Add(new() { Contents = [new DataContent("https://uri", mediaType: "image/png")] });
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateTests.cs
index a54ca225a98..371d9c70bad 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/StreamingChatCompletionUpdateTests.cs
@@ -91,8 +91,8 @@ public void Text_GetSet_UsesFirstTextContent()
Role = ChatRole.User,
Contents =
[
- new AudioContent("http://localhost/audio"),
- new ImageContent("http://localhost/image"),
+ new DataContent("http://localhost/audio"),
+ new DataContent("http://localhost/image"),
new FunctionCallContent("callId1", "fc1"),
new TextContent("text-1"),
new TextContent("text-2"),
@@ -136,8 +136,8 @@ public void Text_Set_AddsTextMessageToListWithNoText()
{
Contents =
[
- new AudioContent("http://localhost/audio"),
- new ImageContent("http://localhost/image"),
+ new DataContent("http://localhost/audio"),
+ new DataContent("http://localhost/image"),
new FunctionCallContent("callId1", "fc1"),
]
};
@@ -169,7 +169,7 @@ public void JsonSerialization_Roundtrips()
Contents =
[
new TextContent("text-1"),
- new ImageContent("http://localhost/image"),
+ new DataContent("http://localhost/image"),
new FunctionCallContent("callId1", "fc1"),
new DataContent("data"u8.ToArray()),
new TextContent("text-2"),
@@ -192,8 +192,8 @@ public void JsonSerialization_Roundtrips()
Assert.IsType(result.Contents[0]);
Assert.Equal("text-1", ((TextContent)result.Contents[0]).Text);
- Assert.IsType(result.Contents[1]);
- Assert.Equal("http://localhost/image", ((ImageContent)result.Contents[1]).Uri);
+ Assert.IsType(result.Contents[1]);
+ Assert.Equal("http://localhost/image", ((DataContent)result.Contents[1]).Uri);
Assert.IsType(result.Contents[2]);
Assert.Equal("fc1", ((FunctionCallContent)result.Contents[2]).Name);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AudioContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AudioContentTests.cs
deleted file mode 100644
index 7aff849e8a1..00000000000
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AudioContentTests.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.AI;
-
-public sealed class AudioContentTests : DataContentTests;
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
index 18aae8c0497..dab6e7f3eed 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
@@ -1,6 +1,254 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
+using System.Text.Json;
+using Xunit;
+
namespace Microsoft.Extensions.AI;
-public sealed class DataContentTests : DataContentTests;
+public sealed class DataContentTests
+{
+ [Theory]
+
+ // Invalid URI
+ [InlineData("", typeof(ArgumentException))]
+ [InlineData("invalid", typeof(UriFormatException))]
+
+ // Format errors
+ [InlineData("data", typeof(UriFormatException))] // data missing colon
+ [InlineData("data:", typeof(UriFormatException))] // data missing comma
+ [InlineData("data:something,", typeof(UriFormatException))] // mime type without subtype
+ [InlineData("data:something;else,data", typeof(UriFormatException))] // mime type without subtype
+ [InlineData("data:type/subtype;;parameter=value;else,", typeof(UriFormatException))] // parameter without value
+ [InlineData("data:type/subtype;parameter=va=lue;else,", typeof(UriFormatException))] // parameter with multiple =
+ [InlineData("data:type/subtype;=value;else,", typeof(UriFormatException))] // empty parameter name
+ [InlineData("data:image/j/peg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD", typeof(UriFormatException))] // multiple slashes in media type
+
+ // Base64 Validation Errors
+ [InlineData("data:text;base64,something!", typeof(UriFormatException))] // Invalid base64 due to invalid character '!'
+ [InlineData("data:text/plain;base64,U29tZQ==\t", typeof(UriFormatException))] // Invalid base64 due to tab character
+ [InlineData("data:text/plain;base64,U29tZQ==\r", typeof(UriFormatException))] // Invalid base64 due to carriage return character
+ [InlineData("data:text/plain;base64,U29tZQ==\n", typeof(UriFormatException))] // Invalid base64 due to line feed character
+ [InlineData("data:text/plain;base64,U29t\r\nZQ==", typeof(UriFormatException))] // Invalid base64 due to carriage return and line feed characters
+ [InlineData("data:text/plain;base64,U29", typeof(UriFormatException))] // Invalid base64 due to missing padding
+ [InlineData("data:text/plain;base64,U29tZQ", typeof(UriFormatException))] // Invalid base64 due to missing padding
+ [InlineData("data:text/plain;base64,U29tZQ=", typeof(UriFormatException))] // Invalid base64 due to missing padding
+ public void Ctor_InvalidUri_Throws(string path, Type exception)
+ {
+ Assert.Throws(exception, () => new DataContent(path));
+ }
+
+ [Theory]
+ [InlineData("type")]
+ [InlineData("type//subtype")]
+ [InlineData("type/subtype/")]
+ [InlineData("type/subtype;key=")]
+ [InlineData("type/subtype;=value")]
+ [InlineData("type/subtype;key=value;another=")]
+ public void Ctor_InvalidMediaType_Throws(string type)
+ {
+ Assert.Throws("mediaType", () => new DataContent("http://localhost/test", type));
+ }
+
+ [Theory]
+ [InlineData("type/subtype")]
+ [InlineData("type/subtype;key=value")]
+ [InlineData("type/subtype;key=value;another=value")]
+ [InlineData("type/subtype;key=value;another=value;yet_another=value")]
+ public void Ctor_ValidMediaType_Roundtrips(string mediaType)
+ {
+ var content = new DataContent("http://localhost/test", mediaType);
+ Assert.Equal(mediaType, content.MediaType);
+
+ content = new DataContent("data:,", mediaType);
+ Assert.Equal(mediaType, content.MediaType);
+
+ content = new DataContent("data:text/plain,", mediaType);
+ Assert.Equal(mediaType, content.MediaType);
+
+ content = new DataContent(new Uri("data:text/plain,"), mediaType);
+ Assert.Equal(mediaType, content.MediaType);
+
+ content = new DataContent(new byte[] { 0, 1, 2 }, mediaType);
+ Assert.Equal(mediaType, content.MediaType);
+
+ content = new DataContent(content.Uri);
+ Assert.Equal(mediaType, content.MediaType);
+ }
+
+ [Fact]
+ public void Ctor_NoMediaType_Roundtrips()
+ {
+ DataContent content;
+
+ foreach (string url in new[] { "http://localhost/test", "about:something", "file://c:\\path" })
+ {
+ content = new DataContent(url);
+ Assert.Equal(url, content.Uri);
+ Assert.Null(content.MediaType);
+ Assert.Null(content.Data);
+ }
+
+ content = new DataContent("data:,something");
+ Assert.Equal("data:,something", content.Uri);
+ Assert.Null(content.MediaType);
+ Assert.Equal("something"u8.ToArray(), content.Data!.Value.ToArray());
+
+ content = new DataContent("data:,Hello+%3C%3E");
+ Assert.Equal("data:,Hello+%3C%3E", content.Uri);
+ Assert.Null(content.MediaType);
+ Assert.Equal("Hello <>"u8.ToArray(), content.Data!.Value.ToArray());
+ }
+
+ [Fact]
+ public void Serialize_MatchesExpectedJson()
+ {
+ Assert.Equal(
+ """{"uri":"data:,"}""",
+ JsonSerializer.Serialize(new DataContent("data:,"), TestJsonSerializerContext.Default.Options));
+
+ Assert.Equal(
+ """{"uri":"http://localhost/"}""",
+ JsonSerializer.Serialize(new DataContent(new Uri("http://localhost/")), TestJsonSerializerContext.Default.Options));
+
+ Assert.Equal(
+ """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
+ JsonSerializer.Serialize(new DataContent(
+ uri: "data:application/octet-stream;base64,AQIDBA=="), TestJsonSerializerContext.Default.Options));
+
+ Assert.Equal(
+ """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
+ JsonSerializer.Serialize(new DataContent(
+ new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"),
+ TestJsonSerializerContext.Default.Options));
+ }
+
+ [Theory]
+ [InlineData("{}")]
+ [InlineData("""{ "mediaType":"text/plain" }""")]
+ public void Deserialize_MissingUriString_Throws(string json)
+ {
+ Assert.Throws("uri", () => JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Options)!);
+ }
+
+ [Fact]
+ public void Deserialize_MatchesExpectedData()
+ {
+ // Data + MimeType only
+ var content = JsonSerializer.Deserialize("""{"mediaType":"application/octet-stream","uri":"data:;base64,AQIDBA=="}""", TestJsonSerializerContext.Default.Options)!;
+
+ Assert.Equal("data:application/octet-stream;base64,AQIDBA==", content.Uri);
+ Assert.NotNull(content.Data);
+ Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data.Value.ToArray());
+ Assert.Equal("application/octet-stream", content.MediaType);
+ Assert.True(content.ContainsData);
+
+ // Uri referenced content-only
+ content = JsonSerializer.Deserialize("""{"mediaType":"application/octet-stream","uri":"http://localhost/"}""", TestJsonSerializerContext.Default.Options)!;
+
+ Assert.Null(content.Data);
+ Assert.Equal("http://localhost/", content.Uri);
+ Assert.Equal("application/octet-stream", content.MediaType);
+ Assert.False(content.ContainsData);
+
+ // Using extra metadata
+ content = JsonSerializer.Deserialize("""
+ {
+ "uri": "data:;base64,AQIDBA==",
+ "modelId": "gpt-4",
+ "additionalProperties":
+ {
+ "key": "value"
+ },
+ "mediaType": "text/plain"
+ }
+ """, TestJsonSerializerContext.Default.Options)!;
+
+ Assert.Equal("data:text/plain;base64,AQIDBA==", content.Uri);
+ Assert.NotNull(content.Data);
+ Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data.Value.ToArray());
+ Assert.Equal("text/plain", content.MediaType);
+ Assert.True(content.ContainsData);
+ Assert.Equal("value", content.AdditionalProperties!["key"]!.ToString());
+ }
+
+ [Theory]
+ [InlineData(
+ """{"uri": "data:;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
+ """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
+ [InlineData(
+ """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
+ """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
+ [InlineData( // Does not support non-readable content
+ """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "unexpected": true}""",
+ """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
+ [InlineData( // Uri comes before mimetype
+ """{"mediaType": "text/plain", "uri": "http://localhost/" }""",
+ """{"uri":"http://localhost/","mediaType":"text/plain"}""")]
+ public void Serialize_Deserialize_Roundtrips(string serialized, string expectedToString)
+ {
+ var content = JsonSerializer.Deserialize(serialized, TestJsonSerializerContext.Default.Options)!;
+ var reSerialization = JsonSerializer.Serialize(content, TestJsonSerializerContext.Default.Options);
+ Assert.Equal(expectedToString, reSerialization);
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("application/octet-stream")]
+ [InlineData("application/pdf")]
+ [InlineData("application/xml")]
+ [InlineData("audio/mpeg")]
+ [InlineData("audio/ogg")]
+ [InlineData("audio/wav")]
+ [InlineData("image/apng")]
+ [InlineData("image/avif")]
+ [InlineData("image/bmp")]
+ [InlineData("image/gif")]
+ [InlineData("image/jpeg")]
+ [InlineData("image/png")]
+ [InlineData("image/svg+xml")]
+ [InlineData("image/tiff")]
+ [InlineData("image/webp")]
+ [InlineData("text/css")]
+ [InlineData("text/csv")]
+ [InlineData("text/html")]
+ [InlineData("text/javascript")]
+ [InlineData("text/plain")]
+ [InlineData("text/plain;charset=UTF-8")]
+ [InlineData("text/xml")]
+ [InlineData("custom/mediatypethatdoesntexists")]
+ public void MediaType_Roundtrips(string mediaType)
+ {
+ DataContent c = new("data:,", mediaType);
+ Assert.Equal(mediaType, c.MediaType);
+ }
+
+ [Theory]
+ [InlineData("image/gif", "image/")]
+ [InlineData("IMAGE/JPEG", "image")]
+ [InlineData("image/vnd.microsoft.icon", "ima")]
+ [InlineData("image/svg+xml", "IMAGE/")]
+ [InlineData("image/nonexistentimagemimetype", "IMAGE")]
+ [InlineData("audio/mpeg", "aUdIo/")]
+ [InlineData("application/json", "")]
+ [InlineData("application/pdf", "application/pdf")]
+ public void HasMediaTypePrefix_ReturnsTrue(string? mediaType, string prefix)
+ {
+ var content = new DataContent("http://localhost/image.png", mediaType);
+ Assert.True(content.MediaTypeStartsWith(prefix));
+ }
+
+ [Theory]
+ [InlineData("audio/mpeg", "image/")]
+ [InlineData("text/css", "text/csv")]
+ [InlineData("application/json", "application/json!")]
+ [InlineData("", "")] // The media type will get normalized to null
+ [InlineData(null, "image/")]
+ [InlineData(null, "")]
+ public void HasMediaTypePrefix_ReturnsFalse(string? mediaType, string prefix)
+ {
+ var content = new DataContent("http://localhost/image.png", mediaType);
+ Assert.False(content.MediaTypeStartsWith(prefix));
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests{T}.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests{T}.cs
deleted file mode 100644
index 68934ccdba5..00000000000
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests{T}.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Reflection;
-using System.Text.Json;
-using Xunit;
-
-namespace Microsoft.Extensions.AI;
-
-public abstract class DataContentTests
- where T : DataContent
-{
- private static T Create(params object?[] args)
- {
- try
- {
- return (T)Activator.CreateInstance(typeof(T), args)!;
- }
- catch (TargetInvocationException e)
- {
- throw e.InnerException!;
- }
- }
-
- public T CreateDataContent(Uri uri, string? mediaType = null) => Create(uri, mediaType)!;
-
-#pragma warning disable S3997 // String URI overloads should call "System.Uri" overloads
- public T CreateDataContent(string uriString, string? mediaType = null) => Create(uriString, mediaType)!;
-#pragma warning restore S3997
-
- public T CreateDataContent(ReadOnlyMemory data, string? mediaType = null) => Create(data, mediaType)!;
-
- [Theory]
-
- // Invalid URI
- [InlineData("", typeof(ArgumentException))]
- [InlineData("invalid", typeof(UriFormatException))]
-
- // Format errors
- [InlineData("data", typeof(UriFormatException))] // data missing colon
- [InlineData("data:", typeof(UriFormatException))] // data missing comma
- [InlineData("data:something,", typeof(UriFormatException))] // mime type without subtype
- [InlineData("data:something;else,data", typeof(UriFormatException))] // mime type without subtype
- [InlineData("data:type/subtype;;parameter=value;else,", typeof(UriFormatException))] // parameter without value
- [InlineData("data:type/subtype;parameter=va=lue;else,", typeof(UriFormatException))] // parameter with multiple =
- [InlineData("data:type/subtype;=value;else,", typeof(UriFormatException))] // empty parameter name
- [InlineData("data:image/j/peg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD", typeof(UriFormatException))] // multiple slashes in media type
-
- // Base64 Validation Errors
- [InlineData("data:text;base64,something!", typeof(UriFormatException))] // Invalid base64 due to invalid character '!'
- [InlineData("data:text/plain;base64,U29tZQ==\t", typeof(UriFormatException))] // Invalid base64 due to tab character
- [InlineData("data:text/plain;base64,U29tZQ==\r", typeof(UriFormatException))] // Invalid base64 due to carriage return character
- [InlineData("data:text/plain;base64,U29tZQ==\n", typeof(UriFormatException))] // Invalid base64 due to line feed character
- [InlineData("data:text/plain;base64,U29t\r\nZQ==", typeof(UriFormatException))] // Invalid base64 due to carriage return and line feed characters
- [InlineData("data:text/plain;base64,U29", typeof(UriFormatException))] // Invalid base64 due to missing padding
- [InlineData("data:text/plain;base64,U29tZQ", typeof(UriFormatException))] // Invalid base64 due to missing padding
- [InlineData("data:text/plain;base64,U29tZQ=", typeof(UriFormatException))] // Invalid base64 due to missing padding
- public void Ctor_InvalidUri_Throws(string path, Type exception)
- {
- Assert.Throws(exception, () => CreateDataContent(path));
- }
-
- [Theory]
- [InlineData("type")]
- [InlineData("type//subtype")]
- [InlineData("type/subtype/")]
- [InlineData("type/subtype;key=")]
- [InlineData("type/subtype;=value")]
- [InlineData("type/subtype;key=value;another=")]
- public void Ctor_InvalidMediaType_Throws(string type)
- {
- Assert.Throws("mediaType", () => CreateDataContent("http://localhost/test", type));
- }
-
- [Theory]
- [InlineData("type/subtype")]
- [InlineData("type/subtype;key=value")]
- [InlineData("type/subtype;key=value;another=value")]
- [InlineData("type/subtype;key=value;another=value;yet_another=value")]
- public void Ctor_ValidMediaType_Roundtrips(string mediaType)
- {
- T content = CreateDataContent("http://localhost/test", mediaType);
- Assert.Equal(mediaType, content.MediaType);
-
- content = CreateDataContent("data:,", mediaType);
- Assert.Equal(mediaType, content.MediaType);
-
- content = CreateDataContent("data:text/plain,", mediaType);
- Assert.Equal(mediaType, content.MediaType);
-
- content = CreateDataContent(new Uri("data:text/plain,"), mediaType);
- Assert.Equal(mediaType, content.MediaType);
-
- content = CreateDataContent(new byte[] { 0, 1, 2 }, mediaType);
- Assert.Equal(mediaType, content.MediaType);
-
- content = CreateDataContent(content.Uri);
- Assert.Equal(mediaType, content.MediaType);
- }
-
- [Fact]
- public void Ctor_NoMediaType_Roundtrips()
- {
- T content;
-
- foreach (string url in new[] { "http://localhost/test", "about:something", "file://c:\\path" })
- {
- content = CreateDataContent(url);
- Assert.Equal(url, content.Uri);
- Assert.Null(content.MediaType);
- Assert.Null(content.Data);
- }
-
- content = CreateDataContent("data:,something");
- Assert.Equal("data:,something", content.Uri);
- Assert.Null(content.MediaType);
- Assert.Equal("something"u8.ToArray(), content.Data!.Value.ToArray());
-
- content = CreateDataContent("data:,Hello+%3C%3E");
- Assert.Equal("data:,Hello+%3C%3E", content.Uri);
- Assert.Null(content.MediaType);
- Assert.Equal("Hello <>"u8.ToArray(), content.Data!.Value.ToArray());
- }
-
- [Fact]
- public void Serialize_MatchesExpectedJson()
- {
- Assert.Equal(
- """{"uri":"data:,"}""",
- JsonSerializer.Serialize(CreateDataContent("data:,"), TestJsonSerializerContext.Default.Options));
-
- Assert.Equal(
- """{"uri":"http://localhost/"}""",
- JsonSerializer.Serialize(CreateDataContent(new Uri("http://localhost/")), TestJsonSerializerContext.Default.Options));
-
- Assert.Equal(
- """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
- JsonSerializer.Serialize(CreateDataContent(
- uriString: "data:application/octet-stream;base64,AQIDBA=="), TestJsonSerializerContext.Default.Options));
-
- Assert.Equal(
- """{"uri":"data:application/octet-stream;base64,AQIDBA==","mediaType":"application/octet-stream"}""",
- JsonSerializer.Serialize(CreateDataContent(
- new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"),
- TestJsonSerializerContext.Default.Options));
- }
-
- [Theory]
- [InlineData("{}")]
- [InlineData("""{ "mediaType":"text/plain" }""")]
- public void Deserialize_MissingUriString_Throws(string json)
- {
- Assert.Throws("uri", () => JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Options)!);
- }
-
- [Fact]
- public void Deserialize_MatchesExpectedData()
- {
- // Data + MimeType only
- var content = JsonSerializer.Deserialize("""{"mediaType":"application/octet-stream","uri":"data:;base64,AQIDBA=="}""", TestJsonSerializerContext.Default.Options)!;
-
- Assert.Equal("data:application/octet-stream;base64,AQIDBA==", content.Uri);
- Assert.NotNull(content.Data);
- Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data!.Value.ToArray());
- Assert.Equal("application/octet-stream", content.MediaType);
- Assert.True(content.ContainsData);
-
- // Uri referenced content-only
- content = JsonSerializer.Deserialize("""{"mediaType":"application/octet-stream","uri":"http://localhost/"}""", TestJsonSerializerContext.Default.Options)!;
-
- Assert.Null(content.Data);
- Assert.Equal("http://localhost/", content.Uri);
- Assert.Equal("application/octet-stream", content.MediaType);
- Assert.False(content.ContainsData);
-
- // Using extra metadata
- content = JsonSerializer.Deserialize("""
- {
- "uri": "data:;base64,AQIDBA==",
- "modelId": "gpt-4",
- "additionalProperties":
- {
- "key": "value"
- },
- "mediaType": "text/plain"
- }
- """, TestJsonSerializerContext.Default.Options)!;
-
- Assert.Equal("data:text/plain;base64,AQIDBA==", content.Uri);
- Assert.NotNull(content.Data);
- Assert.Equal([0x01, 0x02, 0x03, 0x04], content.Data!.Value.ToArray());
- Assert.Equal("text/plain", content.MediaType);
- Assert.True(content.ContainsData);
- Assert.Equal("value", content.AdditionalProperties!["key"]!.ToString());
- }
-
- [Theory]
- [InlineData(
- """{"uri": "data:;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
- """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
- [InlineData(
- """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType": "text/plain"}""",
- """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
- [InlineData( // Does not support non-readable content
- """{"uri": "data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "unexpected": true}""",
- """{"uri":"data:text/plain;base64,AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","mediaType":"text/plain"}""")]
- [InlineData( // Uri comes before mimetype
- """{"mediaType": "text/plain", "uri": "http://localhost/" }""",
- """{"uri":"http://localhost/","mediaType":"text/plain"}""")]
- public void Serialize_Deserialize_Roundtrips(string serialized, string expectedToString)
- {
- var content = JsonSerializer.Deserialize(serialized, TestJsonSerializerContext.Default.Options)!;
- var reSerialization = JsonSerializer.Serialize(content, TestJsonSerializerContext.Default.Options);
- Assert.Equal(expectedToString, reSerialization);
- }
-
- [Theory]
- [InlineData("application/json")]
- [InlineData("application/octet-stream")]
- [InlineData("application/pdf")]
- [InlineData("application/xml")]
- [InlineData("audio/mpeg")]
- [InlineData("audio/ogg")]
- [InlineData("audio/wav")]
- [InlineData("image/apng")]
- [InlineData("image/avif")]
- [InlineData("image/bmp")]
- [InlineData("image/gif")]
- [InlineData("image/jpeg")]
- [InlineData("image/png")]
- [InlineData("image/svg+xml")]
- [InlineData("image/tiff")]
- [InlineData("image/webp")]
- [InlineData("text/css")]
- [InlineData("text/csv")]
- [InlineData("text/html")]
- [InlineData("text/javascript")]
- [InlineData("text/plain")]
- [InlineData("text/plain;charset=UTF-8")]
- [InlineData("text/xml")]
- [InlineData("custom/mediatypethatdoesntexists")]
- public void MediaType_Roundtrips(string mediaType)
- {
- DataContent c = new("data:,", mediaType);
- Assert.Equal(mediaType, c.MediaType);
- }
-}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageContentTests.cs
deleted file mode 100644
index 7b088e3ebf3..00000000000
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageContentTests.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.AI;
-
-public sealed class ImageContentTests : DataContentTests;
diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs
index 3797f4b6c47..599b4403fa9 100644
--- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs
@@ -586,7 +586,7 @@ public async Task MultipleContent_NonStreaming()
Assert.NotNull(await client.CompleteAsync([new(ChatRole.User,
[
new TextContent("Describe this picture."),
- new ImageContent("http://dot.net/someimage.png"),
+ new DataContent("http://dot.net/someimage.png", mediaType: "image/png"),
])]));
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
index bff072e1bd4..a9465a8237b 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
@@ -146,7 +146,7 @@ public virtual async Task MultiModal_DescribeImage()
new(ChatRole.User,
[
new TextContent("What does this logo say?"),
- new ImageContent(GetImageDataUri()),
+ new DataContent(GetImageDataUri(), "image/png"),
])
],
new() { ModelId = GetModel_MultiModal_DescribeImage() });
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs
index 5b7bcdddf73..73e231ccbdb 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ChatClientStructuredOutputExtensionsTests.cs
@@ -148,7 +148,7 @@ public async Task FailureUsage_NullJson()
[Fact]
public async Task FailureUsage_NoJsonInResponse()
{
- var expectedCompletion = new ChatCompletion([new ChatMessage(ChatRole.Assistant, [new ImageContent("https://example.com")])]);
+ var expectedCompletion = new ChatCompletion([new ChatMessage(ChatRole.Assistant, [new DataContent("https://example.com")])]);
using var client = new TestChatClient
{
CompleteAsyncCallback = (messages, options, cancellationToken) => Task.FromResult(expectedCompletion),