Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions extensions/llamacpp-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,22 @@ export default class llamacpp_extension extends AIEngine {
*/
async checkMmprojExists(modelId: string): Promise<boolean> {
try {
const modelConfigPath = await joinPath([
await this.getProviderPath(),
'models',
modelId,
'model.yml',
])

const modelConfig = await invoke<ModelConfig>('read_yaml', {
path: modelConfigPath,
})

// If mmproj_path is not defined in YAML, return false
if (modelConfig.mmproj_path) {
return true
}

const mmprojPath = await joinPath([
await this.getProviderPath(),
'models',
Expand Down Expand Up @@ -1983,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'}`
}
}
}
}
4 changes: 3 additions & 1 deletion web-app/src/containers/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@
const servers = await serviceHub.mcp().getConnectedServers()
setConnectedServers(servers)
} catch (error) {
console.error('Failed to get connected servers:', error)
setConnectedServers([])
}

Check warning on line 90 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

88-90 lines are not covered with tests
}

checkConnectedServers()
Expand All @@ -105,22 +105,24 @@
try {
// Only check mmproj for llamacpp provider
if (selectedProvider === 'llamacpp') {
const hasLocalMmproj = await serviceHub.models().checkMmprojExists(selectedModel.id)
const hasLocalMmproj = await serviceHub
.models()
.checkMmprojExists(selectedModel.id)
setHasMmproj(hasLocalMmproj)
}
// For non-llamacpp providers, only check vision capability
else if (
selectedProvider !== 'llamacpp' &&
selectedModel?.capabilities?.includes('vision')
) {
setHasMmproj(true)
} else {
setHasMmproj(false)
}

Check warning on line 121 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

115-121 lines are not covered with tests
} catch (error) {
console.error('Error checking mmproj:', error)
setHasMmproj(false)
}

Check warning on line 125 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

123-125 lines are not covered with tests
}
}

Expand All @@ -136,8 +138,8 @@
return
}
if (!prompt.trim() && uploadedFiles.length === 0) {
return
}

Check warning on line 142 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

141-142 lines are not covered with tests
setMessage('')
sendMessage(
prompt,
Expand Down Expand Up @@ -178,8 +180,8 @@

useEffect(() => {
if (tooltipToolsAvailable && dropdownToolsAvailable) {
setTooltipToolsAvailable(false)
}

Check warning on line 184 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

183-184 lines are not covered with tests
}, [dropdownToolsAvailable, tooltipToolsAvailable])

// Focus when thread changes
Expand All @@ -201,39 +203,39 @@

const stopStreaming = useCallback(
(threadId: string) => {
abortControllers[threadId]?.abort()
cancelToolCall?.()
},

Check warning on line 208 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

206-208 lines are not covered with tests
[abortControllers, cancelToolCall]
)

const fileInputRef = useRef<HTMLInputElement>(null)

const handleAttachmentClick = () => {
fileInputRef.current?.click()
}

Check warning on line 216 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

215-216 lines are not covered with tests

const handleRemoveFile = (indexToRemove: number) => {
setUploadedFiles((prev) =>
prev.filter((_, index) => index !== indexToRemove)
)
}

Check warning on line 222 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

219-222 lines are not covered with tests

const getFileTypeFromExtension = (fileName: string): string => {
const extension = fileName.toLowerCase().split('.').pop()
switch (extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg'
case 'png':
return 'image/png'
default:
return ''
}
}

Check warning on line 235 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

225-235 lines are not covered with tests

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files

Check warning on line 238 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

238 line is not covered with tests

if (files && files.length > 0) {
const maxSize = 10 * 1024 * 1024 // 10MB in bytes
Expand Down
69 changes: 43 additions & 26 deletions web-app/src/containers/DropdownModelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const DropdownModelProvider = ({
[getProviderByName, updateProvider, serviceHub]
)

// Initialize model provider only once
// Initialize model provider - avoid race conditions with manual selections
useEffect(() => {
const initializeModel = async () => {
// Auto select model when existing thread is passed
Expand All @@ -150,11 +150,13 @@ const DropdownModelProvider = ({
}
// Check mmproj existence for llamacpp models
if (model?.provider === 'llamacpp') {
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
model.id as string,
updateProvider,
getProviderByName
)
await serviceHub
.models()
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
model.id as string,
updateProvider,
getProviderByName
)
// Also check vision capability
await checkAndUpdateModelVisionCapability(model.id as string)
}
Expand All @@ -164,11 +166,13 @@ const DropdownModelProvider = ({
if (lastUsed && checkModelExists(lastUsed.provider, lastUsed.model)) {
selectModelProvider(lastUsed.provider, lastUsed.model)
if (lastUsed.provider === 'llamacpp') {
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
lastUsed.model,
updateProvider,
getProviderByName
)
await serviceHub
.models()
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
lastUsed.model,
updateProvider,
getProviderByName
)
// Also check vision capability
await checkAndUpdateModelVisionCapability(lastUsed.model)
}
Expand All @@ -186,19 +190,28 @@ const DropdownModelProvider = ({
}
selectModelProvider('', '')
}
} else if (PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] && !selectedModel) {
// For web-only builds, always auto-select the first model from jan provider if none is selected
const janProvider = providers.find(
(p) => p.provider === 'jan' && p.active && p.models.length > 0
)
if (janProvider && janProvider.models.length > 0) {
const firstModel = janProvider.models[0]
selectModelProvider(janProvider.provider, firstModel.id)
} else {
// Get current state for web auto-selection check
const currentState = { selectedModel, selectedProvider }
if (
PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] &&
!currentState.selectedModel &&
!currentState.selectedProvider
) {
// For web-only builds, auto-select the first model from jan provider only if nothing is selected
const janProvider = providers.find(
(p) => p.provider === 'jan' && p.active && p.models.length > 0
)
if (janProvider && janProvider.models.length > 0) {
const firstModel = janProvider.models[0]
selectModelProvider(janProvider.provider, firstModel.id)
}
}
}
}

initializeModel()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
model,
selectModelProvider,
Expand All @@ -210,7 +223,7 @@ const DropdownModelProvider = ({
getProviderByName,
checkAndUpdateModelVisionCapability,
serviceHub,
selectedModel,
// selectedModel and selectedProvider intentionally excluded to prevent race conditions
])

// Update display model when selection changes
Expand Down Expand Up @@ -376,11 +389,13 @@ const DropdownModelProvider = ({

// Check mmproj existence for llamacpp models
if (searchableModel.provider.provider === 'llamacpp') {
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
searchableModel.model.id,
updateProvider,
getProviderByName
)
await serviceHub
.models()
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
searchableModel.model.id,
updateProvider,
getProviderByName
)
// Also check vision capability
await checkAndUpdateModelVisionCapability(searchableModel.model.id)
}
Expand Down Expand Up @@ -572,7 +587,9 @@ const DropdownModelProvider = ({
{getProviderTitle(providerInfo.provider)}
</span>
</div>
{PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS] && (
{PlatformFeatures[
PlatformFeature.MODEL_PROVIDER_SETTINGS
] && (
<div
className="size-6 cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
onClick={(e) => {
Expand Down
Loading
Loading