Skip to content

Commit 3296f97

Browse files
authored
fix: Fixed LLM provider configuration detect and handling (#20)
1 parent 2bcf994 commit 3296f97

7 files changed

Lines changed: 89 additions & 11 deletions

File tree

src/Features/ThisWeek/Handlers/CreateWeeklyReviewHandler.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using TenSecondTom.Features.ThisWeek.Commands;
33
using TenSecondTom.Infrastructure.Auth;
4+
using TenSecondTom.Infrastructure.Configuration;
45
using TenSecondTom.Infrastructure.Llm;
56
using TenSecondTom.Infrastructure.Prompts;
67
using TenSecondTom.Infrastructure.Storage;
@@ -21,6 +22,7 @@ public sealed class CreateWeeklyReviewHandler : IRequestHandler<CreateWeeklyRevi
2122
private readonly ILlmProviderFactory _llmFactory;
2223
private readonly IPromptTemplateLoader _promptLoader;
2324
private readonly IAuthenticationService _authService;
25+
private readonly IConfigurationStorageService _configService;
2426
private readonly ILogger<CreateWeeklyReviewHandler> _logger;
2527

2628
/// <summary>
@@ -30,18 +32,21 @@ public sealed class CreateWeeklyReviewHandler : IRequestHandler<CreateWeeklyRevi
3032
/// <param name="llmFactory">The LLM provider factory.</param>
3133
/// <param name="promptLoader">The prompt template loader.</param>
3234
/// <param name="authService">The authentication service.</param>
35+
/// <param name="configService">The configuration storage service.</param>
3336
/// <param name="logger">The logger instance.</param>
3437
public CreateWeeklyReviewHandler(
3538
IMemoryStorageProvider storage,
3639
ILlmProviderFactory llmFactory,
3740
IPromptTemplateLoader promptLoader,
3841
IAuthenticationService authService,
42+
IConfigurationStorageService configService,
3943
ILogger<CreateWeeklyReviewHandler> logger)
4044
{
4145
_storage = storage;
4246
_llmFactory = llmFactory;
4347
_promptLoader = promptLoader;
4448
_authService = authService;
49+
_configService = configService;
4550
_logger = logger;
4651
}
4752

@@ -109,17 +114,45 @@ public async Task<Result<WeeklyEntry>> Handle(
109114

110115
string prompt = RenderPrompt(templateResult.Value, aggregatedContent, dateRange, entriesResult.Value.Count);
111116

112-
// 8. Call LLM provider
113-
string provider = request.LlmProviderOverride ?? LlmProviders.OpenAI;
117+
// 8. Determine LLM provider (use override, or load from config, or default to OpenAI)
118+
string provider;
119+
if (!string.IsNullOrWhiteSpace(request.LlmProviderOverride))
120+
{
121+
provider = request.LlmProviderOverride;
122+
}
123+
else
124+
{
125+
// Load from configuration
126+
Result<Features.Setup.Models.ConfigurationSettings> configResult = await _configService.LoadAsync(cancellationToken).ConfigureAwait(false);
127+
if (configResult.IsSuccess && configResult.Value.Llm.Provider != Features.Setup.Models.LlmProvider.OpenAI)
128+
{
129+
// Convert enum to string
130+
provider = configResult.Value.Llm.Provider.ToString();
131+
}
132+
else
133+
{
134+
// Default to OpenAI
135+
provider = LlmProviders.OpenAI;
136+
if (!configResult.IsSuccess)
137+
{
138+
_logger.LogDebug("Could not load configuration, defaulting to OpenAI: {Error}", configResult.Error);
139+
}
140+
}
141+
}
142+
114143
ILlmProvider llmProvider;
115144
try
116145
{
117146
llmProvider = _llmFactory.CreateProvider(provider);
118147
}
119-
catch (Exception ex)
148+
catch (ArgumentException ex)
120149
{
121150
return Result<WeeklyEntry>.Failure($"Invalid LLM provider '{provider}'. Use 'OpenAI' or 'Anthropic'. Error: {ex.Message}");
122151
}
152+
catch (InvalidOperationException ex)
153+
{
154+
return Result<WeeklyEntry>.Failure($"Failed to create LLM provider '{provider}': {ex.Message}");
155+
}
123156

124157
_logger.LogDebug("Calling LLM provider {Provider} for weekly review", provider);
125158

src/Features/Today/Handlers/CreateDailyEntryHandler.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using TenSecondTom.Features.Today.Commands;
33
using TenSecondTom.Infrastructure.Auth;
4+
using TenSecondTom.Infrastructure.Configuration;
45
using TenSecondTom.Infrastructure.Llm;
56
using TenSecondTom.Infrastructure.Prompts;
67
using TenSecondTom.Infrastructure.Storage;
@@ -16,12 +17,14 @@ namespace TenSecondTom.Features.Today.Handlers;
1617
/// </summary>
1718
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1515:Consider making public types internal", Justification = "Public API by design")]
1819
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "Structured logging pattern")]
20+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1848:Use the LoggerMessage delegates", Justification = "Simple logging calls, delegate overhead not justified")]
1921
public sealed class CreateDailyEntryHandler : IRequestHandler<CreateDailyEntryCommand, Result<DailyEntry>>
2022
{
2123
private readonly IMemoryStorageProvider _storage;
2224
private readonly ILlmProviderFactory _llmFactory;
2325
private readonly IPromptTemplateLoader _promptLoader;
2426
private readonly IAuthenticationService _authService;
27+
private readonly IConfigurationStorageService _configService;
2528
private readonly ILogger<CreateDailyEntryHandler> _logger;
2629

2730
/// <summary>
@@ -31,18 +34,21 @@ public sealed class CreateDailyEntryHandler : IRequestHandler<CreateDailyEntryCo
3134
/// <param name="llmFactory">The LLM provider factory.</param>
3235
/// <param name="promptLoader">The prompt template loader.</param>
3336
/// <param name="authService">The authentication service.</param>
37+
/// <param name="configService">The configuration storage service.</param>
3438
/// <param name="logger">The logger instance.</param>
3539
public CreateDailyEntryHandler(
3640
IMemoryStorageProvider storage,
3741
ILlmProviderFactory llmFactory,
3842
IPromptTemplateLoader promptLoader,
3943
IAuthenticationService authService,
44+
IConfigurationStorageService configService,
4045
ILogger<CreateDailyEntryHandler> logger)
4146
{
4247
_storage = storage;
4348
_llmFactory = llmFactory;
4449
_promptLoader = promptLoader;
4550
_authService = authService;
51+
_configService = configService;
4652
_logger = logger;
4753
}
4854

@@ -94,16 +100,44 @@ public async Task<Result<DailyEntry>> Handle(
94100

95101
string prompt = RenderPrompt(templateResult.Value, userInput);
96102

97-
// 6. Call LLM provider
98-
string provider = request.LlmProviderOverride ?? LlmProviders.OpenAI; // Default to OpenAI if not specified
103+
// 6. Determine LLM provider (use override, or load from config, or default to OpenAI)
104+
string provider;
105+
if (!string.IsNullOrWhiteSpace(request.LlmProviderOverride))
106+
{
107+
provider = request.LlmProviderOverride;
108+
}
109+
else
110+
{
111+
// Load from configuration
112+
Result<Features.Setup.Models.ConfigurationSettings> configResult = await _configService.LoadAsync(cancellationToken).ConfigureAwait(false);
113+
if (configResult.IsSuccess && configResult.Value.Llm.Provider != Features.Setup.Models.LlmProvider.OpenAI)
114+
{
115+
// Convert enum to string
116+
provider = configResult.Value.Llm.Provider.ToString();
117+
}
118+
else
119+
{
120+
// Default to OpenAI
121+
provider = LlmProviders.OpenAI;
122+
if (!configResult.IsSuccess)
123+
{
124+
_logger.LogDebug("Could not load configuration, defaulting to OpenAI: {Error}", configResult.Error);
125+
}
126+
}
127+
}
128+
99129
ILlmProvider llmProvider;
100130
try
101131
{
102132
llmProvider = _llmFactory.CreateProvider(provider);
103133
}
104134
catch (ArgumentException ex)
105135
{
106-
return Result<DailyEntry>.Failure($"Invalid LLM provider. Use 'OpenAI' or 'Anthropic'. Error: {ex.Message}");
136+
return Result<DailyEntry>.Failure($"Invalid LLM provider '{provider}'. Use 'OpenAI' or 'Anthropic'. Error: {ex.Message}");
137+
}
138+
catch (InvalidOperationException ex)
139+
{
140+
return Result<DailyEntry>.Failure($"Failed to create LLM provider '{provider}': {ex.Message}");
107141
}
108142

109143
Result<string> llmResult = await llmProvider.GenerateCompletionAsync(prompt, cancellationToken).ConfigureAwait(false);

src/Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public static IServiceCollection AddTenSecondTomServices(this IServiceCollection
8484
sshKeyLogger);
8585
});
8686

