Skip to content

Commit

Permalink
Fix exception occurring when Azure OpenAI service is throttling reque…
Browse files Browse the repository at this point in the history
…sts (#876)

## Motivation and Context (Why the change? What's the scenario?)

When retrying requests after throttling, the Azure OpenAI SDK is sending
malformed requests, with multiple `Authorization` headers, which are
rejected by the Azure OpenAI service with a `401 (Unauthorized)` error
code, leading to an exception in the SDK.

See 
- Azure/azure-sdk-for-net#46109
- microsoft/semantic-kernel#8929
- #855

## High level description (Approach, Design)

Inject a policy to fix malformed HTTP headers.

Functional test included, to verify the fix.
  • Loading branch information
dluc authored Nov 1, 2024
1 parent 689cace commit b67dfe7
Show file tree
Hide file tree
Showing 17 changed files with 216 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .github/_typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ extend-exclude = [
"encoder.json",
"appsettings.development.json",
"appsettings.Development.json",
"AzureAISearchFilteringTest.cs"
"AzureAISearchFilteringTest.cs",
"KernelMemory.sln.DotSettings"
]

[default.extend-words]
Expand Down
8 changes: 7 additions & 1 deletion KernelMemory.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\extensions\AzureOpenAI\AzureOpenAI.csproj" />
<ProjectReference Include="..\..\..\extensions\AzureOpenAI\AzureOpenAI\AzureOpenAI.csproj" />
<ProjectReference Include="..\..\evaluation\Evaluation.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<AssemblyName>Microsoft.AzureOpenAI.FunctionalTests</AssemblyName>
<RootNamespace>Microsoft.AzureOpenAI.FunctionalTests</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<RollForward>LatestMajor</RollForward>
<IsTestProject>true</IsTestProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<NoWarn>KMEXP01;</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\service\tests\TestHelpers\TestHelpers.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.abstractions" />
<PackageReference Include="Xunit.DependencyInjection" />
<PackageReference Include="Xunit.DependencyInjection.Logging" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
87 changes: 87 additions & 0 deletions extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Issue855Test.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
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
*/
22 changes: 22 additions & 0 deletions extensions/AzureOpenAI/AzureOpenAI.FunctionalTests/Startup.cs
Original file line number Diff line number Diff line change
@@ -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<Startup>()
.AddEnvironmentVariables()
.Build();

hostBuilder.ConfigureHostConfiguration(builder => builder.AddConfiguration(config));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"KernelMemory": {
"Services": {
"AzureOpenAIEmbedding": {
"Auth": "AzureIdentity", // "ApiKey" or "AzureIdentity"
"Endpoint": "https://<...>.openai.azure.com/",
"APIKey": "",
"Deployment": ""
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\service\Abstractions\Abstractions.csproj" />
<ProjectReference Include="..\OpenAI\OpenAI\OpenAI.csproj" />
<ProjectReference Include="..\..\..\service\Abstractions\Abstractions.csproj" />
<ProjectReference Include="..\..\OpenAI\OpenAI\OpenAI.csproj" />
</ItemGroup>

<ItemGroup>
Expand All @@ -28,7 +28,7 @@
</PropertyGroup>

<ItemGroup>
<None Include="README.md" Link="README.md" Pack="true" PackagePath="." Visible="false" />
<None Include="..\README.md" Link="README.md" Pack="true" PackagePath="." Visible="false" />
</ItemGroup>

</Project>
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Bug fix: Remove duplicate Authorization headers from the request.
/// See https://github.com/Azure/azure-sdk-for-net/issues/46109
/// </summary>
internal sealed class SingleAuthorizationHeaderPolicy : PipelinePolicy
{
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RemoveDuplicateHeader(message.Request.Headers);
ProcessNext(message, pipeline, currentIndex);
}

public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> 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);
}
}
}
2 changes: 1 addition & 1 deletion extensions/KM/KernelMemory/KernelMemory.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<ProjectReference Include="..\..\AzureAIDocIntel\AzureAIDocIntel.csproj" />
<ProjectReference Include="..\..\AzureAISearch\AzureAISearch\AzureAISearch.csproj" />
<ProjectReference Include="..\..\AzureBlobs\AzureBlobs.csproj" />
<ProjectReference Include="..\..\AzureOpenAI\AzureOpenAI.csproj" />
<ProjectReference Include="..\..\AzureOpenAI\AzureOpenAI\AzureOpenAI.csproj" />
<ProjectReference Include="..\..\AzureQueues\AzureQueues.csproj" />
<ProjectReference Include="..\..\Elasticsearch\Elasticsearch\Elasticsearch.csproj" />
<ProjectReference Include="..\..\LlamaSharp\LlamaSharp\LlamaSharp.csproj" />
Expand Down

0 comments on commit b67dfe7

Please sign in to comment.