Skip to content

Commit 5e30e10

Browse files
authored
Merge pull request #6388 from menloresearch/feat/import-vision-model
feat: allow user import model include mmproj file
2 parents cd85ae0 + a5b0ced commit 5e30e10

File tree

7 files changed

+825
-120
lines changed

7 files changed

+825
-120
lines changed

extensions/llamacpp-extension/src/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,22 @@ export default class llamacpp_extension extends AIEngine {
17291729
*/
17301730
async checkMmprojExists(modelId: string): Promise<boolean> {
17311731
try {
1732+
const modelConfigPath = await joinPath([
1733+
await this.getProviderPath(),
1734+
'models',
1735+
modelId,
1736+
'model.yml',
1737+
])
1738+
1739+
const modelConfig = await invoke<ModelConfig>('read_yaml', {
1740+
path: modelConfigPath,
1741+
})
1742+
1743+
// If mmproj_path is not defined in YAML, return false
1744+
if (modelConfig.mmproj_path) {
1745+
return true
1746+
}
1747+
17321748
const mmprojPath = await joinPath([
17331749
await this.getProviderPath(),
17341750
'models',
@@ -1983,4 +1999,47 @@ export default class llamacpp_extension extends AIEngine {
19831999
throw new Error(String(e))
19842000
}
19852001
}
2002+
2003+
/**
2004+
* Validate GGUF file and check for unsupported architectures like CLIP
2005+
*/
2006+
async validateGgufFile(filePath: string): Promise<{
2007+
isValid: boolean
2008+
error?: string
2009+
metadata?: GgufMetadata
2010+
}> {
2011+
try {
2012+
logger.info(`Validating GGUF file: ${filePath}`)
2013+
const metadata = await readGgufMetadata(filePath)
2014+
2015+
// Log full metadata for debugging
2016+
logger.info('Full GGUF metadata:', JSON.stringify(metadata, null, 2))
2017+
2018+
// Check if architecture is 'clip' which is not supported for text generation
2019+
const architecture = metadata.metadata?.['general.architecture']
2020+
logger.info(`Model architecture: ${architecture}`)
2021+
2022+
if (architecture === 'clip') {
2023+
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.'
2024+
logger.error('CLIP architecture detected:', architecture)
2025+
return {
2026+
isValid: false,
2027+
error: errorMessage,
2028+
metadata
2029+
}
2030+
}
2031+
2032+
logger.info('Model validation passed. Architecture:', architecture)
2033+
return {
2034+
isValid: true,
2035+
metadata
2036+
}
2037+
} catch (error) {
2038+
logger.error('Failed to validate GGUF file:', error)
2039+
return {
2040+
isValid: false,
2041+
error: `Failed to read model metadata: ${error instanceof Error ? error.message : 'Unknown error'}`
2042+
}
2043+
}
2044+
}
19862045
}

web-app/src/containers/ChatInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
105105
try {
106106
// Only check mmproj for llamacpp provider
107107
if (selectedProvider === 'llamacpp') {
108-
const hasLocalMmproj = await serviceHub.models().checkMmprojExists(selectedModel.id)
108+
const hasLocalMmproj = await serviceHub
109+
.models()
110+
.checkMmprojExists(selectedModel.id)
109111
setHasMmproj(hasLocalMmproj)
110112
}
111113
// For non-llamacpp providers, only check vision capability

web-app/src/containers/DropdownModelProvider.tsx

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ const DropdownModelProvider = ({
139139
[getProviderByName, updateProvider, serviceHub]
140140
)
141141

142-
// Initialize model provider only once
142+
// Initialize model provider - avoid race conditions with manual selections
143143
useEffect(() => {
144144
const initializeModel = async () => {
145145
// Auto select model when existing thread is passed
@@ -150,11 +150,13 @@ const DropdownModelProvider = ({
150150
}
151151
// Check mmproj existence for llamacpp models
152152
if (model?.provider === 'llamacpp') {
153-
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
154-
model.id as string,
155-
updateProvider,
156-
getProviderByName
157-
)
153+
await serviceHub
154+
.models()
155+
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
156+
model.id as string,
157+
updateProvider,
158+
getProviderByName
159+
)
158160
// Also check vision capability
159161
await checkAndUpdateModelVisionCapability(model.id as string)
160162
}
@@ -164,11 +166,13 @@ const DropdownModelProvider = ({
164166
if (lastUsed && checkModelExists(lastUsed.provider, lastUsed.model)) {
165167
selectModelProvider(lastUsed.provider, lastUsed.model)
166168
if (lastUsed.provider === 'llamacpp') {
167-
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
168-
lastUsed.model,
169-
updateProvider,
170-
getProviderByName
171-
)
169+
await serviceHub
170+
.models()
171+
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
172+
lastUsed.model,
173+
updateProvider,
174+
getProviderByName
175+
)
172176
// Also check vision capability
173177
await checkAndUpdateModelVisionCapability(lastUsed.model)
174178
}
@@ -186,19 +190,28 @@ const DropdownModelProvider = ({
186190
}
187191
selectModelProvider('', '')
188192
}
189-
} else if (PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] && !selectedModel) {
190-
// For web-only builds, always auto-select the first model from jan provider if none is selected
191-
const janProvider = providers.find(
192-
(p) => p.provider === 'jan' && p.active && p.models.length > 0
193-
)
194-
if (janProvider && janProvider.models.length > 0) {
195-
const firstModel = janProvider.models[0]
196-
selectModelProvider(janProvider.provider, firstModel.id)
193+
} else {
194+
// Get current state for web auto-selection check
195+
const currentState = { selectedModel, selectedProvider }
196+
if (
197+
PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] &&
198+
!currentState.selectedModel &&
199+
!currentState.selectedProvider
200+
) {
201+
// For web-only builds, auto-select the first model from jan provider only if nothing is selected
202+
const janProvider = providers.find(
203+
(p) => p.provider === 'jan' && p.active && p.models.length > 0
204+
)
205+
if (janProvider && janProvider.models.length > 0) {
206+
const firstModel = janProvider.models[0]
207+
selectModelProvider(janProvider.provider, firstModel.id)
208+
}
197209
}
198210
}
199211
}
200212

201213
initializeModel()
214+
// eslint-disable-next-line react-hooks/exhaustive-deps
202215
}, [
203216
model,
204217
selectModelProvider,
@@ -210,7 +223,7 @@ const DropdownModelProvider = ({
210223
getProviderByName,
211224
checkAndUpdateModelVisionCapability,
212225
serviceHub,
213-
selectedModel,
226+
// selectedModel and selectedProvider intentionally excluded to prevent race conditions
214227
])
215228

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

377390
// Check mmproj existence for llamacpp models
378391
if (searchableModel.provider.provider === 'llamacpp') {
379-
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
380-
searchableModel.model.id,
381-
updateProvider,
382-
getProviderByName
383-
)
392+
await serviceHub
393+
.models()
394+
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
395+
searchableModel.model.id,
396+
updateProvider,
397+
getProviderByName
398+
)
384399
// Also check vision capability
385400
await checkAndUpdateModelVisionCapability(searchableModel.model.id)
386401
}
@@ -572,7 +587,9 @@ const DropdownModelProvider = ({
572587
{getProviderTitle(providerInfo.provider)}
573588
</span>
574589
</div>
575-
{PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS] && (
590+
{PlatformFeatures[
591+
PlatformFeature.MODEL_PROVIDER_SETTINGS
592+
] && (
576593
<div
577594
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"
578595
onClick={(e) => {

0 commit comments

Comments
 (0)