diff --git a/Directory.Packages.props b/Directory.Packages.props index 2b4bd77b..5e885b1f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj index e600d10f..f51ecef9 100644 --- a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj +++ b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj @@ -6,6 +6,7 @@ + diff --git a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs index 059a6d13..24dfdb53 100644 --- a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs +++ b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs @@ -4,8 +4,10 @@ using EssentialCSharp.Chat.Common.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; using Microsoft.SemanticKernel; using Npgsql; +using Polly; namespace EssentialCSharp.Chat.Common.Extensions; @@ -38,6 +40,9 @@ public static IServiceCollection AddAzureOpenAIServices( var endpoint = new Uri(aiOptions.Endpoint); + // Configure HTTP resilience for Azure OpenAI requests + ConfigureAzureOpenAIResilience(services); + // Register Azure OpenAI services with Managed Identity authentication #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. services.AddAzureOpenAIChatClient( @@ -71,6 +76,65 @@ public static IServiceCollection AddAzureOpenAIServices( return services; } + /// + /// Configures HTTP resilience (retry, circuit breaker, timeout) for Azure OpenAI HTTP clients. + /// This handles rate limiting (HTTP 429) and transient errors with exponential backoff. + /// + /// The service collection to configure + /// + /// This method configures resilience for ALL HTTP clients created via IHttpClientFactory. + /// + /// IMPORTANT: The Semantic Kernel's AddAzureOpenAI* extension methods (used in this class) + /// do NOT expose options to configure specific named or typed HttpClients. The internal + /// implementation creates HttpClient instances through IHttpClientFactory without + /// providing hooks for per-client configuration. Therefore, ConfigureHttpClientDefaults + /// is the ONLY way to apply resilience to Azure OpenAI clients when using Semantic Kernel. + /// + /// For Azure OpenAI services specifically, the resilience configuration: + /// - Retries HTTP 429 (rate limit), 408 (timeout), and 5xx errors + /// - Respects Retry-After headers from Azure OpenAI + /// - Uses exponential backoff with jitter + /// - Implements circuit breaker pattern + /// + /// This is appropriate for applications that primarily use Azure OpenAI services. + /// The retry policies are reasonable for most HTTP APIs and should not negatively + /// impact other HTTP clients like hCaptcha or Mailjet. + /// + private static void ConfigureAzureOpenAIResilience(IServiceCollection services) + { + // Configure resilience for all HTTP clients created via IHttpClientFactory + // The Semantic Kernel's AddAzureOpenAI* methods do not support named/typed + // HttpClient configuration, so ConfigureHttpClientDefaults is required. + services.ConfigureHttpClientDefaults(httpClientBuilder => + { + httpClientBuilder.AddStandardResilienceHandler(options => + { + // Configure retry strategy for rate limiting and transient errors + options.Retry.MaxRetryAttempts = 5; + options.Retry.Delay = TimeSpan.FromSeconds(2); + options.Retry.BackoffType = DelayBackoffType.Exponential; + options.Retry.UseJitter = true; + + // The standard resilience handler already handles: + // - HTTP 429 (Too Many Requests / Rate Limit) + // - HTTP 408 (Request Timeout) + // - HTTP 5xx (Server Errors) + // - Respects Retry-After header automatically + + // Configure circuit breaker to prevent overwhelming the service + options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30); + options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15); + options.CircuitBreaker.FailureRatio = 0.2; // Break if 20% of requests fail + + // Configure timeout for individual attempts + options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(30); + + // Configure total timeout for all retry attempts + options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(3); + }); + }); + } + /// /// Adds Azure OpenAI and related AI services to the service collection using configuration /// @@ -183,6 +247,9 @@ public static IServiceCollection AddAzureOpenAIServicesWithApiKey( var endpoint = new Uri(aiOptions.Endpoint); + // Configure HTTP resilience for Azure OpenAI requests + ConfigureAzureOpenAIResilience(services); + // Register Azure OpenAI services with API key authentication #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. services.AddAzureOpenAIChatClient( diff --git a/EssentialCSharp.Web/Extensions/LoggerExtensions.cs b/EssentialCSharp.Web/Extensions/LoggerExtensions.cs index aa183e8b..4658aa3e 100644 --- a/EssentialCSharp.Web/Extensions/LoggerExtensions.cs +++ b/EssentialCSharp.Web/Extensions/LoggerExtensions.cs @@ -4,7 +4,7 @@ namespace EssentialCSharp.Web.Extensions; internal static partial class LoggerExtensions { - [LoggerMessage(Level = LogLevel.Debug, EventId = 1, Message = "Successful captcha with response of: '{JsonResult}'")] + [LoggerMessage(Level = LogLevel.Debug, EventId = 1, Message = "Successful captcha with response")] public static partial void HomeControllerSuccessfulCaptchaResponse( - this ILogger logger, JsonResult jsonResult); + this ILogger logger); }