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 @@ - +