Http client consistency (#1153)

### Motivation, Context and Description
This PR introduces the following changes:

1. Reuse of globally configured policies and handlers: The AzureOpenAI,
OpenAI, and custom connectors now use the globally configured policies
and handlers provided with the custom HttpClient, eliminating the need
for connector-specific implementations. This change improves consistency
and allows reuse of globally configured settings.

2. Mandatory HttpClient parameter: HttpClient is now a mandatory
parameter for AzureOpenAI, OpenAI, and custom connectors. This removes
the responsibility of connectors to create their own HttpClients and
manage(dispose) their lifetime, simplifying the code and promoting
centralized HttpClient management.

3. To minimize the number of handlers utilized by the SK SDK, all
AzureOpenAI, OpenAI, and custom connectors have been configured to reuse
the same instance of HttpClientHandler.

Co-authored-by: Stephen Toub <[email protected]>
Co-authored-by: Lee Miller <[email protected]>
This commit is contained in:
SergeyMenshykh
2023-05-25 12:21:14 -07:00
committed by GitHub
co-authored by Stephen Toub Lee Miller
parent 26d68e9912
commit 74452850a8
6 changed files with 175 additions and 44 deletions
@@ -20,30 +20,20 @@ using Microsoft.SemanticKernel.Text;
namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient;
#pragma warning disable CA1063 // Class isn't publicly extensible and thus doesn't implement the full IDisposable pattern
#pragma warning disable CA1816 // No derived types implement a finalizer
/// <summary>Base type for OpenAI clients.</summary>
public abstract class OpenAIClientBase : IDisposable
public abstract class OpenAIClientBase
{
/// <summary>Initialize the client.</summary>
private protected OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
/// <summary>
/// Initializes a new instance of the <see cref="OpenAIClientBase"/> class.
/// </summary>
/// <param name="httpClient">The HttpClient used for making HTTP requests.</param>
/// <param name="logger">The ILogger used for logging. If null, a NullLogger instance will be used.</param>
private protected OpenAIClientBase(HttpClient? httpClient, ILogger? logger = null)
{
this._httpClient = httpClient ?? new HttpClient(s_defaultHttpClientHandler, disposeHandler: false);
this._disposeHttpClient = this._httpClient != httpClient; // dispose a non-shared client when this is disposed
this._httpClient = httpClient ?? new HttpClient(NonDisposableHttpClientHandler.Instance, disposeHandler: false);
this._log = logger ?? NullLogger.Instance;
}
/// <summary>Clean up resources used by this instance.</summary>
public void Dispose()
{
if (this._disposeHttpClient)
{
this._httpClient.Dispose();
}
}
/// <summary>Adds headers to use for OpenAI HTTP requests.</summary>
private protected virtual void AddRequestHeaders(HttpRequestMessage request)
{
@@ -111,22 +101,16 @@ public abstract class OpenAIClientBase : IDisposable
#region private ================================================================================
// Shared singleton HttpClientHandler used when an existing HttpClient isn't provided
private static readonly HttpClientHandler s_defaultHttpClientHandler = new() { CheckCertificateRevocationList = true };
// HTTP user agent sent to remote endpoints
private const string HttpUserAgent = "Microsoft-Semantic-Kernel";
// Set to true to dispose of HttpClient when disposing. If HttpClient was passed in, then the caller can manage.
private readonly bool _disposeHttpClient;
/// <summary>
/// Logger
/// </summary>
private readonly ILogger _log;
/// <summary>
/// The <see cref="_httpClient"/> to use for issuing requests.
/// The HttpClient used for making HTTP requests.
/// </summary>
private readonly HttpClient _httpClient;
@@ -11,7 +11,6 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ImageGeneration;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Reliability;
#pragma warning disable IDE0130
// ReSharper disable once CheckNamespace - Using NS of KernelConfig
@@ -47,7 +46,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
apiKey,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
@@ -79,7 +78,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
credentials,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
@@ -111,7 +110,7 @@ public static class OpenAIKernelBuilderExtensions
modelId,
apiKey,
orgId,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
return builder;
@@ -146,7 +145,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
apiKey,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
return builder;
@@ -177,7 +176,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
credential,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
return builder;
@@ -208,7 +207,7 @@ public static class OpenAIKernelBuilderExtensions
modelId,
apiKey,
orgId,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
return builder;
@@ -244,7 +243,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
apiKey,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger);
builder.WithAIService<IChatCompletion>(serviceId, Factory, setAsDefault);
@@ -284,7 +283,7 @@ public static class OpenAIKernelBuilderExtensions
deploymentName,
endpoint,
credentials,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger);
builder.WithAIService<IChatCompletion>(serviceId, Factory, setAsDefault);
@@ -324,7 +323,7 @@ public static class OpenAIKernelBuilderExtensions
modelId,
apiKey,
orgId,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger);
builder.WithAIService<IChatCompletion>(serviceId, Factory, setAsDefault);
@@ -338,13 +337,6 @@ public static class OpenAIKernelBuilderExtensions
return builder;
}
private static HttpClient CreateHttpClient(this IDelegatingHandlerFactory handlerFactory, ILogger? logger)
{
var retryHandler = handlerFactory.Create(logger);
retryHandler.InnerHandler = new HttpClientHandler { CheckCertificateRevocationList = true };
return new HttpClient(retryHandler);
}
#endregion
#region Images
@@ -370,12 +362,31 @@ public static class OpenAIKernelBuilderExtensions
new OpenAIImageGeneration(
apiKey,
orgId,
httpClient ?? parameters.Config.HttpHandlerFactory.CreateHttpClient(parameters.Logger),
GetHttpClient(parameters.Config, httpClient, parameters.Logger),
parameters.Logger),
setAsDefault);
return builder;
}
/// <summary>
/// Retrieves an instance of HttpClient.
/// </summary>
/// <param name="config">The kernel configuration.</param>
/// <param name="httpClient">An optional pre-existing instance of HttpClient.</param>
/// <param name="logger">An optional logger.</param>
/// <returns>An instance of HttpClient.</returns>
private static HttpClient GetHttpClient(KernelConfig config, HttpClient? httpClient, ILogger? logger)
{
if (httpClient == null)
{
var retryHandler = config.HttpHandlerFactory.Create(logger);
retryHandler.InnerHandler = NonDisposableHttpClientHandler.Instance;
return new HttpClient(retryHandler, false); // We should refrain from disposing the underlying SK default HttpClient handler as it would impact other HTTP clients that utilize the same handler.
}
return httpClient;
}
#endregion
}
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http;
/// <summary>
/// Represents a singleton implementation of <see cref="HttpClientHandler"/> that is not disposable.
/// </summary>
internal sealed class NonDisposableHttpClientHandler : HttpClientHandler
{
/// <summary>
/// Private constructor to prevent direct instantiation of the class.
/// </summary>
private NonDisposableHttpClientHandler()
{
this.CheckCertificateRevocationList = true;
}
/// <summary>
/// Gets the singleton instance of <see cref="NonDisposableHttpClientHandler"/>.
/// </summary>
public static NonDisposableHttpClientHandler Instance { get; } = new NonDisposableHttpClientHandler();
/// <summary>
/// Disposes the underlying resources held by the <see cref="NonDisposableHttpClientHandler"/>.
/// This implementation does nothing to prevent unintended disposal, as it may affect all references.
/// </summary>
/// <param name="disposing">True if called from <see cref="Dispose"/>, false if called from a finalizer.</param>
#pragma warning disable CA2215 // Dispose methods should call base class dispose
protected override void Dispose(bool disposing)
#pragma warning restore CA2215 // Dispose methods should call base class dispose
{
// Do nothing if called explicitly from Dispose, as it may unintentionally affect all references.
// The base.Dispose(disposing) is not called to avoid invoking the disposal of HttpClientHandler resources.
// This implementation assumes that the HttpClientHandler is being used as a singleton and should not be disposed directly.
}
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
/**
* These examples show how to use HttpClient and HttpClientFactory within SK SDK.
*/
public static class Example41_HttpClientUsage
{
public static void Run()
{
//Examples showing how to use HttpClient.
UseDefaultHttpClientAsync();
UseCustomHttpClientAsync();
//Examples showing how to use HttpClientFactory.
UseBasicRegistrationWithHttpClientFactoryAsync();
UseNamedRegistrationWitHttpClientFactoryAsync();
}
/// <summary>
/// Demonstrates the usage of the default HttpClient provided by the SK SDK.
/// </summary>
private static void UseDefaultHttpClientAsync()
{
var kernel = Kernel.Builder
.WithOpenAITextCompletionService("<model-id>", "<api-key>") // If you need to use the default HttpClient from the SK SDK, simply omit the argument for the httpMessageInvoker parameter.
.Build();
}
/// <summary>
/// Demonstrates the usage of a custom HttpClient.
/// </summary>
private static void UseCustomHttpClientAsync()
{
using var httpClient = new HttpClient();
// If you need to use a custom HttpClient, simply pass it as an argument for the httpClient parameter.
var kernel = Kernel.Builder
.WithOpenAITextCompletionService("<model-id>", "<api-key>", httpClient: httpClient)
.Build();
}
/// <summary>
/// Demonstrates the "basic usage" approach for HttpClientFactory.
/// </summary>
private static void UseBasicRegistrationWithHttpClientFactoryAsync()
{
//More details - https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#basic-usage
var serviceCollection = new ServiceCollection();
serviceCollection.AddHttpClient();
var kernel = serviceCollection.AddTransient<IKernel>((sp) =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var kernel = Kernel.Builder
.WithOpenAITextCompletionService("<model-id>", "<api-key>", httpClient: factory.CreateClient())
.Build();
return kernel;
});
}
/// <summary>
/// Demonstrates the "named clients" approach for HttpClientFactory.
/// </summary>
private static void UseNamedRegistrationWitHttpClientFactoryAsync()
{
// More details https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#named-clients
var serviceCollection = new ServiceCollection();
serviceCollection.AddHttpClient();
//Registration of a named HttpClient.
serviceCollection.AddHttpClient("test-client", (client) =>
{
client.BaseAddress = new Uri("https://api.openai.com/v1/", UriKind.Absolute);
});
var kernel = serviceCollection.AddTransient<IKernel>((sp) =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var kernel = Kernel.Builder
.WithOpenAITextCompletionService("<model-id>", "<api-key>", httpClient: factory.CreateClient("test-client"))
.Build();
return kernel;
});
}
}
@@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0-preview.4.23259.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="7.2.3" />
@@ -123,9 +123,11 @@ public static class Program
Console.WriteLine("== DONE ==");
await Example39_Postgres.RunAsync();
Console.WriteLine("== DONE ==");
await Example40_DIContainer.RunAsync();
Console.WriteLine("== DONE ==");
Example41_HttpClientUsage.Run();
Console.WriteLine("== DONE ==");
}
}