-
Notifications
You must be signed in to change notification settings - Fork 20
Refactor/model configuration #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
5f4b42e to
c1f0045
Compare
There was a problem hiding this 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
AIModelbase class withLocalModelandCloudModelsubclasses, plus capability interfaces (IReasoningModel,IVisionModel) - Added
ModelRegistryto replace the deprecatedKnownModelsclass with thread-safe registration and lookup - Created concrete model classes for 30+ local models and 12+ cloud models with proper metadata
- Updated
Chatentity withModelId(string) andModelInstance(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.
| 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"; |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| return _models.TryRemove(NormalizeId(id), out _); | ||
| } | ||
|
|
||
| private static string NormalizeId(string id) => id.Trim(); |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.WriteLine($"Warning: Could not register model type {type.Name}: {ex.Message}"); |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| public AIModel? ModelInstance | ||
| { | ||
| get => _modelInstance ??= ModelRegistry.GetById(ModelId); | ||
| set => (_modelInstance, ModelId) = (value, value?.Id ?? ModelId); |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| // 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) |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| // ===== Ollama Models ===== | ||
|
|
||
| public sealed record OllamaGemma3_4b() : CloudModel( | ||
| "gemma3:4b", |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| if (chat.ModelId == ImageGenService.LocalImageModels.FLUX) | ||
| { | ||
| chat.Visual = true; // TODO: add IImageGenModel interface and check for that instead |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| 4096, | ||
| "Vision-language model for image analysis, OCR, and visual Q&A"), IVisionModel | ||
| { | ||
| public string MMProjectPath => "mmproj-model-f16.gguf"; |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| var x = model.GetModel("llama3.2:3b"); | ||
| await model.DownloadAsync(x.Name); | ||
| // Get models using ModelRegistry | ||
| var gemma = modelContext.GetModel("gemma3-4b"); |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| var gemma = modelContext.GetModel("gemma3-4b"); | |
| modelContext.GetModel("gemma3-4b"); |
| await model.DownloadAsync(x.Name); | ||
| // Get models using ModelRegistry | ||
| var gemma = modelContext.GetModel("gemma3-4b"); | ||
| var llama = modelContext.GetModel("llama3.2-3b"); |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
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.
| var llama = modelContext.GetModel("llama3.2-3b"); |
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.