Skip to content

Commit 0264765

Browse files
committed
Support custom model path and MMProj hints
Home: when a custom Model Path is set for Self backend, bake the resolved full file path into a generic local model instance so the LLMService loads from the correct location (creates GenericLocal[Vision][Reasoning]Model variants with FileName set to the resolved path). This fixes cases where Chat.Properties don't reach the service and the model file must be embedded in the model object. Settings: add RegisteredMmProjPathHint to display the expected MMProj file path for registered local vision models (shows a "MMProj file: ..." hint derived from ResolvedModelPathPreview or fallback model directory), keeping the hint in sync with the "Will load:" preview. LLMService: when resolving an mmproj for image models, prefer the directory of a fully-qualified modelKey (custom model file path) and fall back to the configured models path; this ensures mmproj files are located next to custom model files.
1 parent 1041cc5 commit 0264765

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

src/MaIN.InferPage/Components/Pages/Home.razor

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,40 @@
330330
if (string.IsNullOrEmpty(Utils.Model)) return null;
331331

332332
if (ModelRegistry.TryGetById(Utils.Model, out var registeredModel))
333+
{
334+
// When a custom Model Path is set, bake the resolved full path into a generic
335+
// model so LLMService loads from the right location. This is needed because
336+
// ctx._chat (which reaches LLMService) is a separate object from Home.razor's Chat,
337+
// so Chat.Properties never reach the service — the path must be in model.FileName.
338+
if (Utils.BackendType == BackendType.Self && registeredModel is LocalModel localReg
339+
&& !string.IsNullOrEmpty(Utils.Path))
340+
{
341+
var fullPath = Utils.Path.EndsWith(".gguf", StringComparison.OrdinalIgnoreCase)
342+
? Utils.Path
343+
: System.IO.Path.Combine(Utils.Path, localReg.FileName);
344+
345+
return localReg switch
346+
{
347+
IVisionModel vm when localReg is IReasoningModel { ReasonFunction: not null } rm
348+
=> new GenericLocalVisionReasoningModel(
349+
FileName: fullPath, MMProjectPath: vm.MMProjectName ?? "",
350+
ReasonFunction: rm.ReasonFunction,
351+
MaxContextWindowSize: localReg.MaxContextWindowSize),
352+
IVisionModel vm
353+
=> new GenericLocalVisionModel(
354+
FileName: fullPath, MMProjectPath: vm.MMProjectName ?? "",
355+
MaxContextWindowSize: localReg.MaxContextWindowSize),
356+
IReasoningModel { ReasonFunction: not null } rm
357+
=> new GenericLocalReasoningModel(
358+
FileName: fullPath, ReasonFunction: rm.ReasonFunction,
359+
MaxContextWindowSize: localReg.MaxContextWindowSize),
360+
_ => new GenericLocalModel(
361+
FileName: fullPath,
362+
MaxContextWindowSize: localReg.MaxContextWindowSize)
363+
};
364+
}
333365
return registeredModel;
366+
}
334367

335368
// Self = local GGUF file; Ollama Local is an HTTP server, not a file path
336369
if (Utils.BackendType == BackendType.Self)

src/MaIN.InferPage/Components/Pages/Settings.razor

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
<span class="no-capabilities">No special capabilities</span>
103103
}
104104
</div>
105+
@if (RegisteredMmProjPathHint is { } mmProjHint)
106+
{
107+
<span class="api-key-hint">MMProj file: @mmProjHint</span>
108+
}
105109
}
106110
else
107111
{
@@ -201,6 +205,30 @@
201205
private bool _detectedReasoning;
202206
private bool _detectedImageGen;
203207

208+
// Shows expected MMProj file path for registered local vision models.
209+
// Derives the directory from ResolvedModelPathPreview so it always stays in sync
210+
// with the "Will load:" hint (including any custom Model Path the user typed).
211+
private string? RegisteredMmProjPathHint
212+
{
213+
get
214+
{
215+
if (_selectedBackend?.BackendType != BackendType.Self || !_isRegisteredModel) return null;
216+
if (!_detectedVision) return null;
217+
if (string.IsNullOrWhiteSpace(_modelName)) return null;
218+
if (!ModelRegistry.TryGetById(_modelName, out var aiModel) || aiModel is not LocalModel localModel) return null;
219+
if (localModel is not IVisionModel visionModel || visionModel.MMProjectName is not { } mmProjName) return null;
220+
221+
// Reuse the already-resolved model file path to derive the directory,
222+
// so changes to the Model Path field are immediately reflected here too.
223+
var modelFilePath = ResolvedModelPathPreview;
224+
var dir = modelFilePath != null
225+
? (Path.GetDirectoryName(modelFilePath) ?? "")
226+
: (MaINSettings.ModelsPath ?? Environment.GetEnvironmentVariable("MaIN_ModelsPath") ?? "");
227+
228+
return string.IsNullOrEmpty(dir) ? mmProjName : Path.Combine(dir, mmProjName);
229+
}
230+
}
231+
204232
// Manual capability selection (unregistered models)
205233
private bool _manualVision;
206234
private bool _manualReasoning;

src/MaIN.Services/Services/LLMService/LLMService.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,12 @@ private async Task<List<LLMTokenValue>> ProcessChatRequest(
243243
MtmdWeights? mtmdWeights = null;
244244
if (mmProjName is not null && lastMsg.Image != null)
245245
{
246-
var mmProjPath = ResolvePath(null, mmProjName);
246+
// When the model file is at a custom location (fully-qualified path),
247+
// look for the mmproj in the same directory. Otherwise fall back to modelsPath.
248+
var mmProjDir = Path.IsPathFullyQualified(modelKey)
249+
? Path.GetDirectoryName(modelKey)
250+
: null;
251+
var mmProjPath = ResolvePath(mmProjDir, mmProjName);
247252
mtmdWeights = await MtmdWeights.LoadFromFileAsync(
248253
mmProjPath, llmModel, MtmdContextParams.Default(), cancellationToken);
249254
}

0 commit comments

Comments
 (0)