Skip to content

Conversation

@srebrek
Copy link
Collaborator

@srebrek srebrek commented Feb 2, 2026

Closes #113
Closes #96

Add Concrete classes for every supported model. Models can be either Local or Cloud and can implement various capability interfaces such as IReasoning or IVision... (more will be added soon). It is a foundation for safer runtime behavior eg. text to text model to generate an image. It is recommended to create concrete classes for not supported models by yourself and use generic types only for runtime cases eg. dynamically load modal as a plugin.

Tested on local examples and gemini examples.

Will add ImagaGeneration and ToolCalling interfaces in the future as this PR is already big.

Tested with examples for local models and gemini.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the model configuration system to address issues #113 and #96 by introducing a type-safe, capability-based model architecture. The changes replace the string-based model selection with concrete model classes and interfaces.

Changes:

  • Introduced abstract AIModel base class with LocalModel and CloudModel subclasses, plus capability interfaces (IReasoningModel, IVisionModel)
  • Added ModelRegistry to replace the deprecated KnownModels class with thread-safe registration and lookup
  • Created concrete model classes for 30+ local models and 12+ cloud models with proper metadata
  • Updated Chat entity with ModelId (string) and ModelInstance (AIModel) properties for backward compatibility
  • Refactored all service layers, contexts, examples, and tests to use the new type-safe model system

Reviewed changes

Copilot reviewed 58 out of 58 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
src/MaIN.Domain/Models/Abstract/AIModel.cs Defines base model classes and generic variants for runtime model registration
src/MaIN.Domain/Models/Abstract/IModelCapabilities.cs Defines capability interfaces for vision, reasoning, embedding, and TTS
src/MaIN.Domain/Models/Abstract/ModelRegistry.cs Thread-safe registry for model registration and lookup with reflection-based initialization
src/MaIN.Domain/Models/Concrete/LocalModels.cs 30+ concrete local model definitions with download URLs and metadata
src/MaIN.Domain/Models/Concrete/CloudModels.cs 12+ cloud model definitions for OpenAI, Anthropic, Gemini, xAI, GroqCloud, DeepSeek, Ollama
src/MaIN.Domain/Entities/Chat.cs Added ModelInstance property with lazy loading from ModelId
src/MaIN.Services/Services/LLMService/LLMService.cs Updated to use LocalModel with type checking and interface-based capabilities
src/MaIN.Core/Hub/Contexts/ChatContext.cs Added type-safe WithModel overloads accepting AIModel or generic type parameters
src/MaIN.Core/Hub/Contexts/ModelContext.cs Updated download and registry operations to work with new model system
Examples/**/*.cs Updated 15+ examples to use strongly-typed model classes
Test files Updated unit and integration tests to use new model IDs and types
Comments suppressed due to low confidence (2)

src/MaIN.Domain/Exceptions/Models/MissingModelIdException.cs:8

  • The PublicErrorMessage still says "Model name cannot be null or empty" but should be updated to say "Model id cannot be null or empty" to match the renamed exception class and parameter.
    src/MaIN.InferPage/Components/Pages/Home.razor:215
  • Call to obsolete method WithModel.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +77 to +88
public string MMProjectPath => "mmproj-model-f16.gguf";
}

public sealed record Llava16Mistral_7b() : LocalModel(
"llava-1.6-mistral-7b",
"llava-1.6-mistral-7b.gguf",
new Uri("https://huggingface.co/cjpais/llava-1.6-mistral-7b-gguf/resolve/main/llava-v1.6-mistral-7b.Q3_K_XS.gguf?download=true"),
"LLaVA 1.6 Mistral 7B",
4096,
"Vision-language model for image analysis, OCR, and visual Q&A"), IVisionModel
{
public string MMProjectPath => "mmproj-model-f16.gguf";
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Llava_7b and Llava16Mistral_7b use the same MMProjectPath "mmproj-model-f16.gguf". This will cause conflicts if both models are used, as they likely require different projection files. Each model should have its own unique MMProject file name.

Copilot uses AI. Check for mistakes.
return _models.TryRemove(NormalizeId(id), out _);
}

