Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
using Microsoft.SemanticKernel.TextToAudio;
using Microsoft.SemanticKernel.TextToImage;

#pragma warning disable MEAI001 // Image generation APIs are experimental.

namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions;

/// <summary>
Expand Down Expand Up @@ -185,6 +187,50 @@ public void KernelBuilderExtensionsAddAzureOpenAITextToImageService(Initializati

#endregion

#region Image Generation

[Theory]
[InlineData(InitializationType.ApiKey, null)]
[InlineData(InitializationType.ApiKey, "images")]
[InlineData(InitializationType.TokenCredential, null)]
[InlineData(InitializationType.TokenCredential, "images")]
[InlineData(InitializationType.ClientInline, null)]
[InlineData(InitializationType.ClientInline, "images")]
[InlineData(InitializationType.ClientInServiceProvider, null)]
[InlineData(InitializationType.ClientInServiceProvider, "images")]
[InlineData(InitializationType.ApiVersion, null)]
[InlineData(InitializationType.ApiVersion, "images")]
public void KernelBuilderAddAzureOpenAIImageGeneratorAddsValidService(InitializationType type, string? serviceId)
{
// Arrange
var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken());
var client = new AzureOpenAIClient(new Uri("https://localhost"), new ApiKeyCredential("key"));
var builder = Kernel.CreateBuilder();
bool telemetryConfigured = false;
void configureTelemetry(OpenTelemetryImageGenerator _) => telemetryConfigured = true;
builder.Services.AddSingleton(client);

// Act
builder = type switch
{
InitializationType.ApiKey => builder.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", serviceId, openTelemetryConfig: configureTelemetry),
InitializationType.TokenCredential => builder.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", credentials, serviceId, openTelemetryConfig: configureTelemetry),
InitializationType.ClientInline => builder.AddAzureOpenAIImageGenerator("deployment-name", client, serviceId, openTelemetryConfig: configureTelemetry),
InitializationType.ClientInServiceProvider => builder.AddAzureOpenAIImageGenerator("deployment-name", serviceId: serviceId, openTelemetryConfig: configureTelemetry),
InitializationType.ApiVersion => builder.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", serviceId, apiVersion: "2025-04-01-preview", openTelemetryConfig: configureTelemetry),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};

// Assert
var kernel = builder.Build();
var service = kernel.GetRequiredService<IImageGenerator>(serviceId);
Assert.Equal("deployment-name", service.GetService<ImageGeneratorMetadata>()!.DefaultModelId);
Assert.Same(service, kernel.GetRequiredService<IImageGenerator>(serviceId));
Assert.True(telemetryConfigured);
}

#endregion

#region Audio to text

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

using System;
using System.ClientModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.ChatCompletion;
Expand All @@ -22,6 +26,8 @@
using Moq;
using Moq.Protected;

#pragma warning disable MEAI001 // Image generation APIs are experimental.

namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions;
/// <summary>
/// Unit tests for the service collection extensions in the <see cref="Microsoft.SemanticKernel.AzureOpenAIServiceCollectionExtensions"/> class.
Expand Down Expand Up @@ -226,6 +232,141 @@ public void ServiceCollectionExtensionsAddAzureOpenAITextToImageService(Initiali

#endregion

#region Image Generation

[Theory]
[InlineData(InitializationType.ApiKey, null)]
[InlineData(InitializationType.ApiKey, "images")]
[InlineData(InitializationType.TokenCredential, null)]
[InlineData(InitializationType.TokenCredential, "images")]
[InlineData(InitializationType.ClientInline, null)]
[InlineData(InitializationType.ClientInline, "images")]
[InlineData(InitializationType.ClientInServiceProvider, null)]
[InlineData(InitializationType.ClientInServiceProvider, "images")]
[InlineData(InitializationType.ApiVersion, null)]
[InlineData(InitializationType.ApiVersion, "images")]
[InlineData(InitializationType.HttpClient, null)]
[InlineData(InitializationType.HttpClient, "images")]
public async Task ServiceCollectionAddAzureOpenAIImageGeneratorAddsValidServiceAsync(InitializationType type, string? serviceId)
{
// Arrange
using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(File.ReadAllText("./TestData/text-to-image-b64_json-format-response.json"))
};
using var messageHandler = new HttpMessageHandlerStub { ResponseToReturn = responseMessage };
using var httpClient = new HttpClient(messageHandler, false);
var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken("test-token", DateTimeOffset.UtcNow.AddHours(1)));
var client = new AzureOpenAIClient(
new Uri("https://endpoint"),
new ApiKeyCredential("key"),
AzureClientCore.GetAzureOpenAIClientOptions(httpClient));
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(client);
builder.Services.AddSingleton(httpClient);
builder.Services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
bool telemetryConfigured = false;
void configureTelemetry(OpenTelemetryImageGenerator _) => telemetryConfigured = true;
string sourceName = $"AzureOpenAIImages.{Guid.NewGuid():N}";
Activity? imageActivity = null;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == sourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ActivityStopped = activity => imageActivity = activity
};
ActivitySource.AddActivityListener(listener);

