Feature: Add dynamic model selection to ViewReceipt page

Enhance receipt parsing model selection:
- Fetch available models from LlamaCpp server dynamically
- Show loaded/unloaded status in model dropdown
- Persist selected model to appsettings.json
- Read default model from AI:ReceiptParsingModel config
- Inject LlamaCppVisionClient and IConfiguration dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 22:53:40 -05:00
parent fc4855bd1a
commit 38b7d8a9ca
3 changed files with 70 additions and 13 deletions

View File

@@ -165,20 +165,25 @@
<div class="mb-2"> <div class="mb-2">
<label for="model" class="form-label small">AI Model</label> <label for="model" class="form-label small">AI Model</label>
<select name="model" id="model" class="form-select form-select-sm"> <select name="model" id="model" class="form-select form-select-sm">
<optgroup label="Local (llama.cpp)"> @if (Model.AvailableModels.Any())
<option value="llamacpp:GLM-4.6V-UD-Q4_K_XL-00001-of-00002">GLM-4.6V (Vision)</option> {
</optgroup> <optgroup label="Local (llama.cpp)">
<optgroup label="Local (Ollama)"> @foreach (var m in Model.AvailableModels)
<option value="ollama:llava">LLaVA (Vision)</option> {
<option value="ollama:llava:13b">LLaVA 13B (Vision)</option> var modelValue = $"llamacpp:{m.Id}";
</optgroup> <option value="@modelValue" selected="@(Model.SelectedModel == modelValue)">
@(m.IsLoaded ? "[Loaded] " : "")@m.Id
</option>
}
</optgroup>
}
<optgroup label="OpenAI"> <optgroup label="OpenAI">
<option value="gpt-4o-mini" selected>GPT-4o Mini (Fast & Cheap)</option> <option value="gpt-4o-mini" selected="@(Model.SelectedModel == "gpt-4o-mini")">GPT-4o Mini (Fast & Cheap)</option>
<option value="gpt-4o">GPT-4o (Smarter)</option> <option value="gpt-4o" selected="@(Model.SelectedModel == "gpt-4o")">GPT-4o (Smarter)</option>
</optgroup> </optgroup>
<optgroup label="Anthropic"> <optgroup label="Anthropic">
<option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku (Fast)</option> <option value="claude-3-5-haiku-20241022" selected="@(Model.SelectedModel == "claude-3-5-haiku-20241022")">Claude 3.5 Haiku (Fast)</option>
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet (Best)</option> <option value="claude-3-5-sonnet-20241022" selected="@(Model.SelectedModel == "claude-3-5-sonnet-20241022")">Claude 3.5 Sonnet (Best)</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>

View File