87-
// Register OpenAI ChatClient
88-
services.AddSingleton<ChatClient>(serviceProvider =>
87+
// Register OpenAI ChatClient (lazy - only instantiated when OpenAI provider is actually used)
88+
services.AddTransient<ChatClient>(serviceProvider =>
8989
{
9090
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
9191
string? apiKey = configuration["OPENAI_API_KEY"] ??
@@ -102,8 +102,8 @@ public static IServiceCollection AddTenSecondTomServices(this IServiceCollection
102102
return openAIClient.GetChatClient(model);
103103
});
104104

105-
// Register Anthropic AnthropicClient
106-
services.AddSingleton<AnthropicClient>(serviceProvider =>
105+
// Register Anthropic AnthropicClient (lazy - only instantiated when Anthropic provider is actually used)
106+
services.AddTransient<AnthropicClient>(serviceProvider =>
107107
{
108108
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
109109
string? apiKey = configuration["ANTHROPIC_API_KEY"] ??

src/TenSecondTom.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<PublishTrimmed>true</PublishTrimmed>
1212
<TrimMode>link</TrimMode>
1313
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
14+
<!-- Enable JSON serialization reflection for Anthropic SDK compatibility -->
15+
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
1416
<!-- Suppress specific trimming warnings -->
1517
<NoWarn>$(NoWarn);IL2026;IL2067;IL2070;IL2075;IL2077;IL3050</NoWarn>
1618
</PropertyGroup>
@@ -21,6 +23,7 @@
2123
<TrimmerRootAssembly Include="Serilog.Sinks.File" />
2224
<TrimmerRootAssembly Include="Serilog.Enrichers.Environment" />
2325
<TrimmerRootAssembly Include="Serilog.Settings.Configuration" />
26+
<TrimmerRootAssembly Include="Anthropic.SDK" />
2427
</ItemGroup>
2528

2629
<ItemGroup>

src/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"MaxTokens": 2000
1212
},
1313
"Anthropic": {
14-
"Model": "claude-3-sonnet-20240229",
14+
"Model": "claude-sonnet-4-20250514",
1515
"MaxTokens": 2000
1616
},
1717
"DataRetention": {

tests/TenSecondTom.Tests/Unit/Features/ThisWeek/CreateWeeklyReviewHandlerTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using TenSecondTom.Features.ThisWeek.Commands;
55
using TenSecondTom.Features.ThisWeek.Handlers;
66
using TenSecondTom.Infrastructure.Auth;
7+
using TenSecondTom.Infrastructure.Configuration;
78
using TenSecondTom.Infrastructure.Llm;
89
using TenSecondTom.Infrastructure.Prompts;
910
using TenSecondTom.Infrastructure.Storage;
@@ -23,6 +24,7 @@ public sealed class CreateWeeklyReviewHandlerTests
2324
private readonly Mock<ILlmProvider> _mockLlmProvider;
2425
private readonly Mock<IPromptTemplateLoader> _mockPromptLoader;
2526
private readonly Mock<IAuthenticationService> _mockAuthService;
27+
private readonly Mock<IConfigurationStorageService> _mockConfigService;
2628
private readonly Mock<ILogger<CreateWeeklyReviewHandler>> _mockLogger;
2729
private readonly CreateWeeklyReviewHandler _handler;
2830

@@ -33,6 +35,7 @@ public CreateWeeklyReviewHandlerTests()
3335
_mockLlmProvider = new Mock<ILlmProvider>();
3436
_mockPromptLoader = new Mock<IPromptTemplateLoader>();
3537
_mockAuthService = new Mock<IAuthenticationService>();
38+
_mockConfigService = new Mock<IConfigurationStorageService>();
3639
_mockLogger = new Mock<ILogger<CreateWeeklyReviewHandler>>();
3740

3841
// Setup default successful behaviors
@@ -79,6 +82,7 @@ Noticed pattern of afternoon productivity dips.
7982
_mockLlmFactory.Object,
8083
_mockPromptLoader.Object,
8184
_mockAuthService.Object,
85+
_mockConfigService.Object,
8286
_mockLogger.Object);
8387
}
8488

tests/TenSecondTom.Tests/Unit/Features/Today/CreateDailyEntryHandlerTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using TenSecondTom.Features.Today.Commands;
55
using TenSecondTom.Features.Today.Handlers;
66
using TenSecondTom.Infrastructure.Auth;
7+
using TenSecondTom.Infrastructure.Configuration;
78
using TenSecondTom.Infrastructure.Llm;
89
using TenSecondTom.Infrastructure.Prompts;
910
using TenSecondTom.Infrastructure.Storage;
@@ -24,6 +25,7 @@ public sealed class CreateDailyEntryHandlerTests
2425
private readonly Mock<ILlmProvider> _mockLlmProvider;
2526
private readonly Mock<IPromptTemplateLoader> _mockPromptLoader;
2627
private readonly Mock<IAuthenticationService> _mockAuthService;
28+
private readonly Mock<IConfigurationStorageService> _mockConfigService;
2729
private readonly Mock<ILogger<CreateDailyEntryHandler>> _mockLogger;
2830
private readonly CreateDailyEntryHandler _handler;
2931

@@ -34,6 +36,7 @@ public CreateDailyEntryHandlerTests()
3436
_mockLlmProvider = new Mock<ILlmProvider>();
3537
_mockPromptLoader = new Mock<IPromptTemplateLoader>();
3638
_mockAuthService = new Mock<IAuthenticationService>();
39+
_mockConfigService = new Mock<IConfigurationStorageService>();
3740
_mockLogger = new Mock<ILogger<CreateDailyEntryHandler>>();
3841

3942
// Setup default successful behaviors
@@ -72,6 +75,7 @@ public CreateDailyEntryHandlerTests()
7275
_mockLlmFactory.Object,
7376
_mockPromptLoader.Object,
7477
_mockAuthService.Object,
78+
_mockConfigService.Object,
7579
_mockLogger.Object);
7680
}
7781

0 commit comments

Comments
 (0)