KernelService Internal Architecture: Dynamic AI Model Provider Loading in AntSK
The KernelService in AntSK dynamically instantiates fresh Semantic Kernel instances per application, binding AI providers like OpenAI, Azure OpenAI, SparkDesk, and Ollama at runtime through a three-layer factory architecture that isolates provider configuration from plugin management.
The AntSK open-source project (aidotnet/antsk) implements a sophisticated kernel management system that enables multi-tenant AI applications to switch between diverse language model providers without code changes. The KernelService acts as the central orchestrator, resolving model configurations, registering provider-specific services, and injecting application-specific plugins. This design ensures complete isolation between applications while supporting dynamic provider selection based on database-driven AIModel configurations.
Three-Layer Architecture Overview
The KernelService implementation in src/AntSK.Domain/Domain/Service/KernelService.cs organizes its responsibilities into three distinct layers:
- Kernel Factory: Builds fresh
Kernelinstances on demand, ensuring application isolation - Provider Selector: Maps
AITypeenum values to concrete Semantic Kernel service registrations - Plugin Injector: Registers default system plugins and application-specific API or native functions
This separation allows the system to support eight distinct AI providers—from standard OpenAI to custom implementations like SparkDesk—while maintaining clean dependency injection patterns.
Kernel Factory: Creating Isolated Instances per Application
The factory layer ensures that each application receives an independent kernel instance, preventing plugin cross-contamination between tenants. The entry points are GetKernelByApp() and GetKernelByAIModelID().
GetKernelByApp Implementation
Located at lines 50-64 in KernelService.cs, this method resolves the application's configured model and constructs a new kernel:
public Kernel GetKernelByApp(Apps app)
{
// Resolve the model linked to the app
var chatModel = _aIModels_Repositories.GetFirst(p => p.Id == app.ChatModelID);
var chatHttpClient = OpenAIHttpClientHandlerUtil.GetHttpClient(chatModel.EndPoint);
// Build a new kernel and plug the correct provider
var builder = Kernel.CreateBuilder();
WithTextGenerationByAIType(builder, chatModel, chatHttpClient);
_kernel = builder.Build();
// Register default plugins (conversation summary, KMS)
RegisterPluginsWithKernel(_kernel);
return _kernel;
}
The method always instantiates a new KernelBuilder rather than reusing instances, ensuring that plugin registrations from one application cannot leak into another. The companion method GetKernelByAIModelID() (lines 71-80) provides the same functionality when only the model identifier is available.
Provider Selector: Dynamic AI Model Binding
The WithTextGenerationByAIType() method (lines 82-151) serves as the provider selection engine, using a switch statement on the AIType enum to register the appropriate Semantic Kernel services.
Standard Provider Registration
For OpenAI and Azure OpenAI, the method calls the standard Semantic Kernel extension methods:
case AIType.OpenAI:
builder.AddOpenAIChatCompletion(
modelId: chatModel.ModelName,
apiKey: chatModel.ModelKey,
httpClient: chatHttpClient);
break;
case AIType.AzureOpenAI:
builder.AddAzureOpenAIChatCompletion(
deploymentName: chatModel.ModelName,
apiKey: chatModel.ModelKey,
endpoint: chatModel.EndPoint);
break;
Custom Provider Registration via Keyed DI
For non-standard providers like SparkDesk, DashScope, and Mock implementations, the system uses keyed dependency injection to register custom service implementations:
case AIType.SparkDesk:
var settings = chatModel.ModelKey.Split("|");
var modelVersion = ResolveSparkDeskVersion(chatModel.ModelName);
var options = new SparkDeskOptions
{
AppId = settings[0],
ApiSecret = settings[1],
ApiKey = settings[2],
ModelVersion = modelVersion
};
builder.Services.AddKeyedSingleton<ITextGenerationService>(
"spark-desk", new SparkDeskTextCompletion(options, chatModel.Id));
builder.Services.AddKeyedSingleton<IChatCompletionService>(
"spark-desk-chat", new SparkDeskChatCompletion(options, chatModel.Id));
break;
case AIType.DashScope:
builder.Services.AddDashScopeChatCompletion(chatModel.ModelKey, chatModel.ModelName);
break;
The AIType.LLamaFactory and AIType.Ollama cases reuse the OpenAI chat completion method but with a dummy API key, leveraging compatibility layers provided by those services.
Plugin and Function Injection
After kernel construction, the system registers two categories of plugins: default system plugins and application-specific functions.
Default System Plugins
The RegisterPluginsWithKernel() method (lines 303-309) ensures every kernel includes baseline capabilities:
private void RegisterPluginsWithKernel(Kernel kernel)
{
kernel.ImportPluginFromObject(new ConversationSummaryPlugin(), "ConversationSummaryPlugin");
kernel.ImportPluginFromPromptDirectory(
Path.Combine(RepoFiles.SamplePluginsPath(), "KMSPlugin"));
}
These provide conversation summarization and knowledge management system (KMS) capabilities regardless of the specific application configuration.
Application-Specific Functions
The ImportFunctionsByApp() method (lines 59-74) dynamically imports functions defined in the application's configuration:
public void ImportFunctionsByApp(Apps app, Kernel _kernel)
{
if (_kernel.Plugins.Any(p => p.Name == "AntSKFunctions"))
return;
var functions = new List<KernelFunction>();
ImportApiFunction(app, functions); // HTTP-based plugins
ImportNativeFunction(app, functions); // Local method plugins
_kernel.ImportPluginFromFunctions("AntSKFunctions", functions);
}
API Functions: ImportApiFunction() creates KernelFunction instances that execute HTTP requests using RestSharp, allowing external APIs to be exposed as native kernel functions.
Native Functions: ImportNativeFunction() discovers methods marked with the FunctionService attribute in the DI container, converting them to KernelFunction objects via CreateFunctionFromMethod.
End-to-End Execution Flow
When a request arrives for a specific application:
- Resolution:
GetKernelByApp()queries theAIModelsrepository using the app'sChatModelID - Provider Binding:
WithTextGenerationByAIType()registers the appropriateITextGenerationServiceandIChatCompletionServicebased on theAIType - Default Plugins:
RegisterPluginsWithKernel()adds conversation summarization and KMS capabilities - Custom Functions:
ImportFunctionsByApp()injects application-specific API and native functions - Execution: The fully configured
Kernelinstance returns to the caller for chat completion or text generation
This flow ensures that each application operates within an isolated kernel environment with precisely the capabilities and AI provider specified in its database configuration.
Practical Implementation Examples
Instantiating a Kernel for an Application
// Resolve the application entity
var app = _appsRepository.GetById(appId);
// Create the kernel with dynamic provider loading
var kernel = _kernelService.GetKernelByApp(app);
// Import application-specific functions
_kernelService.ImportFunctionsByApp(app, kernel);
Invoking Dynamically Loaded Functions
// Retrieve a function registered via ImportApiFunction
var apiFunc = kernel.Plugins.GetFunction("AntSKFunctions", "MyExternalApi");
// Execute with JSON payload
var result = await kernel.InvokeAsync(apiFunc,
new KernelArguments { ["jsonbody"] = "{\"query\":\"Hello\"}" });
Console.WriteLine(result.GetValue<string>());
Using SparkDesk Provider
// Application configured with SparkDesk model
var app = _appsRepository.GetById(appId);
var kernel = _kernelService.GetKernelByApp(app);
// The kernel automatically uses SparkDeskTextCompletion and SparkDeskChatCompletion
// registered via keyed DI in WithTextGenerationByAIType
Summary
- KernelService acts as a factory creating fresh, isolated Semantic Kernel instances per application to prevent cross-tenant contamination
- Dynamic provider selection occurs in
WithTextGenerationByAIType(), which maps theAITypeenum to specific service registrations for OpenAI, Azure OpenAI, SparkDesk, DashScope, LLaMA Factory, Ollama, and Mock providers - Keyed dependency injection enables custom provider implementations like SparkDesk to coexist with standard Semantic Kernel services
- Plugin architecture separates default system capabilities (conversation summary, KMS) from application-specific API and native functions imported at runtime
- Database-driven configuration allows AI model selection to change without code deployment, with all provider parameters stored in the
AIModelsentity
Frequently Asked Questions
How does KernelService handle different AI providers without hardcoding credentials?
The service retrieves provider configuration from the AIModels repository, which stores the AIType, ModelName, ModelKey, and EndPoint for each model. The WithTextGenerationByAIType() method uses this data to call the appropriate Semantic Kernel extension methods or register keyed services, ensuring credentials remain in the database while the code remains provider-agnostic.
What is the purpose of keyed singleton registration for providers like SparkDesk?
Keyed dependency injection (AddKeyedSingleton) allows multiple implementations of ITextGenerationService and IChatCompletionService to coexist in the same dependency injection container. When the kernel requests a service for a specific provider, the keyed registration ensures the correct implementation resolves without conflicts between different AI types.
How does the architecture prevent plugin conflicts between applications?
By calling Kernel.CreateBuilder() and builder.Build() for every request in GetKernelByApp(), the service creates completely isolated kernel instances. Since plugins are registered on the specific kernel instance rather than a shared static container, one application's plugin configuration cannot interfere with another's execution environment.
Can applications mix multiple AI providers simultaneously?
While GetKernelByApp() creates a kernel configured for a single chat model, the GetKernelByAIModelID() method allows creating additional kernels with different providers. Applications could instantiate multiple kernels—each with different AI types—and orchestrate them manually, though each individual kernel instance binds to one specific provider configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →