chore: validate gguf file base metadata architecture
This commit is contained in:
parent
836990b7d9
commit
be851ebcf1
@ -1999,4 +1999,47 @@ export default class llamacpp_extension extends AIEngine {
|
|||||||
throw new Error(String(e))
|
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'}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
IconLoader2,
|
IconLoader2,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
|
IconAlertTriangle,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
|
|
||||||
type ImportVisionModelDialogProps = {
|
type ImportVisionModelDialogProps = {
|
||||||
@ -37,6 +38,175 @@ export const ImportVisionModelDialog = ({
|
|||||||
const [modelFile, setModelFile] = useState<string | null>(null)
|
const [modelFile, setModelFile] = useState<string | null>(null)
|
||||||
const [mmProjFile, setMmProjFile] = useState<string | null>(null)
|
const [mmProjFile, setMmProjFile] = useState<string | null>(null)
|
||||||
const [modelName, setModelName] = useState('')
|
const [modelName, setModelName] = useState('')
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(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<void> => {
|
||||||
|
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<string, string> }
|
||||||
|
).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<void> => {
|
||||||
|
await validateGgufFile(filePath, 'model')
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateMmprojFile = async (filePath: string): Promise<void> => {
|
||||||
|
await validateGgufFile(filePath, 'mmproj')
|
||||||
|
}
|
||||||
|
|
||||||
const handleFileSelect = async (type: 'model' | 'mmproj') => {
|
const handleFileSelect = async (type: 'model' | 'mmproj') => {
|
||||||
const selectedFile = await serviceHub.dialog().open({
|
const selectedFile = await serviceHub.dialog().open({
|
||||||
@ -55,8 +225,13 @@ export const ImportVisionModelDialog = ({
|
|||||||
.replace(/\.(gguf|GGUF)$/, '')
|
.replace(/\.(gguf|GGUF)$/, '')
|
||||||
.replace(/[^a-zA-Z0-9/_.-]/g, '') // Remove any characters not allowed in model IDs
|
.replace(/[^a-zA-Z0-9/_.-]/g, '') // Remove any characters not allowed in model IDs
|
||||||
setModelName(sanitizedName)
|
setModelName(sanitizedName)
|
||||||
|
|
||||||
|
// Validate the selected model file
|
||||||
|
await validateModelFile(selectedFile)
|
||||||
} else {
|
} else {
|
||||||
setMmProjFile(selectedFile)
|
setMmProjFile(selectedFile)
|
||||||
|
// Validate the selected mmproj file
|
||||||
|
await validateMmprojFile(selectedFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,6 +306,10 @@ export const ImportVisionModelDialog = ({
|
|||||||
setMmProjFile(null)
|
setMmProjFile(null)
|
||||||
setModelName('')
|
setModelName('')
|
||||||
setIsVisionModel(false)
|
setIsVisionModel(false)
|
||||||
|
setValidationError(null)
|
||||||
|
setIsValidating(false)
|
||||||
|
setMmprojValidationError(null)
|
||||||
|
setIsValidatingMmproj(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
@ -209,24 +388,73 @@ export const ImportVisionModelDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{modelFile ? (
|
{modelFile ? (
|
||||||
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<IconCheck size={16} className="text-accent" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-main-view-fg">
|
{isValidating ? (
|
||||||
{modelFile.split(/[\\/]/).pop()}
|
<IconLoader2
|
||||||
</span>
|
size={16}
|
||||||
|
className="text-accent animate-spin"
|
||||||
|
/>
|
||||||
|
) : validationError ? (
|
||||||
|
<IconAlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-destructive"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconCheck size={16} className="text-accent" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium text-main-view-fg">
|
||||||
|
{modelFile.split(/[\\/]/).pop()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleFileSelect('model')}
|
||||||
|
disabled={importing || isValidating}
|
||||||
|
className="text-accent hover:text-accent/80"
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleFileSelect('model')}
|
|
||||||
disabled={importing}
|
|
||||||
className="text-accent hover:text-accent/80"
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Error Display */}
|
||||||
|
{validationError && (
|
||||||
|
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<IconAlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-destructive mt-0.5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-destructive-fg">
|
||||||
|
Model Validation Error
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-destructive-fg/70 mt-1">
|
||||||
|
{validationError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validation Loading State */}
|
||||||
|
{isValidating && (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconLoader2
|
||||||
|
size={16}
|
||||||
|
className="text-blue-500 animate-spin"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
Validating model file...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -252,24 +480,73 @@ export const ImportVisionModelDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mmProjFile ? (
|
{mmProjFile ? (
|
||||||
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<IconCheck size={16} className="text-accent" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-main-view-fg">
|
{isValidatingMmproj ? (
|
||||||
{mmProjFile.split(/[\\/]/).pop()}
|
<IconLoader2
|
||||||
</span>
|
size={16}
|
||||||
|
className="text-accent animate-spin"
|
||||||
|
/>
|
||||||
|
) : mmprojValidationError ? (
|
||||||
|
<IconAlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-destructive"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconCheck size={16} className="text-accent" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium text-main-view-fg">
|
||||||
|
{mmProjFile.split(/[\\/]/).pop()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleFileSelect('mmproj')}
|
||||||
|
disabled={importing || isValidatingMmproj}
|
||||||
|
className="text-accent hover:text-accent/80"
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleFileSelect('mmproj')}
|
|
||||||
disabled={importing}
|
|
||||||
className="text-accent hover:text-accent/80"
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MMProj Validation Error Display */}
|
||||||
|
{mmprojValidationError && (
|
||||||
|
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<IconAlertTriangle
|
||||||
|
size={16}
|
||||||
|
className="text-destructive mt-0.5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-destructive-fg">
|
||||||
|
MMProj Validation Error
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-destructive-fg/70 mt-1">
|
||||||
|
{mmprojValidationError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MMProj Validation Loading State */}
|
||||||
|
{isValidatingMmproj && (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconLoader2
|
||||||
|
size={16}
|
||||||
|
className="text-blue-500 animate-spin"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
Validating MMProj file...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -303,7 +580,11 @@ export const ImportVisionModelDialog = ({
|
|||||||
importing ||
|
importing ||
|
||||||
!modelFile ||
|
!modelFile ||
|
||||||
!modelName ||
|
!modelName ||
|
||||||
(isVisionModel && !mmProjFile)
|
(isVisionModel && !mmProjFile) ||
|
||||||
|
validationError !== null ||
|
||||||
|
isValidating ||
|
||||||
|
mmprojValidationError !== null ||
|
||||||
|
isValidatingMmproj
|
||||||
}
|
}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,7 +11,13 @@ import {
|
|||||||
modelInfo,
|
modelInfo,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { Model as CoreModel } from '@janhq/core'
|
import { Model as CoreModel } from '@janhq/core'
|
||||||
import type { ModelsService, ModelCatalog, HuggingFaceRepo, CatalogModel } from './types'
|
import type {
|
||||||
|
ModelsService,
|
||||||
|
ModelCatalog,
|
||||||
|
HuggingFaceRepo,
|
||||||
|
CatalogModel,
|
||||||
|
ModelValidationResult,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
// TODO: Replace this with the actual provider later
|
// TODO: Replace this with the actual provider later
|
||||||
const defaultProvider = 'llamacpp'
|
const defaultProvider = 'llamacpp'
|
||||||
@ -151,7 +157,9 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
|
|
||||||
async updateModel(model: Partial<CoreModel>): Promise<void> {
|
async updateModel(model: Partial<CoreModel>): Promise<void> {
|
||||||
if (model.settings)
|
if (model.settings)
|
||||||
this.getEngine()?.updateSettings(model.settings as SettingComponentProps[])
|
this.getEngine()?.updateSettings(
|
||||||
|
model.settings as SettingComponentProps[]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async pullModel(
|
async pullModel(
|
||||||
@ -266,7 +274,10 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
if (models) await Promise.all(models.map((model) => this.stopModel(model)))
|
if (models) await Promise.all(models.map((model) => this.stopModel(model)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async startModel(provider: ProviderObject, model: string): Promise<SessionInfo | undefined> {
|
async startModel(
|
||||||
|
provider: ProviderObject,
|
||||||
|
model: string
|
||||||
|
): Promise<SessionInfo | undefined> {
|
||||||
const engine = this.getEngine(provider.provider)
|
const engine = this.getEngine(provider.provider)
|
||||||
if (!engine) return undefined
|
if (!engine) return undefined
|
||||||
|
|
||||||
@ -312,7 +323,10 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
|
|
||||||
async checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
async checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
updateProvider?: (providerName: string, data: Partial<ModelProvider>) => void,
|
updateProvider?: (
|
||||||
|
providerName: string,
|
||||||
|
data: Partial<ModelProvider>
|
||||||
|
) => void,
|
||||||
getProviderByName?: (providerName: string) => ModelProvider | undefined
|
getProviderByName?: (providerName: string) => ModelProvider | undefined
|
||||||
): Promise<{ exists: boolean; settingsUpdated: boolean }> {
|
): Promise<{ exists: boolean; settingsUpdated: boolean }> {
|
||||||
let settingsUpdated = false
|
let settingsUpdated = false
|
||||||
@ -374,7 +388,8 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
(p: { provider: string }) => p.provider === 'llamacpp'
|
(p: { provider: string }) => p.provider === 'llamacpp'
|
||||||
)
|
)
|
||||||
const model = llamacppProvider?.models?.find(
|
const model = llamacppProvider?.models?.find(
|
||||||
(m: { id: string; settings?: Record<string, unknown> }) => m.id === modelId
|
(m: { id: string; settings?: Record<string, unknown> }) =>
|
||||||
|
m.id === modelId
|
||||||
)
|
)
|
||||||
|
|
||||||
if (model?.settings) {
|
if (model?.settings) {
|
||||||
@ -429,7 +444,10 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async isModelSupported(modelPath: string, ctxSize?: number): Promise<'RED' | 'YELLOW' | 'GREEN' | 'GREY'> {
|
async isModelSupported(
|
||||||
|
modelPath: string,
|
||||||
|
ctxSize?: number
|
||||||
|
): Promise<'RED' | 'YELLOW' | 'GREEN' | 'GREY'> {
|
||||||
try {
|
try {
|
||||||
const engine = this.getEngine('llamacpp') as AIEngine & {
|
const engine = this.getEngine('llamacpp') as AIEngine & {
|
||||||
isModelSupported?: (
|
isModelSupported?: (
|
||||||
@ -448,4 +466,29 @@ export class DefaultModelsService implements ModelsService {
|
|||||||
return 'GREY' // Error state, assume not supported
|
return 'GREY' // Error state, assume not supported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateGgufFile(filePath: string): Promise<ModelValidationResult> {
|
||||||
|
try {
|
||||||
|
const engine = this.getEngine('llamacpp') as AIEngine & {
|
||||||
|
validateGgufFile?: (path: string) => Promise<ModelValidationResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engine && typeof engine.validateGgufFile === 'function') {
|
||||||
|
return await engine.validateGgufFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the specific method isn't available, we can fallback to a basic check
|
||||||
|
console.warn('validateGgufFile method not available in llamacpp engine')
|
||||||
|
return {
|
||||||
|
isValid: true, // Assume valid for now
|
||||||
|
error: 'Validation method not available',
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error validating GGUF file ${filePath}:`, error)
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -69,6 +69,18 @@ export interface HuggingFaceRepo {
|
|||||||
readme?: string
|
readme?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GgufMetadata {
|
||||||
|
version: number
|
||||||
|
tensor_count: number
|
||||||
|
metadata: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelValidationResult {
|
||||||
|
isValid: boolean
|
||||||
|
error?: string
|
||||||
|
metadata?: GgufMetadata
|
||||||
|
}
|
||||||
|
|
||||||
export interface ModelsService {
|
export interface ModelsService {
|
||||||
fetchModels(): Promise<modelInfo[]>
|
fetchModels(): Promise<modelInfo[]>
|
||||||
fetchModelCatalog(): Promise<ModelCatalog>
|
fetchModelCatalog(): Promise<ModelCatalog>
|
||||||
@ -104,4 +116,5 @@ export interface ModelsService {
|
|||||||
): Promise<{ exists: boolean; settingsUpdated: boolean }>
|
): Promise<{ exists: boolean; settingsUpdated: boolean }>
|
||||||
checkMmprojExists(modelId: string): Promise<boolean>
|
checkMmprojExists(modelId: string): Promise<boolean>
|
||||||
isModelSupported(modelPath: string, ctxSize?: number): Promise<'RED' | 'YELLOW' | 'GREEN' | 'GREY'>
|
isModelSupported(modelPath: string, ctxSize?: number): Promise<'RED' | 'YELLOW' | 'GREEN' | 'GREY'>
|
||||||
|
validateGgufFile(filePath: string): Promise<ModelValidationResult>
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user