diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
index 046b5999bee6..0d89e02beb21 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj
@@ -1,4 +1,4 @@
-
+
SemanticKernel.Connectors.OpenAI.UnitTests
@@ -7,7 +7,7 @@
true
enable
false
- $(NoWarn);SKEXP0001;SKEXP0070;CS1591;IDE1006;RCS1261;CA1031;CA1308;CA1861;CA2007;CA2234;VSTHRD111
+ $(NoWarn);SKEXP0001;SKEXP0070;SKEXP0010;CS1591;IDE1006;RCS1261;CA1031;CA1308;CA1861;CA2007;CA2234;VSTHRD111
@@ -29,11 +29,21 @@
-
+
+
+
+
+
+ Always
+
+
+ Always
+
+
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs
new file mode 100644
index 000000000000..a3415663459a
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Http;
+using Moq;
+using OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core;
+public partial class ClientCoreTests
+{
+ [Fact]
+ public void ItCanBeInstantiatedAndPropertiesSetAsExpected()
+ {
+ // Act
+ var logger = new Mock>().Object;
+ var openAIClient = new OpenAIClient(new ApiKeyCredential("key"));
+
+ var clientCoreModelConstructor = new ClientCore("model1", "apiKey");
+ var clientCoreOpenAIClientConstructor = new ClientCore("model1", openAIClient, logger: logger);
+
+ // Assert
+ Assert.NotNull(clientCoreModelConstructor);
+ Assert.NotNull(clientCoreOpenAIClientConstructor);
+
+ Assert.Equal("model1", clientCoreModelConstructor.ModelId);
+ Assert.Equal("model1", clientCoreOpenAIClientConstructor.ModelId);
+
+ Assert.NotNull(clientCoreModelConstructor.Client);
+ Assert.NotNull(clientCoreOpenAIClientConstructor.Client);
+ Assert.Equal(openAIClient, clientCoreOpenAIClientConstructor.Client);
+ Assert.Equal(NullLogger.Instance, clientCoreModelConstructor.Logger);
+ Assert.Equal(logger, clientCoreOpenAIClientConstructor.Logger);
+ }
+
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("http://localhost", null)]
+ [InlineData(null, "http://localhost")]
+ [InlineData("http://localhost-1", "http://localhost-2")]
+ public void ItUsesEndpointAsExpected(string? clientBaseAddress, string? providedEndpoint)
+ {
+ // Arrange
+ Uri? endpoint = null;
+ HttpClient? client = null;
+ if (providedEndpoint is not null)
+ {
+ endpoint = new Uri(providedEndpoint);
+ }
+
+ if (clientBaseAddress is not null)
+ {
+ client = new HttpClient { BaseAddress = new Uri(clientBaseAddress) };
+ }
+
+ // Act
+ var clientCore = new ClientCore("model", "apiKey", endpoint: endpoint, httpClient: client);
+
+ // Assert
+ Assert.Equal(endpoint ?? client?.BaseAddress ?? new Uri("https://api.openai.com/v1"), clientCore.Endpoint);
+
+ client?.Dispose();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ItAddOrganizationHeaderWhenProvidedAsync(bool organizationIdProvided)
+ {
+ using HttpMessageHandlerStub handler = new();
+ using HttpClient client = new(handler);
+ handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
+
+ // Act
+ var clientCore = new ClientCore(
+ modelId: "model",
+ apiKey: "test",
+ organizationId: (organizationIdProvided) ? "organization" : null,
+ httpClient: client);
+
+ var pipelineMessage = clientCore.Client.Pipeline.CreateMessage();
+ pipelineMessage.Request.Method = "POST";
+ pipelineMessage.Request.Uri = new Uri("http://localhost");
+ pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test"));
+
+ // Assert
+ await clientCore.Client.Pipeline.SendAsync(pipelineMessage);
+
+ if (organizationIdProvided)
+ {
+ Assert.True(handler.RequestHeaders!.Contains("OpenAI-Organization"));
+ Assert.Equal("organization", handler.RequestHeaders.GetValues("OpenAI-Organization").FirstOrDefault());
+ }
+ else
+ {
+ Assert.False(handler.RequestHeaders!.Contains("OpenAI-Organization"));
+ }
+ }
+
+ [Fact]
+ public async Task ItAddSemanticKernelHeadersOnEachRequestAsync()
+ {
+ using HttpMessageHandlerStub handler = new();
+ using HttpClient client = new(handler);
+ handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
+
+ // Act
+ var clientCore = new ClientCore(modelId: "model", apiKey: "test", httpClient: client);
+
+ var pipelineMessage = clientCore.Client.Pipeline.CreateMessage();
+ pipelineMessage.Request.Method = "POST";
+ pipelineMessage.Request.Uri = new Uri("http://localhost");
+ pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test"));
+
+ // Assert
+ await clientCore.Client.Pipeline.SendAsync(pipelineMessage);
+
+ Assert.True(handler.RequestHeaders!.Contains(HttpHeaderConstant.Names.SemanticKernelVersion));
+ Assert.Equal(HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore)), handler.RequestHeaders.GetValues(HttpHeaderConstant.Names.SemanticKernelVersion).FirstOrDefault());
+
+ Assert.True(handler.RequestHeaders.Contains("User-Agent"));
+ Assert.Contains(HttpHeaderConstant.Values.UserAgent, handler.RequestHeaders.GetValues("User-Agent").FirstOrDefault());
+ }
+
+ [Fact]
+ public async Task ItDoNotAddSemanticKernelHeadersWhenOpenAIClientIsProvidedAsync()
+ {
+ using HttpMessageHandlerStub handler = new();
+ using HttpClient client = new(handler);
+ handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
+
+ // Act
+ var clientCore = new ClientCore(
+ modelId: "model",
+ openAIClient: new OpenAIClient(
+ new ApiKeyCredential("test"),
+ new OpenAIClientOptions()
+ {
+ Transport = new HttpClientPipelineTransport(client),
+ RetryPolicy = new ClientRetryPolicy(maxRetries: 0),
+ NetworkTimeout = Timeout.InfiniteTimeSpan
+ }));
+
+ var pipelineMessage = clientCore.Client.Pipeline.CreateMessage();
+ pipelineMessage.Request.Method = "POST";
+ pipelineMessage.Request.Uri = new Uri("http://localhost");
+ pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test"));
+
+ // Assert
+ await clientCore.Client.Pipeline.SendAsync(pipelineMessage);
+
+ Assert.False(handler.RequestHeaders!.Contains(HttpHeaderConstant.Names.SemanticKernelVersion));
+ Assert.DoesNotContain(HttpHeaderConstant.Values.UserAgent, handler.RequestHeaders.GetValues("User-Agent").FirstOrDefault());
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("value")]
+ public void ItAddAttributesButDoesNothingIfNullOrEmpty(string? value)
+ {
+ // Arrange
+ var clientCore = new ClientCore("model", "apikey");
+ // Act
+
+ clientCore.AddAttribute("key", value);
+
+ // Assert
+ if (string.IsNullOrEmpty(value))
+ {
+ Assert.False(clientCore.Attributes.ContainsKey("key"));
+ }
+ else
+ {
+ Assert.True(clientCore.Attributes.ContainsKey("key"));
+ Assert.Equal(value, clientCore.Attributes["key"]);
+ }
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs
new file mode 100644
index 000000000000..83ec6a20568d
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models;
+
+public class AddHeaderRequestPolicyTests
+{
+ [Fact]
+ public void ItCanBeInstantiated()
+ {
+ // Arrange
+ var headerName = "headerName";
+ var headerValue = "headerValue";
+
+ // Act
+ var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue);
+
+ // Assert
+ Assert.NotNull(addHeaderRequestPolicy);
+ }
+
+ [Fact]
+ public void ItOnSendingRequestAddsHeaderToRequest()
+ {
+ // Arrange
+ var headerName = "headerName";
+ var headerValue = "headerValue";
+ var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue);
+ var pipeline = ClientPipeline.Create();
+ var message = pipeline.CreateMessage();
+
+ // Act
+ addHeaderRequestPolicy.OnSendingRequest(message);
+
+ // Assert
+ message.Request.Headers.TryGetValue(headerName, out var value);
+ Assert.NotNull(value);
+ Assert.Equal(headerValue, value);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs
new file mode 100644
index 000000000000..cae4b32b4283
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models;
+public class PipelineSynchronousPolicyTests
+{
+ [Fact]
+ public async Task ItProcessAsyncWhenSpecializationHasReceivedResponseOverrideShouldCallIt()
+ {
+ // Arrange
+ var first = new MyHttpPipelinePolicyWithoutOverride();
+ var last = new MyHttpPipelinePolicyWithOverride();
+
+ IReadOnlyList policies = [first, last];
+
+ // Act
+ await policies[0].ProcessAsync(ClientPipeline.Create().CreateMessage(), policies, 0);
+
+ // Assert
+ Assert.True(first.CalledProcess);
+ Assert.True(last.CalledProcess);
+ Assert.True(last.CalledOnReceivedResponse);
+ }
+
+ private class MyHttpPipelinePolicyWithoutOverride : PipelineSynchronousPolicy
+ {
+ public bool CalledProcess { get; private set; }
+
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.CalledProcess = true;
+ base.Process(message, pipeline, currentIndex);
+ }
+
+ public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.CalledProcess = true;
+ return base.ProcessAsync(message, pipeline, currentIndex);
+ }
+ }
+
+ private sealed class MyHttpPipelinePolicyWithOverride : MyHttpPipelinePolicyWithoutOverride
+ {
+ public bool CalledOnReceivedResponse { get; private set; }
+
+ public override void OnReceivedResponse(PipelineMessage message)
+ {
+ this.CalledOnReceivedResponse = true;
+ }
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
new file mode 100644
index 000000000000..0b95f904d893
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions;
+
+public class ClientResultExceptionExtensionsTests
+{
+ [Fact]
+ public void ItCanRecoverFromResponseErrorAndConvertsToHttpOperationExceptionWithDefaultData()
+ {
+ // Arrange
+ var exception = new ClientResultException("message", ClientPipeline.Create().CreateMessage().Response);
+
+ // Act
+ var httpOperationException = exception.ToHttpOperationException();
+
+ // Assert
+ Assert.NotNull(httpOperationException);
+ Assert.Equal(exception, httpOperationException.InnerException);
+ Assert.Equal(exception.Message, httpOperationException.Message);
+ Assert.Null(httpOperationException.ResponseContent);
+ Assert.Null(httpOperationException.StatusCode);
+ }
+
+ [Fact]
+ public void ItCanProvideResponseContentAndStatusCode()
+ {
+ // Arrange
+ using var pipelineResponse = new MockPipelineResponse();
+
+ pipelineResponse.SetContent("content");
+ pipelineResponse.SetStatus(200);
+
+ var exception = new ClientResultException("message", pipelineResponse);
+
+ // Act
+ var httpOperationException = exception.ToHttpOperationException();
+
+ // Assert
+ Assert.NotNull(httpOperationException);
+ Assert.NotNull(httpOperationException.StatusCode);
+ Assert.Equal(exception, httpOperationException.InnerException);
+ Assert.Equal(exception.Message, httpOperationException.Message);
+ Assert.Equal(pipelineResponse.Content.ToString(), httpOperationException.ResponseContent);
+ Assert.Equal(pipelineResponse.Status, (int)httpOperationException.StatusCode!);
+ }
+
+ [Fact]
+ public void ItProvideStatusForResponsesWithoutContent()
+ {
+ // Arrange
+ using var pipelineResponse = new MockPipelineResponse();
+
+ pipelineResponse.SetStatus(200);
+
+ var exception = new ClientResultException("message", pipelineResponse);
+
+ // Act
+ var httpOperationException = exception.ToHttpOperationException();
+
+ // Assert
+ Assert.NotNull(httpOperationException);
+ Assert.NotNull(httpOperationException.StatusCode);
+ Assert.Empty(httpOperationException.ResponseContent!);
+ Assert.Equal(exception, httpOperationException.InnerException);
+ Assert.Equal(exception.Message, httpOperationException.Message);
+ Assert.Equal(pipelineResponse.Status, (int)httpOperationException.StatusCode!);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs
new file mode 100644
index 000000000000..25cdc4ec61aa
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Services;
+using OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;
+public class OpenAITextEmbeddingGenerationServiceTests
+{
+ [Fact]
+ public void ItCanBeInstantiatedAndPropertiesSetAsExpected()
+ {
+ // Arrange
+ var sut = new OpenAITextEmbeddingGenerationService("model", "apiKey", dimensions: 2);
+ var sutWithOpenAIClient = new OpenAITextEmbeddingGenerationService("model", new OpenAIClient(new ApiKeyCredential("apiKey")), dimensions: 2);
+
+ // Assert
+ Assert.NotNull(sut);
+ Assert.NotNull(sutWithOpenAIClient);
+ Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]);
+ Assert.Equal("model", sutWithOpenAIClient.Attributes[AIServiceExtensions.ModelIdKey]);
+ }
+
+ [Fact]
+ public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty()
+ {
+ // Arrange
+ var sut = new OpenAITextEmbeddingGenerationService("model", "apikey");
+
+ // Act
+ var result = await sut.GenerateEmbeddingsAsync([], null, CancellationToken.None);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace()
+ {
+ using HttpMessageHandlerStub handler = new()
+ {
+ ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-response.txt"))
+ }
+ };
+ using HttpClient client = new(handler);
+
+ // Arrange
+ var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client);
+
+ // Act
+ var result = await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None);
+
+ // Assert
+ Assert.Single(result);
+ Assert.Equal(4, result[0].Length);
+ }
+
+ [Fact]
+ public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync()
+ {
+ using HttpMessageHandlerStub handler = new()
+ {
+ ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-multiple-response.txt"))
+ }
+ };
+ using HttpClient client = new(handler);
+
+ // Arrange
+ var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () => await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None));
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt
new file mode 100644
index 000000000000..46a9581cf0cc
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt
@@ -0,0 +1,20 @@
+{
+ "object": "list",
+ "data": [
+ {
+ "object": "embedding",
+ "index": 0,
+ "embedding": "zcyMP83MDEAzM1NAzcyMQA=="
+ },
+ {
+ "object": "embedding",
+ "index": 1,
+ "embedding": "zcyMP83MDEAzM1NAzcyMQA=="
+ }
+ ],
+ "model": "text-embedding-ada-002",
+ "usage": {
+ "prompt_tokens": 7,
+ "total_tokens": 7
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt
new file mode 100644
index 000000000000..c715b851b78c
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt
@@ -0,0 +1,15 @@
+{
+ "object": "list",
+ "data": [
+ {
+ "object": "embedding",
+ "index": 0,
+ "embedding": "zcyMP83MDEAzM1NAzcyMQA=="
+ }
+ ],
+ "model": "text-embedding-ada-002",
+ "usage": {
+ "prompt_tokens": 7,
+ "total_tokens": 7
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs
new file mode 100644
index 000000000000..6fe18b9c1684
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/* Phase 01
+This class was imported and adapted from the System.ClientModel Unit Tests.
+https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockPipelineResponse.cs
+*/
+
+using System;
+using System.ClientModel.Primitives;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests;
+
+public class MockPipelineResponse : PipelineResponse
+{
+ private int _status;
+ private string _reasonPhrase;
+ private Stream? _contentStream;
+ private BinaryData? _bufferedContent;
+
+ private readonly PipelineResponseHeaders _headers;
+
+ private bool _disposed;
+
+ public MockPipelineResponse(int status = 0, string reasonPhrase = "")
+ {
+ this._status = status;
+ this._reasonPhrase = reasonPhrase;
+ this._headers = new MockResponseHeaders();
+ }
+
+ public override int Status => this._status;
+
+ public void SetStatus(int value) => this._status = value;
+
+ public override string ReasonPhrase => this._reasonPhrase;
+
+ public void SetReasonPhrase(string value) => this._reasonPhrase = value;
+
+ public void SetContent(byte[] content)
+ {
+ this.ContentStream = new MemoryStream(content, 0, content.Length, false, true);
+ }
+
+ public MockPipelineResponse SetContent(string content)
+ {
+ this.SetContent(Encoding.UTF8.GetBytes(content));
+ return this;
+ }
+
+ public override Stream? ContentStream
+ {
+ get => this._contentStream;
+ set => this._contentStream = value;
+ }
+
+ public override BinaryData Content
+ {
+ get
+ {
+ if (this._contentStream is null)
+ {
+ return new BinaryData(Array.Empty());
+ }
+
+ if (this.ContentStream is not MemoryStream memoryContent)
+ {
+ throw new InvalidOperationException("The response is not buffered.");
+ }
+
+ if (memoryContent.TryGetBuffer(out ArraySegment segment))
+ {
+ return new BinaryData(segment.AsMemory());
+ }
+ return new BinaryData(memoryContent.ToArray());
+ }
+ }
+
+ protected override PipelineResponseHeaders HeadersCore
+ => this._headers;
+
+ public sealed override void Dispose()
+ {
+ this.Dispose(true);
+
+ GC.SuppressFinalize(this);
+ }
+
+ protected void Dispose(bool disposing)
+ {
+ if (disposing && !this._disposed)
+ {
+ Stream? content = this._contentStream;
+ if (content != null)
+ {
+ this._contentStream = null;
+ content.Dispose();
+ }
+
+ this._disposed = true;
+ }
+ }
+
+ public override BinaryData BufferContent(CancellationToken cancellationToken = default)
+ {
+ if (this._bufferedContent is not null)
+ {
+ return this._bufferedContent;
+ }
+
+ if (this._contentStream is null)
+ {
+ this._bufferedContent = new BinaryData(Array.Empty());
+ return this._bufferedContent;
+ }
+
+ MemoryStream bufferStream = new();
+ this._contentStream.CopyTo(bufferStream);
+ this._contentStream.Dispose();
+ this._contentStream = bufferStream;
+
+ // Less efficient FromStream method called here because it is a mock.
+ // For intended production implementation, see HttpClientTransportResponse.
+ this._bufferedContent = BinaryData.FromStream(bufferStream);
+ return this._bufferedContent;
+ }
+
+ public override async ValueTask BufferContentAsync(CancellationToken cancellationToken = default)
+ {
+ if (this._bufferedContent is not null)
+ {
+ return this._bufferedContent;
+ }
+
+ if (this._contentStream is null)
+ {
+ this._bufferedContent = new BinaryData(Array.Empty());
+ return this._bufferedContent;
+ }
+
+ MemoryStream bufferStream = new();
+
+ await this._contentStream.CopyToAsync(bufferStream, cancellationToken).ConfigureAwait(false);
+ await this._contentStream.DisposeAsync().ConfigureAwait(false);
+
+ this._contentStream = bufferStream;
+
+ // Less efficient FromStream method called here because it is a mock.
+ // For intended production implementation, see HttpClientTransportResponse.
+ this._bufferedContent = BinaryData.FromStream(bufferStream);
+ return this._bufferedContent;
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs
new file mode 100644
index 000000000000..fceef64e4bae
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/* Phase 01
+This class was imported and adapted from the System.ClientModel Unit Tests.
+https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockResponseHeaders.cs
+*/
+
+using System;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+
+namespace SemanticKernel.Connectors.OpenAI.UnitTests;
+
+public class MockResponseHeaders : PipelineResponseHeaders
+{
+ private readonly Dictionary _headers;
+
+ public MockResponseHeaders()
+ {
+ this._headers = new Dictionary();
+ }
+
+ public override IEnumerator> GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override bool TryGetValue(string name, out string? value)
+ {
+ return this._headers.TryGetValue(name, out value);
+ }
+
+ public override bool TryGetValues(string name, out IEnumerable? values)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj
index d5e129765dc9..b17b14eb91ef 100644
--- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj
@@ -30,5 +30,6 @@
+
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs
new file mode 100644
index 000000000000..d11e2799addd
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/*
+Phase 01
+
+This class was created to simplify any Text Embeddings Support from the v1 ClientCore
+*/
+
+using System;
+using System.ClientModel;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenAI.Embeddings;
+
+#pragma warning disable CA2208 // Instantiate argument exceptions correctly
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Base class for AI clients that provides common functionality for interacting with OpenAI services.
+///
+internal partial class ClientCore
+{
+ ///
+ /// Generates an embedding from the given .
+ ///
+ /// List of strings to generate embeddings for
+ /// The containing services, plugins, and other state for use throughout the operation.
+ /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models.
+ /// The to monitor for cancellation requests. The default is .
+ /// List of embeddings
+ internal async Task>> GetEmbeddingsAsync(
+ IList data,
+ Kernel? kernel,
+ int? dimensions,
+ CancellationToken cancellationToken)
+ {
+ var result = new List>(data.Count);
+
+ if (data.Count > 0)
+ {
+ var embeddingsOptions = new EmbeddingGenerationOptions()
+ {
+ Dimensions = dimensions
+ };
+
+ ClientResult response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.ModelId).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false);
+ var embeddings = response.Value;
+
+ if (embeddings.Count != data.Count)
+ {
+ throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}");
+ }
+
+ for (var i = 0; i < embeddings.Count; i++)
+ {
+ result.Add(embeddings[i].Vector);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs
new file mode 100644
index 000000000000..12ca2f3d92fe
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs
@@ -0,0 +1,187 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/*
+Phase 01 : This class was created adapting and merging ClientCore and OpenAIClientCore classes.
+System.ClientModel changes were added and adapted to the code as this package is now used as a dependency over OpenAI package.
+All logic from original ClientCore and OpenAIClientCore were preserved.
+*/
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel.Http;
+using OpenAI;
+
+#pragma warning disable CA2208 // Instantiate argument exceptions correctly
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Base class for AI clients that provides common functionality for interacting with OpenAI services.
+///
+internal partial class ClientCore
+{
+ ///
+ /// Default OpenAI API endpoint.
+ ///
+ private const string OpenAIV1Endpoint = "https://api.openai.com/v1";
+
+ ///
+ /// Identifier of the default model to use
+ ///
+ internal string ModelId { get; init; } = string.Empty;
+
+ ///
+ /// Non-default endpoint for OpenAI API.
+ ///
+ internal Uri? Endpoint { get; init; }
+
+ ///
+ /// Logger instance
+ ///
+ internal ILogger Logger { get; init; }
+
+ ///
+ /// OpenAI / Azure OpenAI Client
+ ///
+ internal OpenAIClient Client { get; }
+
+ ///
+ /// Storage for AI service attributes.
+ ///
+ internal Dictionary Attributes { get; } = [];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Model name.
+ /// OpenAI API Key.
+ /// OpenAI compatible API endpoint.
+ /// OpenAI Organization Id (usually optional).
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ internal ClientCore(
+ string modelId,
+ string? apiKey = null,
+ Uri? endpoint = null,
+ string? organizationId = null,
+ HttpClient? httpClient = null,
+ ILogger? logger = null)
+ {
+ Verify.NotNullOrWhiteSpace(modelId);
+
+ this.Logger = logger ?? NullLogger.Instance;
+ this.ModelId = modelId;
+
+ // Accepts the endpoint if provided, otherwise uses the default OpenAI endpoint.
+ this.Endpoint = endpoint ?? httpClient?.BaseAddress;
+ if (this.Endpoint is null)
+ {
+ Verify.NotNullOrWhiteSpace(apiKey); // For Public OpenAI Endpoint a key must be provided.
+ this.Endpoint = new Uri(OpenAIV1Endpoint);
+ }
+
+ var options = GetOpenAIClientOptions(httpClient, this.Endpoint);
+ if (!string.IsNullOrWhiteSpace(organizationId))
+ {
+ options.AddPolicy(new AddHeaderRequestPolicy("OpenAI-Organization", organizationId!), PipelinePosition.PerCall);
+ }
+
+ this.Client = new OpenAIClient(apiKey ?? string.Empty, options);
+ }
+
+ ///
+ /// Initializes a new instance of the class using the specified OpenAIClient.
+ /// Note: instances created this way might not have the default diagnostics settings,
+ /// it's up to the caller to configure the client.
+ ///
+ /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource
+ /// Custom .
+ /// The to use for logging. If null, no logging will be performed.
+ internal ClientCore(
+ string modelId,
+ OpenAIClient openAIClient,
+ ILogger? logger = null)
+ {
+ Verify.NotNullOrWhiteSpace(modelId);
+ Verify.NotNull(openAIClient);
+
+ this.Logger = logger ?? NullLogger.Instance;
+ this.ModelId = modelId;
+ this.Client = openAIClient;
+ }
+
+ ///
+ /// Logs OpenAI action details.
+ ///
+ /// Caller member name. Populated automatically by runtime.
+ internal void LogActionDetails([CallerMemberName] string? callerMemberName = default)
+ {
+ if (this.Logger.IsEnabled(LogLevel.Information))
+ {
+ this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.ModelId);
+ }
+ }
+
+ ///
+ /// Allows adding attributes to the client.
+ ///
+ /// Attribute key.
+ /// Attribute value.
+ internal void AddAttribute(string key, string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ this.Attributes.Add(key, value);
+ }
+ }
+
+ /// Gets options to use for an OpenAIClient
+ /// Custom for HTTP requests.
+ /// Endpoint for the OpenAI API.
+ /// An instance of .
+ private static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, Uri? endpoint)
+ {
+ OpenAIClientOptions options = new()
+ {
+ ApplicationId = HttpHeaderConstant.Values.UserAgent,
+ Endpoint = endpoint
+ };
+
+ options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall);
+
+ if (httpClient is not null)
+ {
+ options.Transport = new HttpClientPipelineTransport(httpClient);
+ options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided.
+ options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout
+ }
+
+ return options;
+ }
+
+ ///
+ /// Invokes the specified request and handles exceptions.
+ ///
+ /// Type of the response.
+ /// Request to invoke.
+ /// Returns the response.
+ private static async Task RunRequestAsync(Func> request)
+ {
+ try
+ {
+ return await request.Invoke().ConfigureAwait(false);
+ }
+ catch (ClientResultException e)
+ {
+ throw e.ToHttpOperationException();
+ }
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs
new file mode 100644
index 000000000000..2279d639c54e
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/* Phase 1
+Added from OpenAI v1 with adapted logic to the System.ClientModel abstraction
+*/
+
+using System.ClientModel.Primitives;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Helper class to inject headers into System ClientModel Http pipeline
+///
+internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : PipelineSynchronousPolicy
+{
+ private readonly string _headerName = headerName;
+ private readonly string _headerValue = headerValue;
+
+ public override void OnSendingRequest(PipelineMessage message)
+ {
+ message.Request.Headers.Add(this._headerName, this._headerValue);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs
new file mode 100644
index 000000000000..b7690ead8b7f
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/*
+Phase 1
+As SystemClient model does not have any specialization or extension ATM, introduced this class with the adapted to use System.ClientModel abstractions.
+https://github.com/Azure/azure-sdk-for-net/blob/8bd22837639d54acccc820e988747f8d28bbde4a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineSynchronousPolicy.cs
+*/
+
+using System;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Represents a that doesn't do any asynchronous or synchronously blocking operations.
+///
+internal class PipelineSynchronousPolicy : PipelinePolicy
+{
+ private static readonly Type[] s_onReceivedResponseParameters = new[] { typeof(PipelineMessage) };
+
+ private readonly bool _hasOnReceivedResponse = true;
+
+ ///
+ /// Initializes a new instance of
+ ///
+ protected PipelineSynchronousPolicy()
+ {
+ var onReceivedResponseMethod = this.GetType().GetMethod(nameof(OnReceivedResponse), BindingFlags.Instance | BindingFlags.Public, null, s_onReceivedResponseParameters, null);
+ if (onReceivedResponseMethod != null)
+ {
+ this._hasOnReceivedResponse = onReceivedResponseMethod.GetBaseDefinition().DeclaringType != onReceivedResponseMethod.DeclaringType;
+ }
+ }
+
+ ///
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.OnSendingRequest(message);
+ if (pipeline.Count > currentIndex + 1)
+ {
+ // If there are more policies in the pipeline, continue processing
+ ProcessNext(message, pipeline, currentIndex);
+ }
+ this.OnReceivedResponse(message);
+ }
+
+ ///
+ public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ if (!this._hasOnReceivedResponse)
+ {
+ // If OnReceivedResponse was not overridden we can avoid creating a state machine and return the task directly
+ this.OnSendingRequest(message);
+ if (pipeline.Count > currentIndex + 1)
+ {
+ // If there are more policies in the pipeline, continue processing
+ return ProcessNextAsync(message, pipeline, currentIndex);
+ }
+ }
+
+ return this.InnerProcessAsync(message, pipeline, currentIndex);
+ }
+
+ private async ValueTask InnerProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.OnSendingRequest(message);
+ if (pipeline.Count > currentIndex + 1)
+ {
+ // If there are more policies in the pipeline, continue processing
+ await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
+ }
+ this.OnReceivedResponse(message);
+ }
+
+ ///
+ /// Method is invoked before the request is sent.
+ ///
+ /// The containing the request.
+ public virtual void OnSendingRequest(PipelineMessage message) { }
+
+ ///
+ /// Method is invoked after the response is received.
+ ///
+ /// The containing the response.
+ public virtual void OnReceivedResponse(PipelineMessage message) { }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs
new file mode 100644
index 000000000000..7da92e5826ba
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/*
+Phase 01:
+This class is introduced in exchange for the original RequestExceptionExtensions class of Azure.Core to the new ClientException from System.ClientModel,
+Preserved the logic as is.
+*/
+
+using System.ClientModel;
+using System.Net;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// Provides extension methods for the class.
+///
+internal static class ClientResultExceptionExtensions
+{
+ ///
+ /// Converts a to an .
+ ///
+ /// The original .
+ /// An instance.
+ public static HttpOperationException ToHttpOperationException(this ClientResultException exception)
+ {
+ const int NoResponseReceived = 0;
+
+ string? responseContent = null;
+
+ try
+ {
+ responseContent = exception.GetRawResponse()?.Content.ToString();
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead.
+#pragma warning restore CA1031
+
+ return new HttpOperationException(
+ exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status,
+ responseContent,
+ exception.Message,
+ exception);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs
new file mode 100644
index 000000000000..49915031b7fc
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Embeddings;
+using Microsoft.SemanticKernel.Services;
+using OpenAI;
+
+namespace Microsoft.SemanticKernel.Connectors.OpenAI;
+
+///
+/// OpenAI implementation of
+///
+[Experimental("SKEXP0010")]
+public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService
+{
+ private readonly ClientCore _core;
+ private readonly int? _dimensions;
+
+ ///
+ /// Create an instance of
+ ///
+ /// Model name
+ /// OpenAI API Key
+ /// OpenAI Organization Id (usually optional)
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models.
+ public OpenAITextEmbeddingGenerationService(
+ string modelId,
+ string apiKey,
+ string? organization = null,
+ HttpClient? httpClient = null,
+ ILoggerFactory? loggerFactory = null,
+ int? dimensions = null)
+ {
+ this._core = new(
+ modelId: modelId,
+ apiKey: apiKey,
+ organizationId: organization,
+ httpClient: httpClient,
+ logger: loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
+
+ this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId);
+
+ this._dimensions = dimensions;
+ }
+
+ ///
+ /// Create an instance of the OpenAI text embedding connector
+ ///
+ /// Model name
+ /// Custom for HTTP requests.
+ /// The to use for logging. If null, no logging will be performed.
+ /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models.
+ public OpenAITextEmbeddingGenerationService(
+ string modelId,
+ OpenAIClient openAIClient,
+ ILoggerFactory? loggerFactory = null,
+ int? dimensions = null)
+ {
+ this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService)));
+ this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId);
+
+ this._dimensions = dimensions;
+ }
+
+ ///
+ public IReadOnlyDictionary Attributes => this._core.Attributes;
+
+ ///
+ public Task>> GenerateEmbeddingsAsync(
+ IList data,
+ Kernel? kernel = null,
+ CancellationToken cancellationToken = default)
+ {
+ this._core.LogActionDetails();
+ return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken);
+ }
+}
diff --git a/dotnet/src/IntegrationTestsV2/.editorconfig b/dotnet/src/IntegrationTestsV2/.editorconfig
new file mode 100644
index 000000000000..394eef685f21
--- /dev/null
+++ b/dotnet/src/IntegrationTestsV2/.editorconfig
@@ -0,0 +1,6 @@
+# Suppressing errors for Test projects under dotnet folder
+[*.cs]
+dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs
new file mode 100644
index 000000000000..6eca1909a546
--- /dev/null
+++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Microsoft.SemanticKernel.Embeddings;
+using SemanticKernel.IntegrationTests.TestSettings;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Connectors.OpenAI;
+
+public sealed class OpenAITextEmbeddingTests
+{
+ private const int AdaVectorLength = 1536;
+ private readonly IConfigurationRoot _configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")]
+ [InlineData("test sentence")]
+ public async Task OpenAITestAsync(string testInputString)
+ {
+ // Arrange
+ OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get();
+ Assert.NotNull(openAIConfiguration);
+
+ var embeddingGenerator = new OpenAITextEmbeddingGenerationService(openAIConfiguration.ModelId, openAIConfiguration.ApiKey);
+
+ // Act
+ var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString);
+ var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]);
+
+ // Assert
+ Assert.Equal(AdaVectorLength, singleResult.Length);
+ Assert.Equal(3, batchResult.Count);
+ }
+
+ [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")]
+ [InlineData(null, 3072)]
+ [InlineData(1024, 1024)]
+ public async Task OpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength)
+ {
+ // Arrange
+ const string TestInputString = "test sentence";
+
+ OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get();
+ Assert.NotNull(openAIConfiguration);
+
+ var embeddingGenerator = new OpenAITextEmbeddingGenerationService(
+ "text-embedding-3-large",
+ openAIConfiguration.ApiKey,
+ dimensions: dimensions);
+
+ // Act
+ var result = await embeddingGenerator.GenerateEmbeddingAsync(TestInputString);
+
+ // Assert
+ Assert.Equal(expectedVectorLength, result.Length);
+ }
+}
diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj
index cbfbfe9e4df3..f3c704a27307 100644
--- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj
+++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj
@@ -1,7 +1,7 @@
-
+
IntegrationTests
- SemanticKernel.IntegrationTests
+ SemanticKernel.IntegrationTestsV2
net8.0
true
false
@@ -16,7 +16,7 @@
-
+
@@ -44,7 +44,6 @@
-
@@ -64,4 +63,5 @@
Always
+
\ No newline at end of file
diff --git a/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs b/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs
new file mode 100644
index 000000000000..cb3884e3bdfc
--- /dev/null
+++ b/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SemanticKernel.IntegrationTests.TestSettings;
+
+[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated",
+ Justification = "Configuration classes are instantiated through IConfiguration.")]
+internal sealed class OpenAIConfiguration(string serviceId, string modelId, string apiKey, string? chatModelId = null)
+{
+ public string ServiceId { get; set; } = serviceId;
+ public string ModelId { get; set; } = modelId;
+ public string? ChatModelId { get; set; } = chatModelId;
+ public string ApiKey { get; set; } = apiKey;
+}