Skip to content

Commit ef224fd

Browse files
committed
Refactor AI resource management and update configurations
- Removed FoundryLocalResource and GitHubModelsResource classes to streamline AI resource handling. - Updated Program.cs to ensure AI resources are always added, with adjustments to parameter handling. - Enhanced launchSettings.json with new configurations for Ollama, AzureOpenAI, GitHubModels, AzureAIFoundry, and FoundryLocal environments. - Introduced appsettings for AzureAIFoundry and updated existing settings for FoundryLocal and GitHubModels to include model versioning and capacity. - Updated package references in ServiceDefaults and Web projects to the latest versions for improved functionality and security. - Refined resilience configuration in Extensions.cs to allow for customizable HTTP timeout settings. - Modified unit tests to reflect changes in AI provider handling and ensure accurate model defaults. - Added new tests for AI configuration to validate provider and model retrieval logic.
1 parent 3cf4337 commit ef224fd

28 files changed

Lines changed: 584 additions & 638 deletions

BuildWithAspire.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<Project Path="src/BuildWithAspire.AppHost/BuildWithAspire.AppHost.csproj" />
88
<Project Path="src/BuildWithAspire.ServiceDefaults/BuildWithAspire.ServiceDefaults.csproj" />
99
<Project Path="src/BuildWithAspire.Web/BuildWithAspire.Web.csproj" />
10+
<Project Path="src/BuildWithAspire.Abstractions/BuildWithAspire.Abstractions.csproj" />
1011
</Folder>
1112
<Folder Name="/tests/">
1213
<Project Path="tests/BuildWithAspire.ApiService.UnitTests/BuildWithAspire.ApiService.UnitTests.csproj" />
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Microsoft.Extensions.Configuration;
2+
3+
namespace BuildWithAspire.Abstractions;
4+
5+
public static class AIConfiguration
6+
{
7+
public enum AIProvider
8+
{
9+
Ollama,
10+
AzureOpenAI,
11+
GitHubModels,
12+
AzureAIFoundry
13+
}
14+
15+
public record AISettings(AIProvider Provider, string DeploymentName, string Model);
16+
17+
public static AISettings GetSettings(IConfiguration configuration)
18+
{
19+
var provider = GetProvider(configuration);
20+
var deploymentName = GetDeploymentName(configuration);
21+
var model = GetModel(configuration, provider);
22+
23+
return new AISettings(provider, deploymentName, model);
24+
}
25+
26+
public static AIProvider GetProvider(IConfiguration configuration)
27+
{
28+
var providerString = configuration["AI:Provider"]?.ToLowerInvariant() ?? "ollama";
29+
return providerString switch
30+
{
31+
"ollama" => AIProvider.Ollama,
32+
"azureopenai" => AIProvider.AzureOpenAI,
33+
"githubmodels" => AIProvider.GitHubModels,
34+
"foundrylocal" or "azureaifoundry" => AIProvider.AzureAIFoundry,
35+
_ => throw new InvalidOperationException($"Unsupported AI provider: {providerString}. Supported providers: azureopenai, githubmodels, ollama, foundrylocal, azureaifoundry")
36+
};
37+
}
38+
39+
public static string GetDeploymentName(IConfiguration configuration)
40+
{
41+
var deploymentName = configuration["AI:DeploymentName"];
42+
return string.IsNullOrEmpty(deploymentName) ? "chat" : deploymentName;
43+
}
44+
45+
public static string GetModel(IConfiguration configuration, AIProvider? provider = null)
46+
{
47+
provider ??= GetProvider(configuration);
48+
49+
var configuredModel = configuration["AI:Model"];
50+
if (!string.IsNullOrEmpty(configuredModel))
51+
{
52+
// Normalize GitHub Models naming (ensure vendor prefix openai/ when missing)
53+
if (provider == AIProvider.GitHubModels && !configuredModel.Contains('/'))
54+
{
55+
configuredModel = $"openai/{configuredModel}";
56+
}
57+
return configuredModel;
58+
}
59+
60+
var model = provider switch
61+
{
62+
AIProvider.Ollama => "llama3.2",
63+
AIProvider.AzureOpenAI => "gpt-4o",
64+
AIProvider.GitHubModels => "openai/gpt-4o-mini",
65+
AIProvider.AzureAIFoundry => "phi-3.5-mini",
66+
_ => throw new InvalidOperationException($"No default model available for provider: {provider}")
67+
};
68+
return model;
69+
}
70+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
10+
</ItemGroup>
11+
</Project>

src/BuildWithAspire.ApiService/BuildWithAspire.ApiService.csproj

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,30 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.3.1-preview.1.25305.6" />
12-
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.3.1" />
13-
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.5.1-beta.319" />
14-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
15-
<PackageReference Include="Azure.Identity" Version="1.14.1" />
16-
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
11+
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="9.4.2-preview.1.25428.12" />
12+
<PackageReference Include="Aspire.Azure.AI.Inference" Version="9.4.2-preview.1.25428.12" />
13+
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.4.2" />
14+
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.8.0-beta.389" />
15+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
16+
<PackageReference Include="Azure.Identity" Version="1.16.0" />
17+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
1718
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1819
<PrivateAssets>all</PrivateAssets>
1920
</PackageReference>
20-
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
21-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
22-
<PackageReference Include="Microsoft.Extensions.AI.Ollama" Version="9.6.0-preview.1.25310.2" />
23-
<PackageReference Include="Microsoft.Extensions.AI" Version="9.6.0" />
24-
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.6.0-preview.1.25310.2" />
25-
<PackageReference Include="Microsoft.AI.Foundry.Local" Version="0.1.0" />
26-
<PackageReference Include="Scalar.AspNetCore" Version="2.5.6" />
27-
<PackageReference Include="System.Text.Json" Version="9.0.6" />
28-
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.59.0" />
21+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.9" />
22+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.0" />
23+
<PackageReference Include="Microsoft.Extensions.AI.Ollama" Version="9.7.0-preview.1.25356.2" />
24+
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.0" />
25+
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.0-preview.1.25458.4" />
26+
<PackageReference Include="Microsoft.AI.Foundry.Local" Version="0.2.0" />
27+
<PackageReference Include="Scalar.AspNetCore" Version="2.8.5" />
28+
<PackageReference Include="System.Text.Json" Version="9.0.9" />
29+
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.65.0" />
2930
</ItemGroup>
3031

3132
<ItemGroup>
3233
<ProjectReference Include="..\BuildWithAspire.ServiceDefaults\BuildWithAspire.ServiceDefaults.csproj" />
34+
<ProjectReference Include="..\BuildWithAspire.Abstractions\BuildWithAspire.Abstractions.csproj" />
3335
</ItemGroup>
34-
36+
3537
</Project>
Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1 @@
1-
namespace BuildWithAspire.ApiService.Configuration;
2-
3-
public static class AIConfiguration
4-
{
5-
public enum AIProvider
6-
{
7-
Ollama,
8-
AzureOpenAI,
9-
GitHubModels,
10-
FoundryLocal
11-
}
12-
13-
public record AISettings(AIProvider Provider, string DeploymentName, string Model);
14-
15-
public static AISettings GetSettings(IConfiguration configuration)
16-
{
17-
var provider = GetProvider(configuration);
18-
var deploymentName = GetDeploymentName(configuration);
19-
var model = GetModel(configuration, provider);
20-
21-
return new AISettings(provider, deploymentName, model);
22-
}
23-
24-
public static AIProvider GetProvider(IConfiguration configuration)
25-
{
26-
var providerString = configuration["AI:Provider"]?.ToLowerInvariant() ?? "ollama";
27-
return providerString switch
28-
{
29-
"ollama" => AIProvider.Ollama,
30-
"azureopenai" => AIProvider.AzureOpenAI,
31-
"githubmodels" => AIProvider.GitHubModels,
32-
"foundrylocal" => AIProvider.FoundryLocal,
33-
_ => throw new InvalidOperationException($"Unsupported AI provider: {providerString}. Supported providers: azureopenai, ollama, githubmodels, foundrylocal")
34-
};
35-
}
36-
37-
public static string GetDeploymentName(IConfiguration configuration)
38-
{
39-
var deploymentName = configuration["AI:DeploymentName"];
40-
return string.IsNullOrEmpty(deploymentName) ? "chat" : deploymentName;
41-
}
42-
43-
public static string GetModel(IConfiguration configuration, AIProvider? provider = null)
44-
{
45-
provider ??= GetProvider(configuration);
46-
47-
var configuredModel = configuration["AI:Model"];
48-
if (!string.IsNullOrEmpty(configuredModel))
49-
{
50-
return configuredModel;
51-
}
52-
53-
// Default models based on provider
54-
return provider switch
55-
{
56-
AIProvider.Ollama => "llama3.2",
57-
AIProvider.AzureOpenAI => "gpt-4o",
58-
AIProvider.GitHubModels => "gpt-4o-mini",
59-
AIProvider.FoundryLocal => "phi-3.5-mini",
60-
_ => throw new InvalidOperationException($"No default model available for provider: {provider}")
61-
};
62-
}
63-
}
1+
// (Removed) Placeholder no longer needed; logic moved to BuildWithAspire.Abstractions.AIConfiguration.
Lines changed: 39 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using BuildWithAspire.ApiService.Configuration;
2-
using Microsoft.AI.Foundry.Local;
3-
using Microsoft.Extensions.AI;
4-
using OpenAI;
1+
2+
using BuildWithAspire.Abstractions;
53

64
namespace BuildWithAspire.ApiService.Extensions;
75

@@ -16,10 +14,10 @@ public static IHostApplicationBuilder AddAIServices(this IHostApplicationBuilder
1614
{
1715
var aiSettings = AIConfiguration.GetSettings(builder.Configuration);
1816

19-
// Register the AI settings for dependency injection
17+
// Register AI settings once
2018
builder.Services.AddSingleton(aiSettings);
2119

22-
// Configure the appropriate AI provider
20+
// Register provider specific services (pure registration – no provider builds/logging here)
2321
switch (aiSettings.Provider)
2422
{
2523
case AIConfiguration.AIProvider.Ollama:
@@ -31,15 +29,15 @@ public static IHostApplicationBuilder AddAIServices(this IHostApplicationBuilder
3129
case AIConfiguration.AIProvider.GitHubModels:
3230
builder.AddGitHubModelsAIServices(aiSettings);
3331
break;
34-
case AIConfiguration.AIProvider.FoundryLocal:
32+
case AIConfiguration.AIProvider.AzureAIFoundry:
3533
builder.AddFoundryLocalAIServices(aiSettings);
3634
break;
3735
default:
3836
throw new InvalidOperationException($"Unsupported AI provider: {aiSettings.Provider}");
3937
}
4038

41-
// Add logging for AI configuration
42-
builder.Services.AddSingleton<IHostedService, AIConfigurationLogger>();
39+
// Add hosted startup logger (single provider – final container)
40+
builder.Services.AddHostedService<AIStartupLogger>();
4341

4442
return builder;
4543
}
@@ -51,81 +49,51 @@ private static void AddOllamaAIServices(this IHostApplicationBuilder builder, AI
5149
}
5250

5351
private static void AddAzureOpenAIServices(this IHostApplicationBuilder builder, AIConfiguration.AISettings aiSettings)
54-
{
55-
var connectionString = builder.Configuration.GetConnectionString("ai-service");
56-
var logger = builder.Services.BuildServiceProvider().GetRequiredService<ILoggerFactory>().CreateLogger("AIServiceExtensions");
57-
logger.LogDebug("Azure OpenAI connection string: {ConnectionString}", connectionString);
58-
59-
builder.AddAzureOpenAIClient("ai-service")
60-
.AddChatClient(aiSettings.DeploymentName);
61-
}
52+
=> builder.AddAzureOpenAIClient("ai-service")
53+
.AddChatClient(aiSettings.DeploymentName);
6254

6355
private static void AddGitHubModelsAIServices(this IHostApplicationBuilder builder, AIConfiguration.AISettings aiSettings)
64-
{
65-
var githubToken = builder.Configuration["GITHUB_TOKEN"] ??
66-
builder.Configuration["ConnectionStrings:GitHubModels"] ??
67-
throw new InvalidOperationException("GitHub token not found. Set GITHUB_TOKEN environment variable or ConnectionStrings:GitHubModels");
68-
69-
builder.Services.AddSingleton<IChatClient>(serviceProvider =>
70-
{
71-
var openAIClient = new OpenAIClient(new System.ClientModel.ApiKeyCredential(githubToken), new OpenAIClientOptions
72-
{
73-
Endpoint = new Uri("https://models.inference.ai.azure.com")
74-
});
75-
return openAIClient.GetChatClient(aiSettings.Model).AsIChatClient();
76-
});
77-
}
56+
=> builder.AddOpenAIClient(aiSettings.DeploymentName)
57+
.AddChatClient();
7858

7959
private static void AddFoundryLocalAIServices(this IHostApplicationBuilder builder, AIConfiguration.AISettings aiSettings)
80-
{
81-
builder.Services.AddSingleton<IChatClient>(serviceProvider =>
82-
{
83-
var logger = serviceProvider.GetRequiredService<ILogger<IChatClient>>();
84-
try
85-
{
86-
// Initialize FoundryLocalManager with the model
87-
var manager = FoundryLocalManager.StartModelAsync(aliasOrModelId: aiSettings.Model).GetAwaiter().GetResult();
88-
var modelInfo = manager.GetModelInfoAsync(aliasOrModelId: aiSettings.Model).GetAwaiter().GetResult();
89-
90-
logger.LogInformation("Foundry Local initialized with endpoint: {Endpoint}, Model: {Model}",
91-
manager.Endpoint, modelInfo?.ModelId);
92-
93-
var openAIClient = new OpenAIClient(new System.ClientModel.ApiKeyCredential(manager.ApiKey), new OpenAIClientOptions
94-
{
95-
Endpoint = manager.Endpoint
96-
});
97-
return openAIClient.GetChatClient(modelInfo?.ModelId ?? aiSettings.Model).AsIChatClient();
98-
}
99-
catch (Exception ex)
100-
{
101-
logger.LogError(ex, "Failed to initialize Foundry Local client");
102-
throw;
103-
}
104-
});
105-
}
60+
=> builder.AddAzureChatCompletionsClient(aiSettings.DeploymentName)
61+
.AddChatClient();
10662
}
10763

108-
/// <summary>
109-
/// Background service that logs AI configuration on startup.
110-
/// </summary>
111-
internal sealed class AIConfigurationLogger : BackgroundService
64+
// Hosted service that logs final AI configuration once the real container is built.
65+
internal sealed class AIStartupLogger : IHostedService
11266
{
113-
private readonly ILogger<AIConfigurationLogger> _logger;
114-
private readonly AIConfiguration.AISettings _aiSettings;
67+
private readonly ILogger<AIStartupLogger> _logger;
68+
private readonly AIConfiguration.AISettings _settings;
69+
private readonly IConfiguration _configuration;
11570

116-
public AIConfigurationLogger(ILogger<AIConfigurationLogger> logger, AIConfiguration.AISettings aiSettings)
71+
public AIStartupLogger(ILogger<AIStartupLogger> logger,
72+
AIConfiguration.AISettings settings,
73+
IConfiguration configuration)
11774
{
11875
_logger = logger;
119-
_aiSettings = aiSettings;
76+
_settings = settings;
77+
_configuration = configuration;
12078
}
12179

122-
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
80+
public Task StartAsync(CancellationToken cancellationToken)
12381
{
124-
_logger.LogInformation("AI Configuration: Provider={Provider}, Model={Model}, Deployment={Deployment}",
125-
_aiSettings.Provider, _aiSettings.Model, _aiSettings.DeploymentName);
126-
127-
// Complete immediately - this is just for logging
128-
await Task.CompletedTask.ConfigureAwait(false);
82+
if (_settings.Provider == AIConfiguration.AIProvider.AzureOpenAI)
83+
{
84+
var hasConn = !string.IsNullOrEmpty(_configuration.GetConnectionString("ai-service"));
85+
_logger.LogInformation("AI configured: Provider={Provider} Deployment={Deployment} Model={Model} AzureConnPresent={HasConn}",
86+
_settings.Provider, _settings.DeploymentName, _settings.Model, hasConn);
87+
}
88+
else
89+
{
90+
_logger.LogInformation("AI configured: Provider={Provider} Deployment={Deployment} Model={Model}",
91+
_settings.Provider, _settings.DeploymentName, _settings.Model);
92+
}
93+
return Task.CompletedTask;
12994
}
95+
96+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
13097
}
13198

99+

0 commit comments

Comments
 (0)