private static string NormalizeId(string id) => id.Trim();
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NormalizeId function only trims whitespace but doesn't perform case normalization, even though the ConcurrentDictionary is initialized with StringComparer.OrdinalIgnoreCase. This is inconsistent. Either remove the case-insensitive comparer or make NormalizeId also lowercase the ID for consistency.

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not register model type {type.Name}: {ex.Message}");
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception messages are written to Console.WriteLine during initialization. This may not be appropriate for all environments (e.g., ASP.NET applications, libraries). Consider using a proper logging framework or making the warning mechanism configurable/injectable.

Copilot uses AI. Check for mistakes.
public AIModel? ModelInstance
{
get => _modelInstance ??= ModelRegistry.GetById(ModelId);
set => (_modelInstance, ModelId) = (value, value?.Id ?? ModelId);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setter has a potential issue: when setting _modelInstance to null, it will keep the current ModelId instead of updating it. This creates an inconsistent state where ModelInstance is null but ModelId retains its old value. Consider: value is not null, set both _modelInstance and ModelId; if value is null, decide whether to clear ModelId or leave it.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +26
// Get built-in Gemini 2.5 Flash model
var model = AIHub.Model().GetModel(new Gemini2_5Flash().Id);

// Or create the model manually if not available in the hub
var customModel = new GenericCloudModel(
"gemini-2.5-flash",
BackendType.Gemini
);

await AIHub.Chat()
.WithModel("gemini-2.5-flash")
.WithModel(customModel)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example shows creating a GenericCloudModel but the model is retrieved from the registry and not used. Either use the retrieved model variable or remove it. The comment suggests getting from the hub, but then a custom model is created and used instead. This could be confusing for users following the example.

Copilot uses AI. Check for mistakes.
// ===== Ollama Models =====

public sealed record OllamaGemma3_4b() : CloudModel(
"gemma3:4b",
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model ID "gemma3-4b" doesn't match the cloud provider's expected format. According to the Gemini example using "gemini-2.5-flash", Gemini models likely use the "gemini-" prefix. However, this is registered as BackendType.Ollama, so it should use Ollama's format which is typically "model:size" (e.g., "gemma3:4b"). The ID should be consistent with how Ollama references models.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +37
if (chat.ModelId == ImageGenService.LocalImageModels.FLUX)
{
chat.Visual = true; // TODO: add IImageGenModel interface and check for that instead
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this is a TODO to add IImageGenModel interface but the code is checking for a hardcoded model ID. This approach defeats the purpose of the interface-based design. The correct fix would be to check if chat.ModelInstance implements IImageGenModel interface once that interface is added, making this type-safe and extensible.

Copilot uses AI. Check for mistakes.
4096,
"Vision-language model for image analysis, OCR, and visual Q&A"), IVisionModel
{
public string MMProjectPath => "mmproj-model-f16.gguf";
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MMProjectPath is specified as a relative filename "mmproj-model-f16.gguf", but in the usage in LLMService.cs line 233, it's combined with modelsPath using Path.Combine(modelsPath, visionModel.MMProjectPath). This assumes the mmproj file is in the same directory as the models. Consider documenting this assumption or making the path more explicit.

Copilot uses AI. Check for mistakes.
var x = model.GetModel("llama3.2:3b");
await model.DownloadAsync(x.Name);
// Get models using ModelRegistry
var gemma = modelContext.GetModel("gemma3-4b");
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to gemma is useless, since its value is never read.

Suggested change
var gemma = modelContext.GetModel("gemma3-4b");
modelContext.GetModel("gemma3-4b");

Copilot uses AI. Check for mistakes.
await model.DownloadAsync(x.Name);
// Get models using ModelRegistry
var gemma = modelContext.GetModel("gemma3-4b");
var llama = modelContext.GetModel("llama3.2-3b");
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to llama is useless, since its value is never read.

Suggested change
var llama = modelContext.GetModel("llama3.2-3b");

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model declaration is prone to typos Handle model configuration for different providers in generic fashion

2 participants