diff --git a/.github/_typos.toml b/.github/_typos.toml
index a29e3354a..5fa7634f9 100644
--- a/.github/_typos.toml
+++ b/.github/_typos.toml
@@ -15,7 +15,8 @@ extend-exclude = [
"encoder.json",
"appsettings.development.json",
"appsettings.Development.json",
- "AzureAISearchFilteringTest.cs"
+ "AzureAISearchFilteringTest.cs",
+ "KernelMemory.sln.DotSettings"
]
[default.extend-words]
diff --git a/KernelMemory.sln b/KernelMemory.sln
index 20c453af1..39151eaec 100644
--- a/KernelMemory.sln
+++ b/KernelMemory.sln
@@ -187,7 +187,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureQueues", "extensions\A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAIDocIntel", "extensions\AzureAIDocIntel\AzureAIDocIntel.csproj", "{CFE7C192-2561-40CC-8592-136293451EC1}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureOpenAI", "extensions\AzureOpenAI\AzureOpenAI.csproj", "{93FA6DD6-D0B2-4751-8680-3F959E1F7AF2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureOpenAI", "extensions\AzureOpenAI\AzureOpenAI\AzureOpenAI.csproj", "{93FA6DD6-D0B2-4751-8680-3F959E1F7AF2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAISearch.TestApplication", "extensions\AzureAISearch\AzureAISearch.TestApplication\AzureAISearch.TestApplication.csproj", "{11445C36-1B94-4AFB-AC23-976C94924603}"
EndProject
@@ -331,6 +331,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAIContentSafety", "ext
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KernelMemory", "extensions\KM\KernelMemory\KernelMemory.csproj", "{AB097B62-5A0B-4D74-9F8B-A41FE8241447}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureOpenAI.FunctionalTests", "extensions\AzureOpenAI\AzureOpenAI.FunctionalTests\AzureOpenAI.FunctionalTests.csproj", "{8E907766-4A7D-46E2-B5E3-EB2994B1AA54}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -613,6 +615,9 @@ Global
{AB097B62-5A0B-4D74-9F8B-A41FE8241447}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB097B62-5A0B-4D74-9F8B-A41FE8241447}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB097B62-5A0B-4D74-9F8B-A41FE8241447}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E907766-4A7D-46E2-B5E3-EB2994B1AA54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E907766-4A7D-46E2-B5E3-EB2994B1AA54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E907766-4A7D-46E2-B5E3-EB2994B1AA54}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -711,6 +716,7 @@ Global
{82670921-FDCD-4672-84BD-4353F5AC24A0} = {3C17F42B-CFC8-4900-8CFB-88936311E919}
{58E65B3F-EFF0-401A-AC76-A49835AE0220} = {155DA079-E267-49AF-973A-D1D44681970F}
{AB097B62-5A0B-4D74-9F8B-A41FE8241447} = {155DA079-E267-49AF-973A-D1D44681970F}
+ {8E907766-4A7D-46E2-B5E3-EB2994B1AA54} = {3C17F42B-CFC8-4900-8CFB-88936311E919}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CC136C62-115C-41D1-B414-F9473EFF6EA8}
diff --git a/applications/tests/Evaluation.Tests/Evaluation.FunctionalTests.csproj b/applications/tests/Evaluation.Tests/Evaluation.FunctionalTests.csproj
index deea41ea5..0e117d284 100644
--- a/applications/tests/Evaluation.Tests/Evaluation.FunctionalTests.csproj
+++ b/applications/tests/Evaluation.Tests/Evaluation.FunctionalTests.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/AzureOpenAI.FunctionalTests.csproj b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/AzureOpenAI.FunctionalTests.csproj
new file mode 100644
index 000000000..3f17302b5
--- /dev/null
+++ b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/AzureOpenAI.FunctionalTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ Microsoft.AzureOpenAI.FunctionalTests
+ Microsoft.AzureOpenAI.FunctionalTests
+ net8.0
+ LatestMajor
+ true
+ enable
+ enable
+ false
+ KMEXP01;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
\ No newline at end of file
diff --git a/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Issue855Test.cs b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Issue855Test.cs
new file mode 100644
index 000000000..f2a3ab768
--- /dev/null
+++ b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Issue855Test.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.KernelMemory.AI.AzureOpenAI;
+using Microsoft.KM.TestHelpers;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AzureOpenAI.FunctionalTests;
+
+///
+/// References:
+/// - https://github.com/Azure/azure-sdk-for-net/issues/46109
+/// - https://github.com/microsoft/semantic-kernel/issues/8929
+/// - https://github.com/microsoft/kernel-memory/issues/855
+///
+public class Issue855Test : BaseFunctionalTestCase
+{
+ private readonly AzureOpenAITextEmbeddingGenerator _target;
+
+ public Issue855Test(IConfiguration cfg, ITestOutputHelper output) : base(cfg, output)
+ {
+ this._target = new AzureOpenAITextEmbeddingGenerator(this.AzureOpenAIEmbeddingConfiguration);
+ }
+
+ [Fact(Skip = "Enable and run manually")]
+ [Trait("Category", "Manual")]
+ [Trait("Category", "BugFix")]
+ public async Task ItDoesntWhenThrottling()
+ {
+ for (int i = 0; i < 50; i++)
+ {
+ Console.WriteLine($"## {i}");
+ await this._target.GenerateEmbeddingBatchAsync(
+ [RndStr(), RndStr(), RndStr(), RndStr(), RndStr(), RndStr(), RndStr(), RndStr(), RndStr(), RndStr()]);
+ }
+ }
+
+#pragma warning disable CA5394
+ private static string RndStr()
+ {
+ var random = new Random();
+ return new(Enumerable.Repeat(" ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 ", 8000)
+ .Select(s => s[random.Next(s.Length)]).ToArray());
+ }
+}
+
+#pragma warning disable IDE0055
+/* When the test fails: after pausing and trying to restart, an exception occurs.
+
+Microsoft.SemanticKernel.HttpOperationException: Service request failed.
+
+Microsoft.SemanticKernel.HttpOperationException
+Service request failed.
+Status: 401 (Unauthorized) <===== ******* Caused by https://github.com/Azure/azure-sdk-for-net/issues/46109
+
+ at Microsoft.SemanticKernel.Connectors.OpenAI.ClientCore.RunRequestAsync[T](Func`1 request)
+ at Microsoft.SemanticKernel.Connectors.OpenAI.ClientCore.GetEmbeddingsAsync(String targetModel, IList`1 data, Kernel kernel, Nullable`1 dimensions, CancellationToken cancellationToken)
+ at Microsoft.KernelMemory.AI.AzureOpenAI.AzureOpenAITextEmbeddingGenerator.GenerateEmbeddingBatchAsync(IEnumerable`1 textList, CancellationToken cancellationToken) in extensions/AzureOpenAI/AzureOpenAI/AzureOpenAITextEmbeddingGenerator.cs:line 132
+ at Microsoft.AzureOpenAI.FunctionalTests.Issue855Test.ItDoesntFailWith401() in extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Bug46109Test.cs:line 43
+ at Xunit.DependencyInjection.DependencyInjectionTestInvoker.AsyncStack(Task task, Activity activity) in S:\GitHub\Xunit.DependencyInjection\src\Xunit.DependencyInjection\DependencyInjectionTestInvoker.cs:line 174
+
+System.ClientModel.ClientResultException
+Service request failed.
+Status: 401 (Unauthorized)
+
+ at Azure.AI.OpenAI.ClientPipelineExtensions.ProcessMessageAsync(ClientPipeline pipeline, PipelineMessage message, RequestOptions options)
+ at Azure.AI.OpenAI.Embeddings.AzureEmbeddingClient.GenerateEmbeddingsAsync(BinaryContent content, RequestOptions options)
+ at OpenAI.Embeddings.EmbeddingClient.GenerateEmbeddingsAsync(IEnumerable`1 inputs, EmbeddingGenerationOptions options, CancellationToken cancellationToken)
+ at Microsoft.SemanticKernel.Connectors.OpenAI.ClientCore.RunRequestAsync[T](Func`1 request)
+
+
+
+warn: Microsoft.KernelMemory.AI.AzureOpenAI.AzureOpenAITextEmbeddingGenerator[0]
+ Tokenizer not specified, will use GPT4oTokenizer. The token count might be incorrect, causing unexpected errors
+
+## 0
+## 1
+## 2
+## 3
+...
+...
+warn: Microsoft.KernelMemory.AI.AzureOpenAI.Internals.ClientSequentialRetryPolicy[0]
+ Header Retry-After found, value 21
+
+warn: Microsoft.KernelMemory.AI.AzureOpenAI.Internals.ClientSequentialRetryPolicy[0]
+ Delay extracted from HTTP response: 21000 msecs
+*/
diff --git a/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Startup.cs b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Startup.cs
new file mode 100644
index 000000000..c82737d51
--- /dev/null
+++ b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Startup.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+/* IMPORTANT: the Startup class must be at the root of the namespace and
+ * the namespace must match exactly (required by Xunit.DependencyInjection) */
+
+namespace Microsoft.AzureOpenAI.FunctionalTests;
+
+public class Startup
+{
+ public void ConfigureHost(IHostBuilder hostBuilder)
+ {
+ var config = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .AddJsonFile("appsettings.development.json", optional: true)
+ .AddJsonFile("appsettings.Development.json", optional: true)
+ .AddUserSecrets()
+ .AddEnvironmentVariables()
+ .Build();
+
+ hostBuilder.ConfigureHostConfiguration(builder => builder.AddConfiguration(config));
+ }
+}
diff --git a/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/appsettings.json b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/appsettings.json
new file mode 100644
index 000000000..e09ed97ec
--- /dev/null
+++ b/extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "KernelMemory": {
+ "Services": {
+ "AzureOpenAIEmbedding": {
+ "Auth": "AzureIdentity", // "ApiKey" or "AzureIdentity"
+ "Endpoint": "https://<...>.openai.azure.com/",
+ "APIKey": "",
+ "Deployment": ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions/AzureOpenAI/AzureOpenAI.csproj b/extensions/AzureOpenAI/AzureOpenAI/AzureOpenAI.csproj
similarity index 83%
rename from extensions/AzureOpenAI/AzureOpenAI.csproj
rename to extensions/AzureOpenAI/AzureOpenAI/AzureOpenAI.csproj
index 3e910d2a1..a2c44bc35 100644
--- a/extensions/AzureOpenAI/AzureOpenAI.csproj
+++ b/extensions/AzureOpenAI/AzureOpenAI/AzureOpenAI.csproj
@@ -9,8 +9,8 @@
-
-
+
+
@@ -28,7 +28,7 @@
-
+
diff --git a/extensions/AzureOpenAI/AzureOpenAIConfig.cs b/extensions/AzureOpenAI/AzureOpenAI/AzureOpenAIConfig.cs
similarity index 100%
rename from extensions/AzureOpenAI/AzureOpenAIConfig.cs
rename to extensions/AzureOpenAI/AzureOpenAI/AzureOpenAIConfig.cs
diff --git a/extensions/AzureOpenAI/AzureOpenAITextEmbeddingGenerator.cs b/extensions/AzureOpenAI/AzureOpenAI/AzureOpenAITextEmbeddingGenerator.cs
similarity index 100%
rename from extensions/AzureOpenAI/AzureOpenAITextEmbeddingGenerator.cs
rename to extensions/AzureOpenAI/AzureOpenAI/AzureOpenAITextEmbeddingGenerator.cs
diff --git a/extensions/AzureOpenAI/AzureOpenAITextGenerator.cs b/extensions/AzureOpenAI/AzureOpenAI/AzureOpenAITextGenerator.cs
similarity index 100%
rename from extensions/AzureOpenAI/AzureOpenAITextGenerator.cs
rename to extensions/AzureOpenAI/AzureOpenAI/AzureOpenAITextGenerator.cs
diff --git a/extensions/AzureOpenAI/DependencyInjection.cs b/extensions/AzureOpenAI/AzureOpenAI/DependencyInjection.cs
similarity index 100%
rename from extensions/AzureOpenAI/DependencyInjection.cs
rename to extensions/AzureOpenAI/AzureOpenAI/DependencyInjection.cs
diff --git a/extensions/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs b/extensions/AzureOpenAI/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs
similarity index 95%
rename from extensions/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs
rename to extensions/AzureOpenAI/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs
index 3a2a6445f..9ccfdce8d 100644
--- a/extensions/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs
+++ b/extensions/AzureOpenAI/AzureOpenAI/Internals/AzureOpenAIClientBuilder.cs
@@ -29,6 +29,9 @@ internal static AzureOpenAIClient Build(
UserAgentApplicationId = Telemetry.HttpUserAgent,
};
+ // See https://github.com/Azure/azure-sdk-for-net/issues/46109
+ options.AddPolicy(new SingleAuthorizationHeaderPolicy(), PipelinePosition.PerTry);
+
if (httpClient is not null)
{
options.Transport = new HttpClientPipelineTransport(httpClient);
diff --git a/extensions/AzureOpenAI/Internals/ClientSequentialRetryPolicy.cs b/extensions/AzureOpenAI/AzureOpenAI/Internals/ClientSequentialRetryPolicy.cs
similarity index 100%
rename from extensions/AzureOpenAI/Internals/ClientSequentialRetryPolicy.cs
rename to extensions/AzureOpenAI/AzureOpenAI/Internals/ClientSequentialRetryPolicy.cs
diff --git a/extensions/AzureOpenAI/AzureOpenAI/Internals/SingleAuthorizationHeaderPolicy.cs b/extensions/AzureOpenAI/AzureOpenAI/Internals/SingleAuthorizationHeaderPolicy.cs
new file mode 100644
index 000000000..3a1185bac
--- /dev/null
+++ b/extensions/AzureOpenAI/AzureOpenAI/Internals/SingleAuthorizationHeaderPolicy.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Microsoft.KernelMemory.AI.AzureOpenAI.Internals;
+
+///
+/// Bug fix: Remove duplicate Authorization headers from the request.
+/// See https://github.com/Azure/azure-sdk-for-net/issues/46109
+///
+internal sealed class SingleAuthorizationHeaderPolicy : PipelinePolicy
+{
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ RemoveDuplicateHeader(message.Request.Headers);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ RemoveDuplicateHeader(message.Request.Headers);
+ await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
+ }
+
+ private static void RemoveDuplicateHeader(PipelineRequestHeaders headers)
+ {
+ if (!headers.TryGetValues("Authorization", out var headerValues) || headerValues == null)
+ {
+ return;
+ }
+
+ using var enumerator = headerValues.GetEnumerator();
+
+ if (!enumerator.MoveNext()) { return; }
+
+ var firstValue = enumerator.Current;
+
+ // Check if there’s more than one value
+ if (enumerator.MoveNext())
+ {
+ headers.Set("Authorization", firstValue);
+ }
+ }
+}
diff --git a/extensions/AzureOpenAI/Internals/SkClientBuilder.cs b/extensions/AzureOpenAI/AzureOpenAI/Internals/SkClientBuilder.cs
similarity index 100%
rename from extensions/AzureOpenAI/Internals/SkClientBuilder.cs
rename to extensions/AzureOpenAI/AzureOpenAI/Internals/SkClientBuilder.cs
diff --git a/extensions/KM/KernelMemory/KernelMemory.csproj b/extensions/KM/KernelMemory/KernelMemory.csproj
index f2fe5e402..ea33c0c6b 100644
--- a/extensions/KM/KernelMemory/KernelMemory.csproj
+++ b/extensions/KM/KernelMemory/KernelMemory.csproj
@@ -19,7 +19,7 @@
-
+