Skip to content

Commit 7ad19e0

Browse files
Merge origin/main into security/ai-agent-hardening
2 parents 74fce2b + 670b0f5 commit 7ad19e0

13 files changed

Lines changed: 937 additions & 119 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ TestResults/
3232
!Michaelis_TableOfContents.docx
3333

3434
# Old or generated files to not commit
35+
build_output.txt
3536
wwwroot/sitemap.xml
3637
wwwroot/Chapters
3738
EssentialCSharp.Web/wwwroot/Chapters

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
<PackageVersion Include="Mailjet.Api" Version="4.0.0" />
3232
<PackageVersion Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="10.0.8" />
3333
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.8" />
34-
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.7" />
34+
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.8" />
3535
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.8" />
36-
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
36+
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
3737
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
3838
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
3939
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />

EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using Azure.AI.OpenAI;
22
using Azure.Core;
33
using Azure.Identity;
4+
using EssentialCSharp.Chat.Common.Models;
45
using EssentialCSharp.Chat.Common.Services;
56
using Microsoft.Extensions.AI;
67
using Microsoft.Extensions.Configuration;
78
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Options;
810
using Microsoft.SemanticKernel;
911
using Npgsql;
1012

@@ -65,6 +67,15 @@ public static IServiceCollection AddAzureOpenAIServices(
6567
.UseOpenTelemetry();
6668
#pragma warning restore SKEXP0010
6769

70+
// Ensure options are available even when caller provides AIOptions directly.
71+
services.AddOptions<EmbeddingRetryOptions>()
72+
.ValidateDataAnnotations()
73+
.Validate(options =>
74+
{
75+
options.Validate();
76+
return true;
77+
}, "Embedding retry configuration is invalid.");
78+
6879
// Register shared AI services
6980
services.AddSingleton<EmbeddingService>();
7081
services.AddSingleton<AISearchService>();
@@ -89,6 +100,17 @@ public static IServiceCollection AddAzureOpenAIServices(
89100
// Configure AI options from configuration
90101
services.Configure<AIOptions>(configuration.GetSection("AIOptions"));
91102

103+
// Configure retry options from configuration section.
104+
// Environment variables can override via AIOptions__EmbeddingRetry__*.
105+
services.AddOptions<EmbeddingRetryOptions>()
106+
.Bind(configuration.GetSection(EmbeddingRetryOptions.SectionPath))
107+
.ValidateDataAnnotations()
108+
.Validate(options =>
109+
{
110+
options.Validate();
111+
return true;
112+
}, "Embedding retry configuration is invalid.");
113+
92114
var aiOptions = configuration.GetSection("AIOptions").Get<AIOptions>();
93115
if (aiOptions == null)
94116
{
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace EssentialCSharp.Chat.Common.Models;
4+
5+
/// <summary>
6+
/// Configuration options for retry logic when calling external services like Azure OpenAI.
7+
/// </summary>
8+
public sealed class EmbeddingRetryOptions
9+
{
10+
/// <summary>
11+
/// Configuration section path in appsettings.json.
12+
/// </summary>
13+
public const string SectionPath = "AIOptions:EmbeddingRetry";
14+
15+
/// <summary>
16+
/// Maximum number of retries for transient failures.
17+
/// Default is 5 retries (initial attempt + 5 retries = 6 total attempts).
18+
/// </summary>
19+
[Range(0, 20)]
20+
public int MaxRetries { get; set; } = 5;
21+
22+
/// <summary>
23+
/// Base delay in milliseconds before the first retry.
24+
/// Subsequent retries use exponential backoff: baseDelay * (backoffMultiplier ^ attemptNumber).
25+
/// Default is 1000ms (1 second).
26+
/// </summary>
27+
[Range(0, 600000)]
28+
public int BaseDelayMs { get; set; } = 1000;
29+
30+
/// <summary>
31+
/// Maximum delay in milliseconds for exponential backoff before jitter.
32+
/// This caps retry delays to avoid overflow and unbounded waits.
33+
/// </summary>
34+
[Range(1, 600000)]
35+
public int MaxDelayMs { get; set; } = 60000;
36+
37+
/// <summary>
38+
/// Maximum embedding request payload size sent per API call.
39+
/// The service may adaptively downshift below this value when throttled.
40+
/// </summary>
41+
[Range(1, 2048)]
42+
public int MaxEmbeddingBatchSize { get; set; } = 1024;
43+
44+
/// <summary>
45+
/// Minimum delay between embedding API requests in milliseconds.
46+
/// This adds request pacing to reduce sustained rate-limit pressure.
47+
/// </summary>
48+
[Range(0, 600000)]
49+
public int MinInterRequestDelayMs { get; set; } = 250;
50+
51+
/// <summary>
52+
/// Exponential backoff multiplier. Each retry delay is multiplied by this value.
53+
/// For example, with baseDelay=1000ms and multiplier=2.0:
54+
/// - 1st retry: 1000ms
55+
/// - 2nd retry: 2000ms
56+
/// - 3rd retry: 4000ms
57+
/// - 4th retry: 8000ms
58+
/// Default is 2.0 (double each time).
59+
/// </summary>
60+
[Range(1.0, 10.0)]
61+
public double BackoffMultiplier { get; set; } = 2.0;
62+
63+
/// <summary>
64+
/// Maximum jitter fraction added to each retry delay to prevent thundering herd.
65+
/// Jitter is a random value in range [0, maxDelay * maxJitterFraction].
66+
/// For example, with maxJitterFraction=0.2 and delay=1000ms:
67+
/// actual delay will be between 1000ms and 1200ms.
68+
/// Default is 0.2 (20% jitter).
69+
/// </summary>
70+
[Range(0.0, 1.0)]
71+
public double MaxJitterFraction { get; set; } = 0.2;
72+
73+
/// <summary>
74+
/// Validates that configuration values are reasonable.
75+
/// </summary>
76+
/// <exception cref="InvalidOperationException">Thrown if configuration is invalid.</exception>
77+
public void Validate()
78+
{
79+
if (MaxRetries < 0)
80+
throw new InvalidOperationException("MaxRetries must be non-negative.");
81+
82+
if (BaseDelayMs < 0)
83+
throw new InvalidOperationException("BaseDelayMs must be non-negative.");
84+
85+
if (MaxDelayMs <= 0)
86+
throw new InvalidOperationException("MaxDelayMs must be positive.");
87+
88+
if (BaseDelayMs > MaxDelayMs)
89+
throw new InvalidOperationException("BaseDelayMs must be less than or equal to MaxDelayMs.");
90+
91+
if (MaxEmbeddingBatchSize <= 0)
92+
throw new InvalidOperationException("MaxEmbeddingBatchSize must be positive.");
93+
94+
if (MaxEmbeddingBatchSize > 2048)
95+
throw new InvalidOperationException("MaxEmbeddingBatchSize cannot exceed Azure embedding API limit (2048).");
96+
97+
if (MinInterRequestDelayMs < 0)
98+
throw new InvalidOperationException("MinInterRequestDelayMs must be non-negative.");
99+
100+
if (BackoffMultiplier < 1.0)
101+
throw new InvalidOperationException("BackoffMultiplier must be >= 1.0.");
102+
103+
if (MaxJitterFraction < 0.0 || MaxJitterFraction > 1.0)
104+
throw new InvalidOperationException("MaxJitterFraction must be between 0.0 and 1.0.");
105+
}
106+
}

0 commit comments

Comments
 (0)