From ea354ce621e9af45f9f8defa864229ee3518b713 Mon Sep 17 00:00:00 2001
From: Faisal Amir
Date: Tue, 16 Sep 2025 15:44:58 +0700
Subject: [PATCH 1/3] fix/validate-mmproj-from-general-basename
---
.../dialogs/ImportVisionModelDialog.tsx | 52 +++++++++----------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
index f1eb77b22..47851963d 100644
--- a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
+++ b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
@@ -57,8 +57,6 @@ export const ImportVisionModelDialog = ({
}
try {
- console.log(`Reading GGUF metadata for ${fileType}:`, filePath)
-
// Handle validation differently for model files vs mmproj files
if (fileType === 'model') {
// For model files, use the standard validateGgufFile method
@@ -66,16 +64,16 @@ export const ImportVisionModelDialog = ({
const result = await serviceHub.models().validateGgufFile(filePath)
if (result.metadata) {
- // Log full metadata for debugging
- console.log(
- `Full GGUF metadata for ${fileType}:`,
- JSON.stringify(result.metadata, null, 2)
- )
-
// Check architecture from metadata
const architecture =
result.metadata.metadata?.['general.architecture']
- console.log(`${fileType} architecture:`, architecture)
+
+ // Extract baseName and use it as model name if available
+ const baseName = result.metadata.metadata?.['general.basename']
+
+ if (baseName) {
+ setModelName(baseName)
+ }
// Model files should NOT be clip
if (architecture === 'clip') {
@@ -86,11 +84,6 @@ export const ImportVisionModelDialog = ({
'CLIP architecture detected in model file:',
architecture
)
- } else {
- console.log(
- 'Model validation passed. Architecture:',
- architecture
- )
}
}
@@ -109,16 +102,15 @@ export const ImportVisionModelDialog = ({
path: filePath,
})
- console.log(
- `Full GGUF metadata for ${fileType}:`,
- JSON.stringify(metadata, null, 2)
- )
// Check if architecture matches expected type
const architecture = (
metadata as { metadata?: Record }
).metadata?.['general.architecture']
- console.log(`${fileType} architecture:`, architecture)
+
+ // Get general.baseName from metadata
+ const baseName = (metadata as { metadata?: Record })
+ .metadata?.['general.basename']
// MMProj files MUST be clip
if (architecture !== 'clip') {
@@ -128,11 +120,19 @@ export const ImportVisionModelDialog = ({
'Non-CLIP architecture detected in mmproj file:',
architecture
)
- } else {
- console.log(
- 'MMProj validation passed. Architecture:',
- architecture
- )
+ } else if (
+ baseName &&
+ modelName &&
+ !modelName.toLowerCase().includes(baseName.toLowerCase()) &&
+ !baseName.toLowerCase().includes(modelName.toLowerCase())
+ ) {
+ // Validate that baseName and model name are compatible (one should contain the other)
+ const errorMessage = `MMProj file baseName "${baseName}" does not match model name "${modelName}". The MMProj file should be compatible with the selected model.`
+ setMmprojValidationError(errorMessage)
+ console.error('BaseName mismatch in mmproj file:', {
+ baseName,
+ modelName,
+ })
}
} catch (directError) {
console.error('Failed to validate mmproj file directly:', directError)
@@ -179,14 +179,14 @@ export const ImportVisionModelDialog = ({
if (type === 'model') {
setModelFile(selectedFile)
- // Auto-generate model name from GGUF file
+ // Set temporary model name from filename (will be overridden by baseName from metadata if available)
const sanitizedName = fileName
.replace(/\s/g, '-')
.replace(/\.(gguf|GGUF)$/, '')
.replace(/[^a-zA-Z0-9/_.-]/g, '') // Remove any characters not allowed in model IDs
setModelName(sanitizedName)
- // Validate the selected model file
+ // Validate the selected model file (this will update model name with baseName from metadata)
await validateModelFile(selectedFile)
} else {
setMmProjFile(selectedFile)
From 272ef9f8b886389b36e3c3b0503dff02c6be26f9 Mon Sep 17 00:00:00 2001
From: Faisal Amir
Date: Tue, 16 Sep 2025 15:59:59 +0700
Subject: [PATCH 2/3] fix/revalidate-model-gguf
---
.../dialogs/ImportVisionModelDialog.tsx | 21 ++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
index 47851963d..73a2caabf 100644
--- a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
+++ b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
@@ -10,7 +10,7 @@ import {
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { useServiceHub } from '@/hooks/useServiceHub'
-import { useState } from 'react'
+import { useState, useEffect, useCallback } from 'react'
import { toast } from 'sonner'
import {
IconLoader2,
@@ -44,7 +44,7 @@ export const ImportVisionModelDialog = ({
>(null)
const [isValidatingMmproj, setIsValidatingMmproj] = useState(false)
- const validateGgufFile = async (
+ const validateGgufFile = useCallback(async (
filePath: string,
fileType: 'model' | 'mmproj'
): Promise => {
@@ -158,15 +158,15 @@ export const ImportVisionModelDialog = ({
setIsValidatingMmproj(false)
}
}
- }
+ }, [modelName, serviceHub])
- const validateModelFile = async (filePath: string): Promise => {
+ const validateModelFile = useCallback(async (filePath: string): Promise => {
await validateGgufFile(filePath, 'model')
- }
+ }, [validateGgufFile])
- const validateMmprojFile = async (filePath: string): Promise => {
+ const validateMmprojFile = useCallback(async (filePath: string): Promise => {
await validateGgufFile(filePath, 'mmproj')
- }
+ }, [validateGgufFile])
const handleFileSelect = async (type: 'model' | 'mmproj') => {
const selectedFile = await serviceHub.dialog().open({
@@ -272,6 +272,13 @@ export const ImportVisionModelDialog = ({
setIsValidatingMmproj(false)
}
+ // Re-validate MMProj file when model name changes
+ useEffect(() => {
+ if (mmProjFile && modelName && isVisionModel) {
+ validateMmprojFile(mmProjFile)
+ }
+ }, [modelName, mmProjFile, isVisionModel, validateMmprojFile])
+
const handleOpenChange = (newOpen: boolean) => {
if (!importing) {
setOpen(newOpen)
From 0945eaedcd7d3159bbcd44c92af7be836bd90f9f Mon Sep 17 00:00:00 2001
From: Faisal Amir
Date: Tue, 16 Sep 2025 16:53:47 +0700
Subject: [PATCH 3/3] fix: loader when importing
---
.../settings/providers/$providerName.tsx | 136 ++++++++++++------
1 file changed, 95 insertions(+), 41 deletions(-)
diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx
index 533536281..efba6233c 100644
--- a/web-app/src/routes/settings/providers/$providerName.tsx
+++ b/web-app/src/routes/settings/providers/$providerName.tsx
@@ -83,6 +83,7 @@ function ProviderDetail() {
const [refreshingModels, setRefreshingModels] = useState(false)
const [isCheckingBackendUpdate, setIsCheckingBackendUpdate] = useState(false)
const [isInstallingBackend, setIsInstallingBackend] = useState(false)
+ const [importingModel, setImportingModel] = useState(null)
const { checkForUpdate: checkForBackendUpdate, installBackend } =
useBackendUpdater()
const { providerName } = useParams({ from: Route.id })
@@ -102,58 +103,66 @@ function ProviderDetail() {
)
const handleModelImportSuccess = async (importedModelName?: string) => {
- // Refresh the provider to update the models list
- await serviceHub.providers().getProviders().then(setProviders)
+ if (importedModelName) {
+ setImportingModel(importedModelName)
+ }
- // If a model was imported and it might have vision capabilities, check and update
- if (importedModelName && providerName === 'llamacpp') {
- try {
- const mmprojExists = await serviceHub
- .models()
- .checkMmprojExists(importedModelName)
- if (mmprojExists) {
- // Get the updated provider after refresh
- const { getProviderByName, updateProvider: updateProviderState } =
- useModelProvider.getState()
- const llamacppProvider = getProviderByName('llamacpp')
+ try {
+ // Refresh the provider to update the models list
+ await serviceHub.providers().getProviders().then(setProviders)
- if (llamacppProvider) {
- const modelIndex = llamacppProvider.models.findIndex(
- (m: Model) => m.id === importedModelName
- )
- if (modelIndex !== -1) {
- const model = llamacppProvider.models[modelIndex]
- const capabilities = model.capabilities || []
+ // If a model was imported and it might have vision capabilities, check and update
+ if (importedModelName && providerName === 'llamacpp') {
+ try {
+ const mmprojExists = await serviceHub
+ .models()
+ .checkMmprojExists(importedModelName)
+ if (mmprojExists) {
+ // Get the updated provider after refresh
+ const { getProviderByName, updateProvider: updateProviderState } =
+ useModelProvider.getState()
+ const llamacppProvider = getProviderByName('llamacpp')
- // Add 'vision' capability if not already present AND if user hasn't manually configured capabilities
- // Check if model has a custom capabilities config flag
+ if (llamacppProvider) {
+ const modelIndex = llamacppProvider.models.findIndex(
+ (m: Model) => m.id === importedModelName
+ )
+ if (modelIndex !== -1) {
+ const model = llamacppProvider.models[modelIndex]
+ const capabilities = model.capabilities || []
- const hasUserConfiguredCapabilities =
- (model as any)._userConfiguredCapabilities === true
+ // Add 'vision' capability if not already present AND if user hasn't manually configured capabilities
+ // Check if model has a custom capabilities config flag
- if (
- !capabilities.includes('vision') &&
- !hasUserConfiguredCapabilities
- ) {
- const updatedModels = [...llamacppProvider.models]
- updatedModels[modelIndex] = {
- ...model,
- capabilities: [...capabilities, 'vision'],
- // Mark this as auto-detected, not user-configured
- _autoDetectedVision: true,
- } as any
+ const hasUserConfiguredCapabilities =
+ (model as any)._userConfiguredCapabilities === true
- updateProviderState('llamacpp', { models: updatedModels })
- console.log(
- `Vision capability added to model after provider refresh: ${importedModelName}`
- )
+ if (
+ !capabilities.includes('vision') &&
+ !hasUserConfiguredCapabilities
+ ) {
+ const updatedModels = [...llamacppProvider.models]
+ updatedModels[modelIndex] = {
+ ...model,
+ capabilities: [...capabilities, 'vision'],
+ // Mark this as auto-detected, not user-configured
+ _autoDetectedVision: true,
+ } as any
+
+ updateProviderState('llamacpp', { models: updatedModels })
+ console.log(
+ `Vision capability added to model after provider refresh: ${importedModelName}`
+ )
+ }
}
}
}
+ } catch (error) {
+ console.error('Error checking mmproj existence after import:', error)
}
- } catch (error) {
- console.error('Error checking mmproj existence after import:', error)
}
+ } finally {
+ // The importing state will be cleared by the useEffect when model appears in list
}
}
@@ -175,6 +184,29 @@ function ProviderDetail() {
return () => clearInterval(intervalId)
}, [serviceHub, setActiveModels])
+ // Clear importing state when model appears in the provider's model list
+ useEffect(() => {
+ if (importingModel && provider?.models) {
+ const modelExists = provider.models.some(
+ (model) => model.id === importingModel
+ )
+ if (modelExists) {
+ setImportingModel(null)
+ }
+ }
+ }, [importingModel, provider?.models])
+
+ // Fallback: Clear importing state after 10 seconds to prevent infinite loading
+ useEffect(() => {
+ if (importingModel) {
+ const timeoutId = setTimeout(() => {
+ setImportingModel(null)
+ }, 10000) // 10 seconds fallback
+
+ return () => clearTimeout(timeoutId)
+ }
+ }, [importingModel])
+
// Auto-refresh provider settings to get updated backend configuration
const refreshSettings = useCallback(async () => {
if (!provider) return
@@ -831,6 +863,28 @@ function ProviderDetail() {
)}
+ {/* Show importing skeleton first if there's one */}
+ {importingModel && (
+
+
+
+
+ Importing...
+
+
+ {importingModel}
+
+
+
+ }
+ />
+ )}