Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.60.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.PgVector" Version="1.60.0-preview" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.PgVector" />
<PackageReference Include="ModelContextProtocol" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -71,6 +76,65 @@ public static IServiceCollection AddAzureOpenAIServices(
return services;
}

/// <summary>
/// Configures HTTP resilience (retry, circuit breaker, timeout) for Azure OpenAI HTTP clients.
/// This handles rate limiting (HTTP 429) and transient errors with exponential backoff.
/// </summary>
/// <param name="services">The service collection to configure</param>
/// <remarks>
/// 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.
/// </remarks>
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);
});
});
}

/// <summary>
/// Adds Azure OpenAI and related AI services to the service collection using configuration
/// </summary>
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions EssentialCSharp.Web/Extensions/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}