// Act
IServiceCollection collection = type switch
{
InitializationType.ApiKey => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", serviceId, httpClient: httpClient, openTelemetrySourceName: sourceName, openTelemetryConfig: configureTelemetry),
InitializationType.TokenCredential => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", credentials, serviceId, httpClient: httpClient, openTelemetrySourceName: sourceName, openTelemetryConfig: configureTelemetry),
InitializationType.ClientInline => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", client, serviceId, sourceName, configureTelemetry),
InitializationType.ClientInServiceProvider => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", serviceId: serviceId, openTelemetrySourceName: sourceName, openTelemetryConfig: configureTelemetry),
InitializationType.ApiVersion => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", serviceId, apiVersion: "2025-04-01-preview", httpClient: httpClient, openTelemetrySourceName: sourceName, openTelemetryConfig: configureTelemetry),
InitializationType.HttpClient => builder.Services.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", serviceId, openTelemetrySourceName: sourceName, openTelemetryConfig: configureTelemetry),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
using var provider = collection.BuildServiceProvider();
var service = provider.GetRequiredKeyedService<IImageGenerator>(serviceId);
var response = await service.GenerateImagesAsync("A sea otter", new ImageGenerationOptions
{
ImageSize = new System.Drawing.Size(1024, 1024)
});

// Assert
Assert.Same(builder.Services, collection);
Assert.Equal("deployment-name", service.GetService<ImageGeneratorMetadata>()!.DefaultModelId);
Assert.Same(service, provider.GetRequiredKeyedService<IImageGenerator>(serviceId));
var image = Assert.IsType<DataContent>(Assert.Single(response.Contents));
Assert.Equal(Convert.FromBase64String("iVBORw0KGgoAAA=="), image.Data.ToArray());
Assert.Equal("image/png", image.MediaType);
Assert.True(telemetryConfigured);
Assert.NotNull(imageActivity);
Assert.Equal(sourceName, imageActivity.Source.Name);
Assert.Equal("endpoint", messageHandler.RequestUri!.Host);

using var request = JsonDocument.Parse(messageHandler.RequestContent!);
Assert.Equal("deployment-name", request.RootElement.GetProperty("model").GetString());
Assert.Equal("A sea otter", request.RootElement.GetProperty("prompt").GetString());
Assert.Equal("1024x1024", request.RootElement.GetProperty("size").GetString());
Assert.Equal(1, request.RootElement.GetProperty("n").GetInt32());
if (type == InitializationType.ApiVersion)
{
Assert.Contains("api-version=2025-04-01-preview", messageHandler.RequestUri.Query);
}
if (type == InitializationType.TokenCredential)
{
Assert.Equal("Bearer", messageHandler.RequestHeaders!.Authorization!.Scheme);
Assert.Equal("test-token", messageHandler.RequestHeaders.Authorization.Parameter);
}
}