@@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore;
using MoneyMap.Data; using MoneyMap.Data;
using MoneyMap.Models; using MoneyMap.Models;
using MoneyMap.Services; using MoneyMap.Services;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MoneyMap.Pages namespace MoneyMap.Pages
{ {
@@ -12,22 +14,33 @@ namespace MoneyMap.Pages
private readonly MoneyMapContext _db; private readonly MoneyMapContext _db;
private readonly IReceiptManager _receiptManager; private readonly IReceiptManager _receiptManager;
private readonly IEnumerable<IReceiptParser> _parsers; private readonly IEnumerable<IReceiptParser> _parsers;
private readonly LlamaCppVisionClient _llamaClient;
private readonly IConfiguration _config;
private readonly IWebHostEnvironment _env;
public ViewReceiptModel( public ViewReceiptModel(
MoneyMapContext db, MoneyMapContext db,
IReceiptManager receiptManager, IReceiptManager receiptManager,
IEnumerable<IReceiptParser> parsers) IEnumerable<IReceiptParser> parsers,
LlamaCppVisionClient llamaClient,
IConfiguration config,
IWebHostEnvironment env)
{ {
_db = db; _db = db;
_receiptManager = receiptManager; _receiptManager = receiptManager;
_parsers = parsers; _parsers = parsers;
_llamaClient = llamaClient;
_config = config;
_env = env;
} }
public Receipt? Receipt { get; set; } public Receipt? Receipt { get; set; }
public List<ReceiptLineItem> LineItems { get; set; } = new(); public List<ReceiptLineItem> LineItems { get; set; } = new();
public List<ReceiptParseLog> ParseLogs { get; set; } = new(); public List<ReceiptParseLog> ParseLogs { get; set; } = new();
public List<ParserOption> AvailableParsers { get; set; } = new(); public List<ParserOption> AvailableParsers { get; set; } = new();
public List<LlamaCppModel> AvailableModels { get; set; } = new();
public string ReceiptUrl { get; set; } = ""; public string ReceiptUrl { get; set; } = "";
public string SelectedModel => _config["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
[TempData] [TempData]
public string? SuccessMessage { get; set; } public string? SuccessMessage { get; set; }
@@ -59,6 +72,9 @@ namespace MoneyMap.Pages
FullName = p.GetType().Name FullName = p.GetType().Name
}).ToList(); }).ToList();
// Get available LlamaCpp models
AvailableModels = await _llamaClient.GetAvailableModelsAsync();
return Page(); return Page();
} }
@@ -94,6 +110,12 @@ namespace MoneyMap.Pages
return RedirectToPage(new { id }); return RedirectToPage(new { id });
} }
// Save selected model to config if it changed
if (!string.IsNullOrEmpty(model) && model != SelectedModel)
{
await SaveSelectedModelAsync(model);
}
var result = await selectedParser.ParseReceiptAsync(id, model); var result = await selectedParser.ParseReceiptAsync(id, model);
if (result.IsSuccess) if (result.IsSuccess)
@@ -108,6 +130,33 @@ namespace MoneyMap.Pages
return RedirectToPage(new { id }); return RedirectToPage(new { id });
} }
private async Task SaveSelectedModelAsync(string model)
{
try
{
var appSettingsPath = Path.Combine(_env.ContentRootPath, "appsettings.json");
var json = await System.IO.File.ReadAllTextAsync(appSettingsPath);
var jsonNode = JsonNode.Parse(json);
if (jsonNode == null) return;
// Ensure AI section exists
if (jsonNode["AI"] == null)
{
jsonNode["AI"] = new JsonObject();
}
jsonNode["AI"]!["ReceiptParsingModel"] = model;
var options = new JsonSerializerOptions { WriteIndented = true };
await System.IO.File.WriteAllTextAsync(appSettingsPath, jsonNode.ToJsonString(options));
}
catch
{
// Silently fail - not critical if we can't save the preference
}
}
public class ParserOption public class ParserOption
{ {
public string Name { get; set; } = ""; public string Name { get; set; } = "";

View File

@@ -21,6 +21,7 @@ namespace MoneyMap.Services
private readonly LlamaCppVisionClient _llamaCppClient; private readonly LlamaCppVisionClient _llamaCppClient;
private readonly IMerchantService _merchantService; private readonly IMerchantService _merchantService;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _configuration;
private readonly ILogger<AIReceiptParser> _logger; private readonly ILogger<AIReceiptParser> _logger;
private string? _promptTemplate; private string? _promptTemplate;
@@ -34,6 +35,7 @@ namespace MoneyMap.Services
LlamaCppVisionClient llamaCppClient, LlamaCppVisionClient llamaCppClient,
IMerchantService merchantService, IMerchantService merchantService,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IConfiguration configuration,
ILogger<AIReceiptParser> logger) ILogger<AIReceiptParser> logger)
{ {
_db = db; _db = db;
@@ -45,6 +47,7 @@ namespace MoneyMap.Services
_llamaCppClient = llamaCppClient; _llamaCppClient = llamaCppClient;
_merchantService = merchantService; _merchantService = merchantService;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_configuration = configuration;
_logger = logger; _logger = logger;
} }
@@ -57,7 +60,7 @@ namespace MoneyMap.Services
if (receipt == null) if (receipt == null)
return ReceiptParseResult.Failure("Receipt not found."); return ReceiptParseResult.Failure("Receipt not found.");
var selectedModel = model ?? "gpt-4o-mini"; var selectedModel = model ?? _configuration["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
var isLlamaCpp = selectedModel.StartsWith("llamacpp:"); var isLlamaCpp = selectedModel.StartsWith("llamacpp:");
var isOllama = selectedModel.StartsWith("ollama:"); var isOllama = selectedModel.StartsWith("ollama:");
var isClaude = selectedModel.StartsWith("claude-"); var isClaude = selectedModel.StartsWith("claude-");