diff --git a/extensions/llamacpp-extension/src/index.ts b/extensions/llamacpp-extension/src/index.ts index f5dfcdd09..744eed3c4 100644 --- a/extensions/llamacpp-extension/src/index.ts +++ b/extensions/llamacpp-extension/src/index.ts @@ -1999,4 +1999,47 @@ export default class llamacpp_extension extends AIEngine { throw new Error(String(e)) } } + + /** + * Validate GGUF file and check for unsupported architectures like CLIP + */ + async validateGgufFile(filePath: string): Promise<{ + isValid: boolean + error?: string + metadata?: GgufMetadata + }> { + try { + logger.info(`Validating GGUF file: ${filePath}`) + const metadata = await readGgufMetadata(filePath) + + // Log full metadata for debugging + logger.info('Full GGUF metadata:', JSON.stringify(metadata, null, 2)) + + // Check if architecture is 'clip' which is not supported for text generation + const architecture = metadata.metadata?.['general.architecture'] + logger.info(`Model architecture: ${architecture}`) + + if (architecture === 'clip') { + const errorMessage = 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.' + logger.error('CLIP architecture detected:', architecture) + return { + isValid: false, + error: errorMessage, + metadata + } + } + + logger.info('Model validation passed. Architecture:', architecture) + return { + isValid: true, + metadata + } + } catch (error) { + logger.error('Failed to validate GGUF file:', error) + return { + isValid: false, + error: `Failed to read model metadata: ${error instanceof Error ? error.message : 'Unknown error'}` + } + } + } } diff --git a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx index 840da9c8e..c18a083eb 100644 --- a/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx +++ b/web-app/src/containers/dialogs/ImportVisionModelDialog.tsx @@ -17,6 +17,7 @@ import { IconLoader2, IconEye, IconCheck, + IconAlertTriangle, } from '@tabler/icons-react' type ImportVisionModelDialogProps = { @@ -37,6 +38,175 @@ export const ImportVisionModelDialog = ({ const [modelFile, setModelFile] = useState(null) const [mmProjFile, setMmProjFile] = useState(null) const [modelName, setModelName] = useState('') + const [validationError, setValidationError] = useState(null) + const [isValidating, setIsValidating] = useState(false) + const [mmprojValidationError, setMmprojValidationError] = useState< + string | null + >(null) + const [isValidatingMmproj, setIsValidatingMmproj] = useState(false) + + const validateGgufFile = async ( + filePath: string, + fileType: 'model' | 'mmproj' + ): Promise => { + if (fileType === 'model') { + setIsValidating(true) + setValidationError(null) + } else { + setIsValidatingMmproj(true) + setMmprojValidationError(null) + } + + try { + console.log(`Reading GGUF metadata for ${fileType}:`, filePath) + + // Try to use the validateGgufFile method if available + if (typeof serviceHub.models().validateGgufFile === 'function') { + 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) + + // Validate based on file type + if (fileType === 'model') { + // Model files should NOT be clip + if (architecture === 'clip') { + const errorMessage = + 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.' + setValidationError(errorMessage) + console.error( + 'CLIP architecture detected in model file:', + architecture + ) + } else { + console.log( + 'Model validation passed. Architecture:', + architecture + ) + } + } else { + // MMProj files MUST be clip + if (architecture !== 'clip') { + const errorMessage = `This MMProj file has "${architecture}" architecture but should have "clip" architecture. MMProj files must be CLIP models for vision processing.` + setMmprojValidationError(errorMessage) + console.error( + 'Non-CLIP architecture detected in mmproj file:', + architecture + ) + } else { + console.log( + 'MMProj validation passed. Architecture:', + architecture + ) + } + } + } + + if (!result.isValid && fileType === 'model') { + setValidationError(result.error || 'Model validation failed') + console.error('Model validation failed:', result.error) + } else if (!result.isValid && fileType === 'mmproj') { + setMmprojValidationError(result.error || 'MMProj validation failed') + console.error('MMProj validation failed:', result.error) + } + } else { + // Fallback: Try to call the Tauri plugin directly if available + try { + // Import the readGgufMetadata function directly from Tauri + const { invoke } = await import('@tauri-apps/api/core') + + const metadata = await invoke('plugin:llamacpp|read_gguf_metadata', { + 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) + + if (fileType === 'model') { + // Model files should NOT be clip + if (architecture === 'clip') { + const errorMessage = + 'This model has CLIP architecture and cannot be imported as a text generation model. CLIP models are designed for vision tasks and require different handling.' + setValidationError(errorMessage) + console.error( + 'CLIP architecture detected in model file:', + architecture + ) + } else { + console.log( + 'Model validation passed. Architecture:', + architecture + ) + } + } else { + // MMProj files MUST be clip + if (architecture !== 'clip') { + const errorMessage = `This MMProj file has "${architecture}" architecture but should have "clip" architecture. MMProj files must be CLIP models for vision processing.` + setMmprojValidationError(errorMessage) + console.error( + 'Non-CLIP architecture detected in mmproj file:', + architecture + ) + } else { + console.log( + 'MMProj validation passed. Architecture:', + architecture + ) + } + } + } catch (tauriError) { + console.warn( + `Tauri validation fallback failed for ${fileType}:`, + tauriError + ) + // Final fallback: just warn and allow + console.log( + `${fileType} validation skipped - validation service not available` + ) + } + } + } catch (error) { + console.error(`Failed to validate ${fileType} file:`, error) + const errorMessage = `Failed to read ${fileType} metadata: ${error instanceof Error ? error.message : 'Unknown error'}` + + if (fileType === 'model') { + setValidationError(errorMessage) + } else { + setMmprojValidationError(errorMessage) + } + } finally { + if (fileType === 'model') { + setIsValidating(false) + } else { + setIsValidatingMmproj(false) + } + } + } + + const validateModelFile = async (filePath: string): Promise => { + await validateGgufFile(filePath, 'model') + } + + const validateMmprojFile = async (filePath: string): Promise => { + await validateGgufFile(filePath, 'mmproj') + } const handleFileSelect = async (type: 'model' | 'mmproj') => { const selectedFile = await serviceHub.dialog().open({ @@ -55,8 +225,13 @@ export const ImportVisionModelDialog = ({ .replace(/\.(gguf|GGUF)$/, '') .replace(/[^a-zA-Z0-9/_.-]/g, '') // Remove any characters not allowed in model IDs setModelName(sanitizedName) + + // Validate the selected model file + await validateModelFile(selectedFile) } else { setMmProjFile(selectedFile) + // Validate the selected mmproj file + await validateMmprojFile(selectedFile) } } } @@ -131,6 +306,10 @@ export const ImportVisionModelDialog = ({ setMmProjFile(null) setModelName('') setIsVisionModel(false) + setValidationError(null) + setIsValidating(false) + setMmprojValidationError(null) + setIsValidatingMmproj(false) } const handleOpenChange = (newOpen: boolean) => { @@ -209,24 +388,73 @@ export const ImportVisionModelDialog = ({ {modelFile ? ( -
-
-
- - - {modelFile.split(/[\\/]/).pop()} - +
+
+
+
+ {isValidating ? ( + + ) : validationError ? ( + + ) : ( + + )} + + {modelFile.split(/[\\/]/).pop()} + +
+
-
+ + {/* Validation Error Display */} + {validationError && ( +
+
+ +
+

+ Model Validation Error +

+

+ {validationError} +

+
+
+
+ )} + + {/* Validation Loading State */} + {isValidating && ( +
+
+ +

+ Validating model file... +

+
+
+ )}
) : (
-
+ + {/* MMProj Validation Error Display */} + {mmprojValidationError && ( +
+
+ +
+

+ MMProj Validation Error +

+

+ {mmprojValidationError} +

+
+
+
+ )} + + {/* MMProj Validation Loading State */} + {isValidatingMmproj && ( +
+
+ +

+ Validating MMProj file... +

+
+
+ )}
) : (