[Fact]
public async Task ImageGeneratorCanEditImagesAsync()
{
// Arrange
using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""{"created":1726234481,"data":[{"b64_json":"AQID"}]}""")
};
using var messageHandler = new HttpMessageHandlerStub { ResponseToReturn = responseMessage };
using var httpClient = new HttpClient(messageHandler, false);
using var provider = new ServiceCollection()
.AddAzureOpenAIImageGenerator("deployment-name", "https://endpoint", "api-key", apiVersion: "2025-04-01-preview", httpClient: httpClient)
.BuildServiceProvider();
var service = provider.GetRequiredService<IImageGenerator>();

// Act
var response = await service.EditImageAsync(
new DataContent(new byte[] { 1, 2, 3 }, "image/png") { Name = "input.png" },
"Make the background red");

// Assert
var image = Assert.IsType<DataContent>(Assert.Single(response.Contents));
Assert.Equal(new byte[] { 1, 2, 3 }, image.Data.ToArray());
Assert.Equal("image/png", image.MediaType);
Assert.Equal("endpoint", messageHandler.RequestUri!.Host);
Assert.EndsWith("/images/edits", messageHandler.RequestUri.AbsolutePath);
Assert.Contains("api-version=2025-04-01-preview", messageHandler.RequestUri.Query);
Assert.Equal("multipart/form-data", messageHandler.ContentHeaders!.ContentType!.MediaType);
}

[Fact]
public void ImageGeneratorRequiresRegisteredAzureOpenAIClient()
{
// Arrange
using var provider = new ServiceCollection()
.AddAzureOpenAIImageGenerator("deployment-name")
.BuildServiceProvider();

// Act & Assert
Assert.Throws<InvalidOperationException>(() => provider.GetRequiredService<IImageGenerator>());
}

#endregion

#region Audio to text

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.ClientModel;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -195,6 +196,9 @@ public async Task GetUriImageContentsResponseFormatRequestWorksCorrectlyAsync(st
[InlineData("hd", "hd")]
[InlineData("high", "hd")]
[InlineData("standard", "standard")]
[InlineData("medium", "medium")]
[InlineData("low", "low")]
[InlineData("auto", "auto")]
public async Task GetUriImageContentsImageQualityRequestWorksCorrectlyAsync(string? quality, string? expectedQuality)
{
// Arrange
Expand Down Expand Up @@ -335,6 +339,65 @@ public async Task GetUrlImageContentsResponseWorksCorrectlyAsync()
Assert.Equal("my prompt", breakingGlass!.RevisedPrompt);
}

[Fact]
public async Task GenerateGptImageReturnsDataUriAsync()
{
// Arrange
using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(File.ReadAllText("./TestData/text-to-image-b64_json-format-response.json"))
};
this._messageHandlerStub.ResponseToReturn = responseMessage;
var sut = new AzureOpenAITextToImageService("image-deployment", "https://api-host", "api-key", "gpt-image-1", this._httpClient);

// Act
var result = await sut.GenerateImageAsync("A sea otter", 1024, 1024);

// Assert
Assert.Equal("data:image/png;base64,iVBORw0KGgoAAA==", result);
}

[Fact]
public async Task GetGptImageContentsPreservesSettingsWithProvidedClientAsync()
{
// Arrange
using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(File.ReadAllText("./TestData/text-to-image-b64_json-format-response.json"))
};
this._messageHandlerStub.ResponseToReturn = responseMessage;
var client = new AzureOpenAIClient(
new Uri("https://api-host"),
new ApiKeyCredential("api-key"),
AzureClientCore.GetAzureOpenAIClientOptions(this._httpClient, "2025-04-01-preview"));
var sut = new AzureOpenAITextToImageService("image-deployment", client, "gpt-image-1");

// Act
var result = await sut.GetImageContentsAsync("A sea otter", new OpenAITextToImageExecutionSettings
{
Size = (1536, 1024),
Quality = "low",
EndUserId = "test-user"
});

// Assert
var image = Assert.Single(result);
Assert.Equal(Convert.FromBase64String("iVBORw0KGgoAAA=="), image.Data!.Value.ToArray());
Assert.Equal("image/png", image.MimeType);
Assert.Equal("my prompt", Assert.IsType<GeneratedImage>(image.InnerContent).RevisedPrompt);
Assert.Equal("gpt-image-1", sut.Attributes[AIServiceExtensions.ModelIdKey]);
Assert.Equal("image-deployment", sut.Attributes[AzureClientCore.DeploymentNameKey]);
using var request = JsonDocument.Parse(this._messageHandlerStub.RequestContent!);
Assert.Equal("image-deployment", request.RootElement.GetProperty("model").GetString());
Assert.Equal("A sea otter", request.RootElement.GetProperty("prompt").GetString());
Assert.Equal("1536x1024", request.RootElement.GetProperty("size").GetString());
Assert.Equal("low", request.RootElement.GetProperty("quality").GetString());
Assert.Equal("test-user", request.RootElement.GetProperty("user").GetString());
Assert.Equal(1, request.RootElement.GetProperty("n").GetInt32());
Assert.False(request.RootElement.TryGetProperty("response_format", out _));
Assert.Contains("api-version=2025-04-01-preview", this._messageHandlerStub.RequestUri!.Query);
}

[Theory]
[MemberData(nameof(Versions))]
public async Task ItTargetsApiVersionAsExpected(string? apiVersion, string? expectedVersion = null)
Expand Down
Loading
Loading